Skip to content

feat(web): move the map from Leaflet to MapLibre, the app's map, at 2D parity - #592

Open
khagele wants to merge 1 commit into
efiten:masterfrom
khagele:feat/465-maplibre-map
Open

feat(web): move the map from Leaflet to MapLibre, the app's map, at 2D parity#592
khagele wants to merge 1 commit into
efiten:masterfrom
khagele:feat/465-maplibre-map

Conversation

@khagele

@khagele khagele commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #465

The problem

The website drew its map with Leaflet 1.9.4 on Carto raster tiles while the app had moved to MapLibre GL on OpenFreeMap's vector styles (#147). Two engines meant two basemaps, two treatments of the same colour tokens, and no road from the map to the 3D views the app has. Kasper's call (2026-09-06): port the map to the app's map, 2D parity first. Decisions in docs/2026-09-06-map-on-maplibre.md.

My changes in this PR

  • web/mapcore.js: the map, ported from app/src/huntmap.js's shape: the same two OpenFreeMap styles, the bare-background fallback when the style host is unreachable, the app's sky (web/sky.js, a copy pinned by the parity suite), and every overlay re-added on each style load. Sources for points, hex cells, the two CoreScope layers, Locate's inliers and outliers, the node-position connector and circles, and the ticker's ring; an image source for Locate's density cloud; markers in named groups; one popup at a time with open/close reaching the name-resolution deferral (Web: name-resolution redraw closes an open map popup #271); a closeless hover popup for the hex cell's line, since MapLibre has no tooltip.
  • web/mapmodel.js: the pure half, unit-tested: the collections (tier colour and opacity per feature, the index a click resolves), the heat bytes (same ramp and floor), the image corners, bounds, and the zoom convention.
  • Zoom stays in Leaflet's numbers on the outside. MapLibre counts against a 512 px world, Leaflet against 256, one level apart at the same scale. ?z= in shared links and the z the server bins hex cells by are converted at the edge (leafletZoom, mapZoomFromLeaflet), so every existing link lands where it did and no cell changes size.
  • web/map.js talks to the map through bounds, zoom, centre, clicks and popups; the filters, pickers, ticker sync, ignore list, urlstate, gates and notices are unchanged.
  • Controls follow the theme through MapLibre's class names, with the double-class attribution selector (web: Leaflet's zoom control keeps its default white chrome in dark theme #427's lesson) and the hover and disabled states as before.
  • e2e: the fixtures answer the hosted style with an inline bare style, so load fires offline and at once; the specs that counted Leaflet's SVG paths read a source's feature count through a page hook, click a coordinate through __mapProject, and read the heat image through __locateHeat.
  • Changelog entry in both copies.

Verification

Suite Result
web vitest 522 passed, eslint clean
web e2e, all 200, --fail-on-flaky-tests 200 passed (58 s)
app vitest 895 passed, eslint clean

One test added: hovering a hex cell shows its best RSSI, count and hunters, with real pointer movement, since a synthetic mousemove never reaches the WebGL canvas.

Browser, the real OpenFreeMap style, 12 stubbed hex cells and a 60-point Locate: cells draw with their tier colours, the density cloud sits over the grid's bounds, centroid and strongest markers stand, the info card reads "60 points · search radius ~439 m · encircle 100%"; a theme switch swaps the style and puts the 12 cells back; the zoom buttons and the attribution take the surface colour in both themes.

Not in this PR

  • 3D on the website: the core carries pitch and extrusions; the controls for it are a follow-up.
  • A zoom-to-fit animation: fitBounds jumps, as the app's does.
  • The 0.7-opacity raster fade on the heat image is off (raster-fade-duration: 0) so a Locate re-render does not cross-fade between polls.

🤖 Generated with Claude Code

…D parity

The website drew its map with Leaflet on Carto raster tiles while the app
had moved to MapLibre on OpenFreeMap's vector styles (efiten#147): two engines,
two basemaps, two treatments of the same colour tokens, and no road to the
3D views the app has. map.js now builds its map from mapcore.js, a port of
the app's huntmap.js shape (the same styles, the bare fallback, the sky,
overlays re-added on every style load), with the collections the layers
read built in mapmodel.js, which the unit suite covers.

Points, hex cells, the CoreScope layers, Locate's cloud and dots, the node
positions and the ticker's ring are GeoJSON sources swapped whole; markers
and popups are MapLibre's. Zoom keeps Leaflet's numbers in shared links and
in the server's hex binning, converted at the edge, so no link and no cell
changes. The e2e fixtures answer the hosted style inline so load fires
offline, and the specs read sources through page hooks where they counted
Leaflet's SVG paths.

Closes efiten#465

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@efiten

efiten commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Master moved: #569, #574, #580, #581 and #589 are merged, master is at f14d205. The conflict is the changelog arrays plus the tail of web/parity.test.js, where master (#580, #589) and this branch each append a describe block. Both are wanted; #580 and #589 were resolved by keeping master's block then this one's.

Notes below are from reading this PR's diff against master before those merges. Not a full audit, and the green checks are not part of it.

?z= no longer round-trips. leafletZoom() is Math.round(mapZoom + 1), and MapLibre zoom is continuous where Leaflet's was snapped, so a link shared from a wheel-zoomed view carries a rounded level and reopens at a different scale than the sharer saw. Keeping a decimal in the param, or snapping the camera, closes it either way.

In both mode a point click may land on the hex handler. map.on('click', layerId, ...) fires for every layer under the pointer, in registration order, and points is registered before hex. Clicking a point inside a cell should run the point handler (popup plus focusRecord(receptionKey(pt))) and then the hex handler, which re-focuses newestInRing, so the ticker ends up on a different row than the one clicked. Leaflet's marker click did not fall through to the polygon underneath. Worth a check in the browser; if it holds, one guard in the hex handler covers it.

Hex cells lost their touch tooltip. hoverText is mousemove plus mouseleave, where Leaflet's bindTooltip also answered a tap. On a phone a cell now says nothing.

The pending-data replay through addOverlays, the bare-style fallback, the single-popup rule and the heatImageData port all read well, and the MapLibre version matches app/index.html at 4.7.1.

Ordering: this is the base of #594, #596 and #593.

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.

web: the map has no 3D mode, because it is still on Leaflet while the app moved to MapLibre

2 participants