import React, {
useState,
useEffect,
CSSProperties,
ReactElement,
cloneElement,
Ref,
useMemo,
useImperativeHandle,
forwardRef,
useCallback,
useRef,
} from 'react';
import {
BoundsViewport,
AnimationFunction,
Bounds as MapVEuBounds,
} from './Types';
import { BoundsDriftMarkerProps } from './BoundsDriftMarker';
import {
MapContainer,
TileLayer,
LayersControl,
ScaleControl,
useMap,
useMapEvents,
} from 'react-leaflet';
import SemanticMarkers from './SemanticMarkers';
import 'leaflet/dist/leaflet.css';
import '../../dist/css/map_styles.css';
import CustomGridLayer from './CustomGridLayer';
import MouseTools, { MouseMode } from './MouseTools';
import { PlotRef } from '../types/plots';
import { ToImgopts } from 'plotly.js';
import Spinner from '../components/Spinner';
import NoDataOverlay from '../components/NoDataOverlay';
import { LatLngBounds, Map } from 'leaflet';
import domToImage from 'dom-to-image';
import { makeSharedPromise } from '../utils/promise-utils';
import { propTypes } from 'react-bootstrap/esm/Image';
// define Viewport type
export type Viewport = {
center: [number, number];
zoom: number;
};
export const baseLayers = {
Street: {
url:
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
attribution:
'Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012',
maxZoom: 17,
},
// change config
Terrain: {
url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
attribution:
'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)',
maxZoom: 17,
},
Satellite: {
url:
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
attribution:
'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
noWrap: false,
// testing worldmap issue - with bounds props, message like 'map data not yet availalbe' is not shown
// // block this as bounds is not compatible?
// bounds: [
// [-90, -180],
// [90, 180],
// ],
// noWrap: true,
maxZoom: 17,
},
// Not sure these are needed, and the "Light" layer requires an API key
// Light: {
// url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png',
// attribution:
// '© Stadia Maps, © OpenMapTiles © OpenStreetMap contributors',
// maxZoom: 20,
// },
// Dark: {
// url:
// 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png',
// attribution:
// '© OpenStreetMap © CartoDB',
// subdomains: 'abcd',
// // maxZoom='19'
// },
OSM: {
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution:
'© OpenStreetMap contributors CC-BY-SA, Imagery © Mapbox',
// minZoom='2'
// maxZoom='18'
// noWrap='0'
},
};
export type BaseLayerChoice = keyof typeof baseLayers;
/**
* Renders a Leaflet map with semantic zooming markers
*
*
* @param props
*/
export interface MapVEuMapProps {
/** Center lat/long and zoom level */
viewport: Viewport;
/** update handler */
onViewportChanged: (viewport: Viewport) => void;
/** Height and width of plot element */
height: CSSProperties['height'];
width: CSSProperties['width'];
/** CSS styles for the map container other than height and width,
* which have their own dedicated props */
style?: Omit;
/** callback for when viewport has changed, giving access to the bounding box */
onBoundsChanged: (bvp: BoundsViewport) => void;
markers: ReactElement[];
recenterMarkers?: boolean;
// closing sidebar at MapVEuMap: passing setSidebarCollapsed()
sidebarOnClose?: (value: React.SetStateAction) => void;
animation: {
method: string;
duration: number;
animationFunction: AnimationFunction;
} | null;
/** Should a geohash-based grid be shown?
* Optional. See also zoomLevelToGeohashLevel
**/
showGrid?: boolean;
/** A function to map from Leaflet zoom level to Geohash level
*
* Optional, but required for grid functionality if showGrid is true
**/
zoomLevelToGeohashLevel?: (leafletZoomLevel: number) => number;
/** What's the minimum leaflet zoom level allowed? Default = 1 */
minZoom?: number;
/**
* Should the mouse-mode (regular/magnifying glass) icons be shown and active?
**/
showMouseToolbar?: boolean;
/** mouseMode control */
mouseMode?: MouseMode;
/** a function for changing mouseMode */
onMouseModeChange?: (value: MouseMode) => void;
/**
* The name of the tile layer to use. If omitted, defaults to Street.
*/
baseLayer?: BaseLayerChoice;
/** Callback for when the base layer has changed */
onBaseLayerChanged?: (newBaseLayer: BaseLayerChoice) => void;
/** Show layers control, default true */
showLayerSelector?: boolean;
/** Show attribution, default true */
showAttribution?: boolean;
/** Show zoom control, default true */
showZoomControl?: boolean;
/** Whether to zoom and pan map to center on markers */
flyToMarkers?: boolean;
/** How long (in ms) after rendering to wait before flying to markers */
flyToMarkersDelay?: number;
/** Whether to show a loading spinner */
showSpinner?: boolean;
/** Whether to show the "No data" overlay */
showNoDataOverlay?: boolean;
/** Whether to show the Scale in the map */
showScale?: boolean;
/** Whether to allow any interactive control of map location (default: true) */
interactive?: boolean;
/** is map scroll and zoom allowed? default true; will be overridden by `interactive: false` */
scrollingEnabled?: boolean;
}
function MapVEuMap(props: MapVEuMapProps, ref: Ref) {
const {
viewport,
height,
width,
style,
onViewportChanged,
onBoundsChanged,
markers,
animation,
recenterMarkers = true,
showGrid,
zoomLevelToGeohashLevel,
minZoom = 1,
showMouseToolbar,
baseLayer,
onBaseLayerChanged,
flyToMarkers,
flyToMarkersDelay,
showSpinner,
showNoDataOverlay,
showScale = true,
showLayerSelector = true,
showAttribution = true,
showZoomControl = true,
scrollingEnabled = true,
mouseMode,
onMouseModeChange,
interactive = true,
} = props;
// Whether the user is currently dragging the map
const [isDragging, setIsDragging] = useState(false);
// use a ref to avoid unneeded renders
const mapRef = useRef