Skip to content
Draft
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
24 changes: 20 additions & 4 deletions src/components/widgets/Map.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -558,18 +559,23 @@ const mapBase = ref<HTMLElement>()
const isMouseOver = useElementHover(mapBase)

const zoomControl = L.control.zoom({ position: 'bottomright' })
const layerControl = L.control.layers(baseMaps, overlays)
const layerControl = ref<L.Control.Layers>()
const gridLayer = shallowRef<L.LayerGroup | undefined>(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()
}
})
Expand Down Expand Up @@ -665,6 +671,8 @@ const removeScaleControl = (): void => {
}
}

const { setupMapOverlays } = useMapOverlays(map, layerControl)

onMounted(async () => {
reachedWaypoints.value = {}
missionItemsInVehicle.value = []
Expand All @@ -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

Expand All @@ -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]
})
Expand Down
130 changes: 130 additions & 0 deletions src/composables/useMapOverlays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import L from 'leaflet'
import { type Ref, ref } from 'vue'

/* eslint-disable jsdoc/require-jsdoc */
interface Overlay {
id: string
title: string
description: string
url: string
}

/**
* 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<L.Map | undefined>} map Reactive reference to the Leaflet map instance.
* @param {Ref<L.Control.Layers | undefined>} layerControl Reactive reference to the Leaflet layer control used to toggle overlays.
* @returns {{
* setupMapOverlays: (leafletMap: L.Map) => void,
* mapBounds: Ref<L.LatLngBounds | undefined>,
* dynamicOverlays: Ref<Record<string, L.Layer>>,
* overlaysInView: Ref<Overlay[]>,
* }} An object exposing the setup function and reactive overlay state.
*/
export function useMapOverlays(
map: Ref<L.Map | undefined>,
layerControl: Ref<L.Control.Layers | undefined>
): {
setupMapOverlays: (leafletMap: L.Map) => void
mapBounds: Ref<L.LatLngBounds | undefined>
dynamicOverlays: Ref<Record<string, L.Layer>>
overlaysInView: Ref<Overlay[]>
} {
const mapBounds = ref<L.LatLngBounds>()
const dynamicOverlays = ref<Record<string, L.Layer>>({})
const overlaysInView = ref<Overlay[]>([])
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<void> => {
if (!mapBounds.value) return

const now = Date.now()
const timeSinceLastFetch = now - lastFetchTime.value

if (timeSinceLastFetch < 1000) return

const bounds = mapBounds.value
const url = `https://logtools.cloud/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://logtools.cloud/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,
}
}
24 changes: 14 additions & 10 deletions src/views/MissionPlanningView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ import SideConfigPanel from '@/components/SideConfigPanel.vue'
import { useInteractionDialog } from '@/composables/interactionDialog'
import { provideMapContext } from '@/composables/map/useMapContext'
import { useSnackbar } from '@/composables/snackbar'
import { useMapOverlays } from '@/composables/useMapOverlays'
import {
clearAllSurveyAreas,
removeSurveyAreaSquareMeters,
Expand Down Expand Up @@ -3579,6 +3580,9 @@ const onMapClick = (e: L.LeafletMouseEvent): void => {
}
}

const layerControl = ref<L.Control.Layers>()
const { setupMapOverlays } = useMapOverlays(planningMap, layerControl)

const confirmDownloadDialog =
(layerLabel: string) =>
(status: SaveStatus, ok: () => void): void => {
Expand Down Expand Up @@ -3715,17 +3719,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)
Expand Down Expand Up @@ -3780,9 +3787,6 @@ onMounted(async () => {
onMapClick(e)
})

const layerControl = L.control.layers(baseMaps)
planningMap.value.addControl(layerControl)

// Initialize scale control (always show)
createScaleControl()

Expand Down
Loading