Reading product data
Why it matters
The EncDotNet.S100 facade renders any product and lists its features in a
product-neutral way. When your code needs the data itself, such as a warning's
text, a route's waypoints, a depth at a grid cell, or the current at a given
time, read the dataset with its product's own package. Each
EncDotNet.S100.Datasets.Sxxx package parses one product into .NET types that
follow its product specification.
The facade package already references every product package, so these types
are available as soon as you install EncDotNet.S100. The HDF5 examples also
use PureHdfFile from EncDotNet.S100.Hdf5.PureHdf, which the facade brings in
too.
Quick win
Read an S-124 navigational warning into typed objects:
using EncDotNet.S100.Datasets.S124;
using EncDotNet.S100.Datasets.S124.DataModel;
var dataset = S124Dataset.Open("navwarn_mixed.gml");
var warning = S124NavigationalWarning.From(dataset, out var diagnostics);
Console.WriteLine($"{warning.Preamble?.GeneralArea}, {warning.Preamble?.Locality}: {warning.Parts.Count} part(s)");
foreach (var part in warning.Parts)
Console.WriteLine($" {part.Id}: {part.GeometryKind}, {part.Coordinates.Count} position(s)");
Deep dive
Which API for which encoding
| Encoding | Products | Open with | You get |
|---|---|---|---|
GML (.gml) |
S-122, S-124, S-125, S-127, S-128, S-129, S-131, S-201, S-411, S-421 | SxxxDataset.Open(path or stream) |
Features and information types, plus a typed model |
ISO 8211 (.000) |
S-101 | S101Dataset.Open(path or stream) |
The ENC's records; features through S101VectorSource |
HDF5 (.h5) |
S-102, S-104, S-111 | SxxxDatasetReader.Read(PureHdfFile.Open(path)) |
Coverage grids of values, per time step for S-104 and S-111 |
To get a dataset's product, edition and extent without parsing all of it, use
the static ReadMetadata method. It's on the dataset type for GML and S-101,
and on the reader for HDF5:
var metadata = S124Dataset.ReadMetadata("navwarn_mixed.gml");
Console.WriteLine($"{metadata.Spec}, extent {metadata.Extent}");
GML products: features and information types
A GML dataset is a set of features, which have geometry, and information
types, which don't. Every product's feature implements IS100Feature, so
geometry is read the same way for all of them:
var dataset = S124Dataset.Open("navwarn_mixed.gml");
foreach (var feature in dataset.Features)
{
Console.WriteLine($"{feature.Id} {feature.FeatureType} ({feature.GeometryType})");
foreach (var (code, value) in feature.Attributes)
Console.WriteLine($" {code} = {value}");
}
GeometryTypesays which geometry property is filled in:Pointsfor a point or multipoint,Curvesfor curves,ExteriorRingandInteriorRingsfor a surface.- Positions are
GeoPositionvalues: latitude and longitude in WGS 84 degrees. GML stores them as latitude then longitude (S-100 Part 10b); the reader has already handled that. Attributesholds simple attributes as strings, keyed by attribute code. Complex attributes (with sub-attributes) are inComplexAttributes, andxlinkreferences to other features or information types are inReferences.
Typed data models
The attribute dictionaries follow the product's feature catalogue, so reading
them means knowing attribute codes and parsing strings. Every GML product also
has a typed model: a read-only projection of the dataset into classes named
after the product's concepts, with parsed values and resolved references. You
create it with the root type's From method:
using EncDotNet.S100.Datasets.S421;
using EncDotNet.S100.Datasets.S421.DataModel;
var routeDataset = S421Dataset.Open("route.s421.gml");
var plan = S421RoutePlan.From(routeDataset, out var diagnostics);
foreach (var waypoint in plan.Route.Waypoints)
Console.WriteLine($"{waypoint.WaypointNumber} {waypoint.Name}: {waypoint.Position}");
foreach (var diagnostic in diagnostics)
Console.WriteLine($"{diagnostic.Severity} {diagnostic.Code}: {diagnostic.Message}");
From doesn't throw on bad data. A reference that doesn't resolve, an
attribute that doesn't parse, or a feature without geometry becomes a
diagnostic, and the projection carries on with what it could read. So always
check diagnostics. For example, the S-421 route samples in this repository
(tests/datasets/S421) reference their waypoints with IDs that don't match the
waypoints' own IDs; their route plans come out with few or no waypoints and one
xlink.unresolved warning per missing reference.
Typed data models lists each product's root type and the diagnostic codes.
S-101 ENCs
S101Dataset holds the cell's ISO 8211 records as they're encoded: numeric type
codes and geometry split across separate record tables. For features with
geometry and named attributes, read them through S101VectorSource:
using EncDotNet.S100.Datasets.S101;
var cell = S101Dataset.Open("101AA00DS0019.000");
foreach (var feature in new S101VectorSource(cell).GetFeatures().Take(5))
{
Console.WriteLine($"{feature.Id} {feature.FeatureType} ({feature.GeometryType}), " +
$"{feature.Coordinates.Count} position(s)");
foreach (var (code, value) in feature.Attributes)
Console.WriteLine($" {code} = {value}");
}
FeatureType is the feature catalogue code (e.g. DepthArea) and
Coordinates are WGS 84 positions. Attributes values are typed (numbers,
Booleans and strings) rather than all strings. S101Dataset.OpenWithUpdates
applies update files, and cell.Document exposes the raw records if you need
them.
HDF5 coverages: S-102 bathymetry
The HDF5 products are grids of values. Open the file with PureHdfFile
and read it with the product's reader:
using EncDotNet.S100.Datasets.S102;
using EncDotNet.S100.Hdf5.PureHdf;
using var file = PureHdfFile.Open("102US004MI1CI262227.h5");
S102Dataset bathymetry = S102DatasetReader.Read(file);
BathymetryCoverage grid = bathymetry.Coverages[0];
Console.WriteLine($"CRS EPSG:{bathymetry.HorizontalCRS}, " +
$"{grid.NumPointsLatitudinal} x {grid.NumPointsLongitudinal} cells");
for (int row = 0; row < grid.NumPointsLatitudinal; row++)
{
for (int col = 0; col < grid.NumPointsLongitudinal; col++)
{
BathymetryValue value = grid.Values[row * grid.NumPointsLongitudinal + col];
if (value.Depth == 1_000_000f)
continue; // no data
double y = grid.OriginLatitude + row * grid.SpacingLatitudinal;
double x = grid.OriginLongitude + col * grid.SpacingLongitudinal;
Console.WriteLine($"({x}, {y}): {value.Depth} m ± {value.Uncertainty} m");
}
}
Valuesis row-major, starting at the grid origin.- Depth and uncertainty are in metres. Cells without data hold the fill value
1,000,000 (also available as
S102CoverageSource.FillValue). - Origin and spacing are in the dataset's horizontal CRS,
HorizontalCRS, as an EPSG code. For EPSG:4326 they're degrees, withOriginLatitudeas the latitude. Many surveys use a projected CRS such as UTM (for example EPSG:32617), and then origin and spacing are metres:OriginLatitudeis the northing andOriginLongitudethe easting. Transform them yourself if you need geographic positions, for example withEncDotNet.S100.Crs.ProjNet.
Time series: S-111 currents and S-104 water levels
S-104 and S-111 files hold either a regular grid per time step or a series of
values per station. ReadAny returns whichever the file contains:
using EncDotNet.S100.Datasets.S111;
using EncDotNet.S100.Hdf5.PureHdf;
using var file = PureHdfFile.Open("111US00_DBOFS_20260320T18Z_US4DE1BB.h5");
switch (S111DatasetReader.ReadAny(file))
{
case S111DatasetData.GriddedCoverage gridded:
foreach (var step in gridded.Dataset.Coverages.Take(3))
{
var valid = step.Values.Where(v => v.Speed >= 0).ToList();
Console.WriteLine($"{step.TimePoint:u}: {valid.Count} cells, " +
$"max {valid.Max(v => v.Speed)} kn");
}
break;
case S111DatasetData.StationSeries stations:
foreach (var station in stations.Dataset.Stations)
Console.WriteLine($"{station.Identifier} at {station.Latitude}, {station.Longitude}: " +
$"{station.NumberOfTimes} samples from {station.StartTime:u}");
break;
}
For a gridded S-111, each coverage in Coverages is one time step (TimePoint,
UTC) laid out like the S-102 grid above. Each value's Speed is in knots and
its Direction is in degrees true, the direction the current flows towards.
Negative speeds mark land or missing values. For a station series, each station
has its own position and samples; NearestTimeIndex(time) finds the sample
closest to a time.
S-104 works the same way: S104DatasetReader.ReadAny returns
S104DatasetData.GriddedCoverage or S104DatasetData.StationSeries. Its grid
values are WaterLevelValues: Height is the water level in metres relative to
the dataset's vertical datum, and Trend is 1 (decreasing), 2 (increasing),
3 (steady) or 0 (unknown).
Troubleshooting
Important
Check every typed model's diagnostics. A projection with warnings still
returns an object, but it may be missing the entities the warnings name.
Warning
HDF5 grid origin and spacing are in the dataset's own CRS, not always degrees:
check HorizontalCRS before treating them as latitude and longitude.
Note
Fill values mark cells without data: 1,000,000 for S-102 depth, and negative speeds for S-111. Skip them before computing statistics.
Next step
- Typed data models — every product's typed root, and the contract for adding one.
- Loading datasets — opening datasets from folders, ZIPs and exchange sets.
- Custom catalogues and validation — validating datasets, with the product's rules or your own.
- The per-product READMEs, under Packages, describe each product's types in detail.