Skip to content

Fix S-57 area spike artifacts from merged interior holes#469

Merged
philliphoff merged 3 commits into
mainfrom
philliphoff-fix-curve-spike-artifacts
Jul 18, 2026
Merged

Fix S-57 area spike artifacts from merged interior holes#469
philliphoff merged 3 commits into
mainfrom
philliphoff-fix-curve-spike-artifacts

Conversation

@philliphoff

@philliphoff philliphoff commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Fixes #470

Problem

Viewing S-57 (NOAA ENC) datasets showed large straight "spike" segments cutting across otherwise-smooth sea/land area curves — most visible in line/outline rendering (area fills hid them as zero-width slivers). Reproduced with NOAA US3WA01M (Strait of Juan de Fuca).

Root cause

This repo renders S-57 by translating each cell in-memory to an S101Document (S57ToS101Translator) and reusing the S-101 pipeline. In TranslateAreaSpatial, every consecutive interior (USAG=2) boundary edge of an area was grouped into a single interior composite curve. S-57 lists all of an area's interior edges consecutively regardless of which hole they belong to, so multi-hole areas had all their holes merged into one ring. Flattening that ring to coordinates jumped from the end of one hole to the start of the next, producing the long "spike" segments.

Empirically, on US3WA01M: 71 of 134 interior composites merged more than one disjoint hole (e.g. DepthArea merging 4 holes with a 1.32° jump; QualityOfBathymetricData merging 151 chains into one ring).

Fix

TranslateAreaSpatial now chains area boundary edges into contiguous rings by shared begin/end node identity (reversing individual edges as needed), emitting one composite/ring per hole. Applied to exterior edges too for ordering robustness. Two small helpers were added: ChainEdgesIntoRings and EdgeNode.

Verification

  • Node-identity chaining splits the 134 merged interior composites into 1864 rings, all cleanly closed (0 open).
  • Worst interior-ring jump 1.32° → eliminated; remaining long segments are all legitimate rectilinear meta-coverage boundaries (ZOC boxes, TSS lanes, magnetic-variation/administration zones).
  • Confirmed visually in the viewer (smooth depth contours after the fix).
  • Two new xunit regression tests (two-hole area → 2 separate interior rings; no cross-hole spike). Confirmed they fail without the fix (spike = 8.485E-05°, the predicted jump) and pass with it.
  • All 282 S-57 tests pass; dotnet format whitespace / style IDE0005 clean.

Files changed

  • src/EncDotNet.S100.Datasets.S57/S57ToS101Translator.cs — the fix
  • tests/EncDotNet.S100.Datasets.S57.Tests/S57ToS101TranslatorTests.cs — regression tests
  • src/EncDotNet.S100.Datasets.S57/README.md — documented ring-chaining behavior

S57ToS101Translator.TranslateAreaSpatial grouped every consecutive
interior (USAG=2) edge into a single interior composite curve, so areas
with multiple holes had all holes merged into one ring. Flattening that
ring to coordinates jumped between holes, producing long straight
"spike" segments through curves (visible in line/outline rendering;
hidden by area fills).

Chain area boundary edges into contiguous rings by shared begin/end node
identity, reversing edges as needed, so each hole becomes its own
interior ring. Applied to exterior edges too for ordering robustness.

Verified on NOAA US3WA01M (Strait of Juan de Fuca): worst interior-ring
jump 1.32 deg -> eliminated; the two new regression tests fail without
the fix (spike = 8.485E-05 deg) and pass with it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f45ed968-6f44-421b-bd0f-0519d83599e0
Copilot AI review requested due to automatic review settings July 18, 2026 04:57
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Performance Gate

PASSED — no regressions.

Threshold: 10.0%, MAD multiplier (k): 3.0, retry-zone mult: 2.0×

Scenario summary

Scenario Status Δ median (%) z (Δ/MAD) Base median (ms) Samples (b/c)
exchange-set-open ✅ pass -0.2 -0.05 1.59 5/5
s101-portray-cold ✅ pass -0.9 -0.18 0.27 5/5
s101-portray-warm ✅ pass -6.8 -0.20 0.24 5/5
s101-real-cold ✅ pass -0.6 -0.09 0.28 5/5
s101-real-warm ✅ pass +21.4 +0.91 0.25 5/5
s101-render-warm ✅ pass -11.0 -1.13 0.23 5/5
s102-coverage ✅ pass -5.4 -0.82 0.17 5/5
s102-coverage-open ✅ pass 0.0 0.00 2.83 20/20
s102-coverage-render-large ✅ pass +6.4 +0.75 0.22 5/5
s102-real-warm ✅ pass +178.8 +5.33 0.33 5/5
s111-real-warm ✅ pass -8.7 -1.44 0.33 5/5
s124-vector ✅ pass -8.9 -1.33 0.21 5/5
s201-vector ✅ pass -19.2 -2.17 0.23 5/5

exchange-set-open

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 1.59 1.59
Baseline MAD (ms) 0.07
Δ median -0.2%
z (Δ/MAD) -0.05

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.asset.read 4.35 4.32 -0.8% ▫️
s100.exchangeset.parse 46.42 46.67 +0.5% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.asset.read.duration 0.16 0.20 +24.5%

s101-portray-cold

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.27 0.26
Baseline MAD (ms) 0.01
Δ median -0.9%
z (Δ/MAD) -0.18

s101-portray-warm

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.24 0.23
Baseline MAD (ms) 0.08
Δ median -6.8%
z (Δ/MAD) -0.20

s101-real-cold

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.28 0.28
Baseline MAD (ms) 0.02
Δ median -0.6%
z (Δ/MAD) -0.09

s101-real-warm

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.25 0.31
Baseline MAD (ms) 0.06
Δ median +21.4%
z (Δ/MAD) +0.91

s101-render-warm

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.23 0.20
Baseline MAD (ms) 0.02
Δ median -11.0%
z (Δ/MAD) -1.13

s102-coverage

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.17 0.16
Baseline MAD (ms) 0.01
Δ median -5.4%
z (Δ/MAD) -0.82

s102-coverage-open

Iteration statistics

Stat Baseline Candidate
Samples 20 20
Median (ms) 2.83 2.83
Baseline MAD (ms) 0.06
Δ median 0.0%
z (Δ/MAD) 0.00

Spans (sum of all iterations)

Span Baseline (ms) Candidate (ms) Delta Status
s100.dataset.open 450.95 447.83 -0.7% ▫️
s100.hdf5.dataset.read 162.91 162.24 -0.4% ▫️
s100.hdf5.file.open 18.57 18.39 -1.0% ▫️
s100.hdf5.open 18.47 18.53 +0.3% ▫️

Metrics

Metric Baseline Candidate Delta Status
s100.hdf5.read.bytes 36456.00 36456.00 +0.0% ▫️
s100.hdf5.read.duration 26.43 26.35 -0.3% ▫️
s100.hdf5.read.duration 31.42 31.30 -0.4% ▫️
s100.hdf5.read.duration 11.18 11.13 -0.4% ▫️

s102-coverage-render-large

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.22 0.23
Baseline MAD (ms) 0.02
Δ median +6.4%
z (Δ/MAD) +0.75

s102-real-warm

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.33 0.91
Baseline MAD (ms) 0.11
Δ median +178.8%
z (Δ/MAD) +5.33

s111-real-warm

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.33 0.30
Baseline MAD (ms) 0.02
Δ median -8.7%
z (Δ/MAD) -1.44

s124-vector

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.21 0.19
Baseline MAD (ms) 0.01
Δ median -8.9%
z (Δ/MAD) -1.33

s201-vector

Iteration statistics

Stat Baseline Candidate
Samples 5 5
Median (ms) 0.23 0.19
Baseline MAD (ms) 0.02
Δ median -19.2%
z (Δ/MAD) -2.17

Generated by EncDotNet.S100.PerfReport gate command

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes S-57 area “spike” artifacts caused by incorrectly merging multiple interior holes into a single boundary when translating S-57 areas into S-101 surface topology.

Changes:

  • Updates TranslateAreaSpatial to chain boundary edges into contiguous rings by shared begin/end node identity, emitting one ring per hole (and improving exterior ordering robustness).
  • Adds regression tests covering a two-hole area translation and asserting the absence of cross-hole “spike” segments.
  • Documents the ring-chaining behavior and rationale in the S-57 dataset README.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/EncDotNet.S100.Datasets.S57/S57ToS101Translator.cs Rebuilds area boundary rings by node contiguity and introduces helpers for edge chaining and node lookup.
tests/EncDotNet.S100.Datasets.S57.Tests/S57ToS101TranslatorTests.cs Adds regression tests to ensure multi-hole areas produce separate interior rings and no cross-hole spikes.
src/EncDotNet.S100.Datasets.S57/README.md Documents why and how area rings are reassembled to avoid spike artifacts.

Comment thread src/EncDotNet.S100.Datasets.S57/S57ToS101Translator.cs Outdated
Comment thread tests/EncDotNet.S100.Datasets.S57.Tests/S57ToS101TranslatorTests.cs Outdated
Address PR #469 review feedback:
- ChainEdgesIntoRings now seeds each ring using the seed edge's own
  FSPT orientation (deriving start/end nodes accordingly) instead of
  hard-coding forward, so reconstructed rings stay consistent with the
  encoded orientation and the XML docs.
- Harden BuildTwoHoleArea regression fixture: boundary pointers are
  shuffled and each ring's first edge is referenced in reverse, so the
  tests fail if chaining relies on input order or orientation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f45ed968-6f44-421b-bd0f-0519d83599e0
Copilot AI review requested due to automatic review settings July 18, 2026 05:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/EncDotNet.S100.Datasets.S57/S57ToS101Translator.cs Outdated
Comment thread tests/EncDotNet.S100.Datasets.S57.Tests/S57ToS101TranslatorTests.cs
Address PR #469 review feedback:
- ChainEdgesIntoRings now precomputes each edge's (begin,end) nodes once
  and indexes edges by incident node, so each extension step looks up
  connecting edges directly instead of rescanning the pool and re-calling
  EdgeNode. This replaces the O(n^2) scan (noticeable on large NOAA areas
  with many holes) while preserving identical output: input-order seeding,
  seed FSPT orientation, earliest-unused-incident tie-break, and
  forward-before-reverse handling.
- Compute MaxSegment(ring) once in the no-cross-hole-spike test assertion
  and reuse it in both the condition and the failure message.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f45ed968-6f44-421b-bd0f-0519d83599e0
Copilot AI review requested due to automatic review settings July 18, 2026 05:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@philliphoff
philliphoff merged commit a729793 into main Jul 18, 2026
13 checks passed
@philliphoff
philliphoff deleted the philliphoff-fix-curve-spike-artifacts branch July 18, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: S-57 areas render large "spike" segments across curves from merged interior holes

2 participants