From 64490dbc6a0790c9e1517dfc1bb9f68c5ae95f80 Mon Sep 17 00:00:00 2001 From: Willian Galvani Date: Tue, 21 Jan 2025 12:21:22 -0300 Subject: [PATCH 1/3] allow loading external overlays: v1: quick and dirty --- src/components/widgets/Map.vue | 24 +++++-- src/composables/useMapOverlays.ts | 108 ++++++++++++++++++++++++++++++ src/views/MissionPlanningView.vue | 42 +++++++++--- 3 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 src/composables/useMapOverlays.ts diff --git a/src/components/widgets/Map.vue b/src/components/widgets/Map.vue index 08ca6288a5..efeff7d3f2 100644 --- a/src/components/widgets/Map.vue +++ b/src/components/widgets/Map.vue @@ -275,6 +275,7 @@ import type { WaypointCoordinates, } from '@/types/mission' import type { Widget } from '@/types/widgets' +import { useMapOverlays } from '@/composables/useMapOverlays' import ContextMenu from '../ContextMenu.vue' @@ -558,18 +559,23 @@ const mapBase = ref() const isMouseOver = useElementHover(mapBase) const zoomControl = L.control.zoom({ position: 'bottomright' }) -const layerControl = L.control.layers(baseMaps, overlays) +const layerControl = ref() const gridLayer = shallowRef(undefined) watch(showButtons, () => { if (map.value === undefined) return if (showButtons.value) { map.value.addControl(zoomControl) - map.value.addControl(layerControl) + if (!layerControl.value) { + layerControl.value = L.control.layers(baseMaps, overlays) + } + map.value.addControl(layerControl.value) createScaleControl() } else { map.value.removeControl(zoomControl) - map.value.removeControl(layerControl) + if (layerControl.value) { + map.value.removeControl(layerControl.value) + } removeScaleControl() } }) @@ -665,6 +671,8 @@ const removeScaleControl = (): void => { } } +const { setupMapOverlays } = useMapOverlays(map, layerControl) + onMounted(async () => { reachedWaypoints.value = {} missionItemsInVehicle.value = [] @@ -677,7 +685,7 @@ onMounted(async () => { // Bind leaflet instance to map element map.value = L.map(mapId.value, { - layers: [initialBaseLayer, seamarks, marineProfile], + layers: [initialBaseLayer], attributionControl: false, }).setView(mapCenter.value as LatLngTuple, zoom.value) as Map @@ -697,6 +705,14 @@ onMounted(async () => { // Remove default zoom control map.value.removeControl(map.value.zoomControl) + // Setup overlays with custom panes and fetch logic + setupMapOverlays(map.value) + + // Add default overlays with proper pane (pane needs to exist before addTo for z-index to apply) + seamarks.options.pane = 'seamarks' + seamarks.addTo(map.value) + marineProfile.addTo(map.value) + map.value.on('click', (event: LeafletMouseEvent) => { clickedLocation.value = [event.latlng.lat, event.latlng.lng] }) diff --git a/src/composables/useMapOverlays.ts b/src/composables/useMapOverlays.ts new file mode 100644 index 0000000000..dce74b4fe7 --- /dev/null +++ b/src/composables/useMapOverlays.ts @@ -0,0 +1,108 @@ +import L from 'leaflet' +import { ref, type Ref } from 'vue' + +interface Overlay { + id: string + title: string + description: string + url: string +} + +export function useMapOverlays(map: Ref, layerControl: Ref) { + const mapBounds = ref() + const dynamicOverlays = ref>({}) + const overlaysInView = ref([]) + const lastFetchTime = ref(0) + + const updateOverlays = (): void => { + if (!map.value || !layerControl.value) return + + // Keep track of current overlay IDs + const currentOverlayIds = new Set(Object.keys(dynamicOverlays.value)) + const newOverlayIds = new Set(overlaysInView.value.map((o) => o.id.toString())) + + // Remove overlays that are no longer in view + currentOverlayIds.forEach((id) => { + if (!newOverlayIds.has(id)) { + const layer = dynamicOverlays.value[id] + map.value?.removeLayer(layer) + layerControl.value?.removeLayer(layer) + delete dynamicOverlays.value[id] + } + }) + + // Add new overlays + overlaysInView.value.forEach((overlay) => { + const id = overlay.id.toString() + // Skip if we already have this overlay + if (currentOverlayIds.has(id)) return + + const tileLayer = L.tileLayer(overlay.url, { + maxZoom: 22, + minZoom: 1, + tileSize: 256, + attribution: `© ${overlay.title}`, + }) + + // Add to map first (will be underneath existing layers) + tileLayer.addTo(map.value!) + // Then add to control + layerControl.value?.addOverlay(tileLayer, overlay.title) + dynamicOverlays.value[id] = tileLayer + }) + } + + const fetchAndUpdateOverlays = async (): Promise => { + if (!mapBounds.value) return + + const now = Date.now() + const timeSinceLastFetch = now - lastFetchTime.value + + if (timeSinceLastFetch < 1000) return + + const bounds = mapBounds.value + const url = `https://map.galvanicloop.com/api/images/bounds/?min_lat=${bounds.getSouth()}&max_lat=${bounds.getNorth()}&min_lon=${bounds.getWest()}&max_lon=${bounds.getEast()}` + + try { + lastFetchTime.value = now + const response = await fetch(url) + const data = await response.json() + overlaysInView.value = data.map((overlay: any) => ({ + id: overlay.id, + title: overlay.title, + description: overlay.description, + url: `https://map.galvanicloop.com/api/images/${overlay.id}/tiles/{z}/{x}/{y}.png`, + })) + updateOverlays() + } catch (error) { + console.error('Failed to fetch overlays:', error) + } + } + + const setupMapOverlays = (leafletMap: L.Map): void => { + // Create a custom pane for seamarks with highest z-index + leafletMap.createPane('seamarks') + const seamarksPane = leafletMap.getPane('seamarks') + if (seamarksPane) { + seamarksPane.style.zIndex = '650' // Above other overlays (default overlay pane is 400) + } + + // Set up map event handlers for overlay updates + leafletMap.on('moveend', () => { + mapBounds.value = leafletMap.getBounds() + fetchAndUpdateOverlays() + }) + + leafletMap.on('zoomend', () => { + mapBounds.value = leafletMap.getBounds() + fetchAndUpdateOverlays() + }) + } + + return { + setupMapOverlays, + mapBounds, + dynamicOverlays, + overlaysInView, + } +} \ No newline at end of file diff --git a/src/views/MissionPlanningView.vue b/src/views/MissionPlanningView.vue index a5513e7b56..cadacd056e 100644 --- a/src/views/MissionPlanningView.vue +++ b/src/views/MissionPlanningView.vue @@ -664,6 +664,8 @@ import { } from '@/types/mission' import { ScreenBounds } from '@/types/user-interface' +import { useMapOverlays } from '@/composables/useMapOverlays' + const missionStore = useMissionStore() const vehicleStore = useMainVehicleStore() const interfaceStore = useAppInterfaceStore() @@ -3579,6 +3581,9 @@ const onMapClick = (e: L.LeafletMouseEvent): void => { } } +const layerControl = ref() +const { setupMapOverlays } = useMapOverlays(planningMap, layerControl) + const confirmDownloadDialog = (layerLabel: string) => (status: SaveStatus, ok: () => void): void => { @@ -3678,6 +3683,23 @@ onMounted(async () => { } ) + // Default overlays + const defaultOverlays = { + 'Marine Profile': L.tileLayer.wms('https://geoserver.openseamap.org/geoserver/gwc/service/wms', { + layers: 'gebco2021:gebco_2021', + format: 'image/png', + transparent: true, + version: '1.1.1', + attribution: '© GEBCO, OpenSeaMap', + tileSize: 256, + maxZoom: 19, + }), + 'Seamarks': L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', { + maxZoom: 18, + attribution: '© OpenSeaMap contributors', + }) + } + const baseMaps = { 'OpenStreetMap': osm, 'Esri World Imagery': esri, @@ -3715,17 +3737,20 @@ onMounted(async () => { missionStore.userLastMapTileProvider = event.name as MapTileProvider }) + // Initialize layer control and setup external overlay fetching + layerControl.value = L.control.layers(baseMaps) + planningMap.value.addControl(layerControl.value) + setupMapOverlays(planningMap.value) + planningMap.value.on('moveend', () => { - if (planningMap.value === undefined) return - let { lat, lng } = planningMap.value.getCenter() - if (lat && lng) { - mapCenter.value = [lat, lng] - } + if (!planningMap.value) return + const center = planningMap.value.getCenter() + mapCenter.value = [center.lat, center.lng] }) planningMap.value.on('zoomstart', clearLiveMeasure) planningMap.value.on('zoomend', () => { - if (planningMap.value === undefined) return - zoom.value = planningMap.value?.getZoom() ?? mapCenter.value + if (!planningMap.value) return + zoom.value = planningMap.value.getZoom() }) const saveCtlEsri = downloadOfflineMapTiles(esri, 'Esri', 19) @@ -3780,9 +3805,6 @@ onMounted(async () => { onMapClick(e) }) - const layerControl = L.control.layers(baseMaps) - planningMap.value.addControl(layerControl) - // Initialize scale control (always show) createScaleControl() From 061eda567efb0800d0dd7c9ad73f3eaace8bfbcf Mon Sep 17 00:00:00 2001 From: Willian Galvani Date: Wed, 22 Apr 2026 12:56:14 -0300 Subject: [PATCH 2/3] fix url --- src/composables/useMapOverlays.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/composables/useMapOverlays.ts b/src/composables/useMapOverlays.ts index dce74b4fe7..684cbf6660 100644 --- a/src/composables/useMapOverlays.ts +++ b/src/composables/useMapOverlays.ts @@ -61,7 +61,7 @@ export function useMapOverlays(map: Ref, layerControl: Ref, layerControl: Ref Date: Wed, 22 Apr 2026 13:04:17 -0300 Subject: [PATCH 3/3] AAAAAAHHHHHHH --- src/components/widgets/Map.vue | 2 +- src/composables/useMapOverlays.ts | 28 +++++++++++++++++++++++++--- src/views/MissionPlanningView.vue | 20 +------------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/components/widgets/Map.vue b/src/components/widgets/Map.vue index efeff7d3f2..d5ce391dd3 100644 --- a/src/components/widgets/Map.vue +++ b/src/components/widgets/Map.vue @@ -256,6 +256,7 @@ import PoiMapArrows from '@/components/poi/PoiMapArrows.vue' import { useInteractionDialog } from '@/composables/interactionDialog' import { provideMapContext } from '@/composables/map/useMapContext' import { openSnackbar } from '@/composables/snackbar' +import { useMapOverlays } from '@/composables/useMapOverlays' import { MavCmd, MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import { datalogger, DatalogVariable } from '@/libs/sensors-logging' import { degrees } from '@/libs/utils' @@ -275,7 +276,6 @@ import type { WaypointCoordinates, } from '@/types/mission' import type { Widget } from '@/types/widgets' -import { useMapOverlays } from '@/composables/useMapOverlays' import ContextMenu from '../ContextMenu.vue' diff --git a/src/composables/useMapOverlays.ts b/src/composables/useMapOverlays.ts index 684cbf6660..5d78976409 100644 --- a/src/composables/useMapOverlays.ts +++ b/src/composables/useMapOverlays.ts @@ -1,6 +1,7 @@ import L from 'leaflet' -import { ref, type Ref } from 'vue' +import { type Ref, ref } from 'vue' +/* eslint-disable jsdoc/require-jsdoc */ interface Overlay { id: string title: string @@ -8,7 +9,28 @@ interface Overlay { url: string } -export function useMapOverlays(map: Ref, layerControl: Ref) { +/** + * Composable that manages dynamic, bounds-driven map tile overlays fetched from a remote server. + * Registers map event handlers to refetch overlays on pan/zoom and keeps the provided + * layer control in sync with the overlays currently in view. + * @param {Ref} map Reactive reference to the Leaflet map instance. + * @param {Ref} layerControl Reactive reference to the Leaflet layer control used to toggle overlays. + * @returns {{ + * setupMapOverlays: (leafletMap: L.Map) => void, + * mapBounds: Ref, + * dynamicOverlays: Ref>, + * overlaysInView: Ref, + * }} An object exposing the setup function and reactive overlay state. + */ +export function useMapOverlays( + map: Ref, + layerControl: Ref +): { + setupMapOverlays: (leafletMap: L.Map) => void + mapBounds: Ref + dynamicOverlays: Ref> + overlaysInView: Ref +} { const mapBounds = ref() const dynamicOverlays = ref>({}) const overlaysInView = ref([]) @@ -105,4 +127,4 @@ export function useMapOverlays(map: Ref, layerControl: Ref { } ) - // Default overlays - const defaultOverlays = { - 'Marine Profile': L.tileLayer.wms('https://geoserver.openseamap.org/geoserver/gwc/service/wms', { - layers: 'gebco2021:gebco_2021', - format: 'image/png', - transparent: true, - version: '1.1.1', - attribution: '© GEBCO, OpenSeaMap', - tileSize: 256, - maxZoom: 19, - }), - 'Seamarks': L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: '© OpenSeaMap contributors', - }) - } - const baseMaps = { 'OpenStreetMap': osm, 'Esri World Imagery': esri,