From 1d7c40c37b35ecfc52a8253fc7fb413bb6a915ee Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Sat, 25 Apr 2026 04:18:49 -0400 Subject: [PATCH] Moved the scale unit control to its own component file for easier maintenance. Enhanced the behavior of the coordinate display on the map to not display when over the on screen controls, and the navigation bar. Added a toggle to turn off the coordinate display if the user doesn't wish to see it. Intentionally left the coordinate display when over a map marker so that the coordinates of the map marker can be estimated. In the future I intend to add the coordinates of a map marker when the map marker is clicked so that behavior may change in the future. --- .../components/maps/CoordinateOverlay.tsx | 25 ++ .../inertia/components/maps/MapComponent.tsx | 422 +++++++++--------- .../components/maps/ScaleUnitToggle.tsx | 46 ++ admin/inertia/pages/maps.tsx | 62 ++- 4 files changed, 316 insertions(+), 239 deletions(-) create mode 100644 admin/inertia/components/maps/CoordinateOverlay.tsx create mode 100644 admin/inertia/components/maps/ScaleUnitToggle.tsx diff --git a/admin/inertia/components/maps/CoordinateOverlay.tsx b/admin/inertia/components/maps/CoordinateOverlay.tsx new file mode 100644 index 0000000..43166bb --- /dev/null +++ b/admin/inertia/components/maps/CoordinateOverlay.tsx @@ -0,0 +1,25 @@ +type CoordinateOverlayProps = { + latitude: number + longitude: number + x: number + y: number +} + +export default function CoordinateOverlay({ + latitude, + longitude, + x, + y, +}: CoordinateOverlayProps) { + return ( +
+ {latitude.toFixed(6)}, {longitude.toFixed(6)} +
+ ) +} diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index 1e1f00a..07fe1c6 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -9,20 +9,35 @@ import Map, { import type { MapRef, MapLayerMouseEvent } from 'react-map-gl/maplibre' import maplibregl from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' + import { Protocol } from 'pmtiles' import { useEffect, useRef, useState, useCallback } from 'react' -type ScaleUnit = 'imperial' | 'metric' - import { useMapMarkers, PIN_COLORS } from '~/hooks/useMapMarkers' import type { PinColorId } from '~/hooks/useMapMarkers' + import MarkerPin from './MarkerPin' import MarkerPanel from './MarkerPanel' +import CoordinateOverlay from './CoordinateOverlay' +import ScaleUnitToggle from './ScaleUnitToggle' -export default function MapComponent({ isHoveringUI, showCoordinatesEnabled, }: {isHoveringUI: boolean, showCoordinatesEnabled: boolean}) { +type ScaleUnit = 'imperial' | 'metric' + +type MapComponentProps = { + isHoveringUI: boolean + showCoordinatesEnabled: boolean +} + +export default function MapComponent({ + isHoveringUI, + showCoordinatesEnabled, +}: MapComponentProps) { const mapRef = useRef(null) + const animationFrameRef = useRef(null) + const { markers, addMarker, deleteMarker } = useMapMarkers() + const [isDraggingMap, setIsDraggingMap] = useState(false) const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null) const [markerName, setMarkerName] = useState('') const [markerColor, setMarkerColor] = useState('orange') @@ -38,16 +53,8 @@ export default function MapComponent({ isHoveringUI, showCoordinatesEnabled, }: x: number y: number } | null>(null) - - const [showCoordinates, setShowCoordinates] = useState(false) - const toggleScaleUnit = useCallback(() => { - setScaleUnit((prev) => { - const next = prev === 'metric' ? 'imperial' : 'metric' - localStorage.setItem('nomad:map-scale-unit', next) - return next - }) - }, []) + const [showCoordinates, setShowCoordinates] = useState(false) useEffect(() => { const protocol = new Protocol() @@ -58,35 +65,54 @@ export default function MapComponent({ isHoveringUI, showCoordinatesEnabled, }: } }, []) + useEffect(() => { + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + }, []) + const hideCoordinates = useCallback(() => { setShowCoordinates(false) setCursorLngLat(null) }, []) - const handleMouseMove = useCallback((e: MapLayerMouseEvent) => { - const target = e.originalEvent.target as HTMLElement | null + const handleScaleUnitChange = useCallback((unit: ScaleUnit) => { + setScaleUnit(unit) + localStorage.setItem('nomad:map-scale-unit', unit) + }, []) - if (target?.closest('.maplibregl-control-container, .maplibregl-ctrl')) { - hideCoordinates() - return - } + const handleMouseMove = useCallback( + (e: MapLayerMouseEvent) => { + const target = e.originalEvent.target as HTMLElement | null - if (!showCoordinatesEnabled || - isHoveringUI || - target?.closest('.maplibregl-control-container, .maplibregl-ctrl') - ) { - hideCoordinates() - return - } + if ( + !showCoordinatesEnabled || + isHoveringUI || + isDraggingMap || + target?.closest('.maplibregl-control-container, .maplibregl-ctrl') + ) { + hideCoordinates() + return + } - setShowCoordinates(true) - setCursorLngLat({ - lng: e.lngLat.lng, - lat: e.lngLat.lat, - x: e.point.x, - y: e.point.y, - }) - }, [hideCoordinates, isHoveringUI]) + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + + animationFrameRef.current = requestAnimationFrame(() => { + setShowCoordinates(true) + setCursorLngLat({ + lng: e.lngLat.lng, + lat: e.lngLat.lat, + x: e.point.x, + y: e.point.y, + }) + }) + }, + [hideCoordinates, isHoveringUI, isDraggingMap, showCoordinatesEnabled] + ) const handleMapClick = useCallback((e: MapLayerMouseEvent) => { setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) @@ -120,203 +146,169 @@ export default function MapComponent({ isHoveringUI, showCoordinatesEnabled, }: return ( -
{ - const target = e.target as HTMLElement | null - - if ( - target?.closest( - '.maplibregl-control-container, .maplibregl-ctrl, .maplibregl-ctrl-group, .maplibregl-ctrl-scale' - ) - ) { +
{ + setIsDraggingMap(false) hideCoordinates() - } - }} - > - { + const target = e.target as HTMLElement | null + + if ( + target?.closest( + '.maplibregl-control-container, .maplibregl-ctrl, .maplibregl-ctrl-group, .maplibregl-ctrl-scale' + ) + ) { + hideCoordinates() + } }} - onClick={handleMapClick} - onMouseMove={handleMouseMove} - onMouseLeave={hideCoordinates} > - - - + { + setIsDraggingMap(true) + hideCoordinates() + }} + onMouseUp={() => { + setIsDraggingMap(false) + }} + onDragStart={() => { + setIsDraggingMap(true) + hideCoordinates() + }} + onDragEnd={() => { + setIsDraggingMap(false) + hideCoordinates() + }} + onClick={handleMapClick} + onMouseMove={handleMouseMove} + onMouseLeave={hideCoordinates} + > + + + - {showCoordinates && showCoordinates && cursorLngLat && ( -
- {cursorLngLat.lat.toFixed(6)}, {cursorLngLat.lng.toFixed(6)} -
- )} - -
-
- - - -
-
- - {markers.map((marker) => ( - { - e.originalEvent.stopPropagation() - setSelectedMarkerId(marker.id === selectedMarkerId ? null : marker.id) - setPlacingMarker(null) - }} - > - c.id === marker.color)?.hex} - active={marker.id === selectedMarkerId} + {showCoordinates && cursorLngLat && ( + - - ))} + )} - {selectedMarker && ( - setSelectedMarkerId(null)} - closeOnClick={false} - > -
{selectedMarker.name}
-
- )} + - {placingMarker && ( - setPlacingMarker(null)} - closeOnClick={false} - > -
- setMarkerName(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveMarker() - if (e.key === 'Escape') setPlacingMarker(null) - }} - className="block w-full rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500" + {markers.map((marker) => ( + { + e.originalEvent.stopPropagation() + setSelectedMarkerId(marker.id === selectedMarkerId ? null : marker.id) + setPlacingMarker(null) + }} + > + c.id === marker.color)?.hex} + active={marker.id === selectedMarkerId} /> + + ))} -
- {PIN_COLORS.map((c) => ( + {selectedMarker && ( + setSelectedMarkerId(null)} + closeOnClick={false} + > +
{selectedMarker.name}
+
+ )} + + {placingMarker && ( + setPlacingMarker(null)} + closeOnClick={false} + > +
+ setMarkerName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveMarker() + if (e.key === 'Escape') setPlacingMarker(null) + }} + className="block w-full rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500" + /> + +
+ {PIN_COLORS.map((c) => ( + + ))} +
+ +
- ))} -
-
- - - + +
-
- - )} - + + )} +
diff --git a/admin/inertia/components/maps/ScaleUnitToggle.tsx b/admin/inertia/components/maps/ScaleUnitToggle.tsx new file mode 100644 index 0000000..ba0bceb --- /dev/null +++ b/admin/inertia/components/maps/ScaleUnitToggle.tsx @@ -0,0 +1,46 @@ +type ScaleUnit = 'imperial' | 'metric' + +type ScaleUnitToggleProps = { + scaleUnit: ScaleUnit + onChange: (unit: ScaleUnit) => void + onMouseEnter?: () => void +} + +export default function ScaleUnitToggle({ + scaleUnit, + onChange, + onMouseEnter, +}: ScaleUnitToggleProps) { + return ( +
+
+ + + +
+
+ ) +} diff --git a/admin/inertia/pages/maps.tsx b/admin/inertia/pages/maps.tsx index d8585b1..a1d8df1 100644 --- a/admin/inertia/pages/maps.tsx +++ b/admin/inertia/pages/maps.tsx @@ -1,29 +1,34 @@ -import MapsLayout from '~/layouts/MapsLayout' +import { useState } from 'react' import { Head, Link, router } from '@inertiajs/react' +import { IconArrowLeft } from '@tabler/icons-react' + +import MapsLayout from '~/layouts/MapsLayout' import MapComponent from '~/components/maps/MapComponent' import StyledButton from '~/components/StyledButton' -import { IconArrowLeft } from '@tabler/icons-react' -import { FileEntry } from '../../types/files' import Alert from '~/components/Alert' -import { useState } from 'react' + +import { FileEntry } from '../../types/files' export default function Maps(props: { maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] } }) { const [isHoveringUI, setIsHoveringUI] = useState(false) const [showMapCoordinates, setShowMapCoordinates] = useState(true) + const alertMessage = !props.maps.baseAssetsExist ? 'The base map assets have not been installed. Please download them first to enable map functionality.' : props.maps.regionFiles.length === 0 - ? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.' - : null + ? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.' + : null return ( +
- {/* Nav and alerts are overlayed */} -
setIsHoveringUI(true)} onMouseLeave={() => setIsHoveringUI(false)} > @@ -31,24 +36,28 @@ export default function Maps(props: {

Back to Home

-
- - - - Manage Map Regions - - -
+
+ + + + + Manage Map Regions + + +
+ + {/* Alert */} {alertMessage && ( -
setIsHoveringUI(true)} onMouseLeave={() => setIsHoveringUI(false)} > @@ -66,8 +75,13 @@ export default function Maps(props: { />
)} + + {/* Map */}
- +