From 732296d179a4efa4327fc19ff428464204b445d0 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Sat, 25 Apr 2026 03:41:02 -0400 Subject: [PATCH] feat: Updated the map to show the coordinates as the user moves the cursor over the map. Changed the cursor to a crosshairs to make it easier to place map markers. --- .../inertia/components/maps/MapComponent.tsx | 133 +++++++++++++++--- admin/inertia/pages/maps.tsx | 35 +++-- install/management_compose.yaml | 14 +- 3 files changed, 145 insertions(+), 37 deletions(-) diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index bac307d..1e1f00a 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -13,22 +13,34 @@ 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' -export default function MapComponent() { +export default function MapComponent({ isHoveringUI, showCoordinatesEnabled, }: {isHoveringUI: boolean, showCoordinatesEnabled: boolean}) { const mapRef = useRef(null) const { markers, addMarker, deleteMarker } = useMapMarkers() + const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null) const [markerName, setMarkerName] = useState('') const [markerColor, setMarkerColor] = useState('orange') const [selectedMarkerId, setSelectedMarkerId] = useState(null) + const [scaleUnit, setScaleUnit] = useState( () => (localStorage.getItem('nomad:map-scale-unit') as ScaleUnit) || 'metric' ) + const [cursorLngLat, setCursorLngLat] = useState<{ + lng: number + lat: number + x: number + y: number + } | null>(null) + + const [showCoordinates, setShowCoordinates] = useState(false) + const toggleScaleUnit = useCallback(() => { setScaleUnit((prev) => { const next = prev === 'metric' ? 'imperial' : 'metric' @@ -37,15 +49,45 @@ export default function MapComponent() { }) }, []) - // Add the PMTiles protocol to maplibre-gl useEffect(() => { - let protocol = new Protocol() + const protocol = new Protocol() maplibregl.addProtocol('pmtiles', protocol.tile) + return () => { maplibregl.removeProtocol('pmtiles') } }, []) + const hideCoordinates = useCallback(() => { + setShowCoordinates(false) + setCursorLngLat(null) + }, []) + + const handleMouseMove = useCallback((e: MapLayerMouseEvent) => { + const target = e.originalEvent.target as HTMLElement | null + + if (target?.closest('.maplibregl-control-container, .maplibregl-ctrl')) { + hideCoordinates() + return + } + + if (!showCoordinatesEnabled || + isHoveringUI || + 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]) + const handleMapClick = useCallback((e: MapLayerMouseEvent) => { setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) setMarkerName('') @@ -78,6 +120,21 @@ export default function MapComponent() { return ( +
{ + const target = e.target as HTMLElement | null + + if ( + target?.closest( + '.maplibregl-control-container, .maplibregl-ctrl, .maplibregl-ctrl-group, .maplibregl-ctrl-scale' + ) + ) { + hideCoordinates() + } + }} + > + + {showCoordinates && showCoordinates && cursorLngLat && ( +
+ {cursorLngLat.lat.toFixed(6)}, {cursorLngLat.lng.toFixed(6)} +
+ )} +
+
- {/* Existing markers */} {markers.map((marker) => ( ))} - {/* Popup for selected marker */} {selectedMarker && ( )} - {/* Popup for placing a new marker */} {placingMarker && ( setPlacingMarker(null)} closeOnClick={false} > -
+
+
{PIN_COLORS.map((c) => ( ))}
+
+
- {/* Marker panel overlay */} - +
+ +
) } diff --git a/admin/inertia/pages/maps.tsx b/admin/inertia/pages/maps.tsx index 9fe20e0..d8585b1 100644 --- a/admin/inertia/pages/maps.tsx +++ b/admin/inertia/pages/maps.tsx @@ -5,10 +5,13 @@ 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' 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 @@ -20,19 +23,35 @@ export default function Maps(props: {
{/* Nav and alerts are overlayed */} -
+
setIsHoveringUI(true)} + onMouseLeave={() => setIsHoveringUI(false)} + >

Back to Home

- - - Manage Map Regions - - +
+ + + + + Manage Map Regions + + +
{alertMessage && ( -
+
setIsHoveringUI(true)} + onMouseLeave={() => setIsHoveringUI(false)} + > )}
- +
diff --git a/install/management_compose.yaml b/install/management_compose.yaml index f4c7b5f..1ddcb00 100644 --- a/install/management_compose.yaml +++ b/install/management_compose.yaml @@ -9,8 +9,8 @@ name: project-nomad services: admin: - image: ghcr.io/crosstalk-solutions/project-nomad:latest - pull_policy: always + image: project-nomad:local + pull_policy: never #ghcr.io/crosstalk-solutions/project-nomad:latest container_name: nomad_admin restart: unless-stopped extra_hosts: @@ -27,18 +27,18 @@ services: - PORT=8080 - LOG_LEVEL=info # APP_KEY needs to be at least 16 chars or will fail validation and container won't start! - - APP_KEY=replaceme + - APP_KEY=ce9579a56b32e05ee432be91e6d1d9b95cb3a00eb92789559569decdb76c4db2 # # Leave HOST as is so the admin server listens all interfaces within the container - this doesn't affect how you access it from the host, it's just for internal container networking - HOST=0.0.0.0 # URL should be set to the URL you will access the admin interface at (e.g. http://localhost:8080 or http://192.168.1.x:8080) - - URL=replaceme + - URL=http://nomad-dev.brewerhomestead.com/ - DB_HOST=mysql # If you change the MySQL port, make sure to update this accordingly - DB_PORT=3306 - DB_DATABASE=nomad - DB_USER=nomad_user # Needs to match the MYSQL_PASSWORD in the mysql service! - - DB_PASSWORD=replaceme + - DB_PASSWORD=2bc4dad18d5a0ecf5914428d91ebde27d42e8afafd72c73089a0dd1eba9ae1fc - DB_NAME=nomad - DB_SSL=false - REDIS_HOST=redis @@ -72,11 +72,11 @@ services: container_name: nomad_mysql restart: unless-stopped environment: - - MYSQL_ROOT_PASSWORD=replaceme + - MYSQL_ROOT_PASSWORD=d797ecbd5cee07758909a628cb0a2a75285dc3bac42d1513a8d2aed95c632577 - MYSQL_DATABASE=nomad - MYSQL_USER=nomad_user # Needs to match DB_PASSWORD in the admin service! - - MYSQL_PASSWORD=replaceme + - MYSQL_PASSWORD=2bc4dad18d5a0ecf5914428d91ebde27d42e8afafd72c73089a0dd1eba9ae1fc volumes: - /opt/project-nomad/mysql:/var/lib/mysql # Persist MySQL data on the host. This path can be changed if needed, just make sure it's writable by the container. Host persistence is important for the database to ensure your data isn't lost when the container is removed or updated. healthcheck: