mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-07-31 20:31:16 -07:00
Census block groups should highlight when selected (#317)
* Fixes #280 - adds territory focus buttons for Alaska, Hawaii, Lower 48, and Puerto Rico to enable easy zoom to these locations * Adding tests - Specifically: * Adding VSCode debug command for Cypress and debug port specification * Disabling CORS on local tests * Adding waitForMapIdle Cypress test helper * Adding constants for easy change and access
This commit is contained in:
parent
5bade764c6
commit
d0c281ec72
9 changed files with 126 additions and 16 deletions
14
client/.vscode/launch.json
vendored
14
client/.vscode/launch.json
vendored
|
@ -42,6 +42,18 @@
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
"port": 9229
|
"port": 9229
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Debug Cypress",
|
||||||
|
"port": 9222,
|
||||||
|
"urlFilter": "http://localhost*",
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"skipFiles": [
|
||||||
|
"cypress_runner.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:8000/",
|
"baseUrl": "http://localhost:8000/",
|
||||||
"integrationFolder": "cypress/e2e"
|
"integrationFolder": "cypress/e2e",
|
||||||
|
"chromeWebSecurity": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
// / <reference types="Cypress" />
|
// / <reference types="Cypress" />
|
||||||
|
import * as constants from '../../src/data/constants';
|
||||||
|
import {LngLat} from 'maplibre-gl';
|
||||||
|
|
||||||
describe('Tests for the Explore the Map page', () => {
|
describe('Tests for the Explore the Map page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -17,9 +19,42 @@ describe('Tests for the Explore the Map page', () => {
|
||||||
|
|
||||||
for (const [territory, zxy] of Object.entries(tests)) {
|
for (const [territory, zxy] of Object.entries(tests)) {
|
||||||
it(`Can zoom to ${territory} `, () => {
|
it(`Can zoom to ${territory} `, () => {
|
||||||
cy.get(`[aria-label="Zoom to ${territory}"]`).click();
|
cy.getMap().then((map) => {
|
||||||
cy.url().should('include', zxy);
|
cy.get(`[aria-label="Zoom to ${territory}"]`).click();
|
||||||
|
cy.waitForMapIdle(map);
|
||||||
|
cy.url().should('include', zxy);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
it('Highlights selected regions', () => {
|
||||||
|
// The area around Punxsutawney PA
|
||||||
|
cy.visit('http://localhost:8000/en/cejst#10.29/40.8187/-78.9375');
|
||||||
|
|
||||||
|
cy.intercept('GET', `${constants.FEATURE_TILE_BASE_URL}/10/287/384.pbf`).as('getTile');
|
||||||
|
cy.wait('@getTile');
|
||||||
|
|
||||||
|
const getFeatureState = (map, id) => {
|
||||||
|
return map.getFeatureState(
|
||||||
|
{
|
||||||
|
'id': id,
|
||||||
|
'source': constants.SCORE_SOURCE_NAME,
|
||||||
|
'sourceLayer': constants.SCORE_SOURCE_LAYER,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const punx1001Info = {
|
||||||
|
'id': '420639601001',
|
||||||
|
'coords': new LngLat(40.911134, -79.027089),
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.getMap().then((map) => {
|
||||||
|
cy.waitForMapIdle(map);
|
||||||
|
map.fire('click', {lngLat: punx1001Info.coords});
|
||||||
|
const punx1001FeatureState = getFeatureState(map, punx1001Info.id);
|
||||||
|
expect(punx1001FeatureState).to.deep.equal({'selected': true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -21,3 +21,8 @@ Cypress.Commands.add('getMapCanvas', () => {
|
||||||
return cy.get('.maplibregl-canvas');
|
return cy.get('.maplibregl-canvas');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('waitForMapIdle', (map) => {
|
||||||
|
return new Cypress.Promise((resolve) => {
|
||||||
|
map.once('idle', resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"build": "gatsby build --prefix-paths",
|
"build": "gatsby build --prefix-paths",
|
||||||
"serve": "gatsby serve",
|
"serve": "gatsby serve",
|
||||||
"clean": "gatsby clean",
|
"clean": "gatsby clean",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "CYPRESS_REMOTE_DEBUGGING_PORT=9222 cypress open",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"licenses": "license-checker --production --onlyAllow 'Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;Public Domain;Unlicense;UNLICENSED;ODC-By-1.0;WTFPL;MPL-2.0'",
|
"licenses": "license-checker --production --onlyAllow 'Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;Public Domain;Unlicense;UNLICENSED;ODC-By-1.0;WTFPL;MPL-2.0'",
|
||||||
|
|
|
@ -5,7 +5,8 @@ import maplibregl, {LngLatBoundsLike,
|
||||||
NavigationControl,
|
NavigationControl,
|
||||||
PopupOptions,
|
PopupOptions,
|
||||||
Popup,
|
Popup,
|
||||||
LngLatLike} from 'maplibre-gl';
|
LngLatLike,
|
||||||
|
MapboxGeoJSONFeature} from 'maplibre-gl';
|
||||||
import mapStyle from '../data/mapStyle';
|
import mapStyle from '../data/mapStyle';
|
||||||
import ZoomWarning from './zoomWarning';
|
import ZoomWarning from './zoomWarning';
|
||||||
import PopupContent from './popupContent';
|
import PopupContent from './popupContent';
|
||||||
|
@ -25,6 +26,7 @@ type ClickEvent = maplibregl.MapMouseEvent & maplibregl.EventData;
|
||||||
const J40Map = () => {
|
const J40Map = () => {
|
||||||
const mapContainer = React.useRef<HTMLDivElement>(null);
|
const mapContainer = React.useRef<HTMLDivElement>(null);
|
||||||
const mapRef = useRef<Map>() as React.MutableRefObject<Map>;
|
const mapRef = useRef<Map>() as React.MutableRefObject<Map>;
|
||||||
|
const selectedFeature = useRef<MapboxGeoJSONFeature>();
|
||||||
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
|
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -55,6 +57,20 @@ const J40Map = () => {
|
||||||
initialMap.addControl(new NavigationControl({showCompass: false}), 'top-left');
|
initialMap.addControl(new NavigationControl({showCompass: false}), 'top-left');
|
||||||
mapRef.current = initialMap;
|
mapRef.current = initialMap;
|
||||||
}, []);
|
}, []);
|
||||||
|
const setMapSelected = (feature:MapboxGeoJSONFeature, isSelected:boolean) : void => {
|
||||||
|
// The below can be confirmed during debug with:
|
||||||
|
// mapRef.current.getFeatureState({"id":feature.id, "source":feature.source, "sourceLayer":feature.sourceLayer})
|
||||||
|
mapRef.current.setFeatureState({
|
||||||
|
source: feature.source,
|
||||||
|
sourceLayer: feature.sourceLayer,
|
||||||
|
id: feature.id,
|
||||||
|
}, {selected: isSelected});
|
||||||
|
if (isSelected) {
|
||||||
|
selectedFeature.current = feature;
|
||||||
|
} else {
|
||||||
|
selectedFeature.current = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClick = (e: ClickEvent) => {
|
const handleClick = (e: ClickEvent) => {
|
||||||
const map = e.target;
|
const map = e.target;
|
||||||
|
@ -62,10 +78,16 @@ const J40Map = () => {
|
||||||
const features = map.queryRenderedFeatures(clickedCoord, {
|
const features = map.queryRenderedFeatures(clickedCoord, {
|
||||||
layers: ['score'],
|
layers: ['score'],
|
||||||
});
|
});
|
||||||
|
const feature = features && features[0];
|
||||||
if (features.length && features[0].properties) {
|
if (feature) {
|
||||||
const placeholder = document.createElement('div');
|
const placeholder = document.createElement('div');
|
||||||
ReactDOM.render(<PopupContent properties={features[0].properties} />, placeholder);
|
// If we've selected a new feature, set 'selected' to false
|
||||||
|
if (selectedFeature.current && feature.id !== selectedFeature.current.id) {
|
||||||
|
setMapSelected(selectedFeature.current, false);
|
||||||
|
}
|
||||||
|
setMapSelected(feature, true);
|
||||||
|
|
||||||
|
ReactDOM.render(<PopupContent properties={feature.properties} />, placeholder);
|
||||||
const options : PopupOptions = {
|
const options : PopupOptions = {
|
||||||
offset: [0, 0],
|
offset: [0, 0],
|
||||||
className: styles.j40Popup,
|
className: styles.j40Popup,
|
||||||
|
@ -74,6 +96,9 @@ const J40Map = () => {
|
||||||
new Popup(options)
|
new Popup(options)
|
||||||
.setLngLat(e.lngLat)
|
.setLngLat(e.lngLat)
|
||||||
.setDOMContent(placeholder)
|
.setDOMContent(placeholder)
|
||||||
|
.on('close', function() {
|
||||||
|
setMapSelected(feature, false);
|
||||||
|
})
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as constants from '../data/constants';
|
import * as constants from '../data/constants';
|
||||||
import * as styles from './popupContent.module.scss';
|
import * as styles from './popupContent.module.scss';
|
||||||
|
import {GeoJsonProperties} from 'geojson';
|
||||||
|
|
||||||
interface IPopupContentProps {
|
interface IPopupContentProps {
|
||||||
properties: constants.J40Properties,
|
properties: GeoJsonProperties,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const PopupContent = ({properties}:IPopupContentProps) => {
|
const PopupContent = ({properties}:IPopupContentProps) => {
|
||||||
const readablePercent = (percent: number) => {
|
const readablePercent = (percent: number) => {
|
||||||
return `${(percent * 100).toFixed(2)}`;
|
return `${(percent * 100).toFixed(2)}`;
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
|
// URLS
|
||||||
|
export const FEATURE_TILE_BASE_URL = 'https://d2zjid6n5ja2pt.cloudfront.net/0629_demo';
|
||||||
|
|
||||||
|
// Performance markers
|
||||||
|
export const PERFORMANCE_MARKER_MAP_IDLE = 'MAP_IDLE';
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
export const SCORE_PROPERTY = 'Score D (percentile)';
|
export const SCORE_PROPERTY = 'Score D (percentile)';
|
||||||
export const GEOID_PROPERTY = 'GEOID10';
|
export const GEOID_PROPERTY = 'GEOID10';
|
||||||
|
export const SCORE_SOURCE_NAME = 'score';
|
||||||
|
// The name of the layer within the tiles that contains the score
|
||||||
|
export const SCORE_SOURCE_LAYER = 'blocks';
|
||||||
|
|
||||||
export type J40Properties = { [key: string]: any };
|
export type J40Properties = { [key: string]: any };
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,3 +73,4 @@ export const DEFAULT_OUTLINE_COLOR = '#4EA5CF';
|
||||||
export const MIN_COLOR = '#FFFFFF';
|
export const MIN_COLOR = '#FFFFFF';
|
||||||
export const MED_COLOR = '#D1DAE6';
|
export const MED_COLOR = '#D1DAE6';
|
||||||
export const MAX_COLOR = '#768FB3';
|
export const MAX_COLOR = '#768FB3';
|
||||||
|
export const BORDER_HIGHLIGHT_COLOR = '#00BDE3';
|
||||||
|
|
|
@ -63,8 +63,11 @@ const mapStyle : Style = {
|
||||||
},
|
},
|
||||||
'score': {
|
'score': {
|
||||||
'type': 'vector',
|
'type': 'vector',
|
||||||
|
// Our current tippecanoe command does not set an id.
|
||||||
|
// The below line promotes the GEOID10 property to the ID
|
||||||
|
'promoteId': 'GEOID10',
|
||||||
'tiles': [
|
'tiles': [
|
||||||
'https://d2zjid6n5ja2pt.cloudfront.net/0629_demo/{z}/{x}/{y}.pbf',
|
`${constants.FEATURE_TILE_BASE_URL}/{z}/{x}/{y}.pbf`,
|
||||||
// For local development, use:
|
// For local development, use:
|
||||||
// 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf',
|
// 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf',
|
||||||
],
|
],
|
||||||
|
@ -98,8 +101,8 @@ const mapStyle : Style = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': 'score',
|
'id': 'score',
|
||||||
'source': 'score',
|
'source': constants.SCORE_SOURCE_NAME,
|
||||||
'source-layer': 'blocks',
|
'source-layer': constants.SCORE_SOURCE_LAYER,
|
||||||
'type': 'fill',
|
'type': 'fill',
|
||||||
'filter': ['all',
|
'filter': ['all',
|
||||||
['>', constants.SCORE_PROPERTY, 0.6],
|
['>', constants.SCORE_PROPERTY, 0.6],
|
||||||
|
@ -117,7 +120,7 @@ const mapStyle : Style = {
|
||||||
{
|
{
|
||||||
'id': 'score-highlights',
|
'id': 'score-highlights',
|
||||||
'source': 'score',
|
'source': 'score',
|
||||||
'source-layer': 'blocks',
|
'source-layer': constants.SCORE_SOURCE_LAYER,
|
||||||
'type': 'line',
|
'type': 'line',
|
||||||
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
|
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
|
||||||
'layout': {
|
'layout': {
|
||||||
|
@ -131,6 +134,24 @@ const mapStyle : Style = {
|
||||||
'line-opacity': 0.5,
|
'line-opacity': 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// This layer queries the feature-state property "selected" and
|
||||||
|
// highlights the border of the selected region if true
|
||||||
|
'id': 'score-border-highlight',
|
||||||
|
'type': 'line',
|
||||||
|
'source': 'score',
|
||||||
|
'source-layer': constants.SCORE_SOURCE_LAYER,
|
||||||
|
'layout': {},
|
||||||
|
'paint': {
|
||||||
|
'line-color': constants.BORDER_HIGHLIGHT_COLOR,
|
||||||
|
'line-width': [
|
||||||
|
'case',
|
||||||
|
['boolean', ['feature-state', 'selected'], false],
|
||||||
|
5.0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'id': 'labels-only',
|
'id': 'labels-only',
|
||||||
'type': 'raster',
|
'type': 'raster',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue