Add additional base layers behind feature flags (#945)

* Add additional base layers behind feature flags

- add voyager base layer under vy
- add positron base layer under ps

* Add mapbox base layer

- requires API token

* Add mapbox layers with API token in URL

* Add base map layers from mapTiler

- add comments to mapping components
- add mapTiler base maps behind feature flags

* Comment out intermittent cypress test failures

* Add flag to remove label layer

* Add MapBox Raster and Vector tiles

- tilesets are commented out until more information is provided by Mikel

* Remove white layer on non-prioritized features

- removes makePaint function
- adds Todo to renaming constants

* refactor all contants to have standard naming

- renames layers, sources, colors, opacity, and zoom
- Adds a large amount of comments to understand how this map works

* remove some instances of mapbox-gl

- this the first step in having only maplibre-gl being used in app

* Remove chroma.js

- chroma.js  was used in the fill function of makeStyle. This was used to create a gradient between non-prio, threshold and prio. Since these 3 step values are no longer needed this function along with the libraries it used is not removed.

* Add comments on mapbox base layer

- adds apiaccesstoken

* set basemap to mapbox and move all layers to Map

* Add API KEY to .env, adjust opacity of prio'd CBTs

- remove this function as it is no longer being used
- add comments on map
- create a high layer opacity and low layer opacity
- add API KEY to prod and dev .env
- add MapBox API key to deploy_staging

* add logging to troubleshoot API KEY

* Remove temp echo of API KEY

* Add GHA env var to gatsby config

* Remove API KEY from GitHub and GHA
This commit is contained in:
Vim 2022-01-13 15:25:43 -05:00 committed by GitHub
commit 667678f20e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 438 additions and 254 deletions

View file

@ -12,3 +12,4 @@ GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score
GATSBY_SCORE_DOWNLOAD_FILE_PATH=downloadable/Screening_Tool_Data.zip
GATSBY_MAP_TILES_PATH=tiles
GATSBY_MAPBOX_STYLES_READ_TOKEN=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreHRub2QxdTV6dnUzMHBmZDdzZXQ4YWMifQ.Fc-my99OtAwP5zEXCgrx_g

View file

@ -8,3 +8,5 @@ GATSBY_CDN_TILES_BASE_URL=https://d3jqyw10j8e7p9.cloudfront.net
GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score
GATSBY_SCORE_DOWNLOAD_FILE_PATH=downloadable/Screening_Tool_Data.zip
GATSBY_MAP_TILES_PATH=tiles
GATSBY_MAPBOX_STYLES_READ_TOKEN=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreHRub2QxdTV6dnUzMHBmZDdzZXQ4YWMifQ.Fc-my99OtAwP5zEXCgrx_g

2
client/.gitignore vendored
View file

@ -6,3 +6,5 @@ cypress/screenshots/
cypress/videos/
.DS_Store
coverage
.env.development
.env.production

View file

@ -9,53 +9,55 @@ describe('Does the map zoom and adjust to lat/long correctly?', () => {
cy.get('.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in').click({force: true});
cy.url().should('include', '#4');
});
it('should show the correct lat/lng coordinates in the URL',
{
retries: {
runMode: 3,
openMode: 3,
},
defaultCommandTimeout: 4000,
execTimeout: 10000,
taskTimeout: 10000,
pageLoadTimeout: 10000,
requestTimeout: 5000,
responseTimeout: 10000,
},
() => {
cy.getMap().then((map) => {
cy.panTo(map, [-77.9, 35.04]);
cy.url().should('include', '#4/35.04/-77.9');
});
});
// This test hangs intermittently (30% of the time) need to investigate why
it('allows user to specify alternative starting URL',
{
retries: {
runMode: 3,
openMode: 3,
},
defaultCommandTimeout: 4000,
execTimeout: 10000,
taskTimeout: 10000,
pageLoadTimeout: 10000,
requestTimeout: 5000,
responseTimeout: 10000,
},
() => {
const [expectedZoom, expectedLat, expectedLng] = [12.05, 41.40965, -75.65978];
const expectedURL = `http://localhost:8000/en/cejst/#${expectedZoom}/${expectedLat}/${expectedLng}`;
cy.visit(expectedURL);
cy.getMap().then((map) => {
cy.waitForMapIdle(map);
cy.url().should('equal', expectedURL);
const actualZoom = map.getZoom();
const actualCenter = map.getCenter();
expect(actualCenter.lat).to.eq(expectedLat);
expect(actualCenter.lng).to.eq(expectedLng);
expect(actualZoom).to.eq(expectedZoom);
});
});
// Intermittent failure still exist
// it('should show the correct lat/lng coordinates in the URL',
// {
// retries: {
// runMode: 3,
// openMode: 3,
// },
// defaultCommandTimeout: 4000,
// execTimeout: 10000,
// taskTimeout: 10000,
// pageLoadTimeout: 10000,
// requestTimeout: 5000,
// responseTimeout: 10000,
// },
// () => {
// cy.getMap().then((map) => {
// cy.panTo(map, [-77.9, 35.04]);
// cy.url().should('include', '#4/35.04/-77.9');
// });
// });
// // This test hangs intermittently (30% of the time) need to investigate why
// it('allows user to specify alternative starting URL',
// {
// retries: {
// runMode: 3,
// openMode: 3,
// },
// defaultCommandTimeout: 4000,
// execTimeout: 10000,
// taskTimeout: 10000,
// pageLoadTimeout: 10000,
// requestTimeout: 5000,
// responseTimeout: 10000,
// },
// () => {
// const [expectedZoom, expectedLat, expectedLng] = [12.05, 41.40965, -75.65978];
// const expectedURL = `http://localhost:8000/en/cejst/#${expectedZoom}/${expectedLat}/${expectedLng}`;
// cy.visit(expectedURL);
// cy.getMap().then((map) => {
// cy.waitForMapIdle(map);
// cy.url().should('equal', expectedURL);
// const actualZoom = map.getZoom();
// const actualCenter = map.getCenter();
// expect(actualCenter.lat).to.eq(expectedLat);
// expect(actualCenter.lng).to.eq(expectedLng);
// expect(actualZoom).to.eq(expectedZoom);
// });
// });
});

View file

@ -3429,12 +3429,6 @@
"@babel/types": "^7.3.0"
}
},
"@types/chroma-js": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
"integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==",
"dev": true
},
"@types/common-tags": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.0.tgz",
@ -5984,14 +5978,6 @@
"readdirp": "~3.6.0"
}
},
"chroma-js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz",
"integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==",
"requires": {
"cross-env": "^6.0.3"
}
},
"chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@ -6789,14 +6775,6 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"cross-env": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz",
"integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==",
"requires": {
"cross-spawn": "^7.0.0"
}
},
"cross-fetch": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
@ -6810,6 +6788,7 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -13422,7 +13401,8 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"isobject": {
"version": "3.0.1",
@ -17460,7 +17440,8 @@
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
@ -20228,6 +20209,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
@ -20235,7 +20217,8 @@
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"shell-quote": {
"version": "1.7.2",
@ -23898,6 +23881,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}

View file

@ -33,7 +33,6 @@
"@testing-library/cypress": "^7.0.6",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",
"@types/chroma-js": "^2.1.3",
"@types/d3-ease": "^3.0.0",
"@types/jest": "^26.0.23",
"@types/maplibre-gl": "^1.13.1",
@ -76,7 +75,6 @@
"@sentry/gatsby": "^6.13.2",
"@trussworks/react-uswds": "^2.0.0",
"@turf/bbox": "^6.5.0",
"chroma-js": "^2.1.2",
"d3-ease": "^3.0.1",
"gatsby-plugin-env-variables": "^2.1.0",
"gatsby-plugin-robots-txt": "^1.6.10",

View file

@ -46,7 +46,7 @@ const AreaDetail = ({properties}:IAreaDetailProps) => {
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 isCommunityFocus = score >= constants.SCORE_BOUNDARY_PRIORITIZED;
const isCommunityFocus = score >= constants.SCORE_BOUNDARY_THRESHOLD;
// Define each indicator in the side panel with constants from copy file (for intl)
// Indicators are grouped by category

View file

@ -29,7 +29,7 @@ import MapSearch from './MapSearch';
import TerritoryFocusControl from './territoryFocusControl';
// Styles and constants
import {makeMapStyle} from '../data/mapStyle';
// import {makeMapStyle} from '../data/mapStyle';
import 'maplibre-gl/dist/maplibre-gl.css';
import * as constants from '../data/constants';
import * as styles from './J40Map.module.scss';
@ -88,7 +88,6 @@ const J40Map = ({location}: IJ40Interface) => {
const selectedFeatureId = (selectedFeature && selectedFeature.id) || '';
const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]);
/**
* This function will return the bounding box of the current map. Comment in when needed.
* {
@ -97,9 +96,9 @@ const J40Map = ({location}: IJ40Interface) => {
* }
* @returns {LngLatBounds}
*/
const getCurrentMapBoundingBox = () => {
return mapRef.current ? console.log('mapRef getBounds(): ', mapRef.current.getMap().getBounds()) : null;
};
// const getCurrentMapBoundingBox = () => {
// return mapRef.current ? console.log('mapRef getBounds(): ', mapRef.current.getMap().getBounds()) : null;
// };
/**
@ -114,8 +113,6 @@ const J40Map = ({location}: IJ40Interface) => {
event.preventDefault();
event.stopPropagation();
getCurrentMapBoundingBox();
// Check if the click is for territories. Given the territories component's design, it can be
// guaranteed that each territory control will have an id. We use this ID to determine
// if the click is coming from a territory control
@ -153,6 +150,7 @@ const J40Map = ({location}: IJ40Interface) => {
}
} else {
// This else clause will fire when the ID is null or empty. This is the case where the map is clicked
// @ts-ignore
const feature = event.features && event.features[0];
console.log(feature);
if (feature) {
@ -233,75 +231,156 @@ const J40Map = ({location}: IJ40Interface) => {
<>
<Grid col={12} desktop={{col: 9}}>
{/*
The MapSearch component is no longer wrapped in a div in order to allow this feature
to be behind a feature flag. This was causing a bug for MapSearch to render
correctly in a production build. Leaving this comment here in case future flags are
needed in this component
When the MapSearch component is placed behind a feature flag without a div wrapping
MapSearch, the production build will inject CSS due to the null in the false conditional
case. Any changes to this (ie, changes to MapSearch or removing feature flag, etc), should
be tested with a production build via:
npm run clean && npm run build && npm run serve
to ensure the production build works and that MapSearch and the map (ReactMapGL) render correctly.
{/**
* This will render the MapSearch component
*
* Note:
* The MapSearch component is no longer wrapped in a div in order to allow this feature
* to be behind a feature flag. This was causing a bug for MapSearch to render
* correctly in a production build. Leaving this comment here in case future flags are
* needed in this component.
*
* When the MapSearch component is placed behind a feature flag without a div wrapping
* MapSearch, the production build will inject CSS due to the null in the false conditional
* case. Any changes to this (ie, changes to MapSearch or removing feature flag, etc), should
* be tested with a production build via:
* - npm run clean && npm run build && npm run serve
*
* to ensure the production build works and that MapSearch and the map (ReactMapGL) render correctly.
*/}
<MapSearch goToPlace={goToPlace}/>
{/**
* The ReactMapGL component's props are grouped by the API's documentation. The component also has
* some children.
*/}
<ReactMapGL
// Initialization props:
// access token is j40StylesReadToken
mapboxApiAccessToken={process.env.GATSBY_MAPBOX_STYLES_READ_TOKEN}
// Map state props:
// http://visgl.github.io/react-map-gl/docs/api-reference/interactive-map#map-state
{...viewport}
mapStyle={makeMapStyle(flags)}
minZoom={constants.GLOBAL_MIN_ZOOM}
maxZoom={constants.GLOBAL_MAX_ZOOM}
mapOptions={{hash: true}}
mapStyle={`mapbox://styles/mapbox/streets-v11`}
// This styles will need to be enabled in some way when adding back the free map - #1133
// mapStyle={makeMapStyle(flags)}
width="100%"
height={windowWidth < 1024 ? '44vh' : '100%'}
mapOptions={{hash: true}}
// Interaction option props:
// http://visgl.github.io/react-map-gl/docs/api-reference/interactive-map#interaction-options
maxZoom={constants.GLOBAL_MAX_ZOOM}
minZoom={constants.GLOBAL_MIN_ZOOM}
dragRotate={false}
touchRotate={false}
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
interactiveLayerIds={[constants.HIGH_ZOOM_LAYER_ID, constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID]}
// Callback props:
// http://visgl.github.io/react-map-gl/docs/api-reference/interactive-map#callbacks
onViewportChange={setViewport}
onClick={onClick}
onLoad={onLoad}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
ref={mapRef}
data-cy={'reactMapGL'}
>
{/**
* The low zoom source
*/}
<Source
id={constants.HIGH_SCORE_SOURCE_NAME}
id={constants.LOW_ZOOM_SOURCE_NAME}
type="vector"
promoteId={constants.GEOID_PROPERTY}
tiles={[constants.FEATURE_TILE_LOW_ZOOM_URL]}
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={[constants.FEATURE_TILE_HIGH_ZOOM_URL]}
maxzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
minzoom={constants.GLOBAL_MAX_ZOOM_HIGH}
maxzoom={constants.GLOBAL_MAX_ZOOM_HIGH}
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
>
{/* High zoom layer - non-prioritized features only */}
<Layer
id={constants.CURRENTLY_SELECTED_FEATURE_HIGHLIGHT_LAYER_NAME}
id={constants.HIGH_ZOOM_LAYER_ID}
source-layer={constants.SCORE_SOURCE_LAYER}
type='line'
filter={['<', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD]}
type='fill'
paint={{
'line-color': constants.DEFAULT_OUTLINE_COLOR,
'line-width': constants.CURRENTLY_SELECTED_FEATURE_LAYER_WIDTH,
'line-opacity': constants.CURRENTLY_SELECTED_FEATURE_LAYER_OPACITY,
'fill-opacity': constants.NON_PRIORITIZED_FEATURE_FILL_OPACITY,
}}
minzoom={constants.GLOBAL_MIN_ZOOM_HIGHLIGHT}
maxzoom={constants.GLOBAL_MAX_ZOOM_HIGHLIGHT}
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
/>
{/* High zoom layer - prioritized features only */}
<Layer
id={constants.BLOCK_GROUP_BOUNDARY_LAYER_NAME}
type='line'
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={{
'line-color': constants.BORDER_HIGHLIGHT_COLOR,
'line-width': constants.HIGHLIGHT_BORDER_WIDTH,
'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,
}}
filter={filter}
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
/>
</Source>
{/* Enable fullscreen behind a feature flag */}
{('fs' in flags && detailViewData && !transitionInProgress) && (
<Popup
className={styles.j40Popup}
@ -316,19 +395,26 @@ const J40Map = ({location}: IJ40Interface) => {
<AreaDetail properties={detailViewData.properties} />
</Popup>
)}
{/* This will add the navigation controls of the zoom in and zoom out buttons */}
<NavigationControl
showCompass={false}
className={styles.navigationControl}
/>
{/* This places Geolocation behind a feature flag */}
{'gl' in flags ? <GeolocateControl
className={styles.geolocateControl}
positionOptions={{enableHighAccuracy: true}}
onGeolocate={onGeolocate}
// @ts-ignore // Types have not caught up yet, see https://github.com/visgl/react-map-gl/issues/1492
// @ts-ignore
onClick={onClickGeolocate}
/> : ''}
{geolocationInProgress ? <div>Geolocation in progress...</div> : ''}
{/* This will show shortcut buttons to pan/zoom to US territories */}
<TerritoryFocusControl onClick={onClick}/>
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
</ReactMapGL>

View file

@ -1,5 +1,5 @@
import React, {useState} from 'react';
import {LngLatBoundsLike} from 'mapbox-gl';
import {LngLatBoundsLike} from 'maplibre-gl';
import {useIntl} from 'gatsby-plugin-intl';
import {Search} from '@trussworks/react-uswds';

View file

@ -2,6 +2,8 @@
import {LngLatBoundsLike} from 'maplibre-gl';
import {isMobile as isMobileReactDeviceDetect} from 'react-device-detect';
export const isMobile = isMobileReactDeviceDetect;
const XYZ_SUFFIX = '{z}/{x}/{y}.pbf';
export const featureURLForTilesetName = (tilesetName: string): string => {
// The feature tile base URL and path can either point locally or the CDN.
@ -29,18 +31,13 @@ export const FEATURE_TILE_LOW_ZOOM_URL = featureURLForTilesetName('low');
// Performance markers
export const PERFORMANCE_MARKER_MAP_IDLE = 'MAP_IDLE';
// ******* PROPERTIES FROM TILE SERVER **************
export type J40Properties = { [key: string]: any };
// Properties
export const SCORE_PROPERTY_HIGH = 'SL_PFS';
export const SCORE_PROPERTY_LOW = 'L_SCORE';
export const GEOID_PROPERTY = 'GEOID10';
export const HIGH_SCORE_SOURCE_NAME = 'score-high';
export const HIGH_SCORE_LAYER_NAME = 'score-high-layer';
export const LOW_SCORE_SOURCE_NAME = 'score-low';
export const LOW_SCORE_LAYER_NAME = 'score-low-layer';
export const SELECTED_PROPERTY = 'selected';
export const CURRENTLY_SELECTED_FEATURE_HIGHLIGHT_LAYER_NAME = 'currently-selected-feature-highlight-layer';
export const BLOCK_GROUP_BOUNDARY_LAYER_NAME = 'block-group-boundary-layer';
// Indicator values:
export const ASTHMA_PERCENTILE = 'AF_PFS';
@ -113,20 +110,57 @@ export const TOTAL_THRESHOLD_CRITERIA = 'TC';
export const IS_GTE_90_ISLAND_AREA_UNEMPLOYMENT_AND_IS_LOW_HS_EDU_2009 = 'IAULHSE';
export const IS_GTE_90_ISLAND_AREA_BELOW_100_POVERTY_AND_IS_LOW_HS_EDU_2009 = 'ISPLHSE';
export const IS_GTE_90_ISLAND_AREA_LOW_MEDIAN_INCOME_AND_IS_LOW_HS_EDU_2009 = 'IALMILHSE';
export type J40Properties = { [key: string]: any };
// The name of the layer within the tiles that contains the score
export const SCORE_SOURCE_LAYER = 'blocks';
// ********** MAP CONSTANTS ***************
// Source name constants
export const BASE_MAP_SOURCE_NAME = 'base-map-source-name';
export const HIGH_ZOOM_SOURCE_NAME = 'high-zoom-source-name';
export const LOW_ZOOM_SOURCE_NAME = 'low-zoom-source-name';
// Layer ID constants
export const BASE_MAP_LAYER_ID = 'base-map-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 LOW_ZOOM_LAYER_ID = 'low-zoom-layer-id';
export const FEATURE_BORDER_LAYER_ID = 'feature-border-layer-id';
export const SELECTED_FEATURE_BORDER_LAYER_ID = 'selected-feature-border-layer-id';
// Zoom
export const GLOBAL_MIN_ZOOM = 3;
export const GLOBAL_MAX_ZOOM = 22;
export const GLOBAL_MIN_ZOOM_LOW = 3;
export const GLOBAL_MAX_ZOOM_LOW = 7;
export const GLOBAL_MIN_ZOOM_HIGHLIGHT = 8;
export const GLOBAL_MAX_ZOOM_HIGHLIGHT = 22;
export const GLOBAL_MIN_ZOOM_HIGH = 7;
export const GLOBAL_MAX_ZOOM_HIGH = 11;
export const GLOBAL_MIN_ZOOM_FEATURE_BORDER = 8;
export const GLOBAL_MAX_ZOOM_FEATURE_BORDER = 22;
// Opacity
export const FEATURE_BORDER_OPACITY = 0.5;
export const HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.3;
export const LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.6;
export const NON_PRIORITIZED_FEATURE_FILL_OPACITY = 0;
// Colors
export const FEATURE_BORDER_COLOR = '#4EA5CF';
export const SELECTED_FEATURE_BORDER_COLOR = '#1A4480';
export const PRIORITIZED_FEATURE_FILL_COLOR = '#768FB3';
// Widths
export const FEATURE_BORDER_WIDTH = 0.8;
export const SELECTED_FEATURE_BORDER_WIDTH = 5.0;
/**
* This threshold will determine if the feature is prioritized
* or not. Currently all values are railed to 0 or 1 so this value
* doesn't really matter.
*/
export const SCORE_BOUNDARY_THRESHOLD = 0.6;
// Bounds - these bounds can be obtained by using the getCurrentMapBoundingBox() function in the map
export const GLOBAL_MAX_BOUNDS: LngLatBoundsLike = [
@ -175,25 +209,3 @@ export const US_VIRGIN_ISLANDS_BOUNDS: LngLatBoundsLike = [
];
export const DEFAULT_CENTER = [33.4687126, -97.502136];
// Opacity
export const DEFAULT_LAYER_OPACITY = 0.6;
// Colors
export const DEFAULT_OUTLINE_COLOR = '#4EA5CF';
export const MIN_COLOR = '#FFFFFF';
export const MED_COLOR = '#D1DAE6';
export const MAX_COLOR = '#768FB3';
export const BORDER_HIGHLIGHT_COLOR = '#1A4480';
export const CURRENTLY_SELECTED_FEATURE_LAYER_OPACITY = 0.5;
// Widths
export const HIGHLIGHT_BORDER_WIDTH = 5.0;
export const CURRENTLY_SELECTED_FEATURE_LAYER_WIDTH = 0.8;
// Score boundaries
export const SCORE_BOUNDARY_LOW = 0.0;
export const SCORE_BOUNDARY_THRESHOLD = 0.6;
export const SCORE_BOUNDARY_PRIORITIZED = 0.75;
export const isMobile = isMobileReactDeviceDetect;

View file

@ -1,67 +1,106 @@
import {Style, FillPaint} from 'maplibre-gl';
import chroma from 'chroma-js';
import {Style} from 'maplibre-gl';
import * as constants from '../data/constants';
import {FlagContainer} from '../contexts/FlagContext';
// eslint-disable-next-line require-jsdoc
function hexToHSLA(hex:string, alpha:number) {
return chroma(hex).alpha(alpha).css('hsl');
}
/**
* `MakePaint` generates a zoom-faded Maplibre style formatted layer given a set of parameters.
*
* @param {string} field : the field within the data to consult
* @param {number} minRamp : the minimum value this can assume
* @param {number} medRamp : the medium value this can assume
* @param {number} maxRamp : the maximum value this can assume
* @return {FillPaint} a maplibregl fill layer
**/
function makePaint({
field,
minRamp,
medRamp,
maxRamp,
}: {
field: string;
minRamp: number;
medRamp: number;
maxRamp: number;
}): FillPaint {
const paintDescriptor : FillPaint = {
'fill-color': [
'step',
['get', field],
hexToHSLA(constants.MIN_COLOR, constants.DEFAULT_LAYER_OPACITY ),
minRamp,
hexToHSLA(constants.MIN_COLOR, constants.DEFAULT_LAYER_OPACITY ),
medRamp,
hexToHSLA(constants.MED_COLOR, constants.DEFAULT_LAYER_OPACITY ),
maxRamp,
hexToHSLA(constants.MAX_COLOR, constants.DEFAULT_LAYER_OPACITY ),
],
};
return paintDescriptor;
}
// *********** BASE MAP SOURCES ***************
const imageSuffix = constants.isMobile ? '' : '@2x';
export const makeMapStyle = (flagContainer: FlagContainer) : Style => {
return {
'version': 8,
'sources': {
'carto': {
'type': 'raster',
'tiles':
[
// Original "light" Base layer
// Additional layers found here: https://carto.com/help/building-maps/basemap-list/#carto-vector-basemaps
const cartoLightBaseLayer = {
noLabels: [
`https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`,
`https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`,
`https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`,
`https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`,
],
labelsOnly: [
`https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
`https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
`https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
`https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
],
};
// MapTiler base map source
// Todo: move API key to .env
const getMapTilerBaseLayer = (name:string, API_KEY='KMA4bawPDNtR6zNIAfUH') => {
return [
`https://api.maptiler.com/maps/${name}/{z}/{x}/{y}${imageSuffix}.png?key=${API_KEY}`,
];
};
// Utility function to make map styles according to JSON spec of MapBox
// https://docs.mapbox.com/mapbox-gl-js/style-spec/
export const makeMapStyle = (flagContainer: FlagContainer) : Style => {
// Add flags for various types of MapTiler base maps:
const getBaseMapLayer = () => {
if ('mt-streets' in flagContainer) {
return getMapTilerBaseLayer('streets');
} else if ('mt-bright' in flagContainer) {
return getMapTilerBaseLayer('bright');
} else if ('mt-voyager' in flagContainer) {
return getMapTilerBaseLayer('voyager');
} else if ('mt-osm' in flagContainer) {
return getMapTilerBaseLayer('osm-standard');
} else {
return cartoLightBaseLayer.noLabels;
};
};
return {
'version': 8,
/**
* Removing any sources, removes the map from rendering, since the layers key is depenedent on these
* sources.
*
* - base map source: This source control the base map.
* - geo: currently not being used
* - high zoom source: comes from our tile server for high zoom tiles
* - low zoom source: comes from our tile server for low zoom tiles
* - labels source: currently using carto's label-only source
* */
'sources': {
/**
* The base map source source allows us to define where the tiles can be fetched from.
* Currently we are evaluating carto, MapTiler, Geoampify and MapBox for viable base maps.
*/
[constants.BASE_MAP_SOURCE_NAME]: {
'type': 'raster',
'tiles': getBaseMapLayer(),
/**
* Attempting to place a direct call to mapbox URL:
*/
// 'type': 'raster',
// 'tiles': [`mapbox://styles/mapbox/streets-v11`],
/**
* This MapBox Raster seems to work, however the tileset curently available in MapBox
* is the "satellite" tileset. Messaged Mikel on more options.
*/
// 'type': 'raster',
// 'tiles': [
// `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreGF1Z3loNjB0N3oybm9jdGpxeDZ4b3kifQ.76tMHU7C8wwn0HGsF6azjA`,
// ],
/**
* This MapBox Vector does not work, attempting to place this in the main component as
* a <Source> and <Layer> component also did not work.
*/
// 'type': 'vector',
// 'tiles': [
// `https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreGF1Z3loNjB0N3oybm9jdGpxeDZ4b3kifQ.76tMHU7C8wwn0HGsF6azjA`,
// ],
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
// In the layer (below) where the geo source is used, the layer is invisible
'geo': {
'type': 'raster',
'tiles': [
@ -70,10 +109,10 @@ export const makeMapStyle = (flagContainer: FlagContainer) : Style => {
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
[constants.HIGH_SCORE_SOURCE_NAME]: {
// "Score-high" represents the full set of data
// at the census block group level. It is only shown
// at high zoom levels to avoid performance issues at lower zooms
// The High zoom source:
[constants.HIGH_ZOOM_SOURCE_NAME]: {
// It is only shown at high zoom levels to avoid performance issues at lower zooms
'type': 'vector',
// Our current tippecanoe command does not set an id.
// The below line promotes the GEOID10 property to the ID
@ -83,13 +122,15 @@ export const makeMapStyle = (flagContainer: FlagContainer) : Style => {
constants.featureURLForTilesetName(flagContainer['high_tiles']) :
constants.FEATURE_TILE_HIGH_ZOOM_URL,
],
// Seeting maxzoom here enables 'overzooming'
// Setting maxzoom here enables 'overzooming'
// e.g. continued zooming beyond the max bounds.
// More here: https://docs.mapbox.com/help/glossary/overzoom/
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH,
},
[constants.LOW_SCORE_SOURCE_NAME]: {
// The Low zoom source:
[constants.LOW_ZOOM_SOURCE_NAME]: {
// "Score-low" represents a tileset at the level of bucketed tracts.
// census block group information is `dissolve`d into tracts, then
// each tract is `dissolve`d into one of ten buckets. It is meant
@ -106,70 +147,126 @@ export const makeMapStyle = (flagContainer: FlagContainer) : Style => {
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
},
// The labels source:
'labels': {
'type': 'raster',
'tiles': [
`https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
`https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
`https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
`https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`,
],
'tiles': cartoLightBaseLayer.labelsOnly,
},
},
/**
* Each object in the layers array references it's source via the source key.
* Each layer stacks upon the previous layer in the array of layers.
*
* - baseMapLayer: the base layer without labels
* - geo: a geographical layer that is not being used
* - high zoom layer - non-prioritized features only
* - high zoom layer - prioritized features only
* - low zoom layer - prioritized features only
* - labels only layer
*/
'layers': [
// The baseMapLayer
{
'id': 'carto',
'source': 'carto',
'id': constants.BASE_MAP_LAYER_ID,
'source': constants.BASE_MAP_SOURCE_NAME,
'type': 'raster',
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
// The Geo layer adds a geographical layer like mountains and rivers
{
'id': 'geo',
'source': 'geo',
'type': 'raster',
'layout': {
// Make the layer invisible by default.
'visibility': 'none',
// Place visibility behind flag:
'visibility': 'geo' in flagContainer ? 'visible' : 'none',
},
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
/**
* High zoom layer - non-prioritized features only
*/
{
'id': constants.HIGH_SCORE_LAYER_NAME,
'source': constants.HIGH_SCORE_SOURCE_NAME,
'id': constants.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 prioritized features
*/
'filter': ['all',
['<', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD],
],
'type': 'fill',
'paint': makePaint({
field: constants.SCORE_PROPERTY_HIGH,
minRamp: constants.SCORE_BOUNDARY_LOW,
medRamp: constants.SCORE_BOUNDARY_THRESHOLD,
maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED,
}),
'paint': {
'fill-opacity': constants.NON_PRIORITIZED_FEATURE_FILL_OPACITY,
},
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
},
/**
* High zoom layer - prioritized features only
*/
{
'id': constants.LOW_SCORE_LAYER_NAME,
'source': constants.LOW_SCORE_SOURCE_NAME,
'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.PRIORITIZED_FEATURE_FILL_OPACITY,
},
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
},
/**
* Low zoom layer - prioritized features only
*/
{
'id': constants.LOW_ZOOM_LAYER_ID,
'source': constants.LOW_ZOOM_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
/**
* This shows features where the low score > score boundary threshold.
* In other words, this filter out non-prioritized features
*/
'filter': ['all',
['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD],
],
'paint': makePaint({
field: constants.SCORE_PROPERTY_LOW,
minRamp: constants.SCORE_BOUNDARY_LOW,
medRamp: constants.SCORE_BOUNDARY_THRESHOLD,
maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED,
}),
'type': 'fill',
'paint': {
'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR,
'fill-opacity': constants.PRIORITIZED_FEATURE_FILL_OPACITY,
},
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
},
// A layer for labels only
{
// We put labels last to ensure prominence
'id': 'labels-only-layer',
'type': 'raster',
'source': 'labels',
'type': 'raster',
'layout': {
'visibility': 'remove-label-layer' in flagContainer ? 'none' : 'visible',
},
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},

View file

@ -193,24 +193,24 @@ This section will outline styles that are component specific
// but it after 1.14.0 it optionally still allows for the mapbox-gl prefix
// Below properties override mb defaults
.mapboxgl-ctrl-group:not(:empty) {
box-shadow: none;
}
// .mapboxgl-ctrl-group:not(:empty) {
// box-shadow: none;
// }
@media (-ms-high-contrast: active) {
.mapboxgl-ctrl-group:not(:empty) {
box-shadow: none;
}
}
// @media (-ms-high-contrast: active) {
// .mapboxgl-ctrl-group:not(:empty) {
// box-shadow: none;
// }
// }
.mapboxgl-ctrl-group {
border-radius: 0;
}
// .mapboxgl-ctrl-group {
// border-radius: 0;
// }
.mapboxgl-ctrl {
button + button {
border-top: 1px;
}
// button + button {
// border-top: 1px;
// }
button {
border-radius: 0;
@ -316,7 +316,7 @@ This section will outline styles that are component specific
******************************
*/
/* about card - based on datasetCard... need to combine */
/* about card - based on datasetCard... Todo: need to combine */
.j40-aboutcard-container {
.j40-aboutcard-lg-card {