mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-08-03 10:14:18 -07:00
Zoom button styling does not match spec // General custom controls fix (#357)
This commit is contained in:
parent
cfce0dc826
commit
dbf1ae2ad8
19 changed files with 31006 additions and 294 deletions
|
@ -1,131 +1,132 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import React, {useRef, useEffect, useState} from 'react';
|
||||
import maplibregl, {LngLatBoundsLike,
|
||||
Map,
|
||||
import React, {MouseEvent, useRef, useState} from 'react';
|
||||
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
|
||||
import ReactMapGL, {
|
||||
MapEvent,
|
||||
ViewportProps,
|
||||
WebMercatorViewport,
|
||||
NavigationControl,
|
||||
PopupOptions,
|
||||
GeolocateControl,
|
||||
Popup,
|
||||
LngLatLike,
|
||||
MapboxGeoJSONFeature} from 'maplibre-gl';
|
||||
FlyToInterpolator,
|
||||
FullscreenControl,
|
||||
MapRef} from 'react-map-gl';
|
||||
import {makeMapStyle} from '../data/mapStyle';
|
||||
import PopupContent from './popupContent';
|
||||
import * as constants from '../data/constants';
|
||||
import ReactDOM from 'react-dom';
|
||||
import AreaDetail from './areaDetail';
|
||||
import bbox from '@turf/bbox';
|
||||
import * as d3 from 'd3-ease';
|
||||
import {useFlags} from '../contexts/FlagContext';
|
||||
import TerritoryFocusControl from './territoryFocusControl';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
type ClickEvent = maplibregl.MapMouseEvent & maplibregl.EventData;
|
||||
|
||||
|
||||
interface IDetailViewInterface {
|
||||
latitude: number
|
||||
longitude: number
|
||||
zoom: number
|
||||
properties: constants.J40Properties,
|
||||
};
|
||||
|
||||
const J40Map = () => {
|
||||
const mapContainer = React.useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<Map>() as React.MutableRefObject<Map>;
|
||||
const selectedFeature = useRef<MapboxGeoJSONFeature>();
|
||||
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
|
||||
const [viewport, setViewport] = useState<ViewportProps>({
|
||||
latitude: constants.DEFAULT_CENTER[0],
|
||||
longitude: constants.DEFAULT_CENTER[1],
|
||||
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 mapRef = useRef<MapRef>(null);
|
||||
const flags = useFlags();
|
||||
|
||||
useEffect(() => {
|
||||
const initialMap = new Map({
|
||||
container: mapContainer.current!,
|
||||
style: makeMapStyle(flags),
|
||||
center: constants.DEFAULT_CENTER as LngLatLike,
|
||||
zoom: zoom,
|
||||
minZoom: constants.GLOBAL_MIN_ZOOM,
|
||||
maxBounds: constants.GLOBAL_MAX_BOUNDS as LngLatBoundsLike,
|
||||
hash: true, // Adds hash of zoom/lat/long to the url
|
||||
});
|
||||
// disable map rotation using right click + drag
|
||||
initialMap.dragRotate.disable();
|
||||
|
||||
// disable map rotation using touch rotation gesture
|
||||
initialMap.touchZoomRotate.disableRotation();
|
||||
|
||||
setZoom(initialMap.getZoom());
|
||||
|
||||
initialMap.on('load', () => {
|
||||
if (window.Cypress) {
|
||||
window.underlyingMap = initialMap;
|
||||
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 we've selected a new feature, set 'selected' to false
|
||||
if (selectedFeature && feature.id !== selectedFeature.id) {
|
||||
setMapSelected(selectedFeature, false);
|
||||
}
|
||||
});
|
||||
setMapSelected(feature, true);
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
initialMap.on('click', handleClick);
|
||||
initialMap.addControl(new NavigationControl({showCompass: false}), 'top-left');
|
||||
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({
|
||||
mapRef.current && mapRef.current.getMap().setFeatureState({
|
||||
source: feature.source,
|
||||
sourceLayer: feature.sourceLayer,
|
||||
id: feature.id,
|
||||
}, {[constants.SELECTED_PROPERTY]: isSelected});
|
||||
if (isSelected) {
|
||||
selectedFeature.current = feature;
|
||||
setSelectedFeature(feature);
|
||||
} else {
|
||||
selectedFeature.current = undefined;
|
||||
setSelectedFeature(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: ClickEvent) => {
|
||||
const map = e.target;
|
||||
const clickedCoord = e.point;
|
||||
const features = map.queryRenderedFeatures(clickedCoord, {
|
||||
layers: [constants.HIGH_SCORE_LAYER_NAME],
|
||||
});
|
||||
const feature = features && features[0];
|
||||
if (feature) {
|
||||
const placeholder = document.createElement('div');
|
||||
// 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 = {
|
||||
offset: [0, 0],
|
||||
className: styles.j40Popup,
|
||||
focusAfterOpen: false,
|
||||
};
|
||||
new Popup(options)
|
||||
.setLngLat(e.lngLat)
|
||||
.setDOMContent(placeholder)
|
||||
.on('close', function() {
|
||||
setMapSelected(feature, false);
|
||||
})
|
||||
.addTo(map);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mapRef.current.on('move', () => {
|
||||
setZoom(mapRef.current.getZoom());
|
||||
});
|
||||
mapRef.current.on('mouseenter', constants.HIGH_SCORE_LAYER_NAME, () => {
|
||||
mapRef.current.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
mapRef.current.on('mouseleave', constants.HIGH_SCORE_LAYER_NAME, () => {
|
||||
mapRef.current.getCanvas().style.cursor = '';
|
||||
});
|
||||
}, [mapRef]);
|
||||
|
||||
const goToPlace = (bounds:number[][]) => {
|
||||
mapRef.current.fitBounds(bounds as LngLatBoundsLike);
|
||||
};
|
||||
|
||||
const onClickTerritoryFocusButton = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
// currentTarget always refers to the element to which the event handler
|
||||
// has been attached, as opposed to Event.target, which identifies
|
||||
// the element on which the event occurred and which may be its descendant.
|
||||
const buttonID = event.target && event.currentTarget.id;
|
||||
const onClickTerritoryFocusButton = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
const buttonID = event.target && (event.target as HTMLElement).id;
|
||||
|
||||
switch (buttonID) {
|
||||
case '48':
|
||||
|
@ -146,40 +147,74 @@ const J40Map = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const onTransitionStart = () => {
|
||||
setTransitionInProgress(true);
|
||||
};
|
||||
|
||||
const onTransitionEnd = () => {
|
||||
setTransitionInProgress(false);
|
||||
};
|
||||
|
||||
const onGeolocate = () => {
|
||||
setGeolocationInProgress(false);
|
||||
};
|
||||
|
||||
const onClickGeolocate = () => {
|
||||
setGeolocationInProgress(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.territoryFocusContainer}>
|
||||
<button
|
||||
id={'48'}
|
||||
onClick={onClickTerritoryFocusButton}
|
||||
className={styles.territoryFocusButton}
|
||||
aria-label="Zoom to Lower 48" >
|
||||
48
|
||||
</button>
|
||||
<button
|
||||
id={'AK'}
|
||||
onClick={onClickTerritoryFocusButton}
|
||||
className={styles.territoryFocusButton}
|
||||
aria-label="Zoom to Alaska" >
|
||||
AK
|
||||
</button>
|
||||
<button
|
||||
id={'HI'}
|
||||
onClick={onClickTerritoryFocusButton}
|
||||
className={styles.territoryFocusButton}
|
||||
aria-label="Zoom to Hawaii" >
|
||||
HI
|
||||
</button>
|
||||
<button
|
||||
id={'PR'}
|
||||
onClick={onClickTerritoryFocusButton}
|
||||
className={styles.territoryFocusButton}
|
||||
aria-label="Zoom to Puerto Rico" >
|
||||
PR
|
||||
</button>
|
||||
</div>
|
||||
<div ref={mapContainer} className={styles.mapContainer}/>
|
||||
</div>
|
||||
<>
|
||||
<ReactMapGL
|
||||
{...viewport}
|
||||
className={styles.mapContainer}
|
||||
mapStyle={makeMapStyle(flags)}
|
||||
minZoom={constants.GLOBAL_MIN_ZOOM}
|
||||
maxZoom={constants.GLOBAL_MAX_ZOOM}
|
||||
mapOptions={{hash: true}}
|
||||
width="100%"
|
||||
height="52vw"
|
||||
dragRotate={false}
|
||||
touchRotate={false}
|
||||
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
|
||||
onViewportChange={setViewport}
|
||||
onClick={onClick}
|
||||
onLoad={onLoad}
|
||||
onTransitionStart={onTransitionStart}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
ref={mapRef}
|
||||
>
|
||||
{(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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue