mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-08-13 11:44:19 -07:00
Refactor Sources/Layers to allow for tribal switching
- Remove census tracts layers into it's own component - Create a tribal layer component - Update LayerSelector component tests - update OS map to react to layer selector -
This commit is contained in:
parent
70b9072559
commit
d4aed789cc
10 changed files with 399 additions and 154 deletions
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable valid-jsdoc */
|
/* eslint-disable valid-jsdoc */
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
// External Libs:
|
// External Libs:
|
||||||
import React, {useRef, useState, useMemo} from 'react';
|
import React, {useRef, useState} from 'react';
|
||||||
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
|
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
|
||||||
import ReactMapGL, {
|
import ReactMapGL, {
|
||||||
MapEvent,
|
MapEvent,
|
||||||
|
@ -12,7 +12,7 @@ import ReactMapGL, {
|
||||||
Popup,
|
Popup,
|
||||||
FlyToInterpolator,
|
FlyToInterpolator,
|
||||||
FullscreenControl,
|
FullscreenControl,
|
||||||
MapRef, Source, Layer} from 'react-map-gl';
|
MapRef} from 'react-map-gl';
|
||||||
import bbox from '@turf/bbox';
|
import bbox from '@turf/bbox';
|
||||||
import * as d3 from 'd3-ease';
|
import * as d3 from 'd3-ease';
|
||||||
import {isMobile} from 'react-device-detect';
|
import {isMobile} from 'react-device-detect';
|
||||||
|
@ -26,6 +26,7 @@ import {useFlags} from '../contexts/FlagContext';
|
||||||
import AreaDetail from './AreaDetail';
|
import AreaDetail from './AreaDetail';
|
||||||
import MapInfoPanel from './mapInfoPanel';
|
import MapInfoPanel from './mapInfoPanel';
|
||||||
import MapSearch from './MapSearch';
|
import MapSearch from './MapSearch';
|
||||||
|
import MapTractLayers from './MapTractLayers/MapTractLayers';
|
||||||
import LayerSelector from './LayerSelector';
|
import LayerSelector from './LayerSelector';
|
||||||
import TerritoryFocusControl from './territoryFocusControl';
|
import TerritoryFocusControl from './territoryFocusControl';
|
||||||
import {getOSBaseMap} from '../data/getOSBaseMap';
|
import {getOSBaseMap} from '../data/getOSBaseMap';
|
||||||
|
@ -34,9 +35,7 @@ import {getOSBaseMap} from '../data/getOSBaseMap';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
import * as constants from '../data/constants';
|
import * as constants from '../data/constants';
|
||||||
import * as styles from './J40Map.module.scss';
|
import * as styles from './J40Map.module.scss';
|
||||||
import * as COMMON_COPY from '../data/copy/common';
|
import MapTribalLayer from './MapTribalLayer/MapTribalLayer';
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
Cypress?: object;
|
Cypress?: object;
|
||||||
|
@ -56,49 +55,6 @@ export interface IDetailViewInterface {
|
||||||
properties: constants.J40Properties,
|
properties: constants.J40Properties,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This function will determine the URL for the map tiles. It will read in a string that will designate either
|
|
||||||
* high or low tiles. It will allow to overide the URL to the pipeline staging tile URL via feature flag.
|
|
||||||
* Lastly, it allows to set the tiles to be local or via the CDN as well.
|
|
||||||
*
|
|
||||||
* @param {string} tilesetName
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export const featureURLForTilesetName = (tilesetName: string): string => {
|
|
||||||
const flags = useFlags();
|
|
||||||
|
|
||||||
const pipelineStagingBaseURL = `https://justice40-data.s3.amazonaws.com/data-pipeline-staging`;
|
|
||||||
const XYZ_SUFFIX = '{z}/{x}/{y}.pbf';
|
|
||||||
|
|
||||||
if ('stage_hash' in flags) {
|
|
||||||
// Check if the stage_hash is valid
|
|
||||||
const regex = /^[0-9]{4}\/[a-f0-9]{40}$/;
|
|
||||||
if (!regex.test(flags['stage_hash'])) {
|
|
||||||
console.error(COMMON_COPY.CONSOLE_ERROR.STAGE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${pipelineStagingBaseURL}/${flags['stage_hash']}/data/score/tiles/${tilesetName}/${XYZ_SUFFIX}`;
|
|
||||||
} 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_DATA_PIPELINE_SCORE_PATH;
|
|
||||||
|
|
||||||
return [
|
|
||||||
featureTileBaseURL,
|
|
||||||
featureTilePath,
|
|
||||||
process.env.GATSBY_MAP_TILES_PATH,
|
|
||||||
tilesetName,
|
|
||||||
XYZ_SUFFIX,
|
|
||||||
].join('/');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const J40Map = ({location}: IJ40Interface) => {
|
const J40Map = ({location}: IJ40Interface) => {
|
||||||
/**
|
/**
|
||||||
|
@ -128,13 +84,13 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
|
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
|
||||||
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
|
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
|
||||||
const [isMobileMapState, setIsMobileMapState] = useState<boolean>(false);
|
const [isMobileMapState, setIsMobileMapState] = useState<boolean>(false);
|
||||||
|
const [censusSelected, setCensusSelected] = useState(true);
|
||||||
const {width: windowWidth} = useWindowSize();
|
const {width: windowWidth} = useWindowSize();
|
||||||
|
|
||||||
const mapRef = useRef<MapRef>(null);
|
const mapRef = useRef<MapRef>(null);
|
||||||
const flags = useFlags();
|
const flags = useFlags();
|
||||||
|
|
||||||
const selectedFeatureId = (selectedFeature && selectedFeature.id) || '';
|
const selectedFeatureId = (selectedFeature && selectedFeature.id) || '';
|
||||||
const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]);
|
|
||||||
|
|
||||||
const zoomLatLngHash = mapRef.current?.getMap()._hash._getCurrentHash();
|
const zoomLatLngHash = mapRef.current?.getMap()._hash._getCurrentHash();
|
||||||
|
|
||||||
|
@ -352,7 +308,7 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
|
|
||||||
|
|
||||||
{/* This will allow to select between the census tract layer and the tribal lands layer */}
|
{/* This will allow to select between the census tract layer and the tribal lands layer */}
|
||||||
<LayerSelector />
|
<LayerSelector censusSelected={censusSelected} setCensusSelected={setCensusSelected}/>
|
||||||
|
|
||||||
{/**
|
{/**
|
||||||
* The ReactMapGL component's props are grouped by the API's documentation. The component also has
|
* The ReactMapGL component's props are grouped by the API's documentation. The component also has
|
||||||
|
@ -368,7 +324,7 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
// ****** Map state props: ******
|
// ****** Map state props: ******
|
||||||
// http://visgl.github.io/react-map-gl/docs/api-reference/interactive-map#map-state
|
// http://visgl.github.io/react-map-gl/docs/api-reference/interactive-map#map-state
|
||||||
{...viewport}
|
{...viewport}
|
||||||
mapStyle={process.env.MAPBOX_STYLES_READ_TOKEN ? mapBoxBaseLayer : getOSBaseMap()}
|
mapStyle={process.env.MAPBOX_STYLES_READ_TOKEN ? mapBoxBaseLayer : getOSBaseMap(censusSelected)}
|
||||||
width="100%"
|
width="100%"
|
||||||
// Ajusting this height with a conditional statement will not render the map on staging.
|
// Ajusting this height with a conditional statement will not render the map on staging.
|
||||||
// The reason for this issue is unknown. Consider styling the parent container via SASS.
|
// The reason for this issue is unknown. Consider styling the parent container via SASS.
|
||||||
|
@ -382,7 +338,8 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
minZoom={constants.GLOBAL_MIN_ZOOM}
|
minZoom={constants.GLOBAL_MIN_ZOOM}
|
||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchRotate={false}
|
touchRotate={false}
|
||||||
interactiveLayerIds={[constants.HIGH_ZOOM_LAYER_ID, constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID]}
|
// eslint-disable-next-line max-len
|
||||||
|
interactiveLayerIds={censusSelected ? [constants.HIGH_ZOOM_LAYER_ID, constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID] : [constants.TRIBAL_LAYER_ID]}
|
||||||
|
|
||||||
|
|
||||||
// ****** Callback props: ******
|
// ****** Callback props: ******
|
||||||
|
@ -396,96 +353,13 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
data-cy={'reactMapGL'}
|
data-cy={'reactMapGL'}
|
||||||
>
|
>
|
||||||
{/**
|
|
||||||
* The low zoom source
|
|
||||||
*/}
|
|
||||||
<Source
|
|
||||||
id={constants.LOW_ZOOM_SOURCE_NAME}
|
|
||||||
type="vector"
|
|
||||||
promoteId={constants.GEOID_PROPERTY}
|
|
||||||
tiles={[featureURLForTilesetName('low')]}
|
|
||||||
maxzoom={constants.GLOBAL_MAX_ZOOM_LOW}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_LOW}
|
|
||||||
>
|
|
||||||
|
|
||||||
{/* Low zoom layer - prioritized features only */}
|
{/* Load either the Tribal layer or census layer */}
|
||||||
<Layer
|
{
|
||||||
id={constants.LOW_ZOOM_LAYER_ID}
|
censusSelected ?
|
||||||
source-layer={constants.SCORE_SOURCE_LAYER}
|
<MapTractLayers selectedFeature={selectedFeature} selectedFeatureId={selectedFeatureId}/> :
|
||||||
filter={['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD]}
|
<MapTribalLayer />
|
||||||
type='fill'
|
}
|
||||||
paint={{
|
|
||||||
'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
|
|
||||||
'fill-opacity': constants.LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY}}
|
|
||||||
maxzoom={constants.GLOBAL_MAX_ZOOM_LOW}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_LOW}
|
|
||||||
/>
|
|
||||||
</Source>
|
|
||||||
|
|
||||||
{/**
|
|
||||||
* The high zoom source
|
|
||||||
*/}
|
|
||||||
<Source
|
|
||||||
id={constants.HIGH_ZOOM_SOURCE_NAME}
|
|
||||||
type="vector"
|
|
||||||
promoteId={constants.GEOID_PROPERTY}
|
|
||||||
tiles={[featureURLForTilesetName('high')]}
|
|
||||||
maxzoom={constants.GLOBAL_MAX_ZOOM_HIGH}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
|
||||||
>
|
|
||||||
|
|
||||||
{/* High zoom layer - non-prioritized features only */}
|
|
||||||
<Layer
|
|
||||||
id={constants.HIGH_ZOOM_LAYER_ID}
|
|
||||||
source-layer={constants.SCORE_SOURCE_LAYER}
|
|
||||||
filter={['<', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD]}
|
|
||||||
type='fill'
|
|
||||||
paint={{
|
|
||||||
'fill-opacity': constants.NON_PRIORITIZED_FEATURE_FILL_OPACITY,
|
|
||||||
}}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* High zoom layer - prioritized features only */}
|
|
||||||
<Layer
|
|
||||||
id={constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID}
|
|
||||||
source-layer={constants.SCORE_SOURCE_LAYER}
|
|
||||||
filter={['>', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD]}
|
|
||||||
type='fill'
|
|
||||||
paint={{
|
|
||||||
'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
|
|
||||||
'fill-opacity': constants.HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY,
|
|
||||||
}}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* High zoom layer - controls the border between features */}
|
|
||||||
<Layer
|
|
||||||
id={constants.FEATURE_BORDER_LAYER_ID}
|
|
||||||
source-layer={constants.SCORE_SOURCE_LAYER}
|
|
||||||
type='line'
|
|
||||||
paint={{
|
|
||||||
'line-color': constants.FEATURE_BORDER_COLOR,
|
|
||||||
'line-width': constants.FEATURE_BORDER_WIDTH,
|
|
||||||
'line-opacity': constants.FEATURE_BORDER_OPACITY,
|
|
||||||
}}
|
|
||||||
maxzoom={constants.GLOBAL_MAX_ZOOM_FEATURE_BORDER}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_FEATURE_BORDER}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* High zoom layer - border styling around the selected feature */}
|
|
||||||
<Layer
|
|
||||||
id={constants.SELECTED_FEATURE_BORDER_LAYER_ID}
|
|
||||||
source-layer={constants.SCORE_SOURCE_LAYER}
|
|
||||||
filter={filter} // This filter filters out all other features except the selected feature.
|
|
||||||
type='line'
|
|
||||||
paint={{
|
|
||||||
'line-color': constants.SELECTED_FEATURE_BORDER_COLOR,
|
|
||||||
'line-width': constants.SELECTED_FEATURE_BORDER_WIDTH,
|
|
||||||
}}
|
|
||||||
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
|
||||||
/>
|
|
||||||
</Source>
|
|
||||||
|
|
||||||
{/* This will add the navigation controls of the zoom in and zoom out buttons */}
|
{/* This will add the navigation controls of the zoom in and zoom out buttons */}
|
||||||
{ windowWidth > constants.USWDS_BREAKPOINTS.MOBILE_LG && <NavigationControl
|
{ windowWidth > constants.USWDS_BREAKPOINTS.MOBILE_LG && <NavigationControl
|
||||||
|
|
|
@ -4,13 +4,21 @@ import {LocalizedComponent} from '../../test/testHelpers';
|
||||||
import LayerSelector from './LayerSelector';
|
import LayerSelector from './LayerSelector';
|
||||||
|
|
||||||
describe('rendering of the LayerSelector', () => {
|
describe('rendering of the LayerSelector', () => {
|
||||||
|
it('checks if component renders census tracts selected', () => {
|
||||||
const {asFragment} = render(
|
const {asFragment} = render(
|
||||||
<LocalizedComponent>
|
<LocalizedComponent>
|
||||||
<LayerSelector />
|
<LayerSelector censusSelected={true} setCensusSelected={() => {}}/>
|
||||||
</LocalizedComponent>,
|
</LocalizedComponent>,
|
||||||
);
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it('checks if component renders', () => {
|
it('checks if component renders tribal selected', () => {
|
||||||
|
const {asFragment} = render(
|
||||||
|
<LocalizedComponent>
|
||||||
|
<LayerSelector censusSelected={false} setCensusSelected={() => {}}/>
|
||||||
|
</LocalizedComponent>,
|
||||||
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState, Dispatch} from 'react';
|
||||||
import {useIntl} from 'gatsby-plugin-intl';
|
import {useIntl} from 'gatsby-plugin-intl';
|
||||||
import {Button, ButtonGroup} from '@trussworks/react-uswds';
|
import {Button, ButtonGroup} from '@trussworks/react-uswds';
|
||||||
import {useWindowSize} from 'react-use';
|
import {useWindowSize} from 'react-use';
|
||||||
|
@ -7,9 +7,13 @@ import {useWindowSize} from 'react-use';
|
||||||
import * as styles from './LayerSelector.module.scss';
|
import * as styles from './LayerSelector.module.scss';
|
||||||
import * as EXPLORE_COPY from '../../data/copy/explore';
|
import * as EXPLORE_COPY from '../../data/copy/explore';
|
||||||
|
|
||||||
const LayerSelector = () => {
|
interface ILayerSelector {
|
||||||
|
censusSelected: boolean,
|
||||||
|
setCensusSelected: Dispatch<boolean>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayerSelector = ({censusSelected, setCensusSelected}:ILayerSelector) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [censusSelected, setCensusSelected] = useState(true);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* At compile-time, the width/height returned by useWindowSize will be X. When the client requests the
|
* At compile-time, the width/height returned by useWindowSize will be X. When the client requests the
|
||||||
|
@ -45,6 +49,7 @@ const LayerSelector = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layerSelectorContainer}>
|
<div className={styles.layerSelectorContainer}>
|
||||||
|
{/* // Todo: set i18n here */}
|
||||||
<label htmlFor="layer-group">Select layer</label>
|
<label htmlFor="layer-group">Select layer</label>
|
||||||
<ButtonGroup id="layer-group" type="segmented">
|
<ButtonGroup id="layer-group" type="segmented">
|
||||||
<Button id="census" type="button" outline={!censusSelected} onClick={(e) => buttonClickHandler(e)}>
|
<Button id="census" type="button" outline={!censusSelected} onClick={(e) => buttonClickHandler(e)}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`rendering of the LayerSelector checks if component renders 1`] = `
|
exports[`rendering of the LayerSelector checks if component renders census tracts selected 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
@ -40,3 +40,44 @@ exports[`rendering of the LayerSelector checks if component renders 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`rendering of the LayerSelector checks if component renders tribal selected 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="layer-group"
|
||||||
|
>
|
||||||
|
Select layer
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
class="usa-button-group usa-button-group--segmented"
|
||||||
|
id="layer-group"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="usa-button-group__item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="usa-button usa-button--outline"
|
||||||
|
data-testid="button"
|
||||||
|
id="census"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Census Tracts
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="usa-button-group__item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="usa-button"
|
||||||
|
data-testid="button"
|
||||||
|
id="tribal"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Tribal Lands
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
155
client/src/components/MapTractLayers/MapTractLayers.tsx
Normal file
155
client/src/components/MapTractLayers/MapTractLayers.tsx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import {Source, Layer} from 'react-map-gl';
|
||||||
|
|
||||||
|
// Contexts:
|
||||||
|
import {useFlags} from '../../contexts/FlagContext';
|
||||||
|
|
||||||
|
import * as constants from '../../data/constants';
|
||||||
|
import * as COMMON_COPY from '../../data/copy/common';
|
||||||
|
|
||||||
|
// Todo: Update with real types if this works:
|
||||||
|
interface IMapTractLayers {
|
||||||
|
selectedFeatureId: any,
|
||||||
|
selectedFeature: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will determine the URL for the map tiles. It will read in a string that will designate either
|
||||||
|
* high or low tiles. It will allow to overide the URL to the pipeline staging tile URL via feature flag.
|
||||||
|
* Lastly, it allows to set the tiles to be local or via the CDN as well.
|
||||||
|
*
|
||||||
|
* @param {string} tilesetName
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const featureURLForTilesetName = (tilesetName: string): string => {
|
||||||
|
const flags = useFlags();
|
||||||
|
|
||||||
|
const pipelineStagingBaseURL = `https://justice40-data.s3.amazonaws.com/data-pipeline-staging`;
|
||||||
|
const XYZ_SUFFIX = '{z}/{x}/{y}.pbf';
|
||||||
|
|
||||||
|
if ('stage_hash' in flags) {
|
||||||
|
// Check if the stage_hash is valid
|
||||||
|
const regex = /^[0-9]{4}\/[a-f0-9]{40}$/;
|
||||||
|
if (!regex.test(flags['stage_hash'])) {
|
||||||
|
console.error(COMMON_COPY.CONSOLE_ERROR.STAGE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${pipelineStagingBaseURL}/${flags['stage_hash']}/data/score/tiles/${tilesetName}/${XYZ_SUFFIX}`;
|
||||||
|
} 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_DATA_PIPELINE_SCORE_PATH;
|
||||||
|
|
||||||
|
return [
|
||||||
|
featureTileBaseURL,
|
||||||
|
featureTilePath,
|
||||||
|
process.env.GATSBY_MAP_TILES_PATH,
|
||||||
|
tilesetName,
|
||||||
|
XYZ_SUFFIX,
|
||||||
|
].join('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapTractLayers = ({selectedFeatureId, selectedFeature}: IMapTractLayers) => {
|
||||||
|
const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Source
|
||||||
|
id={constants.LOW_ZOOM_SOURCE_NAME}
|
||||||
|
type="vector"
|
||||||
|
promoteId={constants.GEOID_PROPERTY}
|
||||||
|
tiles={[featureURLForTilesetName('low')]}
|
||||||
|
maxzoom={constants.GLOBAL_MAX_ZOOM_LOW}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_LOW}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Low zoom layer - prioritized features only */}
|
||||||
|
<Layer
|
||||||
|
id={constants.LOW_ZOOM_LAYER_ID}
|
||||||
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
||||||
|
filter={['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD]}
|
||||||
|
type='fill'
|
||||||
|
paint={{
|
||||||
|
'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
|
||||||
|
'fill-opacity': constants.LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY}}
|
||||||
|
maxzoom={constants.GLOBAL_MAX_ZOOM_LOW}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_LOW}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
|
||||||
|
{/**
|
||||||
|
* The high zoom source
|
||||||
|
*/}
|
||||||
|
<Source
|
||||||
|
id={constants.HIGH_ZOOM_SOURCE_NAME}
|
||||||
|
type="vector"
|
||||||
|
promoteId={constants.GEOID_PROPERTY}
|
||||||
|
tiles={[featureURLForTilesetName('high')]}
|
||||||
|
maxzoom={constants.GLOBAL_MAX_ZOOM_HIGH}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* High zoom layer - non-prioritized features only */}
|
||||||
|
<Layer
|
||||||
|
id={constants.HIGH_ZOOM_LAYER_ID}
|
||||||
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
||||||
|
filter={['<', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD]}
|
||||||
|
type='fill'
|
||||||
|
paint={{
|
||||||
|
'fill-opacity': constants.NON_PRIORITIZED_FEATURE_FILL_OPACITY,
|
||||||
|
}}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* High zoom layer - prioritized features only */}
|
||||||
|
<Layer
|
||||||
|
id={constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID}
|
||||||
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
||||||
|
filter={['>', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD]}
|
||||||
|
type='fill'
|
||||||
|
paint={{
|
||||||
|
'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
|
||||||
|
'fill-opacity': constants.HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY,
|
||||||
|
}}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* High zoom layer - controls the border between features */}
|
||||||
|
<Layer
|
||||||
|
id={constants.FEATURE_BORDER_LAYER_ID}
|
||||||
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
||||||
|
type='line'
|
||||||
|
paint={{
|
||||||
|
'line-color': constants.FEATURE_BORDER_COLOR,
|
||||||
|
'line-width': constants.FEATURE_BORDER_WIDTH,
|
||||||
|
'line-opacity': constants.FEATURE_BORDER_OPACITY,
|
||||||
|
}}
|
||||||
|
maxzoom={constants.GLOBAL_MAX_ZOOM_FEATURE_BORDER}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_FEATURE_BORDER}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* High zoom layer - border styling around the selected feature */}
|
||||||
|
<Layer
|
||||||
|
id={constants.SELECTED_FEATURE_BORDER_LAYER_ID}
|
||||||
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
||||||
|
filter={filter} // This filter filters out all other features except the selected feature.
|
||||||
|
type='line'
|
||||||
|
paint={{
|
||||||
|
'line-color': constants.SELECTED_FEATURE_BORDER_COLOR,
|
||||||
|
'line-width': constants.SELECTED_FEATURE_BORDER_WIDTH,
|
||||||
|
}}
|
||||||
|
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapTractLayers;
|
3
client/src/components/MapTractLayers/index.tsx
Normal file
3
client/src/components/MapTractLayers/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import MapTractLayers from './MapTractLayers';
|
||||||
|
|
||||||
|
export default MapTractLayers;
|
75
client/src/components/MapTribalLayer/MapTribalLayer.tsx
Normal file
75
client/src/components/MapTribalLayer/MapTribalLayer.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Source, Layer} from 'react-map-gl';
|
||||||
|
|
||||||
|
import * as constants from '../../data/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will determine the URL for the tribal tiles.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const tribalURL = (): string => {
|
||||||
|
const XYZ_SUFFIX = '{z}/{x}/{y}.pbf';
|
||||||
|
|
||||||
|
return [
|
||||||
|
process.env.GATSBY_CDN_TILES_BASE_URL,
|
||||||
|
process.env.GATSBY_DATA_PIPELINE_TRIBAL_PATH,
|
||||||
|
process.env.GATSBY_MAP_TILES_PATH,
|
||||||
|
XYZ_SUFFIX,
|
||||||
|
].join('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapTribalLayer = () => {
|
||||||
|
return (
|
||||||
|
<Source
|
||||||
|
id={constants.TRIBAL_SOURCE_NAME}
|
||||||
|
type="vector"
|
||||||
|
promoteId={constants.TRIBAL_ID}
|
||||||
|
tiles={[tribalURL()]}
|
||||||
|
minzoom={constants.TRIBAL_MIN_ZOOM}
|
||||||
|
maxzoom={constants.TRIBAL_MAX_ZOOM}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Low zoom layer - prioritized features only */}
|
||||||
|
<Layer
|
||||||
|
id={constants.TRIBAL_LAYER_ID}
|
||||||
|
source-layer={constants.TRIBAL_SOURCE_LAYER}
|
||||||
|
// filter={['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD]}
|
||||||
|
type='fill'
|
||||||
|
paint={{
|
||||||
|
'fill-color': constants.TRIBAL_FILL_COLOR,
|
||||||
|
'fill-opacity': constants.TRIBAL_FEATURE_FILL_OPACITY}}
|
||||||
|
minzoom={constants.TRIBAL_MIN_ZOOM}
|
||||||
|
maxzoom={constants.TRIBAL_MAX_ZOOM}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tribal layer - controls the border between features */}
|
||||||
|
<Layer
|
||||||
|
id={constants.FEATURE_BORDER_LAYER_ID}
|
||||||
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
||||||
|
type='line'
|
||||||
|
paint={{
|
||||||
|
'line-color': constants.TRIBAL_BORDER_COLOR,
|
||||||
|
'line-width': constants.FEATURE_BORDER_WIDTH,
|
||||||
|
'line-opacity': constants.FEATURE_BORDER_OPACITY,
|
||||||
|
}}
|
||||||
|
minzoom={constants.TRIBAL_MIN_ZOOM}
|
||||||
|
maxzoom={constants.TRIBAL_MAX_ZOOM}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tribal layer - border styling around the selected feature */}
|
||||||
|
<Layer
|
||||||
|
id={constants.SELECTED_FEATURE_BORDER_LAYER_ID}
|
||||||
|
source-layer={constants.TRIBAL_SOURCE_NAME}
|
||||||
|
type='line'
|
||||||
|
paint={{
|
||||||
|
'line-color': constants.SELECTED_FEATURE_BORDER_COLOR,
|
||||||
|
'line-width': constants.SELECTED_FEATURE_BORDER_WIDTH,
|
||||||
|
}}
|
||||||
|
minzoom={constants.TRIBAL_MIN_ZOOM}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapTribalLayer;
|
||||||
|
//
|
3
client/src/components/MapTribalLayer/index.tsx
Normal file
3
client/src/components/MapTribalLayer/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import MapTribalLayer from './MapTribalLayer';
|
||||||
|
|
||||||
|
export default MapTribalLayer;
|
|
@ -25,6 +25,9 @@ export type J40Properties = { [key: string]: any };
|
||||||
|
|
||||||
// ****** SIDE PANEL BACKEND SIGNALS ***********
|
// ****** SIDE PANEL BACKEND SIGNALS ***********
|
||||||
|
|
||||||
|
// Tribal signals
|
||||||
|
export const TRIBAL_ID = 'tribalId';
|
||||||
|
|
||||||
// Set the threshold percentile used by most indicators in the side panel
|
// Set the threshold percentile used by most indicators in the side panel
|
||||||
export const DEFAULT_THRESHOLD_PERCENTILE = 90;
|
export const DEFAULT_THRESHOLD_PERCENTILE = 90;
|
||||||
|
|
||||||
|
@ -187,15 +190,18 @@ export const ISLAND_AREA_LOW_HS_EDU = 'IALHE';
|
||||||
export const BASE_MAP_SOURCE_NAME = 'base-map-source-name';
|
export const BASE_MAP_SOURCE_NAME = 'base-map-source-name';
|
||||||
export const HIGH_ZOOM_SOURCE_NAME = 'high-zoom-source-name';
|
export const HIGH_ZOOM_SOURCE_NAME = 'high-zoom-source-name';
|
||||||
export const LOW_ZOOM_SOURCE_NAME = 'low-zoom-source-name';
|
export const LOW_ZOOM_SOURCE_NAME = 'low-zoom-source-name';
|
||||||
|
export const TRIBAL_SOURCE_NAME = 'tribal-source-name';
|
||||||
|
|
||||||
// Layer ID constants
|
// Layer ID constants
|
||||||
export const SCORE_SOURCE_LAYER = 'blocks'; // The name of the layer within the tiles that contains the score
|
export const SCORE_SOURCE_LAYER = 'blocks'; // The name of the layer within the tiles that contains the score
|
||||||
|
export const TRIBAL_SOURCE_LAYER = 'blocks';
|
||||||
export const BASE_MAP_LAYER_ID = 'base-map-layer-id';
|
export const BASE_MAP_LAYER_ID = 'base-map-layer-id';
|
||||||
export const HIGH_ZOOM_LAYER_ID = 'high-zoom-layer-id';
|
export const HIGH_ZOOM_LAYER_ID = 'high-zoom-layer-id';
|
||||||
export const PRIORITIZED_HIGH_ZOOM_LAYER_ID = 'prioritized-high-zoom-layer-id';
|
export const PRIORITIZED_HIGH_ZOOM_LAYER_ID = 'prioritized-high-zoom-layer-id';
|
||||||
export const LOW_ZOOM_LAYER_ID = 'low-zoom-layer-id';
|
export const LOW_ZOOM_LAYER_ID = 'low-zoom-layer-id';
|
||||||
export const FEATURE_BORDER_LAYER_ID = 'feature-border-layer-id';
|
export const FEATURE_BORDER_LAYER_ID = 'feature-border-layer-id';
|
||||||
export const SELECTED_FEATURE_BORDER_LAYER_ID = 'selected-feature-border-layer-id';
|
export const SELECTED_FEATURE_BORDER_LAYER_ID = 'selected-feature-border-layer-id';
|
||||||
|
export const TRIBAL_LAYER_ID = 'tribal-layer-id';
|
||||||
|
|
||||||
// Used in layer filters:
|
// Used in layer filters:
|
||||||
export const SCORE_PROPERTY_LOW = 'M_SCORE';
|
export const SCORE_PROPERTY_LOW = 'M_SCORE';
|
||||||
|
@ -213,18 +219,26 @@ export const GLOBAL_MAX_ZOOM_HIGH = 11;
|
||||||
|
|
||||||
export const GLOBAL_MIN_ZOOM_FEATURE_BORDER = 5;
|
export const GLOBAL_MIN_ZOOM_FEATURE_BORDER = 5;
|
||||||
export const GLOBAL_MAX_ZOOM_FEATURE_BORDER = 22;
|
export const GLOBAL_MAX_ZOOM_FEATURE_BORDER = 22;
|
||||||
|
export const TRIBAL_MIN_ZOOM = 3;
|
||||||
|
export const TRIBAL_MAX_ZOOM = 22;
|
||||||
|
|
||||||
// Opacity
|
// Opacity
|
||||||
export const FEATURE_BORDER_OPACITY = 0.5;
|
export const FEATURE_BORDER_OPACITY = 0.5;
|
||||||
export const HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.3;
|
export const HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.3;
|
||||||
export const LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.6;
|
export const LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.6;
|
||||||
export const NON_PRIORITIZED_FEATURE_FILL_OPACITY = 0;
|
export const NON_PRIORITIZED_FEATURE_FILL_OPACITY = 0;
|
||||||
|
export const TRIBAL_FEATURE_FILL_OPACITY = 0.3;
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
export const FEATURE_BORDER_COLOR = '#4EA5CF';
|
export const FEATURE_BORDER_COLOR = '#4EA5CF';
|
||||||
export const SELECTED_FEATURE_BORDER_COLOR = '#1A4480';
|
export const SELECTED_FEATURE_BORDER_COLOR = '#1A4480';
|
||||||
export const PRIORITIZED_FEATURE_FILL_COLOR = '#768FB3';
|
export const PRIORITIZED_FEATURE_FILL_COLOR = '#768FB3';
|
||||||
|
|
||||||
|
export const TRIBAL_BORDER_COLOR = '#0000FF';
|
||||||
|
export const SELECTED_TRIBAL_BORDER_COLOR = '#FF0000';
|
||||||
|
export const TRIBAL_FILL_COLOR = '#00FF00';
|
||||||
|
|
||||||
|
|
||||||
// Widths
|
// Widths
|
||||||
export const FEATURE_BORDER_WIDTH = 0.8;
|
export const FEATURE_BORDER_WIDTH = 0.8;
|
||||||
export const SELECTED_FEATURE_BORDER_WIDTH = 5.0;
|
export const SELECTED_FEATURE_BORDER_WIDTH = 5.0;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {Style} from 'maplibre-gl';
|
import {Style} from 'maplibre-gl';
|
||||||
import * as constants from '../data/constants';
|
import * as constants from '../data/constants';
|
||||||
import {featureURLForTilesetName} from '../components/J40Map';
|
import {featureURLForTilesetName} from '../components/MapTractLayers/MapTractLayers';
|
||||||
|
import {tribalURL} from '../components/MapTribalLayer/MapTribalLayer';
|
||||||
|
|
||||||
// *********** BASE MAP SOURCES ***************
|
// *********** BASE MAP SOURCES ***************
|
||||||
const imageSuffix = constants.isMobile ? '' : '@2x';
|
const imageSuffix = constants.isMobile ? '' : '@2x';
|
||||||
|
@ -25,8 +26,12 @@ const cartoLightBaseLayer = {
|
||||||
|
|
||||||
// Utility function to get OpenSource base maps that are in accordance to JSON spec of MapBox
|
// Utility function to get OpenSource base maps that are in accordance to JSON spec of MapBox
|
||||||
// https://docs.mapbox.com/mapbox-gl-js/style-spec/
|
// https://docs.mapbox.com/mapbox-gl-js/style-spec/
|
||||||
export const getOSBaseMap = () : Style => {
|
export const getOSBaseMap = (censusSelected: boolean) : Style => {
|
||||||
return {
|
return !censusSelected ? {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tribal Source
|
||||||
|
*/
|
||||||
'version': 8,
|
'version': 8,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,6 +49,68 @@ export const getOSBaseMap = () : Style => {
|
||||||
'maxzoom': constants.GLOBAL_MAX_ZOOM,
|
'maxzoom': constants.GLOBAL_MAX_ZOOM,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tribal source
|
||||||
|
*/
|
||||||
|
[constants.TRIBAL_SOURCE_NAME]: {
|
||||||
|
'type': 'vector',
|
||||||
|
'promoteId': constants.TRIBAL_ID,
|
||||||
|
'tiles': [tribalURL()],
|
||||||
|
'minzoom': constants.TRIBAL_MIN_ZOOM,
|
||||||
|
'maxzoom': constants.TRIBAL_MAX_ZOOM,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tribal Layers
|
||||||
|
*/
|
||||||
|
'layers': [
|
||||||
|
|
||||||
|
// The baseMapLayer
|
||||||
|
{
|
||||||
|
'id': constants.BASE_MAP_LAYER_ID,
|
||||||
|
'source': constants.BASE_MAP_SOURCE_NAME,
|
||||||
|
'type': 'raster',
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tribal layer
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
'id': constants.TRIBAL_LAYER_ID,
|
||||||
|
'source': constants.TRIBAL_SOURCE_NAME,
|
||||||
|
'source-layer': constants.TRIBAL_SOURCE_LAYER,
|
||||||
|
'type': 'fill',
|
||||||
|
'paint': {
|
||||||
|
'fill-color': constants.TRIBAL_FILL_COLOR,
|
||||||
|
'fill-opacity': constants.TRIBAL_FEATURE_FILL_OPACITY,
|
||||||
|
},
|
||||||
|
'minzoom': constants.TRIBAL_MIN_ZOOM,
|
||||||
|
'maxzoom': constants.TRIBAL_MAX_ZOOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} :
|
||||||
|
{
|
||||||
|
'version': 8,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Census Tract Source
|
||||||
|
* */
|
||||||
|
'sources': {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base map source source allows us to define where the tiles can be fetched from.
|
||||||
|
*/
|
||||||
|
[constants.BASE_MAP_SOURCE_NAME]: {
|
||||||
|
'type': 'raster',
|
||||||
|
'tiles': cartoLightBaseLayer.noLabels,
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM,
|
||||||
|
},
|
||||||
|
|
||||||
// The High zoom source:
|
// The High zoom source:
|
||||||
[constants.HIGH_ZOOM_SOURCE_NAME]: {
|
[constants.HIGH_ZOOM_SOURCE_NAME]: {
|
||||||
// It is only shown at high zoom levels to avoid performance issues at lower zooms
|
// It is only shown at high zoom levels to avoid performance issues at lower zooms
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue