mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-08-22 14:51:40 -07:00
Allow for Census Tract search in UI
This commit is contained in:
parent
4130c46aee
commit
cf4e35acce
15 changed files with 362 additions and 162 deletions
261
client/package-lock.json
generated
261
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -85,6 +85,7 @@
|
|||
"gatsby-plugin-env-variables": "^2.2.0",
|
||||
"gatsby-plugin-robots-txt": "^1.7.0",
|
||||
"gatsby-plugin-sitemap": "^4.10.0",
|
||||
"js-search": "^2.0.1",
|
||||
"mapbox-gl": "^1.13.2",
|
||||
"maplibre-gl": "^1.14.0",
|
||||
"query-string": "^7.1.3",
|
||||
|
|
|
@ -130,7 +130,7 @@ const J40Map = ({location}: IJ40Interface) => {
|
|||
const onClick = (event: MapEvent | React.MouseEvent<HTMLButtonElement>) => {
|
||||
// Stop all propagation / bubbling / capturing
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
(event as React.MouseEvent<HTMLButtonElement>).stopPropagation?.();
|
||||
|
||||
// Check if the click is for territories. Given the territories component's design, it can be
|
||||
// guaranteed that each territory control will have an id. We use this ID to determine
|
||||
|
@ -167,8 +167,9 @@ const J40Map = ({location}: IJ40Interface) => {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// This else clause will fire when the ID is null or empty. This is the case where the map is clicked
|
||||
} else if (event.target && (event.target as HTMLElement).nodeName == 'DIV' ) {
|
||||
// This else clause will fire when the user clicks on the map and will ignore other controls
|
||||
// such as the search box and buttons.
|
||||
|
||||
// @ts-ignore
|
||||
const feature = event.features && event.features[0];
|
||||
|
|
|
@ -4,6 +4,8 @@ import {LngLatBoundsLike} from 'maplibre-gl';
|
|||
import {useIntl} from 'gatsby-plugin-intl';
|
||||
import {Search} from '@trussworks/react-uswds';
|
||||
import {useWindowSize} from 'react-use';
|
||||
import * as JsSearch from 'js-search';
|
||||
import * as constants from '../../data/constants';
|
||||
|
||||
import MapSearchMessage from '../MapSearchMessage';
|
||||
|
||||
|
@ -14,6 +16,16 @@ interface IMapSearch {
|
|||
goToPlace(bounds: LngLatBoundsLike):void;
|
||||
}
|
||||
|
||||
interface ISearchResult {
|
||||
addresstype: string;
|
||||
lat: string;
|
||||
lon: string;
|
||||
boundingbox: string[];
|
||||
type: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
place_rank: number;
|
||||
}
|
||||
|
||||
const MapSearch = ({goToPlace}:IMapSearch) => {
|
||||
// State to hold if the search results are empty or not:
|
||||
const [isSearchResultsNull, setIsSearchResultsNull] = useState(false);
|
||||
|
@ -30,44 +42,118 @@ const MapSearch = ({goToPlace}:IMapSearch) => {
|
|||
*/
|
||||
const {width, height} = useWindowSize();
|
||||
const [placeholderText, setPlaceholderText]= useState(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER);
|
||||
const [tractSearch, setTractSearch] = useState<JsSearch | null>(null);
|
||||
|
||||
/**
|
||||
* Gets the tract search data and loads in the state.
|
||||
*/
|
||||
const getTractSearchData = async () => {
|
||||
const searchDataUrl = `${constants.TILE_BASE_URL}/${constants.MAP_TRACT_SEARCH_PATH}`;
|
||||
fetch(searchDataUrl)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(`${response.statusText} error with status code of ${response.status}`);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
// We use JsSearch to make it easy to load and quick to search.
|
||||
const search = new JsSearch.Search('GEOID10');
|
||||
search.indexStrategy = new JsSearch.ExactWordIndexStrategy();
|
||||
search.addIndex('GEOID10');
|
||||
search.addDocuments(data);
|
||||
setTractSearch(search);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error('Unable to read search tract table:', error));
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
width > height ? setPlaceholderText(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER): setPlaceholderText(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER_MOBILE);
|
||||
}, [width]);
|
||||
|
||||
useEffect(()=>{
|
||||
getTractSearchData();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Searchs for a given Census tract ID.
|
||||
* @param {string} tract the 11 digit tract ID as a string
|
||||
* @return {Array} an array of one search result, or null if no result found
|
||||
*/
|
||||
const searchForTract = (tract: string): [ISearchResult] | [] => {
|
||||
// We create a bounding box just to get the tract in the view box.
|
||||
// The size is not important.
|
||||
const BOUNDING_BOX_SIZE_DD = 0.2;
|
||||
if (tractSearch) {
|
||||
// Convert 10 digit tracts to 11.
|
||||
const searchTerm = tract.length == 10 ? '0' + tract : tract;
|
||||
const result = tractSearch.search(searchTerm);
|
||||
if (result.length > 0) {
|
||||
const lat = Number(result[0].INTPTLAT10);
|
||||
const lon = Number(result[0].INTPTLON10);
|
||||
return [{
|
||||
addresstype: 'tract',
|
||||
boundingbox: [
|
||||
(lat - (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||
(lat + (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||
(lon - (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||
(lon + (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||
],
|
||||
lat: result[0].INTPTLAT10,
|
||||
lon: result[0].INTPTLON10,
|
||||
type: 'tract',
|
||||
place_rank: 1,
|
||||
}];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
/*
|
||||
onSearchHandler will
|
||||
1. extract the search term from the input field
|
||||
2. fetch data from the API and return the results as JSON and results to US only
|
||||
3. if data is valid, destructure the boundingBox values from the search results
|
||||
4. pan the map to that location
|
||||
2. Determine if the search term is a Census Tract or not.
|
||||
3. If it is a Census Tract, it will search the tract table for a bounding box.
|
||||
4. If it is NOT a Census Tract, it will fetch data from the API and return the
|
||||
results as JSON and results to US only. If data is valid, destructure the
|
||||
boundingBox values from the search results.
|
||||
4. Pan the map to that location
|
||||
*/
|
||||
const onSearchHandler = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const searchTerm = (event.currentTarget.elements.namedItem('search') as HTMLInputElement).value;
|
||||
let searchResults = null;
|
||||
|
||||
const searchResults = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${searchTerm}&format=json&countrycodes=us`,
|
||||
{
|
||||
mode: 'cors',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not OK');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
});
|
||||
|
||||
// If the search term a Census tract
|
||||
const isTract = /^\d{10,11}$/.test(searchTerm);
|
||||
if (isTract) {
|
||||
setIsSearchResultsNull(false);
|
||||
searchResults = searchForTract(searchTerm);
|
||||
} else {
|
||||
searchResults = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${searchTerm}&format=json&countrycodes=us`,
|
||||
{
|
||||
mode: 'cors',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not OK');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
});
|
||||
console.log('Nominatum search results: ', searchResults);
|
||||
}
|
||||
|
||||
// If results are valid, set isSearchResultsNull to false and pan map to location:
|
||||
if (searchResults && searchResults.length > 0) {
|
||||
setIsSearchResultsNull(false);
|
||||
console.log('Nominatum search results: ', searchResults);
|
||||
|
||||
const [latMin, latMax, longMin, longMax] = searchResults[0].boundingbox;
|
||||
goToPlace([[Number(longMin), Number(latMin)], [Number(longMax), Number(latMax)]]);
|
||||
} else {
|
||||
|
|
|
@ -23,7 +23,7 @@ exports[`rendering of the MapSearch checks if component renders 1`] = `
|
|||
data-testid="textInput"
|
||||
id="search-field"
|
||||
name="search"
|
||||
placeholder="Search for an address, city, state or ZIP"
|
||||
placeholder="Search for an address, city, state, ZIP or Census Tract"
|
||||
type="search"
|
||||
/>
|
||||
<button
|
||||
|
|
|
@ -38,13 +38,8 @@ export const featureURLForTilesetName = (tilesetName: string): string => {
|
|||
} else {
|
||||
// The feature tile base URL and path can either point locally or the CDN.
|
||||
// This is selected based on the DATA_SOURCE env variable.
|
||||
const featureTileBaseURL = process.env.DATA_SOURCE === 'local' ?
|
||||
process.env.GATSBY_LOCAL_TILES_BASE_URL :
|
||||
process.env.GATSBY_CDN_TILES_BASE_URL;
|
||||
|
||||
const featureTilePath = process.env.DATA_SOURCE === 'local' ?
|
||||
process.env.GATSBY_DATA_PIPELINE_SCORE_PATH_LOCAL :
|
||||
process.env.GATSBY_2_0_SCORE_PATH;
|
||||
const featureTileBaseURL = constants.TILE_BASE_URL;
|
||||
const featureTilePath = constants.TILE_PATH;
|
||||
|
||||
return [
|
||||
featureTileBaseURL,
|
||||
|
|
|
@ -380,3 +380,13 @@ export const CENSUS_TRACT_SURVEY_LINKS = {
|
|||
EN: "https://eop.gov1.qualtrics.com/jfe/form/SV_8J5wGa8Ya4dMP9c",
|
||||
ES: "https://eop.gov1.qualtrics.com/jfe/form/SV_eJXos5X4yekq6cC",
|
||||
};
|
||||
|
||||
export const TILE_BASE_URL = process.env.DATA_SOURCE === "local" ?
|
||||
process.env.GATSBY_LOCAL_TILES_BASE_URL :
|
||||
process.env.GATSBY_CDN_TILES_BASE_URL;
|
||||
|
||||
export const TILE_PATH = process.env.DATA_SOURCE === "local" ?
|
||||
process.env.GATSBY_DATA_PIPELINE_SCORE_PATH_LOCAL :
|
||||
process.env.GATSBY_1_0_SCORE_PATH;
|
||||
|
||||
export const MAP_TRACT_SEARCH_PATH = "data_pipeline/data/score/search/tracts.json";
|
||||
|
|
|
@ -92,7 +92,7 @@ export const MAP = defineMessages({
|
|||
},
|
||||
SEARCH_PLACEHOLDER: {
|
||||
id: 'explore.map.page.map.search.placeholder.text',
|
||||
defaultMessage: 'Search for an address, city, state or ZIP',
|
||||
defaultMessage: 'Search for an address, city, state, ZIP or Census Tract',
|
||||
description: 'On the explore the map page, on the map, the placeholder text for search',
|
||||
},
|
||||
SEARCH_PLACEHOLDER_MOBILE: {
|
||||
|
|
|
@ -688,7 +688,7 @@
|
|||
"description": "On the explore the map page, on the map, the placeholder text for search"
|
||||
},
|
||||
"explore.map.page.map.search.placeholder.text": {
|
||||
"defaultMessage": "Search for an address, city, state or ZIP",
|
||||
"defaultMessage": "Search for an address, city, state, ZIP or Census Tract",
|
||||
"description": "On the explore the map page, on the map, the placeholder text for search"
|
||||
},
|
||||
"explore.map.page.map.search.results.empty.text": {
|
||||
|
|
|
@ -172,7 +172,7 @@
|
|||
"explore.map.page.map.layer.selector.tribal.long": "Tierras tribales",
|
||||
"explore.map.page.map.layer.selector.tribal.short": "Tribal",
|
||||
"explore.map.page.map.search.placeholder.mobile.text": "Búsqueda de ubicaciones ",
|
||||
"explore.map.page.map.search.placeholder.text": "Busque una dirección, ciudad, estado o código postal.",
|
||||
"explore.map.page.map.search.placeholder.text": "Busque una dirección, ciudad, estado, código postal o distrito censal.",
|
||||
"explore.map.page.map.search.results.empty.text": "No se encontró la ubicación o ubicación desconocida. Intente una búsqueda distinta.",
|
||||
"explore.map.page.map.territoryFocus.alaska.long": "Alaska",
|
||||
"explore.map.page.map.territoryFocus.alaska.short": "AK",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue