Merge branch 'vimusds/1692-tribal-layer-toggle' into vimusds/release/frontend-narwhal-2

This commit is contained in:
Vim USDS 2022-08-17 10:34:32 -07:00
commit 90a4415cd6
22 changed files with 1040 additions and 277 deletions

View file

@ -8,6 +8,7 @@ GATSBY_LOCAL_TILES_BASE_URL=http://localhost:5000/data/data-pipeline
GATSBY_DATA_PIPELINE_SCORE_PATH_LOCAL=data_pipeline/data/score GATSBY_DATA_PIPELINE_SCORE_PATH_LOCAL=data_pipeline/data/score
GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score
GATSBY_DATA_PIPELINE_TRIBAL_PATH=data-pipeline/data/tribal
GATSBY_FILE_DL_PATH_SCREENING_TOOL_DATA_ZIP=downloadable/Screening_Tool_Data.zip GATSBY_FILE_DL_PATH_SCREENING_TOOL_DATA_ZIP=downloadable/Screening_Tool_Data.zip
GATSBY_FILE_DL_PATH_SHAPE_FILE_ZIP=shapefile/usa.zip GATSBY_FILE_DL_PATH_SHAPE_FILE_ZIP=shapefile/usa.zip
@ -22,4 +23,4 @@ GATSBY_MAP_TILES_PATH=tiles
# If you want the map to render a MapBox base map (as opposed to the # If you want the map to render a MapBox base map (as opposed to the
# open source one from CartoDB), please create your own API TOKEN from # open source one from CartoDB), please create your own API TOKEN from
# your MapBox account and add the token here: # your MapBox account and add the token here:
# MAPBOX_STYLES_READ_TOKEN='' MAPBOX_STYLES_READ_TOKEN=''

View file

@ -6,6 +6,7 @@
GATSBY_CDN_TILES_BASE_URL=https://static-data-screeningtool.geoplatform.gov GATSBY_CDN_TILES_BASE_URL=https://static-data-screeningtool.geoplatform.gov
GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score
GATSBY_DATA_PIPELINE_TRIBAL_PATH=data-pipeline/data/tribal
GATSBY_FILE_DL_PATH_SCREENING_TOOL_DATA_ZIP=downloadable/Screening_Tool_Data.zip GATSBY_FILE_DL_PATH_SCREENING_TOOL_DATA_ZIP=downloadable/Screening_Tool_Data.zip
GATSBY_FILE_DL_PATH_SHAPE_FILE_ZIP=shapefile/usa.zip GATSBY_FILE_DL_PATH_SHAPE_FILE_ZIP=shapefile/usa.zip

View file

@ -24,6 +24,7 @@ import mailIcon from '/node_modules/uswds/dist/img/usa-icons/mail_outline.svg';
interface IAreaDetailProps { interface IAreaDetailProps {
properties: constants.J40Properties, properties: constants.J40Properties,
hash: string[], hash: string[],
isCensusLayerSelected: boolean,
} }
/** /**
@ -62,7 +63,8 @@ export interface ICategory {
isExceed1MoreBurden: boolean | null, isExceed1MoreBurden: boolean | null,
isExceedBothSocioBurdens: boolean | null, isExceedBothSocioBurdens: boolean | null,
} }
const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
const AreaDetail = ({properties, hash, isCensusLayerSelected}: IAreaDetailProps) => {
const intl = useIntl(); const intl = useIntl();
// console.log the properties of the census that is selected: // console.log the properties of the census that is selected:
@ -74,6 +76,7 @@ const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
const countyName = properties[constants.COUNTY_NAME] ? properties[constants.COUNTY_NAME] : "N/A"; const countyName = properties[constants.COUNTY_NAME] ? properties[constants.COUNTY_NAME] : "N/A";
const stateName = properties[constants.STATE_NAME] ? properties[constants.STATE_NAME] : "N/A"; const stateName = properties[constants.STATE_NAME] ? properties[constants.STATE_NAME] : "N/A";
const sidePanelState = properties[constants.SIDE_PANEL_STATE]; const sidePanelState = properties[constants.SIDE_PANEL_STATE];
const landAreaName = properties[constants.LAND_AREA_NAME];
const isCommunityFocus = score >= constants.SCORE_BOUNDARY_THRESHOLD; const isCommunityFocus = score >= constants.SCORE_BOUNDARY_THRESHOLD;
@ -590,6 +593,41 @@ const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
{/* Demographics */} {/* Demographics */}
<TractDemographics /> <TractDemographics />
{
isCensusLayerSelected ? (
<>
{/* Census Info */}
<ul className={styles.censusRow}>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.CENSUS_BLOCK_GROUP)}
</span>
<span className={styles.censusText}>{` ${blockGroup}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.COUNTY)}
</span>
<span className={styles.censusText}>{` ${countyName}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{properties[constants.SIDE_PANEL_STATE] !== constants.SIDE_PANEL_STATE_VALUES.NATION ?
intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.TERRITORY) :
intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.STATE)
}
</span>
<span className={styles.censusText}>{` ${stateName}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.POPULATION)}
</span>
<span className={styles.censusText}>{` ${population.toLocaleString()}`}</span>
</li>
</ul>
{/* Disadvantaged? */} {/* Disadvantaged? */}
<div className={styles.categorization}> <div className={styles.categorization}>
@ -644,11 +682,23 @@ const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
</div> </div>
</Button> </Button>
</a> </a>
</div> </div>
</>
) : (
<ul className={styles.censusRow}>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_TRIBAL_INFO.LAND_AREA_NAME)}
</span>
<span className={styles.censusText}>{` ${landAreaName}`}</span>
</li>
</ul>
)
}
{/* All category accordions in this component */} {/* All category accordions in this component */}
<Accordion multiselectable={true} items={categoryItems} /> {isCensusLayerSelected && <Accordion multiselectable={true} items={categoryItems} />}
{/* Methodology version */} {/* Methodology version */}
<div className={styles.versionInfo}> <div className={styles.versionInfo}>

View file

@ -5,6 +5,7 @@ import {LocalizedComponent} from '../../../test/testHelpers';
import * as constants from '../../../data/constants'; import * as constants from '../../../data/constants';
// Todo: Update tests to take into account tribal layer selected
describe('rendering of the AreaDetail', () => { describe('rendering of the AreaDetail', () => {
const properties = { const properties = {
[constants.POVERTY_BELOW_100_PERCENTILE]: .12, [constants.POVERTY_BELOW_100_PERCENTILE]: .12,
@ -27,7 +28,7 @@ describe('rendering of the AreaDetail', () => {
it('checks if indicators for NATION is present', () => { it('checks if indicators for NATION is present', () => {
const {asFragment} = render( const {asFragment} = render(
<LocalizedComponent> <LocalizedComponent>
<AreaDetail properties={properties} hash={hash}/> <AreaDetail properties={properties} hash={hash} isCensusLayerSelected={true}/>
</LocalizedComponent>, </LocalizedComponent>,
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
@ -41,7 +42,7 @@ describe('rendering of the AreaDetail', () => {
const {asFragment} = render( const {asFragment} = render(
<LocalizedComponent> <LocalizedComponent>
<AreaDetail properties={propertiesPR} hash={hash}/> <AreaDetail properties={propertiesPR} hash={hash} isCensusLayerSelected={true}/>
</LocalizedComponent>, </LocalizedComponent>,
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
@ -59,7 +60,7 @@ describe('rendering of the AreaDetail', () => {
const {asFragment} = render( const {asFragment} = render(
<LocalizedComponent> <LocalizedComponent>
<AreaDetail properties={propertiesIA} hash={hash}/> <AreaDetail properties={propertiesIA} hash={hash} isCensusLayerSelected={true}/>
</LocalizedComponent>, </LocalizedComponent>,
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();

View file

@ -59,3 +59,13 @@
overflow-y: auto; overflow-y: auto;
height: 90vh; height: 90vh;
} }
// This will control the height of the map when the device
// width is less than desktop (1024px)
.j40Map {
@include at-media-max("desktop") {
height: 55vh;
@include u-margin-top(7); // Allow for tribal toggle to appear above map on screens < 1024
}
}

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 {useIntl} from 'gatsby-plugin-intl'; import {useIntl} from 'gatsby-plugin-intl';
import bbox from '@turf/bbox'; import bbox from '@turf/bbox';
import * as d3 from 'd3-ease'; import * as d3 from 'd3-ease';
@ -27,6 +27,9 @@ 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 MapTribalLayer from './MapTribalLayers/MapTribalLayers';
import LayerSelector from './LayerSelector';
import TerritoryFocusControl from './territoryFocusControl'; import TerritoryFocusControl from './territoryFocusControl';
import {getOSBaseMap} from '../data/getOSBaseMap'; import {getOSBaseMap} from '../data/getOSBaseMap';
@ -34,10 +37,8 @@ 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 * as EXPLORE_COPY from '../data/copy/explore'; import * as EXPLORE_COPY from '../data/copy/explore';
declare global { declare global {
interface Window { interface Window {
Cypress?: object; Cypress?: object;
@ -57,55 +58,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('/');
}
};
/**
* This the main map component
*
* @param {IJ40Interface} location
* @returns {ReactElement}
*/
const J40Map = ({location}: IJ40Interface) => { const J40Map = ({location}: IJ40Interface) => {
/** /**
* Initializes the zoom, and the map's center point (lat, lng) via the URL hash #{z}/{lat}/{long} * Initializes the zoom, and the map's center point (lat, lng) via the URL hash #{z}/{lat}/{long}
@ -134,6 +86,11 @@ 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<boolean>(true);
// In order to detect that the layer has been toggled (between census and tribal),
// this state variable will hold that information
const [layerToggled, setLayerToggled] = useState<boolean>(false);
const {width: windowWidth} = useWindowSize(); const {width: windowWidth} = useWindowSize();
/** /**
@ -153,7 +110,6 @@ const J40Map = ({location}: IJ40Interface) => {
const intl = useIntl(); const intl = useIntl();
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();
@ -219,13 +175,30 @@ const J40Map = ({location}: IJ40Interface) => {
} }
} else { } else {
// This else clause will fire when the ID is null or empty. This is the case where the map is clicked // This else clause will fire when the ID is null or empty. This is the case where the map is clicked
setLayerToggled(false);
// @ts-ignore // @ts-ignore
const feature = event.features && event.features[0]; const feature = event.features && event.features[0];
/**
* Given that Alaska has Points as their data type, we will not zoom into them when
* selected. In order to detect if a feature is a Point we will use Regex to determine
* the Alaska Point based on it's unique ID:
*
* E.g. {33FF6457-324C-4643-94E8-D543DD4339E0}
*
* The regex will test for any numeric, upper-case alpha with hyphens string enclosed
* in curly braces.
*/
const alaskaIDRegex = /\{[0-9,A-Z,-]+\}/g;
const isFeatureAlaskaPoint = alaskaIDRegex.test(feature.id);
if (feature) { if (feature) {
// Get the current selected feature's bounding box: // Get the current selected feature's bounding box:
const [minLng, minLat, maxLng, maxLat] = bbox(feature); const [minLng, minLat, maxLng, maxLat] = bbox(feature);
// Set the selectedFeature ID // Set the selectedFeature ID
if (feature.id !== selectedFeatureId) { if (feature.id !== selectedFeatureId) {
setSelectedFeature(feature); setSelectedFeature(feature);
@ -233,8 +206,9 @@ const J40Map = ({location}: IJ40Interface) => {
setSelectedFeature(undefined); setSelectedFeature(undefined);
} }
// Go to the newly selected feature
goToPlace([ // Go to the newly selected feature (as long as it's not an Alaska Point)
!isFeatureAlaskaPoint && goToPlace([
[minLng, minLat], [minLng, minLat],
[maxLng, maxLat], [maxLng, maxLat],
]); ]);
@ -376,6 +350,13 @@ const J40Map = ({location}: IJ40Interface) => {
* Any component declarations outside the <ReactMapGL> component may be susceptible to this bug. * Any component declarations outside the <ReactMapGL> component may be susceptible to this bug.
*/} */}
{/* This will allow to select between the census tract layer and the tribal lands layer */}
<LayerSelector
censusSelected={censusSelected}
setCensusSelected={setCensusSelected}
setLayerToggled={setLayerToggled}
/>
{/** {/**
* 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
* some children. * some children.
@ -390,7 +371,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.
@ -404,7 +385,16 @@ 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,
constants.TRIBAL_ALASKA_POINTS_LAYER_ID,
]
}
// ****** Callback props: ****** // ****** Callback props: ******
@ -418,98 +408,19 @@ const J40Map = ({location}: IJ40Interface) => {
ref={mapRef} ref={mapRef}
data-cy={'reactMapGL'} data-cy={'reactMapGL'}
> >
{/**
* Load all data sources and layers
*
* First the low zoom:
*/}
<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 depending on the censusSelected state variable */}
<Layer {
id={constants.LOW_ZOOM_LAYER_ID} censusSelected ?
source-layer={constants.SCORE_SOURCE_LAYER} <MapTractLayers
filter={['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD]} selectedFeature={selectedFeature}
type='fill' selectedFeatureId={selectedFeatureId}
paint={{ /> :
'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR, <MapTribalLayer
'fill-opacity': constants.LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY}} selectedFeature={selectedFeature}
maxzoom={constants.GLOBAL_MAX_ZOOM_LOW} selectedFeatureId={selectedFeatureId}
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 is the first overlayed row on the map: Search and Geolocation */} {/* This is the first overlayed row on the map: Search and Geolocation */}
<div className={styles.mapHeaderRow}> <div className={styles.mapHeaderRow}>
@ -562,7 +473,11 @@ const J40Map = ({location}: IJ40Interface) => {
onClose={setDetailViewData} onClose={setDetailViewData}
captureScroll={true} captureScroll={true}
> >
<AreaDetail properties={detailViewData.properties} hash={zoomLatLngHash}/> <AreaDetail
properties={detailViewData.properties}
hash={zoomLatLngHash}
isCensusLayerSelected={censusSelected}
/>
</Popup> </Popup>
)} )}
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' } {'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
@ -576,6 +491,8 @@ const J40Map = ({location}: IJ40Interface) => {
featureProperties={detailViewData?.properties} featureProperties={detailViewData?.properties}
selectedFeatureId={selectedFeature?.id} selectedFeatureId={selectedFeature?.id}
hash={zoomLatLngHash} hash={zoomLatLngHash}
isCensusLayerSelected={censusSelected}
layerToggled={layerToggled}
/> />
</Grid> </Grid>
</> </>

View file

@ -0,0 +1,30 @@
@use '../../styles/design-system.scss' as *;
.layerSelectorContainer {
background-color: white;
@include u-padding-left(1);
@include u-padding-right(1);
@include u-padding-top(1);
@include u-padding-bottom(1);
width: fit-content;
z-index: 1;
// styles for mobile-lg (480px) and greater widths,
@include at-media('mobile-lg') {
position: absolute;
top: units(2.5);
left: 62%;
}
// styles for less than mobile-lg (480px)
position: absolute;
top: -5.2rem;
}

View file

@ -0,0 +1,12 @@
declare namespace LayerSelectorNamespace {
export interface ILayerSelectorScss {
layerSelectorContainer: string;
}
}
declare const LayerSelectorScssModule: LayerSelectorNamespace.ILayerSelectorScss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: LayerSelectorNamespace.ILayerSelectorScss;
};
export = LayerSelectorScssModule;

View file

@ -0,0 +1,24 @@
import * as React from 'react';
import {render} from '@testing-library/react';
import {LocalizedComponent} from '../../test/testHelpers';
import LayerSelector from './LayerSelector';
describe('rendering of the LayerSelector', () => {
it('checks if component renders census tracts selected', () => {
const {asFragment} = render(
<LocalizedComponent>
<LayerSelector censusSelected={true} setCensusSelected={() => {}} setLayerToggled={() =>{}}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();
});
it('checks if component renders tribal selected', () => {
const {asFragment} = render(
<LocalizedComponent>
<LayerSelector censusSelected={false} setCensusSelected={() => {}} setLayerToggled={()=> {}}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,74 @@
import React, {useEffect, useState, Dispatch} from 'react';
import {useIntl} from 'gatsby-plugin-intl';
import {Button, ButtonGroup} from '@trussworks/react-uswds';
import {useWindowSize} from 'react-use';
import * as styles from './LayerSelector.module.scss';
import * as EXPLORE_COPY from '../../data/copy/explore';
interface ILayerSelector {
censusSelected: boolean,
setCensusSelected: Dispatch<boolean>,
setLayerToggled: Dispatch<boolean>,
}
const LayerSelector = ({censusSelected, setCensusSelected, setLayerToggled}:ILayerSelector) => {
const intl = useIntl();
/**
* At compile-time, the width/height returned by useWindowSize will be X. When the client requests the
* app on run-time from CDN, and the app hydrates, reconcilation no longer occurs and the client is forced
* to use X.
*
* To avoid this, we set the text as a state variable. We also create a useEffect that updates
* that state whenenver the width changes.
*
*/
const {width, height} = useWindowSize();
const [censusText, setCensusText]= useState(EXPLORE_COPY.MAP.CENSUS_TRACT_LONG);
const [tribalText, setTribalText]= useState(EXPLORE_COPY.MAP.TRIBAL_LANDS_LONG);
useEffect( () => {
if (width > height) {
setCensusText(EXPLORE_COPY.MAP.CENSUS_TRACT_LONG);
setTribalText(EXPLORE_COPY.MAP.TRIBAL_LANDS_LONG);
} else {
setCensusText(EXPLORE_COPY.MAP.CENSUS_TRACT_SHORT);
setTribalText(EXPLORE_COPY.MAP.TRIBAL_LANDS_SHORT);
}
}, [width]);
// Anytime the censusSelected state variable changes, set the LayerToggled state
// variable
useEffect( () => {
setLayerToggled(true);
}, [censusSelected]);
// Handles toggle of tracts and tribal layer selection
const buttonClickHandler = (event) => {
if (event.target.id === 'census' && !censusSelected) {
setCensusSelected(true);
} else if (event.target.id === 'tribal' && censusSelected) {
setCensusSelected(false);
}
};
return (
<div className={styles.layerSelectorContainer}>
{/* // Todo: set i18n here */}
<label htmlFor="layer-group">Select layer</label>
<ButtonGroup id="layer-group" type="segmented">
<Button id="census" type="button" outline={!censusSelected} onClick={(e) => buttonClickHandler(e)}>
{intl.formatMessage(censusText)}
</Button>
<Button id="tribal" type="button" outline={censusSelected} onClick={(e) => buttonClickHandler(e)}>
{intl.formatMessage(tribalText)}
</Button>
</ButtonGroup>
</div>
);
};
export default LayerSelector;

View file

@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering of the LayerSelector checks if component renders census tracts 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"
data-testid="button"
id="census"
type="button"
>
Census Tracts
</button>
</li>
<li
class="usa-button-group__item"
>
<button
class="usa-button usa-button--outline"
data-testid="button"
id="tribal"
type="button"
>
Tribal Lands
</button>
</li>
</ul>
</div>
</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,3 @@
import LayerSelector from './LayerSelector';
export default LayerSelector;

View file

@ -0,0 +1,187 @@
import React, {useMemo} from 'react';
import {Source, Layer} from 'react-map-gl';
import {AnyLayer} from 'mapbox-gl';
// Contexts:
import {useFlags} from '../../contexts/FlagContext';
import * as constants from '../../data/constants';
import * as COMMON_COPY from '../../data/copy/common';
interface IMapTractLayers {
selectedFeatureId: AnyLayer,
selectedFeature: AnyLayer,
}
/**
* 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('/');
}
};
/**
* This component will return the appropriate source and layers for the census layer on the
* map.
*
* There are two use cases here, eg, when the MapBox token is or isn't provided. When the token
* is not provided, the open-source map will be rendered. When the open-source map is rendered
* only the interactive layers are returned from this component. The reason being is that the
* other layers are supplied by he getOSBaseMap function.
*
* @param {AnyLayer} selectedFeatureId
* @param {AnyLayer} selectedFeature
* @return {Style}
*/
const MapTractLayers = ({
selectedFeatureId,
selectedFeature,
}: IMapTractLayers) => {
const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]);
return process.env.MAPBOX_STYLES_READ_TOKEN ? (
// In this case the MapBox token is found and All source(s)/layer(s) are returned.
<>
<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 (static) - 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 (static) - 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 (static) - 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 (static) - 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}
/>
</Source>
</>
): (
/**
* In this case the MapBox token is NOT found and ONLY interactive source(s)/layer(s) are returned
* In this case, the other layers (non-interactive) are provided by getOSBaseMap
*/
<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 (dynamic) - 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,144 @@
import React, {useMemo} from 'react';
import {Source, Layer} from 'react-map-gl';
import {AnyLayer} from 'mapbox-gl';
import * as constants from '../../data/constants';
interface IMapTribalLayers {
selectedFeatureId: AnyLayer,
selectedFeature: AnyLayer,
}
/**
* 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('/');
};
/**
* This component will return the appropriate source and layers for the tribal layer on the
* map.
*
* There are two use cases here, eg, when the MapBox token is or isn't provided. When the token
* is not provided, the open-source map will be rendered. When the open-source map is rendered
* only the interactive layers are returned from this component. The reason being is that the
* other layers are supplied by he getOSBaseMap function.
*
* @param {AnyLayer} selectedFeatureId
* @param {AnyLayer} selectedFeature
* @return {Style}
*/
const MapTribalLayer = ({
selectedFeatureId,
selectedFeature,
}: IMapTribalLayers) => {
const tribalSelectionFilter = useMemo(() => ['in', constants.TRIBAL_ID, selectedFeatureId], [selectedFeature]);
return process.env.MAPBOX_STYLES_READ_TOKEN ? (
// In this case the MapBox token is found and ALL source(s)/layer(s) are returned.
<Source
id={constants.TRIBAL_SOURCE_NAME}
type="vector"
promoteId={constants.TRIBAL_ID}
tiles={[tribalURL()]}
minzoom={constants.TRIBAL_MIN_ZOOM}
maxzoom={constants.TRIBAL_MAX_ZOOM}
>
{/* Tribal layer */}
<Layer
id={constants.TRIBAL_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_LAYER}
type='fill'
paint={{
'fill-color': constants.PRIORITIZED_FEATURE_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.TRIBAL_SOURCE_LAYER}
type='line'
paint={{
'line-color': constants.FEATURE_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_TRIBAL_FEATURE_BORDER_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_LAYER}
filter={tribalSelectionFilter}
type='line'
paint={{
'line-color': constants.SELECTED_FEATURE_BORDER_COLOR,
'line-width': constants.SELECTED_FEATURE_BORDER_WIDTH,
}}
minzoom={constants.TRIBAL_MIN_ZOOM}
/>
{/* Alaska layer */}
{/* // Todo: limit zoom in amount */}
<Layer
id={constants.TRIBAL_ALASKA_POINTS_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_LAYER}
filter={['==', ['geometry-type'], 'Point']}
type='circle'
paint={{
'circle-radius': constants.TRIBAL_ALASKA_CIRCLE_RADIUS,
'circle-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
}}
minzoom={constants.TRIBAL_MIN_ZOOM}
maxzoom={constants.TRIBAL_MAX_ZOOM}
/>
</Source>
) : (
/**
* In this case the MapBox token is NOT found and ONLY INTERACTIVE source(s)/layer(s) are returned.
* In this case, the other layers (non-interactive) are provided by getOSBaseMap
*/
<Source
id={constants.TRIBAL_SOURCE_NAME}
type="vector"
promoteId={constants.TRIBAL_ID}
tiles={[tribalURL()]}
minzoom={constants.TRIBAL_MIN_ZOOM}
maxzoom={constants.TRIBAL_MAX_ZOOM}
>
{/* Tribal layer - border styling around the selected feature */}
<Layer
id={constants.SELECTED_TRIBAL_FEATURE_BORDER_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_LAYER}
filter={tribalSelectionFilter}
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 MapTribalLayers from './MapTribalLayers';
export default MapTribalLayers;

View file

@ -7,13 +7,33 @@ interface IMapInfoPanelProps {
featureProperties: { [key:string]: string | number } | undefined, featureProperties: { [key:string]: string | number } | undefined,
selectedFeatureId: string | number | undefined selectedFeatureId: string | number | undefined
hash: string[], hash: string[],
isCensusLayerSelected: boolean,
layerToggled: boolean, // indicates if census layer or tribal layer has been toggled
} }
const MapInfoPanel = ({className, featureProperties, selectedFeatureId, hash}:IMapInfoPanelProps) => { const MapInfoPanel = ({
className,
featureProperties,
selectedFeatureId,
hash,
isCensusLayerSelected,
layerToggled,
}:IMapInfoPanelProps) => {
return ( return (
<div className={className} > <div className={className} >
{(featureProperties && selectedFeatureId ) ? {/* The tertiary conditional statement below will control the side panel state. Currently
<AreaDetail properties={featureProperties} hash={hash}/> : there are two states, namely showing the AreaDetail or SidePanelInfo. When a feature
is selected, on - for example - the census tract layer, and if the Tribal Layer is the selected
the Side Panel should revert back to the SidePanelInfo.
A new boolean called layerToggle captures that a layer has been selected and to render
the SidePanelInfo component */}
{(featureProperties && selectedFeatureId && !layerToggled) ?
<AreaDetail
properties={featureProperties}
hash={hash}
isCensusLayerSelected={isCensusLayerSelected}
/> :
<SidePanelInfo /> <SidePanelInfo />
} }
</div> </div>

View file

@ -25,6 +25,10 @@ export type J40Properties = { [key: string]: any };
// ****** SIDE PANEL BACKEND SIGNALS *********** // ****** SIDE PANEL BACKEND SIGNALS ***********
// Tribal signals
export const TRIBAL_ID = 'tribalId';
export const LAND_AREA_NAME = 'landAreaName';
// 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 +191,20 @@ 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';
export const SELECTED_TRIBAL_FEATURE_BORDER_LAYER_ID = 'selected-feature-tribal-border-layer-id';
export const TRIBAL_ALASKA_POINTS_LAYER_ID = 'tribal-alaska-points-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,21 +222,31 @@ 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 = '##4EA5CF';
export const SELECTED_TRIBAL_BORDER_COLOR = '#1A4480';
export const TRIBAL_FILL_COLOR = '#768FB3';
export const TRIBAL_ALASKA_CIRCLE_FILL_COLOR = '#768FB3';
export const TRIBAL_ALASKA_CIRCLE_RADIUS = 5;
// 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;
export const ALAKSA_POINTS_STROKE_WIDTH = 1.0;
// Bounds - these bounds can be obtained by using the getCurrentMapBoundingBox() function in the map // Bounds - these bounds can be obtained by using the getCurrentMapBoundingBox() function in the map
export const GLOBAL_MAX_BOUNDS: LngLatBoundsLike = [ export const GLOBAL_MAX_BOUNDS: LngLatBoundsLike = [

View file

@ -184,6 +184,26 @@ export const MAP = defineMessages({
// defaultMessage: 'Geolocation locked', // defaultMessage: 'Geolocation locked',
// description: 'On the explore the map page, on the map, this is the message above the gelocation icon that geolocation is locked.', // description: 'On the explore the map page, on the map, this is the message above the gelocation icon that geolocation is locked.',
// }, // },
CENSUS_TRACT_LONG: {
id: 'explore.map.page.map.layer.selector.tracts.long',
defaultMessage: 'Census Tracts',
description: 'On the explore the map page, on the map, the full name indicating Census Tracts',
},
CENSUS_TRACT_SHORT: {
id: 'explore.map.page.map.layer.selector.tracts.short',
defaultMessage: 'Tracts',
description: 'On the explore the map page, on the map, the short name indicating Census Tracts',
},
TRIBAL_LANDS_LONG: {
id: 'explore.map.page.map.layer.selector.tribal.long',
defaultMessage: 'Tribal Lands',
description: 'On the explore the map page, on the map, the full name indicating Tribal Lands',
},
TRIBAL_LANDS_SHORT: {
id: 'explore.map.page.map.layer.selector.tracts.short',
defaultMessage: 'Tribal',
description: 'On the explore the map page, on the map, the short name indicating Tribal Lands',
},
}); });
@ -311,6 +331,15 @@ export const SIDE_PANEL_CBG_INFO = defineMessages({
}, },
}); });
export const SIDE_PANEL_TRIBAL_INFO = defineMessages({
LAND_AREA_NAME: {
id: 'explore.map.page.side.panel.tribalInfo.landAreaName',
defaultMessage: 'Land Area Name:',
description: `Navigate to the explore the map page. Click on Tribal Lands, when the map is in view,
click on the map. The side panel will show the land area name of the feature selected`,
},
});
export const COMMUNITY = { export const COMMUNITY = {
OF_FOCUS: <FormattedMessage OF_FOCUS: <FormattedMessage
id={'explore.map.page.side.panel.community.of.focus'} id={'explore.map.page.side.panel.community.of.focus'}

View file

@ -1,8 +1,11 @@
import {Style} from 'maplibre-gl'; import {Style} from 'maplibre-gl';
import * as constants from '../data/constants';
import {featureURLForTilesetName} from '../components/J40Map';
// *********** BASE MAP SOURCES *************** import {featureURLForTilesetName} from '../components/MapTractLayers/MapTractLayers';
import {tribalURL} from '../components/MapTribalLayers/MapTribalLayers';
import * as constants from '../data/constants';
// *********** OPEN SOURCE BASE MAP CONSTANTS ***************
const imageSuffix = constants.isMobile ? '' : '@2x'; const imageSuffix = constants.isMobile ? '' : '@2x';
// Original "light" Base layer // Original "light" Base layer
@ -23,10 +26,26 @@ const cartoLightBaseLayer = {
}; };
// Utility function to get OpenSource base maps that are in accordance to JSON spec of MapBox // *********** OPEN SOURCE STATIC MAP STYLES ***************
// https://docs.mapbox.com/mapbox-gl-js/style-spec/ /**
export const getOSBaseMap = () : Style => { * This function will be called when there is no MapBox token found. This function will
return { * return the open source base map along with styles for the chosen source. We currently
* have two sources, either the census tracts or the tribal layer.
* *
* This function returns a Style in accordance to JSON spec of MapBox
* https://docs.mapbox.com/mapbox-gl-js/style-spec/
*
* @param {boolean} censusSelected
* @return {Style}
*/
export const getOSBaseMap = (
censusSelected: boolean,
) : Style => {
return !censusSelected ? {
/**
* Tribal Source
*/
'version': 8, 'version': 8,
/** /**
@ -44,6 +63,111 @@ 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,
},
// The labels source:
'labels': {
'type': 'raster',
'tiles': cartoLightBaseLayer.labelsOnly,
},
},
/**
* 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.PRIORITIZED_FEATURE_FILL_COLOR,
'fill-opacity': constants.HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY,
},
'minzoom': constants.TRIBAL_MIN_ZOOM,
'maxzoom': constants.TRIBAL_MAX_ZOOM,
},
/**
* Tribal layer - controls the border between features
*/
{
'id': constants.FEATURE_BORDER_LAYER_ID,
'source': constants.TRIBAL_SOURCE_NAME,
'source-layer': constants.TRIBAL_SOURCE_LAYER,
'type': 'line',
'paint': {
'line-color': constants.FEATURE_BORDER_COLOR,
'line-width': constants.FEATURE_BORDER_WIDTH,
'line-opacity': constants.FEATURE_BORDER_OPACITY},
'minzoom': constants.TRIBAL_MIN_ZOOM,
'maxzoom': constants.TRIBAL_MAX_ZOOM,
},
/**
* Alaska layer
*/
{
'id': constants.TRIBAL_ALASKA_POINTS_LAYER_ID,
'source': constants.TRIBAL_SOURCE_NAME,
'source-layer': constants.TRIBAL_SOURCE_LAYER,
'type': 'circle',
'filter': ['==', ['geometry-type'], 'Point'],
'paint': {
'circle-radius': constants.TRIBAL_ALASKA_CIRCLE_RADIUS,
'circle-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
'circle-opacity': constants.HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY,
'circle-stroke-color': constants.FEATURE_BORDER_COLOR,
'circle-stroke-width': constants.ALAKSA_POINTS_STROKE_WIDTH,
'circle-stroke-opacity': constants.FEATURE_BORDER_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
@ -92,55 +216,19 @@ export const getOSBaseMap = () : Style => {
'maxzoom': constants.GLOBAL_MAX_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM,
}, },
/** // A layer for labels only
* High zoom layer - non-prioritized features only
*/
{ {
'id': constants.HIGH_ZOOM_LAYER_ID, 'id': 'labels-only-layer',
'source': constants.HIGH_ZOOM_SOURCE_NAME, 'source': 'labels',
'source-layer': constants.SCORE_SOURCE_LAYER, 'type': 'raster',
/** 'layout': {
* This shows features where the high score < score boundary threshold. 'visibility': 'visible',
* In other words, this filter out prioritized features
*/
'filter': ['all',
['<', 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, 'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
}, },
/** // Low zoom layer (static) - prioritized features only
* High zoom layer - prioritized features only
*/
{
'id': constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID,
'source': constants.HIGH_ZOOM_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
/**
* This shows features where the high score > score boundary threshold.
* In other words, this filter out non-prioritized features
*/
'filter': ['all',
['>', 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,
},
/**
* Low zoom layer - prioritized features only
*/
{ {
'id': constants.LOW_ZOOM_LAYER_ID, 'id': constants.LOW_ZOOM_LAYER_ID,
'source': constants.LOW_ZOOM_SOURCE_NAME, 'source': constants.LOW_ZOOM_SOURCE_NAME,
@ -162,16 +250,60 @@ export const getOSBaseMap = () : Style => {
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW, 'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
}, },
// A layer for labels only // High zoom layer (static) - non-prioritized features only
{ {
'id': 'labels-only-layer', 'id': constants.HIGH_ZOOM_LAYER_ID,
'source': 'labels', 'source': constants.HIGH_ZOOM_SOURCE_NAME,
'type': 'raster', 'source-layer': constants.SCORE_SOURCE_LAYER,
'layout': { /**
'visibility': 'visible', * This shows features where the high score < score boundary threshold.
* In other words, this filter out prioritized features
*/
'filter': ['all',
['<', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD],
],
'type': 'fill',
'paint': {
'fill-opacity': constants.NON_PRIORITIZED_FEATURE_FILL_OPACITY,
}, },
'minzoom': constants.GLOBAL_MIN_ZOOM, 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
'maxzoom': constants.GLOBAL_MAX_ZOOM, },
// High zoom layer (static) - prioritized features only
{
'id': constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID,
'source': constants.HIGH_ZOOM_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
/**
* This shows features where the high score > score boundary threshold.
* In other words, this filter out non-prioritized features
*/
'filter': ['all',
['>', 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 (static) - controls the border between features
{
'id': constants.FEATURE_BORDER_LAYER_ID,
'source': constants.HIGH_ZOOM_SOURCE_NAME,
'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,
},
'minzoom': constants.GLOBAL_MIN_ZOOM_FEATURE_BORDER,
'maxzoom': constants.GLOBAL_MAX_ZOOM_FEATURE_BORDER,
}, },
], ],
}; };

View file

@ -351,6 +351,22 @@
"defaultMessage": "Finding location...", "defaultMessage": "Finding location...",
"description": "On the explore the map page, on the map, this is the message above the gelocation icon that geolocation is locating." "description": "On the explore the map page, on the map, this is the message above the gelocation icon that geolocation is locating."
}, },
"explore.map.page.map.layer.selector.tracts.long": {
"defaultMessage": "Census Tracts",
"description": "On the explore the map page, on the map, the full name indicating Census Tracts"
},
"explore.map.page.map.layer.selector.tracts.short": {
"defaultMessage": "Tracts",
"description": "On the explore the map page, on the map, the short name indicating Census Tracts"
},
"explore.map.page.map.layer.selector.tribal.long": {
"defaultMessage": "Tribal Lands",
"description": "On the explore the map page, on the map, the full name indicating Tribal Lands"
},
"explore.map.page.map.layer.selector.tribal.short": {
"defaultMessage": "Tribal",
"description": "On the explore the map page, on the map, the short name indicating Tribal Lands"
},
"explore.map.page.map.search.placeholder.mobile.text": { "explore.map.page.map.search.placeholder.mobile.text": {
"defaultMessage": "Search locations", "defaultMessage": "Search locations",
"description": "On the explore the map page, on the map, the placeholder text for search" "description": "On the explore the map page, on the map, the placeholder text for search"
@ -823,6 +839,10 @@
"defaultMessage": "AND", "defaultMessage": "AND",
"description": "Navigate to the explore the map page. When the map is in view, click on the map. Click on a category to expand. This is the AND spacer around thresholds." "description": "Navigate to the explore the map page. When the map is in view, click on the map. Click on a category to expand. This is the AND spacer around thresholds."
}, },
"explore.map.page.side.panel.tribalInfo.landAreaName": {
"defaultMessage": "Land Area Name:",
"description": "Navigate to the explore the map page. Click on Tribal Lands, when the map is in view, \n click on the map. The side panel will show the land area name of the feature selected"
},
"explore.map.page.side.panel.version.title": { "explore.map.page.side.panel.version.title": {
"defaultMessage": "Methodology version {version}", "defaultMessage": "Methodology version {version}",
"description": "Navigate to the explore the map page. When the map is in view, click on the map. The side panel will show the methodology version number" "description": "Navigate to the explore the map page. When the map is in view, click on the map. The side panel will show the methodology version number"