EncDotNet.S100.Datasets.S111
Reader and coverage portrayal pipeline for S-111 Surface Current datasets.
Overview
This library reads S-111 datasets from HDF5 files and provides gridded and positioned-node current time series for the portrayal pipeline. Key types include:
S111Dataset— root model containing horizontal CRS, depth, data coding format, and time-step coverages.S111DatasetReader— reads DCF1, DCF2, DCF3, and DCF8 datasets. DCF1 uses eachGroup_NNN/timePointas the authoritative timestamp and transposes the time-major matrix into positioned node series; this deliberately ignores conflicting instance cadence metadata found in production IC-ENC files.ReadAny(file, new S111ReadOptions { DeferValueReads = true })lazily decodes DCF2 values; station-series formats are materialized eagerly.S111ReadOptions— opt-in read options;DeferValueReadsenables lazy per-time-step value decoding for dcf2 (regular-grid) datasets.S111CoverageSource—ICoverageSourceadapter for the coverage pipeline.S111PortrayalCatalogue— coverage portrayal catalogue for current arrow rendering (see Portrayal below).S111SpeedBandReader— parses the 9 surface-current speed bands and the three scale constants from the bundledRules/select_arrow.xsl.SurfaceCurrentCoverage,SurfaceCurrentValue,SurfaceCurrentStation— surface current data models. Speeds are in knots on every path (SurfaceCurrentValue.Speedfor gridded data,SurfaceCurrentStation.SpeedsKnotsfor station series), matching the S-111 unit ofsurfaceCurrentSpeed; nothing is converted on read.
Portrayal
S111PortrayalCatalogue is driven by the bundled IHO portrayal catalogue under
EncDotNet.S100.Specifications' content/S111/pc/ tree:
- Speed bands & scale constants — parsed from
Rules/select_arrow.xslon first use byS111SpeedBandReaderand cached per-catalogue instance. The XSLT supplies the 9SurfaceCurrentSpeedBand{N}ranges (mapped to colour tokensSCBN{N}and SVG symbolsSCAROW0{N}) plus thescaleFloor,scaleCeilingandscaleFactorIntermediatevariables — no values are hard-coded in C#. - Day / Dusk / Night palettes — read from
ColorProfiles/colorProfile.xml.SwitchPalette(PaletteType)activates the chosen palette;ResolveSymbolSchemeandActivePalettereflect the change immediately.
No coverage colour fill
ResolveColorScheme returns null. The bundled portrayal catalogue
(content/S111/pc/Rules/select_arrow.xsl) defines arrow symbology only —
there is no <coverageFill> instruction on surfaceCurrentSpeed. Synthesising
a continuous heatmap from the speed-band table (as an earlier viewer prototype
did) actively obscured the underlying S-101 chart, so the colour-band sub-layer
has been removed. Per-band colour now travels with the arrow SVG itself via its
fSCBN{N} CSS class; MapsuiCoverageArrowRenderer resolves that token via the
active palette.
Arrow rendering
Every data coding format is drawn with the catalogue's SCAROW arrows. That
covers the dcf2 regular grid and the dcf1/dcf3/dcf8 station series and
ungeorectified meshes alike: the grid path goes through
MapsuiCoverageArrowRenderer (viewer) / SkiaCoverageArrowRenderer (headless
s100 render), the station/mesh path through S111DatasetProcessor's glyph
sub-layer. Both draw one point symbol per kept node, re-rasterised at screen
DPI so arrows stay sharp and keep a stable on-screen size at every zoom
(S-111 §9.2.4 sizes arrows in millimetres on the display). Opening an S-111
dataset requires the S-111 portrayal catalogue; there is no catalogue-less
fallback symbology.
Spec conformance
What S-111 Edition 2.0.0, its portrayal catalogue and S-98 Edition 2.0.0 define, and how this library follows them:
| Rule | Source | Implementation |
|---|---|---|
| Arrow shape, pivot at the symbol's centre, black border | §9.2.1, Figure 9-1, Annex H Rule 1 | Bundled SCAROW01–09.svg, drawn centred on the node. |
| Direction: towards which the current flows, clockwise from true north | §9.2.2, Rule 7 | Rotation = surfaceCurrentDirection (clockwise) on a north-up Mercator display; turns with a rotated map. |
9 speed bands, one symbol and colour token SCBN1–9 per band |
§9.2.3, Rules 3 and 5, select_arrow.xsl |
Bands parsed from select_arrow.xsl; colours from the active palette in colorProfile.xml. |
| Day / dusk / night colours from the portrayal catalogue | Rule 4, Annex F | SwitchPaletteAsync; the composite (--layer) path now passes the palette too. Before, it drew every arrow black. |
Size H = Href · min(max(Slow, S), Shigh) / Sref (Href 10 mm, Sref 5 kn, Slow 2 kn, Shigh 13 kn), the same for every data source |
§9.2.4, Eqn 9.1, Rule 6 | Catalogue scale factors 0.40 / 0.20 × S / 2.60 times the 10 mm arrow, for every DCF. Station series used to clamp the scale to 0.2–2.0 and multiply it by 0.6. |
| No arrow for null speed or direction | Rule 2 | Fill values and NaN are skipped on every path. |
| Thinning must reduce symbol density when zooming out | S-98 §13.1, S-111 §9.3.2 | Zoom-dependent thinning on every path (below). |
Regular grids: every n-th row and column, n = 1 + fix(Lsmax / (D · Rmax)), seeded so the maximum vector is drawn |
§9.3.2 Eqn 9.2/9.3, Annex H Rule 11, S-98 Appendix G-1.1 | SymbolThinning.ThinGrid with D the on-screen cell diagonal and Lsmax the largest arrow in the displayed field. |
| Irregular data and ungeorectified grids: point-by-point overlap elimination (or an implementer heuristic) | §9.3.2, §9.3.3 | SymbolThinning.ThinPoints. |
| No spatial interpolation when zoomed in | §9.3.1 | Arrows are drawn only at nodes; zooming in shows fewer arrows. |
| Viewing group 33060, display plane UnderRadar, drawing priority 10 | SurfaceCurrent.xsl |
Grid arrows go to the S-98 DynamicArrows plane (S-98 Annex A §A-6.9.1). |
Implementation choices
Where the specifications leave a choice to the implementer, we chose:
Rmax= 0.5 (SymbolThinning.DefaultMaxSymbolToSpacingRatio) for grids and points: S-111's and S-98's recommended value. The drawn grid-cell diagonal is then at least twice the largest arrow, so arrows never overlap.- Seed point. The lattice is seeded at the fastest current among the
largest arrows in view, not at the first largest arrow in row-major order.
The two differ only in tie-breaking: below 2 kn every arrow has the
Slowsize, so a row-major seed would make the drawn lattice jump whenever panning changed the top-left cell. S-98 G-1.1 allows adapting the seed. - Displayed field.
Lsmaxand the seed are taken over the view grown by half the largest arrow, so arrows whose pivot is just off screen still show. - Point-by-point clearance. S-111 §9.3.2 does not define "overlap". A kept
arrow of length
L(the longer of the pair) clears a radius ofL / (Rmax · √2): the nearest-neighbour spacing of a thinned square grid. Meshes and grids therefore thin to the same minimum spacing, about 1.41 arrow lengths, and drawn arrows never overlap. Points are visited fastest first. Thinning runs over every node, not just the visible ones, so the selection changes only with zoom and stays put while panning. - Millimetres to pixels. Arrow lengths are converted at 96 DPI
(
SymbolThinning.PixelsPerMillimetre), the scale at which Mapsui and Svg.Skia rasterise the millimetre-dimensioned SVGs. - User symbol scale.
RenderContext.SymbolScale(the viewer's Symbol Scale slider,--symbol-scaleon the CLI) multipliesHref. Annex H Rule 12 suggests makingHref/Srefuser-selectable. Thinning uses the scaled length, so bigger arrows are also spaced further apart. - No extra transparency. §9.2.6 asks for alpha 0.4 (dusk) and 0.2 (night) over an ENC. The catalogue's dusk and night colours already carry Annex F's luminance reduction, and Rule 4 says to use the catalogue colours, so no further alpha is applied. Doing both would make night arrows all but invisible.
- Speed 0. Band 1 is
[0.00, 0.50)inselect_arrow.xsl, so a node with speed exactly 0 is drawn with the band-1 arrow (direction as encoded).
Lazy reads
S111DatasetReader.ReadAny(file, new S111ReadOptions { DeferValueReads = true })
reads only instance metadata up front and defers each time step's
values compound until first access. Per-step time points are derived
arithmetically from dateTimeOfFirstRecord + i × timeRecordInterval
(S-111 Edition 2.0.0 §10.2.6) instead of opening every Group_NNN, so
opening a dataset with hundreds of steps is fast. SurfaceCurrentCoverage.Values
caches the decoded array under a lock on first read.
S111DatasetProcessor opts into deferral for dcf2 (regular-grid) datasets
and therefore implements IDisposable: it retains the underlying HDF5
file/stream for the processor's lifetime so deferred reads can resolve.
Callers that create an S111DatasetProcessor (the viewer's
DatasetLoaderService does this) must dispose it to release the file.
dcf1/dcf3/dcf8 station-series datasets are materialized fully and close their
file immediately.
Validation
A bundled rule pack
(EncDotNet.S100.Datasets.S111.Validation.S111SurfaceCurrentRules.Default)
evaluates a typed S111Dataset against the S-111 Edition 2.0.0
checklist and emits a ValidationReport of findings. The pack is
invoked automatically by S111DatasetProcessor.Validate() and is
also runnable directly:
var report = S111SurfaceCurrentRules.Default.Run(dataset);
foreach (var finding in report.Findings)
Console.WriteLine($"{finding.RuleId} {finding.Severity}: {finding.Message}");
| Rule id | Severity | Checks |
|---|---|---|
S111-R-1.1 |
Error | Each coverage's Values.Length equals NumPointsLatitudinal × NumPointsLongitudinal. |
S111-R-2.1 |
Warning | Coverages are strictly increasing by TimePoint and successive deltas vary by no more than ±10% of the median delta. |
S111-R-3.1 |
Warning | SurfaceCurrentDepth, when set, has magnitude ≤ 1500 m (signed per depthTypeIndex: below the sea surface is negative). |
S111-R-3.2 |
Warning | TypeOfCurrentData, when set, is a member of the S-111 enumerated set {1..6}. |
S111-R-4.1 |
Warning | Non-NODATA current speeds lie in the plausible range [0, 15] m/s; fill / NaN / ±Infinity skipped. |
S111-R-4.2 |
Error | Non-NODATA current directions lie in the half-open range [0, 360) degrees true. |
S111-PROJ-SCHEMA |
Error | Defensive surrogate: emitted when the underlying HDF5 dataset fails schema-level parsing inside Validate(). |
S111-STATION-SHAPE |
Error | Station timestamps, speeds, directions, and declared sample count disagree. |
S111-STATION-TIME |
Error | Explicit station timestamps are not strictly increasing. |
S111-STATION-SPEED |
Error | A station contains a negative or non-finite speed. |
S111-STATION-DIRECTION |
Error | A station contains a direction outside 0–360 degrees. |
R-2.1 reuses the time-axis rule template established by S-104 (V-2), keeping monotonicity / cadence checks consistent across the two time-varying coverage products.
Installation
dotnet add package EncDotNet.S100.Datasets.S111