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

@ -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';
// 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': {
'carto': {
/**
* 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':
[
`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`,
],
'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,
},