Skip to content

Port to maplibre gl - #1699

Merged
CarlSchwan merged 76 commits into
masterfrom
port-to-maplibre-gl
Oct 16, 2026
Merged

CarlSchwan merged 76 commits into
masterfrom
port-to-maplibre-gl

Conversation

@CarlSchwan

Copy link
Copy Markdown
Member

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI (N/A)

Comment thread src/components/map/PhotoSuggestionsLayer.vue Fixed
Comment thread src/components/map/PhotosLayer.vue Fixed
Step 1 of the Leaflet -> vue-maplibre-gl port. Map.vue now renders via
MglMap instead of L.map(), rewritten in <script setup lang="ts">.

- Base map: street (OpenFreeMap "Liberty" vector style, no API key) and
  satellite (ESRI World Imagery raster, unchanged URL) via a small
  MglCustomControl toggle, replacing the old street/satellite easyButtons.
- Native MapLibre terrain (raster-dem, Mapzen Terrarium tiles) toggle.
- Dropped support for switching between many base layers (topographic,
  watercolor, roads overlay, the layers-control UI) - no longer needed.
- src/data/mapLayers.ts: added typed MapLibre style/terrain-source
  definitions; kept the legacy Leaflet-flavoured exports (LayerIds, Layers,
  baseLayersByName, ...) since MapContainer.vue (public favorite share
  page) is still Leaflet-based and depends on them until it's ported too.
- Removed now-fully-unused deps: @maplibre/maplibre-gl-leaflet,
  @raruto/leaflet-elevation, leaflet-contextmenu, leaflet-easybutton,
  leaflet.locatecontrol, mapbox-gl, mapbox-gl-leaflet, pmtiles.
- Added vue-maplibre-gl and @maplibre/maplibre-gl-directions (for a later
  routing step).
- .eslintrc.cjs: use @typescript-eslint/parser for .vue files so
  <script setup lang="ts"> parses.

Not ported yet (left for follow-up steps, on purpose):
favorites/photos/tracks/devices/contacts markers and clustering, routing
(will use @maplibre/maplibre-gl-directions), the right-click context menu,
search box / history control, and the track elevation profile chart.
Map.vue's children rendering those features are temporarily not mounted.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <img> 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 <carl@carlschwan.eu>
FavoritesLayer.vue rewritten in <script setup lang="ts"> against
MapLibre GL instead of Leaflet + leaflet.markercluster:

- A clustered GeoJSON source (native supercluster clustering) with a
  GL circle/count symbol layer for clusters and a symbol layer for
  individual favorites.
- Marker look (colored circle + white star, "selected" highlight) is
  redrawn on an offscreen canvas and registered as a GL sprite image per
  distinct color, since GL symbol layers need raster icons rather than
  the CSS classes/div-icons Leaflet used.
- Hover tooltip (name/category/comment) via a MapLibre Popup.
- Clicking a favorite emits `click` (forwarded by Map.vue as
  `click-favorite`, unchanged from App.vue's perspective).
- Clicking a cluster zooms to its expansion zoom.

Not ported yet, on purpose: drag-to-edit, the right-click context menu
(delete/copy-to-map on a marker, delete-multiple/zoom-to-bounds on a
cluster), and spiderfying overlapping markers at max zoom. These need
the broader per-marker interaction model (shared with tracks, devices,
contacts, photos) redesigned for MapLibre, which is follow-up work.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
ContactsLayer.vue rewritten in <script setup lang="ts"> against MapLibre
GL, following the same clustered-GeoJSON-source pattern as favorites:

- Contact avatars are fetched and clipped to a circle on an offscreen
  canvas, then registered as GL sprite images (symbol layers need
  pre-registered raster icons, not arbitrary URLs); a neutral placeholder
  icon shows until an avatar finishes loading.
- Clusters get a generic icon + count (not a representative avatar -
  GL clustering aggregates values, it can't pick "the first child's photo"
  the way the old code did).
- Hover tooltip and a click popup (delete this address/location, copy to
  map, open in Contacts) via MapLibre Popups, matching the old markup/
  classes closely enough to reuse the existing CSS.

Not ported yet, on purpose: the separate right-click popup (same actions
as the click popup, so nothing is actually lost) and spiderfying
overlapping markers at max zoom.

Fixes along the way:
- ADRTYPE comes back from the backend as a plain string for single-
  address contacts but an array for multi-address ones; .map() on the
  string case threw inside a MapLibre-invoked hover/click handler and
  broke the map's event loop. Normalize it to an array first.
- Map.vue never imported maplibre-gl's own CSS. Without it,
  .maplibregl-map/.maplibregl-ctrl/.maplibregl-popup have no positioning
  rules at all, so controls fell into normal document flow below the map
  and popups anchored to the viewport instead of their marker - this was
  latent since the very first step, just not obvious until a popup showed
  up. Also fixes favorites' hover tooltip positioning retroactively.
- Extracted the HTML-escaping helper FavoritesLayer.vue already had into
  src/utils/domUtils.ts so ContactsLayer.vue can reuse it instead of
  duplicating it.

Also switched this session's new/rewritten files' license headers to
SPDX tags, keeping the original authors' copyright lines where the file
predates this port.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
PhotosLayer.vue rewritten in <script setup lang="ts"> against MapLibre
GL, following the same clustered-GeoJSON-source pattern as favorites
and contacts:

- Photo previews are fetched and drawn to a square thumbnail on an
  offscreen canvas, then registered as GL sprite images; a neutral
  placeholder shows until a preview finishes loading.
- Clusters get a generic icon + count rather than a representative
  preview, same reasoning as contacts' avatar clusters.
- Hover tooltip (preview/date/filename); clicking a photo opens it in
  Viewer; clicking a cluster either zooms in (mirroring the old
  "zoom before showing" behavior for clusters over 10 photos while not
  at max zoom) or opens every photo in that cluster in Viewer via
  getClusterLeaves(), falling back to open-sidebar on the first one if
  Viewer isn't available.
- The date-range slider filter (dateFilterEnabled/Start/End) is ported
  as-is via the existing binSearch helper.

Dropped on purpose, not deferred: the chunked cluster-loading progress
events. MapLibre's clustering is synchronous and fast even for many
thousands of points, so there's no "in progress" state left to report;
App.vue's photosLoading is already managed around the actual data
fetch elsewhere.

Not ported yet, on purpose: the right-click popup (open sidebar, remove
geo data, copy to map, spiderfy) and drag-to-edit.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
TracksLayer.vue rewritten in <script setup lang="ts"> against MapLibre
GL. Unlike the marker-based layers (favorites/contacts/photos), tracks
are line geometry, which maps directly onto MapLibre's native line
layers rather than canvas-drawn sprite icons:

- One GeoJSON source with LineString features (track segments/routes)
  and Point features (first point + waypoints), split by geometry-type
  filters into three layers: a black casing line, a colored line on top
  of it (matching the old two-polyline "casing" technique), and circle
  markers for the points (data-driven circle-color/circle-radius, no
  sprite generation needed here).
- The time-window filtering logic (calculateLines/calculateFirstPoint/
  calculateWaypoints) is carried over as-is - it was already framework-
  agnostic - fixing a transcription slip along the way where a segment's
  own (empty) name was kept instead of its parent track's name.
- Hover tooltip on points (file/name/description/altitude/begin-time);
  clicking a line or a point emits `click` with the track.

Dropped on purpose, not deferred: the point-hover event the old
component emitted while hovering a line. Nothing has listened for it
since the Leaflet-to-vue-maplibre-gl port started (confirmed via git
history), so there's nothing left to preserve.

Not ported yet, on purpose: the right-click popup (change color,
display elevation, copy to map, download) and drag-to-edit.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <script setup> rewrite of Map.vue
dropped all of these, since script setup exposes nothing by default -
every one of those call sites threw "is not a function".

Restored them via defineExpose(), translating Leaflet-shaped bounds
(App.vue still constructs L.latLngBounds(...) at most call sites) to
MapLibre's [[west, south], [east, north]] format. displayElevation is a
no-op for now since the elevation profile chart itself isn't ported yet.

Also fixed an inverted, pre-existing condition in Sidebar.vue: the track
metadata tab was gated behind `v-if="isPublic() && ..."`, but isPublic()
means "no authenticated user" - so it only ever rendered for anonymous
share viewers, never for a logged-in user clicking their own track. This
was unreachable before TracksLayer was ported (nothing rendered tracks
to click), which is why it went unnoticed until now.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Sidebar.vue's custom tabs (favorite, photo-suggestion, maps-track-
metadata) are all gated behind !fileInfo, since they're meant to
replace the generic file sidebar rather than sit alongside it.
onTrackClick was opening the sidebar with the track's real file path,
which makes Sidebar.vue fetch that file's own fileInfo via a PROPFIND
and populate it - which then hides the maps-track-metadata tab, leaving
just the generic file header (a fresh, mostly-empty fileInfo: "0 B,
seconds ago").

Open with no path instead, same as onFavoriteClick already does, so
fileInfo stays null and the track metadata tab renders.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
DevicesLayer.vue rewritten in <script setup lang="ts"> against MapLibre
GL, combining the two patterns already used elsewhere: a canvas-drawn
sprite icon per device (colored circle + a simple desktop/phone glyph,
matching the old thumbnail distinction) for the last known position,
and native GL line layers (casing + colored line, same as tracks) for
the history trail.

- Hover tooltip (device name); clicking a marker or line emits `click`
  (forwarded by Map.vue as `click-device`, currently unconsumed by
  App.vue - same as before this port, nothing has regressed there).

Dropped on purpose, not deferred: the >2500-points-per-device heatmap
fallback (LHeatMap.vue, leaflet.heat) and its now fully-unused
dependency. That existed to keep Leaflet's DOM-based rendering from
choking on very long history trails; MapLibre renders line geometry on
the GPU and doesn't need the same escape hatch. Also dropped the
point-hover event this used to emit while hovering a marker or line,
confirmed dead the same way as tracks' - nothing has listened for it
since the Leaflet-to-vue-maplibre-gl port started.

Not ported yet, on purpose: the right-click popup (toggle history,
change color, export, link to map) and drag-to-edit.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
…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 <carl@carlschwan.eu>
They were shown as soon as two waypoints had names, even with no
router configured (or before a route had actually been fetched), so
clicking them silently did nothing since there was never any route
data to act on. Also restore the left margin that keeps the routing
panel clear of the app's own nav toggle and zoom controls, lost in
the rewrite.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Rebuilds a localized turn-by-turn instruction list from the raw OSRM/
Mapbox route response (src/utils/routingInstructions.ts), ported from
leaflet-routing-machine's OSRMv1._convertRoute()/
Formatter.formatInstruction() logic since maplibre-gl-directions only
draws the route and doesn't decode it into text. Reuses the same
t('maps', ...) strings the old formatter used, so existing
translations keep working unchanged. Clicking an instruction flies
the map to its maneuver location.

Also swaps the routing panel's plain <button> elements for NcButton,
and fixes maplibre-gl's default .maplibregl-ctrl-group chrome (white
background, box-shadow, border-radius) clashing with the panel's own
styling - it's an ancestor of the panel's content so scoped :deep()
can't reach it; :has() targets it globally instead, scoped to just
this control via its distinguishing class.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
setRouter() is called synchronously from an immediate watch(routers)
at setup time, and calls syncWaypoints(), which read hasWaypoints and
waypointCoords - both declared further down in the file. That's a
temporal-dead-zone reference: it threw, aborting the rest of <script
setup> before the watch(waypointCoords, syncWaypoints) further down
ever got to run. Only the router <select>'s own @change handler
(calling setRouter directly, well after setup) ever triggered a
re-fetch as a result.

Move waypointCoords/hasWaypoints/routeReady/latestDirections above
everything that can call syncWaypoints() at setup time, so the
dependency order is safe regardless of when things run.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Adds a variant prop to RoutingStep (start/via/end) with a colored
left border - green/default/red - and tightens the step list into a
single bordered card with hairline separators instead of gapped rows.
The router picker moves below the steps and becomes a pill-style
button matching the app's primary color, instead of a plain <select>
squeezed above them.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
…ents"

This reverts commit 36bef4b.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
SearchControl.vue is now a thin MglCustomControl wrapping the
existing SearchField (unchanged - it only ever produced plain
{lat,lng}-shaped results, never depended on Leaflet being the map
engine). Map.vue owns the result state and decides what a selected
result means, same split as before:
- contact/favorite: fly to it
- device/track: zoom if enabled, otherwise ask App.vue to enable it
- poi: bounded Nominatim category search, rendered by the new
  PoiMarkersLayer.vue
- result: a single already-fetched Nominatim result, same layer
- coordinate: shows the new ClickSearchMarker.vue with a reverse-
  geocoded address
- mylocation: geolocate and fly there

PoiMarkersLayer.vue (ported from PoiMarker.vue) draws all results
with one shared generic pin icon rather than Nominatim's per-category
icon images - avoids a per-poi image fetch and CSP allowance for a
cosmetic difference. Its unused opening-hours computed properties
(dead code - never referenced by the old template) were dropped.

Not ported on either new marker component: add-contact-address - it
depended on a "place an existing contact by address" mode that isn't
implemented yet in the new architecture. Only add-to-favorites
survived.

The search box occupies the same top-left slot as RoutingControl -
only one is shown at a time, same as before.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
geocode/searchAddress/searchPois all called nominatim via plain axios,
which uses XHR and can't override the page's Referrer-Policy per
request. If that policy is stricter than 'origin' (Nextcloud's own
default can be), these requests go out with no referer at all, and
Nominatim's usage policy treats unidentified requests as abusive and
blocks or rate-limits them - the same issue the tile layers already
had, fixed on another branch via their referrerPolicy option.

Switched these three to a plain fetch() with referrerPolicy: 'origin'
set explicitly, wrapping the result as { data } to keep every call
site's existing response.data access working unchanged.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
The immediate watch(routers) that auto-selects a router at setup time
wasn't gated on the map's style being loaded, only on the Map
instance existing. new MapLibreGlDirections() calls map.addSource()
synchronously in its constructor, so if the style hadn't finished
loading yet - routine at startup, since routers can become available
before the map does - it threw 'Style is not done loading', an
uncaught exception during Map.vue's render that could plausibly
disrupt other reactive updates queued in the same flush (e.g. a
just-arrived POI search result).

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
onSearchValidate's poi branch had no .catch(), so a rejected request
(network error, bad response) failed with nothing visible - clicking
a poi category looked like it did nothing at all. Now shows an error
toast on failure, and one for a genuinely empty result set too, so
'no results in view' isn't indistinguishable from 'broken'.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
The icon was drawn off-center (pin-style, at 40% height) with
icon-anchor: bottom, unlike every other layer's marker icon (all
plain centered circles with the default center anchor). Hover/click
weren't registering on the rendered icon at all even though it drew
correctly - a mismatch between the visible icon and the anchor used
for hit-testing is the most likely explanation. Matches the same
centered-circle pattern FavoritesLayer/DevicesLayer/ContactsLayer
already use successfully.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
OpenFreeMap Liberty renders its own POI icons (restaurants, shops,
etc.) directly from the vector tiles - those were purely decorative
before, unlike our own search-result markers. Clicking one now shows
the same kind of popup (name + Add to favorites), using the base
style's poi vector layers (source-layer 'poi', the OpenMapTiles
schema) rather than anything this app added itself.

The poi layer IDs are looked up by source-layer at click/hover time
rather than hardcoded (they're rank/zoom-threshold splits specific to
the current style version, e.g. poi_r7/poi_r20) - this also means the
lookup stays correct across street/satellite style switches without
needing to re-bind listeners.

Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
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 <carl@carlschwan.eu>
Assisted-by: ClaudeCode:claude-sonnet-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
- After picking a suggestion, the typed text stayed in the input on
  top of the selected value, and a focus handler copied the value into
  the input again, so the text was drawn twice.
- The separate "Clear POIs" button duplicated the field's own clear
  button. Clearing the field now removes the search results and the
  searched point from the map instead.
- MapLibre's control frame drew an outline around the field, the
  existing override was less specific than MapLibre's rule.
- Align the field with the navigation toggle: same top offset and
  height, starting one gap after the toggle, also on mobile where it
  could slide under it.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
…URLs

- Without a saved travel mode, selectedRouter is an empty string, which
  the ?? fallback kept instead of falling back to the first router, so
  no routing engine was set up and no route was ever requested.
- Routing URLs are trimmed when saved, and the stored values when
  building the CSP and the routers: a leading space made parse_url()
  miss the host, so the routing server was not allowed by the CSP. A
  trailing slash is stripped as the profile is appended to the URL.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Bring back the "Find directions" button next to the search field, the
way into routing on master that the port dropped.

The routing panel is redesigned:
- A card next to the navigation toggle, aligned with the search field;
  the hidden one of the two no longer pushes the other down.
- A labelled travel mode select with icons instead of emoji, and a
  warning card when no routing service is configured.
- The stops as a timeline with start, via and destination markers,
  reverse and "Add stop" buttons; clearing a stop's field empties it.
- A summary with duration and distance, zoom to route, and an export
  menu replacing the "Export as" dialog.
- The map fits a new route next to the panel, and long turn-by-turn
  instructions wrap inside their row.

Long search suggestions are shown on one line: the option styles were
nested under the field, but the dropdown is rendered in the body.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
OCA.Sharing.ExternalShareActions is deprecated and the action was no
longer offered in the files sidebar. Register it as an inline action of
the share link with registerSidebarInlineAction() from
@nextcloud/sharing/ui instead, updating @nextcloud/sharing to 0.4.0.
It is now only offered for link shares of folders, as only a folder can
be a map.

The toast it shows used the dialogs library without its stylesheet, so
the stylesheet is imported and loaded with the script.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
PublicUtilsController read the routing settings through
$this->appConfig, which AddCspTrait declared private, so the property
was undefined in the subclass and getOptionsValues() failed, leaving
the public map page blank.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
getToken() read the hidden sharingToken input, which mounting the app
replaces, so photos and favorites loaded during setup used the
logged-in endpoints: anonymous visitors got nothing and logged-in
visitors saw their own data. Fall back to the token in the URL.

The photo scan status is the viewer's own, so it is no longer requested
on public pages.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
- The settings are the visitor's own (tracking their position,
  registering geo links) or would save into the shared map's options,
  so they are no longer offered on public map pages.
- Position tracking was started when the shared map's options had it
  enabled, posting the visitor's position to the logged-in devices
  endpoint.
- The public page footer covered the bottom of the navigation, core
  only leaves room for it in the app content.

Assisted-by: ClaudeCode:claude-opus-5-5
- Choosing "Rename" in the map actions closed the switcher panel, as
  the menu is rendered outside of it, so the rename field never showed.
  Rename with an inline input in the actions menu instead.
- The rename response is the map's settings, not the map, and the
  folder is moved, so the renamed entry lost its id, path and current
  state. Reload the map list after renaming.
- Deleting the open map left it open, now the default map is opened.
- A new map is opened right after creating it.

Assisted-by: ClaudeCode:claude-opus-5-5
Use the non-legacy settings dialog with form boxes, like the files
app: switches with descriptions, the geo link registration as a form
box button and the map projection note as a link.

Assisted-by: ClaudeCode:claude-opus-5-5
Popup.on() returns a subscription since MapLibre 5, so chaining
addTo() after it threw and no popup opened.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Clicking a favorite on a public favorites share opens it in a sidebar,
editable when the share allows edits, instead of a popup. Selecting a
favorite no longer zooms the map out.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Drop the commented-out Leaflet track code, the unused myMapsEnabled
option, the "Say hi" placeholder action and styles for the old
multiselect component.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
@CarlSchwan
CarlSchwan force-pushed the port-to-maplibre-gl branch 2 times, most recently from 9e2cf31 to 9dd4775 Compare October 16, 2026 10:51
@CarlSchwan
CarlSchwan marked this pull request as ready for review October 16, 2026 10:52
@CarlSchwan
CarlSchwan force-pushed the port-to-maplibre-gl branch 2 times, most recently from c3b04ab to 5f6db2a Compare October 16, 2026 10:56
IRootFolder::getUserFolder() returns an IUserFolder since Nextcloud 36,
which does not exist on Nextcloud 35.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
@CarlSchwan
CarlSchwan merged commit 38de8d7 into master Oct 16, 2026
36 checks passed
@CarlSchwan
CarlSchwan deleted the port-to-maplibre-gl branch October 16, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants