Table of Contents

EncDotNet.S100.Datasets.S57

Reader and S-101 translator for IHO S-57 Electronic Navigational Chart (ENC) datasets.

Overview

This library reads S-57 (Edition 3.1) ENC base cells via the EncDotNet.S57 NuGet package (itself layered on top of EncDotNet.Iso8211) and translates the parsed records into the S-101 in-memory document model so the existing S-100 Part 9A Lua portrayal pipeline (provided by EncDotNet.S100.Datasets.S101) can render them. This is not an S-52 implementation — symbology is whatever the S-101 portrayal catalogue produces when fed the translated data.

Mappings follow the IHO S-57 to S-101 Conversion Guidance (S-101PT6 INF02A, draft 2021).

Key types:

  • S57Dataset — entry point; opens a .000 file and exposes the parsed EncDotNet.S57.S57Document from the upstream package, the S-57 product specification it declares (DeclaredProductSpecification) and the TranslationTarget that follows from it (S-401 for an inland ENC, otherwise S-101; static TranslationTargetFor(document) for an already-parsed document), plus a static IsS57File(path) discriminator used by EncDotNet.S100.Datasets.Pipelines to disambiguate .000 files that could otherwise be S-101. Also exposes a cheap ReadMetadata "peek" path (issue #460) that folds the WGS-84 extent from the raw spatial coordinates (via the DSPM coordinate multiplication factor) and the whole-cell display window without the S-57 → S-101 translation or any portrayal — so a host can frame a viewport over a loose folder of S-57 cells before deciding to load each in full. The window comes from ResolveCellMinimumDisplayScale(document): the larger of the compilation scale (CSCL) and the cell's largest feature SCAMIN. CSCL is the largest intended viewing scale (S-52 §3.1.7; S-65 Annex B §2.1.6 maps it to the S-101 optimum display scale), so on its own it would hide content producers encode to stay visible further out — e.g. USACE inland cells compiled at 1:5,000 with SCAMIN up to 1:300,000. ResolveCompilationScale(document) returns CSCL alone, which ranks the cell against overlapping cells.
  • S57ToS101Translator — translates an S57Document (package type) into an S101Document by remapping object/attribute codes, exploding multi-point soundings, synthesising the information complex attribute from textual fields, and converting nodes/edges/area-rings into S-101 spatial primitives.
  • S57ProductSpecification — the S-57 product specification codes a cell declares in its DSID/PRSP subfield: 1 (maritime ENC), 2 (Object Catalogue Data Dictionary), and 10 (inland ENC, as declared by IENC producers such as USACE), plus a TryParse for the raw subfield value.
  • S57S101Mapping — embedded code-mapping table sourced from IHO's S-57 → S-101 conversion guidance. ForSpec(spec) returns the table for a target product: Default for S-101, and for S-401 Default restricted (via RestrictToFeatureTypes) to the feature classes the bundled S-401 catalogue defines. The restriction clears 15 deep-sea and natural-feature targets (e.g. RAPIDS → Rapids, LITFLT → LightFloat), which S-401 translation then reports as rule-dropped. It also drops attribute targets the S-401 catalogue does not define (RestrictToAttributes; e.g. CATICE). CATBRG survives: its bridge category targets exist in both catalogues.
  • Inland ENC rules (internal InlandRules, part of the S-401 mapping) — the 53 object classes and 91 attributes of the IEHG Inland ENC Feature Catalogue 2.4 (codes 17000 and up). Most re-register a standard acronym in lower case (bridge = 17011 for BRIDGE = 11) and reuse the standard rule, as the IEHG S-57 ENC to S-401 Conversion Guidance prescribes; the rest map to the S-401 class or attribute that aliases the IENC acronym (notmrk → NoticeMark, wtwdis → waterwayDistance). hunits, the unit of wtwdis, becomes distanceUnitOfMeasurement with the guidance's value remap (hectometres 4 → 7, statute miles 5 → 4, nautical miles 6 → 5; feet has no S-401 code and is dropped), and only on the classes that bind it. A time schedule (tisdge) becomes an S-401 TimeScheduleInGeneral information record carrying cattab, schref, shptyp, useshp, aptref, dirimp and SORDAT. Every feature linked to it, through a C_ASSO or a feature pointer, references it with AdditionalInformation if its class can; that C_ASSO emits nothing itself. Every link is emitted, even beyond S-401's one AdditionalInformation per feature, because IENC encodes one schedule per ship type or period. A schedule no emitted feature can carry (e.g. one on a Bridge, which only takes ServiceHours) is reported as rule-dropped, and the schedule attributes are dropped on features. The usable lock and dock dimensions horcll/horclw become horizontalClearanceLength/horizontalClearanceWidth where the class binds them; on LockBasin, which binds no width, horclw fills horizontalClearanceFixed unless HORCLR already does. The shore-power attributes of a bunker station (catvol, catfrq, amoamp, catplg, shrnum, allcon) target the sub-attributes of S-401's powerCharacteristics; the translator assembles one instance per listed voltage and frequency on BunkerStation and drops them elsewhere. The bridge-arch collection c_brga emits no feature: the SpanFixed of its first member bridge links the other members' fixed spans with S-401's BridgeArchAssociation, and a c_brga with fewer than two fixed spans is reported as rule-dropped. Four codes have no S-401 home and are reported as rule-dropped: NEWOBJ and its three definition attributes (CLSDEF, CLSNAM, SYMINS). The ship-category, assembly and cargo ranges of a maximum permitted ship dimensions area (lc_csi/lc_cse, lc_asi/lc_ase, lc_cci/lc_cce) all convert; lc_csi is the one whose S-401 target carries no alias, so its rule comes from conversion guidance clause 3.82 rather than from the catalogue. Inland bridges convert exactly as maritime BRIDGE does: CATBRG categories on the Bridge, a SpanFixed/SpanOpening carrying the clearances, and point bridges as Landmark (the S-401 catalogue defines all of these); the fixed spans of one bridge arch are linked as above, and the IENC-only CATBRG value 13 (bridge arch) becomes bridgeConstruction 1 (arch), which keeps the arch a fixed span. Rules are resolved by ATTL (ResolveAttribute(ushort, …)), so an inland code and its upper-case twin never shadow each other.
  • S-401 anchorage rules (internal S401AnchorageRules, applied last in the S-401 mapping) — these replace the ACHARE/achare and CATACH/catach rules for S-401 only, so the S-101 default is unchanged.
    • Anchorage and anchor-berth CATACH 10 (IENC "anchorage for pushing-navigation vessels") becomes categoryOfAnchorage 16 (conversion guidance clauses 3.3 and 3.4).
    • An anchorage area whose CATACH is exactly 8 (small craft mooring area) becomes MooringArea, with categoryOfMooringArea 1 (clause 3.85).
    • Clause 3.85's title reads "catach=1, 2, 3". Those are the S-401 categoryOfMooringArea codes, not S-57 values: CATACH 1–3 are unrestricted, deep-water and tanker anchorages. So CATACH 1–3 stay on AnchorageArea.
    • A list such as 7,8 also stays on AnchorageArea and keeps every code, because S-401 categoryOfAnchorage binds 8 too.
  • S57TranslationTarget — the S-100 product a translation targets: the bundled Feature Catalogue its output is checked against and the product specification / edition the translated document declares. S101 is the default; S401 (edition 1.3.0) is for inland ENCs (issue #608). S-101 and S-401 share the S-101 document model, so S57ToS101Translator.ForTarget(target) builds a translator for either from the bundled mapping and catalogues (ForTarget(target, mapping) takes a custom mapping). A target whose catalogue has no RangeSystem class (S-401) skips the C_AGGR → RangeSystem pass and reports those aggregations as unmapped.
  • S101AllowedEnumValues — lazy-loaded helper that consults a bundled Feature Catalogue to drop emitted enumerated attribute values that aren't permitted by the destination FC binding. Default reads the S-101 FC; ForSpec(spec) reads another bundled FC (e.g. S-401), loaded once per spec. S101FeatureAttributeBindings follows the same Default / ForSpec shape and also answers DefinesFeatureType(code) and DefinesAttribute(code). Its Binds / IsSingleValued cover information types too, and BindsInformationType(feature, association, informationType) tells whether a feature class may reference an information type (e.g. S-401 LockBasin → AdditionalInformation → TimeScheduleInGeneral).

Translation behaviour

Aspect Notes
Object/attribute codes Looked up via the embedded S57S101Mapping rules (S-57 numeric code → S-101 acronym/code). Unknown S-57 attribute codes pass through; unknown enumerated values are dropped if the S-101 FC declares an allowable list.
Multi-point soundings (SOUNDG) Exploded into S-101 MultiPoint spatial records and a Sounding feature so each depth value is independently portrayable.
INFORM / TXTDSC (English) Carried on a shared NauticalInformation information type (not on the feature) as a single information complex instance with text and/or fileReference and language = "eng"; the feature binds it via an AdditionalInformation / theInformation information association.
NINFOM / NTXTDS (national language) Carried as a second information instance on the same NauticalInformation record with empty language (S-57 has no language tag; Data Producers can populate it post-conversion).
OBJNAM (English) Carried as an S-101 featureName complex attribute instance with name and language = "eng".
NOBJNM (national language) Carried as a separate featureName instance with empty language.
LITCHR / SIGGRP / SIGPER (light features) On feature classes that bind it (LightAllAround, LightFogDetector, LightAirObstruction), assembled into a single rhythmOfLight complex attribute instance: lightCharacteristic (mandatory), plus signalGroup / signalPeriod when present, and any SIGSEQ-derived signalSequence phases nested inside (see below). An out-of-range LITCHR code drops the instance (its mandatory sub-attribute would be missing). On non-light features (FogSignal, RadarTransponderBeacon) SIGGRP / SIGPER remain directly-bound simple attributes.
DATSTA / DATEND (date ranges) On feature classes that bind it, assembled into a fixedDateRange complex attribute instance with dateStart and/or dateEnd (both optional).
PERSTA / PEREND (periodic date ranges) On feature classes that bind it, assembled into a periodicDateRange complex attribute instance. Both dateStart and dateEnd are mandatory in the S-101 FC, so the instance is emitted only when both endpoints are present; otherwise the lone endpoint is dropped (and reported by the translation diagnostics).
SURSTA / SUREND (survey date ranges) On feature classes that bind it (QualityOfBathymetricData, QualityOfNonBathymetricData, QualityOfSurvey), assembled into a surveyDateRange complex attribute instance. dateEnd (SUREND) is mandatory, so the instance is emitted only when it is present; dateStart (SURSTA) is optional.
CATZOC (zone of confidence) On QualityOfBathymetricData (the sole feature class binding the complex), carried as the categoryOfZoneOfConfidenceInData sub-attribute of a zoneOfConfidence complex attribute instance. The S-57 and S-101 enumerations are identical (1=A1, 2=A2, 3=B, 4=C, 5=D, 6=U); an out-of-range code drops the instance. For quantified zones (A1–C) the nested horizontalPositionUncertainty and verticalUncertainty complexes are populated from the CATZOC-implied accuracy values of the IHO CATZOC table (IHO S-4 §B-290), each as an uncertaintyFixed fixed-metre term plus, where the table defines one, an uncertaintyVariableFactor percentage-of-depth term (e.g. A1 → ±5 m + 5% horizontal, 0.5 m + 1% vertical). ZOC D and U are unquantified, so only the category is emitted. fixedDateRange has no CATZOC-side source and is left unpopulated.
CATPRA (category of production area) Feature-dependent. On ProductionStorageArea (PRDARE) it passes through to categoryOfProductionArea, whose enumeration shares codes 1–12 with S-57 CATPRA. On OffshoreProductionArea (OSPARE) the FC binds the distinct categoryOfOffshoreProductionArea enumeration, so the value is redirected and remapped (8 Tank Farm → 4, 9 Wind Farm → 1, 12 Solar Farm → 6); S-57 production categories with no offshore equivalent (quarry, mine, stockpile, power station, refinery, timber yard, factory, slag/spoil, production plant) are dropped.
MORFAC (mooring/warping facility) + CATMOR S-101 has no MooringWarpingFacility class (it survives only in the sister product S-131); the S-101 portrayal catalogue wires the MORFAC symbols to Dolphin.lua (point) and ShorelineConstruction.lua (line/area), which sets the geometry default: point → Dolphin, line/area → ShorelineConstruction. CATMOR (a near-universal ~99.98% of corpus instances) refines specific point facilities that have their own S-101 class — 3 bollard → Bollard, 5 post/pile → Pile, 7 mooring buoy → MooringBuoy — which redirect regardless of the geometry default (all such instances are points in practice). On the remaining point Dolphins, CATMOR maps to categoryOfDolphin, but the S-57 and S-101 enumerations diverge (S-101: Mooring/Deviation/Berthing/Fender; S-57: dolphin/deviation-dolphin/bollard/tie-up-wall/pile/chain/mooring-buoy), so only the two coincident meanings are carried (1 dolphin → 1 Mooring Dolphin, 2 deviation dolphin → 2 Deviation Dolphin); 4 tie-up wall and 6 chain/wire/cable leave a Dolphin with no category. CATMOR is dropped wherever MORFAC redirects to a class that does not bind categoryOfDolphin (Bollard, Pile, MooringBuoy, ShorelineConstruction).
M_NSYS (navigational system of marks) + ORIENT S-65 Annex B § 12.2: an M_NSYS with a value in ORIENT converts to LocalDirectionOfBuoyage, which binds marksNavigationalSystemOf (MARSYS) and orientationValue (ORIENT) directly. Any other M_NSYS stays NavigationalSystemOfMarks, which binds no orientation, so an empty (unknown) ORIENT is dropped there (a rule drop in the diagnostics). The inland twin m_nsys (17018) splits the same way for S-401 (IEHG conversion guidance 3.77 / 3.88).
NATSUR / NATQUA (seabed) On SeabedArea (SBDARE, the sole feature class binding the complex), assembled into one or more surfaceCharacteristics complex attribute instances. The two S-57 lists are paired positionally: position i yields an instance carrying natureOfSurface = NATSUR[i] and natureOfSurfaceQualifyingTerms = NATQUA[i]. Both sub-attributes are optional, so a position with only one populated (e.g. NATQUA without NATSUR, the most common corpus case) still forms a valid instance; out-of-range enumerate codes are dropped individually. SeabedArea does not bind a top-level natureOfSurface, so on that feature NATSUR is routed exclusively into the complex; on other feature classes (Coastline, LandRegion, …) NATSUR remains a directly-bound simple natureOfSurface.
SIGSEQ (signal sequence) Parsed into signalSequence complex attribute instances. The S-57 value is a +-separated list of phase durations in seconds where a parenthesised duration denotes an eclipse/silence phase; each phase becomes one instance carrying signalDuration (real seconds) and signalStatus (1 = Lit/Sound for a bare duration, 2 = Eclipsed/Silent for a parenthesised one). On the light feature classes that bind rhythmOfLight the phases are nested inside that complex (its last sub-attribute per the FC), and are therefore emitted only when a rhythmOfLight instance is present (a valid LITCHR); on FogSignal / RadarTransponderBeacon signalSequence binds at the top level. Phases that do not parse as a real duration are dropped and reported.
Sector lights (SECTR1 / SECTR2 / COLOUR / VALNMR / LITVIS / LITCHR / SIGGRP / SIGPER / SIGSEQ) A LIGHTS object carrying a sector arc (SECTR1/SECTR2 present) is redirected from LightAllAround to LightSectored, and its light attributes are assembled into the mandatory sectorCharacteristics [1..] complex: lightCharacteristic (from LITCHR, mandatory) plus optional signalGroup/signalPeriod/signalSequence, and one lightSector carrying colour (from the COLOUR list), valueOfNominalRange (VALNMR), lightVisibility (from the LITVIS list), and a sectorLimit whose sectorLimitOne/sectorLimitTwo sectorBearing come from SECTR1/SECTR2 (three levels of nesting). Since each S-57 LIGHTS object encodes a single sector, one LightSectored feature is emitted per S-57 sector-light with one lightSector (conformant, as lightSector is [1..]); co-located sector arcs of one physical light — several LIGHTS objects sharing the same S-101 point — are merged into a single LightSectored feature carrying one sectorCharacteristics instance per arc (see Sector-light merge below). LightSectored binds none of these attributes at the top level, so all are diverted into the complex and none pass through as flat simple attributes.
Directional lights (CATLIT 1 / 16 + ORIENT) A LIGHTS object whose CATLIT list contains 1 (directional function) or 16 (moiré effect) is also redirected to LightSectored (S-65 Annex B §12.8.6.1; the IEHG S-401 guidance matches). Its ORIENT becomes lightSector → directionalCharacter → orientation → orientationValue, with moireEffect = true for CATLIT 16. orientation is mandatory in directionalCharacter, so the complex is only emitted when ORIENT has a value. A directional light without SECTR1/SECTR2 gets no sectorLimit, so the portrayal draws the direction line and oriented light flare. With a sector arc, it keeps its sectorLimit, which the portrayal draws in preference. ORIENT on a light that is not directional stays unconverted. CATLIT 1 and 16 have no categoryOfLight value and are still reported as dropped enumerate values.
TOPSHP / COLOUR / COLPAT (topmark) A TOPMAR object is not an S-101 feature; it is a slave of a buoy/beacon/LightFloat master (linked by the master's FFPT with the master/slave relationship indicator). Its attributes are folded into the master's topmark [0..1] complex: topmarkDaymarkShape (from TOPSHP, the mandatory [1..1] sub-attribute), colour (from the COLOUR list, split into occurrences), and colourPattern (from COLPAT). The fold happens only when the master resolves to one of the 12 S-101 classes binding topmark; a TOPMAR referenced by a non-binding master (or a non-slave relationship) stays unmapped. An instance whose mandatory topmarkDaymarkShape is missing or fails S-101 enumeration validation is rolled back and reported. The absorbed TOPMAR emits no feature of its own; the TopmarksAbsorbed diagnostic counts every TOPMAR consumed by a topmark-binding master (a rolled-back instance is still counted as absorbed even though no topmark is emitted).
HORCLR (horizontal clearance) Assembled into the mandatory horizontalClearanceValue [1..1] sub-attribute of a horizontal-clearance complex, selected per feature by the S-101 FC binding: horizontalClearanceOpen on Gate (GATCON) or horizontalClearanceFixed on the fixed-span classes that bind it (SpanFixed, SpanOpening, Tunnel, ShorelineConstruction, StructureOverNavigableWater, Canal, DockArea, LockBasin). The value is a real carried verbatim; the optional horizontalDistanceUncertainty has no S-57 source and is left unpopulated. On a feature binding neither complex HORCLR has no conformant home and stays unmapped — except on BRIDGE, where it moves to the bridge's span (see the BRIDGE row below).
VERCLR / VERCCL / VERCOP / VERCSA (vertical clearances) The rules target the complexes verticalClearanceFixed / Closed / Open / Safe. The translator assembles one where the resolved class binds it (e.g. CableOverhead, PipelineOverhead, Conveyor, Crane, Tunnel). The S-57 value goes to verticalClearanceValue; an empty (unknown) value stays empty, except on verticalClearanceOpen, whose value is optional. verticalClearanceOpen also gets verticalClearanceUnlimited = false. VERACC nests in each instance with a known value as verticalUncertainty/uncertaintyFixed (S-65 Annex B §2.2.4.3). A clearance whose complex the class doesn't bind is rule-dropped. S-401 sends a gate's VERCLR to verticalClearanceOpen (IEHG conversion guidance clause 3.55). S-65 has no such rule, so the S-101 translation drops it. Bridges are handled separately (see the BRIDGE row).
ORIENT (orientation) On directional lights it goes into directionalCharacter (see above). Otherwise it stays the top-level orientationValue on the classes that bind it directly (e.g. RecommendedTrack, RadarLine). On NavigationLine, CurrentNonGravitational, TidalStreamFloodEbb and Crane (and S-401 Daymark) it is assembled into the orientation complex, the only place those classes bind it. That lets the portrayal rotate current arrows and label navigation-line bearings (S-65 Annex B §3.3.1, §3.4, §10.1.1; IEHG conversion guidance clauses 3.27, 3.28, 3.32).
SORDAT (source date) Mapped to the top-level reportedDate simple attribute (an S100_TruncatedDate), the S-57 YYYYMMDD value carried verbatim (matching the fidelity of dateStart/dateEnd). Because SORDAT is a near-universal S-57 attribute but the FC binds reportedDate on only ~50 feature classes, the mapping is gated on the resolved feature actually binding reportedDate; on a feature that does not (e.g. Coastline, SeabedArea, LandRegion, DepthContour, the light classes) SORDAT has no conformant home and stays unmapped. SORIND (source indication) has no general S-101 equivalent (the FC's source attribute binds only UpdateInformation) and is intentionally not mapped.
VALLMA (value of local magnetic anomaly) Assembled into the valueOfLocalMagneticAnomaly complex on LocalMagneticAnomaly (LOCMAG, the only feature that binds it). The value feeds the mandatory magneticAnomalyValue [1..1] real sub-attribute verbatim; the optional referenceDirection enum has no S-57 source and is left unpopulated.
RADWAL (radar wave length) Assembled into the radarWaveLength complex on RadarTransponderBeacon (RTPBCN, binding it [0..2]). The list-typed S-57 value is a set of value-band pairs (e.g. 0.03-X or 0.03-X,0.10-S); each pair yields one complex instance carrying the mandatory waveLengthValue (real) and radarBand (text). A pair that does not split into both parts is dropped.
CURVEL (current velocity) Assembled into the speed complex on CurrentNonGravitational (CURENT) and TidalStreamFloodEbb (TS_FEB). The value feeds the mandatory speedMaximum [1..1] real sub-attribute verbatim; the optional speedMinimum has no S-57 source and is left unpopulated.
MLTYLT (multiplicity of lights) Assembled into the multiplicityOfFeatures complex on the light classes that bind it (LightAllAround, LightSectored, LightAirObstruction). The S-57 count feeds the optional numberOfFeatures integer verbatim and the mandatory multiplicityKnown boolean is set true.
CATBRG (category of bridge) S-101 has no categoryOfBridge; per S-65 Annex B (Ed 1.2.0) § 4.8.10 the S-57 list is split across the enumerations and Boolean that Bridge binds. Each listed value is mapped: 1 fixed → openingBridge = false; 2 opening → openingBridge = true; 3 swing / 4 lifting / 5 bascule / 7 draw → categoryOfOpeningBridge (same codes); 6 pontoon → bridgeConstruction 3, 8 transporter → bridgeConstruction 5, 10 viaduct → bridgeConstruction 2, 12 suspension → bridgeConstruction 4; 9 footbridge → bridgeFunction 3 (pedestrian), 11 aqueduct → bridgeFunction 4. S-65 does not rule on 13 bridge arch, which is an IENC extension of the attribute (IENC Feature Catalogue Ed. 2.4); the IEHG S-57 ENC to S-401 Conversion Guidance (Ed 1.3.0 draft 2) clauses 3.7 and 3.144 map it to bridgeConstruction 1 (arch), and that rule is applied for both targets because bridgeConstruction 1 is “Arch” in the S-101 and the S-401 catalogue alike. openingBridge is emitted once for the whole list: true if any value is 2 or an opening-bridge category (so CATBRG = 2,6 is an opening pontoon bridge), otherwise false. bridgeFunction is multi-valued; bridgeConstruction and categoryOfOpeningBridge are [0..1], so only the first such value is kept and later ones are reported under RuleDroppedAttributes. Values outside the list are dropped and reported under DroppedEnumValues. The value table lives in the BRIDGE feature rule (DefaultRules); S101FeatureAttributeBindings.IsSingleValued supplies the multiplicity.
BRIDGE → Bridge + SpanFixed / SpanOpening + BridgeAggregation Follows S-65 Annex B (Ed 1.2.0) §4.8.10 and S-101 DCEG 6.6–6.8 / 25.4. A curve/surface BRIDGE becomes a Bridge. If it crosses navigable water it also gets a span component. That span is a SpanOpening when CATBRG contains 2, 3, 4, 5 or 7 (opening, swing, lifting, bascule, draw), otherwise a SpanFixed. The span shares the bridge's geometry and S-57 identity, and the Bridge links to it with a BridgeAggregation / theComponent feature association. The translator can't tell "navigable water" from one object, so it keys on the clearance the span class makes mandatory. A fixed span needs VERCLR, which feeds verticalClearanceFixed [1..1]. An opening span needs VERCCL, which feeds verticalClearanceClosed [1..1]. Presence is what counts: an S-57 empty (unknown) value still yields the span, with verticalClearanceValue populated as empty (null). A bridge without that clearance converts to a Bridge alone. The clearance attributes bind only on the spans, so they never appear on Bridge: VERCLR/VERCCL/VERCOP go to the verticalClearanceValue of verticalClearanceFixed/Closed/Open, and HORCLR goes to horizontalClearanceFixed. HORACC becomes that complex's horizontalDistanceUncertainty (§2.2.4.2). VERACC becomes the nested verticalUncertainty/uncertaintyFixed of each vertical clearance that has a known value (§2.2.4.3). VERDAT becomes the span's verticalDatum (§2.1.2). SCAMIN and DATSTA/DATEND are copied to the span so it displays and date-filters with its bridge. On an opening span, verticalClearanceOpen gets verticalClearanceUnlimited = true when VERCOP is absent, and false when VERCOP is present, even with an empty value. Clearance values with no home (e.g. VERCLR on an opening bridge, or any clearance on a bridge with no span) are recorded as rule-dropped. A point BRIDGE becomes a Landmark, because Bridge permits no point geometry. It gets categoryOfLandmark = 26 (bridge) and, when CONVIS is absent, visualProminence = 2 (not visually conspicuous). These are the defaults S-65 gives for the analogous point DAMCON → Landmark conversion (§4.8.5/§4.8.15). CATBRG and the vertical-clearance/datum attributes are dropped there. The BridgeSpansEmitted diagnostic counts spans. Across the 7,184-cell NOAA base corpus, the 20,413 BRIDGE objects (none of them points) yield 20,248 spans: 18,557 fixed and 1,691 opening.
C_AGGR of BRIDGE (+ PYLONS / PONTON, lights) → one Bridge S-65 Annex B §4.8.10 asks producers to encode each span of a bridge over navigable water as its own BRIDGE object and to group them, plus any pylons or pontoons, with a C_AGGR. The converter should then build a single Bridge from them. A C_AGGR qualifies when it has at least one curve/surface BRIDGE member, every member resolves by LNAM, no BRIDGE member is a point or already claimed (each BRIDGE belongs to at most one collection; the first in document order wins), and no member is a navigation line or track (that makes it a track grouping through the bridge, e.g. NOAA US5WI3FK, left to the range-system handling). Any other member does not disqualify it: IENC collects fenders, notice marks, signal stations, mooring facilities and more with a bridge (IENC Encoding Guide 2.4.1, bridge clause H), and those convert on their own. Each member BRIDGE emits only its span. One aggregated Bridge is then emitted, carrying the C_AGGR's feature identity, with a BridgeAggregation / theComponent association to every emitted span, pylon and pontoon, and a StructureEquipment / theEquipment association to every emitted light and signal station member (IENC collects with a bridge its vertical clearance indicators, sistaw catsiw 16, Encoding Guide clause I.3.3, and its bridge-passage traffic signal stations, sistat catsit 8, clause R.2.1). IENC places bridge lights on the navigable span and the piers bounding it with no master object (bridge light clause C), so collection membership is all that ties a light to its bridge: it is linked to the Bridge, the only S-401 structure that binds more than one light. Each link is gated on the target Feature Catalogue binding it, and a light listed by two collections is linked by the first only (S-101 binds a light to at most one structure); BridgeEquipmentLinked counts them. Its Bridge-level attributes come from a representative member, chosen in this order: the first opening member (S-101 requires an opening bridge when any span opens), then the first named member, then the first member. Any attribute carried on the C_AGGR itself (typically OBJNAM) takes precedence. If neither the C_AGGR nor the representative is named, the first named member supplies the name. Members' own names that differ from the one the Bridge carries are kept as further featureName instances that are not for chart display: nameUsage 3 (No Chart Display) in S-401, and no nameUsage in S-101, which lists only 1 and 2 (spans bind no featureName in either). The geometry is the members' geometry merged into one: curves are chained by shared nodes, and adjacent surfaces lose their shared edges. If the members don't join into a single curve or a single exterior ring (twin or dual bridges, parts separated by gaps), the Bridge is multi-part (S-100 Part 10a SPAS 0..*): one chain of curve segments per connected run, walked from a free end, or one surface per exterior ring with the holes that lie inside it. An outline ring that lies inside another is a hole of the union (members around a gap), not a surface of its own. The renderers and query tools treat each part on its own (#643). Only when the members mix curves and surfaces is the collection not aggregated: its members convert one by one, each as its own Bridge with its own geometry and name, and each light is linked to the nearest member Bridge (by S-57 geometry). A Bridge without geometry would have its name left undrawn by the S-101/S-401 portrayal. BridgeCollectionsUnjoined counts these collections. The BridgeAggregationsEmitted diagnostic counts these bridges. A C_AGGR that does not qualify falls through to the range-system handling below. None of the local NOAA, French or IC-ENC cells contain a qualifying collection; of the 744 bridge collections in the 109 USACE inland cells, 743 aggregate (111 of them as multi-surface bridges) and 1 mixes curves and surfaces, and all 4,246 of their lights are linked.
C_AGGR (aggregation) → RangeSystem + RangeSystemAggregation An S-57 C_AGGR collection whose members (resolved via its FFPT peer feature-pointers, keyed by LNAM) are all permitted RangeSystemAggregation components and include at least one navigational track (NavigationLine, RecommendedTrack, or RecommendedRouteCentreline) is mapped to a synthesised geometry-less RangeSystem collection feature (FC binds theCollection to a dedicated RangeSystem class, not to a member). One RangeSystemAggregation / theComponent feature association is emitted from the RangeSystem to each resolved member (populating the previously-empty FeatureAssociationCatalogue / RoleCatalogue). A two-phase pass allocates and registers every qualifying RangeSystem's record before wiring associations, so nested C_AGGR members (RangeSystem is itself a permitted component) resolve without dangling references. A C_AGGR that has no track member or any non-permitted member has no S-101 home and stays unmapped (counted under UnmappedObjectClasses). C_ASSO (whose NOAA-corpus majority is LandArea-partition groupings) has no S-101 named-association equivalent and is left unmapped.
Spatial relationships S-57 vector pointer records (VRPT, FSPT) translated to S-101 spatial associations and ring orientation.
Complex-attribute nesting Every attribute row carries its ISO 8211 PAIX (S-100 Part 10a): the 1-based position of the complex attribute row it belongs to, or 0 for a row bound directly to the feature. The translator writes complexes in pre-order and sets PAIX from the Feature Catalogue's sub-attribute bindings in a final pass, so zoneOfConfidence → categoryOfZoneOfConfidenceInData, featureName → name / language and the deeper nests (sectorCharacteristics → lightSector → sectorLimit → …) are explicit in the output.
Feature identifiers (FOID) Each S-101 feature keeps the agency, FIDN and FIDS of its S-57 object. A feature the translator derives from an object that also yields another feature (the SpanFixed / SpanOpening of a BRIDGE) keeps the agency and FIDN and takes the next FIDS (wrapping from 65535 to 0) that no feature in the dataset uses; the primary feature (Bridge) keeps the S-57 identifier. Distinct S-57 objects that share a FOID in the source keep it, so S101-R-2.1 still reports the source duplicate. DerivedFeatureIdentifiersAssigned counts the re-identified features.
List-valued enumerations (COLOUR, NATSUR, CATLIT, …) S-57 encodes multiple enumerate selections as a comma-separated string (e.g. COLOUR = "3,1"). Each code is split into a separate S-101 attribute occurrence (distinct ATIX) and validated independently, so one invalid code no longer discards the whole attribute.

Limitations

  • Sibling updates are applied on the exchange-set and CLI paths. Within an exchange set, each base cell's in-set sequential updates (.001, .002, …) are folded in via S57Document.ApplyChanges before translation. The s100 s57 convert CLI likewise auto-discovers and folds sibling update files sitting next to a loose base .000, then writes the product that the update-folded cell's TranslationTarget names (S-401 for an inland ENC, otherwise S-101) unless --target overrides it. The low-level S57Dataset.Open(string) overload still reads a bare base cell only; use S57Dataset.Open(Stream, IReadOnlyList<Stream>) to apply updates programmatically.
  • M_COVR "no coverage available" is dropped. An M_COVR meta-object flagged CATCOV = 2 ("no coverage available") is not translated. S-101 has no categoryOfCoverage concept — its DataCoverage feature always asserts coverage — so emitting a DataCoverage for a no-data region would falsely claim data over the cell's hole and drive cross-cell scale-band overlap suppression to blank the coarser overlapping cell there (issue #438). Coverage-available M_COVR (CATCOV = 1, or absent) still translates to DataCoverage normally; the drop is counted by the RuleDroppedObjectClasses diagnostic.
  • Breadth-first feature coverage. Feature-class coverage is derived from the bundled S-101 Feature Catalogue's <S100FC:alias> (S-57 acronym) bridge and validated against a 3,636-cell NOAA ENC corpus: ~99% of feature instances now translate. The classes that remain unmapped are ones that are aggregation/collection objects with no S-101 named-association home (C_ASSO, and any C_AGGR that is neither a range system nor a bridge collection), or are meta objects with no S-101 equivalent (M_CSCL). Objects that become S-101 attributes rather than features are folded into their host: TOPMAR folds into the master buoy/beacon's topmark complex via the S-57 master/slave relationship (see the attribute table above). Geometry-conditional splits are handled where the S-101 model requires them (e.g. MORFAC → Dolphin/ShorelineConstruction by primitive, refined by CATMOR, and point BRIDGE → Landmark; see the attribute table above). S-101's bridge decomposition is synthesised too: each BRIDGE over navigable water becomes a Bridge plus a SpanFixed/SpanOpening, and a bridge C_AGGR becomes one Bridge with its span, pylon and pontoon components. Range-system C_AGGR collections are synthesised into RangeSystem features (see below).
  • Simple attributes plus selected complex attributes. Most attribute rules cover S-57 attributes that map to a directly feature-bound simple S-101 attribute. A simple attribute passes through only when the resolved class binds it in the target Feature Catalogue. Otherwise it is counted in RuleDroppedAttributes, matching the conversion guidance's "not converted" exceptions (e.g. WATLEV/NATCON on Pile, COLOUR on SeabedArea, CATTSS on the traffic separation scheme parts). The featureName (OBJNAM/NOBJNM), rhythmOfLight (LITCHR/SIGGRP/SIGPER) with its nested signalSequence (SIGSEQ), the three date-range complex attributes — fixedDateRange (DATSTA/DATEND), periodicDateRange (PERSTA/PEREND), and surveyDateRange (SURSTA/SUREND) — the zoneOfConfidence (CATZOC) complex — including its nested horizontalPositionUncertainty / verticalUncertainty uncertainty complexes derived from the IHO CATZOC table — surfaceCharacteristics (NATSUR/NATQUA), the three-level sectorCharacteristics/lightSector/sectorLimit sector geometry on LightSectored (SECTR1/SECTR2/COLOUR/VALNMR/LITVIS plus the light-characteristic attributes), and the horizontalClearanceOpen/horizontalClearanceFixed complexes (HORCLR), the valueOfLocalMagneticAnomaly complex (VALLMA) on LocalMagneticAnomaly, the radarWaveLength complex (RADWAL) on RadarTransponderBeacon, the speed complex (CURVEL) on CurrentNonGravitational/TidalStreamFloodEbb, and the multiplicityOfFeatures complex (MLTYLT) on the light classes are assembled, gated on the S-101 Feature Catalogue's per-feature attribute bindings. signalSequence also binds at the top level on FogSignal / RadarTransponderBeacon. Assembled complex sub-attributes are validated against the destination simple attribute's global enumeration rather than the (sometimes narrower) per-complex permittedValues subset, matching the fidelity of the rest of the pipeline. Attributes that are sub-attributes of the remaining S-101 complex attributes are still left unmapped pending further complex-attribute assembly. SORDAT→reportedDate is mapped as a top-level simple attribute, gated on the resolved feature binding reportedDate (the ~50 feature classes the FC allows); on other features SORDAT stays unmapped. SORIND has no general S-101 equivalent and is intentionally not mapped.
  • Listed-value remapping is best-effort. Some S-57 enumerated attribute values have different numeric codes in S-101; values that aren't permitted by the destination S-101 FC binding are dropped (and reported by the translation diagnostics). Multi-valued (list-type) S-57 enumerations are split into individual S-101 occurrences first, so only the specific invalid codes are dropped rather than the whole attribute.
  • Complex-attribute synthesis covers the common cases. The most frequent S-101 complex attributes are assembled (see the table above), including sectored-light geometry (LightSectored). Co-located sector arcs of one physical light — multiple S-57 LIGHTS objects that each carry a single sector (SECTR1/SECTR2) and resolve to the same S-101 point — are folded into a single LightSectored feature carrying one sectorCharacteristics instance per arc (FC allows [1..*]); the lowest-document-order light is the primary and the rest are absorbed (they emit no feature of their own). The SectorLightsMerged diagnostic counts the absorbed members. Non-sector attributes (height, status, name) come from the primary, matching the corpus where these are invariant within a physical light. The nested uncertainty complexes of zoneOfConfidence (horizontalPositionUncertainty / verticalUncertainty) are populated for quantified zones (A1–C) from the CATZOC-implied IHO CATZOC accuracy table; ZOC D/U remain category-only as their accuracy is unquantified.
  • INFORM/TXTDSC/NINFOM/NTXTDS are emitted as a NauticalInformation information type bound to the feature by an AdditionalInformation / theInformation information association (the "fuller path" in the conversion guidance), rather than as an inline information complex on the feature. A feature with any of the four textual attributes yields one NauticalInformation record (English information instance and/or a national-language instance); the NauticalInformationTypesEmitted diagnostic counts the records emitted. Portrayal is unchanged — the S-101 portrayal reads the associated NauticalInformation (ProcessNauticalInformation), so text/pictorial notes render identically to the former inline encoding.
  • Area rings are reassembled by node contiguity. An S-57 area's boundary edges (FSPT) are chained into rings by shared begin/end node identity — reversing individual edges as needed — rather than by relying on the FSPT listing order. Because S-57 lists every interior edge consecutively (with USAG = interior) regardless of which hole it belongs to, grouping by USAG alone would merge all of an area's holes into a single boundary; flattening that merged ring to coordinates then jumps between holes and renders as long "spike" artifacts through a curve. Chaining reconstructs one closed ring per hole, so each interior boundary becomes its own S-101 interior ring.
  • Bridge spans depend on the clearance attributes. S-57 can't say whether a bridge crosses navigable water, so a BRIDGE gets a SpanFixed/SpanOpening only when it carries the clearance attribute that span class makes mandatory (VERCLR or VERCCL). A navigable bridge encoded without it converts to a Bridge alone and its other clearances are dropped. An opening bridge encoded with VERCLR instead of VERCCL is handled the same way. The span type follows the same CATBRG test as Bridge's openingBridge (see the CATBRG row), so a SpanOpening is only ever attached to an opening Bridge. When several separately encoded BRIDGE spans aren't grouped by a C_AGGR, each still converts to its own Bridge + span pair.
  • Range-system and bridge aggregations are synthesised; other collections are not. A qualifying C_AGGR (all members are permitted RangeSystemAggregation components and at least one is a navigational track) is emitted as a geometry-less RangeSystem collection feature with one RangeSystemAggregation / theComponent feature association per member; the RangeSystemsEmitted diagnostic counts them. Across the 7,184-cell NOAA base corpus this synthesises 2,083 RangeSystem features (931 cells) with 6,690 component associations and zero dangling or component-less collections. C_AGGR groupings that are neither range systems nor bridge collections (see the BRIDGE rows in the table above), and all C_ASSO associations (whose corpus majority is LandArea-partition groupings), have no S-101 named-association equivalent and stay unmapped. (For S-401, a C_ASSO that links an inland time schedule is consumed by the time-schedule conversion.)

Translation diagnostics

S57ToS101Translator.Translate has an optional overload that accepts an S57TranslationDiagnostics sink. When supplied, the translator records — as compact aggregate counters — exactly what it dropped and why, so a caller (for example a corpus-wide conversion audit) can quantify coverage gaps in the embedded S57S101Mapping without re-deriving the translator's logic:

var diagnostics = new S57TranslationDiagnostics();
var s101 = new S57ToS101Translator().Translate(s57, diagnostics);

// Object classes present in the data with no mapping rule (a coverage gap):
foreach (var (objl, count) in diagnostics.UnmappedObjectClasses) { /* … */ }

Passing null (or using the parameterless Translate overloads) disables collection with zero overhead. The counters distinguish a genuine gap (no rule — UnmappedObjectClasses / UnmappedAttributes) from a by-design drop (a rule exists but resolves to nothing — RuleDroppedObjectClasses / RuleDroppedAttributes), and separately report FC-rejected enumerate values (DroppedEnumValues), geometry loss (FeaturesDroppedForNoGeometry), and sounding point accounting. Portrayal-affecting synthesis is also counted: SectorLightsMerged (co-located sector arcs absorbed into one LightSectored), TopmarksAbsorbed (TOPMAR slaves consumed by a topmark-binding master), NauticalInformationTypesEmitted (NauticalInformation records created for INFORM/TXTDSC/NINFOM/NTXTDS), and RangeSystemsEmitted (geometry-less RangeSystem collection features synthesised from range-system C_AGGR aggregations), and TimeSchedulesEmitted (S-401 TimeScheduleInGeneral records created for inland tisdge time schedules), and DerivedFeatureIdentifiersAssigned (derived features, such as bridge spans, given their own FIDS).

Validation

S57DatasetProcessor.Validate() runs validation in two phases:

  1. Pre-translation — S57PreTranslationRules.Default checks the raw EncDotNet.S57.S57Document for things that don't survive translation (DSID / DSPM presence and a positive compilation scale; the presence of an M_COVR meta-feature).
  2. Post-translation — the translated S-101 document is fed into S101DatasetRules.Default (the same pack that runs against native S-101 datasets) and the resulting findings are rebadged with the prefix S101-as-S57/ so the user can tell whether a problem originated in the raw S-57 input or in the translated S-101 projection. The translation is itself checked against this pack: across the 7,184 cells of the NOAA ENC corpus the translated S-101 reports no findings other than the source data's own duplicate FOIDs. Findings about a feature or spatial record carry its location (a point, or the envelope of its geometry), so the viewer's validation overlay can mark them.

Pre-translation rules shipped by S57PreTranslationRules.Default:

Rule id Severity Checks
S57-R-1.1 Error DSID record present AND DSPM compilation scale denominator (CSCL) > 0.
S57-R-1.2 Warning At least one M_COVR meta-feature is present in the cell.
S57-PROJ-PARSE — Placeholder reserving the namespace for future parser-diagnostic findings.

The two reports are concatenated by ConcatReports.Concat(pre, post, rebadgePrefix: "S101-as-S57/") in EncDotNet.S100.Datasets.Pipelines and cached on the processor.

Exchange-set integrity verification

S57ExchangeSetVerification integrity-verifies an S-57 / S-63 exchange set (a folder containing a CATALOG.031) and maps the result onto the same S-100 ExchangeSetVerificationResult model used for native S-100 exchange sets, so both products surface uniformly through the CLI (s100 validate) and any future viewer wiring.

using EncDotNet.S100.Datasets.S57;
using EncDotNet.S100.ExchangeSets;

ExchangeSetVerificationResult result =
    await S57ExchangeSetVerification.VerifyAsync("/path/to/s57set");

bool integrity = result.IntegrityVerified;   // no CRC mismatches / missing files
bool signed    = result.AllValid;            // strict: every file signature Ok

It is a deliberately thin adapter, not a shared interface: the upstream S-57 verifier (EncDotNet.S57 0.5.0+) is rooted at a directory path (CATALOG.031 plus the files it references), whereas the S-100 verifier is IAssetSource-based. Forcing both behind one interface would distort both, so only the result is mapped.

Mapping notes:

  • Outcomes are mapped by name (S57VerificationOutcome → VerificationOutcome). The two enums are kept byte-identical; mapping by name means a future divergence fails loudly rather than silently mis-mapping.
  • Both the signature dimension (Outcome) and the checksum dimension (ChecksumOutcome) are preserved independently.
  • S-57 integrity uses a CRC-32 (the CATALOG.031 field), not the SHA-256 digest the S-100 model carries, so the per-file CRC values are surfaced through FileVerificationResult.Detail and ComputedSha256 is left null.
  • The verdict semantics match S-100: NoChecksum / NotSigned are non-failing (an unsigned-but-intact set has IntegrityVerified == true but AllValid == false); only ChecksumMismatch / FileMissing / Error / invalid signatures fail integrity. This is identical to the upstream S-57 AllValid.
  • Today the upstream S-57 verifier reports every file as NotSigned (S-63 signature verification is a seam); CRC checking is fully wired.

Exchange-set cell enumeration

S57ExchangeSetCatalog is the loading-side companion to the verification adapter above. It reads a CATALOG.031 (via EncDotNet.S57's S57CatalogReader) and groups the catalogued CATD files into renderable base cells, each with its in-set sequential updates and (optionally) its extent:

using EncDotNet.S100.Datasets.S57;

IReadOnlyList<S57ExchangeSetCell> cells =
    S57ExchangeSetCatalog.ReadBaseCells("/path/to/s57set");

foreach (S57ExchangeSetCell cell in cells)
{
    // cell.RelativePath          → "…/US5MA1BO.000" (platform-normalised)
    // cell.UpdateRelativePaths   → ["…/US5MA1BO.001", "…/US5MA1BO.002"]
    // cell.BoundingBox           → EPSG:4326 extent, or null
}

BoundingBox? union = S57ExchangeSetCatalog.UnionBoundingBox(cells);

Files are grouped by their 8-character cell name; the .000 file is the base and .001, .002, … are its updates in application order. Non-dataset entries (.TXT, certificates, the catalogue itself) are ignored, and update files with no matching base are skipped. The pure grouping logic is exposed as SelectBaseCells(S57Catalog) for unit testing without a real catalogue on disk. The viewer pairs this enumeration with a FileSystemAssetSource rooted at the exchange-set directory so each cell flows through the same S57DatasetProcessor code path as a single dropped .000 file (with its in-set updates folded in via S57Document.ApplyChanges before translation to S-101).

Like the verification adapter, this is a deliberately thin adapter over the directory-rooted S-57 model rather than a shared interface.

Usage

using EncDotNet.S100.Datasets.S57;
using EncDotNet.S100.Datasets.S101;

var s57 = S57Dataset.Open("US5NY16M.000");
var s101Document = new S57ToS101Translator().Translate(s57);
var dataset = S101Dataset.FromDocument(s101Document);
// Use the existing S-101 pipeline from here:
// S101LuaRuleExecutor, FeatureGeometryProvider&lt;Feature&gt;, etc.

Installation

dotnet add package EncDotNet.S100.Datasets.S57