mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-08-01 22:34:18 -07:00
Use MapLibre for Map Visualization (#300)
* switching to MapLibre (see more at https://github.com/usds/justice40-tool/issues/299) * Removing traces of OpenLayers * Review comments - removing unused properties, component This is a pre-requisite for addressing issue #280 and other similar control-related tickets
This commit is contained in:
parent
27d9472326
commit
2257627938
21 changed files with 53 additions and 505 deletions
13
client/src/components/J40Map.module.scss.d.ts
vendored
Normal file
13
client/src/components/J40Map.module.scss.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
declare namespace J40MapModuleScssNamespace {
|
||||
export interface IJ40MapModuleScss {
|
||||
mapContainer: string;
|
||||
j40Popup: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const J40MapModuleScssModule: J40MapModuleScssNamespace.IJ40MapModuleScss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: J40MapModuleScssNamespace.IJ40MapModuleScss;
|
||||
};
|
||||
|
||||
export = J40MapModuleScssModule;
|
|
@ -1,29 +1,28 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import React, {useRef, useEffect, useState} from 'react';
|
||||
import {LngLatBoundsLike,
|
||||
import maplibregl, {LngLatBoundsLike,
|
||||
Map,
|
||||
NavigationControl,
|
||||
PopupOptions,
|
||||
Popup,
|
||||
LngLatLike} from 'mapbox-gl';
|
||||
LngLatLike} from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import mapStyle from '../data/mapStyle';
|
||||
import ZoomWarning from './zoomWarning';
|
||||
import PopupContent from './popupContent';
|
||||
import * as constants from '../data/constants';
|
||||
import ReactDOM from 'react-dom';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import * as styles from './mapboxMap.module.scss';
|
||||
import * as styles from './J40Map.module.scss';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Cypress?: object;
|
||||
mapboxGlMap: Map;
|
||||
underlyingMap: Map;
|
||||
}
|
||||
}
|
||||
type ClickEvent = maplibregl.MapMouseEvent & maplibregl.EventData;
|
||||
|
||||
type ClickEvent = mapboxgl.MapMouseEvent & mapboxgl.EventData;
|
||||
|
||||
const MapboxMap = () => {
|
||||
const J40Map = () => {
|
||||
const mapContainer = React.useRef<HTMLDivElement>(null);
|
||||
const map = useRef<Map>() as React.MutableRefObject<Map>;
|
||||
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
|
||||
|
@ -38,7 +37,6 @@ const MapboxMap = () => {
|
|||
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,
|
||||
hash: true, // Adds hash of zoom/lat/long to the url
|
||||
});
|
||||
|
@ -52,7 +50,7 @@ const MapboxMap = () => {
|
|||
|
||||
initialMap.on('load', () => {
|
||||
if (window.Cypress) {
|
||||
window.mapboxGlMap = initialMap;
|
||||
window.underlyingMap = initialMap;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -104,4 +102,4 @@ const MapboxMap = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default MapboxMap;
|
||||
export default J40Map;
|
|
@ -1,3 +0,0 @@
|
|||
.mapControlContainer {
|
||||
margin: 18.5px 42px 23px 42px;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
declare namespace MapControlModuleScssNamespace {
|
||||
export interface IMapControlModuleScss {
|
||||
mapControlContainer: 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;
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import {Button, ButtonGroup} from '@trussworks/react-uswds';
|
||||
import Feature from 'ol/Feature';
|
||||
import Geometry from 'ol/geom/Geometry';
|
||||
import * as styles from './mapControls.module.scss';
|
||||
|
||||
interface IMapControlsProps {
|
||||
setFeatures: (arg0: Feature<Geometry>[]) => void;
|
||||
}
|
||||
|
||||
const MapControls = ({setFeatures}: IMapControlsProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.mapControlContainer}>
|
||||
<h2>Explore the Tool</h2>
|
||||
<ButtonGroup type="segmented">
|
||||
<Button type="button">Combined</Button>
|
||||
<Button type="button" outline={true}>Poverty</Button>
|
||||
<Button type="button" outline={true}>Linguistic Isolation</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapControls;
|
|
@ -1,16 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import {useFlags} from '../contexts/FlagContext';
|
||||
import MapboxMap from './mapboxMap';
|
||||
import OpenLayersMap from './openlayersMap';
|
||||
import J40Map from './J40Map';
|
||||
|
||||
const MapWrapper = () => {
|
||||
const flags = useFlags();
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
flags.includes('mb') ?
|
||||
<MapboxMap /> :
|
||||
<OpenLayersMap features={[]}/>
|
||||
<J40Map />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
13
client/src/components/mapboxMap.module.scss.d.ts
vendored
13
client/src/components/mapboxMap.module.scss.d.ts
vendored
|
@ -1,13 +0,0 @@
|
|||
declare namespace MapboxMapModuleScssNamespace {
|
||||
export interface IMapboxMapModuleScss {
|
||||
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;
|
|
@ -1,6 +0,0 @@
|
|||
.mapContainer {
|
||||
height: 676px;
|
||||
margin-bottom: 29px;
|
||||
max-width: revert;
|
||||
margin-top: 50px;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
declare namespace MapModuleScssNamespace {
|
||||
export interface IMapModuleScss {
|
||||
mapContainer:string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const MapModuleScssModule: MapModuleScssNamespace.IMapModuleScss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: MapModuleScssNamespace.IMapModuleScss;
|
||||
};
|
||||
|
||||
export = MapModuleScssModule;
|
|
@ -1,132 +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 {Coordinate} from 'ol/coordinate';
|
||||
import olms from 'ol-mapbox-style';
|
||||
import mapStyle from '../data/mapStyle';
|
||||
import ZoomWarning from './zoomWarning';
|
||||
import OpenlayersPopup from './openlayersPopup';
|
||||
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?
|
||||
<OpenlayersPopup selectedFeature={selectedFeature!} map={map!} position={currentOverlayPosition} /> :
|
||||
''
|
||||
}
|
||||
<ZoomWarning zoomLevel={currentZoom} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapWrapper;
|
|
@ -1,52 +0,0 @@
|
|||
.popupContainer {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #cccccc;
|
||||
bottom: 12px;
|
||||
left: -50px;
|
||||
min-width: 280px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.popupContainer:after,
|
||||
.popupContainer:before {
|
||||
top: 100%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.popupContainer:after {
|
||||
border-top-color: white;
|
||||
border-width: 10px;
|
||||
left: 48px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.popupContainer:before {
|
||||
border-top-color: #cccccc;
|
||||
border-width: 11px;
|
||||
left: 48px;
|
||||
margin-left: -11px;
|
||||
}
|
||||
|
||||
.popupCloser {
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.popupCloser:after {
|
||||
content: "✖";
|
||||
}
|
||||
|
||||
.popupContent {
|
||||
max-height: 300px;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
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;
|
|
@ -1,58 +0,0 @@
|
|||
import React, {useRef, useEffect, useState} from 'react';
|
||||
import * as styles from './openlayersPopup.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 IOpenlayersPopupProps {
|
||||
map: Map,
|
||||
selectedFeature: FeatureLike;
|
||||
position: Coordinate;
|
||||
}
|
||||
|
||||
const OpenlayersPopup = ({map, selectedFeature, position}: IOpenlayersPopupProps) => {
|
||||
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 OpenlayersPopup;
|
Loading…
Add table
Add a link
Reference in a new issue