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:
Vim USDS 2022-08-04 00:29:22 -07:00
commit d4aed789cc
10 changed files with 399 additions and 154 deletions

View file

@ -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

View file

@ -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();
}); });
}); });

View file

@ -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)}>

View file

@ -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>
`;

View 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;

View file

@ -0,0 +1,3 @@
import MapTractLayers from './MapTractLayers';
export default MapTractLayers;

View 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;
//

View file

@ -0,0 +1,3 @@
import MapTribalLayer from './MapTribalLayer';
export default MapTribalLayer;

View file

@ -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;

View file

@ -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