mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-02-23 10:04:18 -08:00
Zoom fade for higher zoom levels (#265)
This commit is contained in:
parent
f9ffe305b2
commit
92efc5c937
21 changed files with 1617 additions and 1348 deletions
1896
client/package-lock.json
generated
1896
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -32,7 +32,9 @@
|
||||||
"@testing-library/cypress": "^7.0.6",
|
"@testing-library/cypress": "^7.0.6",
|
||||||
"@testing-library/jest-dom": "^5.12.0",
|
"@testing-library/jest-dom": "^5.12.0",
|
||||||
"@testing-library/react": "^11.2.7",
|
"@testing-library/react": "^11.2.7",
|
||||||
|
"@types/chroma-js": "^2.1.3",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
|
"@types/mapbox-gl": "^2.3.0",
|
||||||
"@types/node": "^15.3.1",
|
"@types/node": "^15.3.1",
|
||||||
"@types/ol": "^6.5.1",
|
"@types/ol": "^6.5.1",
|
||||||
"@types/react": "^17.0.1",
|
"@types/react": "^17.0.1",
|
||||||
|
@ -68,7 +70,9 @@
|
||||||
"ts-jest": "^27.0.0"
|
"ts-jest": "^27.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trussworks/react-uswds": "github:nathillardusds/react-uswds#nathillardusds/ssr",
|
"@trussworks/react-uswds": "git+https://www.github.com/nathillardusds/react-uswds#nathillardusds/ssr",
|
||||||
|
"chroma-js": "^2.1.2",
|
||||||
|
"mapbox-gl": "^1.13.0",
|
||||||
"ol": "^6.5.0",
|
"ol": "^6.5.0",
|
||||||
"ol-mapbox-style": "^6.3.2",
|
"ol-mapbox-style": "^6.3.2",
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.0",
|
||||||
|
|
|
@ -1,301 +0,0 @@
|
||||||
import React, {useState, useEffect, useRef} from 'react';
|
|
||||||
import Map from 'ol/Map';
|
|
||||||
import View from 'ol/View';
|
|
||||||
import Feature, {FeatureLike} from 'ol/Feature';
|
|
||||||
import Geometry from 'ol/geom/Geometry';
|
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
|
||||||
import VectorSource from 'ol/source/Vector';
|
|
||||||
import {fromLonLat} from 'ol/proj';
|
|
||||||
import * as styles from './map.module.scss';
|
|
||||||
import olms from 'ol-mapbox-style';
|
|
||||||
import Overlay from 'ol/Overlay';
|
|
||||||
// import {Table} from '@trussworks/react-uswds';
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import zoomIcon from '/node_modules/uswds/dist/img/usa-icons/zoom_in.svg';
|
|
||||||
|
|
||||||
const mapConfig = {
|
|
||||||
'version': 8,
|
|
||||||
'cursor': 'pointer',
|
|
||||||
'sources': {
|
|
||||||
'carto-light': {
|
|
||||||
'type': 'raster',
|
|
||||||
'tiles': [
|
|
||||||
'https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
|
||||||
'https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
|
||||||
'https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
|
||||||
'https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'custom': {
|
|
||||||
'type': 'vector',
|
|
||||||
'tiles': [
|
|
||||||
'http://usds-geoplatform-justice40-website.s3-website-us-east-1.amazonaws.com/0624_demo/{z}/{x}/{y}.pbf',
|
|
||||||
// For local development, use:
|
|
||||||
// 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'labels': {
|
|
||||||
'type': 'raster',
|
|
||||||
'tiles': [
|
|
||||||
'https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
|
||||||
'https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
|
||||||
'https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
|
||||||
'https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'layers': [
|
|
||||||
{
|
|
||||||
'id': 'carto-light-layer',
|
|
||||||
'source': 'carto-light',
|
|
||||||
'type': 'raster',
|
|
||||||
'minzoom': 0,
|
|
||||||
'maxzoom': 22,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'blocks',
|
|
||||||
'type': 'fill',
|
|
||||||
'source': 'custom',
|
|
||||||
'source-layer': 'blocks',
|
|
||||||
'minzoom': 0,
|
|
||||||
'layout': {
|
|
||||||
'line-cap': 'round',
|
|
||||||
'line-join': 'round',
|
|
||||||
},
|
|
||||||
// 01=AL, 30=MT, 34=NJ, 35=NM, 36=NY
|
|
||||||
'filter': ['in', 'STATEFP10', '01', '30', '34', '35', '36'],
|
|
||||||
'paint': {
|
|
||||||
'fill-color': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['to-number', [
|
|
||||||
'get',
|
|
||||||
'Score C (percentile)',
|
|
||||||
]],
|
|
||||||
0.0,
|
|
||||||
'white',
|
|
||||||
1,
|
|
||||||
'blue',
|
|
||||||
],
|
|
||||||
'fill-opacity': 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'labels-only',
|
|
||||||
'type': 'raster',
|
|
||||||
'source': 'labels',
|
|
||||||
'minzoom': 0,
|
|
||||||
'maxzoom': 22,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IMapWrapperProps {
|
|
||||||
features: Feature<Geometry>[],
|
|
||||||
};
|
|
||||||
|
|
||||||
// The below adapted from
|
|
||||||
// https://taylor.callsen.me/using-openlayers-with-react-functional-components/
|
|
||||||
const MapWrapper = ({features}: IMapWrapperProps) => {
|
|
||||||
const [map, setMap] = useState<Map>();
|
|
||||||
const [featuresLayer, setFeaturesLayer] = useState<VectorLayer>();
|
|
||||||
const [currentOverlay, setCurrentOverlay] = useState<Overlay>();
|
|
||||||
const [selectedFeature, setSelectedFeature] = useState<FeatureLike>();
|
|
||||||
const [currentZoom, setCurrentZoom] = useState<number>(0);
|
|
||||||
|
|
||||||
const mapElement = useRef() as
|
|
||||||
React.MutableRefObject<HTMLInputElement>;
|
|
||||||
|
|
||||||
// create state ref that can be accessed in OpenLayers onclick callback function
|
|
||||||
// https://stackoverflow.com/a/60643670
|
|
||||||
const mapRef = useRef() as React.MutableRefObject<Map>;
|
|
||||||
if (map) {
|
|
||||||
mapRef.current = map;
|
|
||||||
}
|
|
||||||
|
|
||||||
const popupContainer = React.useRef<HTMLDivElement>(null);
|
|
||||||
const popupCloser = React.useRef<HTMLAnchorElement>(null);
|
|
||||||
const popupContent = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const overlayRef = useRef() as React.MutableRefObject<Overlay>;
|
|
||||||
overlayRef.current = currentOverlay!;
|
|
||||||
|
|
||||||
useEffect( () => {
|
|
||||||
// create and add initial vector source layer, to be replaced layer
|
|
||||||
const initialFeaturesLayer = new VectorLayer({
|
|
||||||
source: new VectorSource(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!popupCloser || !popupContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
popupCloser.current!.onclick = function() {
|
|
||||||
overlay.setPosition(undefined);
|
|
||||||
popupCloser.current!.blur();
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const overlay = new Overlay({
|
|
||||||
// Using the non-null assertion operator as we check for null above
|
|
||||||
element: popupContainer.current!,
|
|
||||||
autoPan: true,
|
|
||||||
autoPanAnimation: {
|
|
||||||
duration: 250,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const initialMap = new Map({
|
|
||||||
target: mapElement.current,
|
|
||||||
view: new View({
|
|
||||||
center: fromLonLat([-86.502136, 32.4687126]),
|
|
||||||
zoom: 4,
|
|
||||||
}),
|
|
||||||
controls: [],
|
|
||||||
overlays: [overlay],
|
|
||||||
});
|
|
||||||
const currentZoom = Math.floor(initialMap.getView().getZoom());
|
|
||||||
|
|
||||||
initialMap.on('moveend', handleMoveEnd);
|
|
||||||
initialMap.on('click', handleMapClick);
|
|
||||||
setMap(initialMap);
|
|
||||||
setCurrentZoom(currentZoom);
|
|
||||||
setCurrentOverlay(overlay);
|
|
||||||
setFeaturesLayer(initialFeaturesLayer);
|
|
||||||
olms(initialMap, mapConfig);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// update map if features prop changes
|
|
||||||
useEffect( () => {
|
|
||||||
if (features.length) { // may be empty on first render
|
|
||||||
// set features to map
|
|
||||||
featuresLayer?.setSource(
|
|
||||||
new VectorSource({
|
|
||||||
features: features,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const extent = featuresLayer?.getSource().getExtent();
|
|
||||||
if (extent != null) {
|
|
||||||
// fit map to feature extent (with 100px of padding)
|
|
||||||
map?.getView().fit(extent, {
|
|
||||||
padding: [100, 100, 100, 100],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [features]);
|
|
||||||
|
|
||||||
const handleMapClick = (event: { pixel: any; }) => {
|
|
||||||
const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);
|
|
||||||
|
|
||||||
mapRef.current.forEachFeatureAtPixel(event.pixel, (feature) => {
|
|
||||||
setSelectedFeature(feature);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
overlayRef.current.setPosition(clickedCoord);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMoveEnd = () => {
|
|
||||||
const newZoom = Math.floor(mapRef.current.getView().getZoom()!);
|
|
||||||
if (currentZoom != newZoom) {
|
|
||||||
setCurrentZoom(newZoom);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const readablePercent = (percent: number) => {
|
|
||||||
return `${(percent * 100).toFixed(2)}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
let properties;
|
|
||||||
if (selectedFeature) {
|
|
||||||
properties = selectedFeature.getProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCategorization = (percentile: number) => {
|
|
||||||
let categorization = '';
|
|
||||||
if (percentile >= 0.75 ) {
|
|
||||||
categorization = 'Prioritized';
|
|
||||||
} else if (0.60 <= percentile && percentile < 0.75) {
|
|
||||||
categorization = 'Threshold';
|
|
||||||
} else {
|
|
||||||
categorization = 'Non-prioritized';
|
|
||||||
}
|
|
||||||
return categorization;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTitleContent = (properties: { [key: string]: any; }) => {
|
|
||||||
const blockGroup = properties['GEOID10'];
|
|
||||||
const score = properties['Score C (percentile)'];
|
|
||||||
return (
|
|
||||||
<table className={styles.popupHeaderTable}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Census Block Group:</strong></td>
|
|
||||||
<td>{blockGroup}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Just Progress Categorization:</strong></td>
|
|
||||||
<td>{getCategorization(score)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Cumulative Index Score:</strong></td>
|
|
||||||
<td>{readablePercent(score)}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// const propertyTest = (propertyName:string) => {
|
|
||||||
// // Filter out properties in all caps
|
|
||||||
// return !propertyName.match(/^[A-Z0-9]+$/);
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.mapWrapperContainer}>
|
|
||||||
<div ref={mapElement} className={styles.mapContainer} />
|
|
||||||
|
|
||||||
<div ref={popupContainer} className={styles.popupContainer}>
|
|
||||||
<a href="#" ref={popupCloser} className={styles.popupCloser}></a>
|
|
||||||
<div ref={popupContent} className={styles.popupContent}>
|
|
||||||
{(selectedFeature && properties) ?
|
|
||||||
<div>
|
|
||||||
{getTitleContent(properties)}
|
|
||||||
{/* <Table bordered={false}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Indicator</th>
|
|
||||||
<th scope="col">Percentile(0-100)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{
|
|
||||||
Object.keys(properties).map((key, index) => (
|
|
||||||
(propertyTest(key)) &&
|
|
||||||
<tr key={key} >
|
|
||||||
<td>{key}</td>
|
|
||||||
<td>{properties[key]}</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</Table> */}
|
|
||||||
</div> :
|
|
||||||
''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ currentZoom < 5 ?
|
|
||||||
<div className={styles.zoomWarning}>
|
|
||||||
<img
|
|
||||||
src={zoomIcon} alt={'zoom icon'}/>
|
|
||||||
Zoom in to the state or regional level to see prioritized communities on the map.
|
|
||||||
</div> :
|
|
||||||
''
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MapWrapper;
|
|
|
@ -1,10 +1,3 @@
|
||||||
.mapContainer {
|
|
||||||
height: 676px;
|
|
||||||
margin-bottom: 29px;
|
|
||||||
max-width: revert;
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popupContainer {
|
.popupContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
@ -15,6 +8,7 @@
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
left: -50px;
|
left: -50px;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupContainer:after,
|
.popupContainer:after,
|
||||||
|
@ -55,31 +49,4 @@
|
||||||
|
|
||||||
.popupContent {
|
.popupContent {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popupHeaderTable {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
font: normal 12px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popupHeaderTable tbody td:first {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoomWarning {
|
|
||||||
background-color: #953a10;
|
|
||||||
height: 8%;
|
|
||||||
width: 66%;
|
|
||||||
margin: auto;
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoomWarning > img {
|
|
||||||
filter: invert(100%);
|
|
||||||
}
|
}
|
14
client/src/components/mapPopup.module.scss.d.ts
vendored
Normal file
14
client/src/components/mapPopup.module.scss.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
declare namespace MapControlModuleScssNamespace {
|
||||||
|
export interface IMapControlModuleScss {
|
||||||
|
popupContainer: string;
|
||||||
|
popupCloser: string;
|
||||||
|
popupContent: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const MapControlModuleScssModule: MapControlModuleScssNamespace.IMapControlModuleScss & {
|
||||||
|
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||||
|
locals: MapControlModuleScssNamespace.IMapControlModuleScss;
|
||||||
|
};
|
||||||
|
|
||||||
|
export = MapControlModuleScssModule;
|
58
client/src/components/mapPopup.tsx
Normal file
58
client/src/components/mapPopup.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import React, {useRef, useEffect, useState} from 'react';
|
||||||
|
import * as styles from './mapPopup.module.scss';
|
||||||
|
import Overlay from 'ol/Overlay';
|
||||||
|
import {Coordinate} from 'ol/Coordinate';
|
||||||
|
import Map from 'ol/Map';
|
||||||
|
import {FeatureLike} from 'ol/Feature';
|
||||||
|
import PopupContent from './popupContent';
|
||||||
|
|
||||||
|
interface IMapPopupProps {
|
||||||
|
map: Map,
|
||||||
|
selectedFeature: FeatureLike;
|
||||||
|
position: Coordinate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MapPopup = ({map, selectedFeature, position}: IMapPopupProps) => {
|
||||||
|
const popupContainerElement = useRef<HTMLDivElement>(null);
|
||||||
|
const popupCloserElement = useRef<HTMLAnchorElement>(null);
|
||||||
|
const popupContentElement = useRef<HTMLDivElement>(null);
|
||||||
|
const [currentOverlay, setCurrentOverlay] = useState<Overlay>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
popupCloserElement.current!.onclick = function() {
|
||||||
|
overlay.setPosition(undefined);
|
||||||
|
popupCloserElement.current!.blur();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const overlay = new Overlay({
|
||||||
|
element: popupContainerElement.current!,
|
||||||
|
autoPan: true,
|
||||||
|
autoPanAnimation: {
|
||||||
|
duration: 250,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setCurrentOverlay(overlay);
|
||||||
|
map.addOverlay(overlay);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if (position && currentOverlay && selectedFeature) { // may be empty on first render
|
||||||
|
currentOverlay.setPosition(position);
|
||||||
|
}
|
||||||
|
}, [position]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={popupContainerElement}
|
||||||
|
className={styles.popupContainer}>
|
||||||
|
<a href="#" ref={popupCloserElement} className={styles.popupCloser}></a>
|
||||||
|
<div ref={popupContentElement} className={styles.popupContent}>
|
||||||
|
<PopupContent properties={selectedFeature?.getProperties()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapPopup;
|
22
client/src/components/mapWrapper.tsx
Normal file
22
client/src/components/mapWrapper.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useFlags} from '../contexts/FlagContext';
|
||||||
|
import MapboxMap from './mapboxMap';
|
||||||
|
import OpenLayersMap from './openlayersMap';
|
||||||
|
import * as constants from '../data/constants';
|
||||||
|
|
||||||
|
const MapWrapper = () => {
|
||||||
|
const flags = useFlags();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
flags.includes('mb') ?
|
||||||
|
<MapboxMap /> :
|
||||||
|
<OpenLayersMap features={[]}/>
|
||||||
|
}
|
||||||
|
<p>Current Score Property: {constants.SCORE_PROPERTY}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default MapWrapper;
|
33
client/src/components/mapboxMap.module.scss
Normal file
33
client/src/components/mapboxMap.module.scss
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
$sidebar-background: rgba(35, 55, 75, 0.9);
|
||||||
|
$sidebar-color: #ffffff;
|
||||||
|
|
||||||
|
.mapContainer {
|
||||||
|
height: 676px;
|
||||||
|
margin-bottom: 29px;
|
||||||
|
max-width: revert;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: $sidebar-background;
|
||||||
|
color: $sidebar-color;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 300px;
|
||||||
|
left: 0;
|
||||||
|
margin: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.j40Popup {
|
||||||
|
max-height: 300px;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: scroll;
|
||||||
|
pointer-events: all !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.j40Popup .mapboxgl-popup-content {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
14
client/src/components/mapboxMap.module.scss.d.ts
vendored
Normal file
14
client/src/components/mapboxMap.module.scss.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
declare namespace MapboxMapModuleScssNamespace {
|
||||||
|
export interface IMapboxMapModuleScss {
|
||||||
|
sidebar: string;
|
||||||
|
mapContainer: string;
|
||||||
|
j40Popup: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const MapboxMapModuleScssModule: MapboxMapModuleScssNamespace.IMapboxMapModuleScss & {
|
||||||
|
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||||
|
locals: MapboxMapModuleScssNamespace.IMapboxMapModuleScss;
|
||||||
|
};
|
||||||
|
|
||||||
|
export = MapboxMapModuleScssModule;
|
85
client/src/components/mapboxMap.tsx
Normal file
85
client/src/components/mapboxMap.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
import React, {useRef, useEffect, useState} from 'react';
|
||||||
|
import {LngLatBoundsLike,
|
||||||
|
Map,
|
||||||
|
NavigationControl,
|
||||||
|
PopupOptions,
|
||||||
|
Popup,
|
||||||
|
LngLatLike} from 'mapbox-gl';
|
||||||
|
import mapStyle from '../data/mapStyle';
|
||||||
|
import ZoomWarning from './zoomWarning';
|
||||||
|
import PopupContent from './popupContent';
|
||||||
|
import * as styles from './mapboxMap.module.scss';
|
||||||
|
import * as constants from '../data/constants';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
|
type ClickEvent = mapboxgl.MapMouseEvent & mapboxgl.EventData;
|
||||||
|
|
||||||
|
const MapboxMap = () => {
|
||||||
|
const mapContainer = React.useRef<HTMLDivElement>(null);
|
||||||
|
const map = useRef<Map>() as React.MutableRefObject<Map>;
|
||||||
|
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only initialize once
|
||||||
|
if (map.current && mapContainer.current) return;
|
||||||
|
|
||||||
|
const initialMap = new Map({
|
||||||
|
container: mapContainer.current!,
|
||||||
|
style: mapStyle,
|
||||||
|
center: constants.DEFAULT_CENTER as LngLatLike,
|
||||||
|
zoom: zoom,
|
||||||
|
minZoom: constants.GLOBAL_MIN_ZOOM,
|
||||||
|
maxZoom: constants.GLOBAL_MAX_ZOOM,
|
||||||
|
maxBounds: constants.GLOBAL_MAX_BOUNDS as LngLatBoundsLike,
|
||||||
|
});
|
||||||
|
initialMap.on('click', handleClick);
|
||||||
|
initialMap.addControl(new NavigationControl());
|
||||||
|
map.current = initialMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = (e: ClickEvent) => {
|
||||||
|
const map = e.target;
|
||||||
|
const clickedCoord = e.point;
|
||||||
|
const features = map.queryRenderedFeatures(clickedCoord, {
|
||||||
|
layers: ['score-low'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (features.length && features[0].properties) {
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
ReactDOM.render(<PopupContent properties={features[0].properties} />, placeholder);
|
||||||
|
const options : PopupOptions = {
|
||||||
|
offset: [0, 0],
|
||||||
|
className: styles.j40Popup,
|
||||||
|
};
|
||||||
|
new Popup(options)
|
||||||
|
.setLngLat(e.lngLat)
|
||||||
|
.setDOMContent(placeholder)
|
||||||
|
.setMaxWidth('300px')
|
||||||
|
.addTo(map);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map.current) return; // wait for map to initialize
|
||||||
|
map.current.on('move', () => {
|
||||||
|
setZoom(map.current.getZoom());
|
||||||
|
});
|
||||||
|
map.current.on('mouseenter', 'score-low', () => {
|
||||||
|
map.current.getCanvas().style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
map.current.on('mouseleave', 'score-low', () => {
|
||||||
|
map.current.getCanvas().style.cursor = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div ref={mapContainer} className={styles.mapContainer}/>
|
||||||
|
<ZoomWarning zoomLevel={zoom} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapboxMap;
|
6
client/src/components/openlayersMap.module.scss
Normal file
6
client/src/components/openlayersMap.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.mapContainer {
|
||||||
|
height: 676px;
|
||||||
|
margin-bottom: 29px;
|
||||||
|
max-width: revert;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
|
@ -1,12 +1,6 @@
|
||||||
declare namespace MapModuleScssNamespace {
|
declare namespace MapModuleScssNamespace {
|
||||||
export interface IMapModuleScss {
|
export interface IMapModuleScss {
|
||||||
mapContainer: string;
|
mapContainer:string;
|
||||||
popupContainer: string;
|
|
||||||
popupCloser: string;
|
|
||||||
popupContent: string;
|
|
||||||
popupHeaderTable: string;
|
|
||||||
zoomWarning:string;
|
|
||||||
mapWrapperContainer:string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
132
client/src/components/openlayersMap.tsx
Normal file
132
client/src/components/openlayersMap.tsx
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import React, {useState, useEffect, useRef} from 'react';
|
||||||
|
import Map from 'ol/Map';
|
||||||
|
import View from 'ol/View';
|
||||||
|
import Feature, {FeatureLike} from 'ol/Feature';
|
||||||
|
import Geometry from 'ol/geom/Geometry';
|
||||||
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
|
import VectorSource from 'ol/source/Vector';
|
||||||
|
import {fromLonLat} from 'ol/proj';
|
||||||
|
import {Coordinate} from 'ol/coordinate';
|
||||||
|
import olms from 'ol-mapbox-style';
|
||||||
|
import mapStyle from '../data/mapStyle';
|
||||||
|
import ZoomWarning from './zoomWarning';
|
||||||
|
import MapPopup from './mapPopup';
|
||||||
|
import {transformExtent} from 'ol/src/proj';
|
||||||
|
import * as styles from './openlayersMap.module.scss';
|
||||||
|
import * as constants from '../data/constants';
|
||||||
|
import {Extent} from 'ol/src/extent';
|
||||||
|
|
||||||
|
interface IMapWrapperProps {
|
||||||
|
features: Feature<Geometry>[],
|
||||||
|
}
|
||||||
|
|
||||||
|
// The below adapted from
|
||||||
|
// https://taylor.callsen.me/using-openlayers-with-react-functional-components/
|
||||||
|
const MapWrapper = ({features}: IMapWrapperProps) => {
|
||||||
|
const [map, setMap] = useState<Map>();
|
||||||
|
const [featuresLayer, setFeaturesLayer] = useState<VectorLayer>();
|
||||||
|
const [selectedFeature, setSelectedFeature] = useState<FeatureLike>();
|
||||||
|
const [currentZoom, setCurrentZoom] = useState<number>(4);
|
||||||
|
const [currentOverlayPosition, setCurrentOverlayPosition] = useState<Coordinate>([]);
|
||||||
|
|
||||||
|
const mapElement = useRef() as
|
||||||
|
React.MutableRefObject<HTMLInputElement>;
|
||||||
|
|
||||||
|
// create state ref that can be accessed in OpenLayers onclick callback function
|
||||||
|
// https://stackoverflow.com/a/60643670
|
||||||
|
const mapRef = useRef() as React.MutableRefObject<Map>;
|
||||||
|
if (map) {
|
||||||
|
mapRef.current = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = (extent: Extent) : Extent => {
|
||||||
|
return transformExtent(extent, 'EPSG:4326', 'EPSG:3857');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
const view = new View({
|
||||||
|
center: fromLonLat(constants.DEFAULT_CENTER),
|
||||||
|
zoom: 4,
|
||||||
|
maxZoom: constants.GLOBAL_MAX_ZOOM,
|
||||||
|
minZoom: constants.GLOBAL_MIN_ZOOM,
|
||||||
|
extent: transform(constants.GLOBAL_MAX_BOUNDS.flat()) as [number, number, number, number],
|
||||||
|
});
|
||||||
|
|
||||||
|
// create and add initial vector source layer, to be replaced layer
|
||||||
|
const initialFeaturesLayer = new VectorLayer({
|
||||||
|
source: new VectorSource(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialMap = new Map({
|
||||||
|
target: mapElement.current,
|
||||||
|
view: view,
|
||||||
|
controls: [],
|
||||||
|
});
|
||||||
|
const currentZoom = Math.floor(initialMap.getView().getZoom() || constants.GLOBAL_MIN_ZOOM);
|
||||||
|
|
||||||
|
initialMap.on('moveend', handleMoveEnd);
|
||||||
|
initialMap.on('click', handleMapClick);
|
||||||
|
setMap(initialMap);
|
||||||
|
setCurrentZoom(currentZoom);
|
||||||
|
setFeaturesLayer(initialFeaturesLayer);
|
||||||
|
olms(initialMap, mapStyle);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// update map if features prop changes
|
||||||
|
useEffect( () => {
|
||||||
|
if (features.length) { // may be empty on first render
|
||||||
|
// set features to map
|
||||||
|
featuresLayer?.setSource(
|
||||||
|
new VectorSource({
|
||||||
|
features: features,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const extent = featuresLayer?.getSource().getExtent();
|
||||||
|
if (extent) {
|
||||||
|
// fit map to feature extent (with 100px of padding)
|
||||||
|
map?.getView().fit(extent, {
|
||||||
|
padding: [100, 100, 100, 100],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [features]);
|
||||||
|
|
||||||
|
const handleMapClick = (event: { pixel: any }) => {
|
||||||
|
const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);
|
||||||
|
|
||||||
|
let featureFound = false;
|
||||||
|
mapRef.current.forEachFeatureAtPixel(event.pixel, (feature) => {
|
||||||
|
featureFound = true;
|
||||||
|
setSelectedFeature(feature);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!featureFound) {
|
||||||
|
setSelectedFeature(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentOverlayPosition(clickedCoord);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveEnd = () => {
|
||||||
|
const newZoom = Math.floor(mapRef.current.getView().getZoom() || constants.GLOBAL_MIN_ZOOM);
|
||||||
|
if (currentZoom != newZoom) {
|
||||||
|
setCurrentZoom(newZoom);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={mapElement} className={styles.mapContainer}/>
|
||||||
|
{map?
|
||||||
|
<MapPopup selectedFeature={selectedFeature!} map={map!} position={currentOverlayPosition} /> :
|
||||||
|
''
|
||||||
|
}
|
||||||
|
<ZoomWarning zoomLevel={currentZoom} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapWrapper;
|
87
client/src/components/popupContent.tsx
Normal file
87
client/src/components/popupContent.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Table} from '@trussworks/react-uswds';
|
||||||
|
import * as constants from '../data/constants';
|
||||||
|
|
||||||
|
interface IPopupContentProps {
|
||||||
|
properties: constants.J40Properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PopupContent = ({properties}:IPopupContentProps) => {
|
||||||
|
const readablePercent = (percent: number) => {
|
||||||
|
return `${(percent * 100).toFixed(2)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategorization = (percentile: number) => {
|
||||||
|
let categorization;
|
||||||
|
if (percentile >= 0.75 ) {
|
||||||
|
categorization = 'Prioritized';
|
||||||
|
} else if (0.60 <= percentile && percentile < 0.75) {
|
||||||
|
categorization = 'Threshold';
|
||||||
|
} else {
|
||||||
|
categorization = 'Non-prioritized';
|
||||||
|
}
|
||||||
|
return categorization;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitleContent = (properties: constants.J40Properties) => {
|
||||||
|
const blockGroup = properties[constants.GEOID_PROPERTY];
|
||||||
|
const score = properties[constants.SCORE_PROPERTY] as number;
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Census Block Group:</strong></td>
|
||||||
|
<td>{blockGroup}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Just Progress Categorization:</strong></td>
|
||||||
|
<td>{getCategorization(score)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Cumulative Index Score:</strong></td>
|
||||||
|
<td>{readablePercent(score)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBodyContent = (properties: constants.J40Properties) => {
|
||||||
|
const rows = [];
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
// Filter out all caps
|
||||||
|
if (!key.match(/^[A-Z0-9]+$/)) {
|
||||||
|
rows.push(<tr key={key} >
|
||||||
|
<td>{key}</td>
|
||||||
|
<td>{value}</td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{properties ?
|
||||||
|
<div>
|
||||||
|
{getTitleContent(properties)}
|
||||||
|
<Table bordered={false}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Indicator</th>
|
||||||
|
<th scope="col">Percentile(0-100)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{getBodyContent(properties)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div> :
|
||||||
|
'' }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupContent;
|
17
client/src/components/zoomWarning.module.scss
Normal file
17
client/src/components/zoomWarning.module.scss
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.zoomWarning {
|
||||||
|
background-color: #953a10;
|
||||||
|
height: 5.5%;
|
||||||
|
width: 66%;
|
||||||
|
margin: auto;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoomWarning > img {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
13
client/src/components/zoomWarning.module.scss.d.ts
vendored
Normal file
13
client/src/components/zoomWarning.module.scss.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
declare namespace ZoomWarningModuleScssNamespace {
|
||||||
|
export interface IZoomWarningModuleScss {
|
||||||
|
zoomWarning: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const ZoomWarningModuleScssModule: ZoomWarningModuleScssNamespace.IZoomWarningModuleScss & {
|
||||||
|
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||||
|
locals: ZoomWarningModuleScssNamespace.IZoomWarningModuleScss;
|
||||||
|
};
|
||||||
|
|
||||||
|
export = ZoomWarningModuleScssModule;
|
||||||
|
|
26
client/src/components/zoomWarning.tsx
Normal file
26
client/src/components/zoomWarning.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as styles from './zoomWarning.module.scss';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import zoomIcon from '/node_modules/uswds/dist/img/usa-icons/zoom_in.svg';
|
||||||
|
|
||||||
|
interface IZoomWarningProps {
|
||||||
|
zoomLevel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZoomWarning = ({zoomLevel}: IZoomWarningProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{zoomLevel <= 5 ? (
|
||||||
|
<div className={styles.zoomWarning}>
|
||||||
|
<img src={zoomIcon} alt={'zoom icon'}/>
|
||||||
|
Zoom in to the state or regional level to see prioritized communities on the map.
|
||||||
|
</div>
|
||||||
|
) :
|
||||||
|
''
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZoomWarning;
|
11
client/src/data/constants.tsx
Normal file
11
client/src/data/constants.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const SCORE_PROPERTY = 'Score D (percentile)';
|
||||||
|
export const GEOID_PROPERTY = 'GEOID10';
|
||||||
|
export const GLOBAL_MIN_ZOOM = 3;
|
||||||
|
export const GLOBAL_MAX_ZOOM = 11;
|
||||||
|
export const GLOBAL_MIN_ZOOM_LOW = 3;
|
||||||
|
export const GLOBAL_MAX_ZOOM_LOW = 11;
|
||||||
|
export const GLOBAL_MIN_ZOOM_HIGH = 9;
|
||||||
|
export const GLOBAL_MAX_ZOOM_HIGH = 11;
|
||||||
|
export const GLOBAL_MAX_BOUNDS = [[-167.276413, 5.499550], [-52.233040, 83.162102]];
|
||||||
|
export const DEFAULT_CENTER = [32.4687126, -86.502136];
|
||||||
|
export type J40Properties = { [key: string]: any };
|
184
client/src/data/mapStyle.tsx
Normal file
184
client/src/data/mapStyle.tsx
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import {Style, FillPaint} from 'mapbox-gl';
|
||||||
|
import chroma from 'chroma-js';
|
||||||
|
import * as constants from '../data/constants';
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-jsdoc
|
||||||
|
function hexToHSLA(hex:string, alpha:number) {
|
||||||
|
return chroma(hex).alpha(alpha).css('hsl');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `MakePaint` generates a zoom-faded Mapbox 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
|
||||||
|
* @param {boolean} high : whether this is a "high" or "low" layer
|
||||||
|
* @return {FillPaint} a mapboxgl fill layer
|
||||||
|
**/
|
||||||
|
function makePaint({
|
||||||
|
field,
|
||||||
|
minRamp,
|
||||||
|
medRamp,
|
||||||
|
maxRamp,
|
||||||
|
high,
|
||||||
|
}: {
|
||||||
|
field: string;
|
||||||
|
minRamp: number;
|
||||||
|
medRamp: number;
|
||||||
|
maxRamp: number;
|
||||||
|
high: boolean;
|
||||||
|
}): FillPaint {
|
||||||
|
const minColor = 'white'; // '232, 88%, 100%';
|
||||||
|
const medColor = '#D1DAE6';
|
||||||
|
const maxColor = '#768FB3'; // '0, 98%, 56%';
|
||||||
|
return {
|
||||||
|
'fill-color': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['zoom'],
|
||||||
|
high ? 9 : 0,
|
||||||
|
[
|
||||||
|
'step',
|
||||||
|
['get', field],
|
||||||
|
hexToHSLA(minColor, high ? 0 : 0.5 ),
|
||||||
|
minRamp,
|
||||||
|
hexToHSLA(minColor, high ? 0 : 0.5 ),
|
||||||
|
medRamp,
|
||||||
|
hexToHSLA(medColor, high ? 0 : 0.5 ),
|
||||||
|
maxRamp,
|
||||||
|
hexToHSLA(maxColor, high ? 0 : 0.5 ),
|
||||||
|
],
|
||||||
|
high ? 11 : 9,
|
||||||
|
[
|
||||||
|
'step',
|
||||||
|
['get', field],
|
||||||
|
hexToHSLA(minColor, high ? 0.5 : 0.5 ),
|
||||||
|
minRamp,
|
||||||
|
hexToHSLA(minColor, high ? 0.5 : 0.5 ),
|
||||||
|
medRamp,
|
||||||
|
hexToHSLA(medColor, high ? 0.5 : 0.5 ),
|
||||||
|
maxRamp,
|
||||||
|
hexToHSLA(maxColor, high ? 0.5 : 0.5 ),
|
||||||
|
],
|
||||||
|
high ? 22 : 11,
|
||||||
|
[
|
||||||
|
'step',
|
||||||
|
['get', field],
|
||||||
|
hexToHSLA(minColor, high ? 0.5 : 0 ),
|
||||||
|
minRamp,
|
||||||
|
hexToHSLA(minColor, high ? 0.5 : 0 ),
|
||||||
|
medRamp,
|
||||||
|
hexToHSLA(medColor, high ? 0.5 : 0 ),
|
||||||
|
maxRamp,
|
||||||
|
hexToHSLA(maxColor, high ? 0.5 : 0 ),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStyle : Style = {
|
||||||
|
'version': 8,
|
||||||
|
'sources': {
|
||||||
|
'carto': {
|
||||||
|
'type': 'raster',
|
||||||
|
'tiles': [
|
||||||
|
'https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||||
|
'https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||||
|
'https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||||
|
'https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'geo': {
|
||||||
|
'type': 'raster',
|
||||||
|
'tiles': [
|
||||||
|
'https://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'custom': {
|
||||||
|
'type': 'vector',
|
||||||
|
'tiles': [
|
||||||
|
'https://d2zjid6n5ja2pt.cloudfront.net/0629_demo/{z}/{x}/{y}.pbf',
|
||||||
|
// For local development, use:
|
||||||
|
// 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'labels': {
|
||||||
|
'type': 'raster',
|
||||||
|
'tiles': [
|
||||||
|
'https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
||||||
|
'https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
||||||
|
'https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
||||||
|
'https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'layers': [
|
||||||
|
{
|
||||||
|
'id': 'carto',
|
||||||
|
'source': 'carto',
|
||||||
|
'type': 'raster',
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM - 1,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'geo',
|
||||||
|
'source': 'geo',
|
||||||
|
'type': 'raster',
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM - 1,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM + 1,
|
||||||
|
'layout': {
|
||||||
|
// Make the layer visible by default.
|
||||||
|
'visibility': 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'score-low',
|
||||||
|
'source': 'custom',
|
||||||
|
'source-layer': 'blocks',
|
||||||
|
'type': 'fill',
|
||||||
|
'filter': ['all',
|
||||||
|
['>', constants.SCORE_PROPERTY, 0.6],
|
||||||
|
// ['in', 'STATEFP10', '01', '30', '34', '35', '36'],
|
||||||
|
],
|
||||||
|
'paint': makePaint({
|
||||||
|
field: constants.SCORE_PROPERTY,
|
||||||
|
minRamp: 0,
|
||||||
|
medRamp: 0.6,
|
||||||
|
maxRamp: 0.75,
|
||||||
|
high: false,
|
||||||
|
}),
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM_LOW,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'score-high',
|
||||||
|
'source': 'custom',
|
||||||
|
'source-layer': 'blocks',
|
||||||
|
'type': 'fill',
|
||||||
|
'filter': ['all',
|
||||||
|
['>', constants.SCORE_PROPERTY, 0.6],
|
||||||
|
// ['in', 'STATEFP10', '01', '30', '34', '35', '36'],
|
||||||
|
],
|
||||||
|
'paint': makePaint({
|
||||||
|
field: constants.SCORE_PROPERTY,
|
||||||
|
minRamp: 0,
|
||||||
|
medRamp: 0.6,
|
||||||
|
maxRamp: 1.0,
|
||||||
|
high: true,
|
||||||
|
}),
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'labels-only',
|
||||||
|
'type': 'raster',
|
||||||
|
'source': 'labels',
|
||||||
|
'minzoom': constants.GLOBAL_MIN_ZOOM,
|
||||||
|
'maxzoom': constants.GLOBAL_MAX_ZOOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mapStyle;
|
|
@ -1,9 +1,8 @@
|
||||||
import React, {useState} from 'react';
|
import React from 'react';
|
||||||
import Layout from '../components/layout';
|
import Layout from '../components/layout';
|
||||||
import MapWrapper from '../components/map';
|
// import MapWrapper from '../components/map';
|
||||||
|
import MapWrapper from '../components/mapWrapper';
|
||||||
import HowYouCanHelp from '../components/HowYouCanHelp';
|
import HowYouCanHelp from '../components/HowYouCanHelp';
|
||||||
import Feature from 'ol/Feature';
|
|
||||||
import Geometry from 'ol/geom/Geometry';
|
|
||||||
import {Alert} from '@trussworks/react-uswds';
|
import {Alert} from '@trussworks/react-uswds';
|
||||||
import * as styles from './cejst.module.scss';
|
import * as styles from './cejst.module.scss';
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@ interface IMapPageProps {
|
||||||
const CEJSTPage = ({location}: IMapPageProps) => {
|
const CEJSTPage = ({location}: IMapPageProps) => {
|
||||||
// We temporarily removed MapControls, which would enable you to `setFeatures` also, for now
|
// We temporarily removed MapControls, which would enable you to `setFeatures` also, for now
|
||||||
// We will bring back later when we have interactive controls.
|
// We will bring back later when we have interactive controls.
|
||||||
const [features] = useState<Feature<Geometry>[]>([]);
|
|
||||||
return (
|
return (
|
||||||
<Layout location={location}>
|
<Layout location={location}>
|
||||||
<main id="main-content" role="main">
|
<main id="main-content" role="main">
|
||||||
|
@ -50,7 +48,7 @@ const CEJSTPage = ({location}: IMapPageProps) => {
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
<h2>Explore the Tool</h2>
|
<h2>Explore the Tool</h2>
|
||||||
<MapWrapper features={features} />
|
<MapWrapper/>
|
||||||
<HowYouCanHelp />
|
<HowYouCanHelp />
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["dom", "es2015", "es2017"],
|
"lib": ["dom", "es2015", "es2017", "es2019"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"removeComments": false,
|
"removeComments": false,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"ol": ["node_modules/ol/src"],
|
"ol": ["node_modules/ol/src"],
|
||||||
|
@ -22,8 +23,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*",
|
"./src/**/*ts",
|
||||||
"**/*.ts",
|
"./src/**/*tsx",
|
||||||
"node_modules/ol/**/*",
|
"node_modules/ol/**/*",
|
||||||
"node_modules/ol-mapbox-style/**/*"
|
"node_modules/ol-mapbox-style/**/*"
|
||||||
],
|
],
|
||||||
|
|
Loading…
Add table
Reference in a new issue