From 940191b72ce2154b0d4b77279060efc41be3beac Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 06:06:21 -0400 Subject: [PATCH 01/15] fix(bake): best-available for cells without M_COVR; scale-boundary width 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S-57 best-available suppression keyed entirely on M_COVR(CATCOV=1) coverage, so cells lacking M_COVR (e.g. the S-52 PresLib "ECDIS Chart 1" test cells) got no suppression — a coarser cell's symbols double-drew over a finer cell covering the same ground (the "smashed together" symbols). extractCoverage now derives a coverage rectangle from a cell's data extent when it has no M_COVR; the streaming coverage pass re-parses such a cell fully so the fallback has its geometry. Derived rectangles are flagged covMeta.derived and gate POINT suppression only (where a finer footprint supersedes a coarse symbol), never area/line FILL suppression — a derived extent marks where a cell IS, not where it has data, so a sparse cell wouldn't punch nodata holes. Real NOAA cells always carry M_COVR ⇒ no behaviour change for them. Also set the chart scale-boundary stroke to width 1 per S-52 §10.1.9.1 LS(SOLD,1,CHGRD) (was 2). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/bake/bake.go | 90 ++++++++++++-- internal/engine/bake/crossband_line_test.go | 2 +- internal/engine/bake/preslib_chart1_test.go | 130 ++++++++++++++++++++ internal/engine/baker/baker.go | 14 ++- 4 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 internal/engine/bake/preslib_chart1_test.go diff --git a/internal/engine/bake/bake.go b/internal/engine/bake/bake.go index 89a8b88..cb8437a 100644 --- a/internal/engine/bake/bake.go +++ b/internal/engine/bake/bake.go @@ -397,6 +397,14 @@ type covMeta struct { displayMin uint32 // lowest zoom this cell's data is shown at (0 for overview/general which overzoom down) bb geo.BoundingBox rings [][][]float64 + // derived is true when these rings were synthesised from the cell's geometry + // extent because it carried no M_COVR (see extractCoverage). A derived rectangle + // marks where the cell IS, not where it actually has area data — so it is trusted + // only for POINT suppression (a finer cell's footprint supersedes a coarser cell's + // point symbol), NOT for area/line FILL suppression, where it would punch nodata + // holes wherever the finer cell's extent has no fill (a sparse legend cell, an + // inter-cell gap). Always false for conformant cells, where M_COVR == data extent. + derived bool } // sectorKey identifies one constructed sector-figure element (anchor + ray/arc @@ -485,7 +493,8 @@ func (b *Baker) groupCoLocatedLights(features []s57.Feature) (primaryText map[in // extractCoverage records a cell's M_COVR (CATCOV=1) data-coverage polygons into // covMeta (keyed to the cell's native band [zr]) — the input to best-available // suppression and DATCVR scale boundaries. -func (b *Baker) extractCoverage(features []s57.Feature, zr ZoomRange, cell string, cscl, displayMin uint32) { +func (b *Baker) extractCoverage(features []s57.Feature, zr ZoomRange, cell string, cscl, displayMin uint32) int { + added := 0 for i := range features { f := &features[i] if f.ObjectClass() != "M_COVR" || intAttr(f.Attributes(), "CATCOV") != 1 { @@ -507,7 +516,59 @@ func (b *Baker) extractCoverage(features []s57.Feature, zr ZoomRange, cell strin b.coverage = append(b.coverage, cov) b.covMeta = append(b.covMeta, cm) b.bbox.ExtendBox(cm.bb) // so the streaming bake has full bounds after pass 1 + added++ + } + // Fallback for a cell with no M_COVR(CATCOV=1) coverage (S-57 requires it, but + // synthetic/test cells — e.g. the S-52 PresLib ECDIS Chart 1 — omit it): derive a + // rectangular coverage from the bounding box of all the cell's geometry. Without + // this the cell contributes NO covMeta, so best-available suppression and DATCVR + // scale boundaries have nothing to test against and a coarser cell's symbols + // double-draw over a finer cell covering the same ground. Real NOAA cells always + // carry M_COVR, so this never triggers for them (no behaviour change). + if added == 0 { + if rect, bb, ok := cellExtentRect(features); ok { + b.coverage = append(b.coverage, CellCoverage{Cell: cell, Rings: [][][]float64{rect}}) + b.covMeta = append(b.covMeta, covMeta{bandMin: zr.Min, bandMax: zr.Max, cscl: cscl, displayMin: displayMin, bb: bb, rings: [][][]float64{rect}, derived: true}) + b.bbox.ExtendBox(bb) + added++ + } + } + return added +} + +// cellExtentRect returns a closed rectangular ring ([lon,lat] points, CW from the +// SW corner) spanning the bounding box of every feature's geometry in a cell, plus +// that box. Used as a coverage fallback for cells lacking M_COVR (see +// extractCoverage). ok is false when the cell has no spatial geometry at all. +func cellExtentRect(features []s57.Feature) ([][]float64, geo.BoundingBox, bool) { + bb := geo.EmptyBox() + any := false + ext := func(pt []float64) { + if len(pt) >= 2 { + bb.ExtendPoint(geo.LatLon{Lon: pt[0], Lat: pt[1]}) + any = true + } + } + for i := range features { + g := features[i].Geometry() + for _, pt := range g.Coordinates { + ext(pt) + } + for _, r := range g.Rings { + for _, pt := range r.Coordinates { + ext(pt) + } + } + } + if !any || bb.MinLon > bb.MaxLon || bb.MinLat > bb.MaxLat { + return nil, bb, false } + rect := [][]float64{ + {bb.MinLon, bb.MinLat}, {bb.MaxLon, bb.MinLat}, + {bb.MaxLon, bb.MaxLat}, {bb.MinLon, bb.MaxLat}, + {bb.MinLon, bb.MinLat}, + } + return rect, bb, true } // cellStem is a cell's dataset name without the .000/.NNN extension. @@ -521,12 +582,14 @@ func cellStem(name string) string { // AddCellCoverage extracts ONLY a cell's coverage + native band (no feature // routing) — the streaming bake's first pass, building the global covMeta once so // each later per-band routing pass can suppress against finer bands without -// re-deriving coverage. Returns the cell's native band. -func (b *Baker) AddCellCoverage(chart *s57.Chart) Band { +// re-deriving coverage. Returns the cell's native band and how many coverage +// polygons it contributed (0 ⇒ the cell had no M_COVR and the extent fallback found +// no geometry — e.g. an M_COVR-only filtered parse; the caller re-parses fully). +func (b *Baker) AddCellCoverage(chart *s57.Chart) (Band, int) { band := BandForScale(uint32(chart.CompilationScale())) cscl := uint32(chart.CompilationScale()) - b.extractCoverage(chart.Features(), band.ZoomRange(), cellStem(chart.DatasetName()), cscl, cellDisplayMin(band, band.ZoomRange())) - return band + n := b.extractCoverage(chart.Features(), band.ZoomRange(), cellStem(chart.DatasetName()), cscl, cellDisplayMin(band, band.ZoomRange())) + return band, n } // cellDisplayMin is the lowest zoom a band's cells are actually drawn at (matches @@ -1251,7 +1314,7 @@ func (b *Baker) addScaleBoundary(pts []geo.LatLon, zMin, zMax uint32) { attrs: []mvt.KeyValue{ {Key: "class", Value: mvt.StringVal("SCLBDY")}, {Key: "color_token", Value: mvt.StringVal("CHGRD")}, - {Key: "width_px", Value: mvt.IntVal(2)}, + {Key: "width_px", Value: mvt.IntVal(1)}, // S-52 §10.1.9.1 LS(SOLD,1,CHGRD) }, } b.add(r, bb) @@ -1643,7 +1706,7 @@ func (b *Baker) emitTileInto(coord tile.TileCoord, extent uint32, buffer float64 case r.kind == mvt.GeomPoint: // A point tests its OWN position — a boundary tile keeps coarse points // that fall outside the finer coverage. - if s := b.coverageScaleAt(unnormY(r.wMinY), r.wMinX*360-180, bandZ); s != 0 && s < r.cscl { + if s := b.coverageScaleAt(unnormY(r.wMinY), r.wMinX*360-180, bandZ, true); s != 0 && s < r.cscl { suppressed = true } case r.kind == mvt.GeomLineString: @@ -1658,7 +1721,7 @@ func (b *Baker) emitTileInto(coord tile.TileCoord, extent uint32, buffer float64 // coarse line by the tile CENTRE (a line spans the tile, so the centre is // its representative point): it yields only where the centre has no finer // cell — best-available where the finer cell genuinely carries no data. - if s := b.coverageScaleAt(ctrLat, ctrLon, bandZ); s != 0 && s < r.cscl { + if s := b.coverageScaleAt(ctrLat, ctrLon, bandZ, false); s != 0 && s < r.cscl { suppressed = true } default: @@ -1676,7 +1739,7 @@ func (b *Baker) emitTileInto(coord tile.TileCoord, extent uint32, buffer float64 nLat, sLat := unnormY(float64(coord.Y)/n), unnormY(float64(coord.Y+1)/n) suppressed = true for _, pt := range [...][2]float64{{ctrLat, ctrLon}, {nLat, wLon}, {nLat, eLon}, {sLat, wLon}, {sLat, eLon}} { - if s := b.coverageScaleAt(pt[0], pt[1], bandZ); s == 0 || s >= r.cscl { + if s := b.coverageScaleAt(pt[0], pt[1], bandZ, false); s == 0 || s >= r.cscl { suppressed = false // part of the tile has no finer cell — keep the coarse prim break } @@ -2008,8 +2071,8 @@ func (b *Baker) coverageBandAt(lat, lon float64) uint32 { p := geo.LatLon{Lat: lat, Lon: lon} for i := range b.covMeta { cm := &b.covMeta[i] - if cm.bandMax <= best || !cm.bb.Contains(p) { - continue + if cm.derived || cm.bandMax <= best || !cm.bb.Contains(p) { + continue // derived extents gate point suppression only, not area/line fills (see covMeta.derived) } if pointInRings(lon, lat, cm.rings) { best = cm.bandMax @@ -2025,7 +2088,7 @@ func (b *Baker) coverageBandAt(lat, lon float64) uint32 { // across bands AND between cells of different scale that fall in the SAME band (the // per-band coverageBandAt above can't distinguish those). bandZ-gated so a finer // cell that isn't shown yet at this zoom doesn't punch a hole in the coarser one. -func (b *Baker) coverageScaleAt(lat, lon float64, bandZ uint32) uint32 { +func (b *Baker) coverageScaleAt(lat, lon float64, bandZ uint32, pointQuery bool) uint32 { var best uint32 // 0 = none found yet; otherwise the finest (smallest) cscl p := geo.LatLon{Lat: lat, Lon: lon} for i := range b.covMeta { @@ -2033,6 +2096,9 @@ func (b *Baker) coverageScaleAt(lat, lon float64, bandZ uint32) uint32 { if cm.cscl == 0 || cm.displayMin > bandZ { continue // unscaled, or this cell isn't drawn at this zoom } + if cm.derived && !pointQuery { + continue // a derived extent rectangle suppresses points, not fills (see covMeta.derived) + } if best != 0 && cm.cscl >= best { continue // not finer than the best so far — skip the costly point test } diff --git a/internal/engine/bake/crossband_line_test.go b/internal/engine/bake/crossband_line_test.go index 5118347..7fb73b0 100644 --- a/internal/engine/bake/crossband_line_test.go +++ b/internal/engine/bake/crossband_line_test.go @@ -82,7 +82,7 @@ func TestCrossBandLineNoDoubleDraw(t *testing.T) { ctrLat := unnormY((float64(c.Y) + 0.5) / n) // A strictly-finer cell covers this tile's centre, yet a coarse line was // kept here AND the finer band also drew one: an interior double-draw. - if s := b.coverageScaleAt(ctrLat, ctrLon, z); s != 0 && s < approachCscl { + if s := b.coverageScaleAt(ctrLat, ctrLon, z, false); s != 0 && s < approachCscl { interior++ t.Errorf("interior line double-draw at %v: tile centre covered by finer cell (cscl %d < approach %d)", c, s, approachCscl) } diff --git a/internal/engine/bake/preslib_chart1_test.go b/internal/engine/bake/preslib_chart1_test.go new file mode 100644 index 0000000..eb5fcba --- /dev/null +++ b/internal/engine/bake/preslib_chart1_test.go @@ -0,0 +1,130 @@ +package bake + +import ( + "archive/zip" + "strings" + "testing" + + "github.com/beetlebugorg/chartplotter/pkg/geo" + "github.com/beetlebugorg/chartplotter/pkg/s57" +) + +// preslibZip is the IHO S-52 PresLib e4.0.0 digital-files download (untracked; +// see scripts/preslib-chart1.sh). Its "ECDIS Chart 1" is a fully symbol-exercising +// dataset of 14 cells, one overview (1:60 000) + 13 harbor pages (1:14 000), but — +// unlike conformant NOAA data — the cells carry NO M_COVR data-coverage features. +const preslibZip = "../../../testdata/S-52_PresLib_e4.0.0_Digital_Files_Draft.zip" + +// loadPresLibChart1 parses every ECDIS-Chart-1 .000 cell straight out of the zip, +// or skips the test if the (untracked) download is absent. +func loadPresLibChart1(t *testing.T) []*s57.Chart { + t.Helper() + zr, err := zip.OpenReader(preslibZip) + if err != nil { + t.Skipf("PresLib zip not present (%v); see scripts/preslib-chart1.sh", err) + } + t.Cleanup(func() { zr.Close() }) + var charts []*s57.Chart + for _, f := range zr.File { + if !strings.Contains(f.Name, "ECDIS_Chart_1/") || !strings.HasSuffix(f.Name, ".000") { + continue + } + chart, err := s57.ParseFS(zr, f.Name) + if err != nil { + t.Fatalf("parse %s: %v", f.Name, err) + } + charts = append(charts, chart) + } + if len(charts) == 0 { + t.Fatal("no ECDIS_Chart_1 .000 cells found in zip") + } + return charts +} + +// TestPresLibChart1DerivedCoverage guards the cross-band best-available fix for +// cells lacking M_COVR. The S-52 PresLib ECDIS Chart 1 stacks a 1:60 000 overview +// over thirteen 1:14 000 harbor pages covering the same ground, but none of the +// cells carry an M_COVR coverage polygon — so the M_COVR-driven suppression had +// nothing to test and every overview feature double-drew over the harbor pages +// (the "smashed together" symbols). extractCoverage now derives a coverage +// rectangle from each cell's data extent when M_COVR is absent, restoring +// per-cell (block) suppression. The invariant: a point inside a harbor page must +// report a FINER (smaller) covering scale than the overview, so an overview +// symbol there is suppressed. +func TestPresLibChart1DerivedCoverage(t *testing.T) { + charts := loadPresLibChart1(t) + + b := New() + var overviewCscl, harborCscl uint32 + for _, chart := range charts { + // Sanity: these cells really have no M_COVR — that's why the fix is needed. + for i := range chart.Features() { + if chart.Features()[i].ObjectClass() == "M_COVR" { + t.Fatalf("cell %s unexpectedly has M_COVR — fixture changed", chart.DatasetName()) + } + } + band, n := b.AddCellCoverage(chart) + if n == 0 { + t.Errorf("cell %s contributed no coverage (derived-extent fallback failed)", chart.DatasetName()) + } + switch cscl := uint32(chart.CompilationScale()); band { + case BandApproach: + overviewCscl = cscl + case BandHarbor: + harborCscl = cscl + } + } + if overviewCscl == 0 || harborCscl == 0 { + t.Fatalf("expected both an approach overview and harbor pages (overview=%d harbor=%d)", overviewCscl, harborCscl) + } + if harborCscl >= overviewCscl { + t.Fatalf("harbor scale %d should be finer (smaller) than overview %d", harborCscl, overviewCscl) + } + + // A point in the middle of harbor page AA5C1CDE (top row, third column). At a + // harbor display zoom the finest covering cell there must be the harbor page, + // not the overview — i.e. an overview prim at this point gets suppressed. + const harborZ = 13 // BandHarbor display min + if got := b.coverageScaleAt(15.11405, -5.05035, harborZ, true); got != harborCscl { + t.Errorf("coverageScaleAt inside harbor page = %d, want harbor cscl %d "+ + "(no finer cover ⇒ overview would double-draw)", got, harborCscl) + } + + // Every cell's derived coverage must lie within the overview's footprint + // (they tile the same ground), confirming the rectangles are sane. + ov := geo.LatLon{Lat: 15.0668, Lon: -5.0669} // overview centre + if !b.coverageBandAtOK(ov) { + t.Error("overview centre not covered by any derived coverage polygon") + } +} + +// coverageBandAtOK reports whether any coverage polygon contains p (test helper). +func (b *Baker) coverageBandAtOK(p geo.LatLon) bool { + for i := range b.covMeta { + cm := &b.covMeta[i] + if cm.bb.Contains(p) && pointInRings(p.Lon, p.Lat, cm.rings) { + return true + } + } + return false +} + +// TestPresLibChart1NoMCovrIsExtentFallback verifies the lower-level contract: a +// cell with no M_COVR yields exactly one DERIVED coverage rectangle spanning its +// geometry, and a (hypothetical) conformant cell would not. +func TestPresLibChart1NoMCovrIsExtentFallback(t *testing.T) { + charts := loadPresLibChart1(t) + b := New() + for _, chart := range charts { + before := len(b.covMeta) + b.AddCellCoverage(chart) + added := b.covMeta[before:] + if len(added) != 1 { + t.Errorf("cell %s: expected 1 derived coverage rect, got %d", chart.DatasetName(), len(added)) + continue + } + if !added[0].derived { + t.Errorf("cell %s: coverage not flagged derived", chart.DatasetName()) + } + } +} diff --git a/internal/engine/baker/baker.go b/internal/engine/baker/baker.go index dda44f3..c201e86 100644 --- a/internal/engine/baker/baker.go +++ b/internal/engine/baker/baker.go @@ -419,7 +419,19 @@ func BakeToPMTilesBandsStreaming(cells map[string]CellData, maxZoom uint32, onSk }, func(string, *s57.Chart) struct{} { return struct{}{} }, func(name string, chart *s57.Chart, _ struct{}) { - band := b.AddCellCoverage(chart) + band, n := b.AddCellCoverage(chart) + if n == 0 { + // The M_COVR-only coverage parse found no data-coverage polygon (the + // cell omits M_COVR — non-conformant, e.g. the S-52 PresLib test cells). + // Re-parse it fully so AddCellCoverage's bounding-box fallback has the + // cell's geometry to derive a coverage rectangle from; otherwise the cell + // contributes nothing to covMeta and a coarser band's symbols double-draw + // over it. Rare (real ENCs always carry M_COVR), so the extra parse is fine. + cd := cells[name] + if full, err := ParseCellWithUpdates(name, cd.Base, cd.Updates); err == nil { + band, _ = b.AddCellCoverage(full) + } + } byBand[band.ZoomRange().Max] = append(byBand[band.ZoomRange().Max], name) parsed++ if progress != nil { From ab9b63a8060aa2608df8a7b5e3981cd693148c65 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 06:06:37 -0400 Subject: [PATCH 02/15] feat(portrayal): honor SYMINS for NEWOBJ; SWPARE fallback; true symbol size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEWOBJ aliases to the point-only S-101 VirtualAISAidToNavigation rule, so its line/area variants errored (drew nothing) and every NEWOBJ point stamped a V-AIS mark — ignoring the producer's SYMINS instruction (e.g. SYMINS="SY(INFORM01)"). 381 of 386 ECDIS-Chart-1 NEWOBJ carry SYMINS (164 TX labels, 91 LS, 80 SY, 27 AC, 17 LC, 2 AP) — that string IS the portrayal. Resurrect SYMINS02 (was deleted with pkg/s52): parseSYMINS parses the ';'-separated SY/TX/TE/LS/LC/AC/AP ops into SymbolCall/DrawText/StrokeLine/ LinePattern/FillPolygon/PatternFill (reusing formatSubstitute for TE), hooked at the top of buildFeature so a NEWOBJ-with-SYMINS overrides the V-AIS stream. NEWOBJ without SYMINS falls back to a dashed magenta new-object boundary; SWPARE (no SweptArea.lua in the catalogue) falls back to a dashed boundary + "swept to " label. Fix symbol physical size: DefaultPxPerSymbolUnit divided by 0.35278mm (the 1/72" point) while the app measures the screen at 0.26458mm (1/96" CSS px), rendering every symbol ~25% too small. Use 0.26458 so symbols hit their encoded size — the S-52 size-check SY(CHKSYM01) box now measures ~5mm. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/portrayal/build.go | 86 ++++++ internal/engine/portrayal/primitive.go | 11 +- internal/engine/portrayal/s101build.go | 22 ++ internal/engine/portrayal/symins.go | 331 +++++++++++++++++++++++ internal/engine/portrayal/symins_test.go | 98 +++++++ 5 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 internal/engine/portrayal/symins.go create mode 100644 internal/engine/portrayal/symins_test.go diff --git a/internal/engine/portrayal/build.go b/internal/engine/portrayal/build.go index 715f91b..80d7f4f 100644 --- a/internal/engine/portrayal/build.go +++ b/internal/engine/portrayal/build.go @@ -657,6 +657,92 @@ func unknownObjectBuild(f *s57.Feature) FeatureBuild { } } +// newObjectBuild portrays an S-57 NEWOBJ whose primitive the S-101 alias rule +// (VirtualAISAidToNavigation) rejects: that rule is POINT-only, so a line or area +// NEWOBJ errors and would otherwise be suppressed (drawing nothing). The S-52 +// PresLib reference (§10.3.3.8, "Default symbol for NEWOBJ") draws line/area new +// objects with a dashed magenta boundary, so emit that. Point NEWOBJ never reaches +// here — it portrays through the V-AIS rule, which is correct for real S-101 data +// (V-AIS is encoded as a point NEWOBJ). +func newObjectBuild(f *s57.Feature) FeatureBuild { + g := f.Geometry() + toLL := func(cs [][]float64) []geo.LatLon { + out := make([]geo.LatLon, 0, len(cs)) + for _, c := range cs { + if len(c) >= 2 { + out = append(out, geo.LatLon{Lat: c[1], Lon: c[0]}) + } + } + return out + } + dashed := func(pts []geo.LatLon, closed bool) Primitive { + if closed && len(pts) > 1 && pts[0] != pts[len(pts)-1] { + pts = append(pts, pts[0]) // close the ring + } + return StrokeLine{Points: pts, ColorToken: "CHMGF", WidthPx: 1.5, Dash: DashDashed} + } + var prims []Primitive + switch g.Type { + case s57.GeometryTypeLineString: + if pts := toLL(g.Coordinates); len(pts) >= 2 { + prims = append(prims, dashed(pts, false)) + } + case s57.GeometryTypePolygon: + for _, r := range g.Rings { + if pts := toLL(r.Coordinates); len(pts) >= 2 { + prims = append(prims, dashed(pts, true)) + } + } + } + if len(prims) == 0 { + return FeatureBuild{DisplayCategory: displayStandard} + } + return FeatureBuild{Primitives: prims, DisplayPriority: 6, DisplayCategory: displayStandard} +} + +// sweptAreaBuild portrays an S-57 SWPARE (swept area). Its S-101 class is +// SweptArea, but the Portrayal Catalogue ships no SweptArea.lua rule (an IHO gap), +// so it errors and would be suppressed. The S-52 PresLib reference (page 243) +// draws a dashed boundary around the area plus a "swept to " depth label, +// so emit that. +func sweptAreaBuild(f *s57.Feature) FeatureBuild { + g := f.Geometry() + if g.Type != s57.GeometryTypePolygon { + return FeatureBuild{DisplayCategory: displayStandard} + } + ringLL := func(cs [][]float64) []geo.LatLon { + out := make([]geo.LatLon, 0, len(cs)) + for _, c := range cs { + if len(c) >= 2 { + out = append(out, geo.LatLon{Lat: c[1], Lon: c[0]}) + } + } + if len(out) > 1 && out[0] != out[len(out)-1] { + out = append(out, out[0]) // close the ring + } + return out + } + var prims []Primitive + for _, r := range g.Rings { + if pts := ringLL(r.Coordinates); len(pts) >= 2 { + prims = append(prims, StrokeLine{Points: pts, ColorToken: "CHGRD", WidthPx: 1, Dash: DashDashed}) + } + } + if len(prims) == 0 { + return FeatureBuild{DisplayCategory: displayStandard} + } + // "swept to " depth label at the area's representative point. + if d, ok := floatAttr(f.Attributes(), "DRVAL1"); ok { + if a, ok := areaSurfacePoint(ringLL(exteriorRing(g))); ok { + prims = append(prims, DrawText{ + Anchor: a, Text: "swept to " + strconv.FormatFloat(d, 'f', -1, 64), + FontSizePx: 11, ColorToken: "CHBLK", HAlign: HAlignCenter, VAlign: VAlignMiddle, + }) + } + } + return FeatureBuild{Primitives: prims, DisplayPriority: 6, DisplayCategory: displayStandard} +} + // representativePoint returns a single lat/lon to anchor a point symbol on a // feature of any geometry: the point itself, a line's midpoint vertex, or an // area's exterior-ring centroid. ok is false when the geometry carries no usable diff --git a/internal/engine/portrayal/primitive.go b/internal/engine/portrayal/primitive.go index f66d184..881042f 100644 --- a/internal/engine/portrayal/primitive.go +++ b/internal/engine/portrayal/primitive.go @@ -12,9 +12,14 @@ import "github.com/beetlebugorg/chartplotter/pkg/geo" // DefaultPxPerSymbolUnit is screen px per 0.01-mm PresLib symbol unit at 100% // zoom — the nominal S-52 feature scale shared by the symbol/linestyle renderers -// and the tile engine's LC/AP/sector sizing. 0.01 / 0.35278 mm-per-pt renders -// every glyph at its encoded physical size. -const DefaultPxPerSymbolUnit float32 = 0.01 / 0.35278 +// and the tile engine's LC/AP/sector sizing. It MUST use the same reference pixel +// pitch the rest of the app measures the screen with (web util.mjs +// DEFAULT_PX_PITCH_MM = 0.26458 mm, the 1/96-inch CSS reference pixel) so a symbol +// renders at its encoded physical size: the S-52 size-check symbol SY(CHKSYM01), +// a 5 mm box, then measures 5 mm (500 units × this = 18.9 px × 0.26458 mm = 5 mm). +// (Previously 0.35278 — the 1/72-inch point — which rendered every symbol ~25% too +// small against the app's 0.26458 mm pixel.) +const DefaultPxPerSymbolUnit float32 = 0.01 / 0.26458 // Dash is a simple line-stroke dash style (LS instruction). type Dash uint8 diff --git a/internal/engine/portrayal/s101build.go b/internal/engine/portrayal/s101build.go index 3d0d293..18f172f 100644 --- a/internal/engine/portrayal/s101build.go +++ b/internal/engine/portrayal/s101build.go @@ -251,6 +251,15 @@ func (b *S101Builder) Build(f *s57.Feature) (FeatureBuild, bool) { // buildFeature turns one feature's emitted instruction stream into its FeatureBuild. func (b *S101Builder) buildFeature(f *s57.Feature, stream string) FeatureBuild { + // NEWOBJ with a SYMINS attribute: portray the producer's explicit symbol + // instruction (S-52 SYMINS02) rather than the S-101 V-AIS alias the engine + // emitted — SYMINS carries the real symbols, TX/TE labels, boundaries and fills + // (the bulk of the ECDIS-Chart-1 test content). See parseSYMINS. + if f.ObjectClass() == "NEWOBJ" { + if fb, ok := parseSYMINS(f); ok { + return fb + } + } // Genuinely-unknown object class (no S-101 alias) → the magenta "unknown // object" mark (S-52 §10.1.1 parity). if strings.HasPrefix(stream, "UNMAPPED:") { @@ -260,6 +269,19 @@ func (b *S101Builder) buildFeature(f *s57.Feature, stream string) FeatureBuild { // chart with placeholders. (Most current errors are line/area rules needing // the S-57 spatial topology the host doesn't model yet — a tracked gap.) if stream == "" || strings.HasPrefix(stream, "ERROR:") { + // NEWOBJ aliases to the POINT-only VirtualAISAidToNavigation rule, so its + // line/area variants always error here; draw the S-52 dashed magenta new-object + // boundary instead of dropping them (the missing boxes/lines around things). + switch f.ObjectClass() { + case "NEWOBJ": + if nb := newObjectBuild(f); len(nb.Primitives) > 0 { + return nb + } + case "SWPARE": + if sb := sweptAreaBuild(f); len(sb.Primitives) > 0 { + return sb + } + } return FeatureBuild{DisplayCategory: displayStandard} } diff --git a/internal/engine/portrayal/symins.go b/internal/engine/portrayal/symins.go new file mode 100644 index 0000000..766a936 --- /dev/null +++ b/internal/engine/portrayal/symins.go @@ -0,0 +1,331 @@ +package portrayal + +import ( + "fmt" + "strconv" + "strings" + + "github.com/beetlebugorg/chartplotter/pkg/geo" + "github.com/beetlebugorg/chartplotter/pkg/s57" +) + +// SYMINS02 (S-52 PresLib §13.2.18 / §10.3.3.8) — portray an S-57 NEWOBJ from its +// SYMINS attribute, the producer's explicit "symbol instruction" string. SYMINS is +// a ';'-separated list of S-52 draw instructions — SY()/TX()/TE()/LS()/LC()/AC()/ +// AP() — that we render verbatim, instead of routing NEWOBJ to the V-AIS alias +// (the S-101 FeatureCatalogue maps NEWOBJ→VirtualAISAidToNavigation, which would +// stamp a V-AIS mark and ignore the producer's instruction). This is how the S-52 +// PresLib "ECDIS Chart 1" labels (164 TX), boundaries (LS/LC), fills (AC/AP) and +// the size-check symbol SY(CHKSYM01) are drawn. +// +// Returns ok=false when the feature has no usable SYMINS, so the caller falls back +// to the default new-object symbology. +func parseSYMINS(f *s57.Feature) (FeatureBuild, bool) { + attrs := f.Attributes() + raw, _ := attrs["SYMINS"].(string) + raw = strings.TrimSpace(raw) + if raw == "" { + return FeatureBuild{}, false + } + g := geometryOf(f.Geometry()) + anchor, hasAnchor := representativePoint(f) + + var prims []Primitive + for _, instr := range splitSyminsInstructions(raw) { + op, params, ok := splitSyminsOp(instr) + if !ok { + continue + } + switch op { + case "SY": // point symbol — SY(NAME[,rot]) + if !hasAnchor { + continue + } + args := splitSyminsArgs(params) + name := strings.TrimSpace(firstOr(args, "")) + if name == "" { + continue + } + rot := float32(0) + if len(args) > 1 { + if v, err := strconv.ParseFloat(strings.TrimSpace(args[1]), 32); err == nil { + rot = float32(v) + } + } + prims = append(prims, SymbolCall{ + Anchor: anchor, SymbolName: name, RotationDeg: rot, + Scale: DefaultPxPerSymbolUnit, SoundingDepthM: nan32, DangerDepthM: nan32, + }) + case "TX", "TE": // text label + if !hasAnchor { + continue + } + if t, ok := parseSyminsText(op, params, attrs, anchor); ok { + prims = append(prims, t) + } + case "LS": // simple line — LS(style,width,colour) + args := splitSyminsArgs(params) + if len(args) < 3 { + continue + } + w, _ := strconv.Atoi(strings.TrimSpace(args[1])) + if w <= 0 { + w = 1 + } + color := strings.TrimSpace(args[2]) + dash := syminsDash(strings.TrimSpace(args[0])) + for _, line := range syminsLines(g) { + prims = append(prims, StrokeLine{Points: line, ColorToken: color, WidthPx: float32(w), Dash: dash}) + } + case "LC": // complex (symbolised) line — LC(LINESTYLE) + name := strings.TrimSpace(firstOr(splitSyminsArgs(params), "")) + if name == "" { + continue + } + for _, line := range syminsLines(g) { + prims = append(prims, LinePattern{Points: line, LinestyleName: name}) + } + case "AC": // area colour fill — AC(COLOUR[,transp]) + color := strings.TrimSpace(firstOr(splitSyminsArgs(params), "")) + if color != "" && len(g.area) > 0 { + prims = append(prims, FillPolygon{Rings: g.area, ColorToken: color}) + } + case "AP": // area pattern fill — AP(PATTERN) + name := strings.TrimSpace(firstOr(splitSyminsArgs(params), "")) + if name != "" && len(g.area) > 0 { + prims = append(prims, PatternFill{Rings: g.area, PatternName: name}) + } + } + } + if len(prims) == 0 { + return FeatureBuild{}, false + } + return FeatureBuild{Primitives: prims, DisplayPriority: 6, DisplayCategory: displayStandard}, true +} + +// parseSyminsText parses a SYMINS TX()/TE() instruction into a DrawText. +// +// TX(string|attr, hjust, vjust, space, chars, xoffs, yoffs, colour, display) +// TE(format, attribs, hjust, vjust, space, chars, xoffs, yoffs, colour, display) +func parseSyminsText(op, params string, attrs map[string]any, anchor geo.LatLon) (DrawText, bool) { + args := splitSyminsArgs(params) + var text string + var hjustIdx, vjustIdx, charsIdx, xoffIdx, yoffIdx, colorIdx, displayIdx int + if op == "TE" { + if len(args) < 10 { + return DrawText{}, false + } + format := strings.Trim(args[0], "'\"") + var names []string + for _, a := range strings.Split(strings.Trim(args[1], "'\""), ",") { + if a = strings.TrimSpace(a); a != "" { + names = append(names, a) + } + } + t, ok := formatSubstitute(attrs, format, names) + if !ok { + return DrawText{}, false + } + text = t + hjustIdx, vjustIdx, charsIdx, xoffIdx, yoffIdx, colorIdx, displayIdx = 2, 3, 5, 6, 7, 8, 9 + } else { // TX + if len(args) < 9 { + return DrawText{}, false + } + rawStr := args[0] + if strings.HasPrefix(rawStr, "'") || strings.HasPrefix(rawStr, "\"") { + text = strings.Trim(rawStr, "'\"") // literal + } else { // attribute reference + v, ok := attrs[strings.TrimSpace(rawStr)] + if !ok || v == nil { + return DrawText{}, false + } + text = fmt.Sprintf("%v", v) + } + hjustIdx, vjustIdx, charsIdx, xoffIdx, yoffIdx, colorIdx, displayIdx = 1, 2, 4, 5, 6, 7, 8 + } + if text == "" { + return DrawText{}, false + } + color := strings.TrimSpace(argAt(args, colorIdx)) + if color == "" { + color = "CHBLK" + } + hjust, _ := strconv.Atoi(strings.TrimSpace(argAt(args, hjustIdx))) + vjust, _ := strconv.Atoi(strings.TrimSpace(argAt(args, vjustIdx))) + group, _ := strconv.Atoi(strings.TrimSpace(argAt(args, displayIdx))) + xoff, _ := strconv.Atoi(strings.TrimSpace(argAt(args, xoffIdx))) + yoff, _ := strconv.Atoi(strings.TrimSpace(argAt(args, yoffIdx))) + fontPx := syminsFontPx(strings.Trim(argAt(args, charsIdx), "'\"")) + var halo *TextHalo + if fontPx >= 10 { + halo = &TextHalo{ColorToken: "CHWHT", WidthPx: 1} + } + return DrawText{ + Anchor: anchor, Text: text, FontSizePx: fontPx, ColorToken: color, Halo: halo, + HAlign: syminsHAlign(hjust), VAlign: syminsVAlign(vjust), + // S-52 §8.3.3.2 XOFFS/YOFFS are in units of the text body size (+x right, +y down). + OffsetXPx: float32(xoff) * fontPx, OffsetYPx: float32(yoff) * fontPx, + Group: group, + }, true +} + +// syminsFontPx converts a SYMINS CHARS field (e.g. '15110' = style/weight/slant + +// two-digit body size) to a pixel font size. The body size is in points; one point +// is 0.351 mm, scaled to px at the app's reference pixel pitch (100·DefaultPxPerSymbolUnit +// px/mm). Falls back to the engine default (12 px) on a malformed field. +func syminsFontPx(chars string) float32 { + if len(chars) >= 5 { + if body, err := strconv.Atoi(chars[3:5]); err == nil && body > 0 { + return float32(body) * 0.351 * 100 * float32(DefaultPxPerSymbolUnit) + } + } + return 12 +} + +// syminsHAlign maps S-52 HJUST (1 centre, 2 right, 3 left) to HAlign. +func syminsHAlign(h int) HAlign { + switch h { + case 1: + return HAlignCenter + case 2: + return HAlignRight + default: + return HAlignLeft + } +} + +// syminsVAlign maps S-52 VJUST (1 bottom, 2 centre, 3 top) to VAlign. +func syminsVAlign(v int) VAlign { + switch v { + case 1: + return VAlignBottom + case 3: + return VAlignTop + default: + return VAlignMiddle + } +} + +func syminsDash(style string) Dash { + switch strings.ToUpper(style) { + case "DASH": + return DashDashed + case "DOTT": + return DashDotted + default: + return DashSolid + } +} + +// syminsLines returns the polyline(s) a line/area instruction (LS/LC) strokes: a +// line feature's polyline, or each ring of an area feature (closed). +func syminsLines(g geom) [][]geo.LatLon { + switch g.kind { + case geomLine: + if len(g.line) >= 2 { + return [][]geo.LatLon{g.line} + } + case geomArea: + var out [][]geo.LatLon + for _, r := range g.area { + if len(r) >= 2 { + if r[0] != r[len(r)-1] { + r = append(append([]geo.LatLon(nil), r...), r[0]) + } + out = append(out, r) + } + } + return out + } + return nil +} + +// splitSyminsInstructions splits a SYMINS string on ';', honouring quotes and +// nested parens (so a ';' inside TX('a;b',…) or between parens isn't a split). +func splitSyminsInstructions(s string) []string { + var out []string + var cur strings.Builder + depth, inQuote := 0, false + for i := 0; i < len(s); i++ { + switch c := s[i]; c { + case '\'', '"': + inQuote = !inQuote + cur.WriteByte(c) + case '(': + if !inQuote { + depth++ + } + cur.WriteByte(c) + case ')': + if !inQuote { + depth-- + } + cur.WriteByte(c) + case ';': + if !inQuote && depth == 0 { + out = append(out, cur.String()) + cur.Reset() + } else { + cur.WriteByte(c) + } + default: + cur.WriteByte(c) + } + } + if cur.Len() > 0 { + out = append(out, cur.String()) + } + return out +} + +// splitSyminsOp splits "OP(params)" into the op and the inner params. +func splitSyminsOp(instr string) (op, params string, ok bool) { + instr = strings.TrimSpace(instr) + open := strings.IndexByte(instr, '(') + closeI := strings.LastIndexByte(instr, ')') + if open <= 0 || closeI < open { + return "", "", false + } + return strings.TrimSpace(instr[:open]), instr[open+1 : closeI], true +} + +// splitSyminsArgs splits an instruction's params on ',', honouring single/double +// quotes (so a comma inside a quoted format/string stays in one arg). +func splitSyminsArgs(params string) []string { + var out []string + var cur strings.Builder + inQuote := false + for i := 0; i < len(params); i++ { + switch c := params[i]; c { + case '\'', '"': + inQuote = !inQuote + cur.WriteByte(c) + case ',': + if inQuote { + cur.WriteByte(c) + } else { + out = append(out, strings.TrimSpace(cur.String())) + cur.Reset() + } + default: + cur.WriteByte(c) + } + } + out = append(out, strings.TrimSpace(cur.String())) + return out +} + +func firstOr(args []string, def string) string { + if len(args) > 0 { + return args[0] + } + return def +} + +func argAt(args []string, i int) string { + if i >= 0 && i < len(args) { + return args[i] + } + return "" +} diff --git a/internal/engine/portrayal/symins_test.go b/internal/engine/portrayal/symins_test.go new file mode 100644 index 0000000..ead2158 --- /dev/null +++ b/internal/engine/portrayal/symins_test.go @@ -0,0 +1,98 @@ +package portrayal + +import ( + "testing" + + "github.com/beetlebugorg/chartplotter/pkg/s57" +) + +// TestSYMINSPointSymbol: a NEWOBJ point with SYMINS="SY(INFORM01)" portrays the +// named symbol (the producer's instruction), NOT the V-AIS alias. +func TestSYMINSPointSymbol(t *testing.T) { + f := s57.NewFeature(1, "NEWOBJ", + s57.Geometry{Type: s57.GeometryTypePoint, Coordinates: [][]float64{{-5.1, 15.1}}}, + map[string]any{"SYMINS": "SY(INFORM01)"}, + ) + fb, ok := parseSYMINS(&f) + if !ok { + t.Fatal("parseSYMINS returned ok=false") + } + if len(fb.Primitives) != 1 { + t.Fatalf("want 1 primitive, got %d", len(fb.Primitives)) + } + sc, ok := fb.Primitives[0].(SymbolCall) + if !ok || sc.SymbolName != "INFORM01" { + t.Fatalf("want SymbolCall INFORM01, got %#v", fb.Primitives[0]) + } +} + +// TestSYMINSTextLabel: a TX literal label is parsed into a DrawText with the text, +// colour and text group (display field) from the instruction. +func TestSYMINSTextLabel(t *testing.T) { + f := s57.NewFeature(2, "NEWOBJ", + s57.Geometry{Type: s57.GeometryTypePoint, Coordinates: [][]float64{{-5.1, 15.1}}}, + map[string]any{"SYMINS": "TX('Information about',3,2,2,'14108',0,0,CHBLK,11)"}, + ) + fb, ok := parseSYMINS(&f) + if !ok { + t.Fatal("ok=false") + } + dt, ok := fb.Primitives[0].(DrawText) + if !ok { + t.Fatalf("want DrawText, got %#v", fb.Primitives[0]) + } + if dt.Text != "Information about" { + t.Errorf("text = %q, want %q", dt.Text, "Information about") + } + if dt.ColorToken != "CHBLK" { + t.Errorf("colour = %q, want CHBLK", dt.ColorToken) + } + if dt.Group != 11 { + t.Errorf("text group = %d, want 11", dt.Group) + } + if dt.HAlign != HAlignLeft { // HJUST 3 = left + t.Errorf("HAlign = %v, want left", dt.HAlign) + } +} + +// TestSYMINSAreaBoundaryAndFill: an area NEWOBJ with a dashed boundary + colour +// fill emits a StrokeLine per ring and a FillPolygon. +func TestSYMINSAreaBoundaryAndFill(t *testing.T) { + ring := [][]float64{{-5.1, 15.1}, {-5.0, 15.1}, {-5.0, 15.2}, {-5.1, 15.2}, {-5.1, 15.1}} + f := s57.NewFeature(3, "NEWOBJ", + s57.Geometry{Type: s57.GeometryTypePolygon, Coordinates: ring}, + map[string]any{"SYMINS": "AC(CHMGF);LS(DASH,2,CHMGD)"}, + ) + fb, ok := parseSYMINS(&f) + if !ok { + t.Fatal("ok=false") + } + var hasFill, hasDashedStroke bool + for _, p := range fb.Primitives { + switch v := p.(type) { + case FillPolygon: + if v.ColorToken == "CHMGF" { + hasFill = true + } + case StrokeLine: + if v.ColorToken == "CHMGD" && v.Dash == DashDashed { + hasDashedStroke = true + } + } + } + if !hasFill || !hasDashedStroke { + t.Fatalf("want CHMGF fill + dashed CHMGD stroke, got %#v", fb.Primitives) + } +} + +// TestSYMINSEmptyFallsThrough: no SYMINS ⇒ ok=false so the caller uses the default +// new-object symbology. +func TestSYMINSEmptyFallsThrough(t *testing.T) { + f := s57.NewFeature(4, "NEWOBJ", + s57.Geometry{Type: s57.GeometryTypePoint, Coordinates: [][]float64{{-5.1, 15.1}}}, + map[string]any{}, + ) + if _, ok := parseSYMINS(&f); ok { + t.Fatal("want ok=false for a feature with no SYMINS") + } +} From 9e7185d83e87c5fdfaf5aa5a162c920b099867f5 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 06:06:46 -0400 Subject: [PATCH 03/15] test(preslib): ECDIS Chart 1 render harness + per-class audit breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make preslib-chart1` (scripts/preslib-chart1.sh + .mjs) renders each S-52 PresLib "ECDIS Chart 1" panel by reference-plot page number for visual diffing against the spec (PresLib e4.0.0 Part I §16, doc pages 238-253): extracts the cells, serves a throwaway server, imports via the normal server-side bake, screenshots each panel framed to its cell, tears down. Output → testdata/preslib-chart1-out/ (gitignored). TestS101Audit now also dumps a per-class error breakdown (which classes fail portrayal and how often) — surfaced the NEWOBJ/SWPARE gaps. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 4 ++ Makefile | 10 +++- internal/engine/baker/s101audit_test.go | 6 ++ scripts/preslib-chart1.mjs | 78 +++++++++++++++++++++++++ scripts/preslib-chart1.sh | 61 +++++++++++++++++++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 scripts/preslib-chart1.mjs create mode 100755 scripts/preslib-chart1.sh diff --git a/.gitignore b/.gitignore index ffea684..11b8a17 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ coverage.* /testdata/full/ /All_ENCs.zip +# S-52 PresLib "ECDIS Chart 1" harness: extracted cells + rendered panels +/testdata/preslib_chart1/ +/testdata/preslib-chart1-out/ + # Generated tile archives / runtime chart state (never committed) *.pmtiles web/noaa.pmtiles diff --git a/Makefile b/Makefile index 038a65d..da41bc1 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ S101_PC ?= $(HOME)/Projects/s101-portrayal-catalogue/PortrayalCatalog S101_FC ?= $(HOME)/Projects/s101-feature-catalogue/S-101FC/FeatureCatalogue.xml S101_CACHE ?= $(CACHE)/s101 -.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo +.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo preslib-chart1 # Prebaked prod test set (US Inland ENC bundle + the NOAA world archive). # NB: keep these as bare values with NO inline `#` comments — Make folds any @@ -205,6 +205,14 @@ serve-demo: demo ## Preview the static demo bundle locally (range-capable static docs: ## Run the documentation site dev server (Docusaurus; DOCS_HOST/DOCS_PORT overridable) cd docs && { [ -d node_modules ] || npm install; } && npm start -- --host $(DOCS_HOST) --port $(DOCS_PORT) +# Render the S-52 PresLib "ECDIS Chart 1" panels (one PNG per reference-plot page) +# with our implementation, for visual diffing against the spec's reference plots +# (PresLib e4.0.0 Part I §16). Self-contained: extracts the cells, bakes+serves via +# the import path, screenshots each panel, tears down. Needs the PresLib zip in +# testdata/ + a headless Chromium. Output → testdata/preslib-chart1-out/ (gitignored). +preslib-chart1: ## Render PresLib "ECDIS Chart 1" panels for spec comparison (one PNG per reference page) + scripts/preslib-chart1.sh + # Regenerate the documentation UI screenshots (docs/static/img/ui/*.png) from the # live app, so they stay in sync when the UI changes. Needs baked charts in the # S-101 cache (e.g. after `make serve` has imported a region); Chromium + diff --git a/internal/engine/baker/s101audit_test.go b/internal/engine/baker/s101audit_test.go index efe8bba..811709c 100644 --- a/internal/engine/baker/s101audit_test.go +++ b/internal/engine/baker/s101audit_test.go @@ -196,6 +196,12 @@ func TestS101Audit(t *testing.T) { } } // Empty-but-mapped is the silent-suppression gap; list those classes. + t.Logf("=== classes with errors (errd>0) ===") + for _, c := range sortedKeys3(classStat) { + if s := classStat[c]; s.errd > 0 { + t.Logf(" %-8s errd=%d total=%d ok=%d", c, s.errd, s.total, s.ok) + } + } t.Logf("=== classes with empty (mapped, no-error, emitted nothing) ===") for _, c := range sortedKeys3(classStat) { s := classStat[c] diff --git a/scripts/preslib-chart1.mjs b/scripts/preslib-chart1.mjs new file mode 100644 index 0000000..f3b166b --- /dev/null +++ b/scripts/preslib-chart1.mjs @@ -0,0 +1,78 @@ +// Render every panel of the S-52 PresLib "ECDIS Chart 1" (PresLib e4.0.0 Part I +// §16, document pages 238–253) with OUR implementation, one PNG per reference +// page, so the result can be diffed against the spec's reference plots. +// +// The 14 ECDIS-Chart-1 cells tile the chart in a 4-wide grid; each cell IS one +// reference panel and its name encodes the panel letters (AA5C1CDE → panel +// "C,D,E"). The PANELS table below maps each reference page to the cell it +// portrays, framed at the cells' 1:14 000 compilation scale (≈ z14.2, where one +// cell fills the screen — the scale the legend was drawn for). +// +// Usage: +// node scripts/preslib-chart1.mjs [settleMs] +// The baseURL must already serve the imported Chart-1 pack (see +// scripts/preslib-chart1.sh, which sets that up, runs this, and tears it down). +import { createRequire } from "node:module"; +import { execSync } from "node:child_process"; +import { mkdirSync } from "node:fs"; +const require = createRequire(import.meta.url); +function findPlaywright() { + try { return require("playwright-core"); } catch {} + const root = execSync("npm root -g", { encoding: "utf8" }).trim(); + return require(`${root}/promptfoo/node_modules/playwright-core`); +} +function findChromium() { + for (const p of ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/chrome"]) { + try { execSync(`test -x ${p}`); return p; } catch {} + } + return undefined; +} + +const [baseURL = "http://127.0.0.1:8101", outDir = "/tmp/preslib-chart1", settle = "8000"] = process.argv.slice(2); + +// One reference page per row: the document page number (the figure to diff +// against), a slug for the filename, the cell center [lng,lat] + framing zoom, +// and the colour scheme. PANEL_Z frames a single ~3.3 km cell with ~15% margin +// on every side, so the panel clears the app chrome (status bar / controls) and +// nothing is clipped; the overview frames the whole 14-cell chart with margin. +const PANEL_Z = 13.9; +const PANELS = [ + { page: 238, slug: "overview", c: [-5.0669, 15.0668], z: 12.0, scheme: "day" }, + { page: 239, slug: "info-AB1", c: [-5.11545, 15.11405], z: PANEL_Z, scheme: "day" }, + { page: 240, slug: "info-AB2", c: [-5.08295, 15.11405], z: PANEL_Z, scheme: "day" }, + { page: 241, slug: "natural-CDE", c: [-5.05035, 15.11405], z: PANEL_Z, scheme: "day" }, + { page: 242, slug: "port-FOO", c: [-5.01785, 15.11405], z: PANEL_Z, scheme: "day" }, + { page: 243, slug: "depths-HIO", c: [-5.11545, 15.08250], z: PANEL_Z, scheme: "day" }, + { page: 244, slug: "seabed-JKL", c: [-5.08295, 15.08250], z: PANEL_Z, scheme: "day" }, + { page: 245, slug: "traffic-MOO", c: [-5.05035, 15.08250], z: PANEL_Z, scheme: "day" }, + { page: 246, slug: "special-NOO", c: [-5.01785, 15.08250], z: PANEL_Z, scheme: "day" }, + { page: 247, slug: "aids-PRS", c: [-5.11545, 15.05095], z: PANEL_Z, scheme: "day" }, + { page: 248, slug: "buoys-QO1", c: [-5.08290, 15.05095], z: PANEL_Z, scheme: "day" }, + { page: 250, slug: "topmarks-QO2", c: [-5.05030, 15.05095], z: PANEL_Z, scheme: "day" }, + { page: 251, slug: "newobj-vais-MNS", c: [-5.11545, 15.01940], z: PANEL_Z, scheme: "day" }, + { page: 252, slug: "colourtest-WOO-day", c: [-5.01785, 15.05095], z: PANEL_Z, scheme: "day" }, + { page: 253, slug: "colourtest-WOO-dusk", c: [-5.01785, 15.05095], z: PANEL_Z, scheme: "dusk" }, +]; + +mkdirSync(outDir, { recursive: true }); +const { chromium } = findPlaywright(); +const browser = await chromium.launch({ executablePath: findChromium(), args: ["--no-sandbox", "--hide-scrollbars"] }); + +for (const p of PANELS) { + const page = await browser.newPage({ viewport: { width: 1000, height: 1000 }, deviceScaleFactor: 1 }); + page.on("pageerror", (e) => console.error(`[page ${p.page}]`, e.message)); + await page.addInitScript((a) => { + localStorage.setItem("chartplotter:scheme", a.scheme); + localStorage.setItem("chartplotter:basemap", "coastline"); + localStorage.setItem("chartplotter:enc-agreement", "1"); + localStorage.setItem("chartplotter:view", JSON.stringify({ center: a.c, zoom: a.z })); + }, p); + try { await page.goto(baseURL + "/?prod", { waitUntil: "domcontentloaded", timeout: 45000 }); } + catch (e) { console.error(`[page ${p.page}] nav: ${e.message} — continuing`); } + await page.waitForTimeout(+settle); + const out = `${outDir}/page-${p.page}-${p.slug}.png`; + await page.screenshot({ path: out }); + console.log(`wrote ${out}`); + await page.close(); +} +await browser.close(); diff --git a/scripts/preslib-chart1.sh b/scripts/preslib-chart1.sh new file mode 100755 index 0000000..eb16a7f --- /dev/null +++ b/scripts/preslib-chart1.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Render every panel of the S-52 PresLib "ECDIS Chart 1" with our implementation, +# one PNG per reference-plot page (PresLib e4.0.0 Part I §16, doc pages 238–253), +# for diffing against the spec. Self-contained: extracts the cells, bakes+serves +# them through the normal server-side import path, drives scripts/preslib-chart1.mjs, +# then tears the server down. Re-runnable as-is. +# +# scripts/preslib-chart1.sh [OUT_DIR] +# +# OUT_DIR defaults to testdata/preslib-chart1-out/ (gitignored). Requires the +# PresLib zip in testdata/ (an untracked IHO download) and a headless Chromium. +set -euo pipefail +cd "$(dirname "$0")/.." + +ZIP="testdata/S-52_PresLib_e4.0.0_Digital_Files_Draft.zip" +OUT="${1:-testdata/preslib-chart1-out}" +PORT="${PORT:-8123}" +BIN="bin/chartplotter" + +if [[ ! -f "$ZIP" ]]; then + echo "missing $ZIP — download the S-52 PresLib e4.0.0 digital files into testdata/" >&2 + exit 1 +fi + +echo "==> building $BIN" +make build >/dev/null + +WORK="$(mktemp -d)" +SRV_PID="" +cleanup() { + [[ -n "$SRV_PID" ]] && kill "$SRV_PID" 2>/dev/null || true + rm -rf "$WORK" +} +trap cleanup EXIT + +echo "==> extracting Chart-1 cells" +unzip -qo "$ZIP" -d "$WORK" +ENC_ROOT="$(dirname "$(find "$WORK" -name '*.000' | head -1)")" +( cd "$ENC_ROOT/.." && zip -qr "$WORK/chart1.zip" "$(basename "$ENC_ROOT")" ) + +echo "==> serving on :$PORT (temp cache/data)" +"$BIN" serve --assets web/ --cache "$WORK/cache" --data "$WORK/data" --port "$PORT" >"$WORK/serve.log" 2>&1 & +SRV_PID=$! +for _ in $(seq 1 30); do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null 2>&1 && break; sleep 0.5; done + +echo "==> importing (server-side bake)" +JOB="$(curl -fsS -X POST "http://127.0.0.1:$PORT/api/import?set=preslib" \ + --data-binary @"$WORK/chart1.zip" -H 'Content-Type: application/zip' \ + | python3 -c 'import sys,json;print(json.load(sys.stdin)["job"])')" +for _ in $(seq 1 60); do + STATE="$(curl -fsS "http://127.0.0.1:$PORT/api/import/status?job=$JOB" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin).get("state",""))' 2>/dev/null || true)" + [[ "$STATE" == "done" ]] && break + [[ "$STATE" == "error" ]] && { echo "import failed"; cat "$WORK/serve.log"; exit 1; } + sleep 1 +done + +echo "==> rendering panels → $OUT" +node scripts/preslib-chart1.mjs "http://127.0.0.1:$PORT" "$OUT" + +echo "==> done: $(ls "$OUT" | wc -l) PNG(s) in $OUT" From f6f4ff41ca0311173d31c117358d70e5e9e25630 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 06:17:29 -0400 Subject: [PATCH 04/15] feat(web): spec mode (chrome-free) + render Chart-1 pages at compilation scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "spec" mode (?spec / [spec]): a clean, full-bleed map with every floating control, the status readout, attribution, load bar and the S-52 scalebar hidden — for capturing reference-style plots. The scalebar lives in chart-canvas's shadow root, so it's hidden at the source when an ancestor chart-plotter[-app] has [spec]. Rework the ECDIS-Chart-1 harness to render each page in spec mode at ITS compilation scale: the viewport is sized to the cell's ground extent / CSCL / pixel-pitch (1:14 000 harbor pages, 1:60 000 overview), so each panel is captured full-screen at the scale the legend was drawn for — matching the reference figure. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/preslib-chart1.mjs | 88 +++++++++++++++------------ web/src/chart-canvas/chart-canvas.mjs | 3 + web/src/chartplotter.mjs | 4 ++ web/src/chartplotter.view.mjs | 6 ++ 4 files changed, 63 insertions(+), 38 deletions(-) diff --git a/scripts/preslib-chart1.mjs b/scripts/preslib-chart1.mjs index f3b166b..67e272b 100644 --- a/scripts/preslib-chart1.mjs +++ b/scripts/preslib-chart1.mjs @@ -1,17 +1,16 @@ // Render every panel of the S-52 PresLib "ECDIS Chart 1" (PresLib e4.0.0 Part I // §16, document pages 238–253) with OUR implementation, one PNG per reference -// page, so the result can be diffed against the spec's reference plots. +// page, for diffing against the spec's reference plots. // -// The 14 ECDIS-Chart-1 cells tile the chart in a 4-wide grid; each cell IS one -// reference panel and its name encodes the panel letters (AA5C1CDE → panel -// "C,D,E"). The PANELS table below maps each reference page to the cell it -// portrays, framed at the cells' 1:14 000 compilation scale (≈ z14.2, where one -// cell fills the screen — the scale the legend was drawn for). +// Each of the 14 ECDIS-Chart-1 cells IS one reference panel (its name encodes the +// panel letters, e.g. AA5C1CDE → "C,D,E"). The PANELS table maps each reference +// page to its cell BOUNDS + compilation scale. We render in "spec mode" (?spec — +// no app chrome) and size the window so the cell fills it AT ITS COMPILATION SCALE +// (1:14 000 for the harbor pages, 1:60 000 for the overview): viewport_px = +// ground_metres / scale / pixel_pitch. So each page is captured full-screen at the +// scale the legend was drawn for, exactly like the reference figure. // -// Usage: -// node scripts/preslib-chart1.mjs [settleMs] -// The baseURL must already serve the imported Chart-1 pack (see -// scripts/preslib-chart1.sh, which sets that up, runs this, and tears it down). +// Usage: node scripts/preslib-chart1.mjs [settleMs] import { createRequire } from "node:module"; import { execSync } from "node:child_process"; import { mkdirSync } from "node:fs"; @@ -30,28 +29,32 @@ function findChromium() { const [baseURL = "http://127.0.0.1:8101", outDir = "/tmp/preslib-chart1", settle = "8000"] = process.argv.slice(2); -// One reference page per row: the document page number (the figure to diff -// against), a slug for the filename, the cell center [lng,lat] + framing zoom, -// and the colour scheme. PANEL_Z frames a single ~3.3 km cell with ~15% margin -// on every side, so the panel clears the app chrome (status bar / controls) and -// nothing is clipped; the overview frames the whole 14-cell chart with margin. -const PANEL_Z = 13.9; +// Display geometry, shared with the app (web/src/lib/util.mjs): the 512-tile +// metres-per-pixel at z0 and the 1/96-inch CSS reference pixel. +const M_PER_PX_Z0 = 78271.516964020485; +const PX_PITCH_M = 0.00026458; +const zoomForScale = (scale, lat) => Math.log2(M_PER_PX_Z0 * Math.cos((lat * Math.PI) / 180) / (PX_PITCH_M * scale)); +const spanPx = (metres, scale) => Math.max(1, Math.round(metres / scale / PX_PITCH_M)); + +// page → cell: bounds [W,S,E,N] + compilation scale (CSCL). Bounds are the cells' +// data extents (AA4C1XMS = the 1:60 000 overview; AA5C1* = 1:14 000 harbor pages). +const HARBOR = 14000, OVERVIEW = 60000; const PANELS = [ - { page: 238, slug: "overview", c: [-5.0669, 15.0668], z: 12.0, scheme: "day" }, - { page: 239, slug: "info-AB1", c: [-5.11545, 15.11405], z: PANEL_Z, scheme: "day" }, - { page: 240, slug: "info-AB2", c: [-5.08295, 15.11405], z: PANEL_Z, scheme: "day" }, - { page: 241, slug: "natural-CDE", c: [-5.05035, 15.11405], z: PANEL_Z, scheme: "day" }, - { page: 242, slug: "port-FOO", c: [-5.01785, 15.11405], z: PANEL_Z, scheme: "day" }, - { page: 243, slug: "depths-HIO", c: [-5.11545, 15.08250], z: PANEL_Z, scheme: "day" }, - { page: 244, slug: "seabed-JKL", c: [-5.08295, 15.08250], z: PANEL_Z, scheme: "day" }, - { page: 245, slug: "traffic-MOO", c: [-5.05035, 15.08250], z: PANEL_Z, scheme: "day" }, - { page: 246, slug: "special-NOO", c: [-5.01785, 15.08250], z: PANEL_Z, scheme: "day" }, - { page: 247, slug: "aids-PRS", c: [-5.11545, 15.05095], z: PANEL_Z, scheme: "day" }, - { page: 248, slug: "buoys-QO1", c: [-5.08290, 15.05095], z: PANEL_Z, scheme: "day" }, - { page: 250, slug: "topmarks-QO2", c: [-5.05030, 15.05095], z: PANEL_Z, scheme: "day" }, - { page: 251, slug: "newobj-vais-MNS", c: [-5.11545, 15.01940], z: PANEL_Z, scheme: "day" }, - { page: 252, slug: "colourtest-WOO-day", c: [-5.01785, 15.05095], z: PANEL_Z, scheme: "day" }, - { page: 253, slug: "colourtest-WOO-dusk", c: [-5.01785, 15.05095], z: PANEL_Z, scheme: "dusk" }, + { page: 238, slug: "overview", b: [-5.135803, 15.00018, -4.997983, 15.133311], cscl: OVERVIEW, scheme: "day" }, + { page: 239, slug: "info-AB1", b: [-5.1307, 15.0993, -5.1002, 15.1288], cscl: HARBOR, scheme: "day" }, + { page: 240, slug: "info-AB2", b: [-5.0982, 15.0993, -5.0677, 15.1288], cscl: HARBOR, scheme: "day" }, + { page: 241, slug: "natural-CDE", b: [-5.0656, 15.0992, -5.0351, 15.1288], cscl: HARBOR, scheme: "day" }, + { page: 242, slug: "port-FOO", b: [-5.0331, 15.0993, -5.0026, 15.1288], cscl: HARBOR, scheme: "day" }, + { page: 243, slug: "depths-HIO", b: [-5.1307, 15.0677, -5.1002, 15.0973], cscl: HARBOR, scheme: "day" }, + { page: 244, slug: "seabed-JKL", b: [-5.0982, 15.0677, -5.0677, 15.0973], cscl: HARBOR, scheme: "day" }, + { page: 245, slug: "traffic-MOO", b: [-5.0656, 15.0677, -5.0351, 15.0973], cscl: HARBOR, scheme: "day" }, + { page: 246, slug: "special-NOO", b: [-5.0331, 15.0677, -5.0026, 15.0973], cscl: HARBOR, scheme: "day" }, + { page: 247, slug: "aids-PRS", b: [-5.1307, 15.0362, -5.1002, 15.0657], cscl: HARBOR, scheme: "day" }, + { page: 248, slug: "buoys-QO1", b: [-5.0982, 15.0362, -5.0676, 15.0657], cscl: HARBOR, scheme: "day" }, + { page: 250, slug: "topmarks-QO2", b: [-5.0656, 15.0362, -5.0350, 15.0657], cscl: HARBOR, scheme: "day" }, + { page: 251, slug: "newobj-vais-MNS", b: [-5.1307, 15.0046, -5.1002, 15.0342], cscl: HARBOR, scheme: "day" }, + { page: 252, slug: "colourtest-WOO-day", b: [-5.0331, 15.0362, -5.0026, 15.0657], cscl: HARBOR, scheme: "day" }, + { page: 253, slug: "colourtest-WOO-dusk", b: [-5.0331, 15.0362, -5.0026, 15.0657], cscl: HARBOR, scheme: "dusk" }, ]; mkdirSync(outDir, { recursive: true }); @@ -59,20 +62,29 @@ const { chromium } = findPlaywright(); const browser = await chromium.launch({ executablePath: findChromium(), args: ["--no-sandbox", "--hide-scrollbars"] }); for (const p of PANELS) { - const page = await browser.newPage({ viewport: { width: 1000, height: 1000 }, deviceScaleFactor: 1 }); - page.on("pageerror", (e) => console.error(`[page ${p.page}]`, e.message)); + const [w, s, e, n] = p.b; + const lat = (s + n) / 2; + const center = [(w + e) / 2, lat]; + const zoom = zoomForScale(p.cscl, lat); + // Window sized to the cell at its compilation scale → the page fills it. + const lonM = (e - w) * 111320 * Math.cos((lat * Math.PI) / 180); + const latM = (n - s) * 110574; + const width = spanPx(lonM, p.cscl), height = spanPx(latM, p.cscl); + + const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 1 }); + page.on("pageerror", (err) => console.error(`[page ${p.page}]`, err.message)); await page.addInitScript((a) => { localStorage.setItem("chartplotter:scheme", a.scheme); localStorage.setItem("chartplotter:basemap", "coastline"); localStorage.setItem("chartplotter:enc-agreement", "1"); - localStorage.setItem("chartplotter:view", JSON.stringify({ center: a.c, zoom: a.z })); - }, p); - try { await page.goto(baseURL + "/?prod", { waitUntil: "domcontentloaded", timeout: 45000 }); } - catch (e) { console.error(`[page ${p.page}] nav: ${e.message} — continuing`); } + localStorage.setItem("chartplotter:view", JSON.stringify({ center: a.center, zoom: a.zoom })); + }, { scheme: p.scheme, center, zoom }); + try { await page.goto(baseURL + "/?prod&spec", { waitUntil: "domcontentloaded", timeout: 45000 }); } + catch (err) { console.error(`[page ${p.page}] nav: ${err.message} — continuing`); } await page.waitForTimeout(+settle); const out = `${outDir}/page-${p.page}-${p.slug}.png`; await page.screenshot({ path: out }); - console.log(`wrote ${out}`); + console.log(`wrote ${out} (${width}x${height} @ 1:${p.cscl})`); await page.close(); } await browser.close(); diff --git a/web/src/chart-canvas/chart-canvas.mjs b/web/src/chart-canvas/chart-canvas.mjs index fe6f682..f3728e4 100644 --- a/web/src/chart-canvas/chart-canvas.mjs +++ b/web/src/chart-canvas/chart-canvas.mjs @@ -361,6 +361,9 @@ export class ChartCanvas extends HTMLElement { // since 1 NM ≡ 1 arcminute of latitude. Re-rendered on every move. this._scaleEl = document.createElement("div"); this._scaleEl.className = "s52-scalebar maplibregl-ctrl"; + // Spec mode (chrome-free capture, see chartplotter.mjs) hides the scalebar too — + // it lives in this element's shadow root, out of reach of the app's :host([spec]) CSS. + if (document.querySelector("chart-plotter-app[spec], chart-plotter[spec]")) this._scaleEl.style.display = "none"; map.addControl({ onAdd: () => this._scaleEl, onRemove: () => { this._scaleEl = null; } }, "bottom-left"); map.on("move", () => this._renderScalebar()); diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs index 46ba101..915c1b2 100644 --- a/web/src/chartplotter.mjs +++ b/web/src/chartplotter.mjs @@ -284,6 +284,10 @@ export class ChartPlotter extends HTMLElement { // attribute so the :host([widget]) styles apply either way. this._widget = this.hasAttribute("widget") || new URLSearchParams(location.search).has("widget"); if (this._widget) this.setAttribute("widget", ""); + // Spec mode (?spec / [spec]): a clean, chrome-free full-bleed map — every + // floating control + readout hidden — for capturing reference-style plots (the + // S-52 PresLib "ECDIS Chart 1" panels diff against the spec). See :host([spec]). + if (this.hasAttribute("spec") || new URLSearchParams(location.search).has("spec")) this.setAttribute("spec", ""); // Display settings (scheme · basemap · mariner toggles · cell-boundary toggle · // bands-off) are persisted SERVER-side so every screen pointed at this boat's // server shares them and they survive a restart. Adopt them BEFORE the renderer diff --git a/web/src/chartplotter.view.mjs b/web/src/chartplotter.view.mjs index b13fe38..3aa8a2d 100644 --- a/web/src/chartplotter.view.mjs +++ b/web/src/chartplotter.view.mjs @@ -85,6 +85,12 @@ export const STYLE = ` affordances. Settings drop the Advanced tab (gated in JS). */ :host([widget]) #empty-add, :host([widget]) #empty .welcome-sub { display:none; } :host([widget]) #charts-btn, :host([widget]) #share-btn { display:none; } + /* Spec mode: a clean, chrome-free full-bleed map for capturing reference-style + plots (the S-52 PresLib "ECDIS Chart 1" panels). Hide every floating control, + the status readout, the attribution and the load bar so only the chart shows. */ + :host([spec]) #tl-controls, :host([spec]) #tr-controls, :host([spec]) #br-controls, + :host([spec]) #databox, :host([spec]) #noaa-attr, :host([spec]) #load-bar, + :host([spec]) #toasts { display:none; } .box-sel { position:absolute; z-index:5; border:2px solid var(--ui-accent); background:rgba(21,101,192,.12); pointer-events:none; } /* charts panel: action header + "your charts" cards */ .charts-actions { display:flex; gap:8px; margin-bottom:10px; } From a402a5dee0c1a20724a6257e8f1d96755388c276 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 06:42:50 -0400 Subject: [PATCH 05/15] fix(lights): sector legs honor the AugmentedRay length CRS (no more shooting out) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AugmentedRay instruction carries the leg LENGTH's CRS: LocalCRS = display millimetres (the 25 mm short sector leg), GeographicCRS = a fixed ground distance in metres (a sectorLineLength or full-VALNMR leg). The parser ignored the CRS and treated every length as mm, so a GeographicCRS leg of nmi2metres(0.1 NM)=185 m rendered as 185 mm — ~10× too long, shooting the sector legs off the chart (visible in the PresLib ECDIS-Chart-1 "Aids and services" panel). Parse the length CRS into AugmentedGeom.LengthGroundM (metres) vs LengthMM, carry it onto AugmentedFigure, and in tessellateFigure convert a ground-length leg to pixels at the tile zoom (like the full-length leg) instead of mm. Sector legs now render at their correct contained length. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/bake/bake.go | 8 ++++++ internal/engine/portrayal/primitive.go | 4 +++ internal/engine/portrayal/s101emit.go | 1 + pkg/s100/instructions/augray_crs_test.go | 32 ++++++++++++++++++++++++ pkg/s100/instructions/instructions.go | 20 ++++++++++++--- 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 pkg/s100/instructions/augray_crs_test.go diff --git a/internal/engine/bake/bake.go b/internal/engine/bake/bake.go index cb8437a..1468704 100644 --- a/internal/engine/bake/bake.go +++ b/internal/engine/bake/bake.go @@ -2007,7 +2007,15 @@ func tessellateFigure(sp *sectorPrim, z uint32) []sectorStroke { } return append(out, sectorStroke{points: pts, colorToken: sp.fig.ColorToken, widthPx: float32(widthPx), dashed: dashed, sleg: sleg}) } + // Short leg: display mm by default, but a GeographicCRS leg length is a fixed + // GROUND distance (metres) — convert it to px at this zoom (like the full leg), + // not metres-as-mm (which rendered legs ~10× too long, "shooting out"). legShort := sp.fig.LengthMM * pxPerMM + if sp.fig.LengthGroundM > 0 { + if cosLat := math.Cos(sp.fig.Anchor.Lat * math.Pi / 180.0); cosLat > 1e-6 { + legShort = sp.fig.LengthGroundM / (cosLat * earthCircumM) * worldPx + } + } if sp.fig.FullLengthNM <= 0 { return emit(nil, legShort, -1) // can't extend: the leg is always shown } diff --git a/internal/engine/portrayal/primitive.go b/internal/engine/portrayal/primitive.go index 881042f..367ebd2 100644 --- a/internal/engine/portrayal/primitive.go +++ b/internal/engine/portrayal/primitive.go @@ -167,6 +167,10 @@ type AugmentedFigure struct { // Ray params (true-north bearing, already from-seaward-reversed by the rule). BearingDeg float64 LengthMM float64 + // LengthGroundM is a ray leg's length when given as a GROUND distance (metres, + // from a GeographicCRS sectorLineLength / full-VALNMR leg) rather than display + // mm — drawn zoom-dependently. 0 ⇒ use LengthMM (display mm). See tessellateFigure. + LengthGroundM float64 // Arc params (centred on Anchor); a full 360° sweep is an all-round ring. RadiusMM float64 StartDeg float64 diff --git a/internal/engine/portrayal/s101emit.go b/internal/engine/portrayal/s101emit.go index 03db125..e81865b 100644 --- a/internal/engine/portrayal/s101emit.go +++ b/internal/engine/portrayal/s101emit.go @@ -156,6 +156,7 @@ func emitPrimitives(cmd instructions.DrawCommand, geom S101Geometry, cat *catalo fig.Ray = true fig.BearingDeg = ag.BearingDeg fig.LengthMM = ag.LengthMM + fig.LengthGroundM = ag.LengthGroundM case instructions.AugArc: fig.RadiusMM = ag.RadiusMM fig.StartDeg = ag.StartDeg diff --git a/pkg/s100/instructions/augray_crs_test.go b/pkg/s100/instructions/augray_crs_test.go new file mode 100644 index 0000000..22225e8 --- /dev/null +++ b/pkg/s100/instructions/augray_crs_test.go @@ -0,0 +1,32 @@ +package instructions + +import "testing" + +// TestAugmentedRayLengthCRS: the AugmentedRay length's CRS decides its unit. +// LocalCRS ⇒ display millimetres (the 25 mm short sector leg); GeographicCRS ⇒ +// a fixed ground distance in metres (a sectorLineLength / full-VALNMR leg). +// Conflating them rendered geographic legs at metres-as-mm — ~10× too long. +func TestAugmentedRayLengthCRS(t *testing.T) { + cases := []struct { + in string + wantMM, wantGndM float64 + }{ + {"AugmentedRay:GeographicCRS,123.4,GeographicCRS,185.2;LineInstruction:_simple_", 0, 185.2}, + {"AugmentedRay:GeographicCRS,123.4,LocalCRS,25;LineInstruction:_simple_", 25, 0}, + } + for _, c := range cases { + cmds, _ := Reduce(ParseStream(c.in)) + var got *AugmentedGeom + for i := range cmds { + if cmds[i].Augmented != nil { + got = cmds[i].Augmented + } + } + if got == nil { + t.Fatalf("%s: no augmented geom", c.in) + } + if got.LengthMM != c.wantMM || got.LengthGroundM != c.wantGndM { + t.Errorf("%s: LengthMM=%v LengthGroundM=%v, want %v / %v", c.in, got.LengthMM, got.LengthGroundM, c.wantMM, c.wantGndM) + } + } +} diff --git a/pkg/s100/instructions/instructions.go b/pkg/s100/instructions/instructions.go index 00fe20c..49932ec 100644 --- a/pkg/s100/instructions/instructions.go +++ b/pkg/s100/instructions/instructions.go @@ -81,6 +81,10 @@ type AugmentedGeom struct { // +180 reversal) of length LengthMM. BearingDeg float64 LengthMM float64 + // LengthGroundM is the leg length when the rule gave it in GeographicCRS — a + // fixed GROUND distance in metres (a sectorLineLength or full-VALNMR leg), + // rendered zoom-dependently. Mutually exclusive with LengthMM (display mm). + LengthGroundM float64 // Arc ("ArcByRadius:,,,,"): centred on the // anchor, RadiusMM, from StartDeg sweeping SweepDeg degrees clockwise. A full // 360° sweep is an all-round ring. @@ -226,9 +230,19 @@ func Reduce(ins []Instruction) (cmds []DrawCommand, unsupported []string) { curAug = nil // --- geometry construction (screen-space figures the rule builds) --- case "AugmentedRay": - // "AugmentedRay:,,," — a leg from the - // anchor. The rule emits the bearing already from-seaward-reversed. - curAug = &AugmentedGeom{Kind: AugRay, BearingDeg: atof(arg(in, 1)), LengthMM: atof(arg(in, 3))} + // "AugmentedRay:,,," — a leg from the + // anchor. The rule emits the bearing already from-seaward-reversed. The + // LENGTH's CRS (arg 2) decides its unit: LocalCRS ⇒ display mm (the 25 mm + // short sector leg); GeographicCRS ⇒ ground metres (a sectorLineLength / + // full-VALNMR leg — a fixed ground distance, NOT mm). Conflating the two + // rendered geographic legs at metres-as-mm, ~10× too long ("shooting out"). + ar := &AugmentedGeom{Kind: AugRay, BearingDeg: atof(arg(in, 1))} + if arg(in, 2) == "GeographicCRS" { + ar.LengthGroundM = atof(arg(in, 3)) + } else { + ar.LengthMM = atof(arg(in, 3)) + } + curAug = ar case "ArcByRadius": // "ArcByRadius:,,,," — an arc/ring // centred on the anchor (the cx,cy offset is 0 for sector figures). From 724b0b8b82503970a0941721ac76231e717eebc9 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 07:05:51 -0400 Subject: [PATCH 06/15] fix(web): keep S-52 text labels single-line (text-max-width) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S-52 TX/TE labels are single-line, but the `text` layer set no text-max-width, so MapLibre's 10-em default wrapped longer labels (e.g. "Information about chart display (A,B)") onto a second line — which then collided and was dropped (text-optional), so the label looked truncated. Set text-max-width: 40 so each label stays on one line, matching the PresLib reference plots. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/chart-canvas/chart-style.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/chart-canvas/chart-style.mjs b/web/src/chart-canvas/chart-style.mjs index a67f7fe..d727fb6 100644 --- a/web/src/chart-canvas/chart-style.mjs +++ b/web/src/chart-canvas/chart-style.mjs @@ -89,6 +89,11 @@ function textLayers(mariner, palette) { "text-field": ["coalesce", ["get", "text"], ""], "text-font": FONT, "text-size": ["coalesce", ["get", "font_size_px"], 11], "text-anchor": TEXT_ANCHOR, + // S-52 TX/TE labels are single-line; MapLibre's default text-max-width (10 em) + // wrapped longer labels (e.g. "Information about chart display (A,B)") onto a + // second line that then collided and dropped. A wide max-width keeps each label + // on one line, matching the spec plots. + "text-max-width": 40, "symbol-sort-key": TEXT_SORT_KEY, "text-allow-overlap": false, "text-optional": true, visibility: "visible", From 98acc056d9ad8bfa342f4c3e7f15a70837d0c79f Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 07:22:36 -0400 Subject: [PATCH 07/15] =?UTF-8?q?feat(portrayal):=20SY(INFORM01)=20additio?= =?UTF-8?q?nal-information=20callout=20(S-52=20=C2=A710.6.1.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Objects carrying ancillary info (INFORM/NINFOM, or TXTDSC/NTXTDS/PICREP) now get SY(INFORM01) at their position — a box-on-a-leader "info available" marker. This is what produces the leader-line callouts on the PresLib "Approved new object symbols" V-AIS (page 251), whose SYMINS draws only the ring+topmark; the callout is the §10.6.1.1 indicator, not part of the symbol. addInformSymbol appends it for any feature with a non-empty info attribute (all build paths). The bake routes INFORM01 as display priority 8 / category Other (overriding the host feature's category), so it clears Standard display and only shows when the mariner enables Other — matching the spec. The Chart-1 harness enables Other so the spec render shows the callouts. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/bake/bake.go | 10 +++++- internal/engine/portrayal/inform_test.go | 38 +++++++++++++++++++++ internal/engine/portrayal/s101build.go | 42 +++++++++++++++++++++++- scripts/preslib-chart1.mjs | 3 ++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 internal/engine/portrayal/inform_test.go diff --git a/internal/engine/bake/bake.go b/internal/engine/bake/bake.go index 1468704..d439390 100644 --- a/internal/engine/bake/bake.go +++ b/internal/engine/bake/bake.go @@ -846,7 +846,15 @@ func (b *Baker) addCell(chart *s57.Chart, pc CellPortrayal) { continue } } - b.route(p, class, fb.DisplayPriority, fb.DisplayCategory, zr, zMin, dr.Max, bnd, pts, drval1, drval2, valdco) + // SY(INFORM01) is the S-52 §10.6.1.1 "additional information available" + // marker — always display priority 8, category Other, regardless of the + // host feature's category (so it clears Standard display and only shows + // when the mariner enables Other). + cat, prio := fb.DisplayCategory, fb.DisplayPriority + if sc, ok := p.(portrayal.SymbolCall); ok && sc.SymbolName == "INFORM01" { + cat, prio = displayCatOther, 8 + } + b.route(p, class, prio, cat, zr, zMin, dr.Max, bnd, pts, drval1, drval2, valdco) } } } diff --git a/internal/engine/portrayal/inform_test.go b/internal/engine/portrayal/inform_test.go new file mode 100644 index 0000000..3e10b6f --- /dev/null +++ b/internal/engine/portrayal/inform_test.go @@ -0,0 +1,38 @@ +package portrayal + +import ( + "testing" + + "github.com/beetlebugorg/chartplotter/pkg/s57" +) + +// TestInformSymbolAdditional: an object carrying INFORM gets SY(INFORM01) appended +// (S-52 §10.6.1.1); one without does not. +func TestInformSymbolAdditional(t *testing.T) { + pt := s57.Geometry{Type: s57.GeometryTypePoint, Coordinates: [][]float64{{-5.1, 15.1}}} + countInform := func(fb FeatureBuild) int { + n := 0 + for _, p := range fb.Primitives { + if sc, ok := p.(SymbolCall); ok && sc.SymbolName == "INFORM01" { + n++ + } + } + return n + } + + withInfo := s57.NewFeature(1, "BOYLAT", pt, map[string]any{"INFORM": "lit by night"}) + if got := countInform(addInformSymbol(FeatureBuild{}, &withInfo)); got != 1 { + t.Errorf("INFORM-bearing feature: INFORM01 count = %d, want 1", got) + } + + noInfo := s57.NewFeature(2, "BOYLAT", pt, map[string]any{"COLOUR": "3"}) + if got := countInform(addInformSymbol(FeatureBuild{}, &noInfo)); got != 0 { + t.Errorf("plain feature: INFORM01 count = %d, want 0", got) + } + + // TXTDSC also qualifies (case 2 of §10.6.1.1). + txt := s57.NewFeature(3, "WRECKS", pt, map[string]any{"TXTDSC": "wreck.txt"}) + if got := countInform(addInformSymbol(FeatureBuild{}, &txt)); got != 1 { + t.Errorf("TXTDSC-bearing feature: INFORM01 count = %d, want 1", got) + } +} diff --git a/internal/engine/portrayal/s101build.go b/internal/engine/portrayal/s101build.go index 18f172f..e6860c0 100644 --- a/internal/engine/portrayal/s101build.go +++ b/internal/engine/portrayal/s101build.go @@ -249,8 +249,48 @@ func (b *S101Builder) Build(f *s57.Feature) (FeatureBuild, bool) { return m[f.ID()], true } -// buildFeature turns one feature's emitted instruction stream into its FeatureBuild. +// buildFeature turns one feature's emitted instruction stream into its FeatureBuild, +// then adds the S-52 §10.6.1.1 additional-information indicator when the object +// carries it (see addInformSymbol). func (b *S101Builder) buildFeature(f *s57.Feature, stream string) FeatureBuild { + fb := b.buildFeatureBody(f, stream) + return addInformSymbol(fb, f) +} + +// addInformSymbol appends SY(INFORM01) at the object's position when it carries +// additional information (INFORM/NINFOM, or TXTDSC/NTXTDS/PICREP) — S-52 §10.6.1.1. +// INFORM01 is a box-on-a-leader "info available" marker; it's baked display-category +// Other (the bake routes it so, overriding the host feature's category), so it +// clears Standard display and only shows when the mariner enables Other. The pivot +// goes at a point's position / a line's midpoint / an area's centre. +func addInformSymbol(fb FeatureBuild, f *s57.Feature) FeatureBuild { + if !hasAdditionalInfo(f.Attributes()) { + return fb + } + anchor, ok := representativePoint(f) + if !ok { + return fb + } + fb.Primitives = append(fb.Primitives, SymbolCall{ + Anchor: anchor, SymbolName: "INFORM01", Scale: DefaultPxPerSymbolUnit, + SoundingDepthM: nan32, DangerDepthM: nan32, + }) + return fb +} + +// hasAdditionalInfo reports whether an object carries S-52 §10.6.1.1 ancillary +// information (a non-empty INFORM/NINFOM/TXTDSC/NTXTDS/PICREP attribute). +func hasAdditionalInfo(attrs map[string]any) bool { + for _, k := range [...]string{"INFORM", "NINFOM", "TXTDSC", "NTXTDS", "PICREP"} { + if s, _ := attrs[k].(string); strings.TrimSpace(s) != "" { + return true + } + } + return false +} + +// buildFeatureBody turns one feature's emitted instruction stream into its FeatureBuild. +func (b *S101Builder) buildFeatureBody(f *s57.Feature, stream string) FeatureBuild { // NEWOBJ with a SYMINS attribute: portray the producer's explicit symbol // instruction (S-52 SYMINS02) rather than the S-101 V-AIS alias the engine // emitted — SYMINS carries the real symbols, TX/TE labels, boundaries and fills diff --git a/scripts/preslib-chart1.mjs b/scripts/preslib-chart1.mjs index 67e272b..ad93092 100644 --- a/scripts/preslib-chart1.mjs +++ b/scripts/preslib-chart1.mjs @@ -77,6 +77,9 @@ for (const p of PANELS) { localStorage.setItem("chartplotter:scheme", a.scheme); localStorage.setItem("chartplotter:basemap", "coastline"); localStorage.setItem("chartplotter:enc-agreement", "1"); + // The reference plots are all-categories-on, so enable the "Other" display + // category — that's where the SY(INFORM01) additional-info callouts live (§10.6.1.1). + localStorage.setItem("chartplotter:mariner", JSON.stringify({ displayOther: true })); localStorage.setItem("chartplotter:view", JSON.stringify({ center: a.center, zoom: a.zoom })); }, { scheme: p.scheme, center, zoom }); try { await page.goto(baseURL + "/?prod&spec", { waitUntil: "domcontentloaded", timeout: 45000 }); } From d1c675405bc749d23c1c9aa194e2b5d4a1d8d743 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 07:27:32 -0400 Subject: [PATCH 08/15] test(preslib): set explicit reference mariner settings in the Chart-1 harness The IHO PresLib reference plots show ALL symbology, so set the harness mariner explicitly rather than relying on defaults: display category Other on (INFORM01 callouts, "other marks", magnetic variation), data-quality overlay on (the CATZOC "quality of data" panels), depths in metres (IHO, not NOAA feet), 25 mm short sector legs + symbolized boundaries (the S-52 defaults the reference uses). Makes the spec render deterministic and match the reference (e.g. page 243 depths now read 5.5m/4m and the quality-of-data zones appear). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/preslib-chart1.mjs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/preslib-chart1.mjs b/scripts/preslib-chart1.mjs index ad93092..763cbf6 100644 --- a/scripts/preslib-chart1.mjs +++ b/scripts/preslib-chart1.mjs @@ -57,6 +57,21 @@ const PANELS = [ { page: 253, slug: "colourtest-WOO-dusk", b: [-5.0331, 15.0362, -5.0026, 15.0657], cscl: HARBOR, scheme: "dusk" }, ]; +// Mariner display state matching the IHO PresLib reference plots: ALL symbology +// shown. Display category Other on (INFORM01 callouts, "other marks", magnetic +// variation…); data-quality overlay on (the CATZOC "quality of data" panels); +// metres (IHO depths, not NOAA feet); 25 mm short sector legs and symbolized +// boundaries (both the S-52 defaults the reference uses). Everything else +// (soundings, text groups, light descriptions) is default-on. +const MARINER = { + displayBase: true, displayStandard: true, displayOther: true, + dataQuality: true, + depthUnit: "m", + showFullSectorLines: false, + boundaryStyle: "symbolized", + simplifiedPoints: false, +}; + mkdirSync(outDir, { recursive: true }); const { chromium } = findPlaywright(); const browser = await chromium.launch({ executablePath: findChromium(), args: ["--no-sandbox", "--hide-scrollbars"] }); @@ -77,11 +92,9 @@ for (const p of PANELS) { localStorage.setItem("chartplotter:scheme", a.scheme); localStorage.setItem("chartplotter:basemap", "coastline"); localStorage.setItem("chartplotter:enc-agreement", "1"); - // The reference plots are all-categories-on, so enable the "Other" display - // category — that's where the SY(INFORM01) additional-info callouts live (§10.6.1.1). - localStorage.setItem("chartplotter:mariner", JSON.stringify({ displayOther: true })); + localStorage.setItem("chartplotter:mariner", JSON.stringify(a.mariner)); localStorage.setItem("chartplotter:view", JSON.stringify({ center: a.center, zoom: a.zoom })); - }, { scheme: p.scheme, center, zoom }); + }, { scheme: p.scheme, center, zoom, mariner: MARINER }); try { await page.goto(baseURL + "/?prod&spec", { waitUntil: "domcontentloaded", timeout: 45000 }); } catch (err) { console.error(`[page ${p.page}] nav: ${err.message} — continuing`); } await page.waitForTimeout(+settle); From 56e095dd2209def5a0a84af4ca9d9da34ebf21c1 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 07:34:59 -0400 Subject: [PATCH 09/15] feat(web): label installed-chart coverage boxes (find charts on a blank map) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the basemap off, a remote installed cell was just a faint outline box you had to spot by eye. Add a cell-name label at each coverage box's centroid, capped at the band's render zoom (like the fill/line) so it vanishes once the chart itself draws — visible when you're zoomed out and lost, gone when you're there. Decluttered (drops on overlap). Tap-to-fly-in already worked. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/plugins/coverage-boxes.mjs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/web/src/plugins/coverage-boxes.mjs b/web/src/plugins/coverage-boxes.mjs index bdc8b77..549ae13 100644 --- a/web/src/plugins/coverage-boxes.mjs +++ b/web/src/plugins/coverage-boxes.mjs @@ -44,6 +44,23 @@ export class CoverageBoxes { const f = ["==", ["get", "band"], band]; if (!map.getLayer(`inst-fill-${band}`)) map.addLayer({ id: `inst-fill-${band}`, type: "fill", source: "inst-bounds", maxzoom: mz, filter: f, layout: { visibility: vis }, paint: { "fill-color": BAND_COLOR[band], "fill-opacity": 0.06 } }); if (!map.getLayer(`inst-line-${band}`)) map.addLayer({ id: `inst-line-${band}`, type: "line", source: "inst-bounds", maxzoom: mz, filter: f, layout: { visibility: vis }, paint: { "line-color": BAND_COLOR[band], "line-width": 1.1, "line-opacity": 0.85 } }); + // Cell-name label at the footprint centroid, so on a blank (no-basemap) map you + // can SEE which chart is where (and tap it). Capped at the band's render zoom + // (same as the fill/line) so it vanishes once the chart itself draws — not + // obtrusive when you're already there. Decluttered (drops on overlap). + if (!map.getLayer(`inst-label-${band}`)) map.addLayer({ id: `inst-label-${band}`, type: "symbol", source: "inst-bounds", maxzoom: mz, filter: f, layout: { + visibility: vis, + "symbol-placement": "point", + "text-field": ["coalesce", ["get", "name"], ""], + "text-font": ["Noto Sans Regular"], + "text-size": 12, + "text-allow-overlap": false, + "text-optional": true, + }, paint: { + "text-color": BAND_COLOR[band], + "text-halo-color": "#ffffff", + "text-halo-width": 1.8, + } }); } // Always-on footprint outline (NOT maxzoom-capped): when SCAMIN suppresses every // feature in a cell the tiles render blank, so keep a thin dashed outline at ALL @@ -71,7 +88,7 @@ export class CoverageBoxes { setVisible(on) { this._visible = !!on; const map = this.map, vis = on ? "visible" : "none"; - for (const band of BANDS) for (const pre of ["inst-fill-", "inst-line-"]) if (map.getLayer(pre + band)) map.setLayoutProperty(pre + band, "visibility", vis); + for (const band of BANDS) for (const pre of ["inst-fill-", "inst-line-", "inst-label-"]) if (map.getLayer(pre + band)) map.setLayoutProperty(pre + band, "visibility", vis); if (map.getLayer("inst-outline")) map.setLayoutProperty("inst-outline", "visibility", vis); } From 4a9d24cfb0b3b0f62a3b5ec58cfd2a9e3bae7993 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 08:11:45 -0400 Subject: [PATCH 10/15] =?UTF-8?q?feat(search):=20find=20installed=20cells?= =?UTF-8?q?=20by=20name=20=E2=80=94=20index=20+=20/api/cells=3Factive=20+?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finding an installed cell on a zoomed-out blank map didn't scale: coverage boxes are per-pack (a single global box over a 7k-cell "import" set) and per-cell would freeze the map. The scalable answer is search-by-name → fly-to. Server: a small persistent name→bbox index over the cached cells (/cells-index.json), backfilled once in the background by reading each cell's header (baker.ParseCellBytes bounds), refreshed after import. GET /api/cells now also returns a "bbox" map and accepts ?active=1 — only cells whose footprint overlaps an ENABLED pack (charts actually on the map), and only those indexed (an un-indexed cell has no footprint to fly to). No cell→pack table needed; overlap with enabled-pack bounds defines "active". Client: the search box's catalog is now the ACTIVE installed cells (name+bbox from /api/cells?active=1), so typing a cell name finds it and flies to its footprint — verified flying to AA5C1CDE on a blank map. (Search targets active charts, not the NOAA discovery catalogue.) Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/server/cellindex.go | 136 +++++++++++++++++++++++ internal/engine/server/cellindex_test.go | 55 +++++++++ internal/engine/server/http.go | 4 +- internal/engine/server/import.go | 3 + internal/engine/server/tilesets.go | 63 +++++++++-- web/src/chartplotter.mjs | 6 +- web/src/data/chart-service.mjs | 11 ++ web/src/plugins/search-box.mjs | 2 +- 8 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 internal/engine/server/cellindex.go create mode 100644 internal/engine/server/cellindex_test.go diff --git a/internal/engine/server/cellindex.go b/internal/engine/server/cellindex.go new file mode 100644 index 0000000..6b3e5de --- /dev/null +++ b/internal/engine/server/cellindex.go @@ -0,0 +1,136 @@ +package server + +import ( + "encoding/json" + "log" + "os" + "path/filepath" + "sort" + "sync" + + "github.com/beetlebugorg/chartplotter/internal/engine/baker" +) + +// cellIndex is a small, persistent name→bounding-box index over the cached source +// cells (/ENC_ROOT//.000). It lets the server answer "where +// is cell X" and "which installed cells are active" without re-parsing thousands +// of cells on every request: each cell's header is read ONCE (the bbox cached to +// /cells-index.json), then queries hit the in-memory map. Kept +// deliberately simple — a flat JSON map, not a database; the data is tiny (a few +// floats per cell) and read-mostly. +type cellIndex struct { + mu sync.RWMutex + bbox map[string][4]float64 // cell stem → [W,S,E,N] + path string // cells-index.json + encRoot string // /ENC_ROOT + built bool // backfill scan finished +} + +func newCellIndex(dataDir string) *cellIndex { + ci := &cellIndex{ + bbox: map[string][4]float64{}, + path: filepath.Join(dataDir, "cells-index.json"), + encRoot: filepath.Join(dataDir, "ENC_ROOT"), + } + if data, err := os.ReadFile(ci.path); err == nil { + _ = json.Unmarshal(data, &ci.bbox) + } + return ci +} + +// get returns a cell's [W,S,E,N] bounds if indexed. +func (ci *cellIndex) get(name string) ([4]float64, bool) { + ci.mu.RLock() + defer ci.mu.RUnlock() + b, ok := ci.bbox[name] + return b, ok +} + +// snapshot returns a copy of the current index (sorted names + their bboxes). +func (ci *cellIndex) snapshot() ([]string, map[string][4]float64) { + ci.mu.RLock() + defer ci.mu.RUnlock() + names := make([]string, 0, len(ci.bbox)) + out := make(map[string][4]float64, len(ci.bbox)) + for n, b := range ci.bbox { + names = append(names, n) + out[n] = b + } + sort.Strings(names) + return names, out +} + +func (ci *cellIndex) save() { + ci.mu.RLock() + data, err := json.Marshal(ci.bbox) + ci.mu.RUnlock() + if err != nil { + return + } + tmp := ci.path + ".tmp" + if err := os.WriteFile(tmp, data, 0o644); err != nil { + return + } + _ = os.Rename(tmp, ci.path) +} + +// rebuild re-opens the backfill (e.g. after an import added new cached cells) and +// indexes any not already present. Run in a goroutine. +func (ci *cellIndex) rebuild() { + ci.mu.Lock() + ci.built = false + ci.mu.Unlock() + ci.build() +} + +// build backfills the index by reading every cached cell's header once. Runs in a +// background goroutine (started once) so it never blocks a request; queries see +// the index grow as it fills, and it's a no-op after the first complete pass. +func (ci *cellIndex) build() { + ci.mu.Lock() + if ci.built { + ci.mu.Unlock() + return + } + ci.built = true // claim the build; reset only if the scan can't start + ci.mu.Unlock() + + entries, err := os.ReadDir(ci.encRoot) + if err != nil { + ci.mu.Lock() + ci.built = false + ci.mu.Unlock() + return + } + n, added := 0, 0 + for _, e := range entries { + if !e.IsDir() || !isCellName(e.Name()) { + continue + } + name := e.Name() + if _, ok := ci.get(name); ok { + continue // already indexed + } + data, err := os.ReadFile(filepath.Join(ci.encRoot, name, name+".000")) + if err != nil { + continue + } + chart, err := baker.ParseCellBytes(name, data) + if err != nil { + continue + } + b := chart.Bounds() + ci.mu.Lock() + ci.bbox[name] = [4]float64{b.MinLon, b.MinLat, b.MaxLon, b.MaxLat} + ci.mu.Unlock() + added++ + if added%200 == 0 { + ci.save() // periodic checkpoint for a long backfill + } + n++ + } + if added > 0 { + ci.save() + log.Printf("cell index: backfilled %d cell bound(s) → %s", added, ci.path) + } +} diff --git a/internal/engine/server/cellindex_test.go b/internal/engine/server/cellindex_test.go new file mode 100644 index 0000000..48063cd --- /dev/null +++ b/internal/engine/server/cellindex_test.go @@ -0,0 +1,55 @@ +package server + +import ( + "os" + "path/filepath" + "testing" +) + +// TestBBoxOverlapsAny — the ?active overlap test. +func TestBBoxOverlapsAny(t *testing.T) { + world := [4]float64{-180, -90, 180, 90} + cell := [4]float64{-5.13, 15.0, -5.0, 15.13} + if !bboxOverlapsAny(cell, [][4]float64{world}) { + t.Error("cell should overlap the world pack") + } + far := [4]float64{100, -40, 120, -20} + if bboxOverlapsAny(cell, [][4]float64{far}) { + t.Error("cell should NOT overlap a disjoint pack") + } + if bboxOverlapsAny(cell, nil) { + t.Error("no packs ⇒ not active") + } +} + +// TestCellIndexBuild — backfill reads a cached cell's header once and records its +// bounds; a reload from disk sees the same. +func TestCellIndexBuild(t *testing.T) { + const cell = "US4MD81M" + data, err := os.ReadFile("../../../testdata/" + cell + ".000") + if err != nil { + t.Skipf("testdata cell absent: %v", err) + } + dir := t.TempDir() + cdir := filepath.Join(dir, "ENC_ROOT", cell) + if err := os.MkdirAll(cdir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(cdir, cell+".000"), data, 0o644); err != nil { + t.Fatal(err) + } + + ci := newCellIndex(dir) + ci.build() + bb, ok := ci.get(cell) + if !ok { + t.Fatal("cell not indexed after build") + } + if !(bb[0] < bb[2] && bb[1] < bb[3]) { + t.Errorf("degenerate bounds %v", bb) + } + // Persisted: a fresh index loads the same bounds without re-parsing. + if bb2, ok := newCellIndex(dir).get(cell); !ok || bb2 != bb { + t.Errorf("reload mismatch: %v vs %v (ok=%v)", bb2, bb, ok) + } +} diff --git a/internal/engine/server/http.go b/internal/engine/server/http.go index 641c2e8..04336d1 100644 --- a/internal/engine/server/http.go +++ b/internal/engine/server/http.go @@ -45,6 +45,7 @@ type Server struct { packs map[string]string // ALL baked packs on disk: set name → pmtiles path prefs *prefs // persisted enable/disable state (/prefs.json) auxIdx *auxIndex // index of companion aux.zips for /api/aux (TXTDSC/PICREP) + cellIdx *cellIndex // persistent name→bbox index over cached cells (/api/cells, search fly-to) vessel *nmea.Store // latest NMEA0183 vessel state (fed by nmeaMgr) nmeaMgr *nmea.Manager // live NMEA0183 connections (writes into vessel) @@ -63,7 +64,8 @@ func New(assetsDir, cacheDir, dataDir string, allowRemote bool) *Server { if dataDir == "" { dataDir = cacheDir } - s := &Server{assetsDir: assetsDir, cacheDir: cacheDir, dataDir: dataDir, allowRemote: allowRemote, sets: newTileSets(), imports: newImportJobs(), auxIdx: newAuxIndex()} + s := &Server{assetsDir: assetsDir, cacheDir: cacheDir, dataDir: dataDir, allowRemote: allowRemote, sets: newTileSets(), imports: newImportJobs(), auxIdx: newAuxIndex(), cellIdx: newCellIndex(dataDir)} + go s.cellIdx.build() // backfill cell bounds once, in the background (never blocks a request) // Discover every baked pack on disk (provider trees + flat tiles/), then // register the ENABLED ones (disabled packs stay on disk but off the map). State // lives in /prefs.json so it survives restarts and is shared across clients. diff --git a/internal/engine/server/import.go b/internal/engine/server/import.go index 54d6925..b211e01 100644 --- a/internal/engine/server/import.go +++ b/internal/engine/server/import.go @@ -329,6 +329,9 @@ func (s *Server) cacheCells(cells map[string]baker.CellData) { _ = os.WriteFile(filepath.Join(dir, filepath.Base(un)), ub, 0o644) } } + if s.cellIdx != nil { + go s.cellIdx.rebuild() // index the newly-cached cells' bounds in the background + } } // filterCells keeps only the cells whose stem (name sans .000) is in names. diff --git a/internal/engine/server/tilesets.go b/internal/engine/server/tilesets.go index 59c127a..0256cd1 100644 --- a/internal/engine/server/tilesets.go +++ b/internal/engine/server/tilesets.go @@ -1,6 +1,7 @@ package server import ( + "encoding/json" "fmt" "io" "math" @@ -289,22 +290,68 @@ func (s *Server) handleSetEnabled(w http.ResponseWriter, r *http.Request) { // serveCells returns the names of cells currently in the server's ENC_ROOT source // store. The client uses this so its installed-set (and the persisted baked sets) // survive a page reload — the cells live server-side in the XDG data dir. +// serveCells returns the installed source cells. The "cells" array is every +// cached cell name (back-compat: the installed list). "bbox" maps each INDEXED +// cell to its [W,S,E,N] footprint (fills in as the background index backfills), +// so the client can search a cell by name and fly to it. With ?active=1 the +// result is restricted to cells whose footprint overlaps an ENABLED pack — i.e. +// charts actually on the map right now (and only those that are indexed, since an +// un-indexed cell has no footprint to test or fly to). func (s *Server) serveCells(w http.ResponseWriter, r *http.Request) { + active := r.URL.Query().Get("active") == "1" + var enabled [][4]float64 + if active { + enabled = s.enabledPackBounds() + } + _, idx := s.cellIdx.snapshot() entries, _ := os.ReadDir(filepath.Join(s.dataDir, "ENC_ROOT")) names := make([]string, 0, len(entries)) + boxes := make(map[string][4]float64) for _, e := range entries { - if e.IsDir() && isCellName(e.Name()) { - names = append(names, e.Name()) + if !e.IsDir() || !isCellName(e.Name()) { + continue + } + n := e.Name() + box, has := idx[n] + if active && (!has || !bboxOverlapsAny(box, enabled)) { + continue + } + names = append(names, n) + if has { + boxes[n] = box } } sort.Strings(names) w.Header().Set("Content-Type", jsonCT) - fmt.Fprint(w, `{"cells":[`) - for i, n := range names { - if i > 0 { - fmt.Fprint(w, ",") + _ = json.NewEncoder(w).Encode(struct { + Cells []string `json:"cells"` + BBox map[string][4]float64 `json:"bbox"` + }{names, boxes}) +} + +// enabledPackBounds is each enabled pack's [W,S,E,N] (read from its archive), +// used by the ?active filter to test which cells are currently on the map. +func (s *Server) enabledPackBounds() [][4]float64 { + var out [][4]float64 + for _, name := range sortedKeys(s.packs) { + if s.prefs.isDisabled(name) { + continue + } + if src, err := tilesource.Open(s.packs[name]); err == nil { + m := src.Meta() + _ = tilesource.Close(src) + out = append(out, [4]float64{m.W, m.S, m.E, m.N}) } - fmt.Fprintf(w, "%q", n) } - fmt.Fprint(w, "]}") + return out +} + +// bboxOverlapsAny reports whether [W,S,E,N] box intersects any of the rects. +func bboxOverlapsAny(b [4]float64, rects [][4]float64) bool { + for _, r := range rects { + if b[0] <= r[2] && b[2] >= r[0] && b[1] <= r[3] && b[3] >= r[1] { + return true + } + } + return false } diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs index 915c1b2..d33747b 100644 --- a/web/src/chartplotter.mjs +++ b/web/src/chartplotter.mjs @@ -205,6 +205,7 @@ export class ChartPlotter extends HTMLElement { // NOAA catalogue/discovery lives in this._dl (ChartDownloader, created in // boot); _catalog/_byName/_districts/_catalogDate are proxy getters onto it. this._installed = new Set(); // all stored cell names + this._activeCells = []; // active (enabled-pack) cells {n,l,bb} — the search catalog this._cellError = new Map(); // name -> error message, for cells that failed to parse this._cellBounds = new Map(); // name -> [w,s,e,n] footprint (from the baker), to locate uploaded cells this._cellScale = new Map(); // name -> compilation scale (CSCL) of uploaded cells, for picking a detail zoom @@ -1481,6 +1482,9 @@ export class ChartPlotter extends HTMLElement { if (this._aux && !this._dl.auxUrl) this._aux.loadApi(this._assets).catch(() => {}); const cells = await this._api.cells(); if (cells) this._installed = cells; // null → keep current view + // Active (enabled-pack) cells WITH bounds — the search catalog, so you can find + // an installed chart by name and fly to it (esp. on a blank/no-basemap map). + this._activeCells = await this._api.activeCells(); // Management keys on the DISTRICT name (noaa-d5); enable/disable/remove hit the // district and the server fans to its band-sets. this._installedSets = new Set(packs.map((p) => p.name)); @@ -1845,7 +1849,7 @@ export class ChartPlotter extends HTMLElement { getInput: () => $("search-input"), getSearchPop: () => $("search"), getSearchTab: () => $("search-tab"), - getCatalog: () => this._catalog, + getCatalog: () => this._activeCells || [], // search ACTIVE installed charts (name → fly to footprint) isChartSource, classLabel: (acr) => S57_CLASS[acr], layerLabel: (srcLayer) => INSPECT_LAYER_LABEL[srcLayer], diff --git a/web/src/data/chart-service.mjs b/web/src/data/chart-service.mjs index b2f7006..5984699 100644 --- a/web/src/data/chart-service.mjs +++ b/web/src/data/chart-service.mjs @@ -148,6 +148,17 @@ export class ChartService { catch (e) { return null; } } + // GET /api/cells?active=1 — the ACTIVE (enabled-pack) cells that are indexed, + // as search-catalog entries {n,l,bb}: so a cell can be found by name and flown + // to its footprint. Only cells with known bounds (indexed) are returned. + async activeCells() { + try { + const j = await fetch(this._url("api/cells?active=1")).then((r) => (r.ok ? r.json() : null)); + const bb = (j && j.bbox) || {}; + return Object.keys(bb).map((n) => ({ n, l: n, bb: bb[n] })); + } catch (e) { return []; } + } + // POST /api/set/{enable,disable} — toggle a pack's rendering (data is kept). async setEnabled(set, on) { await fetch(this._url(`api/set/${on ? "enable" : "disable"}?set=${encodeURIComponent(set)}`), { method: "POST" }); diff --git a/web/src/plugins/search-box.mjs b/web/src/plugins/search-box.mjs index dab3d7f..8fb82f1 100644 --- a/web/src/plugins/search-box.mjs +++ b/web/src/plugins/search-box.mjs @@ -58,7 +58,7 @@ export class SearchBox { el.innerHTML = hits.length ? hits.map((h, i) => { const sel = i === 0 ? " sel" : ""; - if (h.type === "cell") return `
${esc(h.c.l || h.c.n)}
Chart · ${esc(h.c.n)} · 1:${(h.c.s || 0).toLocaleString()}
`; + if (h.type === "cell") { const sub = h.c.s ? `Chart · ${esc(h.c.n)} · 1:${h.c.s.toLocaleString()}` : `Chart · ${esc(h.c.n)}`; return `
${esc(h.c.l || h.c.n)}
${sub}
`; } return `
${esc(h.label)}
${esc(h.sub)}
`; }).join("") : `
No matches in view
`; From 2ba0cd707aaf4ab8d4991be052b951133d9572aa Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 08:20:21 -0400 Subject: [PATCH 11/15] feat(search): browse active charts when the box is empty (no need to know a name) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search-by-name only helps if you know the name. Opening search with nothing typed now lists the active installed charts — ordered NEAREST to the current view first (most relevant to where you're looking), capped at 40 with a "N charts — type to narrow" footer. Triggered on open, so you see your charts immediately and can click one to fly there, or type to filter. Verified: empty search lists the 14 PresLib cells, nearest-first. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/chartplotter.mjs | 2 +- web/src/plugins/search-box.mjs | 46 +++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs index d33747b..07d4c01 100644 --- a/web/src/chartplotter.mjs +++ b/web/src/chartplotter.mjs @@ -1856,7 +1856,7 @@ export class ChartPlotter extends HTMLElement { positionCaret: (pop, tab) => this._positionCaret(pop, tab), }); const closeSearch = () => { $("search").hidden = true; $("search-tab").classList.remove("on"); }; - const openSearch = () => { $("search").hidden = false; $("search-tab").classList.add("on"); this._search.position(); si.focus(); }; + const openSearch = () => { $("search").hidden = false; $("search-tab").classList.add("on"); this._search.position(); si.focus(); this._search.doSearch(si.value); /* show the browse list (active charts) right away */ }; $("search-tab").onclick = () => ($("search").hidden ? openSearch() : closeSearch()); si.oninput = () => this._search.doSearch(si.value); si.onkeydown = (e) => { diff --git a/web/src/plugins/search-box.mjs b/web/src/plugins/search-box.mjs index 8fb82f1..6a48f42 100644 --- a/web/src/plugins/search-box.mjs +++ b/web/src/plugins/search-box.mjs @@ -41,27 +41,45 @@ export class SearchBox { const el = this._getResultsEl(); if (!el) return; const needle = q.trim().toLowerCase(); - if (needle.length < 2) { el.hidden = true; el.innerHTML = ""; this._hits = []; this.position(); return; } - // 1) Catalog cells (chart titles / numbers), fuzzy-matched. Best score wins; - // ties break to the coarser chart (overview before an arbitrary harbour inset). + // BROWSE mode: nothing typed yet (or too short to fuzzy-match). Instead of a + // blank box, list the active charts — so you can find one without knowing its + // name. Ordered NEAREST-to-view first (most relevant to where you're looking), + // capped, with a "type to narrow" footer when there are more. + const browse = needle.length < 2; + const BROWSE_LIMIT = 40; + let center = null; + try { const c = this._getMap().getCenter(); center = [c.lng, c.lat]; } catch {} + + // 1) Catalog cells (active installed charts), fuzzy-matched — or all, in browse. const cells = []; for (const c of this._getCatalog()) { if (!Array.isArray(c.bb) || c.bb.length !== 4) continue; + if (browse) { cells.push({ c, score: 0 }); continue; } const score = Math.max(fuzzyScore(needle, (c.l || "").toLowerCase()), fuzzyScore(needle, c.n.toLowerCase())); if (score >= 0) cells.push({ c, score }); } - cells.sort((a, b) => (b.score - a.score) || ((b.c.s || 0) - (a.c.s || 0))); - // 2) Every loaded chart feature, fuzzy-matched across its attribute data. - const feats = this._searchFeatures(needle); - const hits = [...cells.slice(0, 5).map(({ c }) => ({ type: "cell", c })), ...feats.slice(0, 8)]; + if (browse) { + const d2 = (c) => center ? ((c.bb[0] + c.bb[2]) / 2 - center[0]) ** 2 + ((c.bb[1] + c.bb[3]) / 2 - center[1]) ** 2 : 0; + cells.sort((a, b) => (d2(a.c) - d2(b.c)) || (a.c.n < b.c.n ? -1 : 1)); // nearest first, then by name + } else { + cells.sort((a, b) => (b.score - a.score) || ((b.c.s || 0) - (a.c.s || 0))); // best match; ties → coarser chart + } + // 2) Loaded chart features (skip in browse — there's no query to match). + const feats = browse ? [] : this._searchFeatures(needle); + const more = browse && cells.length > BROWSE_LIMIT; + const hits = [...cells.slice(0, browse ? BROWSE_LIMIT : 5).map(({ c }) => ({ type: "cell", c })), ...feats.slice(0, 8)]; this._hits = hits; - el.innerHTML = hits.length - ? hits.map((h, i) => { - const sel = i === 0 ? " sel" : ""; - if (h.type === "cell") { const sub = h.c.s ? `Chart · ${esc(h.c.n)} · 1:${h.c.s.toLocaleString()}` : `Chart · ${esc(h.c.n)}`; return `
${esc(h.c.l || h.c.n)}
${sub}
`; } - return `
${esc(h.label)}
${esc(h.sub)}
`; - }).join("") - : `
No matches in view
`; + const rows = hits.map((h, i) => { + const sel = i === 0 ? " sel" : ""; + if (h.type === "cell") { const sub = h.c.s ? `Chart · ${esc(h.c.n)} · 1:${h.c.s.toLocaleString()}` : `Chart · ${esc(h.c.n)}`; return `
${esc(h.c.l || h.c.n)}
${sub}
`; } + return `
${esc(h.label)}
${esc(h.sub)}
`; + }); + if (hits.length) { + if (more) rows.push(`
${cells.length} charts — type to narrow
`); + el.innerHTML = rows.join(""); + } else { + el.innerHTML = `
${browse ? "No installed charts" : "No matches in view"}
`; + } el.hidden = false; el.querySelectorAll(".sr-item[data-i]").forEach((d) => (d.onmousedown = (e) => { e.preventDefault(); this.gotoHit(+d.dataset.i); })); this.position(); // re-align to the search tab as the result count changes the height From d450a8a17384a1d2aee7b32cdcd4ce93a66055e1 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 08:29:03 -0400 Subject: [PATCH 12/15] fix(server): keep the cell index fresh on add / update / remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The name→bbox index could drift: build() skipped already-indexed cells (so a re-imported cell kept stale bounds) and never dropped entries for removed cells. Now build() reconciles against ENC_ROOT — pruning entries whose cell is no longer on disk — and an import forget()s the cells it re-caches so the rebuild re-parses their bounds. (Search results were already remove-safe: serveCells lists live ENC_ROOT dirs, looking up bbox in the index, so a removed cell never appears even before the prune.) Add/update/remove all covered; verified by test. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/server/cellindex.go | 32 ++++++++++++++++--- internal/engine/server/cellindex_test.go | 40 ++++++++++++++++++++++++ internal/engine/server/import.go | 7 ++++- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/internal/engine/server/cellindex.go b/internal/engine/server/cellindex.go index 6b3e5de..2237c75 100644 --- a/internal/engine/server/cellindex.go +++ b/internal/engine/server/cellindex.go @@ -102,14 +102,16 @@ func (ci *cellIndex) build() { ci.mu.Unlock() return } - n, added := 0, 0 + present := make(map[string]bool, len(entries)) + added := 0 for _, e := range entries { if !e.IsDir() || !isCellName(e.Name()) { continue } name := e.Name() + present[name] = true if _, ok := ci.get(name); ok { - continue // already indexed + continue // already indexed (forget() drops a re-imported cell so it re-parses) } data, err := os.ReadFile(filepath.Join(ci.encRoot, name, name+".000")) if err != nil { @@ -127,10 +129,30 @@ func (ci *cellIndex) build() { if added%200 == 0 { ci.save() // periodic checkpoint for a long backfill } - n++ } - if added > 0 { + // Reconcile: drop entries for cells no longer on disk (removed packs/cells), so + // the index never reports a chart that isn't installed anymore. + removed := 0 + ci.mu.Lock() + for name := range ci.bbox { + if !present[name] { + delete(ci.bbox, name) + removed++ + } + } + ci.mu.Unlock() + if added > 0 || removed > 0 { ci.save() - log.Printf("cell index: backfilled %d cell bound(s) → %s", added, ci.path) + log.Printf("cell index: +%d / -%d cell bound(s) → %s", added, removed, ci.path) } } + +// forget drops cells from the index so the next build re-parses them — used when +// an import re-caches a cell whose bounds may have changed. +func (ci *cellIndex) forget(names []string) { + ci.mu.Lock() + for _, n := range names { + delete(ci.bbox, n) + } + ci.mu.Unlock() +} diff --git a/internal/engine/server/cellindex_test.go b/internal/engine/server/cellindex_test.go index 48063cd..38e98c4 100644 --- a/internal/engine/server/cellindex_test.go +++ b/internal/engine/server/cellindex_test.go @@ -53,3 +53,43 @@ func TestCellIndexBuild(t *testing.T) { t.Errorf("reload mismatch: %v vs %v (ok=%v)", bb2, bb, ok) } } + +// TestCellIndexFreshness: rebuild prunes a removed cell, and forget() drops one so +// it re-indexes — the add/update/remove freshness contract. +func TestCellIndexFreshness(t *testing.T) { + const cell = "US4MD81M" + data, err := os.ReadFile("../../../testdata/" + cell + ".000") + if err != nil { + t.Skipf("testdata cell absent: %v", err) + } + dir := t.TempDir() + cdir := filepath.Join(dir, "ENC_ROOT", cell) + if err := os.MkdirAll(cdir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(cdir, cell+".000"), data, 0o644); err != nil { + t.Fatal(err) + } + ci := newCellIndex(dir) + ci.build() + if _, ok := ci.get(cell); !ok { + t.Fatal("not indexed") + } + // forget → re-build re-parses it (update path). + ci.forget([]string{cell}) + if _, ok := ci.get(cell); ok { + t.Fatal("forget did not drop the entry") + } + ci.rebuild() + if _, ok := ci.get(cell); !ok { + t.Fatal("rebuild did not re-index after forget") + } + // remove the cell on disk → rebuild prunes it (remove path). + if err := os.RemoveAll(cdir); err != nil { + t.Fatal(err) + } + ci.rebuild() + if _, ok := ci.get(cell); ok { + t.Error("rebuild did not prune a removed cell") + } +} diff --git a/internal/engine/server/import.go b/internal/engine/server/import.go index b211e01..39334cf 100644 --- a/internal/engine/server/import.go +++ b/internal/engine/server/import.go @@ -330,7 +330,12 @@ func (s *Server) cacheCells(cells map[string]baker.CellData) { } } if s.cellIdx != nil { - go s.cellIdx.rebuild() // index the newly-cached cells' bounds in the background + stems := make([]string, 0, len(cells)) + for name := range cells { + stems = append(stems, strings.TrimSuffix(name, ".000")) + } + s.cellIdx.forget(stems) // re-imported cells: drop stale bounds so the rebuild re-parses + go s.cellIdx.rebuild() // index the (re-)cached cells' bounds in the background } } From a0b64563fe6b051f68385d9b401c47b5d20395f8 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 08:53:24 -0400 Subject: [PATCH 13/15] test(s64): render harness for the S-64 ENC test pages (sibling of preslib-chart1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make s64-pages` (scripts/s64-pages.sh + .mjs) extracts the S-64 ENC TDS, serves + imports it, and renders each rendering test in spec mode at the cell's compilation scale → testdata/s64-pages-out/ (gitignored), for diffing against the S-64 reference plots. Unlike PresLib Chart 1 (one all-symbology plot), S-64 varies the mariner per page — §3.1 renders the same area at Base / Standard / Other — and uses the S-64 setup (safety contour/depth 10 m, symbolized boundaries, metres). Covers §3.1, 3.2, 3.3, 3.4, 3.6, 3.7/3.7.7, 2.1.1, 5/6/7. §3.9 Polar (beyond Web-Mercator) and §3.8.5 AML (non-ENC names) are omitted with a note. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + Makefile | 10 +++- scripts/s64-pages.mjs | 106 ++++++++++++++++++++++++++++++++++++++++++ scripts/s64-pages.sh | 50 ++++++++++++++++++++ 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 scripts/s64-pages.mjs create mode 100755 scripts/s64-pages.sh diff --git a/.gitignore b/.gitignore index 11b8a17..02775f6 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ web/patterns.json web/patterns.png web/sprite.json web/sprite.png +/testdata/s64-pages-out/ diff --git a/Makefile b/Makefile index da41bc1..eac9ae7 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ S101_PC ?= $(HOME)/Projects/s101-portrayal-catalogue/PortrayalCatalog S101_FC ?= $(HOME)/Projects/s101-feature-catalogue/S-101FC/FeatureCatalogue.xml S101_CACHE ?= $(CACHE)/s101 -.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo preslib-chart1 +.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo preslib-chart1 s64-pages # Prebaked prod test set (US Inland ENC bundle + the NOAA world archive). # NB: keep these as bare values with NO inline `#` comments — Make folds any @@ -213,6 +213,14 @@ docs: ## Run the documentation site dev server (Docusaurus; DOCS_HOST/DOCS_PORT preslib-chart1: ## Render PresLib "ECDIS Chart 1" panels for spec comparison (one PNG per reference page) scripts/preslib-chart1.sh +# Render the IHO S-64 ENC test dataset's rendering pages (one PNG per test section) +# for diffing against the S-64 reference plots. Same self-contained flow as +# preslib-chart1, but the S-64 tests vary the mariner settings per page (§3.1 renders +# Base/Standard/Other). Needs the S-64 zip in testdata/ + a headless Chromium. +# Output → testdata/s64-pages-out/ (gitignored). +s64-pages: ## Render S-64 ENC test pages for spec comparison (one PNG per test section) + scripts/s64-pages.sh + # Regenerate the documentation UI screenshots (docs/static/img/ui/*.png) from the # live app, so they stay in sync when the UI changes. Needs baked charts in the # S-101 cache (e.g. after `make serve` has imported a region); Chromium + diff --git a/scripts/s64-pages.mjs b/scripts/s64-pages.mjs new file mode 100644 index 0000000..08ec430 --- /dev/null +++ b/scripts/s64-pages.mjs @@ -0,0 +1,106 @@ +// Render the IHO S-64 ENC test dataset's rendering pages with OUR implementation, +// one PNG per test, for diffing against the S-64 reference plots (testdata/"S-64 +// Ed 3.0.3_EN_Clean_Final.pdf"). Mirrors scripts/preslib-chart1.mjs: spec mode +// (chrome-free), each page framed to its cell at the cell's compilation scale. +// +// Unlike PresLib Chart 1 (one all-symbology plot), S-64 tests vary the MARINER +// settings per test — most importantly §3.1 ENC Display renders the SAME area at +// the Base / Standard / Other display categories. The S-64 setup uses safety +// contour 10 m / safety depth 10 m, symbolized boundaries, metres. +// +// Usage: node scripts/s64-pages.mjs [settleMs] +import { createRequire } from "node:module"; +import { execSync } from "node:child_process"; +import { mkdirSync } from "node:fs"; +const require = createRequire(import.meta.url); +function findPlaywright() { + try { return require("playwright-core"); } catch {} + const root = execSync("npm root -g", { encoding: "utf8" }).trim(); + return require(`${root}/promptfoo/node_modules/playwright-core`); +} +function findChromium() { + for (const p of ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/chrome"]) { + try { execSync(`test -x ${p}`); return p; } catch {} + } + return undefined; +} + +const [baseURL = "http://127.0.0.1:8101", outDir = "/tmp/s64-pages", settle = "8000"] = process.argv.slice(2); + +const M_PER_PX_Z0 = 78271.516964020485; +const PX_PITCH_M = 0.00026458; +const zoomForScale = (scale, lat) => Math.log2(M_PER_PX_Z0 * Math.cos((lat * Math.PI) / 180) / (PX_PITCH_M * scale)); +const spanPx = (metres, scale) => Math.max(1, Math.round(metres / scale / PX_PITCH_M)); + +// S-64 standard mariner setup: safety contour/depth 10 m, symbolized boundaries, +// metres, all categories on (per-page overrides below). Mirrors DEFAULT_MARINER keys. +const S64 = { + displayBase: true, displayStandard: true, displayOther: true, + dataQuality: true, depthUnit: "m", + shallowContour: 2, safetyContour: 10, safetyDepth: 10, deepContour: 30, + showFullSectorLines: false, boundaryStyle: "symbolized", simplifiedPoints: false, +}; +const merge = (o) => ({ ...S64, ...o }); + +// One page per row: the S-64 test section (the reference figure to diff against), +// a slug, the cell's bounds [W,S,E,N] + compilation scale (from parsing the cells), +// the colour scheme, and the mariner overrides for that test. +const HARBOR = 25000; +const PAGES = [ + // §3.1 ENC Display — same area at the three display categories (every object class). + { section: "3.1 Base", slug: "AA5DBASE", b: [9.833, 10.0, 10.0, 10.167], cscl: 60000, mariner: merge({ displayStandard: false, displayOther: false, dataQuality: false }) }, + { section: "3.1 Standard", slug: "AA5STNDR", b: [10.0, 10.0, 10.167, 10.167], cscl: 70000, mariner: merge({ displayOther: false, dataQuality: false }) }, + { section: "3.1 Other", slug: "AA5OTHER", b: [10.167, 10.0, 10.333, 10.167], cscl: 60000, mariner: merge({}) }, + // §3.6 Display Priorities (overlapping object draw order). + { section: "3.6 DisplayPriorities", slug: "2J5X0001", b: [61.333, -32.375, 61.4, -32.333], cscl: HARBOR, mariner: merge({}) }, + // §3.7 Overlap / §3.7.7 Scale minimum / §3.3 Settings / §3.2 Invalid object. + { section: "3.7 Overlap", slug: "GB3OVRLP", b: [60.6, -32.5, 61.1, -32.2], cscl: 90000, mariner: merge({}) }, + { section: "3.7.7 ScaleMin", slug: "AA3SCAMN", b: [60.267, -32.633, 60.767, -32.317], cscl: 90000, mariner: merge({}) }, + { section: "3.3 Settings", slug: "GB4X0001", b: [61.333, -32.633, 61.5, -32.317], cscl: 52000, mariner: merge({}) }, + { section: "3.2 InvalidObject", slug: "AA3INVOB", b: [-104.75, 39.333, -104.5, 39.5], cscl: 50000, mariner: merge({}) }, + // §3.4 Non-official data (new producer codes). + { section: "3.4 NonOfficial", slug: "1B5X01NE", b: [60.967, -32.533, 61.0, -32.45], cscl: HARBOR, mariner: merge({}) }, + { section: "3.4 NewProducer", slug: "IC3NEWPC", b: [60.0, -30.5, 60.1, -30.4], cscl: 90000, mariner: merge({}) }, + // §2.1.1 Power Up — the GB region overview (band-4 cell covering the GB5X tiles). + { section: "2.1.1 PowerUp", slug: "GB4X0000", b: [60.767, -32.633, 61.333, -32.317], cscl: 52000, mariner: merge({}) }, + // §5/6/7 detection tests (Colorado): nav hazards, special conditions, safety contour. + { section: "5.0 NavHazards", slug: "AA3NAVHZ", b: [-105.0, 39.833, -104.75, 40.0], cscl: 75000, mariner: merge({}) }, + { section: "5.0 Overview", slug: "AA2OVRVU", b: [-105.5, 39.167, -104.167, 40.167], cscl: 350000, mariner: merge({}) }, + { section: "6.0 SpecialConditions", slug: "AA3ARSPC", b: [-105.0, 39.667, -104.75, 39.833], cscl: 90000, mariner: merge({}) }, + { section: "7.0 SafetyContour", slug: "AA3SAFCO", b: [-105.0, 39.5, -104.75, 39.667], cscl: 90000, mariner: merge({}) }, +]; +// NOTE: §3.9 Polar (AA1NPOL*) is omitted — those cells sit at 85–90°N, beyond the +// Web-Mercator limit, so they can't be displayed. §3.8.5 AML non-ENC cells are +// omitted — their long underscored names aren't ENC cell names (not baked). + +mkdirSync(outDir, { recursive: true }); +const { chromium } = findPlaywright(); +const browser = await chromium.launch({ executablePath: findChromium(), args: ["--no-sandbox", "--hide-scrollbars"] }); + +for (const p of PAGES) { + const [w, s, e, n] = p.b; + const lat = (s + n) / 2; + const center = [(w + e) / 2, lat]; + const zoom = zoomForScale(p.cscl, lat); + const lonM = (e - w) * 111320 * Math.cos((lat * Math.PI) / 180); + const latM = (n - s) * 110574; + const width = spanPx(lonM, p.cscl), height = spanPx(latM, p.cscl); + + const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 1 }); + page.on("pageerror", (err) => console.error(`[${p.section}]`, err.message)); + await page.addInitScript((a) => { + localStorage.setItem("chartplotter:scheme", "day"); + localStorage.setItem("chartplotter:basemap", "coastline"); + localStorage.setItem("chartplotter:enc-agreement", "1"); + localStorage.setItem("chartplotter:mariner", JSON.stringify(a.mariner)); + localStorage.setItem("chartplotter:view", JSON.stringify({ center: a.center, zoom: a.zoom })); + }, { mariner: p.mariner, center, zoom }); + try { await page.goto(baseURL + "/?prod&spec", { waitUntil: "domcontentloaded", timeout: 45000 }); } + catch (err) { console.error(`[${p.section}] nav: ${err.message} — continuing`); } + await page.waitForTimeout(+settle); + const file = `${outDir}/${p.section.replace(/[ .]/g, "_")}-${p.slug}.png`; + await page.screenshot({ path: file }); + console.log(`wrote ${file} (${width}x${height} @ 1:${p.cscl})`); + await page.close(); +} +await browser.close(); diff --git a/scripts/s64-pages.sh b/scripts/s64-pages.sh new file mode 100755 index 0000000..3763732 --- /dev/null +++ b/scripts/s64-pages.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Render the IHO S-64 ENC test dataset's rendering pages with our implementation, +# one PNG per test, for diffing against the S-64 reference plots (testdata/"S-64 +# Ed 3.0.3_EN_Clean_Final.pdf"). Self-contained: extracts the cells, bakes+serves +# them through the normal server-side import path, drives scripts/s64-pages.mjs, +# then tears the server down. Re-runnable. Sibling of scripts/preslib-chart1.sh. +# +# scripts/s64-pages.sh [OUT_DIR] +# +# OUT_DIR defaults to testdata/s64-pages-out/ (gitignored). Requires the S-64 zip +# in testdata/ (an untracked IHO download) and a headless Chromium. +set -euo pipefail +cd "$(dirname "$0")/.." + +ZIP="testdata/S-64_ENC_Unencrypted_TDS.zip" +OUT="${1:-testdata/s64-pages-out}" +PORT="${PORT:-8124}" +BIN="bin/chartplotter" + +[[ -f "$ZIP" ]] || { echo "missing $ZIP — download the S-64 ENC unencrypted TDS into testdata/" >&2; exit 1; } + +echo "==> building $BIN" +make build >/dev/null + +WORK="$(mktemp -d)" +SRV_PID="" +cleanup() { [[ -n "$SRV_PID" ]] && kill "$SRV_PID" 2>/dev/null || true; rm -rf "$WORK"; } +trap cleanup EXIT + +echo "==> serving on :$PORT (temp cache/data)" +"$BIN" serve --assets web/ --cache "$WORK/cache" --data "$WORK/data" --port "$PORT" >"$WORK/serve.log" 2>&1 & +SRV_PID=$! +for _ in $(seq 1 30); do curl -fsS "http://127.0.0.1:$PORT/" >/dev/null 2>&1 && break; sleep 0.5; done + +echo "==> importing S-64 (server-side bake of all cells across the test sections)" +JOB="$(curl -fsS -X POST "http://127.0.0.1:$PORT/api/import?set=s64" \ + --data-binary @"$ZIP" -H 'Content-Type: application/zip' \ + | python3 -c 'import sys,json;print(json.load(sys.stdin)["job"])')" +for _ in $(seq 1 120); do + STATE="$(curl -fsS "http://127.0.0.1:$PORT/api/import/status?job=$JOB" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin).get("state",""))' 2>/dev/null || true)" + [[ "$STATE" == "done" ]] && break + [[ "$STATE" == "error" ]] && { echo "import failed"; cat "$WORK/serve.log"; exit 1; } + sleep 1 +done + +echo "==> rendering pages → $OUT" +node scripts/s64-pages.mjs "http://127.0.0.1:$PORT" "$OUT" + +echo "==> done: $(ls "$OUT" | wc -l) PNG(s) in $OUT" From eb4bd5e34b89ddebd4ac44ee188985c052a82923 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 09:25:07 -0400 Subject: [PATCH 14/15] docs(chart1): live S-52 "ECDIS Chart 1" symbol-compliance page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Chart 1 page to the docs that embeds the S-52 PresLib "ECDIS Chart 1" reference sheet LIVE — one read-only widget — and turns it into a symbol-compliance checker: a list of every reference panel (PresLib §16, pp. 238–253) beside the chart; click one and the widget fitBounds() to that panel (chrome-aware padding) so you can diff our render against the spec plot. The sheet is one contiguous synthetic ENC, so it's a single ~1 MB tile bundle and navigation is just the map camera. - scripts/fetch-preslib-cells.sh + `make demo-chart1`: fetch the IHO PresLib draft (or a local testdata copy), bake the cells to a tile bundle that reuses the demo bundle's frontend assets (widget points its catalog= at the Chart 1 manifest). - CI (docs.yml): cache the source cells + bake the bundle into docs/static/chart1 alongside the existing Annapolis demo; gitignored, never committed. - web: expose a public `map` getter on so embedders can frame a region with MapLibre's own fitBounds (used here, floored at the 1:139 000 SCAMIN so the whole-sheet fit never zooms features out of existence). - README + docs intro: highlight ECDIS Chart 1 rendering as a feature. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs.yml | 20 ++ Makefile | 20 +- README.md | 11 ++ docs/.gitignore | 5 + docs/docs/chart1.mdx | 56 ++++++ docs/docs/intro.mdx | 4 + docs/sidebars.js | 1 + docs/src/components/Chart1Tests.js | 186 +++++++++++++++++++ docs/src/css/custom.css | 122 ++++++++++++ docs/static/img/chart1/page-238-overview.png | Bin 0 -> 169933 bytes scripts/fetch-preslib-cells.sh | 46 +++++ web/src/chartplotter.mjs | 7 + 12 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 docs/docs/chart1.mdx create mode 100644 docs/src/components/Chart1Tests.js create mode 100644 docs/static/img/chart1/page-238-overview.png create mode 100755 scripts/fetch-preslib-cells.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cde97dd..1d1611c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,6 +47,14 @@ jobs: path: ${{ runner.temp }}/demo-cells key: demo-cells-${{ hashFiles('scripts/fetch-demo-cells.sh', 'Makefile') }} + # Cache the S-52 PresLib "ECDIS Chart 1" source cells (the IHO draft zip is + # fetched once and extracted), keyed on the fetch script so a change re-pulls. + - name: Cache PresLib Chart 1 cells + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/preslib-cells + key: preslib-cells-${{ hashFiles('scripts/fetch-preslib-cells.sh') }} + # Build the read-only widget demo bundle into docs/static/demo so Docusaurus # copies it into the published site at /chartplotter/demo/. Needs the S-101 # catalogue (IHO material kept out of the repo): clone it and let `make build` @@ -61,6 +69,18 @@ jobs: DEMO_CACHE="$RUNNER_TEMP/demo-cells" \ DEMO_OUT="docs/static/demo" + # Bake the S-52 "ECDIS Chart 1" reference sheet to tiles into docs/static/chart1 + # (published at /chartplotter/chart1/). The symbol-compliance docs page embeds + # it live, reusing the demo bundle's frontend assets. Reuses the S-101 catalogue + # cloned above; the source cells come from the IHO PresLib draft (fetched once). + - name: Build live Chart 1 tiles (docs/static/chart1) + run: | + make demo-chart1 \ + S101_PC="$RUNNER_TEMP/s101-pc/PortrayalCatalog" \ + S101_FC="$RUNNER_TEMP/s101-fc/S-101FC/FeatureCatalogue.xml" \ + PRESLIB_CACHE="$RUNNER_TEMP/preslib-cells" \ + DEMO_CHART1_OUT="docs/static/chart1" + - uses: actions/setup-node@v6 with: node-version: "20" diff --git a/Makefile b/Makefile index eac9ae7..04dd0c3 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ S101_PC ?= $(HOME)/Projects/s101-portrayal-catalogue/PortrayalCatalog S101_FC ?= $(HOME)/Projects/s101-feature-catalogue/S-101FC/FeatureCatalogue.xml S101_CACHE ?= $(CACHE)/s101 -.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo preslib-chart1 s64-pages +.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo demo-chart1 serve-demo preslib-chart1 s64-pages # Prebaked prod test set (US Inland ENC bundle + the NOAA world archive). # NB: keep these as bare values with NO inline `#` comments — Make folds any @@ -192,6 +192,24 @@ demo: build ## Assemble the read-only Annapolis widget demo bundle into $(DEMO_O @cp -R web/src web/vendor web/glyphs web/basemap "$(DEMO_OUT)/" @echo " demo bundle ready: $(DEMO_OUT)/ — host it on any static server / CDN" +# ---- live "ECDIS Chart 1" tiles for the docs symbol-compliance page ---- +# The S-52 PresLib "ECDIS Chart 1" reference sheet, baked to tiles so the docs +# Chart-1 page embeds it LIVE: one widget that reuses the demo +# bundle's frontend assets ($(DEMO_OUT)) and points its tile manifest here via +# catalog="…". So this target emits ONLY the tiles + manifest (~1 MB) — no second +# frontend copy. The whole sheet is one contiguous synthetic ENC, so a click in the +# page's test list just setView()s the widget to that panel at its compilation scale. +# Source cells come from the IHO PresLib draft (fetched + cached; see the script). +PRESLIB_CACHE ?= $(CACHE)/preslib +DEMO_CHART1_OUT ?= dist/chart1 +CHART1_MAXZOOM ?= 16 + +demo-chart1: build ## Bake the S-52 ECDIS Chart 1 sheet to tiles for the docs (into $(DEMO_CHART1_OUT)) + PRESLIB_CACHE="$(PRESLIB_CACHE)" scripts/fetch-preslib-cells.sh + @mkdir -p "$(DEMO_CHART1_OUT)" + $(BIN) bake "$(PRESLIB_CACHE)/cells" -o "$(DEMO_CHART1_OUT)/chart1.pmtiles" --bands --max-zoom $(CHART1_MAXZOOM) --manifest "$(DEMO_CHART1_OUT)/charts-index.json" + @echo " chart1 tiles ready: $(DEMO_CHART1_OUT)/ — served beside the demo bundle as /chart1/" + # LOCAL PREVIEW ONLY. The bundle is pure static files — deploy it to ANY # range-capable static host (GitHub Pages, S3/CloudFront, nginx, `npx serve`); it # needs no backend. PMTiles are read with HTTP Range, which python's http.server diff --git a/README.md b/README.md index 64fe7cc..0c06e9e 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,17 @@ static binary** for any platform with `GOOS`/`GOARCH` and nothing else to instal - **Live position and AIS (early).** Point a **NMEA 0183** feed at the server (over TCP) and it shows your **own ship** and **basic AIS targets** on the chart. A built-in `simulate` command generates traffic for testing. +- **Draws the whole symbol set.** It renders the complete S-52 Presentation Library + **ECDIS "Chart 1"** reference sheet — every symbol, line style, area fill, and + colour — drawn by the same pipeline that bakes real NOAA charts and diffed against + the spec's own plots. [See the rendered sheet →](https://beetlebugorg.github.io/chartplotter/chart1) + +

+ + chartplotter's render of the full S-52 ECDIS Chart 1 symbol sheet + +
The S-52 PresLib ECDIS Chart 1 symbol sheet, rendered by chartplotter — make preslib-chart1. +

## 🧩 Beyond the chart diff --git a/docs/.gitignore b/docs/.gitignore index 7ecad62..1e50fad 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -12,6 +12,11 @@ # (the read-only widget app + baked Annapolis .pmtiles), never committed. /static/demo/ +# Live "ECDIS Chart 1" tiles — generated by +# `make demo-chart1 DEMO_CHART1_OUT=docs/static/chart1` in CI (the baked S-52 +# PresLib reference sheet the symbol-compliance page embeds), never committed. +/static/chart1/ + # Misc .DS_Store .env.local diff --git a/docs/docs/chart1.mdx b/docs/docs/chart1.mdx new file mode 100644 index 0000000..c70fdff --- /dev/null +++ b/docs/docs/chart1.mdx @@ -0,0 +1,56 @@ +--- +id: chart1 +title: Chart 1 (symbol compliance) +sidebar_position: 5 +--- + +import Chart1Tests from '@site/src/components/Chart1Tests'; + +# ECDIS Chart 1 + +The S-52 Presentation Library's reference symbol sheet — drawn live by the same +S-101 pipeline that bakes real NOAA charts. **Click a panel to frame it** and diff +our render against the spec's reference plots. + + + +## What it is + +The IHO S-52 Presentation Library ships a reference dataset known as +**ECDIS "Chart 1"** — the digital equivalent of the paper *Chart No. 1* legend +sheets. It is a single synthetic ENC that exercises **every symbol, line style, +area fill, and text instruction** the portrayal catalogue can produce, laid out in +labelled panels by feature group. Rendering the whole sheet correctly is the +standard way to check that a chart engine portrays the catalogue faithfully; each +panel above maps to a reference plot in the Presentation Library (Part I §16, doc +pages 238–253). + +:::tip +The colour-test panels come in **Day** and **Dusk** — the same baked tiles, restyled +instantly, because colours are stored as S-101 colour *names*, not fixed RGB. You can +switch Day / Dusk / Night yourself from the widget's display settings at any zoom. +::: + +## Reproducing it + +The whole sheet is a repeatable test you can run headlessly. With the S-52 PresLib +digital-files zip in `testdata/`, one command extracts the cells, bakes them through +the normal server-side import path, and renders each panel chrome-free at its +compilation scale: + +```bash +make preslib-chart1 +``` + +It writes one PNG per reference page to `testdata/preslib-chart1-out/` (gitignored) +for diffing against the Presentation Library's own plots. A sibling harness, +`make s64-pages`, does the same for the IHO **S-64** ENC test dataset. + +The live chart on this page is built by `make demo-chart1`, which bakes the same +cells to a small tile bundle the docs site serves beside the +[home-page demo](./intro.mdx). + +:::note +ECDIS Chart 1 demonstrates **symbol portrayal coverage**, not navigational fitness. +See [Known limitations](./limitations.md) for what the engine does not yet do. +::: diff --git a/docs/docs/intro.mdx b/docs/docs/intro.mdx index cdae1db..4a7fe21 100644 --- a/docs/docs/intro.mdx +++ b/docs/docs/intro.mdx @@ -58,6 +58,10 @@ static binary for any platform with just `GOOS`/`GOARCH`. - **Shows live position and AIS (early).** Point a NMEA 0183 feed at the server over TCP and it draws your own ship and basic AIS targets on the chart; a `simulate` command generates traffic for testing. +- **Draws the whole symbol set.** It renders the complete S-52 Presentation + Library **[ECDIS Chart 1](./chart1.mdx)** reference sheet — every symbol, line + style, area fill, and colour — drawn by the same pipeline that bakes real NOAA + charts and diffed against the spec's own plots. ## Beyond the chart diff --git a/docs/sidebars.js b/docs/sidebars.js index cc80ea9..a4dbf73 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -6,6 +6,7 @@ const sidebars = { 'intro', 'installation', 'getting-started', + 'chart1', 'widget', 'cli', 'architecture', diff --git a/docs/src/components/Chart1Tests.js b/docs/src/components/Chart1Tests.js new file mode 100644 index 0000000..473905c --- /dev/null +++ b/docs/src/components/Chart1Tests.js @@ -0,0 +1,186 @@ +import React, {useEffect, useRef, useState} from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +// Chart1Tests embeds the S-52 PresLib "ECDIS Chart 1" reference sheet LIVE — one +// read-only widget — and turns the docs page into a symbol-compliance +// checker: every panel of the sheet is a row in the list; click one and the widget +// frames that panel. The whole sheet is one contiguous synthetic ENC, so navigation +// is just map.fitBounds(panel) — which fits the panel to the actual map size with +// padding that keeps the widget's own chrome (HUD, controls, scalebar) off the data. +// Tiles load from the /chart1/ bundle (`make demo-chart1`); the frontend assets are +// shared with the /demo/ bundle. + +// Web-Mercator scale↔zoom (512-tile metres/px at z0, 1/96-inch CSS px) — only used +// for the pre-fit first paint and the no-map fallback; the real framing is fitBounds. +const M_PER_PX_Z0 = 78271.516964020485; +const PX_PITCH_M = 0.00026458; +const zoomForScale = (scale, lat) => + Math.log2((M_PER_PX_Z0 * Math.cos((lat * Math.PI) / 180)) / (PX_PITCH_M * scale)); + +// Inset the fit so the sheet clears the widget's overlaid chrome on every edge. +const PAD = {top: 48, bottom: 56, left: 48, right: 48}; + +// One row per PresLib reference-plot page (Part I §16, doc pages 238–253). Bounds +// are the cells' data extents [W, S, E, N]; the harbor pages are 1:14 000, the +// overview 1:60 000. Kept in step with the PANELS table in scripts/preslib-chart1.mjs. +const HARBOR = 14000; +const RAW = [ + {page: 238, label: 'Whole sheet (overview)', b: [-5.135803, 15.00018, -4.997983, 15.133311], scale: 60000}, + {page: 239, label: 'Information about (A, B)', b: [-5.1307, 15.0993, -5.1002, 15.1288]}, + {page: 240, label: 'Information about (cont.)', b: [-5.0982, 15.0993, -5.0677, 15.1288]}, + {page: 241, label: 'Natural & man-made (C, D, E)', b: [-5.0656, 15.0992, -5.0351, 15.1288]}, + {page: 242, label: 'Port features (F)', b: [-5.0331, 15.0993, -5.0026, 15.1288]}, + {page: 243, label: 'Depths & currents (H, I)', b: [-5.1307, 15.0677, -5.1002, 15.0973]}, + {page: 244, label: 'Seabed & obstructions (J, K, L)', b: [-5.0982, 15.0677, -5.0677, 15.0973]}, + {page: 245, label: 'Traffic routes (M)', b: [-5.0656, 15.0677, -5.0351, 15.0973]}, + {page: 246, label: 'Special areas (N)', b: [-5.0331, 15.0677, -5.0026, 15.0973]}, + {page: 247, label: 'Lights, buoys & beacons (P–S)', b: [-5.1307, 15.0362, -5.1002, 15.0657]}, + {page: 248, label: 'Buoys & beacons (Q)', b: [-5.0982, 15.0362, -5.0676, 15.0657]}, + {page: 250, label: 'Topmarks (Q)', b: [-5.0656, 15.0362, -5.0350, 15.0657]}, + {page: 251, label: 'Approved new objects / V-AIS', b: [-5.1307, 15.0046, -5.1002, 15.0342]}, + {page: 252, label: 'Colour-test diagram (Day)', b: [-5.0331, 15.0362, -5.0026, 15.0657], scheme: 'day'}, + {page: 253, label: 'Colour-test diagram (Dusk)', b: [-5.0331, 15.0362, -5.0026, 15.0657], scheme: 'dusk'}, +]; +const PANELS = RAW.map((p) => { + const [w, s, e, n] = p.b; + return {...p, scale: p.scale || HARBOR, lng: (w + e) / 2, lat: (s + n) / 2}; +}); +const SHEET = PANELS[0]; // page 238 = the whole sheet +const INITIAL_SCALE = 105000; // generous pre-fit paint; fitBounds refines on ready +// These features' SCAMIN is 1:139 000 — zoom out past it and they vanish. Floor the +// map so neither the whole-sheet fit (on a small map) nor a scroll can cross it. +const SCAMIN_MIN_ZOOM = zoomForScale(139000, SHEET.lat); + +// Fit the map to a panel's bounds with chrome padding. Returns false if the map +// isn't up yet (caller falls back to setView). +function fitPanel(el, p, animate) { + const m = el && el.map; + if (!m || typeof m.fitBounds !== 'function') return false; + const [w, s, e, n] = p.b; + m.fitBounds([[w, s], [e, n]], {padding: PAD, duration: animate ? 900 : 0}); + return true; +} + +function Chart() { + // /demo/ holds the widget frontend (baked by `make demo`); /chart1/ holds just + // the Chart 1 tiles + manifest (baked by `make demo-chart1`). The widget reuses + // the former for assets and points its tile manifest at the latter via catalog=. + const demo = useBaseUrl('/demo/'); + const manifest = useBaseUrl('/chart1/charts-index.json'); + const overviewImg = useBaseUrl('/img/chart1/page-238-overview.png'); + const ref = useRef(null); + const [active, setActive] = useState(238); + const [status, setStatus] = useState('checking'); // checking | ready | missing + + // Only boot the live widget if the tile bundle is actually published. Locally + // (no `make demo-chart1`) fall back to the static overview image. + useEffect(() => { + let cancelled = false; + fetch(manifest) + .then((r) => { + if (cancelled) return; + if (!r.ok) { setStatus('missing'); return; } + setStatus('ready'); + const id = 'chartplotter-widget-module'; + if (!document.getElementById(id)) { + const sc = document.createElement('script'); + sc.type = 'module'; + sc.id = id; + sc.src = `${demo}src/chartplotter.mjs`; + document.head.appendChild(sc); + } + }) + .catch(() => { if (!cancelled) setStatus('missing'); }); + return () => { cancelled = true; }; + }, [demo, manifest]); + + // Once the widget's map is ready, frame the whole sheet (fitBounds, not a guessed + // scale, so the entire box + labels show with margin for the chrome). + useEffect(() => { + if (status !== 'ready') return undefined; + let tries = 0; + const iv = setInterval(() => { + const m = ref.current && ref.current.map; + if (m) { + try { m.setMinZoom(SCAMIN_MIN_ZOOM); } catch (e) { /* older map */ } + fitPanel(ref.current, SHEET, false); + clearInterval(iv); + } else if (++tries > 60) { + clearInterval(iv); + } + }, 200); + return () => clearInterval(iv); + }, [status]); + + const go = (p) => { + setActive(p.page); + const el = ref.current; + if (!el) return; + if (p.scheme && typeof el.applyScheme === 'function') { + try { el.applyScheme(p.scheme); } catch (e) { /* widget-mode best-effort */ } + } + if (!fitPanel(el, p, true) && typeof el.setView === 'function') { + el.setView({lng: p.lng, lat: p.lat, scale: p.scale, animate: true, duration: 900}); + } + }; + + if (status === 'missing') { + return ( +
+ The S-52 ECDIS Chart 1 symbol sheet rendered by chartplotter +

+ The live, clickable version needs the baked tiles. Build them locally with{' '} + make demo DEMO_OUT=docs/static/demo and{' '} + make demo-chart1 DEMO_CHART1_OUT=docs/static/chart1, then{' '} + make docs. +

+
+ ); + } + + const zoom = zoomForScale(INITIAL_SCALE, SHEET.lat); + return ( +
+
+
+ Reference panels PresLib §16, pp. 238–253 +
+
    + {PANELS.map((p) => ( +
  1. + +
  2. + ))} +
+
+
+ {/* widget = read-only viewer; assets = demo frontend; catalog = Chart 1 tiles */} + +
+
+ ); +} + +export default function Chart1Tests() { + return ( + Loading the chart…}> + {() => } + + ); +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index c5bfc35..7b74ee3 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -52,3 +52,125 @@ font-size: 0.85rem; text-align: right; } + +/* The symbol-compliance checker: a live widget over the S-52 ECDIS + Chart 1 sheet, with the reference-panel list beside it so the whole thing fits + one screen. Click a panel → the widget fitBounds() to it (chrome-aware padding). */ +.chart1 { + display: grid; + grid-template-columns: 250px minmax(0, 1fr); + gap: 1rem; + align-items: start; + margin: 1.5rem 0; +} +.chart1--poster { + display: block; +} +.chart1__panel { + grid-column: 1; + grid-row: 1; + display: flex; + flex-direction: column; + max-height: 78vh; + min-height: 520px; +} +.chart1__title { + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ifm-color-emphasis-700); + padding: 0 0.3rem 0.4rem; +} +.chart1__sub { + display: block; + font-weight: 400; + text-transform: none; + letter-spacing: 0; + font-size: 0.72rem; + color: var(--ifm-color-emphasis-500); +} +.chart1__list { + list-style: none; + margin: 0; + padding: 0; + overflow: auto; + display: flex; + flex-direction: column; + gap: 2px; +} +.chart1__test { + display: flex; + align-items: baseline; + gap: 0.5rem; + width: 100%; + padding: 0.4rem 0.55rem; + text-align: left; + cursor: pointer; + border: 0; + border-left: 3px solid transparent; + border-radius: 6px; + background: transparent; + color: inherit; + font: inherit; + transition: background 0.12s, border-color 0.12s; +} +.chart1__test:hover { + background: var(--ifm-color-emphasis-100); +} +.chart1__test--active { + background: var(--ifm-color-primary-lightest); + border-left-color: var(--ifm-color-primary); +} +.chart1__page { + min-width: 2.7em; + font-size: 0.68rem; + font-variant-numeric: tabular-nums; + color: var(--ifm-color-emphasis-600); + white-space: nowrap; +} +.chart1__label { + flex: 1; + font-size: 0.85rem; + line-height: 1.25; +} +.chart1__map { + grid-column: 2; + grid-row: 1; + margin: 0; + height: 78vh; + min-height: 520px; +} +.chart1__poster { + width: 100%; + border-radius: 12px; + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.18); +} +.chart1__hint { + margin: 0.75rem 0 0; + font-size: 0.85rem; + color: var(--ifm-color-emphasis-700); +} + +/* Narrow screens (Docusaurus drops the right TOC < 997px): stack, map first. */ +@media (max-width: 996px) { + .chart1 { + grid-template-columns: 1fr; + } + .chart1__map { + grid-column: 1; + grid-row: 1; + height: 58vh; + min-height: 340px; + } + .chart1__panel { + grid-column: 1; + grid-row: 2; + max-height: none; + min-height: 0; + } + .chart1__list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + } +} diff --git a/docs/static/img/chart1/page-238-overview.png b/docs/static/img/chart1/page-238-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..c89644d03c5be6dd4851d83364e729211310d0c1 GIT binary patch literal 169933 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

HayOg>~uI$Nq8ApuWvvB0rvVf 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 U8G8BVpk+1-=FKcTIU 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%0DSA+6L`&t95e>9l_|5~HTj;uM?#F1 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 OZYm)vD^ek(@AE%}%$LLf literal 0 HcmV?d00001 diff --git a/scripts/fetch-preslib-cells.sh b/scripts/fetch-preslib-cells.sh new file mode 100755 index 0000000..346e983 --- /dev/null +++ b/scripts/fetch-preslib-cells.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Fetch the S-52 PresLib "ECDIS Chart 1" ENC cells for the live docs demo. The +# source is the IHO PresLib e4.0.0 digital-files draft zip; we extract just the +# ENC cells (*.000 + any update files) into $OUT/cells so `make demo-chart1` can +# bake them. Mirrors scripts/fetch-demo-cells.sh. +# +# Idempotent: if $OUT/cells already holds a *.000 it does nothing, so CI can cache +# the directory across runs. Prefers a local copy of the zip (the untracked +# testdata download) so local builds don't re-download. Override via env: +# PRESLIB_CACHE output dir (default ./.preslib-cache) +# PRESLIB_URL download URL (default the IHO legacy host) +# PRESLIB_ZIP path to a local zip to use instead of downloading +set -euo pipefail + +OUT="${PRESLIB_CACHE:-.preslib-cache}" +URL="${PRESLIB_URL:-https://legacy.iho.int/iho_pubs/draft_pubs/PresLib_e4.0.0/Digital_Files/S-52_PresLib_e4.0.0_Digital_Files_Draft.zip}" +LOCAL="${PRESLIB_ZIP:-testdata/S-52_PresLib_e4.0.0_Digital_Files_Draft.zip}" + +mkdir -p "$OUT/cells" +if compgen -G "$OUT/cells/*.000" >/dev/null; then + echo "cached PresLib Chart 1 cells in $OUT/cells" + ls -1 "$OUT/cells" + exit 0 +fi + +ZIP="$OUT/preslib.zip" +if [ -s "$LOCAL" ]; then + echo "local $LOCAL" + ZIP="$LOCAL" +elif [ ! -s "$ZIP" ]; then + echo "fetch $URL" + curl -fSL --retry 3 -o "$ZIP" "$URL" +fi + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +echo "extract Chart 1 ENC cells" +unzip -qo "$ZIP" -d "$TMP" +ENC_ROOT="$(dirname "$(find "$TMP" -name '*.000' | head -1)")" +[ -n "$ENC_ROOT" ] || { echo "no *.000 cells in $ZIP" >&2; exit 1; } +# Copy the cell base files and any ENC update files (*.001, *.002, …) flat. +find "$ENC_ROOT" -maxdepth 1 -type f \( -name '*.000' -o -regex '.*\.[0-9][0-9][0-9]$' \) \ + -exec cp {} "$OUT/cells/" \; + +echo "PresLib Chart 1 cells ready in $OUT/cells:" +ls -1 "$OUT/cells" diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs index 07d4c01..fbffd43 100644 --- a/web/src/chartplotter.mjs +++ b/web/src/chartplotter.mjs @@ -1065,6 +1065,13 @@ export class ChartPlotter extends HTMLElement { return this._plotter ? this._plotter.setView(opts) : null; } + // Public: the underlying MapLibre map, once ready (null before the first paint). + // Lets embedders frame a region with the library's own camera helpers, e.g. + // app.map?.fitBounds([[w, s], [e, n]], { padding: 56 }) + get map() { + return this._map || null; + } + saveView() { // The cell-picker "charts mode" (whose zoomed-out framing we used to skip // persisting) was removed; the live view is always the one to save. From 6943754cb89f4190be6051c746c5b06c22249def Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Fri, 26 Jun 2026 09:33:21 -0400 Subject: [PATCH 15/15] fix(web): don't add the catalog overlay before the style finishes loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After merging main's physical-scale restage, onReady awaits the catalog load and then calls addCatalogOverlay — but a setStyle (physical-scale / SCAMIN buckets) can still be in flight at that point, so addSource threw "Style is not done loading". The throw happened BEFORE the style.load self-heal listener was registered, so the overlays never recovered. Bail when !isStyleLoaded() (the style.load handler re-adds once ready) and guard the initial call so that listener always registers. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/chartplotter.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs index 0ad4e9d..1a99f18 100644 --- a/web/src/chartplotter.mjs +++ b/web/src/chartplotter.mjs @@ -533,7 +533,10 @@ export class ChartPlotter extends HTMLElement { try { this._plotter.setHiddenCells([...this._hiddenCells]); } catch (e) { console.warn(e); } } await this._catalogReady; - this.addCatalogOverlay(map); + // Best-effort: if the style is mid-rebuild this no-ops (or throws on older + // maps) — either way the style.load handler below re-adds the overlay once the + // fresh style is ready, so never let it skip registering that listener. + try { this.addCatalogOverlay(map); } catch (e) { console.warn("[overlay] deferring to style.load:", e); } // The plotter rebuilds the whole style (setStyle) when server sets load or the // SCAMIN buckets refresh, wiping every app-added overlay (coverage boxes, pick & // inspect highlights). Re-apply them after each rebuild, and repopulate the @@ -1094,6 +1097,12 @@ export class ChartPlotter extends HTMLElement { // layers. A style.load handler (see onReady) re-invokes this against the fresh // style; the guard makes a redundant call (when the overlay is still present) a // no-op so we never double-add. + // + // The style may still be REBUILDING when this first runs from onReady (a + // setStyle for the physical-scale restage / SCAMIN buckets can be in flight + // after the awaited catalog load) — addSource would throw "Style is not done + // loading". Bail; the onReady style.load handler re-invokes us once it's ready. + if (!map.isStyleLoaded()) return; if (map.getSource("focus")) return; const empty = { type: "FeatureCollection", features: [] }; map.addSource("focus", { type: "geojson", data: empty });