-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
\ No newline at end of file
+
diff --git a/src/components/map/LHeatMap.vue b/src/components/map/LHeatMap.vue
deleted file mode 100644
index eb354fd14..000000000
--- a/src/components/map/LHeatMap.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/map/PhotoSuggestionsLayer.vue b/src/components/map/PhotoSuggestionsLayer.vue
index 164154d44..8607f08e1 100644
--- a/src/components/map/PhotoSuggestionsLayer.vue
+++ b/src/components/map/PhotoSuggestionsLayer.vue
@@ -1,289 +1,446 @@
+
+
+
-
+
+
+
+
+
-
-
\ No newline at end of file
+
+let draggedIndex = -1
+
+/**
+ * @param event the underlying MapLibre mouse event
+ */
+function onPointMouseDown(event: MapMouseEvent & { features?: MapGeoJSONFeature[] }): void {
+ const map = mapInstance.map
+ const feature = event.features?.[0]
+ const index = feature?.properties.index
+ if (!map || !props.draggable || index === undefined) {
+ return
+ }
+ event.preventDefault()
+ hoverPopup?.remove()
+ draggedIndex = index
+ map.dragPan.disable()
+ map.getCanvas().style.cursor = 'grabbing'
+ map.on('mousemove', onDragMove)
+ map.on('mouseup', onDragEnd)
+}
+
+/**
+ * @param event the underlying MapLibre mouse event
+ */
+function onDragMove(event: MapMouseEvent): void {
+ if (draggedIndex === -1) {
+ return
+ }
+ draggingOverride.value = { index: draggedIndex, lat: event.lngLat.lat, lng: event.lngLat.lng }
+}
+
+function onDragEnd(): void {
+ const map = mapInstance.map
+ map?.off('mousemove', onDragMove)
+ map?.off('mouseup', onDragEnd)
+ map?.dragPan.enable()
+ if (map) {
+ map.getCanvas().style.cursor = 'grab'
+ }
+ if (draggedIndex !== -1 && draggingOverride.value) {
+ emit('photo-suggestion-moved', draggedIndex, { lat: draggingOverride.value.lat, lng: draggingOverride.value.lng })
+ }
+ draggedIndex = -1
+ draggingOverride.value = null
+}
+
+let actionPopup: Popup | null = null
+
+/**
+ * @param event the underlying MapLibre mouse event
+ */
+function onPointContextMenu(event: MapMouseEvent & { features?: MapGeoJSONFeature[] }): void {
+ const map = mapInstance.map
+ const feature = event.features?.[0]
+ const photo = feature && props.photoSuggestions[feature.properties.index]
+ if (!map || !feature || !photo) {
+ return
+ }
+ event.preventDefault()
+
+ const container = document.createElement('div')
+ container.className = 'popup-photo-wrapper'
+ container.innerHTML = `
`
+ container.addEventListener('click', (domEvent) => {
+ const action = (domEvent.target as HTMLElement).getAttribute('data-action')
+ if (action === 'display') {
+ viewPhoto(photo)
+ }
+ actionPopup?.remove()
+ })
+
+ hoverPopup?.remove()
+ actionPopup?.remove()
+ actionPopup = new Popup({ closeButton: false, className: 'popovermenu open popupMarker photoPopup' })
+ .setLngLat((feature.geometry as { coordinates: [number, number] }).coordinates)
+ .setDOMContent(container)
+ .addTo(map)
+}
+
+/**
+ * @param event the underlying MapLibre mouse event
+ */
+function onClusterContextMenu(event: MapMouseEvent & { features?: MapGeoJSONFeature[] }): void {
+ const map = mapInstance.map
+ const feature = event.features?.[0]
+ const source = map?.getSource(SOURCE_ID)
+ if (!map || !feature || !source) {
+ return
+ }
+ event.preventDefault()
+ const clusterId = feature.properties.cluster_id
+ const pointCount = feature.properties.point_count
+ const coordinates = (feature.geometry as { coordinates: [number, number] }).coordinates
+
+ // @ts-expect-error getClusterLeaves exists on GeoJSONSource but isn't in this narrowed type
+ source.getClusterLeaves(clusterId, pointCount, 0).then((leaves: MapGeoJSONFeature[]) => {
+ const children = leaves
+ .map((leaf) => ({ index: leaf.properties?.index, photo: props.photoSuggestions[leaf.properties?.index] }))
+ .filter((c): c is { index: number, photo: PhotoSuggestion } => !!c.photo)
+ if (children.length === 0) {
+ return
+ }
+
+ const container = document.createElement('div')
+ container.className = 'popup-photo-wrapper'
+ container.innerHTML = `
+
+
+
+ `
+ container.addEventListener('click', (domEvent) => {
+ const action = (domEvent.target as HTMLElement).getAttribute('data-action')
+ if (action === 'selectall') {
+ children.forEach((c) => emit('photo-suggestion-selected', c.index))
+ } else if (action === 'display') {
+ const photoList = children.map((c) => c.photo).sort((a, b) => (a.dateTaken ?? 0) - (b.dateTaken ?? 0))
+ emit('open-sidebar', photoList[0].path)
+ if (window.OCA?.Viewer?.open) {
+ window.OCA.Viewer.open({ path: photoList[0].path, list: photoList })
+ }
+ } else if (action === 'zoom') {
+ // @ts-expect-error getClusterExpansionZoom exists on GeoJSONSource but isn't in this narrowed type
+ source.getClusterExpansionZoom(clusterId).then((zoom: number) => {
+ map.easeTo({ center: coordinates, zoom })
+ }).catch(() => {})
+ }
+ actionPopup?.remove()
+ })
+
+ actionPopup?.remove()
+ actionPopup = new Popup({ closeOnClick: false, className: 'popovermenu open popupMarker photoPopup' })
+ .setLngLat(coordinates)
+ .setDOMContent(container)
+ .addTo(map)
+ }).catch(() => {})
+}
+
+function onClusterMouseEnter(): void {
+ const map = mapInstance.map
+ if (map) {
+ map.getCanvas().style.cursor = 'pointer'
+ }
+}
+
+function onClusterMouseLeave(): void {
+ const map = mapInstance.map
+ if (map) {
+ map.getCanvas().style.cursor = ''
+ }
+}
+
+let listenersBound = false
+
+watch(() => mapInstance.isLoaded, (loaded) => {
+ const map = mapInstance.map
+ if (!loaded || !map || listenersBound) {
+ return
+ }
+ listenersBound = true
+ map.on('click', POINT_LAYER_ID, onPointClick)
+ map.on('click', CLUSTER_LAYER_ID, onClusterClick)
+ map.on('contextmenu', POINT_LAYER_ID, onPointContextMenu)
+ map.on('contextmenu', CLUSTER_LAYER_ID, onClusterContextMenu)
+ map.on('mousedown', POINT_LAYER_ID, onPointMouseDown)
+ map.on('mouseenter', POINT_LAYER_ID, onPointMouseEnter)
+ map.on('mouseleave', POINT_LAYER_ID, onPointMouseLeave)
+ map.on('mouseenter', CLUSTER_LAYER_ID, onClusterMouseEnter)
+ map.on('mouseleave', CLUSTER_LAYER_ID, onClusterMouseLeave)
+}, { immediate: true })
+
+onBeforeUnmount(() => {
+ const map = mapInstance.map
+ hoverPopup?.remove()
+ actionPopup?.remove()
+ if (!map || !listenersBound) {
+ return
+ }
+ map.off('click', POINT_LAYER_ID, onPointClick)
+ map.off('click', CLUSTER_LAYER_ID, onClusterClick)
+ map.off('contextmenu', POINT_LAYER_ID, onPointContextMenu)
+ map.off('contextmenu', CLUSTER_LAYER_ID, onClusterContextMenu)
+ map.off('mousedown', POINT_LAYER_ID, onPointMouseDown)
+ map.off('mouseenter', POINT_LAYER_ID, onPointMouseEnter)
+ map.off('mouseleave', POINT_LAYER_ID, onPointMouseLeave)
+ map.off('mouseenter', CLUSTER_LAYER_ID, onClusterMouseEnter)
+ map.off('mouseleave', CLUSTER_LAYER_ID, onClusterMouseLeave)
+ map.off('mousemove', onDragMove)
+ map.off('mouseup', onDragEnd)
+})
+
diff --git a/src/components/map/PhotosLayer.vue b/src/components/map/PhotosLayer.vue
index 39db15698..97eced575 100644
--- a/src/components/map/PhotosLayer.vue
+++ b/src/components/map/PhotosLayer.vue
@@ -1,289 +1,478 @@
+
-
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/components/map/PlaceContactMarker.vue b/src/components/map/PlaceContactMarker.vue
new file mode 100644
index 000000000..dd1f6a594
--- /dev/null
+++ b/src/components/map/PlaceContactMarker.vue
@@ -0,0 +1,325 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/map/PlaceContactPopup.vue b/src/components/map/PlaceContactPopup.vue
deleted file mode 100644
index 72e069e87..000000000
--- a/src/components/map/PlaceContactPopup.vue
+++ /dev/null
@@ -1,316 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/map/PoiMarker.vue b/src/components/map/PoiMarker.vue
deleted file mode 100644
index 07ba70916..000000000
--- a/src/components/map/PoiMarker.vue
+++ /dev/null
@@ -1,194 +0,0 @@
-
-
-
-
-
diff --git a/src/components/map/PoiMarkersLayer.vue b/src/components/map/PoiMarkersLayer.vue
new file mode 100644
index 000000000..ff8fe9109
--- /dev/null
+++ b/src/components/map/PoiMarkersLayer.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/map/SearchControl.vue b/src/components/map/SearchControl.vue
index ef95da05a..cdb3680f2 100644
--- a/src/components/map/SearchControl.vue
+++ b/src/components/map/SearchControl.vue
@@ -1,142 +1,105 @@
+
+
+
-
-
+
+
-
-
+ @validate="emit('validate', $event)"
+ @clear="emit('clear')" />
+
+
+
+
+
-
+
-
+
+
\ No newline at end of file
+
diff --git a/src/components/map/SearchField.vue b/src/components/map/SearchField.vue
index 5f466bf81..23d45d36d 100644
--- a/src/components/map/SearchField.vue
+++ b/src/components/map/SearchField.vue
@@ -7,8 +7,6 @@
:get-option-key="getOptionKey"
:model-value="mySelectedOption"
:limit="8"
- keep-open
- :clear-search-on-select="false"
:placeholder="placeholder"
:loading="searching || loading"
:options="filteredOptions"
@@ -35,8 +33,6 @@
diff --git a/src/components/map/StylePoiLayer.vue b/src/components/map/StylePoiLayer.vue
new file mode 100644
index 000000000..ce83354ae
--- /dev/null
+++ b/src/components/map/StylePoiLayer.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/map/TrackHoverMarker.vue b/src/components/map/TrackHoverMarker.vue
deleted file mode 100644
index 26828d22a..000000000
--- a/src/components/map/TrackHoverMarker.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/components/map/TracksLayer.vue b/src/components/map/TracksLayer.vue
index 7a36291c6..29765d57e 100644
--- a/src/components/map/TracksLayer.vue
+++ b/src/components/map/TracksLayer.vue
@@ -1,228 +1,399 @@
+
+
-
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/components/map/routing/RoutingControl.vue b/src/components/map/routing/RoutingControl.vue
index e28f2b17b..932cc4655 100644
--- a/src/components/map/routing/RoutingControl.vue
+++ b/src/components/map/routing/RoutingControl.vue
@@ -1,183 +1,543 @@
+
+
+
-
-
+
-
-
+
+
\ No newline at end of file
+
diff --git a/src/components/map/routing/RoutingMachine.vue b/src/components/map/routing/RoutingMachine.vue
deleted file mode 100644
index df4c3cc26..000000000
--- a/src/components/map/routing/RoutingMachine.vue
+++ /dev/null
@@ -1,545 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/map/routing/RoutingStep.vue b/src/components/map/routing/RoutingStep.vue
index 7d5c56c4c..0c8d09d09 100644
--- a/src/components/map/routing/RoutingStep.vue
+++ b/src/components/map/routing/RoutingStep.vue
@@ -1,112 +1,132 @@
-
-
+
+
+
+
+
+
-
-
+ @validate="onValidate"
+ @clear="emit('cleared')" />
+
+
+
+
+
+
+
-
diff --git a/src/components/map/routing/RoutingSteps.vue b/src/components/map/routing/RoutingSteps.vue
index 9f81be0d9..5bf6b3de5 100644
--- a/src/components/map/routing/RoutingSteps.vue
+++ b/src/components/map/routing/RoutingSteps.vue
@@ -1,134 +1,136 @@
-