Parameterize zoom experiments (#339)

* Adding ability to set flags in url
* parameterizing tile layers
This commit is contained in:
Nat Hillard 2021-07-14 11:26:12 -04:00 committed by GitHub
parent 6c8d71c5b9
commit 3cd6e06115
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 163 deletions

View file

@ -25,7 +25,7 @@ const J40Header = () => {
const toggleMobileNav = (): void =>
setMobileNavOpen((prevOpen) => !prevOpen);
const headerLinks = (flags: string[] | undefined) => {
const headerLinks = (flags: {[key: string] : any} | undefined) => {
// static map of all possible menu items. Originally, it was all strings,
// but we need to handle both onsite and offsite links.
const menuData = new Map<string, JSX.Element>([
@ -64,7 +64,7 @@ const J40Header = () => {
// select which items from the above map to show, right now it's only two
// possibilities so it's simple. Note: strings are used as react keys
const menu =
flags?.includes('sprint3') ?
('sprint3' in flags!) ?
['about', 'cejst', 'methodology', 'contact'] :
['about', 'cejst', 'methodology', 'contact'];
// TODO: make feature flags flags work.

View file

@ -7,10 +7,12 @@ import maplibregl, {LngLatBoundsLike,
Popup,
LngLatLike,
MapboxGeoJSONFeature} from 'maplibre-gl';
import mapStyle from '../data/mapStyle';
import {makeMapStyle} from '../data/mapStyle';
import PopupContent from './popupContent';
import * as constants from '../data/constants';
import ReactDOM from 'react-dom';
import {useFlags} from '../contexts/FlagContext';
import 'maplibre-gl/dist/maplibre-gl.css';
import * as styles from './J40Map.module.scss';
@ -27,11 +29,12 @@ const J40Map = () => {
const mapRef = useRef<Map>() as React.MutableRefObject<Map>;
const selectedFeature = useRef<MapboxGeoJSONFeature>();
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
const flags = useFlags();
useEffect(() => {
const initialMap = new Map({
container: mapContainer.current!,
style: mapStyle,
style: makeMapStyle(flags),
center: constants.DEFAULT_CENTER as LngLatLike,
zoom: zoom,
minZoom: constants.GLOBAL_MIN_ZOOM,

View file

@ -6,7 +6,7 @@ describe('URL params are parsed and passed to children', () => {
describe('when the URL has a "flags" parameter set', () => {
// We artificially set the URL to localhost?flags=1,2,3
beforeEach(() => {
window.history.pushState({}, 'Test Title', '/?flags=1,2,3');
window.history.pushState({}, 'Test Title', '/?flags=1,2,3,test=4');
});
describe('when using useFlags', () => {
beforeEach(() => {
@ -14,10 +14,11 @@ describe('URL params are parsed and passed to children', () => {
const flags = useFlags();
return (
<>
<div>{flags.includes('1') ? 'yes1' : 'no1'}</div>
<div>{flags.includes('2') ? 'yes2' : 'no2'}</div>
<div>{flags.includes('3') ? 'yes3' : 'no3'}</div>
<div>{flags.includes('4') ? 'yes4' : 'no4'}</div>
<div>{'1' in flags ? 'yes1' : 'no1'}</div>
<div>{'2' in flags ? 'yes2' : 'no2'}</div>
<div>{'3' in flags ? 'yes3' : 'no3'}</div>
<div>{'4' in flags ? 'yes4' : 'no4'}</div>
<div>{flags['test'] == 4 ? 'yes5' : 'no5'}</div>
</>
);
};
@ -33,6 +34,7 @@ describe('URL params are parsed and passed to children', () => {
expect(screen.queryByText('yes2')).toBeInTheDocument();
expect(screen.queryByText('yes3')).toBeInTheDocument();
expect(screen.queryByText('yes4')).not.toBeInTheDocument();
expect(screen.queryByText('yes5')).toBeInTheDocument();
});
});
});

View file

@ -1,6 +1,8 @@
import * as React from 'react';
import * as queryString from 'query-string';
export type FlagContainer = { [key: string]: any };
/**
* FlagContext stores feature flags and passes them to consumers
*/
@ -8,7 +10,7 @@ import * as queryString from 'query-string';
/**
* Contains a list of all currently-active flags
*/
flags: string[];
flags: FlagContainer;
}
const FlagContext = React.createContext<IFlagContext>({flags: []});
@ -16,9 +18,9 @@ const FlagContext = React.createContext<IFlagContext>({flags: []});
/**
* `useFlags` returns all feature flags.
*
* @return {Flags[]} flags All project feature flags
* @return {FlagContainer} flags All project feature flags
*/
const useFlags = () : string[] => {
const useFlags = () : FlagContainer => {
const {flags} = React.useContext(FlagContext);
return flags;
};
@ -39,9 +41,18 @@ interface IURLFlagProviderProps {
**/
const URLFlagProvider = ({children, location}: IURLFlagProviderProps) => {
const flagString = queryString.parse(location.search).flags;
let flags: string[] = [];
const flags : FlagContainer = {};
let flagList: string[] = [];
if (flagString && typeof flagString === 'string') {
flags = (flagString as string).split(',');
flagList = (flagString as string).split(',');
}
for (const flag of flagList) {
if (flag.includes('=')) {
const [key, value] = flag.split('=');
flags[key] = value;
} else {
flags[flag] = true;
}
}
console.log(JSON.stringify(location), JSON.stringify(flags));

View file

@ -1,8 +1,11 @@
// URLS
export const FEATURE_TILE_BASE_URL = 'https://d2zjid6n5ja2pt.cloudfront.net';
const XYZ_SUFFIX = '{z}/{x}/{y}.pbf';
export const FEATURE_TILE_HIGH_ZOOM_URL = `${FEATURE_TILE_BASE_URL}/0629_demo/${XYZ_SUFFIX}`;
export const FEATURE_TILE_LOW_ZOOM_URL = `${FEATURE_TILE_BASE_URL}/tiles_low/${XYZ_SUFFIX}`;
export const featureURLForTilesetName = (tilesetName :string ) : string => {
return `${FEATURE_TILE_BASE_URL}/${tilesetName}/${XYZ_SUFFIX}`;
};
export const FEATURE_TILE_HIGH_ZOOM_URL = featureURLForTilesetName('0629_demo');
export const FEATURE_TILE_LOW_ZOOM_URL = featureURLForTilesetName('tiles_low');
// Performance markers

View file

@ -1,6 +1,7 @@
import {Style, FillPaint} from 'maplibre-gl';
import chroma from 'chroma-js';
import * as constants from '../data/constants';
import {FlagContainer} from '../contexts/FlagContext';
// eslint-disable-next-line require-jsdoc
function hexToHSLA(hex:string, alpha:number) {
@ -45,172 +46,177 @@ function makePaint({
const imageSuffix = constants.isMobile ? '' : '@2x';
const mapStyle : Style = {
'version': 8,
'sources': {
'carto': {
'type': 'raster',
'tiles':
export const makeMapStyle = (flagContainer: FlagContainer) : Style => {
return {
'version': 8,
'sources': {
'carto': {
'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`,
],
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
'geo': {
'type': 'raster',
'tiles': [
'https://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
],
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
[constants.HIGH_SCORE_SOURCE_NAME]: {
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
'geo': {
'type': 'raster',
'tiles': [
'https://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
],
'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
'type': 'vector',
// Our current tippecanoe command does not set an id.
// The below line promotes the GEOID10 property to the ID
'promoteId': constants.GEOID_PROPERTY,
'tiles': [
constants.FEATURE_TILE_HIGH_ZOOM_URL,
],
// Seeting 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]: {
'type': 'vector',
// Our current tippecanoe command does not set an id.
// The below line promotes the GEOID10 property to the ID
'promoteId': constants.GEOID_PROPERTY,
'tiles': [
'high_tiles' in flagContainer ?
constants.featureURLForTilesetName(flagContainer['high_tiles']) :
constants.FEATURE_TILE_HIGH_ZOOM_URL,
],
// Seeting 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]: {
// "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
// to give us a favorable tradeoff between performance and fidelity.
'type': 'vector',
'promoteId': constants.GEOID_PROPERTY,
'tiles': [
constants.FEATURE_TILE_LOW_ZOOM_URL,
'type': 'vector',
'promoteId': constants.GEOID_PROPERTY,
'tiles': [
'low_tiles' in flagContainer ?
constants.featureURLForTilesetName(flagContainer['low_tiles']) :
constants.FEATURE_TILE_LOW_ZOOM_URL,
// For local development, use:
// 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf',
],
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
},
'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`,
],
},
},
'layers': [
{
'id': 'carto',
'source': 'carto',
'type': 'raster',
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
{
'id': 'geo',
'source': 'geo',
'type': 'raster',
'layout': {
// Make the layer invisible by default.
'visibility': 'none',
],
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
},
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
{
'id': constants.HIGH_SCORE_LAYER_NAME,
'source': constants.HIGH_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'type': 'fill',
'filter': ['all',
['>', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD],
],
'paint': makePaint({
field: constants.SCORE_PROPERTY_HIGH,
minRamp: constants.SCORE_BOUNDARY_LOW,
medRamp: constants.SCORE_BOUNDARY_THRESHOLD,
maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED,
}),
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
},
{
'id': constants.LOW_SCORE_LAYER_NAME,
'source': constants.LOW_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'type': 'fill',
'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,
}),
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
},
{
// "Score-highlights" represents the border
// around given tiles that appears at higher zooms
'id': 'score-highlights-layer',
'source': constants.HIGH_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'type': 'line',
'layout': {
'visibility': 'visible',
'line-join': 'round',
'line-cap': 'round',
},
'paint': {
'line-color': constants.DEFAULT_OUTLINE_COLOR,
'line-width': 0.8,
'line-opacity': 0.5,
},
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGHLIGHT,
'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGHLIGHT,
},
{
// "score-border-highlight" is used to highlight
// the currently-selected feature
'id': 'score-border-highlight-layer',
'type': 'line',
'source': constants.HIGH_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'layout': {},
'paint': {
'line-color': constants.BORDER_HIGHLIGHT_COLOR,
'line-width': [
'case',
['boolean', ['feature-state', constants.SELECTED_PROPERTY], false],
constants.HIGHLIGHT_BORDER_WIDTH,
0,
'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`,
],
},
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH,
},
{
'layers': [
{
'id': 'carto',
'source': 'carto',
'type': 'raster',
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
{
'id': 'geo',
'source': 'geo',
'type': 'raster',
'layout': {
// Make the layer invisible by default.
'visibility': 'none',
},
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
{
'id': constants.HIGH_SCORE_LAYER_NAME,
'source': constants.HIGH_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'type': 'fill',
'filter': ['all',
['>', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD],
],
'paint': makePaint({
field: constants.SCORE_PROPERTY_HIGH,
minRamp: constants.SCORE_BOUNDARY_LOW,
medRamp: constants.SCORE_BOUNDARY_THRESHOLD,
maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED,
}),
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
},
{
'id': constants.LOW_SCORE_LAYER_NAME,
'source': constants.LOW_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'type': 'fill',
'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,
}),
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
},
{
// "Score-highlights" represents the border
// around given tiles that appears at higher zooms
'id': 'score-highlights-layer',
'source': constants.HIGH_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'type': 'line',
'layout': {
'visibility': 'visible',
'line-join': 'round',
'line-cap': 'round',
},
'paint': {
'line-color': constants.DEFAULT_OUTLINE_COLOR,
'line-width': 0.8,
'line-opacity': 0.5,
},
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGHLIGHT,
'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGHLIGHT,
},
{
// "score-border-highlight" is used to highlight
// the currently-selected feature
'id': 'score-border-highlight-layer',
'type': 'line',
'source': constants.HIGH_SCORE_SOURCE_NAME,
'source-layer': constants.SCORE_SOURCE_LAYER,
'layout': {},
'paint': {
'line-color': constants.BORDER_HIGHLIGHT_COLOR,
'line-width': [
'case',
['boolean', ['feature-state', constants.SELECTED_PROPERTY], false],
constants.HIGHLIGHT_BORDER_WIDTH,
0,
],
},
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH,
},
{
// We put labels last to ensure prominence
'id': 'labels-only-layer',
'type': 'raster',
'source': 'labels',
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
],
'id': 'labels-only-layer',
'type': 'raster',
'source': 'labels',
'minzoom': constants.GLOBAL_MIN_ZOOM,
'maxzoom': constants.GLOBAL_MAX_ZOOM,
},
],
};
};
export default mapStyle;