Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map type plugin feature #399

Merged
merged 20 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
92d0a55
Add temp readme
dmfalke Aug 2, 2023
c82803b
Restructure side menu to allow for subheadings. (#398)
dmfalke Aug 2, 2023
cc4ca38
Some additional clean-up (#401)
dmfalke Aug 3, 2023
b80069d
Merge branch 'main' into map-collection-variable-map-type
dmfalke Aug 9, 2023
85b4baf
Merge branch 'main' into map-collection-variable-map-type
dmfalke Aug 9, 2023
758b612
Have the active map type config panel open by default
chowington Aug 14, 2023
8d9f506
Merge branch 'main' into map-collection-variable-map-type
dmfalke Aug 18, 2023
1b9f33a
Remember whether side panel is expanded on subsequent page loads
chowington Aug 21, 2023
7f7167b
Merge remote-tracking branch 'origin/main' into map-collection-variab…
dmfalke Aug 23, 2023
cadcba3
Remove unused app state property
chowington Aug 30, 2023
eb7f285
Merge pull request #421 from VEuPathDB/403-maptype-config-UX
chowington Aug 30, 2023
41ea287
Merge remote-tracking branch 'origin/main' into map-collection-variab…
dmfalke Sep 19, 2023
77f4b82
Merge remote-tracking branch 'origin/main' into map-collection-variab…
dmfalke Oct 18, 2023
66e1235
Merge remote-tracking branch 'origin/main' into map-collection-variab…
dmfalke Oct 18, 2023
f6e39d3
EDA Map Refactor: Introduce MapType plugin and implementations (#424)
dmfalke Oct 25, 2023
8c76d32
Merge remote-tracking branch 'origin/main' into map-collection-variab…
dmfalke Oct 25, 2023
0e2832f
Post-merge updates
dmfalke Oct 25, 2023
50153dd
Post-merge updates, 2
dmfalke Oct 25, 2023
64dea3e
Fix filter button in header
dmfalke Oct 25, 2023
a2acc16
remove file
dmfalke Oct 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 78 additions & 139 deletions packages/libs/components/src/map/MapVEuMap.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import React, {
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 { BoundsViewport, AnimationFunction, Bounds } from './Types';
import {
MapContainer,
TileLayer,
Expand All @@ -24,15 +17,14 @@ import {
useMap,
useMapEvents,
} from 'react-leaflet';
import SemanticMarkers from './SemanticMarkers';
import 'leaflet/dist/leaflet.css';
import './styles/map-styles.css';
import CustomGridLayer from './CustomGridLayer';
import { PlotRef } from '../types/plots';
import { ToImgopts } from 'plotly.js';
import Spinner from '../components/Spinner';
import NoDataOverlay from '../components/NoDataOverlay';
import { LatLngBounds, Map, DomEvent } from 'leaflet';
import { Map, DomEvent, LatLngBounds } from 'leaflet';
import domToImage from 'dom-to-image';
import { makeSharedPromise } from '../utils/promise-utils';
import { Undo } from '@veupathdb/coreui';
Expand Down Expand Up @@ -111,6 +103,8 @@ export interface MapVEuMapProps {
/** update handler */
onViewportChanged: (viewport: Viewport) => void;

onBoundsChanged: (boundsViewport: BoundsViewport) => void;

/** Height and width of plot element */
height: CSSProperties['height'];
width: CSSProperties['width'];
Expand All @@ -119,18 +113,8 @@ export interface MapVEuMapProps {
* which have their own dedicated props */
style?: Omit<React.CSSProperties, 'height' | 'width'>;

/** callback for when viewport has changed, giving access to the bounding box */
onBoundsChanged: (bvp: BoundsViewport) => void;

markers: ReactElement<BoundsDriftMarkerProps>[];
recenterMarkers?: boolean;
// closing sidebar at MapVEuMap: passing setSidebarCollapsed()
sidebarOnClose?: (value: React.SetStateAction<boolean>) => void;
animation: {
method: string;
duration: number;
animationFunction: AnimationFunction;
} | null;
/** Should a geohash-based grid be shown?
* Optional. See also zoomLevelToGeohashLevel
**/
Expand All @@ -155,10 +139,6 @@ export interface MapVEuMapProps {
/** 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 */
Expand All @@ -171,6 +151,7 @@ export interface MapVEuMapProps {
scrollingEnabled?: boolean;
/** pass default viewport */
defaultViewport?: Viewport;
children?: React.ReactNode;
}

function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
Expand All @@ -181,22 +162,15 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
style,
onViewportChanged,
onBoundsChanged,
markers,
animation,
recenterMarkers = true,
showGrid,
zoomLevelToGeohashLevel,
minZoom = 1,
baseLayer,
onBaseLayerChanged,
flyToMarkers,
flyToMarkersDelay,
showSpinner,
showNoDataOverlay,
showScale = true,
showLayerSelector = true,
showAttribution = true,
showZoomControl = true,
scrollingEnabled = true,
interactive = true,
defaultViewport,
Expand All @@ -215,8 +189,12 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
(map: Map) => {
mapRef.current = map;
sharedPlotCreation.run();
onBoundsChanged({
bounds: constrainLongitudeToMainWorld(boundsToGeoBBox(map.getBounds())),
zoomLevel: map.getZoom(),
});
},
[sharedPlotCreation.run]
[onBoundsChanged, sharedPlotCreation]
);

useEffect(() => {
Expand All @@ -226,7 +204,7 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
if (gitterBtn) {
gitterBtn.style.display = 'none';
}
() => {
return () => {
if (gitterBtn) {
gitterBtn.style.display = 'inline';
}
Expand All @@ -246,13 +224,9 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
return domToImage.toPng(mapRef.current.getContainer(), imageOpts);
},
}),
[domToImage, mapRef]
[sharedPlotCreation.promise]
);

const finalMarkers = useMemo(() => {
return markers.map((marker) => cloneElement(marker, { showPopup: true }));
}, [markers]);

const disabledInteractiveProps = {
dragging: false,
keyboard: false,
Expand All @@ -279,12 +253,7 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
/>

<SemanticMarkers
onBoundsChanged={onBoundsChanged}
markers={finalMarkers}
animation={animation}
recenterMarkers={recenterMarkers}
/>
{props.children}

{showGrid && zoomLevelToGeohashLevel ? (
<CustomGridLayer zoomLevelToGeohashLevel={zoomLevelToGeohashLevel} />
Expand All @@ -309,18 +278,11 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {
{/* add Scale in the map */}
{showScale && <ScaleControl position="bottomright" />}

{/* PerformFlyToMarkers component for flyTo functionality */}
{flyToMarkers && (
<PerformFlyToMarkers
markers={markers}
flyToMarkers={flyToMarkers}
flyToMarkersDelay={flyToMarkersDelay}
/>
)}
{/* component for map events */}
<MapVEuMapEvents
onViewportChanged={onViewportChanged}
onBaseLayerChanged={onBaseLayerChanged}
onBoundsChanged={onBoundsChanged}
/>
{/* set ScrollWheelZoom */}
<MapScrollWheelZoom scrollingEnabled={scrollingEnabled} />
Expand All @@ -332,67 +294,41 @@ function MapVEuMap(props: MapVEuMapProps, ref: Ref<PlotRef>) {

export default forwardRef(MapVEuMap);

// for flyTo
interface PerformFlyToMarkersProps {
/* markers */
markers: ReactElement<BoundsDriftMarkerProps>[];
/** 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;
}

// component to implement flyTo functionality
function PerformFlyToMarkers(props: PerformFlyToMarkersProps) {
const { markers, flyToMarkers, flyToMarkersDelay } = props;

// instead of using useRef() to the map in v2, useMap() should be used instead in v3
const map = useMap();

const markersBounds = useMemo(() => {
return computeMarkersBounds(markers);
}, [markers]);

const performFlyToMarkers = useCallback(() => {
if (markersBounds) {
const boundingBox = computeBoundingBox(markersBounds);
if (boundingBox) map.fitBounds(boundingBox);
}
}, [markersBounds, map]);

useEffect(() => {
const asyncEffect = async () => {
if (flyToMarkersDelay)
await new Promise((resolve) => setTimeout(resolve, flyToMarkersDelay));
performFlyToMarkers();
};

if (flyToMarkers && markers.length > 0) asyncEffect();
}, [markers, flyToMarkers, flyToMarkersDelay, performFlyToMarkers]);

return null;
}

interface MapVEuMapEventsProps {
onViewportChanged: (viewport: Viewport) => void;
onBoundsChanged: (bondsViewport: BoundsViewport) => void;
onBaseLayerChanged?: (newBaseLayer: BaseLayerChoice) => void;
}

// function to handle map events such as onViewportChanged and baselayerchange
function MapVEuMapEvents(props: MapVEuMapEventsProps) {
const { onViewportChanged, onBaseLayerChanged } = props;
const { onViewportChanged, onBaseLayerChanged, onBoundsChanged } = props;
const mapEvents = useMapEvents({
zoomend: () => {
onViewportChanged({
center: [mapEvents.getCenter().lat, mapEvents.getCenter().lng],
zoom: mapEvents.getZoom(),
});
const boundsViewport: BoundsViewport = {
bounds: constrainLongitudeToMainWorld(
boundsToGeoBBox(mapEvents.getBounds())
),
zoomLevel: mapEvents.getZoom(),
};
onBoundsChanged(boundsViewport);
},
moveend: () => {
onViewportChanged({
center: [mapEvents.getCenter().lat, mapEvents.getCenter().lng],
zoom: mapEvents.getZoom(),
});
const boundsViewport: BoundsViewport = {
bounds: constrainLongitudeToMainWorld(
boundsToGeoBBox(mapEvents.getBounds())
),
zoomLevel: mapEvents.getZoom(),
};
onBoundsChanged(boundsViewport);
},
baselayerchange: (e: { name: string }) => {
onBaseLayerChanged && onBaseLayerChanged(e.name as BaseLayerChoice);
Expand Down Expand Up @@ -501,55 +437,58 @@ function CustomZoomControl(props: CustomZoomControlProps) {
);
}

// compute markers bounds
function computeMarkersBounds(markers: ReactElement<BoundsDriftMarkerProps>[]) {
if (markers) {
let [minLat, maxLat, minLng, maxLng] = [90, -90, 180, -180];

for (const marker of markers) {
const bounds = marker.props.bounds;
const ne = bounds.northEast;
const sw = bounds.southWest;

if (ne.lat > maxLat) maxLat = ne.lat;
if (ne.lat < minLat) minLat = ne.lat;

if (ne.lng > maxLng) maxLng = ne.lng;
if (ne.lng < minLng) minLng = ne.lng;

if (sw.lat > maxLat) maxLat = sw.lat;
if (sw.lat < minLat) minLat = sw.lat;

if (sw.lng > maxLng) maxLng = sw.lng;
if (sw.lng < minLng) minLng = sw.lng;
}
function boundsToGeoBBox(bounds: LatLngBounds): Bounds {
var south = bounds.getSouth();
if (south < -90) {
south = -90;
}
var north = bounds.getNorth();
if (north > 90) {
north = 90;
}
var east = bounds.getEast();
var west = bounds.getWest();

return {
southWest: { lat: minLat, lng: minLng },
northEast: { lat: maxLat, lng: maxLng },
};
} else {
return null;
if (east - west > 360) {
const center = (east + west) / 2;
west = center - 180;
east = center + 180;
}
}

// compute bounding box
function computeBoundingBox(markersBounds: MapVEuBounds | null) {
if (markersBounds) {
const ne = markersBounds.northEast;
const sw = markersBounds.southWest;
return {
southWest: { lat: south, lng: west },
northEast: { lat: north, lng: east },
};
}

const bufferFactor = 0.1;
const latBuffer = (ne.lat - sw.lat) * bufferFactor;
const lngBuffer = (ne.lng - sw.lng) * bufferFactor;
// put longitude bounds within normal -180 to 180 range
function constrainLongitudeToMainWorld({
southWest: { lat: south, lng: west },
northEast: { lat: north, lng: east },
}: Bounds): Bounds {
let newEast = east;
let newWest = west;
while (newEast > 180) {
newEast -= 360;
}
while (newEast < -180) {
newEast += 360;
}
while (newWest < -180) {
newWest += 360;
}
while (newWest > 180) {
newWest -= 360;
}

const boundingBox = new LatLngBounds([
[sw.lat - latBuffer, sw.lng - lngBuffer],
[ne.lat + latBuffer, ne.lng + lngBuffer],
]);
// fully zoomed out, the longitude bounds are often the same
// but we need to make sure that west is slightly greater than east
// so that they "wrap around" the whole globe
// (if west was slightly less than east, it would represent a very tiny sliver)
if (Math.abs(newEast - newWest) < 1e-8) newWest = newEast + 1e-8;

return boundingBox;
} else {
return undefined;
}
return {
southWest: { lat: south, lng: newWest },
northEast: { lat: north, lng: newEast },
};
}
3 changes: 1 addition & 2 deletions packages/libs/components/src/map/MapVEuMapSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ interface MapVEuMapPropsCutAndPasteCopy {
animation: {
method: string;
duration: number;
animationFunction: AnimationFunction;
animationFunction: AnimationFunction<BoundsDriftMarkerProps>;
} | null;
showGrid: boolean;
}
Expand Down Expand Up @@ -167,7 +167,6 @@ export default function MapVEuMapSidebarSibling({
</LayersControl>

<SemanticMarkers
onBoundsChanged={onViewportChanged}
markers={markers}
animation={animation}
// nudge={nudge}
Expand Down
Loading