diff --git a/apps/lrauv-dash2/components/DeploymentMap.tsx b/apps/lrauv-dash2/components/DeploymentMap.tsx index 25ea7796..137b3978 100644 --- a/apps/lrauv-dash2/components/DeploymentMap.tsx +++ b/apps/lrauv-dash2/components/DeploymentMap.tsx @@ -1,4 +1,5 @@ import dynamic from 'next/dynamic' +import { useRouter } from 'next/router' import React, { useCallback, useState, useRef, useEffect, useMemo } from 'react' import { useManagedWaypoints } from '@mbari/react-ui' import useGoogleElevator from '../lib/useGoogleElevator' @@ -11,14 +12,20 @@ import toast from 'react-hot-toast' import { createLogger } from '@mbari/utils' import VehicleColorsModal from './VehicleColorsModal' import useTrackedVehicles from '../lib/useTrackedVehicles' -import { useDepthRequest } from '@mbari/utils/useDepthRequest' - // This is a tricky workaround to prevent leaflet from crashing next.js // SSR. If we don't do this, the leaflet map will be loaded server side // and throw a window error. const Map = dynamic(() => import('@mbari/react-ui/dist/Map/Map'), { ssr: false, }) + +const MapDepthDisplay = dynamic( + () => + import('@mbari/react-ui/dist/Map/Map').then((m) => ({ + default: m.MapDepthDisplay, + })), + { ssr: false } +) const DraggableMarker = dynamic(() => import('./DraggableMarker'), { ssr: false, }) @@ -64,6 +71,7 @@ const DeploymentMap: React.FC = ({ startTime, endTime, }) => { + const router = useRouter() const mapRef = useRef(null) const { updatedWaypoints, @@ -80,18 +88,7 @@ const DeploymentMap: React.FC = ({ ), [updatedWaypoints, handleWaypointsUpdate] ) - const { handleDepthRequest, elevationAvailable } = useGoogleElevator() - // Depth request hook - const { handleDepthRequestWithFeedback } = useDepthRequest( - handleDepthRequest, - { - warningToastId: 'depth-unavailable', - errorToastId: 'depth-result', - loadingToastId: 'depth-loading', - warningToastClass: 'blue-toast', - toastDuration: 5000, - } - ) + const { handleDepthRequest } = useGoogleElevator() // Filter out waypoints with NaN lat/lon const plottedWaypoints = updatedWaypoints.filter( @@ -477,20 +474,17 @@ const DeploymentMap: React.FC = ({ setShowLayersModal(false) }, []) - const handleVehicleColorRequest = useCallback(() => { - // Add debugging to verify values - logger.debug('Opening color modal with:', { - vehicleName, - trackedVehicles, - modalTrackedVehicles: vehicleName ? [vehicleName] : [], - }) - - setColorModalPosition({ - top: 100, - left: 100, - }) - setColorModalOpen(true) - }, [vehicleName, trackedVehicles]) + const handleVehicleColorRequest = useCallback( + (anchor?: { top: number; left: number }) => { + if (anchor) { + setColorModalPosition(anchor) + } else { + setColorModalPosition({ top: 100, left: 100 }) + } + setColorModalOpen(true) + }, + [] + ) const handleCloseVehicleColors = useCallback((vehicleName?: string) => { setShowVehicleColors(false) @@ -551,16 +545,22 @@ const DeploymentMap: React.FC = ({ ) : null} { logger.debug('Map is ready!') mapRef.current = map - }} - onRequestDepth={async (lat, lng) => { - const result = await handleDepthRequestWithFeedback(lat, lng) - return result.depth ?? 0 + ;[200, 800].forEach((delay) => { + setTimeout(() => { + try { + map.invalidateSize() + } catch (e) { + logger.warn('Could not invalidate map size:', e) + } + }, delay) + }) }} center={center} centerZoom={centerZoom} @@ -624,6 +624,16 @@ const DeploymentMap: React.FC = ({ ) } > + {selectedStations.map((station) => { const lng = station.geojson.geometry.coordinates[0] const lat = station.geojson.geometry.coordinates[1] @@ -675,15 +685,15 @@ const DeploymentMap: React.FC = ({ disableAutoFit={isTimelineScrubbing} /> )} - setColorModalOpen(false)} - anchorPosition={colorModalPosition} - trackedVehicles={vehicleName ? [vehicleName] : []} - activeVehicle={vehicleName || undefined} - forceShowAll={true} - /> + setColorModalOpen(false)} + anchorPosition={colorModalPosition} + trackedVehicles={vehicleName ? [vehicleName] : []} + activeVehicle={vehicleName || undefined} + forceShowAll={true} + /> ) } diff --git a/apps/lrauv-dash2/components/GoogleMapsProvider.tsx b/apps/lrauv-dash2/components/GoogleMapsProvider.tsx index d04cee7b..6e572bc8 100644 --- a/apps/lrauv-dash2/components/GoogleMapsProvider.tsx +++ b/apps/lrauv-dash2/components/GoogleMapsProvider.tsx @@ -2,12 +2,10 @@ import React, { useState, useEffect } from 'react' import { useGoogleMapsApiKey } from './useGoogleMapsApiKey' import toast from 'react-hot-toast' import { createLogger } from '@mbari/utils' +import { initLeafletGoogle } from '../lib/leafletPlugins' const logger = createLogger('GoogleMapsProvider') -// Global variable to track if script has been loaded -let googleMapsScriptAdded = false - interface GoogleMapsProviderProps { children: React.ReactNode } @@ -15,10 +13,9 @@ interface GoogleMapsProviderProps { export const GoogleMapsProvider: React.FC = ({ children, }) => { - const { apiKey, isLoading, error, keySource } = useGoogleMapsApiKey() + const { apiKey, isLoading, keySource } = useGoogleMapsApiKey() const [isLoaded, setIsLoaded] = useState(false) - // Check if Maps API is already available on mount useEffect(() => { if (typeof window !== 'undefined' && window.google?.maps) { setIsLoaded(true) @@ -26,17 +23,14 @@ export const GoogleMapsProvider: React.FC = ({ } }, []) - // Handle script loading based on API key useEffect(() => { if (isLoading) return - // Log the source of the API key if (keySource === 'server') { logger.debug('Using Google Maps from Tethys API') } else if (keySource === 'local') { logger.debug('Using Google Maps from local .env') } else if (keySource === 'none' && !apiKey) { - // Notify but don't block interface toast.error( 'Google Maps API unavailable - Google Hybrid map and elevation data unavailable‼️ ', { @@ -48,62 +42,32 @@ export const GoogleMapsProvider: React.FC = ({ return } - // If Maps already loaded, nothing more to do if (window.google?.maps) { logger.debug('Google Maps already loaded via window.google') setIsLoaded(true) return } - // If we already started loading the script, don't add it again - if (googleMapsScriptAdded) { - logger.debug('Google Maps script tag already added') - return - } - - // Load the script if we have an API key - if (apiKey) { - logger.debug('Loading Google Maps API script') - - // Check if script already exists - const existingScript = document.getElementById('google-maps-script') - if (existingScript) { - logger.debug('Found existing Google Maps script tag') - return - } + if (!apiKey) return - const script = document.createElement('script') - script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places` - script.async = true - script.defer = true - script.id = 'google-maps-script' - - script.onload = () => { - logger.debug('✅ Google Maps API script loaded successfully') + logger.debug('Loading Google Maps API via initializer') + initLeafletGoogle(apiKey) + .then(() => { + logger.debug('✅ Google Maps API and Leaflet plugins ready') setIsLoaded(true) - } - - script.onerror = (e) => { - logger.error('❌ Google Maps script failed to load:', e) + }) + .catch((e) => { + logger.error('❌ Google Maps initializer failed:', e) toast.error('Google Hybrid map unavailable', { duration: 3000, id: 'maps-loading-error', }) - } - - document.head.appendChild(script) - - // Mark as added to prevent duplicate loading - googleMapsScriptAdded = true - } + }) }, [isLoading, keySource, apiKey]) - // Just a minimal loading indicator that doesn't take much space if (isLoading) { return
Loading maps...
} - // Always render children - maps will be available when loaded - // This avoids the flash of content and UI blocking return <>{children} } diff --git a/apps/lrauv-dash2/components/VehicleColorsModal.tsx b/apps/lrauv-dash2/components/VehicleColorsModal.tsx index 4ddbbb57..37de75a3 100644 --- a/apps/lrauv-dash2/components/VehicleColorsModal.tsx +++ b/apps/lrauv-dash2/components/VehicleColorsModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, useCallback } from 'react' import { useVehicleColors } from './VehicleColorsContext' -import { Modal } from '@mbari/react-ui/src/Modal/Modal' +import { Modal } from '@mbari/react-ui' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes, faEyeDropper } from '@fortawesome/free-solid-svg-icons' import { SketchPicker } from 'react-color' diff --git a/apps/lrauv-dash2/components/VehiclePath.tsx b/apps/lrauv-dash2/components/VehiclePath.tsx index 9d46b7dd..13ee5020 100644 --- a/apps/lrauv-dash2/components/VehiclePath.tsx +++ b/apps/lrauv-dash2/components/VehiclePath.tsx @@ -7,6 +7,7 @@ import { } from '@mbari/api-client' import { Polyline, useMap, Circle, Tooltip } from 'react-leaflet' import { LatLng, LeafletMouseEventHandlerFn } from 'leaflet' +import { useRouter } from 'next/router' import { useSharedPath } from './SharedPathContextProvider' import { distance } from '@turf/turf' import { parseISO, getTime } from 'date-fns' @@ -69,6 +70,7 @@ const VehiclePath: React.FC = ({ disableAutoFit = false, }) => { const map = useMap() + const router = useRouter() const { sharedPath, dispatch } = useSharedPath() const { data: lastDeployment } = useLastDeployment( @@ -282,13 +284,26 @@ const VehiclePath: React.FC = ({ ]) // OVERVIEW MAP - // Fit bounds for OverViewMap + // Re-run grouped fitBounds on route/tab switches after layout settles. useEffect(() => { + if (!grouped) return + const coords = Object.values(sharedPath).flat() - if (grouped && coords.length > 1) { - map.fitBounds(coords) + if (coords.length <= 1) return + + const applyFit = () => { + try { + map.invalidateSize() + map.fitBounds(coords) + } catch { + // noop; next delayed retry may succeed after layout settles + } } - }, [sharedPath, grouped, map]) + + applyFit() + const timers = [250, 800].map((delay) => setTimeout(applyFit, delay)) + return () => timers.forEach((t) => clearTimeout(t)) + }, [sharedPath, grouped, map, router.asPath]) // Determine Time Difference since last gpsFix const latest = diff --git a/apps/lrauv-dash2/lib/elevationService.ts b/apps/lrauv-dash2/lib/elevationService.ts index 15785aaa..e70b50c0 100644 --- a/apps/lrauv-dash2/lib/elevationService.ts +++ b/apps/lrauv-dash2/lib/elevationService.ts @@ -1,10 +1,9 @@ import { createLogger } from '@mbari/utils' -// Extend the Window interface +// Extend the Window interface (google is declared in types/global.d.ts) declare global { interface Window { [GOOGLE_MAPS_LOADED_FLAG]?: boolean - google?: any } } diff --git a/apps/lrauv-dash2/lib/leafletPlugins.ts b/apps/lrauv-dash2/lib/leafletPlugins.ts new file mode 100644 index 00000000..b4ef1492 --- /dev/null +++ b/apps/lrauv-dash2/lib/leafletPlugins.ts @@ -0,0 +1,94 @@ +/** + * Single initializer for Google Maps JS + Leaflet googlemutant plugin. + * Run only in the browser. Call from GoogleMapsProvider; do not load Google elsewhere. + */ + +let initPromise: Promise | null = null + +/** + * Ensures Google Maps JS is loaded (with places,elevation) then loads + * leaflet.gridlayer.googlemutant (registers L.gridLayer.googleMutant). + * Idempotent: repeated calls reuse the same promise. + */ +export function initLeafletGoogle(apiKey: string): Promise { + if (typeof window === 'undefined') { + return Promise.reject( + new Error('initLeafletGoogle must run in the browser') + ) + } + + if (window.google?.maps) { + return initPromise ?? (initPromise = loadGoogleMutantOnly()) + } + + if (!initPromise) { + initPromise = (async () => { + const existing = document.getElementById('google-maps-script') + if (existing) { + await waitForGoogle(10000) + await loadGoogleMutantOnly() + return + } + + const script = document.createElement('script') + script.id = 'google-maps-script' + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,elevation` + script.async = true + script.defer = true + + await new Promise((resolve, reject) => { + script.onload = () => resolve() + script.onerror = (e) => reject(e) + document.head.appendChild(script) + }) + + await waitForGoogle(10000) + await loadGoogleMutantOnly() + })().catch((err) => { + initPromise = null + throw err + }) + } + + return initPromise +} + +function waitForGoogle(timeoutMs = 10000): Promise { + if (window.google?.maps) return Promise.resolve() + + return new Promise((resolve, reject) => { + const start = Date.now() + let rafId = 0 + + const check = () => { + if (window.google?.maps) { + cancelAnimationFrame(rafId) + resolve() + return + } + + if (Date.now() - start >= timeoutMs) { + cancelAnimationFrame(rafId) + reject(new Error('Timed out waiting for Google Maps API')) + return + } + + rafId = requestAnimationFrame(check) + } + + rafId = requestAnimationFrame(check) + }) +} + +async function loadLeafletAndExposeGlobal(): Promise { + if (typeof window === 'undefined') return + if ((window as any).L) return + + const leaflet = await import('leaflet') + ;(window as any).L = leaflet.default ?? leaflet +} + +async function loadGoogleMutantOnly(): Promise { + await loadLeafletAndExposeGlobal() + await import('leaflet.gridlayer.googlemutant/dist/Leaflet.GoogleMutant.js') +} diff --git a/apps/lrauv-dash2/lib/useGoogleMaps.ts b/apps/lrauv-dash2/lib/useGoogleMaps.ts index 93163dc4..87612098 100644 --- a/apps/lrauv-dash2/lib/useGoogleMaps.ts +++ b/apps/lrauv-dash2/lib/useGoogleMaps.ts @@ -1,30 +1,31 @@ -import { useTethysApiContext } from '@mbari/api-client' import { useState, useEffect } from 'react' -import { Loader } from '@googlemaps/js-api-loader' +/** + * Simple "is Google Maps available" hook. Does not load or inject Google; + * the provider/initializer is the only loader. + */ export const useGoogleMaps = () => { - const { siteConfig } = useTethysApiContext() - const googleMapsApiKey = siteConfig?.appConfig.googleApiKey + const [mapsLoaded, setMapsLoaded] = useState( + typeof window !== 'undefined' && !!window.google?.maps + ) - const [mapsLoaded, setMapsLoaded] = useState(false) useEffect(() => { - if (typeof google !== 'undefined' && !mapsLoaded) { + if (typeof window === 'undefined') return + + if (window.google?.maps) { setMapsLoaded(true) + return } - if (!googleMapsApiKey || mapsLoaded || typeof google !== 'undefined') return - new Loader({ - apiKey: googleMapsApiKey, - version: 'weekly', - libraries: ['elevation'], - }) - .importLibrary('elevation') - .then(() => { + + const id = setInterval(() => { + if (window.google?.maps) { + clearInterval(id) setMapsLoaded(true) - }) - .catch((e) => { - console.warn('Error loading Google Maps API', e) - }) - }, [googleMapsApiKey, mapsLoaded, setMapsLoaded]) + } + }, 200) + + return () => clearInterval(id) + }, []) return { mapsLoaded } } diff --git a/apps/lrauv-dash2/next.config.js b/apps/lrauv-dash2/next.config.js index f88860ca..0e39deb4 100644 --- a/apps/lrauv-dash2/next.config.js +++ b/apps/lrauv-dash2/next.config.js @@ -9,32 +9,6 @@ const nextConfig = { // Next.js 13+ uses transpilePackages transpilePackages: ['@mbari/react-ui', '@mbari/utils', '@mbari/api-client'], - - // Make sure webpack can handle workspace TypeScript files - webpack: (config, { isServer }) => { - // Add TypeScript loader for workspace dependencies - config.module.rules.push({ - test: /\.(tsx|ts)$/, - include: [ - /node_modules\/@mbari\/react-ui/, - /node_modules\/@mbari\/utils/, - /node_modules\/@mbari\/api-client/, - /packages\/react-ui/, - /packages\/utils/, - /packages\/api-client/, - ], - use: [ - { - loader: 'babel-loader', - options: { - presets: ['next/babel'], - }, - }, - ], - }) - - return config - }, } module.exports = nextConfig diff --git a/apps/lrauv-dash2/pages/index.tsx b/apps/lrauv-dash2/pages/index.tsx index 2bcdd6bf..04f3b190 100644 --- a/apps/lrauv-dash2/pages/index.tsx +++ b/apps/lrauv-dash2/pages/index.tsx @@ -20,20 +20,25 @@ import { StationsListModal } from '../components/StationsListModal' import { MapLayersListModal } from '../components/MapLayersListModal' import { useSelectedStations } from '../components/SelectedStationContext' import { useMarkers } from '../components/MarkerContext' -import { useDepthRequest } from '@mbari/utils/useDepthRequest' import toast from 'react-hot-toast' -import type { MapProps } from '@mbari/react-ui/dist/Map/Map' import { createLogger } from '@mbari/utils' import { PlatformsListModal } from '../components/PlatformsListModal' +import VehicleColorsModal from '../components/VehicleColorsModal' // This is a tricky workaround to prevent leaflet from crashing next.js // SSR. If we don't do this, the leaflet map will be loaded server side // and throw a window error. -const Map = dynamic( - () => import('@mbari/react-ui/dist/Map/Map'), - { - ssr: false, - } +// Map types are not imported from @mbari/react-ui to avoid module resolution issues. +const Map = dynamic(() => import('@mbari/react-ui/dist/Map/Map'), { + ssr: false, +}) + +const MapDepthDisplay = dynamic( + () => + import('@mbari/react-ui/dist/Map/Map').then((m) => ({ + default: m.MapDepthDisplay, + })), + { ssr: false } ) const VehiclePath = dynamic(() => import('../components/VehiclePath'), { @@ -73,14 +78,6 @@ const styles = { 'flex w-full flex-shrink-0 flex-col bg-white border-t-2 border-secondary-300/60', } -// Interface CustomMarkerProps -type CustomMapProps = MapProps & - React.RefAttributes & { - isAddingMarkers?: boolean - onToggleMarkerMode?: () => void - trackedVehicles?: { name: string; id?: string }[] // Match the actual type being used - } - // Interface MarkerData interface MarkerData { id: number @@ -97,6 +94,7 @@ const OverViewMap: React.FC<{ }> = ({ trackedVehicles }) => { // Add mapRef to store the Leaflet map instance const mapRef = useRef(null) + const router = useRouter() const { handleDepthRequest, elevationAvailable } = useGoogleElevator() const [center, setCenter] = useState() const [centerZoom, setCenterZoom] = useState(undefined) @@ -111,20 +109,14 @@ const OverViewMap: React.FC<{ const [selectedMarkerId, setSelectedMarkerId] = useState(null) const [defaultMarkerColor, setDefaultMarkerColor] = useState('red') const { selectedStations } = useSelectedStations() - const { handleDepthRequestWithFeedback } = useDepthRequest( - handleDepthRequest, - { - warningToastId: 'depth-unavailable', - errorToastId: 'depth-result', - loadingToastId: 'depth-loading', - warningToastClass: 'blue-toast', - toastDuration: 5000, - } - ) const [layersModalPosition, setLayersModalPosition] = useState({ top: 0, left: 0, }) + const [colorModalPosition, setColorModalPosition] = useState({ + top: 100, + left: 100, + }) // Marker state const { @@ -218,9 +210,6 @@ const OverViewMap: React.FC<{ (gps: VPosDetail) => { if ((latestGPS?.isoTime ?? 0) > gps.isoTime || !latestGPS) { setLatestGPS(gps) - const coords: [number, number] = [gps.latitude, gps.longitude] - setCenter(coords) - setViewMode('center') } // Store position for bounds calculation vehiclePositions.current.push([gps.latitude, gps.longitude]) @@ -531,10 +520,14 @@ const OverViewMap: React.FC<{ setShowPlatformsModal(false) }, []) - // handleVehicleColorRequest- Show vehicle colors - const handleVehicleColorRequest = useCallback((vehicleName?: string) => { - setShowVehicleColors(true) - }, []) + // handleVehicleColorRequest - Show vehicle colors at anchor (from Map button) + const handleVehicleColorRequest = useCallback( + (anchor?: { top: number; left: number }) => { + setColorModalPosition(anchor ?? { top: 100, left: 100 }) + setShowVehicleColors(true) + }, + [] + ) // handleCloseVehicleColors - vehicle colors modal is closed const handleCloseVehicleColors = useCallback((vehicleName?: string) => { @@ -594,42 +587,26 @@ const OverViewMap: React.FC<{ ) : null} { logger.debug('🌍 Map ready callback triggered in OverViewMap') - // Store the Leaflet instance mapRef.current = map - - // Force redraw - after map is ready - setTimeout(() => { - try { - map.invalidateSize() - } catch (e) { - logger.warn('Could not invalidate map size:', e) - } - }, 200) + ;[200, 800].forEach((delay) => { + setTimeout(() => { + try { + map.invalidateSize() + } catch (e) { + logger.warn('Could not invalidate map size:', e) + } + }, delay) + }) }} trackedVehicles={trackedVehicles?.map((vehicle) => ({ ...vehicle, id: vehicle.id || vehicle.name, // Ensure id is always present }))} - onRequestDepth={async (lat, lng) => { - try { - // Try to remove any leading zeros - const formattedLat = parseFloat(String(lat).replace(/^0+/, '')) - // Then use in depth request - const result = await handleDepthRequestWithFeedback( - formattedLat, - lng - ) - return result.depth !== null ? result.depth : 0 - } catch (error) { - logger.warn('❌ Error in depth request:', error) - toast.error('Depth data unavailable', { id: 'depth-error' }) - return 0 - } - }} center={center} centerZoom={centerZoom} fitBounds={bounds} @@ -692,6 +669,16 @@ const OverViewMap: React.FC<{ ) } > + {uniqueTrackedVehicles?.map((name, index) => ( + {showVehicleColors ? ( + v.name) ?? []} + forceShowAll={true} + /> + ) : null} ) } diff --git a/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx b/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx index 03e753a1..e034cabd 100644 --- a/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx +++ b/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx @@ -467,7 +467,7 @@ const Vehicle: NextPage = () => { {/* Single map instance: render one layout to avoid duplicate controls */} {isDesktop ? ( -
+
GoogleMapsProvider + GoogleMapsProvider --> leafletPlugins + GoogleMapsProvider --> useGoogleMapsApiKey + leafletPlugins -.->|"loads script\n(no TS import)"| leaflet + + %% === Pages use map components === + index --> useGoogleElevator + index --> useGoogleMaps + index -->|dynamic| Map + index -->|dynamic| MapDepthDisplay + index -->|dynamic| VehiclePath + index --> MapLayersListModal + deploymentPage -->|dynamic| DeploymentMap + deploymentPage --> useGoogleMaps + + %% === DeploymentMap is the main map host === + DeploymentMap -->|dynamic import| Map + DeploymentMap -->|dynamic import| MapDepthDisplay + DeploymentMap --> useGoogleElevator + DeploymentMap --> useManagedWaypoints + DeploymentMap --> useTrackedVehicles + DeploymentMap -->|dynamic| VehiclePath + DeploymentMap -->|dynamic| ClickableMapPoint + DeploymentMap -->|dynamic| MapClickHandler + DeploymentMap -->|dynamic| DraggableMarker + DeploymentMap -->|dynamic| CustomMarkerSet + DeploymentMap -->|dynamic| WaypointPreviewPath + DeploymentMap -->|dynamic| PlatformPaths + DeploymentMap -->|dynamic| StationMarker + DeploymentMap --> MapLayersListModal + DeploymentMap --> SelectedStations["SelectedStationContext"] + DeploymentMap --> MarkerCtx["MarkerContext"] + + %% === Elevation chain (one-way, no cycle) === + useGoogleElevator --> elevationService + elevationService --> utils + DeploymentMap -->|"handleDepthRequest"| MapDepthDisplay + index -->|"handleDepthRequest"| MapDepthDisplay + + %% === react-ui Map internal === + Map --> useMapBaseLayer + Map --> MapTypes + Map --> Measurement + Map --> MovingDot + Map --> MapViews + Map -->|re-export| MapDepthDisplay + MapDepthDisplay --> MouseCoordinates + MapDepthDisplay --> utils + MapDepthDisplay --> react-leaflet + MouseCoordinates --> react-leaflet + MouseCoordinates --> utils + MovingDot --> Measurement + Measurement --> react-leaflet + Measurement --> leaflet + MapViews --> react-leaflet + useMapBaseLayer --> recoil["recoil"] + + %% === VehiclePath (no import of Map/DeploymentMap) === + VehiclePath --> apiClient + VehiclePath --> react-leaflet + VehiclePath --> SharedPathContextProvider + VehiclePath --> VehicleColorsContext + + %% === Other map children === + ClickableMapPoint --> react-leaflet + ClickableMapPoint -.->|"⚠ from 'react-ui/dist'"| useManagedWaypoints + MapClickHandler --> react-leaflet + DraggableMarker --> react-leaflet + CustomMarkerSet --> react-leaflet + WaypointPreviewPath --> react-leaflet + PlatformPaths --> PlatformPath["PlatformPath"] + PlatformPaths --> usePlatformList["usePlatformList"] + + %% Styling for notes + classDef warn fill:#f9f,stroke:#333,stroke-width:2px + classDef lib fill:#bfb,stroke:#333 + classDef pkg fill:#bbf,stroke:#333 + class ClickableMapPoint warn +``` + +## Legend + +- **Solid arrows**: direct or dynamic `import` / dependency. +- **Dashed arrows**: data passed as props or runtime dependency (e.g. `handleDepthRequest`), or non-TypeScript load (e.g. script tag). +- **Subgraphs**: app vs lib vs react-ui vs external. + +## Circular dependency check + +- **No circular imports** among Map, DeploymentMap, VehiclePath, elevationService, and MapDepthDisplay. +- Flow is one-way: app → react-ui Map (and MapDepthDisplay); app → useGoogleElevator → elevationService; depth callback is passed from app into MapDepthDisplay as a prop. + +## Potential issues + +| Item | Description | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **ClickableMapPoint** | Imports `useManagedWaypoints` from `'react-ui/dist'` instead of `'@mbari/react-ui'`. Fragile (tied to build output) and inconsistent with the rest of the app. | +| **Duplicate dynamic imports** | `pages/index.tsx` and `DeploymentMap.tsx` both dynamically import Map, MapDepthDisplay, VehiclePath, etc. Consider a small “map bundle” or shared lazy component to avoid duplication and ensure one place for SSR: false. | +| **Cross-feature dependency** | `DeploymentMap` depends on `useManagedWaypoints` from react-ui’s **Modals** (mission modal). Map feature is coupled to mission modal hooks; changes in Modals can affect map behavior and re-renders. | +| **elevationService** | Singleton + `window` flag; fine for one app, but ensure only one initializer (e.g. GoogleMapsProvider / leafletPlugins) loads the Google script so elevation and map stay in sync. | + +## Depth data flow (summary) + +1. **GoogleMapsProvider** (in \_app) runs **leafletPlugins.initLeafletGoogle(apiKey)**, which loads Google Maps (with elevation) and the Leaflet Google layer. +2. **useGoogleElevator** uses **elevationService** (`getElevationService`, `getCachedElevation`) and exposes **handleDepthRequest**. +3. **DeploymentMap** (and **index** for OverViewMap) call **useGoogleElevator()** and pass **handleDepthRequest** into **MapDepthDisplay** as the **depthRequest** prop. +4. **MapDepthDisplay** uses **useDepthRequest(depthRequest, options)** from `@mbari/utils` and **MouseCoordinates**; it renders coordinates + depth in the map’s top-right pane. + +No cycle in this chain; elevation is app → service → hook → prop → MapDepthDisplay. diff --git a/docs/map-development-guide.md b/docs/map-development-guide.md new file mode 100644 index 00000000..08d95057 --- /dev/null +++ b/docs/map-development-guide.md @@ -0,0 +1,249 @@ +Map development guide + +Goal + +Make map features easy to add without breaking SSR, Leaflet, Google Maps, or creating circular dependencies between the app and @mbari/react-ui. + +⸻ + +The golden rules + +Rule 1: The library map stays “pure” + +packages/react-ui must never import from any app (like apps/lrauv-dash2). + +✅ OK in @mbari/react-ui +• React + react-leaflet components +• Map controls, base layers, measurement UI +• Generic map helpers and types +• Callback props (to ask the app to do something) + +❌ Not OK in @mbari/react-ui +• Importing app modals/components +• Loading Google Maps scripts +• Importing Leaflet side-effect plugins (googlemutant, markercluster, etc.) +• Reading env variables or calling app APIs directly + +⸻ + +Rule 2: The app owns side effects and plugins + +Anything that: +• touches window, document, navigator +• loads a