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.000file and exposes the parsedEncDotNet.S57.S57Documentfrom the upstream package, the S-57 product specification it declares (DeclaredProductSpecification) and theTranslationTargetthat follows from it (S-401 for an inland ENC, otherwise S-101; staticTranslationTargetFor(document)for an already-parsed document), plus a staticIsS57File(path)discriminator used byEncDotNet.S100.Datasets.Pipelinesto disambiguate.000files that could otherwise be S-101. Also exposes a cheapReadMetadata"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 fromResolveCellMinimumDisplayScale(document): the larger of the compilation scale (CSCL) and the cell's largest featureSCAMIN. 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 withSCAMINup to 1:300,000.ResolveCompilationScale(document)returns CSCL alone, which ranks the cell against overlapping cells.S57ToS101Translator— translates anS57Document(package type) into anS101Documentby remapping object/attribute codes, exploding multi-point soundings, synthesising theinformationcomplex 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 itsDSID/PRSPsubfield:1(maritime ENC),2(Object Catalogue Data Dictionary), and10(inland ENC, as declared by IENC producers such as USACE), plus aTryParsefor 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:Defaultfor S-101, and for S-401Defaultrestricted (viaRestrictToFeatureTypes) 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).CATBRGsurvives: 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 forBRIDGE= 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 ofwtwdis, becomesdistanceUnitOfMeasurementwith 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-401TimeScheduleInGeneralinformation record carryingcattab,schref,shptyp,useshp,aptref,dirimpandSORDAT. Every feature linked to it, through aC_ASSOor a feature pointer, references it withAdditionalInformationif its class can; thatC_ASSOemits nothing itself. Every link is emitted, even beyond S-401's oneAdditionalInformationper feature, because IENC encodes one schedule per ship type or period. A schedule no emitted feature can carry (e.g. one on aBridge, which only takesServiceHours) is reported as rule-dropped, and the schedule attributes are dropped on features. The usable lock and dock dimensionshorcll/horclwbecomehorizontalClearanceLength/horizontalClearanceWidthwhere the class binds them; onLockBasin, which binds no width,horclwfillshorizontalClearanceFixedunlessHORCLRalready does. The shore-power attributes of a bunker station (catvol,catfrq,amoamp,catplg,shrnum,allcon) target the sub-attributes of S-401'spowerCharacteristics; the translator assembles one instance per listed voltage and frequency onBunkerStationand drops them elsewhere. The bridge-arch collectionc_brgaemits no feature: theSpanFixedof its first member bridge links the other members' fixed spans with S-401'sBridgeArchAssociation, and ac_brgawith fewer than two fixed spans is reported as rule-dropped. Four codes have no S-401 home and are reported as rule-dropped:NEWOBJand 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_csiis the one whose S-401 target carries noalias, so its rule comes from conversion guidance clause 3.82 rather than from the catalogue. Inland bridges convert exactly as maritimeBRIDGEdoes:CATBRGcategories on theBridge, aSpanFixed/SpanOpeningcarrying the clearances, and point bridges asLandmark(the S-401 catalogue defines all of these); the fixed spans of one bridge arch are linked as above, and the IENC-onlyCATBRGvalue 13 (bridge arch) becomesbridgeConstruction1 (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 theACHARE/achareandCATACH/catachrules for S-401 only, so the S-101 default is unchanged.- Anchorage and anchor-berth
CATACH10 (IENC "anchorage for pushing-navigation vessels") becomescategoryOfAnchorage16 (conversion guidance clauses 3.3 and 3.4). - An anchorage area whose
CATACHis exactly 8 (small craft mooring area) becomesMooringArea, withcategoryOfMooringArea1 (clause 3.85). - Clause 3.85's title reads "catach=1, 2, 3". Those are the S-401
categoryOfMooringAreacodes, not S-57 values:CATACH1–3 are unrestricted, deep-water and tanker anchorages. SoCATACH1–3 stay onAnchorageArea. - A list such as
7,8also stays onAnchorageAreaand keeps every code, because S-401categoryOfAnchoragebinds 8 too.
- Anchorage and anchor-berth
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.S101is the default;S401(edition 1.3.0) is for inland ENCs (issue #608). S-101 and S-401 share the S-101 document model, soS57ToS101Translator.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 noRangeSystemclass (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.Defaultreads the S-101 FC;ForSpec(spec)reads another bundled FC (e.g. S-401), loaded once per spec.S101FeatureAttributeBindingsfollows the sameDefault/ForSpecshape and also answersDefinesFeatureType(code)andDefinesAttribute(code). ItsBinds/IsSingleValuedcover information types too, andBindsInformationType(feature, association, informationType)tells whether a feature class may reference an information type (e.g. S-401LockBasin→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 viaS57Document.ApplyChangesbefore translation. Thes100 s57 convertCLI likewise auto-discovers and folds sibling update files sitting next to a loose base.000, then writes the product that the update-folded cell'sTranslationTargetnames (S-401 for an inland ENC, otherwise S-101) unless--targetoverrides it. The low-levelS57Dataset.Open(string)overload still reads a bare base cell only; useS57Dataset.Open(Stream, IReadOnlyList<Stream>)to apply updates programmatically. M_COVR"no coverage available" is dropped. AnM_COVRmeta-object flaggedCATCOV = 2("no coverage available") is not translated. S-101 has nocategoryOfCoverageconcept — itsDataCoveragefeature always asserts coverage — so emitting aDataCoveragefor 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-availableM_COVR(CATCOV = 1, or absent) still translates toDataCoveragenormally; the drop is counted by theRuleDroppedObjectClassesdiagnostic.- 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 anyC_AGGRthat 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:TOPMARfolds into the master buoy/beacon'stopmarkcomplex 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/ShorelineConstructionby primitive, refined byCATMOR, and pointBRIDGE→Landmark; see the attribute table above). S-101's bridge decomposition is synthesised too: eachBRIDGEover navigable water becomes aBridgeplus aSpanFixed/SpanOpening, and a bridgeC_AGGRbecomes oneBridgewith its span, pylon and pontoon components. Range-systemC_AGGRcollections are synthesised intoRangeSystemfeatures (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/NATCONonPile,COLOURonSeabedArea,CATTSSon the traffic separation scheme parts). ThefeatureName(OBJNAM/NOBJNM),rhythmOfLight(LITCHR/SIGGRP/SIGPER) with its nestedsignalSequence(SIGSEQ), the three date-range complex attributes —fixedDateRange(DATSTA/DATEND),periodicDateRange(PERSTA/PEREND), andsurveyDateRange(SURSTA/SUREND) — thezoneOfConfidence(CATZOC) complex — including its nestedhorizontalPositionUncertainty/verticalUncertaintyuncertainty complexes derived from the IHO CATZOC table —surfaceCharacteristics(NATSUR/NATQUA), the three-levelsectorCharacteristics/lightSector/sectorLimitsector geometry onLightSectored(SECTR1/SECTR2/COLOUR/VALNMR/LITVISplus the light-characteristic attributes), and thehorizontalClearanceOpen/horizontalClearanceFixedcomplexes (HORCLR), thevalueOfLocalMagneticAnomalycomplex (VALLMA) onLocalMagneticAnomaly, theradarWaveLengthcomplex (RADWAL) onRadarTransponderBeacon, thespeedcomplex (CURVEL) onCurrentNonGravitational/TidalStreamFloodEbb, and themultiplicityOfFeaturescomplex (MLTYLT) on the light classes are assembled, gated on the S-101 Feature Catalogue's per-feature attribute bindings.signalSequencealso binds at the top level onFogSignal/RadarTransponderBeacon. Assembled complex sub-attributes are validated against the destination simple attribute's global enumeration rather than the (sometimes narrower) per-complexpermittedValuessubset, 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→reportedDateis mapped as a top-level simple attribute, gated on the resolved feature bindingreportedDate(the ~50 feature classes the FC allows); on other featuresSORDATstays unmapped.SORINDhas 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-57LIGHTSobjects that each carry a single sector (SECTR1/SECTR2) and resolve to the same S-101 point — are folded into a singleLightSectoredfeature carrying onesectorCharacteristicsinstance 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). TheSectorLightsMergeddiagnostic 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 ofzoneOfConfidence(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/NTXTDSare emitted as aNauticalInformationinformation type bound to the feature by anAdditionalInformation/theInformationinformation association (the "fuller path" in the conversion guidance), rather than as an inlineinformationcomplex on the feature. A feature with any of the four textual attributes yields oneNauticalInformationrecord (Englishinformationinstance and/or a national-language instance); theNauticalInformationTypesEmitteddiagnostic counts the records emitted. Portrayal is unchanged — the S-101 portrayal reads the associatedNauticalInformation(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 theFSPTlisting order. Because S-57 lists every interior edge consecutively (withUSAG = interior) regardless of which hole it belongs to, grouping byUSAGalone 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
BRIDGEgets aSpanFixed/SpanOpeningonly when it carries the clearance attribute that span class makes mandatory (VERCLRorVERCCL). A navigable bridge encoded without it converts to aBridgealone and its other clearances are dropped. An opening bridge encoded withVERCLRinstead ofVERCCLis handled the same way. The span type follows the sameCATBRGtest asBridge'sopeningBridge(see theCATBRGrow), so aSpanOpeningis only ever attached to an openingBridge. When several separately encodedBRIDGEspans aren't grouped by aC_AGGR, each still converts to its ownBridge+ span pair. - Range-system and bridge aggregations are synthesised; other collections are not. A qualifying
C_AGGR(all members are permittedRangeSystemAggregationcomponents and at least one is a navigational track) is emitted as a geometry-lessRangeSystemcollection feature with oneRangeSystemAggregation/theComponentfeature association per member; theRangeSystemsEmitteddiagnostic counts them. Across the 7,184-cell NOAA base corpus this synthesises 2,083RangeSystemfeatures (931 cells) with 6,690 component associations and zero dangling or component-less collections.C_AGGRgroupings that are neither range systems nor bridge collections (see theBRIDGErows in the table above), and allC_ASSOassociations (whose corpus majority isLandArea-partition groupings), have no S-101 named-association equivalent and stay unmapped. (For S-401, aC_ASSOthat 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:
- Pre-translation —
S57PreTranslationRules.Defaultchecks the rawEncDotNet.S57.S57Documentfor things that don't survive translation (DSID / DSPM presence and a positive compilation scale; the presence of anM_COVRmeta-feature). - 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 prefixS101-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.031field), not the SHA-256 digest the S-100 model carries, so the per-file CRC values are surfaced throughFileVerificationResult.DetailandComputedSha256is leftnull. - The verdict semantics match S-100:
NoChecksum/NotSignedare non-failing (an unsigned-but-intact set hasIntegrityVerified == truebutAllValid == false); onlyChecksumMismatch/FileMissing/Error/ invalid signatures fail integrity. This is identical to the upstream S-57AllValid. - 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<Feature>, etc.
Installation
dotnet add package EncDotNet.S100.Datasets.S57