From 845e9cc9d8b33c9a924c099a039caa6a7a5f6726 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:05:20 +0200 Subject: [PATCH 01/76] refactor(maps): port core map engine from Leaflet to vue-maplibre-gl Step 1 of the Leaflet -> vue-maplibre-gl port. Map.vue now renders via MglMap instead of L.map(), rewritten in diff --git a/src/components/MapContainer.vue b/src/components/MapContainer.vue index 382d7552f..32be5c133 100644 --- a/src/components/MapContainer.vue +++ b/src/components/MapContainer.vue @@ -56,7 +56,7 @@ import ClickPopup from './map/ClickPopup.vue' import FavoritePopup from './map/FavoritePopup.vue' import { isPublicShare } from '../utils/common.js' import { PUBLIC_FAVORITES_NAMESPACE } from '../store/modules/publicFavorites.js' -import { LayerIds, Layers } from '../data/mapLayers.js' +import { LayerIds, Layers } from '../data/mapLayers.ts' import { getThemingColorFromCategoryKey } from '../utils/favoritesUtils.js' import { getShouldMapUseImperial } from '../utils/mapUtils.js' diff --git a/src/data/mapLayers.js b/src/data/mapLayers.ts similarity index 50% rename from src/data/mapLayers.js rename to src/data/mapLayers.ts index 4f5d685a5..3c7bb9543 100644 --- a/src/data/mapLayers.js +++ b/src/data/mapLayers.ts @@ -19,13 +19,75 @@ * along with this program. If not, see . * */ +import type { RasterDEMSourceSpecification, StyleSpecification } from 'maplibre-gl' -const attributionESRI = 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN...' +export const OSM_ATTRIBUTION = '© OpenStreetMap contributors, CC-BY-SA' +export const ESRI_ATTRIBUTION = 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN...' + +export const BaseStyleIds = { + Street: 'street', + Satellite: 'satellite', +} as const + +export type BaseStyleId = typeof BaseStyleIds[keyof typeof BaseStyleIds] + +/** + * The available base styles for the (ported) main map. Support for switching + * between many base layers (topographic, watercolor, a roads overlay, ...) + * was dropped as part of the port to vue-maplibre-gl; only a vector street + * map and satellite imagery remain. + * + * The street style is OpenFreeMap's "Liberty" style - a full vector style + * served for free, no API key required. See https://openfreemap.org/. + */ +export const BaseStyles: Record = { + [BaseStyleIds.Street]: 'https://tiles.openfreemap.org/styles/liberty', + [BaseStyleIds.Satellite]: { + version: 8, + sources: { + satellite: { + type: 'raster', + tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], + tileSize: 256, + maxzoom: 19, + attribution: ESRI_ATTRIBUTION, + }, + }, + layers: [ + { id: 'satellite', type: 'raster', source: 'satellite' }, + ], + }, +} + +export const TERRAIN_SOURCE_ID = 'terrain-dem' + +/** + * Public-domain elevation tiles (Mapzen Terrarium, hosted by AWS Open Data) + * used to drive MapLibre's native 3D terrain rendering. No API key required. + */ +export const TERRAIN_SOURCE: RasterDEMSourceSpecification = { + type: 'raster-dem', + tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'], + tileSize: 256, + maxzoom: 15, + encoding: 'terrarium', + attribution: 'Elevation data © Mapzen, tilezen, and contributors', +} + +/* + * Everything below is the pre-port, Leaflet-flavoured layer catalogue. + * `Map.vue` no longer uses it, but `MapContainer.vue` (public favorite + * share page) hasn't been ported to vue-maplibre-gl yet and still renders + * its own Leaflet map with these layers, so they stay until that follow-up + * step lands. + */ export const LayerTypes = { Base: 'base', Overlay: 'overlay', -} +} as const + +export type LayerType = typeof LayerTypes[keyof typeof LayerTypes] export const LayerIds = { OSM: 'osm', @@ -34,9 +96,30 @@ export const LayerIds = { ESRITopo: 'esri-topo', OpenTopo: 'open-topo', Watercolor: 'watercolor', +} as const + +export interface MapLayerOptions { + id: string + noWrap: boolean + detectRetina: boolean + maxZoom: number + referrerPolicy: string + ext?: string + subdomains?: string +} + +export interface MapLayer { + id: string + name: string + type: LayerType + /** XYZ tile URL template, using Leaflet's `{s}`/`{z}`/`{x}`/`{y}` placeholders. */ + url: string + attribution: string + options: MapLayerOptions + opacity?: number } -export const Layers = [ +export const Layers: MapLayer[] = [ { id: LayerIds.RoadsOverlay, name: 'Roads Overlay', @@ -57,7 +140,7 @@ export const Layers = [ name: 'Street map', type: LayerTypes.Base, url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - attribution: '© OpenStreetMap contributors, CC-BY-SA', + attribution: OSM_ATTRIBUTION, options: { id: 'Open Street Map', noWrap: false, @@ -71,7 +154,7 @@ export const Layers = [ name: 'Satellite map', type: LayerTypes.Base, url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', - attribution: attributionESRI, + attribution: ESRI_ATTRIBUTION, options: { id: 'ESRI', noWrap: false, @@ -85,7 +168,7 @@ export const Layers = [ name: 'Topographic', type: LayerTypes.Base, url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', - attribution: attributionESRI, + attribution: ESRI_ATTRIBUTION, options: { id: 'ESRI topo', noWrap: false, @@ -112,8 +195,8 @@ export const Layers = [ }, ] -export const baseLayersByName = {} -export const overlayLayersByName = {} +export const baseLayersByName: Record = {} +export const overlayLayersByName: Record = {} Layers.forEach((l) => { if (l.type === LayerTypes.Base) { baseLayersByName[l.options.id] = l From bdd81b4bcf1e2faf80a742f555196464cceea8ab Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:10:29 +0200 Subject: [PATCH 02/76] fix(maps): allow MapLibre GL to fetch its map sources under CSP MapLibre GL JS always fetches its sources (style JSON, vector tiles, sprites, glyphs, raster tiles, terrain DEM tiles) via fetch()/XHR, even for raster sources - unlike Leaflet's tags, these fall under connect-src rather than img-src. Without this, loading the OpenFreeMap style, the ESRI satellite raster tiles, or the terrain DEM tiles failed with a CSP-driven NetworkError. Also switched the terrain DEM source to the bucket-specific elevation-tiles-prod.s3.amazonaws.com endpoint instead of the shared s3.amazonaws.com host, so the CSP allowlist only opens that one bucket rather than the whole generic S3 namespace. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- lib/Controller/AddCspTrait.php | 8 ++++++++ src/data/mapLayers.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/Controller/AddCspTrait.php b/lib/Controller/AddCspTrait.php index a3fc14dc4..4957d26b9 100644 --- a/lib/Controller/AddCspTrait.php +++ b/lib/Controller/AddCspTrait.php @@ -21,6 +21,14 @@ private function addCsp(Response $response): void { $csp->addAllowedImageDomain('https://*.ssl.fastly.net'); $csp->addAllowedImageDomain('https://*.openstreetmap.se'); + // MapLibre GL always fetches its sources (style JSON, vector tiles, + // sprites, glyphs, raster tiles, terrain DEM tiles) via fetch()/XHR, + // even for raster sources - unlike Leaflet's tags, these need + // connect-src, not img-src. + $csp->addAllowedConnectDomain('https://tiles.openfreemap.org'); + $csp->addAllowedConnectDomain('https://server.arcgisonline.com'); + $csp->addAllowedConnectDomain('https://elevation-tiles-prod.s3.amazonaws.com'); + // default routing engine $csp->addAllowedConnectDomain('https://*.project-osrm.org'); $csp->addAllowedConnectDomain('https://api.mapbox.com'); diff --git a/src/data/mapLayers.ts b/src/data/mapLayers.ts index 3c7bb9543..3bdb1b457 100644 --- a/src/data/mapLayers.ts +++ b/src/data/mapLayers.ts @@ -67,7 +67,7 @@ export const TERRAIN_SOURCE_ID = 'terrain-dem' */ export const TERRAIN_SOURCE: RasterDEMSourceSpecification = { type: 'raster-dem', - tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'], + tiles: ['https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png'], tileSize: 256, maxzoom: 15, encoding: 'terrarium', From 592874114211e5ee8714f6a81994e84f34739e81 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:15:56 +0200 Subject: [PATCH 03/76] feat(maps): port favorites markers and clustering to vue-maplibre-gl FavoritesLayer.vue rewritten in \ No newline at end of file + From 20eae9b258b76f7ec27b9236b1c7b7e1a2691e8e Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:21:00 +0200 Subject: [PATCH 04/76] fix(maps): translate MapLibre click event shape for add-to-map clicks App.vue's onMapAddClick (used by the "add a favorite" flow) still reads the old Leaflet event shape (e.latlng.lat/lng). MapLibre's MapMouseEvent carries the position as e.lngLat instead, so add-click was firing with lat/lng undefined, throwing a TypeError from onMapAddClick before a favorite could ever be placed. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/Map.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Map.vue b/src/components/Map.vue index afb141a8b..ceaa55cf8 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -118,7 +118,7 @@ const props = withDefaults(defineProps<{ }) const emit = defineEmits<{ - 'add-click': [event: MapMouseEvent] + 'add-click': [event: { latlng: { lat: number, lng: number } }] 'click-favorite': [favorite: Favorite] }>() @@ -151,7 +151,9 @@ watch(() => mapInstance.isLoaded, (loaded) => { */ function onMapClick(event: MapMouseEvent) { if (props.state === 'adding') { - emit('add-click', event) + // App.vue still expects the old Leaflet event shape (e.latlng.lat/lng), + // MapLibre's event carries the position as e.lngLat.lat/lng instead. + emit('add-click', { latlng: { lat: event.lngLat.lat, lng: event.lngLat.lng } }) } } From 320157bf7f5e75d433bb85851e82ae6942f22b70 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:25:52 +0200 Subject: [PATCH 05/76] fix(maps): unwrap vue-maplibre-gl's map event envelope for add-click vue-maplibre-gl's @map:click doesn't forward MapLibre's native MapMouseEvent directly - it wraps it as { type, map, component, event }, with the actual event nested under .event. onMapClick was reading lngLat off the wrapper instead of the nested event, so add-click still fired with an undefined position. Also moved add-click's payload to App.vue's own event vocabulary instead of shimming the old Leaflet shape in Map.vue: it now emits a plain { lat, lng } position, and App.vue's onMapAddClick reads that directly rather than the old e.latlng.lat/lng. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/Map.vue | 10 ++++------ src/views/App.vue | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/Map.vue b/src/components/Map.vue index ceaa55cf8..0fdaa38e4 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -118,7 +118,7 @@ const props = withDefaults(defineProps<{ }) const emit = defineEmits<{ - 'add-click': [event: { latlng: { lat: number, lng: number } }] + 'add-click': [position: { lat: number, lng: number }] 'click-favorite': [favorite: Favorite] }>() @@ -147,13 +147,11 @@ watch(() => mapInstance.isLoaded, (loaded) => { }) /** - * @param event the underlying MapLibre mouse event + * @param wrapper vue-maplibre-gl's map event envelope; the native MapMouseEvent is under `.event` */ -function onMapClick(event: MapMouseEvent) { +function onMapClick(wrapper: { event: MapMouseEvent }) { if (props.state === 'adding') { - // App.vue still expects the old Leaflet event shape (e.latlng.lat/lng), - // MapLibre's event carries the position as e.lngLat.lat/lng instead. - emit('add-click', { latlng: { lat: event.lngLat.lat, lng: event.lngLat.lng } }) + emit('add-click', wrapper.event.lngLat) } } diff --git a/src/views/App.vue b/src/views/App.vue index d6d3499a1..12ea49f1a 100644 --- a/src/views/App.vue +++ b/src/views/App.vue @@ -824,7 +824,7 @@ export default { }, onMapAddClick(e) { if (this.mapState === 'adding' && this.addingFavorite) { - this.addFavorite({ lat: e.latlng.lat, lng: e.latlng.lng }, null, null, null, null, true, true) + this.addFavorite({ lat: e.lat, lng: e.lng }, null, null, null, null, true, true) this.addingFavorite = false } }, From ca79f79a860748b0469df6667d4ba32c2e28091e Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:34:52 +0200 Subject: [PATCH 06/76] feat(maps): port contacts markers and clustering to vue-maplibre-gl ContactsLayer.vue rewritten in \ No newline at end of file + diff --git a/src/components/map/FavoritesLayer.vue b/src/components/map/FavoritesLayer.vue index dc9c5bd2f..c4b6bb813 100644 --- a/src/components/map/FavoritesLayer.vue +++ b/src/components/map/FavoritesLayer.vue @@ -1,23 +1,8 @@ + - SPDX-FileCopyrightText: 2019 Paul Schwörer + - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + + - \ No newline at end of file + From 28b6850617ac7e20c086aa8ef44a95852523fe60 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 13:53:44 +0200 Subject: [PATCH 08/76] feat(maps): show a representative photo thumbnail on cluster bubbles Use a GeoJSON clusterProperties reducer to carry one child point's iconId onto its cluster, and use that (falling back to the placeholder) as the cluster's icon-image instead of a fixed generic icon. supercluster aggregates bottom-up, so this isn't guaranteed to be the very first photo geographically/temporally, just some deterministic representative one - same caveat the old "first child marker" Leaflet code had, just for a different reason. Also added a text halo to the cluster count label, since it now renders over an arbitrary photo instead of a flat color and needs to stay legible either way. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/map/PhotosLayer.vue | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/components/map/PhotosLayer.vue b/src/components/map/PhotosLayer.vue index 2b96761fb..fdaf49c84 100644 --- a/src/components/map/PhotosLayer.vue +++ b/src/components/map/PhotosLayer.vue @@ -16,16 +16,17 @@ + :cluster-radius="CLUSTER_RADIUS" + :cluster-properties="CLUSTER_PROPERTIES"> + :layout="{ 'icon-image': ['coalesce', ['get', 'iconId'], PLACEHOLDER_ICON_ID], 'icon-allow-overlap': true, 'icon-size': 1 }" /> + :paint="{ 'text-color': '#ffffff', 'text-halo-color': '#000000', 'text-halo-width': 1 }" /> ()) @@ -164,18 +167,6 @@ function ensureFallbackIcons(): void { map.addImage(PLACEHOLDER_ICON_ID, image) } } - if (!map.hasImage(CLUSTER_ICON_ID)) { - const image = drawIcon(MARKER_SIZE, (ctx) => { - ctx.fillStyle = '#0082c9' - ctx.fillRect(0, 0, MARKER_SIZE, MARKER_SIZE) - ctx.strokeStyle = '#ffffff' - ctx.lineWidth = 2 - ctx.strokeRect(1, 1, MARKER_SIZE - 2, MARKER_SIZE - 2) - }) - if (image) { - map.addImage(CLUSTER_ICON_ID, image) - } - } } /** From 47229d0292f9cc181d1c0cad213889131b5cf733 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 14:01:22 +0200 Subject: [PATCH 09/76] feat(maps): port track lines and waypoints to vue-maplibre-gl TracksLayer.vue rewritten in \ No newline at end of file + From 74496e44b46c7e264689961f84bdfd25f3971173 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 14:06:41 +0200 Subject: [PATCH 10/76] fix(maps): restore Map.vue's exposed zoom/fitBounds methods App.vue calls this.$refs.map.zoomOnTrack/zoomOnDevice/ zoomOnPhotoSuggestion/fitBounds/displayElevation directly from several places (track/device/photo-suggestion selection, "zoom to all favorites/contacts", etc.). The \ 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 From c6d5ee20c969e424c478c14b9a0696ab2596d7e4 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 14:35:13 +0200 Subject: [PATCH 13/76] fix(maps): stop exceeding the device points endpoint's max limit getDevice() requested limit=100000 points per page, but DevicesController::getDevicePoints() only accepts int<1, 10000> - Nextcloud's request validation rejects anything outside that range with a 400 before the controller body even runs. This has been broken since whichever server version started enforcing that constraint; device markers/history just never got tested through this exact path until now, since the map didn't render or interact with devices at all before this port. Use 10000 (the actual max) instead, keeping the same "keep paginating while a full page comes back" logic - it just takes more round trips for a device with a huge point history now. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- lib/Controller/DevicesController.php | 5 ++++- src/views/App.vue | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Controller/DevicesController.php b/lib/Controller/DevicesController.php index 7a3d233ca..cdba7dc3d 100644 --- a/lib/Controller/DevicesController.php +++ b/lib/Controller/DevicesController.php @@ -95,8 +95,11 @@ public function getDevicesByTokens(array $tokens): DataResponse { return new DataResponse(array_values($devices)); } + /** + * @params int<1, 10000> $limit + */ #[NoAdminRequired] - public function getDevicePoints($id, ?int $pruneBefore = 0, ?int $limit = 10000, ?int $offset = 0, ?array $tokens = null): DataResponse { + public function getDevicePoints(int $id, ?int $pruneBefore = 0, ?int $limit = 10000, ?int $offset = 0, ?array $tokens = null): DataResponse { if (is_null($tokens)) { $points = $this->devicesService->getDevicePointsFromDB($this->userId, $id, $pruneBefore, $limit, $offset); } else { diff --git a/src/views/App.vue b/src/views/App.vue index e06d2de2d..c7c149ee2 100644 --- a/src/views/App.vue +++ b/src/views/App.vue @@ -2110,7 +2110,7 @@ export default { }, getDevice(device, enable = false, save = true, zoom = false) { device.loading = true - network.getDevice(device.id, this.myMapId, 100000, device.points?.length || 0, device.tokens).then((response) => { + network.getDevice(device.id, this.myMapId, 10000, device.points?.length || 0, device.tokens).then((response) => { // There are too many points making it responsiv crashes most browsers // this.$set(device, 'points', response.data /* .sort((p1, p2) => (p1.timestamp || 0) - (p2.timestamp || 0)) */) if (device.points) { @@ -2118,7 +2118,7 @@ export default { } else { device.points = response.data } - if (response.data.length >= 100000) { + if (response.data.length >= 10000) { this.getDevice(device, false, false, false) } if (enable) { From d131a3ce4cffad5d81f9453073291a46ead87a32 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 14:55:20 +0200 Subject: [PATCH 14/76] feat(maps): port routing from leaflet-routing-machine to maplibre-gl-directions RoutingMachine.vue is gone; RoutingControl.vue now drives a @maplibre/maplibre-gl-directions instance directly through useMap(), configured from the existing OSRM/Mapbox admin settings. The directions library's own click/drag waypoint interactivity is disabled - the existing step list (RoutingSteps/RoutingStep, unchanged) already provides full waypoint CRUD, and the two interaction models don't mix since the library has no concept of an empty waypoint slot. Not ported: the turn-by-turn instructions panel, since maplibre-gl-directions only draws the route and doesn't decode it into textual steps. GraphHopper routing is dropped rather than ported: maplibre-gl-directions only speaks OSRM/Mapbox-Directions-API request/response formats, and it was removed from the admin settings along with its CSP allowances and read/write endpoints. Toggled via a new button next to the street/satellite/terrain switch in Map.vue, since the search box that used to toggle it isn't ported yet. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- README.md | 2 +- appinfo/info.xml | 2 +- lib/Controller/AddCspTrait.php | 2 - lib/Controller/PublicUtilsController.php | 2 - lib/Controller/UtilsController.php | 4 - lib/Settings/AdminSettings.php | 2 - package-lock.json | 80 --- package.json | 3 - src/components/Map.vue | 25 +- src/components/map/routing/RoutingControl.vue | 441 +++++++++----- src/components/map/routing/RoutingMachine.vue | 545 ------------------ src/main.js | 1 - src/optionsController.js | 4 - src/views/AdminSettings.vue | 21 - 14 files changed, 330 insertions(+), 804 deletions(-) delete mode 100644 src/components/map/routing/RoutingMachine.vue diff --git a/README.md b/README.md index 4d9b0b874..66af14694 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **🗺 Beautiful map:** Using [OpenStreetMap](https://www.openstreetmap.org) and [Leaflet](https://leafletjs.com), you can choose between standard map, satellite, topographical, dark mode or even watercolor! 🎨 - **⭐ Favorites:** Save your favorite places, privately! Sync with [GNOME Maps](https://github.com/nextcloud/maps/issues/30) and mobile apps is planned. -- **🧭 Routing:** Possible using either [OSRM](http://project-osrm.org), [GraphHopper](https://www.graphhopper.com) or [Mapbox](https://www.mapbox.com). +- **🧭 Routing:** Possible using either [OSRM](http://project-osrm.org) or [Mapbox](https://www.mapbox.com). - **🖼 Photos on the map:** No more boring slideshows, just show directly where you were! - **🙋 Contacts on the map:** See where your friends live and plan your next visit. - **📱 Devices:** Lost your phone? Check the map! diff --git a/appinfo/info.xml b/appinfo/info.xml index c077fb55f..c8dc79877 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -8,7 +8,7 @@ - **🗺 Beautiful map:** Using [OpenStreetMap](https://www.openstreetmap.org) and [Leaflet](https://leafletjs.com), you can choose between standard map, satellite, topographical, dark mode or even watercolor! 🎨 - **⭐ Favorites:** Save your favorite places, privately! Sync with [GNOME Maps](https://github.com/nextcloud/maps/issues/30) and mobile apps is planned. -- **🧭 Routing:** Possible using either [OSRM](http://project-osrm.org), [GraphHopper](https://www.graphhopper.com) or [Mapbox](https://www.mapbox.com). +- **🧭 Routing:** Possible using either [OSRM](http://project-osrm.org) or [Mapbox](https://www.mapbox.com). - **🖼 Photos on the map:** No more boring slideshows, just show directly where you were! - **🙋 Contacts on the map:** See where your friends live and plan your next visit. - **📱 Devices:** Lost your phone? Check the map! diff --git a/lib/Controller/AddCspTrait.php b/lib/Controller/AddCspTrait.php index 4957d26b9..a17c34204 100644 --- a/lib/Controller/AddCspTrait.php +++ b/lib/Controller/AddCspTrait.php @@ -33,7 +33,6 @@ private function addCsp(Response $response): void { $csp->addAllowedConnectDomain('https://*.project-osrm.org'); $csp->addAllowedConnectDomain('https://api.mapbox.com'); $csp->addAllowedConnectDomain('https://events.mapbox.com'); - $csp->addAllowedConnectDomain('https://graphhopper.com'); $csp->addAllowedWorkerSrcDomain('blob:'); $csp->addAllowedScriptDomain('https://unpkg.com'); @@ -42,7 +41,6 @@ private function addCsp(Response $response): void { 'osrmBikeURL', 'osrmCarURL', 'osrmFootURL', - 'graphhopperURL' ]; foreach ($urlKeys as $key) { $url = $this->appConfig->getValueString('maps', $key); diff --git a/lib/Controller/PublicUtilsController.php b/lib/Controller/PublicUtilsController.php index 81fcd8024..866dd2d62 100644 --- a/lib/Controller/PublicUtilsController.php +++ b/lib/Controller/PublicUtilsController.php @@ -183,12 +183,10 @@ public function getOptionsValues(): DataResponse { 'osrmBikeURL', 'osrmFootURL', 'osrmDEMO', - 'graphhopperAPIKEY', 'mapboxAPIKEY', 'maplibreStreetStyleURL', 'maplibreStreetStyleAuth', 'maplibreStreetStylePmtiles', - 'graphhopperURL' ]; foreach ($settingsKeys as $k) { $v = $this->appConfig->getValueString('maps', $k); diff --git a/lib/Controller/UtilsController.php b/lib/Controller/UtilsController.php index 5d17a334e..b4221967a 100644 --- a/lib/Controller/UtilsController.php +++ b/lib/Controller/UtilsController.php @@ -123,12 +123,10 @@ public function getOptionsValues($myMapId = null): DataResponse { 'osrmBikeURL', 'osrmFootURL', 'osrmDEMO', - 'graphhopperAPIKEY', 'mapboxAPIKEY', 'maplibreStreetStyleURL', 'maplibreStreetStyleAuth', 'maplibreStreetStylePmtiles', - 'graphhopperURL' ]; foreach ($settingsKeys as $k) { $v = $this->config->getAppValue('maps', $k); @@ -148,12 +146,10 @@ public function setRoutingSettings($values): DataResponse { 'osrmBikeURL', 'osrmFootURL', 'osrmDEMO', - 'graphhopperAPIKEY', 'mapboxAPIKEY', 'maplibreStreetStyleURL', 'maplibreStreetStyleAuth', 'maplibreStreetStylePmtiles', - 'graphhopperURL' ]; foreach ($values as $k => $v) { if (in_array($k, $acceptedKeys)) { diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 30da1e619..d7650f980 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -19,11 +19,9 @@ public function getForm(): TemplateResponse { 'osrmCarURL', 'osrmBikeURL', 'osrmFootURL', - 'graphhopperAPIKEY', 'mapboxAPIKEY', 'maplibreStreetStyleURL', 'maplibreStreetStyleAuth', - 'graphhopperURL', ]; $parameters = []; foreach ($keys as $k) { diff --git a/package-lock.json b/package-lock.json index 56cf57c5e..580ad6662 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,12 +28,9 @@ "axios": "^1.18.1", "geojson": "^0.5.0", "leaflet": "^1.9.4", - "leaflet-control-geocoder": "^2.4.0", "leaflet-mouse-position": "^1.0.4", - "leaflet-routing-machine": "^3.2.12", "leaflet.featuregroup.subgroup": "^1.0.2", "leaflet.markercluster": "^1.5.3", - "lrm-graphhopper": "^1.3.0", "maplibre-gl": "^5.16.0", "nouislider": "^15.8.1", "opening_hours": "^3.9.0", @@ -1390,12 +1387,6 @@ "license": "MIT", "peer": true }, - "node_modules/@mapbox/corslite": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@mapbox/corslite/-/corslite-0.0.7.tgz", - "integrity": "sha512-w/uS474VFjmqQ7fFWIMZINQM1BAQxDLuoJaZZIPES1BmeYpCtlh9MtbFxKGGDAsfvut8/HircIsVvEYRjQ+iMg==", - "license": "BSD" - }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -1410,17 +1401,6 @@ "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", "license": "ISC" }, - "node_modules/@mapbox/polyline": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-0.2.0.tgz", - "integrity": "sha512-GCddO0iw6AzOQqZgBmjEQI9Pgo40/yRgkTkikGctE01kNBN0ThWYuAnTD+hRWrAWMV6QJ0rNm4m8DAsaAXE7Pg==", - "bin": { - "polyline": "bin/polyline.bin.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/@mapbox/tiny-sdf": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", @@ -5724,12 +5704,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, - "node_modules/corslite": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/corslite/-/corslite-0.0.6.tgz", - "integrity": "sha512-CqrvLTZk8IhahX7RHYxBPbTuKlpNDAnS3cYJTidbZ5yl69J20c0RojeAFQ+r6tnpuHtl2+LU4yK6Taw44SANDg==", - "license": "BSD" - }, "node_modules/cosmiconfig": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", @@ -9356,35 +9330,12 @@ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "license": "BSD-2-Clause" }, - "node_modules/leaflet-control-geocoder": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/leaflet-control-geocoder/-/leaflet-control-geocoder-2.4.0.tgz", - "integrity": "sha512-b2QlxuFd40uIDbnoUI3U9fzfnB4yKUYlmsXjquJ2d2YjoJqnyVYcIJeErAVv3kPvX3nI0gzvBq1XHMgSVFrGkQ==", - "license": "BSD-2-Clause", - "optionalDependencies": { - "open-location-code": "^1.0.0" - }, - "peerDependencies": { - "leaflet": "^1.6.0" - } - }, "node_modules/leaflet-mouse-position": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/leaflet-mouse-position/-/leaflet-mouse-position-1.2.0.tgz", "integrity": "sha512-iKS25GZDRdoKZn0O5sjTyNcBYrq/wenLiOidj4jJ5iD4SfhyqVj+bJHgBwuMxOlf8Vc/9B1ZPlqYV4QA97nyFg==", "license": "MIT" }, - "node_modules/leaflet-routing-machine": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/leaflet-routing-machine/-/leaflet-routing-machine-3.2.12.tgz", - "integrity": "sha512-HLde58G1YtD9xSIzZavJ6BPABZaV1hHeGst8ouhzuxmSC3s32NVtADT+njbIUMW1maHRCrsgTk/E4hz5QH7FrA==", - "license": "ISC", - "dependencies": { - "@mapbox/corslite": "0.0.7", - "@mapbox/polyline": "^0.2.0", - "osrm-text-instructions": "^0.13.2" - } - }, "node_modules/leaflet.featuregroup.subgroup": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/leaflet.featuregroup.subgroup/-/leaflet.featuregroup.subgroup-1.0.2.tgz", @@ -9510,16 +9461,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/lrm-graphhopper": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/lrm-graphhopper/-/lrm-graphhopper-1.3.0.tgz", - "integrity": "sha512-Guvm2rGtUJU/OqymOKKtcgUORo8GCC5BGM5vwX4vaqIllEDWqPPNh60fDJoAi9/XclBPQVR5hsMsVBEjyhmljg==", - "license": "ISC", - "dependencies": { - "corslite": "0.0.6", - "polyline": "0.0.3" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10875,13 +10816,6 @@ "wrappy": "1" } }, - "node_modules/open-location-code": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz", - "integrity": "sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/opening_hours": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/opening_hours/-/opening_hours-3.9.0.tgz", @@ -10922,12 +10856,6 @@ "dev": true, "license": "MIT" }, - "node_modules/osrm-text-instructions": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/osrm-text-instructions/-/osrm-text-instructions-0.13.4.tgz", - "integrity": "sha512-ge4ZTIetMQKAHKq2MwWf83ntzdJN20ndRKRaVNoZ3SkDkBNO99Qddz7r6+hrVx38I+ih6Rk5T1yslczAB6Q9Pg==", - "license": "BSD-2-Clause" - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -11300,14 +11228,6 @@ "robust-predicates": "^3.0.2" } }, - "node_modules/polyline": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/polyline/-/polyline-0.0.3.tgz", - "integrity": "sha512-kM9l6Zi5m8GxNH4VF2amQm64gP+RcdQZN7DzwqeYZT6ILyFRhjibRAgTnV9jsckzApRByPiVOtwna1/6fOLGYg==", - "engines": { - "node": "*" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 2784145a2..d8547b6a6 100644 --- a/package.json +++ b/package.json @@ -56,12 +56,9 @@ "axios": "^1.18.1", "geojson": "^0.5.0", "leaflet": "^1.9.4", - "leaflet-control-geocoder": "^2.4.0", "leaflet-mouse-position": "^1.0.4", - "leaflet-routing-machine": "^3.2.12", "leaflet.featuregroup.subgroup": "^1.0.2", "leaflet.markercluster": "^1.5.3", - "lrm-graphhopper": "^1.3.0", "maplibre-gl": "^5.16.0", "nouislider": "^15.8.1", "opening_hours": "^3.9.0", diff --git a/src/components/Map.vue b/src/components/Map.vue index de385dd0d..eb7b9697c 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -7,11 +7,9 @@ + + - \ 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/main.js b/src/main.js index 47750dffd..8b7e939ea 100644 --- a/src/main.js +++ b/src/main.js @@ -28,7 +28,6 @@ import optionsController from './optionsController.js' import { emit } from '@nextcloud/event-bus' import L from 'leaflet' -import 'lrm-graphhopper' import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png' import iconUrl from 'leaflet/dist/images/marker-icon.png' diff --git a/src/optionsController.js b/src/optionsController.js index 6190f4f02..ca4946a78 100644 --- a/src/optionsController.js +++ b/src/optionsController.js @@ -191,10 +191,6 @@ const optionsController = { if ('mapboxAPIKEY' in optionsValues && optionsValues.mapboxAPIKEY !== '') { this.nbRouters++ } - if (('graphhopperURL' in optionsValues && optionsValues.graphhopperURL !== '') - || ('graphhopperAPIKEY' in optionsValues && optionsValues.graphhopperAPIKEY !== '')) { - this.nbRouters++ - } if ('locControlEnabled' in optionsValues && optionsValues.locControlEnabled === 'true') { this.locControlEnabled = true diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index 38a1fb7d0..bcb267920 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -37,27 +37,6 @@ - - - - - - - - - - - diff --git a/src/components/map/ClickSearchMarker.vue b/src/components/map/ClickSearchMarker.vue new file mode 100644 index 000000000..3988d7ee8 --- /dev/null +++ b/src/components/map/ClickSearchMarker.vue @@ -0,0 +1,124 @@ + + + + + + + + diff --git a/src/components/map/PoiMarkersLayer.vue b/src/components/map/PoiMarkersLayer.vue new file mode 100644 index 000000000..3fea3ac43 --- /dev/null +++ b/src/components/map/PoiMarkersLayer.vue @@ -0,0 +1,251 @@ + + + + + + + + diff --git a/src/components/map/SearchControl.vue b/src/components/map/SearchControl.vue index ef95da05a..d57757c90 100644 --- a/src/components/map/SearchControl.vue +++ b/src/components/map/SearchControl.vue @@ -1,85 +1,60 @@ + + + - -:deep(.icon-dot-circle) { - background-color: var(--color-main-text); - mask: url('../../../img/dot-circle.svg') no-repeat; - mask-size: 16px auto; - mask-position: center; - -webkit-mask: url('../../../img/dot-circle.svg') no-repeat; - -webkit-mask-size: 16px auto; - -webkit-mask-position: center; + \ No newline at end of file + diff --git a/src/network.js b/src/network.js index f19406bf2..61d4ff701 100644 --- a/src/network.js +++ b/src/network.js @@ -71,6 +71,14 @@ export function searchAddress(address, limit = 8) { return realAxios.get(url) } +export function searchPois(subtype, value, bounds, limit = 100) { + const query = subtype + '=' + encodeURIComponent(value) + const viewbox = [bounds.west, bounds.south, bounds.east, bounds.north].join(',') + const url = 'https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&extratags=1&namedetails=1&limit=' + limit + + '&viewbox=' + viewbox + '&bounded=1&' + query + return realAxios.get(url) +} + export function exportRoute(type, coords, name, totDist, totTime, myMapId = null) { const req = { type, From d8ab23db7308bf3c66a6bf3cbb6536dfa7703814 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 21 Aug 2026 16:04:00 +0200 Subject: [PATCH 21/76] feat(maps): add right-click menu and drag-to-edit to favorites Right-click a favorite for delete/copy-to-map (matching the old per-marker menu), or a cluster for delete-favorites/zoom-to-bounds (via getClusterLeaves/getClusterExpansionZoom - MapLibre clustering has no spiderfy equivalent, so that part of the old cluster menu stays dropped, noted in the header comment). Drag-to-edit is plain pointer-event tracking (mousedown/mousemove/ mouseup) since GL symbol layers aren't real DOM elements and can't use native HTML5 drag: a local draggingOverride ref feeds the dragged point's live position into the GeoJSON source so it follows the cursor without a round-trip through the parent on every move, and only commits (emits edit) on mouseup. Map panning is disabled for the duration of the drag. Wired through Map.vue with the same event names App.vue's template already listens for (edit-favorite/delete-favorite/delete-favorites/ add-to-map-favorite/favorites-draggable), so no App.vue changes were needed. This is the first of the deferred per-layer right-click/drag-to-edit batches; contacts/photos/tracks/devices follow the same pattern. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/Map.vue | 13 +- src/components/map/FavoritesLayer.vue | 188 ++++++++++++++++++++++++-- 2 files changed, 191 insertions(+), 10 deletions(-) diff --git a/src/components/Map.vue b/src/components/Map.vue index e934cf181..6efdf5a6b 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -45,7 +45,12 @@ + :draggable="favoritesDraggable" + @click="emit('click-favorite', $event)" + @edit="emit('edit-favorite', $event)" + @delete="emit('delete-favorite', $event)" + @delete-favorites="emit('delete-favorites', $event)" + @add-to-map-favorite="emit('add-to-map-favorite', $event)" /> favoriteCategories?: Record favoritesEnabled?: boolean + favoritesDraggable?: boolean contacts?: Contact[] contactGroups?: Record contactsEnabled?: boolean @@ -179,6 +185,7 @@ const props = withDefaults(defineProps<{ favorites: () => ({}), favoriteCategories: () => ({}), favoritesEnabled: false, + favoritesDraggable: false, contacts: () => [], contactGroups: () => ({}), contactsEnabled: false, @@ -198,6 +205,10 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ 'add-click': [position: { lat: number, lng: number }] 'click-favorite': [favorite: Favorite] + 'edit-favorite': [favorite: Favorite] + 'delete-favorite': [id: number] + 'delete-favorites': [ids: number[]] + 'add-to-map-favorite': [favorite: Favorite] 'address-deleted': [contact: Contact] 'add-to-map-contact': [contact: Contact] 'open-sidebar': [path: string] diff --git a/src/components/map/FavoritesLayer.vue b/src/components/map/FavoritesLayer.vue index c4b6bb813..9589cfeb6 100644 --- a/src/components/map/FavoritesLayer.vue +++ b/src/components/map/FavoritesLayer.vue @@ -6,10 +6,10 @@ @@ -121,7 +121,7 @@ function ensureIcon(): void { } ctx.beginPath() - ctx.arc(size / 2, size * 0.4, size * 0.35, 0, Math.PI * 2) + ctx.arc(size / 2, size / 2, size / 2 - 2, 0, Math.PI * 2) ctx.fillStyle = '#e9762b' ctx.fill() ctx.lineWidth = 2 @@ -129,7 +129,7 @@ function ensureIcon(): void { ctx.stroke() ctx.beginPath() - ctx.arc(size / 2, size * 0.4, size * 0.12, 0, Math.PI * 2) + ctx.arc(size / 2, size / 2, size * 0.12, 0, Math.PI * 2) ctx.fillStyle = '#ffffff' ctx.fill() From 9bddaab77994b1b7b9bb46f8a1b0453ddc009327 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 22 Aug 2026 11:19:34 +0200 Subject: [PATCH 26/76] fix(maps): render poi markers and give them real category icons MglSymbolLayer had no :filter, unlike every other layer's symbol layer in this app. With it omitted, the layer and its geojson source both existed correctly (confirmed via getLayer/getSource), the icon was registered (confirmed via hasImage), and the layout was correct (confirmed via getLayoutProperty) - but queryRenderedFeatures for the layer always came back empty: nothing was actually being placed. Adding an explicit filter, matching the pattern every other layer already used, fixed it. Also, following MapLibre's own "Display a popup on click" example (https://maplibre.org/maplibre-gl-js/docs/examples/display-a-popup-on-click/), each feature now carries a per-result icon name and icon-image reads it via ['get', 'icon']. OpenFreeMap Liberty's own sprite already ships icons for most amenity categories (restaurant, cafe, fuel, library, pharmacy, ...) under the same OSM tag names Nominatim returns as `type`, so those are reused directly instead of drawing one generic marker for everything; a teardrop pin (anchored at its point, like a normal map pin) is drawn only as a fallback for categories the style doesn't have an icon for. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/map/PoiMarkersLayer.vue | 54 +++++++++++++++++++------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/components/map/PoiMarkersLayer.vue b/src/components/map/PoiMarkersLayer.vue index fdb4a40a1..6bf33e015 100644 --- a/src/components/map/PoiMarkersLayer.vue +++ b/src/components/map/PoiMarkersLayer.vue @@ -5,11 +5,15 @@ --> + + + + + From f8b5823f21d5c6a2f9e61ca4db524c9eb3ad94a2 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 22 Aug 2026 11:33:09 +0200 Subject: [PATCH 28/76] style(maps): make search result markers bigger icon-size 1.5 instead of 1 so search results stand out from the base style's own (unscaled) poi icons at the same zoom level. Also bumped the fallback pin's canvas resolution so it stays reasonably crisp at the larger size. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/map/PoiMarkersLayer.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/map/PoiMarkersLayer.vue b/src/components/map/PoiMarkersLayer.vue index 6bf33e015..32a2510f5 100644 --- a/src/components/map/PoiMarkersLayer.vue +++ b/src/components/map/PoiMarkersLayer.vue @@ -24,7 +24,7 @@ + :layout="{ 'icon-image': ['get', 'icon'], 'icon-anchor': ['get', 'iconAnchor'], 'icon-allow-overlap': true, 'icon-size': 1.5 }" /> @@ -59,8 +59,8 @@ const emit = defineEmits<{ const SOURCE_ID = 'search-pois' const POINT_LAYER_ID = 'search-pois-points' const ICON_ID = 'poi-marker' -const MARKER_WIDTH = 26 -const MARKER_HEIGHT = 34 +const MARKER_WIDTH = 32 +const MARKER_HEIGHT = 42 const mapInstance = useMap() From a25b8e9123f0b950c31bf1fee834e4d7cd47117a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 22 Aug 2026 11:35:56 +0200 Subject: [PATCH 29/76] docs(maps): update the port-progress tracking comment Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/Map.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Map.vue b/src/components/Map.vue index 602ca368a..a9d25664f 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -8,10 +8,11 @@ Port in progress: the map engine moved from Leaflet to vue-maplibre-gl. Ported so far: the base map (street/satellite/terrain), click-to-add, favorites/contacts/photos/tracks/devices markers and clustering, - routing, and the search box. Everything below is intentionally not - wired up yet and will come back one feature at a time in follow-up - steps: - - right-click context menu, drag-to-edit + routing, and the search box (including the base style's own poi + icons). Favorites has its right-click menu and drag-to-edit back; + contacts/photos/tracks/devices don't yet - see their own header + comments for exactly what's still missing on each. Everything below + is intentionally not wired up yet: - history control / slider - track elevation profile chart --> From dd8c7b45314dc8974aacde01c8bbf67970fa5391 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Sat, 22 Aug 2026 11:48:13 +0200 Subject: [PATCH 30/76] feat(maps): add right-click menus to contacts/photos/tracks/devices Same pattern as favorites' menu (native contextmenu, preventDefault, a plain-HTML popup with data-action click delegation): - contacts: right-click now opens the same popup as left-click (delete/copy-to-map) - it was always the same actions, just missing the second entry point. - photos: point right-click adds open-in-sidebar/display/remove-geo- data/copy-to-map; cluster right-click adds display-pictures/zoom- on-bounds/remove-geo-data (with a confirmation dialog, matching the old "are you sure" prompt, via getDialogBuilder instead of the removed OC.dialogs.confirmDestructive). Spiderfying a cluster has no MapLibre equivalent and stays dropped - display-pictures/zoom-on- bounds cover the same need. - tracks: right-click (on either the line or its point markers) adds change-color/display-elevation/copy-to-map/download - download is a plain download link, not an emit, same as before. - devices: right-click adds toggle-history/change-color/export/link- to-map. All four wire through Map.vue using the same event names App.vue's template already listens for, so no App.vue changes were needed. Drag-to-edit for photos/devices is still its own separate batch. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Carl Schwan --- src/components/Map.vue | 27 ++++- src/components/map/ContactsLayer.vue | 16 ++- src/components/map/DevicesLayer.vue | 78 +++++++++++++- src/components/map/PhotosLayer.vue | 152 +++++++++++++++++++++++++-- src/components/map/TracksLayer.vue | 91 ++++++++++++++-- 5 files changed, 341 insertions(+), 23 deletions(-) diff --git a/src/components/Map.vue b/src/components/Map.vue index a9d25664f..82356fe41 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -35,13 +35,20 @@ :tracks="tracks" :start="sliderStartTimestamp" :end="sliderEndTimestamp" - @click="emit('click-track', $event)" /> + @click="emit('click-track', $event)" + @change-color="emit('change-track-color', $event)" + @display-elevation="displayElevation($event)" + @add-to-map-track="emit('add-to-map-track', $event)" /> + @click="emit('click-device', $event)" + @toggle-history="emit('toggle-device-history', $event)" + @change-color="emit('change-device-color', $event)" + @export="emit('export-device', $event)" + @add-to-map-device="emit('add-to-map-device', $event)" /> + @open-sidebar="emit('open-sidebar', $event)" + @coords-reset="emit('coords-reset', $event)" + @add-to-map-photo="emit('add-to-map-photo', $event)" /> () const baseStyleId = ref(BaseStyleIds.Street) @@ -335,8 +352,10 @@ function zoomOnPhotoSuggestion(photo: { lat?: number, lng?: number }) { /** * The track elevation profile chart isn't ported to vue-maplibre-gl yet. + * + * @param _track the track whose elevation profile would be displayed */ -function displayElevation() { +function displayElevation(_track?: Track) { } /** diff --git a/src/components/map/ContactsLayer.vue b/src/components/map/ContactsLayer.vue index 066191b2d..e24e7f74c 100644 --- a/src/components/map/ContactsLayer.vue +++ b/src/components/map/ContactsLayer.vue @@ -21,8 +21,7 @@