Skip to content

Commit

Permalink
Collection map type (#399)
Browse files Browse the repository at this point in the history
* Restructure side menu to allow for subheadings. (#398)

Changes include:

- Variable renaming
- Modifications to finding active menu item and active map type
- Extracting some components

* Some additional clean-up (#401)

* Move active viz to realm of map type config, and clean up some labels and ids.

* Have the active map type config panel open by default

* Remember whether side panel is expanded on subsequent page loads

* Remove unused app state property

* EDA Map Refactor: Introduce MapType plugin and implementations (#424)

* Remove marker-related props from MapVEuMap

* Move onBoundsChange logic to MapVEuMap; clean up SemanticMarkers hooks

* Extract getDefaultAxisRange function

* Move DraggableLegendPanel to its own module

* Move defaultAnimation to its own module

* Extract bubble overlay config into its own function

* Add MapType plugins

* Wire up MapType plugins

* Use plugin displayName in menu

* Reduce props passed to getData

* Checkpoint commit using @tanstack/query

* Incorporate additional changes from merge

* Checkpoint

- Remove `getData` from `MapTypePlugin` interface
- Utilize react-query to cache network requests
- Move constants from index module, to prevent circular imports

* Add TODO

* Propagate selected values for marker configuration to overlay config

* Remove TODO

* Keep previous query data; use isFetching status to show spinners for maker data

* Introduce `MapTypeHeaderDetails` to `MapTypePlugin`

* Remove unused type

* Crudely reimplment "flyToData"

* Pass valueSpec to standalone map markers

* Remove out of date comments and code

* Remove old comments

* Move shared interface to shared file

* Use destructured variables

* bring back old behaviour for now

---------

Co-authored-by: Bob <[email protected]>

* Post-merge updates

* Post-merge updates, 2

* Fix filter button in header

* remove file

---------

Co-authored-by: Connor Howington <[email protected]>
Co-authored-by: Bob <[email protected]>
  • Loading branch information
3 people authored Oct 25, 2023
1 parent 016427b commit 38c98c7
Show file tree
Hide file tree
Showing 50 changed files with 4,164 additions and 2,181 deletions.
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

0 comments on commit 38c98c7

Please sign in to comment.