mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-02-23 18:14:19 -08:00
* Add basic accordion in AreaDetail * Refactor AreaDetail to use a Grid layout - adds useWindowSize to detect window resizes for mobile view - Map and AreaDetail to use Grid - removes some component styling from J40 - updates snapshot - MapWrapper to use Grid * Add custom Accordion styling - make J40 map a 9:3 Grid layout split - override native Accordion heading styles - make the Accordion multi-selectable - add some dummy data for indicators * Update AreaDetail to match design - remove styles in AreaDetail - increase height of MapInfoPanel - add Accordian items (indicators) - updates snapshot * Add a Beta Tag to the logo * Change the line height on indicators descriptions * Update package-lock after the rebase * Remove threshold from MapLegend - move feature selected border color to utils - remove all tooltip logic - remove all styles associated with tooltips - add legend label and descript to constants - refactor tests to be snapshots * Add borders between additional indicators * Modify copy and update styles - add the ordinal superscript back - update the copy - update the snapshots * Add additional indicators keys * Connect indicator keys to the UI - update the areaDetail snapshot * Render additional indicators accordion open onLoad - update snapshot * Update copy on About page * Update copy on indicator descriptions - update snapshots * Update the "How you can help section" - update the snapshot * Add a comma to "ZIP file will contain..." * Add the Datasets section to the methodology page - update snapshot * Update Methodology process list to trussworks - remove custom process list - remove custom CSS from global file - change copy * Modify layout of Methodology to using Grid - modify Dataset section to use Grid - remove outdated component CSS - update the snapshot * Update copy based on product feedback - update snapshots * Remove Accordions - updates snapshots - white CBG groups will show "Not community of focus"
272 lines
8.5 KiB
TypeScript
272 lines
8.5 KiB
TypeScript
/* eslint-disable no-unused-vars */
|
|
// External Libs:
|
|
import React, {MouseEvent, useRef, useState, useMemo} from 'react';
|
|
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
|
|
import ReactMapGL, {
|
|
MapEvent,
|
|
ViewportProps,
|
|
WebMercatorViewport,
|
|
NavigationControl,
|
|
GeolocateControl,
|
|
Popup,
|
|
FlyToInterpolator,
|
|
FullscreenControl,
|
|
MapRef, Source, Layer} from 'react-map-gl';
|
|
import bbox from '@turf/bbox';
|
|
import * as d3 from 'd3-ease';
|
|
import {isMobile} from 'react-device-detect';
|
|
import {Grid} from '@trussworks/react-uswds';
|
|
import {useWindowSize} from 'react-use';
|
|
|
|
// Contexts:
|
|
import {useFlags} from '../contexts/FlagContext';
|
|
|
|
// Components:
|
|
import TerritoryFocusControl from './territoryFocusControl';
|
|
import MapInfoPanel from './mapInfoPanel';
|
|
import AreaDetail from './AreaDetail';
|
|
|
|
// Styles and constants
|
|
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';
|
|
|
|
|
|
declare global {
|
|
interface Window {
|
|
Cypress?: object;
|
|
underlyingMap: Map;
|
|
}
|
|
}
|
|
|
|
interface IJ40Interface {
|
|
location: Location;
|
|
};
|
|
|
|
|
|
export interface IDetailViewInterface {
|
|
latitude: number
|
|
longitude: number
|
|
zoom: number
|
|
properties: constants.J40Properties,
|
|
};
|
|
|
|
const J40Map = ({location}: IJ40Interface) => {
|
|
// Hash portion of URL is of the form #zoom/lat/lng
|
|
const [zoom, lat, lng] = location.hash.slice(1).split('/');
|
|
const [viewport, setViewport] = useState<ViewportProps>({
|
|
latitude: lat && parseFloat(lat) || constants.DEFAULT_CENTER[0],
|
|
longitude: lng && parseFloat(lng) || constants.DEFAULT_CENTER[1],
|
|
zoom: zoom && parseFloat(zoom) || constants.GLOBAL_MIN_ZOOM,
|
|
});
|
|
|
|
const [selectedFeature, setSelectedFeature] = useState<MapboxGeoJSONFeature>();
|
|
const [detailViewData, setDetailViewData] = useState<IDetailViewInterface>();
|
|
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
|
|
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
|
|
const [isMobileMapState, setIsMobileMapState] = useState<boolean>(false);
|
|
const {width: windowWidth} = useWindowSize();
|
|
|
|
const mapRef = useRef<MapRef>(null);
|
|
const flags = useFlags();
|
|
|
|
const selectedFeatureId = (selectedFeature && selectedFeature.id) || '';
|
|
const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]);
|
|
|
|
const onClick = (event: MapEvent) => {
|
|
const feature = event.features && event.features[0];
|
|
if (feature) {
|
|
const [minLng, minLat, maxLng, maxLat] = bbox(feature);
|
|
const newViewPort = new WebMercatorViewport({height: viewport.height!, width: viewport.width!});
|
|
const {longitude, latitude, zoom} = newViewPort.fitBounds(
|
|
[
|
|
[minLng, minLat],
|
|
[maxLng, maxLat],
|
|
],
|
|
{
|
|
padding: 40,
|
|
},
|
|
);
|
|
if (feature.id !== selectedFeatureId) {
|
|
setSelectedFeature(feature);
|
|
} else {
|
|
setSelectedFeature(undefined);
|
|
}
|
|
const popupInfo = {
|
|
longitude: longitude,
|
|
latitude: latitude,
|
|
zoom: zoom,
|
|
properties: feature.properties,
|
|
};
|
|
goToPlace([
|
|
[minLng, minLat],
|
|
[maxLng, maxLat],
|
|
]);
|
|
setDetailViewData(popupInfo);
|
|
}
|
|
};
|
|
|
|
const onLoad = () => {
|
|
if (typeof window !== 'undefined' && window.Cypress && mapRef.current) {
|
|
window.underlyingMap = mapRef.current.getMap();
|
|
}
|
|
|
|
if (isMobile) setIsMobileMapState(true);
|
|
};
|
|
|
|
|
|
const goToPlace = (bounds: LngLatBoundsLike ) => {
|
|
const {longitude, latitude, zoom} = new WebMercatorViewport({height: viewport.height!, width: viewport.width!})
|
|
.fitBounds(bounds as [[number, number], [number, number]], {
|
|
padding: 20,
|
|
offset: [0, -100],
|
|
});
|
|
setViewport({
|
|
...viewport,
|
|
longitude,
|
|
latitude,
|
|
zoom,
|
|
transitionDuration: 1000,
|
|
transitionInterpolator: new FlyToInterpolator(),
|
|
transitionEasing: d3.easeCubic,
|
|
});
|
|
};
|
|
|
|
const onClickTerritoryFocusButton = (event: MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
const buttonID = event.target && (event.target as HTMLElement).id;
|
|
|
|
switch (buttonID) {
|
|
case '48':
|
|
goToPlace(constants.LOWER_48_BOUNDS);
|
|
break;
|
|
case 'AK':
|
|
goToPlace(constants.ALASKA_BOUNDS);
|
|
break;
|
|
case 'HI':
|
|
goToPlace(constants.HAWAII_BOUNDS);
|
|
break;
|
|
case 'PR':
|
|
goToPlace(constants.PUERTO_RICO_BOUNDS);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
const onTransitionStart = () => {
|
|
setTransitionInProgress(true);
|
|
};
|
|
|
|
const onTransitionEnd = () => {
|
|
setTransitionInProgress(false);
|
|
};
|
|
|
|
const onGeolocate = () => {
|
|
setGeolocationInProgress(false);
|
|
};
|
|
|
|
const onClickGeolocate = () => {
|
|
setGeolocationInProgress(true);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Grid col={12} desktop={{col: 9}}>
|
|
<ReactMapGL
|
|
{...viewport}
|
|
mapStyle={makeMapStyle(flags)}
|
|
minZoom={constants.GLOBAL_MIN_ZOOM}
|
|
maxZoom={constants.GLOBAL_MAX_ZOOM}
|
|
mapOptions={{hash: true}}
|
|
width="100%"
|
|
height={windowWidth < 1024 ? '44vh' : '100%'}
|
|
dragRotate={false}
|
|
touchRotate={false}
|
|
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
|
|
onViewportChange={setViewport}
|
|
onClick={onClick}
|
|
onLoad={onLoad}
|
|
onTransitionStart={onTransitionStart}
|
|
onTransitionEnd={onTransitionEnd}
|
|
ref={mapRef}
|
|
data-cy={'reactMapGL'}
|
|
>
|
|
<Source
|
|
id={constants.HIGH_SCORE_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}
|
|
>
|
|
<Layer
|
|
id={constants.CURRENTLY_SELECTED_FEATURE_HIGHLIGHT_LAYER_NAME}
|
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
|
type='line'
|
|
paint={{
|
|
'line-color': constants.DEFAULT_OUTLINE_COLOR,
|
|
'line-width': constants.CURRENTLY_SELECTED_FEATURE_LAYER_WIDTH,
|
|
'line-opacity': constants.CURRENTLY_SELECTED_FEATURE_LAYER_OPACITY,
|
|
}}
|
|
minzoom={constants.GLOBAL_MIN_ZOOM_HIGHLIGHT}
|
|
maxzoom={constants.GLOBAL_MAX_ZOOM_HIGHLIGHT}
|
|
/>
|
|
|
|
<Layer
|
|
id={constants.BLOCK_GROUP_BOUNDARY_LAYER_NAME}
|
|
type='line'
|
|
source-layer={constants.SCORE_SOURCE_LAYER}
|
|
paint={{
|
|
'line-color': constants.BORDER_HIGHLIGHT_COLOR,
|
|
'line-width': constants.HIGHLIGHT_BORDER_WIDTH,
|
|
}}
|
|
filter={filter}
|
|
minzoom={constants.GLOBAL_MIN_ZOOM_HIGH}
|
|
/>
|
|
</Source>
|
|
{('fs' in flags && detailViewData && !transitionInProgress) && (
|
|
<Popup
|
|
className={styles.j40Popup}
|
|
tipSize={5}
|
|
anchor="top"
|
|
longitude={detailViewData.longitude!}
|
|
latitude={detailViewData.latitude!}
|
|
closeOnClick={true}
|
|
onClose={setDetailViewData}
|
|
captureScroll={true}
|
|
>
|
|
<AreaDetail properties={detailViewData.properties} />
|
|
</Popup>
|
|
)}
|
|
<NavigationControl
|
|
showCompass={false}
|
|
className={styles.navigationControl}
|
|
/>
|
|
{'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
|
|
onClick={onClickGeolocate}
|
|
/> : ''}
|
|
{geolocationInProgress ? <div>Geolocation in progress...</div> : ''}
|
|
<TerritoryFocusControl onClickTerritoryFocusButton={onClickTerritoryFocusButton}/>
|
|
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
|
|
</ReactMapGL>
|
|
</Grid>
|
|
|
|
<Grid col={12} desktop={{col: 3}} className={styles.mapInfoPanel}>
|
|
<MapInfoPanel
|
|
className={styles.mapInfoPanel}
|
|
featureProperties={detailViewData?.properties}
|
|
selectedFeatureId={selectedFeature?.id}
|
|
/>
|
|
</Grid>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default J40Map;
|