Integrate tile57 engine#40
Merged
Merged
Conversation
S-52 §14.5 fine-grained viewing-group selection. The S-101 portrayal already computed a viewing-group number per feature; the baker now carries the MOST-VISIBLE draw's VG onto FeatureBuild and bakes it as the per-feature `vg` tile tag (areas/lines/symbols/text via route(), soundings, and sector-light legs). band(vg) == cat always holds, matching tile57's Portrayal.vg rule for Go↔Zig parity; emitted only when non-zero, so unbanded features' tile footprint is unchanged. Client: a `viewingGroupFilter` deny-list on mariner.viewingGroupsOff (byte-identical to tile57's chartstyle filter), a curated taxonomy of the standard mariner-selectable groups (viewing-groups.mjs, ~32 groups in 7 sections), and a "Viewing groups" settings tab of grouped toggles that add/remove a group's vg ids from the deny-list. Re-applies live (no re-bake) and persists via the existing mariner path. Bundles the settings reorg it builds on — the Display tab split and the cumulative Base/Standard/Other detail-level control — because they share core-settings.mjs (the cumulative `detailLevel` get/set is in the same hunks as the vg get/set). Spec: specs/viewing-groups.md (gitignored, untracked). tile57 engine counterpart already landed (../tile57/specs/viewing-groups.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three readability fixes to the settings dialog: - Setting rows stack: the description moves to its own full-width line BENEATH the label+control, instead of sharing the narrow label column beside the control (where long text — e.g. "Non-dangerous underwater rocks, wrecks & obstructions" — wrapped hard). - Section headers read as distinct dividers and STICK to the top of the scrolling pane until the next section's header pushes them up (mirrors the chart library's .mcol-head). Pane top padding dropped so the pinned header sits flush with no row peeking through above it. - Remove the duplicate "Screen calibration" group from General (the screen-diagonal method); the dedicated Calibration tab (the S-52 5 mm CHKSYM ruler-measure method) is the single calibration UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the native tile57 engine (chartplotter-native, via the ../tile57 symlink) as an OPT-IN tile/asset/style backend, fully behind the `tile57` build tag so the default build and all release/xbuild binaries stay CGO-free (the CLAUDE.md invariant). `make build-tile57` links libtile57; `make serve-tile57 ENC_ROOT=<dir>` serves a live set; `cp bake --tile57` and `cp serve --tile57 <ENC_ROOT>` are the runtime flags. - Build: build-tile57/serve-tile57 Makefile targets; go.mod requires the tile57 Go binding via a local replace (../tile57/bindings/go); CLAUDE.md documents the opt-in backend. - CLI/server: tile57_*.go pairs (//go:build tile57 / !tile57) provide the bake/serve/assets/style entry points, with no-op stubs in the default build so nothing tile57-tagged leaks into the CGO-free path. - Server: wire the native baker/style/asset emission into the existing bake + serve + tileset paths. - Web: client adopts the engine-emitted S-52 style/assets (chart-sources, chart-style) and the server-reported bake-engine selector (chartplotter, coverage-boxes). CGO-free build + vet + server tests pass. Binary artifacts (examples/*.wasm, *.cpx, testdata/*) remain intentionally untracked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…point
Server-side plumbing so the client can render from the engine's MapLibre
style (the "use the tile57 style for tile57 tiles" direction):
- /api/style.json now reads the FULL mariner from the query (marinerFromQuery)
— display categories, contours, boundary/point style, text groups, dates,
viewingGroupsOff, sizeScale, scaminFilterGate — so the engine bakes the live
display state into the style.
- SET-AWARE: the chart-source tiles URL derives from ?set, so the same endpoint
serves the live --tile57 set AND any registered tile57-baked pack (not just a
hardcoded "tile57" set). 404 when the set isn't registered.
- POST /api/style-diff {from,to}: engine-computed op array (tile57.StyleDiff)
for flicker-free mariner toggles — the client applies setFilter/setPaintProperty
in place instead of re-fetching the whole style.
- gzip /api/style.json and /api/style-diff (the style is multi-MB; ~37x smaller).
- Binding-compat with the updated tile57 Go binding: PickAttrs enum (PickInclude)
and Style(minZoom,maxZoom).
Both the default CGO-free build and -tags tile57 compile; the default build keeps
501 stubs for the tile57-only endpoints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… & baked packs) On the tile57 backend the client now renders from the engine's MapLibre style (/api/style.json) instead of the JS builder — ONE style source, so text halo, LocalOffsets, colours and SCAMIN all come from the engine (no JS-vs-engine drift; this is what fixes the "--tile57-bake has a halo, --tile57 doesn't" mismatch). - Engine mode activates for the live "tile57" serve set AND for a single tile57-baked pack (_engineSet, chosen from the active server sets; re-evaluated in setServerSets so packs activated post-boot are caught). The server 404s /api/style.json for a non-tile57 set, so the probe is the real gate. Multi-pack installs fall back to the JS builder. - buildStyle() is a wrapper: _engineStyleMerged (grafts the client basemap + no-data + overlays onto the engine chart layers) or _buildStyleJS (the Go-backend path, unchanged). - Flicker-free mariner toggles: POST /api/style-diff, apply the op array in place (setFilter/setPaintProperty), debounced so a boot burst coalesces to one diff; rebuild fallback. No setStyle per toggle. - SCAMIN filter-gate controller: the engine emits ONE live-filtered layer per render-type (scaminFilterGate) instead of ~1200 per-value buckets; the client rewrites the current display-scale denominator on ladder-boundary crossings (_scaminUpdate). Guards on isStyleLoaded so it never touches the map mid-load. Go-backend rendering (the JS builder) is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-client "bakeEngine" setting (defaulting to "go") could override the --tile57-bake launch default and, after a few UI bakes, silently pin a tile57 server back to the Go baker → Go-baked packs rendered by the JS builder (halo, SCAMIN buckets, text drift). A tile57-capable binary now ALWAYS bakes with libtile57; the CGO-free build still falls back to the Go per-band baker. Removes the client bakeEngine setting, the Advanced → "Bake engine" toggle, and the /api/health bakeEngines capability advertisement (all now vestigial). Phase 1 of specs/tile57-only-engine.md — independent of the full removal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/xbuild-tile57.sh + `make xbuild-tile57`: cross-compile the CGO binary that statically links libtile57, using Zig as the C toolchain. Proven for linux + windows (amd64/arm64) — all four cross-link cleanly from any host with Zig alone. Per target it builds libtile57 for that arch (normalizing Zig's Windows tile57.lib → libtile57.a), cross-builds with `zig cc -target …`, then restores the host lib. darwin is deliberately excluded and built natively on a macOS CI runner: with GOOS=darwin, Go's own crypto/x509 links -framework Security/CoreFoundation, which Zig doesn't bundle. Spiking all six targets (specs/tile57-only-engine.md phase 2) established this; the flip of the default build + the goreleaser/CI rework land with the Go-engine deletion (phase 4), where they're coherent and no longer coupled to the CGO-free variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a per-band libtile57 bake so the district/demo/widget workflows keep
working once the Go baker is gone (Phase 4a of specs/tile57-only-engine.md,
"native parity first"). Cells are grouped into navigational bands by compilation
scale (the same BandForScale mapping the Go baker uses) and each band's subset is
baked on its own via tile57.BakeCells into <out-stem>-<slug>.pmtiles, so the
archive is naturally clipped to that band's coverage. Honors --max-zoom and
--overzoom; writes the same {aux, districts:[{band,bounds,file}]} manifest shape
the Go baker's --bands emits, so the frontend's per-band MultiArchive loads it
unchanged. Verified on the S-64 TDS: 30 cells → 5 valid per-band PMTiles archives
+ manifest.
Also DRYs the two existing manifest writers into writeManifestJSON.
NOTE: these archives carry tiles only. tile57 tiles render from the ENGINE style,
and rendering per-band SEPARATE archives needs a multi-source style — the job the
JS builder does today and what Phase 5 (multi-pack engine style) must replace. So
the render side of this parity is on Phase 5's critical path, as the spec predicts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… sole baker
Rips out the entire Go tile-baking + S-101 portrayal engine (~12.7k LOC):
internal/engine/{bake,portrayal,mvt,assets,s101,tile} plus the Go baker guts,
the server's Go dynamic per-tile generator (tile.go + /api/tile route) and Go
bake fallback, and the Go asset emitter. The native libtile57 engine is now the
ONLY tile/portrayal/asset engine.
Kept as the surviving CGO-free metadata layer: internal/engine/baker slimmed to
just cell parse (ParseCellBytes/WithUpdates/Coverage), CellData, ExtractCellMeta,
and the compilation-scale → band mapping (moved out of the deleted bake pkg into
baker/bands.go), used by the chart library, cell index, and the tile57 bake path.
s101catalog kept as the embedded catalogue FS the tile57 asset emitter reads.
The -tags tile57 split stays for now: the CGO-free default build still COMPILES
(CI stays green) but bake/emit-assets error via stubs — you must build with
`make build-tile57`. Flipping the default to CGO+tile57 (and the CI/goreleaser
rework) is the next step. `cp bake` is now tile57-only (--bands / bundle);
`cp bake --bands` no longer needs an explicit --tile57.
Verified: both builds compile, full test suite passes, `cp bake --bands` bakes
the S-64 TDS (30 cells → 5 per-band archives + manifest). Drops the unused
gopher-lua dependency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate chartplotter to tile57's breaking ABI refactor:
- BakeCells → BakePmtiles([]Cell, BakeOpts); CellInput → Cell; the pick enum →
BakeOpts.OmitPickAttrs.
- BakeBundle(input, outDir, BakeOpts, progress) (was 9 positional args).
- The 4 asset generators (Colortables/Linestyles/SpriteAtlas/PatternAtlas) →
one BakeAssets(catalogDir) → Assets. emitS101Assets now takes a catalog DIR
("" = libtile57's EMBEDDED catalogue) and drops the catalogFS/namedFromDir
plumbing entirely — so `cp emit-assets` / serve asset emission no longer needs
the Go s101catalog embed or an external --s101 dir (verified: emits all 6 files
from the embedded catalogue).
- Live serve: OpenCells([]Cell) → streaming Open(path) (reads the ENC_ROOT from
disk on demand; zips staged to a temp dir first).
Both builds compile, full tests pass; verified `cp bake --bands` (S-64 TDS) and
`cp emit-assets` against the new lib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te 404s
Two live-render bugs surfaced now that libtile57 is the sole engine (no Go/JS
fallback):
1. /api/style.json 404 for a baked pack. The endpoint defaulted ?set to the
literal "tile57" (the live serve set) and 404'd otherwise — a holdover from
"engine style only for the live set, else fall back to the JS builder." With
the Go engine gone, EVERY pack renders from the engine style. New
resolveStyleSet() maps the requested ?set to a real registered set (the
requested one, else the sole pack), and styleCtx derives the tiles URL from
the RESOLVED set — so a client asking for set=tile57 against a single baked
noaa-d1 pack now gets that pack's style pointing at /tiles/noaa-d1/. (An
ambiguous multi-pack install still 404s → Phase 5 multi-source composition.)
2. /sprite@2x.{json,png} 404s. The client renders chart symbols via its own
SpriteBuilder (map.addImage from the tile57-custom sprite.json/png it fetches
itself), so carrying the engine style's `sprite` field only made MapLibre fetch
a standard @2x atlas that doesn't exist. Drop `sprite` from _engineStyleMerged.
Verified: baked the S-64 TDS as a bundle into a noaa/d1 cache layout, served it,
and /api/style.json?set=tile57 → 200 with tiles=/tiles/noaa-d1/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…clamp" The tile57 ABI migration set BakeOpts.MaxZoom = 0 for the bundle bake intending "no clamp", but MaxZoom 0 clamps every band down to z0 — an empty archive (the old positional call passed 24 and its comment warned exactly this). Result: every imported/baked pack had a 550-byte empty chart.pmtiles (maxzoom 0), so the client loaded the engine style but got HTTP 204 for every tile → nothing rendered. Use MaxZoom 24 (the ABI's no-clamp sentinel) in both BakeBundle callers. Verified: the S-64 TDS bundle goes from 550 bytes → 7.5 MB, TileJSON maxzoom 0 → 18, and tiles serve real MVT at z16–18. Also: a corrupt cell libtile57 tolerates bakes n=1 with an inverted bbox (no coverage); guard on that (and n==0) so such an import errors and registers no empty pack instead of silently succeeding — restores TestImportJobErrorPath. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…57 split libtile57 is the sole engine, so the build-tag split is gone: the 5 tile57_*.go files become unconditional and the 5 tile57_*_off.go CGO-free stubs are deleted. A plain `go build ./...` now links libtile57 (CGO required); there is no CGO-free build. Removes the now-dead tile57Available / bakeTile57Available consts, the Server.BakeEngine field, and the --tile57-bake serve flag (imports always bake tile57 bundles). The tile57 tests now run under plain `go test` too. Next: flip `make build` to this, and rework ci.yml/goreleaser to install Zig + build libtile57 (they still assume the CGO-free build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`make build` now builds the lib with Zig on demand and CGO-links it — libtile57 is the sole engine and embeds the S-101 catalogue, so there is no embed_s101 sync step. `build-tile57` is a back-compat alias for `build`. `serve-tile57` drops the removed --tile57-bake flag. `version` reports the libtile57 version instead of the (now vestigial) Go s101catalog embed; s101catalog has no Go importers left (its custom-overlay symbols belong in libtile57 now). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The -tags removal made the default build CGO+libtile57, so CI must check out the sibling chartplotter-native repo, install Zig, and build the static lib before gofmt/vet/test/build. Both jobs now do this. CAVEAT: untested (can't run GitHub Actions locally). Needs a test push to confirm the mlugg/setup-zig version and the two-checkout paths. If chartplotter-native is private, add a PAT/repo secret and pass it as `token:` on that checkout (the default GITHUB_TOKEN can't read a different private repo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…101 path The old CGO-free `make xbuild` can't work anymore (the libtile57 binding needs cgo), so `xbuild` is now an alias of `xbuild-tile57` (zig cc cross-compile, linux+windows; darwin on a Mac runner). Drops the embed_s101 tag from the script and target — the S-101 catalogue lives in libtile57, so there's no sync/embed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… sole engine Phase 6 of specs/tile57-only-engine.md: CLAUDE.md described a CGO-free build with an opt-in -tags tile57 backend. That's inverted now — libtile57 is the sole engine linked via CGO, CGO_ENABLED=0 no longer builds, and cross-compilation is preserved with Zig as the C toolchain. Update the overview, commands, and conventions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e 5) With libtile57 the sole engine, a multi-pack install (several NOAA district bundles) must render entirely from the engine style — there's no JS-builder multi-source fallback. The server now composes ONE MapLibre style spanning every active pack: ?set is a CSV (default = all registered), and each set's style is built with its OWN scamin ladder + top zoom, then merged — each "chart" source becomes "chart-<set>" and each layer id gets a "--<set>" suffix (source ref rewritten, backgrounds dropped, sprite/glyphs shared). Sets are ordered coarse→fine. Single-set requests keep the original single-source style unchanged. Client: _initEngineStyle adopts ALL active sets (drops the sole-set gating / JS-builder pick), sends the CSV as ?set, and unions the scamin ladders across packs for the filter-gate. The style-diff endpoint stays single-set (a multi-pack CSV 404s → the client falls back to a full re-fetch). Verified: two baked packs → /api/style.json composes 2 sources (chart-noaa-d1, chart-noaa-d5) + 708 layers; a single ?set keeps 1 source + 355 layers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…machinery s101catalog (the Go embedded-catalogue package) has had no importers since asset emission moved to libtile57's embedded catalogue, and its only custom-overlay symbol (NEWOBJ01 "!") is handled natively by libtile57 (s57_mvt.zig). Delete the package, the Makefile sync-s101 target + S101_EMBED_DIR/S101_CUSTOM/S101_CACHE vars, and the .gitignore embed-dir entry. Simplify serve/serve-demo/docs-shots to not force --s101 — the catalogue lives in libtile57 now; --s101 stays an optional external override (for rule iteration). Strip the now-dead `make sync-s101` step from release.yml (it + .goreleaser.yaml still need the full CGO/Zig rework — flagged in-file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…riority pickCmp sorted by draw_prio first, so a LNDARE fill (S-52 priority 12) outranked every point symbol below priority 12 and features on land were effectively unclickable — the pick report always led with the land polygon. A click is aimed at the discrete object under the cursor; the skin-of-the-earth polygon it sits on is context. Rank point < line < area (labels last) first and apply §10.8.4's priority ordering within each rank. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l .a content With bin/chartplotter already present, go build's action ID (which ignores libtile57.a's content) matched the existing buildid and silently skipped the link, so a rebuilt engine never reached the output. rm -f the binary first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-mode load setServerSets is the one setStyle path that ENTERS engine mode, and unlike _engineRestyle / _engineRebuild it had no post-apply _scaminUpdate. The engine style bakes the filter-gate clause at curDenom=0 (show-all); every other setStyle path re-injects the live display-scale cutoff after the new style loads, but the boot / set-switch path did not — so `_scaminBandLast` stayed -1 and every SCAMIN-gated feature (soundings, symbols, text) drew at ALL scales until the first `move` fired _scaminUpdate. Symptom: opening a share-link (or just loading the app and not panning) showed the fully-ungated, massively over-cluttered chart — e.g. at 1:75,000 the whole sheet was peppered with 1:59,999-SCAMIN spot soundings that S-52 says drop by ~z11.9. A single pan self-healed it, which masked the bug. Fix: after the source rebuild in setServerSets, re-inject the cutoff once the new engine style has settled (once "idle" → _scaminLayersCache=null; _scaminUpdate(true)), mirroring the force-apply the other setStyle paths already do. See scamin-layers.md (client half of the filter-gate). Verified against the live app: on a fresh load with no interaction, band now transitions -1 → 7 automatically and the spot-sounding pepper is decluttered, while dangers / contours / depth areas / quality boundaries remain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maxTileCacheZoomLevels 5→20 covers the full z3.5–18 range so switching between overview and approach scales hits the cache instead of re-requesting. fadeDuration 300→100ms so newly-loaded tiles appear faster during pan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…/geo)
pkg/s100/{catalog,fc,instructions,symbols} and pkg/geo have had zero
importers since the Go bake/portrayal engine was deleted — libtile57 owns
all portrayal. The untracked internal/engine/s101catalog data tree is
removed from disk too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ials The app should know as little S-57 as possible — libtile57 is the engine. What survives in Go is only what tile57's ABI can't provide yet: per-cell header/coverage metadata (DSID/DSPM + M_COVR) for band grouping and the chart library, CATALOG.031 titles on import, and the full feature parse the NMEA simulator's water mask needs. Deleted capabilities (all zero-caller since the Go baker went away): - conformance validation (conformance.go, ValidateConformance, Warnings) - derived coastline-coincident + explicit edge-masking outputs (MaskCoastlineCoincidentBoundaries, BoundaryLines/Lines/Quapos) — tile57 masks edges natively - the R-tree spatial index + FeaturesInBounds/FeatureCount query path (drops the rtreego dep; bounds still derive from M_COVR) - pkg/s57 convenience API: Parser interface/NewParser/Parse/ParseFS, attributes.go schemas, Bounds ops, unused DSID getters + UsageBand/ CoordinateUnits/DepthUnits types, SOUNDG DEPTHS synthesis - catalog: Catalog.Bounds, ParseCatalogFS, CRC/Comment fields - baker: ParseCellWithUpdates/IsBaseCell/IsUpdateCell/Band.ZoomRange (tile57 applies .001+ updates itself on every bake/serve path) The server cell index now uses the cheap M_COVR-only coverage parse instead of a full feature parse per cell. Also drops the orphaned srwiley SVG-raster deps left from the deleted Go sprite emitter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine style's chart source carried minzoom 9 for every pack: build() and the style-diff path hardcoded minZoom=0, and the old libtile57 ABI treated 0 as "engine default" (9). MapLibre never requests vector tiles below a source's minzoom, so zooms 0-8 rendered blank even when the baked archive holds tiles from z0 (noaa-d5: header minzoom 0, real content at z0-8, nothing on screen below z9). styleCtxForSet now carries meta.MinZoom and both style paths pass it; paired with the libtile57 fix (chartplotter-native eba6d2a) that emits the caller's minzoom verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve frame
Every ladder-boundary crossing rewrote all gated layers' filters mid-zoom
(measured: 14 bursts x 68 setFilter calls in one z8-to-z16 sweep, each burst
~110ms of main-thread validation plus a full worker re-parse + symbol
re-placement of every chart source). That was the zoom jank, and the reload
churn read as symbols blinking out mid-gesture.
Crossings now apply once the camera settles (moveend + 120ms, cancelled on
movestart, retried past mid-load isStyleLoaded windows), and the setFilter
loop skips MapLibre's filter validation ({validate:false} - filters are
engine-generated, not user input). Verified headless: 0 setFilter during a
15s continuous zoom, one coalesced ~3ms apply after settle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settle-only gating left a long continuous zoom rendering a stale cutoff until moveend. While the map is actively zooming (isZooming — pans still wait for settle), a crossed ladder boundary now applies immediately but at most every 500ms; the moveend pass remains the final exact apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gate Pre-lands the client half of ../tile57's band-handoff spec: carry-down features from the next-coarser band arrive tagged smax (the handoff denominator), gated by ["<", ["coalesce",["get","smax"],0], N]. The gate injector now sets that N together with the scamin clause's. No-op on tiles that don't carry the clause yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine's band-handoff carry-down puts the smax clause on EVERY chart layer (carried fills/lines land in base layers with no scamin clause); those layers must receive the live denominator at ladder crossings too, or carried coarse content stays hidden at its baked placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r zoom The straggler sweep reloaded (re-fetched + re-parsed) every tile of every chart source on the first idle after EVERY gate apply — i.e. after every zoom settle that crossed a SCAMIN band. On a multi-pack install that re-fetches all tiles across all sources on every zoom, a big part of 'tiles crawl in after you stop scrolling'. The race it guards is a one-time BOOT condition (later tiles parse under the broadcast filters), so sweep once and never again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… explosion The per-value SCAMIN bucket layers (~18 kinds × up to 33 values × N packs = thousands of style layers) can instead be collapsed to ~1 layer per kind using the engine's EXISTING merged zoom-expression gate (style.zig zoom_gate): with an empty SCAMIN manifest and the filter-gate off, each *_scamin layer carries the self-evaluating clause [">=",["zoom"],log2(DENOM_Z0/scamin)] (smax/oscl likewise use their .zoom_k variant), so the live zoom gates the features with NO client setFilter and NO source reload. No engine change — just pass an empty manifest. Server: styleCtxForSet skips the meta.Scamin population when ?scaminMerge is set, and marinerFromQuery forces ScaminFilterGate off so the empty manifest reaches the zoom_gate branch (which style.zig checks after filter_gate). Works for the single, multi-pack, and style-diff paths (all read the same query). Client: ?scaminmerge sets this._scaminMerged, which (a) sends scaminMerge=1 instead of scaminFilterGate=1 on the style request, and (b) early-returns the entire client SCAMIN injection path (_scaminUpdate / _scaminApplySettled / _scaminForceWhenReady and the straggler sweep), since the zoom-expression self-gates. Normal (non-merged) mode is byte-for-byte unchanged — this is a pure A/B toggle, default off. Verified (single-cell tile57 set, SCAMIN [6000..44999]): - layer count: default #sm buckets 65 *_scamin layers → merged 13 (0 '#sm' ids, all 13 carrying the zoom-expression clause); scales to the ~5009→~160 win on a 9-pack install. - SCAMIN still gates: at z13 the source carries scamin [17999,21999,29999,44999] but only 44999 renders (the three smaller-scamin features are gated off at the SAME tiles); more features appear as you zoom in, none at z11. - zero reloads on zoom: an easeTo z13→16.4→11.3 issues setFilter=0 _reloadSource=0 in merged mode vs setFilter=37 _reloadSource=1 in the filter-gate mode. - z14 settled render is pixel-identical between the two modes. Caveat: the merged gate snaps to integer OGC zoom (["zoom"] in a filter is evaluated at the tile's integer zoom), whereas the filter-gate injects the latitude/pxPitch physical display-scale denominator — so the two can differ by up to one ladder boundary right at a crossing (observed z13: merged shows 44999, filter-gate shows 29999+44999). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…explosion ?scaminmerge uses the engine's zoom_gate mode (empty manifest → one self-evaluating [>=,zoom,...] layer per kind instead of per-value #sm buckets). Collapses ~5009→~160 layers on a 9-pack install, and — the point — ZERO setFilter/reload on zoom (vs 37+1/crossing), killing the settle reload storm. A/B toggle; default (bucket) path byte-identical. Tradeoff: integer- zoom snap near ladder boundaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
?scaminmerge proved out, so merged (zoom-expression) mode is now the default: no param → the client requests scaminMerge=1 and the whole injection path stays disabled → zero setFilter/reload on zoom (verified). This is the fix for the 'tiles crawl in a second after you stop scrolling' lag. Opt back to the exact per-value filter-gate with ?scaminexact for comparison or exact-declutter needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feet got the same 1–2 decimals as metric, so a depth showed "12.3 ft". Feet read as whole numbers on charts — give ft one fewer decimal (10–100 ft → whole), keeping one decimal only under 10 ft for shoal detail. Metric/NM unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Working-state checkpoint of the in-flight migration: - tile57 is now a git submodule (github.com/beetlebugorg/tile57) built on demand; Makefile/go.mod/CLAUDE.md/Dockerfile/CI/docs updated to match. - Server bakes one archive PER PROVIDER (noaa/ienc/user); districts live under <provider>/ENC_ROOT/<district>/ (new provider.go, import_packs.go; import.go, tilesets.go, http.go, tile57_bake/style, cellindex/cells/prefs updated). A bodyless POST /api/import?set=<provider> re-bakes a provider from its cached ENC_ROOT. - Web client tracks the provider model (chart-library, chart-service, chartplotter, coverage-boxes, bands) + the cell-boundary debug overlay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provider-enc-root renamed packs from noaa-d<N> to bare providers (noaa/ienc/user), but _rebuildAllPerBand still matched the old /^noaa-d(\d+)$/ pattern, so it matched nothing and silently did "nothing to rebuild". Re-bake each installed provider via the server's cache-rebake (bodyless POST /api/import?set=<provider>) — the server re-bakes every district from its cached ENC_ROOT into the provider's one archive. Drops the now-unused client cell-list plumbing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…transparency) S-101 ColorFill emits "TOKEN,alpha" (TRFCF,0.75 for traffic-separation zones; CHGRF,0.5; NODTA,0.5), stored verbatim in color_token. The palette match looked up the whole "TRFCF,0.75" string, missed the bare "TRFCF" key, fell to the opaque magenta FALLBACK — and area fills carry no fill-opacity, so the alpha was dropped twice. Split the suffix: match the base token and fold alpha into an rgba fill-color (fill-opacity is not data-driven in MapLibre; fill-color is). Tokens without a comma are matched whole (no regression). Scoped to areasFillColor — line/text tokens carry no alpha. Verified by evaluating the expression: TRFCF,0.75 -> rgba(203,169,249,0.75). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ined map.getStyle() RETURNS undefined (doesn't throw) before the style loads, so the try/catch missed it and `style.layers` threw at init (`can't access property "layers", style is undefined`), aborting _renderInstalledSets. Add `if (!style) return null;`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er schema (Phase 2 W8) The engine now emits 6 merged source-layers (areas, area_patterns, lines, point_symbols, soundings, text — the `_scamin` twins and complex/sector lines folded in). The client's engine-mode render path needs NO functional change: it grafts the engine's chart layers verbatim (_engineStyleMerged) and its SCAMIN filter-gate controller (_scaminGatedLayers / setScaminDenom) + chartLayerIds select layers structurally (filter shape / `chart` source prefix), never by hardcoded source-layer name. debug-snapshot.mjs is likewise fully generic. This only refreshes chartLayerIds' illustrative comment, which still enumerated the retired v1 names (complex_lines / sector_lines / *_scamin). The v1 name literals that remain live in the JS style builder (chart-style.mjs / chart-sources.mjs — dropped in engine mode, kept for the Go backend) and the query/pick lists (search-box / dev-tools / chartplotter — already dual-schema safe: querying a missing source-layer is a no-op and chartplotter strips the `_scamin` suffix); both are intentionally left on v1 until the W9 library re-bake, so the user's not-yet-rebaked v1 tiles keep rendering. node --check clean; web unit tests green (42 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…style; drop the per-value bucket path The param-less /api/style.json used to populate the engine's SCAMIN manifest from the set metadata, which drove the per-value #sm bucket explosion (~835 layers). The client always sent ?scaminMerge=1 to avoid it, but a bare request (or any direct API consumer) got the 835-layer style. Now the merged band-independent style is the unconditional default: styleCtxForSet no longer populates ctx.scamin, so the engine emits its self-gating zoom-gate (one layer per render family, ~19 total). ?scaminexact still opts into the exact live filter-gate. The manifest is no longer needed for the style — it rides the TileJSON (served by the tile route) for the filter-gate client's ladder crossings. Removed the now-dead meta.Scamin population, the ?scaminMerge special-casing (its behavior is the default), the inert ?scamin override, and the unused queryBool helper. Verified end to end against an isolated live-set server: /api/style.json (no params) = 19 layers over the 6 source-layers with the zoom-gate clause; ?scamin=… also 19 (buckets are gone); ?scaminexact = 19 with the filter-gate clause. gofmt/vet clean; server package tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cross-compile comments The spec doc was removed in a specs/ cleanup; the CGO-only / `zig cc` cross-compile rationale it documented is now captured inline. Strip the two dangling pointers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gine The <chart-plotter widget> demo (web/demo.html) is server-less: no /api/style.json to adopt, so it fell back to the JS chart-style builder (buildChartLayers), which drifts from the tile57 engine's own portrayal (text offsets, etc.). Wire it to the SAME engine the server uses, run client-side from the vendored WASM style engine. - Vendor @beetlebug/tile57-style-engine (index.js + .d.ts + style-engine.wasm, the tile57/1 committed submodule pin) into web/vendor/tile57-style-engine/. make demo copies web/vendor into the demo bundle; the wasm is fetched relative to `assets`. - _initEngineStyle: where a server-less client early-returned, now load the WASM once (_loadClientEngine), generateStyle(<client mariner→MarinerSettings>), and fan the engine's chart layers across the per-band PMTiles sources (chart-<slug>) exactly like the server's mergeStyles — group-outer/band-inner (coarse→fine) with the coarse-band maxzoom caps, so the draw order and cross-band stacking mirror buildChartLayers. Flip _engineMode/_engineClient; _scaminMerged stays on (the engine layers self-gate SCAMIN on the web-mercator zoom, so no client filter-gate). - _engineStyleMerged: client branch keeps the JS builder's per-band chart SOURCES (live PMTiles URLs/zoom/encoding) and grafts the engine LAYERS. Server-backed mode is UNCHANGED. - Restyle (scheme/mariner) regenerates via WASM + setStyle(diff:true) instead of the server-only /api/style-diff. The JS builder (chart-style.mjs / buildChartLayers) stays in place as the fallback until the tile57/2 WASM lands. Verified against the tile57/1 demo: base portrayal (fills/coastline/contours/areas) matches pixel-close; the residual delta is SCAMIN-gated mark density (physical-scale vs web-mercator gate) + engine text-offset corrections, converging as you zoom in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ax gate Baked tiles are the only tile path (engine d78b28a): - serve loses --tile57 / registerTile57Set / the serve-tile57 Make target — charts enter through import/bake and are served from the baked archives; the dynamic-set mechanism itself stays (plugin tiles use it). orderedUpdates moves to tile57_bake.go (its remaining consumer). - Client: the band-handoff smax matcher half of setScaminDenom is retired — the engine style emits no smax clause (cross-band occlusion is baked geometry now); the denom injector handles scamin + oscl only. The debug gates snapshot reports scamin/oscl per layer accordingly. go vet + go test green; web suite 42/42. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… overscale margin The uniform 1:MIN_DETAIL_SCALE floor let the camera zoom into water whose finest covering band has no tiles at that depth — blank chart under an honest overscale HUD (MapLibre cannot stretch absent vector tiles; 404s resolve as empty, parent fallback needs a genuinely errored tile). _applyScaleFloor now also caps max zoom at BAND_MAXZOOM[finest covering band] + OVERSCALE_MARGIN (+FLOOR_GIVE), matching the bake's capped overscale fill-up (bake_enc.FILLUP_DZ = 2): the camera stops exactly where tiles stop. The finest covering band comes from the active-cells bbox index at the view centre; no active cells (widget/empty library) leaves the uniform floor alone — OVERSCALE_MARGIN finally gets its long-promised consumer. Web suite 42/42. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ile decoder MapLibre reads a vector source's `encoding` only at source CREATION; archives load after the initial (mvt-default) style is built, so an MLT archive (the tile57 default bake format) could never switch a live source's decoder in place — the stale mvt decoder parsed MLT bytes as MVT and MapLibre threw "Unable to parse the tile". Track the encoding each style build commits per source and rebuild() (recreate sources) instead of refresh() when a loaded archive disagrees; zoom ranges keep updating in place.
… guard The encoding comparison in _updateSourceZoom ran after the !src/maxzoom-undefined continue, so an archive loading while the style was still materializing (no live source yet) never flagged the rebuild — the stale decoder survived until an unrelated restyle. The check compares the archive against our own _srcEncoding bookkeeping, not the live source, so it now runs first; the in-place zoom sync keeps the guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the per-band widget fan (P5)
The coverage-clipped composite resolves best-available INSIDE the archive, so
the per-band archive split (and the client-side six-source re-composition it
required) is obsolete:
- bake: `-o *.pmtiles|*.mbtiles` writes ONE flat merged archive (+ --manifest
with a single band-less entry + aux.zip); any other -o stays a bundle
directory. --bands and --overzoom are retired (MinZoom 0 — the coarsest
populated band extends down to the world view, which covered --overzoom).
- Makefile: districts bake to noaa-d<NN>.pmtiles (no stamps), serve-widget
links/manifests single archives, demo/chart1 drop --bands.
- widget client: a band-less manifest entry loads into the single `all`
source only (legacy banded entries keep the old fan). The merged archive is
a SPARSE pyramid (each band bakes to its native max + FILLUP_DZ), which a
single source can't describe with one maxzoom, so:
· the pmtiles protocol now ERRORS an absent tile when any ancestor holds
data (directory-only hasTile probes on PMTilesArchive/MultiArchive) —
MapLibre retains/loads the parent and renders it stretched one level
(a delivered-empty tile would paint permanent blank);
· the camera stops at the data depth: _applyScaleFloor probes
dataMaxZoomAt (deepest directory entry at the view centre) when there is
no active-cell index, capping max zoom at depth + 1 stretch level —
the widget twin of the server-mode band cap.
Verified on the Annapolis demo (one 13.6MB demo.pmtiles): crisp full detail
at z13 Annapolis; coastal-only water renders stretched at +1 past its
fill-up depth and the camera clamps there (z15 request → z14.15) instead of
blank water/hatch; suite 42/42, go vet/test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oach/harbor charts The Severn/Magothy, Bay Bridge, Kent Island, and South/West/Rhode river cells join the curated set so the demo zooms from the whole bay down to the docks across the immediate area. The Makefile's DEMO_CELLS default (which overrides the script's when driven by `make demo`) is synced to the same list.
Depth/height readouts (the shared units.format and the pick-report number formatter) picked decimals purely by magnitude, so feet could show a decimal (e.g. "8.3 ft", and the pick report showed "12.3 ft"). Charts read feet as whole numbers; metric carries the tenths digit feet drops. Now: unit "ft" formats to 0 decimals always; unit "m" to at most 1. Other units (nm/km/kn/…) keep the compact by-magnitude decimals unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bake -o *.pmtiles used the in-memory BakePmtiles path: every cell of a band parsed+portrayed at once, no coarse riders, no super-tile locality, no TILE57_SUPER_DZ/TILE57_LRU_BUDGET tuning, and the whole archive buffered in memory before writing. District-scale flat bakes (the Makefile's noaa-d<NN>.pmtiles) got the worst of both worlds. runTile57Archive now drives BakeBundle (the streaming bakeRoot: geometry LRU, coarse riders, capped fill-up, tiles streamed to disk) into a temp dir beside -o and renames the finished tiles/chart.pmtiles onto -o, discarding the bundle scaffolding. minzoom 0 — the coarsest populated band extends to the world view, same as before. aux.zip now collects from the input tree without reading cells into memory (collectAuxDir); zip inputs stage their aux content beside the cells so the walk finds it (tile57Input previously dropped it). The in-memory bbox fold + per-cell meta pass are gone with the old path. Verified: 3-cell root → one 5.4MB flat pmtiles (zoom 0..16, riders in the band log, coastal extend_min to z0), band-less manifest entry with real bounds, coverage-edge view renders both sides. go build/vet green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…up depth The server-mode camera cap allows BAND_MAXZOOM + margin; tiles only exist to band max + TILE57_FILLUP_DZ (default 1 now), so the constant follows — a margin past the baked depth would re-open a blank window. The widget path probes real archive depth and self-adjusts regardless. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng nothing
tile57Input handed any lone directory straight to the engine, but libtile57's
baker only reads extracted .000 cells — a directory of NOAA exchange-set .zip
bundles (the `make demo` cache, an IENC download) has none, so the bake covered
nothing ("tile57: bundle bake covered nothing"). Pass a lone directory through
as-is only when it ALREADY holds a .000 (a real ENC_ROOT); otherwise fall to
collectCells, which walks + unpacks the zips into the temp cell dir. Restores
`make demo`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ENC feature attachments (TXTDSC text / PICREP pictures) shipped in a companion .aux.zip that the client unzipped (hosted) or the server extracted per-file via /api/aux (dynamic). Replace both with LOOSE static files: the baker writes a companion aux/ dir (files + index.json) via one shared helper (auxfiles.WriteDir), used identically by the CLI bake and the C-ABI server bake, so output matches. - Server: GET /aux/index.json (manifest) + GET /aux/<stored> served as plain files from the aux/ dir; /api/aux removed. Legacy .aux.zip companions are still indexed + served (back-compat), so districts baked before the loose layout keep resolving until re-baked. - Client: AuxStore.load(manifestUrl) is the single path — static fetch of index.json + per-file GETs, identical online (server /aux) or offline (a bundle's aux/index.json on any static host / file://). loadApi gone; a legacy .aux.zip URL is still auto-detected and unzipped. Works offline with no zip and no server. Verified end-to-end: CLI bake emits the aux dir + manifest; server serves /aux; browser resolves text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworked the cursor-pick panel for readability and stable layout: - Two-column table: dim KEY, dark VALUE, 1/4 : 3/4 split, left-aligned, generous row rhythm; genuinely block content (TXTDSC/PICREP) stacks full-width. Body scrolls (card capped to the viewport) instead of the whole card running off-screen. - Dropped duplicate labels: the acronym only shows when it differs from the resolved name (empty catalogue printed "CATLMK CATLMK"). - Cycle controls (◀ n/n ▶) moved into the fixed header and the source cell stacked under the class name, so neither jumps as you step across features whose subtitle/meta differ. - ←/→ step through the picked stack (mirroring the header arrows), ignored in text fields and only while a multi-feature pick is open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… stale) The server-less widget/demo runs the tile57 chartstyle engine CLIENT-side via the vendored WASM (web/vendor/tile57-style-engine) — the same engine the server exposes at /api/style.json, just without a backend. But `make demo` only copied web/vendor verbatim, so a committed .wasm that lagged the engine shipped a STALE style: its *_scamin layers were emitted ungated, so SCAMIN scale-gating silently vanished and every buoy / light / sector arc drew at every zoom (the whole chart piled into an overview blob). The engine already gates correctly (empty manifest → the self-gating zoom filter, style.zig zoom_gate); only the shipped binary was old. Make the demo own it: - New `vendor-style-engine` target: `zig build wasm` in $(TILE57) → re-vendor style-engine.wasm + index.js/.d.ts into web/vendor/tile57-style-engine. - `demo` depends on it, so the bundle's WASM always tracks the current engine. - Re-vendored the current .wasm (was stale → dropped SCAMIN gating). Verified: with the fresh WASM the demo's overview is clean (0 point symbols rendered at z6, 119 scamin-gated layers) vs. thousands ungated before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pinned engine was 42 commits behind main. Bump it to the current main tip so the default (no-go.work) build tracks the live engine — incl. the cover-clip FULL/EMPTY/SEAM pre-pass. .gitmodules already tracks main, so `git submodule update --remote` keeps it moving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # web/src/chart-canvas/chart-canvas.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://github.com/beetlebugorg/tile57
This replaces the Go tile engine with a more robust Zig engine accessed via CGO.