Skip to content

feat: MapLibre GL + PMTiles frontend rendering#140

Merged
fank merged 26 commits into
mainfrom
feat/maplibre-pmtiles
Feb 6, 2026
Merged

feat: MapLibre GL + PMTiles frontend rendering#140
fank merged 26 commits into
mainfrom
feat/maplibre-pmtiles

Conversation

@fank
Copy link
Copy Markdown
Member

@fank fank commented Feb 6, 2026

Summary

  • Add dual-mode CRS support: legacy raster tiles (L.CRS.OCAP) and MapLibre GL + PMTiles (L.CRS.EPSG3857), activated by maplibreStyle field in map.json
  • All coordinate conversions go through armaToLatLng() gate function — works transparently across both modes
  • Add style switcher control (Topo/Satellite/Hybrid) with HEAD-based availability probing, localStorage persistence, and direct initial style loading
  • Update grid layer zoom thresholds, popup hide threshold, and debug helper for MapLibre zoom range
  • Add HEAD handler for /images/maps/* on the server
  • Legacy raster tile mode fully preserved — zero changes when maplibreStyle is absent

Changed files

  • static/scripts/ocap.js — dual-mode CRS, MapLibre layer init, saved style resolution
  • static/leaflet/L.Control.MaplibreStyles.js — new style switcher control
  • static/leaflet/L.Control.MaplibreStyles.css — styling for the control
  • static/leaflet/L.Layer.Grid.js — MapLibre-mode coordinate conversion and zoom thresholds
  • static/index.html — CDN includes for MapLibre GL, PMTiles, leaflet adapter
  • internal/server/handler.go — HEAD route for map assets

Test plan

  • Load a mission on a map with PMTiles data (e.g. malden) — verify MapLibre renders
  • Switch between Topo/Satellite/Hybrid styles — verify tiles change
  • Reload page — verify saved style preference is restored
  • Load a mission on a legacy raster map — verify legacy mode unchanged
  • Load a map missing style variants — verify unavailable buttons are hidden

fank added 26 commits February 4, 2026 23:00
…entation

- Change --profile=raster to --profile=mercator in gdal2tiles so tiles
  are proper Web Mercator tiles that MapLibre can render at correct scale
- Add EPSG:4326 georeferencing to VRT (places image at equator matching
  armaToLatLng coordinate conversion)
- Remove DstYOff flip — satellite tile Y maps directly to image row,
  GeoTransform handles orientation (south-up raster, GDAL reprojects)
- Remove MBTiles y-flip — mercator profile outputs TMS convention,
  same as MBTiles (no conversion needed)
- Calculate zoom levels dynamically via MercatorZoomForWorld instead of
  hardcoded maxZoom=6 (Altis: z10-17, Stratis: z12-16)
- Use math.Round instead of math.Ceil for maxZoom to avoid oversampling
  (Altis: ~14K tiles/~350MB vs ~54K tiles/~1.3GB)
- Add -r average resampling to reduce edge artifacts
- Add minzoom/maxzoom to style.json source metadata
Arma satellite tiles have 32px overlap on all edges for in-engine texture
blending. Crop to 480px effective size in the VRT SrcRect/DstRect to
eliminate visible seam duplication in PMTiles output.

Also upscale undersized 4×4 ocean placeholder tiles to 512×512 via
nearest-neighbor before PNG encoding, so all tiles match the VRT layout.

Other fixes:
- Remove TMS Y-flip in MBTiles (gdal2tiles mercator already uses TMS)
- Add georeferenced EPSG:4326 GeoTransform to VRT (north-up, equator)
- Compute min/max zoom from world size via MercatorZoomForWorld()
- Pass MinZoom/MaxZoom through Job and into map.json/style.json
…gery

The satellite tile grid is sparse — Altis has 1863 tiles spanning X=2-60,
Y=8-55, not the full 64×64 grid. Using (maxTile+1)*tileEffective as VRT
dimensions stretched the image to fill worldSize, causing markers to
misalign: 3× vertical error, 1× horizontal (14.3% vs 4.9% stretch).

Fix: set VRT canvas to worldSize×worldSize (1 pixel = 1 meter). Tiles are
placed at their correct pixel positions; missing ocean tiles are transparent.
Parse OPRW v24/25 WRP files to extract elevation, building objects, and
road networks, then generate vector.pmtiles via tippecanoe. The pipeline
produces contour lines (marching squares), building point features, and
road polylines as separate vector tile layers.

Key implementation details:
- Full WRP binary parser with QuadTree, LZO decompression, and section
  navigation using LZO end-of-stream markers to handle go-lzo's bufio
  overread
- Contour generation via marching squares algorithm at configurable
  intervals (10m minor, 50m major)
- Style.json updated with vector layers when vector.pmtiles exists
- Vector tile stage is optional (skips gracefully if tippecanoe missing
  or WRP parsing fails)
… extent

topo.pmtiles had world-spanning bounds (-180,-85 to 180,85) because
TilesToMBTiles only wrote name/format/type metadata. pmtiles convert
falls back to global bounds when these fields are missing. Now writes
bounds, minzoom, maxzoom, and center derived from worldSize.
Surface roads in Arma WRP files are terrain textures, not geometry.
The actual road polylines live in roads.shp inside the map's _data.pbo.

- Add shapefile reader (.shp + .dbf) and RoadsLib.cfg parser
- Auto-detect coordinate offset from shapefile bounds vs worldSize
- Extract 1414 road polylines from shapefile + 40 bridges from RoadNet
- Style roads by type (main road, road, track) with varying width/color
- Fall back to RoadNet-only extraction when data PBO is unavailable
# Conflicts:
#	internal/maptool/mbtiles.go
#	internal/maptool/metadata.go
#	internal/maptool/vector.go
#	internal/maptool/wrp.go
# Conflicts:
#	internal/maptool/mbtiles.go
#	internal/maptool/metadata.go
#	internal/maptool/tiles.go
#	internal/maptool/vector.go
#	internal/maptool/wrp.go
Adds L.Control.MaplibreStyles for switching between the three generated
style variants at runtime via MapLibre GL's setStyle() API.
Probe satellite.json and hybrid.json with HEAD requests on init,
hide buttons for unavailable styles. Hide entire control if only
one variant exists.
Saves the user's style choice (topo/satellite/hybrid) and restores
it on next page load. Falls back to topo if localStorage unavailable
or saved index is out of range.
HEAD returns 405 on the Go server — switch to GET for variant probing.
Resolve saved style preference before creating the MapLibre layer so
the correct style loads on first render with no flash or double-load.
Avoid calling map.project() with undefined mapMaxNativeZoom in
MapLibre mode. Output Arma coordinates instead.
The Go server only registered GET for map assets, returning 405 for
HEAD requests. Register HEAD route so the style switcher can probe
variant availability without downloading the full response body.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 6, 2026

Merging this branch will not change overall coverage

Impacted Packages Coverage Δ 🤖
github.com/OCAP2/web/internal/server 36.47% (ø)

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/OCAP2/web/internal/server/handler.go 33.05% (ø) 929 307 622

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

@fank fank merged commit 7b67293 into main Feb 6, 2026
2 checks passed
@fank fank deleted the feat/maplibre-pmtiles branch February 6, 2026 18:01
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.

1 participant