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

Features/6301 mobile enhancement #284

Merged
merged 7 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
169 changes: 81 additions & 88 deletions src/components/map/mapbox/component/MapPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
forwardRef,
ForwardRefRenderFunction,
ReactNode,
import React, {
MouseEventHandler,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useState,
} from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "@mui/material/styles";
Expand All @@ -20,6 +16,7 @@ import { OGCCollection } from "../../../common/store/OGCCollectionDefinitions";
import { useAppDispatch } from "../../../common/store/hooks";
import ComplexMapHoverTip from "../../../common/hover-tip/ComplexMapHoverTip";
import { TabNavigation } from "../../../../hooks/useTabNavigation";
import useBreakpoint from "../../../../hooks/useBreakpoint";

interface MapPopupProps {
layerId: string;
Expand All @@ -40,14 +37,6 @@ const defaultPopupConfig: PopupConfig = {
popupHeight: 370,
};

const popup = new Popup({
closeButton: false,
closeOnClick: false,
maxWidth: "none",
// Add 5px vertical offset for popup
offset: [0, -5],
});

const renderLoadingBox = ({
popupHeight,
popupWidth,
Expand Down Expand Up @@ -78,15 +67,14 @@ const handleDatasetSelect = (
}
};

const MapPopup: ForwardRefRenderFunction<MapPopupRef, MapPopupProps> = (
{ layerId, onDatasetSelected, tabNavigation },
ref
) => {
const MapPopup: React.FC<MapPopupProps> = ({
layerId,
onDatasetSelected,
tabNavigation,
}) => {
const { map } = useContext(MapContext);
const dispatch = useAppDispatch();
const [isMouseOverPoint, setIsMouseOverPoint] = useState(false);
const [isMouseOverPopup, setIsMouseOverPopup] = useState(false);
const [popupContent, setPopupContent] = useState<ReactNode | null>(null);
const { isUnderLaptop } = useBreakpoint();

// TODO: there is bug that map popup is not re-render for the interaction with bookmark button
const getCollectionData = useCallback(
Expand All @@ -103,7 +91,10 @@ const MapPopup: ForwardRefRenderFunction<MapPopupRef, MapPopupProps> = (
);

const renderContentBox = useCallback(
(collection: void | OGCCollection) => {
(
collection: void | OGCCollection,
onMouseLeave: MouseEventHandler<HTMLDivElement>
) => {
if (!collection) {
return null;
}
Expand All @@ -117,8 +108,7 @@ const MapPopup: ForwardRefRenderFunction<MapPopupRef, MapPopupProps> = (
width: defaultPopupConfig.popupWidth,
borderRadius: 0,
}}
onMouseEnter={() => setIsMouseOverPopup(true)}
onMouseLeave={() => setIsMouseOverPopup(false)}
onMouseLeave={onMouseLeave}
>
<CardContent
sx={{
Expand All @@ -140,52 +130,61 @@ const MapPopup: ForwardRefRenderFunction<MapPopupRef, MapPopupProps> = (
[onDatasetSelected, tabNavigation]
);

const removePopup = useCallback(() => {
if (!isMouseOverPoint && !isMouseOverPopup) {
popup.remove();
if (map) {
map.getCanvas().style.cursor = "";
useEffect(() => {
const popup = new Popup({
closeButton: false,
closeOnClick: false,
maxWidth: "none",
// Add 5px vertical offset for popup
offset: [0, -5],
});

const container = document.createElement("div");
const root = createRoot(container);

const onPointMouseLeave = (event: MapLayerMouseEvent) => {
const rect = popup.getElement().getBoundingClientRect();
// Use event.originalEvent.clientX / clientY for screen coordinates
const mouseX = event.originalEvent.clientX;
const mouseY = event.originalEvent.clientY;
if (
mouseX >= rect.left &&
mouseX <= rect.right &&
mouseY >= rect.top &&
mouseY <= rect.bottom
) {
// Inside the popup, do nothing
} else {
popup.remove();
}
setPopupContent(null);
}
}, [isMouseOverPoint, isMouseOverPopup, map]);
};

// Force remove the popup regardless of mouse position.
const forceRemovePopup = useCallback(() => {
popup.remove();
if (map) {
map.getCanvas().style.cursor = "";
}
setPopupContent(null);
setIsMouseOverPoint(false);
setIsMouseOverPopup(false);
}, [map]);

useImperativeHandle(ref, () => ({
forceRemovePopup,
}));

// Delay the remove of popup for user move mouse into the popup when hover a point
const onPointMouseLeave = useCallback(() => {
setTimeout(() => setIsMouseOverPoint(false), 200);
setTimeout(removePopup, 200);
}, [removePopup]);

const onPointMouseEnter = useCallback(
async (ev: MapLayerMouseEvent): Promise<void> => {
const onPopupMouseLeave = (ev: React.MouseEvent<HTMLDivElement>) => {
const rect = popup.getElement().getBoundingClientRect();
if (
ev.clientX >= rect.left &&
ev.clientX <= rect.right &&
ev.clientY >= rect.top &&
ev.clientY <= rect.bottom
) {
// Inside the popup, do nothing
} else {
popup.remove();
}
};

const onPointMouseEnter = (ev: MapLayerMouseEvent) => {
if (!ev.target || !map) return;

setIsMouseOverPoint(true);
ev.target.getCanvas().style.cursor = "pointer";

if (ev.features && ev.features.length > 0) {
// Copy coordinates array.
const feature = ev.features[0] as Feature<Point>;
const geometry = feature.geometry;
const coordinates = geometry.coordinates.slice();

// Render a loading state in the popup
setPopupContent(
root.render(
renderLoadingBox({
popupHeight: defaultPopupConfig.popupHeight,
popupWidth: defaultPopupConfig.popupWidth,
Expand All @@ -196,46 +195,40 @@ const MapPopup: ForwardRefRenderFunction<MapPopupRef, MapPopupProps> = (
// subscribe to close event to clean up resource.
popup
.setLngLat(coordinates as [number, number])
.setDOMContent(document.createElement("div"))
.setDOMContent(container)
.addTo(map);

const uuid = feature.properties?.uuid as string;
const collection = await getCollectionData(uuid);
setPopupContent(renderContentBox(collection));
getCollectionData(uuid).then((collection) => {
root.render(renderContentBox(collection, onPopupMouseLeave));
});
}
},
[map, getCollectionData, renderContentBox]
);
};

useEffect(() => {
map?.on("mouseleave", layerId, onPointMouseLeave);
map?.on("mouseenter", layerId, onPointMouseEnter);
const onSourceChange = (event: any) => {
if (event.sourceId === layerId && event.isSourceLoaded) {
popup.remove();
}
};
// No need to show popup as no hover event for handheld device
if (!isUnderLaptop) {
map?.on("mouseleave", layerId, onPointMouseLeave);
map?.on("mouseenter", layerId, onPointMouseEnter);
// Handle case when move out of map without leaving popup box
// then do a search
map?.on("sourcedata", onSourceChange);
}

return () => {
map?.off("mouseleave", layerId, onPointMouseLeave);
map?.off("mouseenter", layerId, onPointMouseEnter);
map?.off("sourcedata", onSourceChange);
popup?.remove();
setTimeout(() => root?.unmount(), 500);
};
}, [map, layerId, onPointMouseEnter, onPointMouseLeave]);

useEffect(() => {
removePopup();
}, [isMouseOverPoint, isMouseOverPopup, removePopup]);

useEffect(() => {
if (popupContent) {
const container = document.createElement("div");
const root = createRoot(container);
root.render(popupContent);
popup.setDOMContent(container);

// Important to free up resources, and must timeout to avoid race condition
return () => {
setTimeout(() => root.unmount(), 500);
};
}
}, [popupContent]);
}, [getCollectionData, isUnderLaptop, layerId, map, renderContentBox]);

return null;
return <React.Fragment />;
};

export default forwardRef(MapPopup);
export default MapPopup;
1 change: 0 additions & 1 deletion src/components/map/mapbox/component/SpiderDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,6 @@ const SpiderDiagram: FC<SpiderDiagramProps> = ({
<>
{spiderifiedCluster && (
<MapPopup
ref={mapPopupRef}
layerId={getSpiderPinsLayerId(spiderifiedCluster.id)}
onDatasetSelected={onDatasetSelected}
tabNavigation={tabNavigation}
Expand Down
57 changes: 42 additions & 15 deletions src/components/map/mapbox/controls/NavigationControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,63 @@ import MapContext from "../MapContext";
import { NavigationControl as MapboxNavigationControl } from "mapbox-gl";

interface NavigationControlProps {
visible?: boolean;
showCompass?: boolean;
showZoom?: boolean;
visualizePitch?: boolean;
}

const NavigationControl = ({
visible = true,
showCompass = true,
showZoom = true,
visualizePitch = true,
}: NavigationControlProps) => {
const { map } = useContext(MapContext);
const [init, setInit] = useState<boolean>(false);
const [_, setControl] = useState<MapboxNavigationControl | undefined>(
undefined
);

useEffect(() => {
if (map === null) return;

setInit((prev) => {
if (prev === false) {
const n = new MapboxNavigationControl({
showCompass: showCompass,
showZoom: showZoom,
visualizePitch: visualizePitch,
});

map?.addControl(n, "top-left");
}
return true;
});
if (map !== null) {
setControl((prev: MapboxNavigationControl | undefined) => {
if (!prev) {
const n = new MapboxNavigationControl({
showCompass: showCompass,
showZoom: showZoom,
visualizePitch: visualizePitch,
});

map?.addControl(n, "top-left");
return n;
}
return prev;
});
}
}, [map, showCompass, showZoom, visualizePitch]);

useEffect(() => {
const zoomIn: HTMLButtonElement | null = document.querySelector(
".mapboxgl-ctrl-zoom-in"
);
if (zoomIn) {
zoomIn.style.display = visible ? "block" : "none";
}

const zoomOut: HTMLButtonElement | null = document.querySelector(
".mapboxgl-ctrl-zoom-out"
);
if (zoomOut) {
zoomOut.style.display = visible ? "block" : "none";
}

const compass: HTMLButtonElement | null = document.querySelector(
".mapboxgl-ctrl-compass"
);
if (compass) {
compass.style.display = visible ? "block" : "none";
}
}, [visible]);
return <React.Fragment />;
};

Expand Down
9 changes: 7 additions & 2 deletions src/components/map/mapbox/controls/menu/MenuControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class MapControl implements IControl {
private container: HTMLDivElement | null = null;
private root: Root | null = null;
private readonly component: MapControlType;
private height: string = "";
private marginTop: string = "";

// When the user clicks somewhere on the map, notify the MenuControl
private readonly mapClickHandler: (event: MapMouseEvent) => void;
Expand All @@ -49,8 +51,8 @@ class MapControl implements IControl {
if (this.container) {
this.container.style.visibility = visible ? "visible" : "hidden";
// Magic numbers below are Mapbox default styles for controls
this.container.style.height = visible ? "29px" : "0px";
this.container.style.marginTop = visible ? "10px" : "0px";
this.container.style.height = visible ? this.height : "0px";
this.container.style.marginTop = visible ? this.marginTop : "0px";
}
}

Expand All @@ -65,6 +67,9 @@ class MapControl implements IControl {
// according to document, you need "!" at the end of container
this.root = createRoot(this.container!);
this.render();
// Remember the initial height and marginTop
this.height = this.container!.style.height;
this.marginTop = this.container!.style.marginTop;

map?.on("click", this.mapClickHandler);
map?.on("movestart", this.mapMoveStartHandler);
Expand Down
Loading