Reading S-57 charts
EncDotNet.S57 reads IHO S-57 Edition 3.1 cells, applies update cells, and
reads and verifies exchange sets. It has two models of a cell:
- S57Document, read with S57DocumentReader, maps the file's records directly: feature records, vector records and the dataset metadata.
- S57Chart classifies those records into typed point, line, area and meta features and indexed nodes, edges and faces.
Start with S57Chart if you want geometry. Use S57Document if you want the
records as encoded, or to apply updates yourself.
Read a cell
S57DocumentReader reads from a path, a byte array, a span or a stream, with
async overloads for files and streams. Each method takes an optional
ILogger that receives warnings about records it can't fully decode.
using EncDotNet.S57;
S57Document document = await S57DocumentReader.ReadFromFileAsync("path/to/cell.000");
S57DataSetIdentification? dsid = document.DataSetIdentification;
Console.WriteLine($"{dsid?.DataSetName} edition {dsid?.EditionNumber}, update {dsid?.UpdateNumber}");
DataSetIdentification holds the DSID field (name, edition, update number,
producing agency, product specification) and DataSetParameters holds DSPM
(datums, compilation scale, units and multiplication factors).
Find features
Each S57FeatureRecord has an object class
(ObjectCode, from OBJL), a geometric primitive (point, line, area or
none), attributes, and pointers to its geometry and to related features.
using EncDotNet.S57;
S57Document document = S57DocumentReader.ReadFromFile("path/to/cell.000");
foreach (S57FeatureRecord light in document.GetFeaturesByObjectCode(S57ObjectCode.LIGHTS))
{
Console.WriteLine($"{light.RecordName} ({light.Primitive})");
foreach (S57AttributeValue attribute in light.Attributes)
{
Console.WriteLine($" {attribute.AttributeCode} = {attribute.Value}");
}
}
Attribute codes are the numeric ATTL values from S-57 Appendix A,
Chapter 2; for example COLOUR is 75 and VALNMR (nominal range) is 178.
Values are strings. Depending on the attribute's type they hold an
enumeration value, a comma-separated list, a number or free text. National
attributes (NATF) are in NationalAttributes.
Convert coordinates
S-57 stores positions as integers. Divide by the coordinate multiplication
factor (COMF, usually 10,000,000) for decimal degrees, and depths by the
sounding multiplication factor (SOMF, usually 10).
S57Coordinate2D and S57Sounding
do the division for you:
using EncDotNet.S57;
S57Document document = S57DocumentReader.ReadFromFile("path/to/cell.000");
int comf = document.CoordinateMultiplicationFactor;
int somf = document.SoundingMultiplicationFactor;
foreach (S57VectorRecord vector in document.VectorRecords)
{
foreach (S57Coordinate2D coordinate in vector.Coordinates2D)
{
var (longitude, latitude) = coordinate.ToDecimalDegrees(comf);
Console.WriteLine($"{vector.RecordName}: {latitude:F6}, {longitude:F6}");
}
foreach (S57Sounding sounding in vector.Soundings)
{
var (longitude, latitude, depth) = sounding.ToDecimalValues(comf, somf);
Console.WriteLine($"{vector.RecordName}: {latitude:F6}, {longitude:F6} depth {depth:F1}");
}
}
Use the typed chart model
S57Chart sorts features by primitive and spatial records by kind, and
indexes them by record name. It's built from a file, a stream or an existing
document:
using EncDotNet.S57;
using EncDotNet.S57.Charts;
S57Chart chart = S57Chart.FromFile("path/to/cell.000");
Console.WriteLine($"Scale 1:{chart.CompilationScale}");
Console.WriteLine($"{chart.PointFeatures.Count} point, {chart.LineFeatures.Count} line, {chart.AreaFeatures.Count} area features");
Console.WriteLine($"{chart.IsolatedNodes.Count} isolated nodes, {chart.ConnectedNodes.Count} connected nodes, {chart.Edges.Count} edges, {chart.Faces.Count} faces");
if (chart.FeaturesByObjectCode.TryGetValue(S57ObjectCode.SOUNDG, out S57ObjectCodeFeatures? soundings))
{
foreach (S57PointFeature feature in soundings.Points)
{
foreach (S57SpatialPointer pointer in feature.SpatialReferences)
{
S57IsolatedNode? node = chart.GetIsolatedNode(pointer.Name);
Console.WriteLine($"{feature.RecordName}: {node?.Soundings.Count ?? 0} soundings");
}
}
}
Typed features expose GetAttributeValue(int) and GetAttributeValues(int)
for attribute lookups. Area features list their faces (FaceReferences) and,
for cells without full topology, their exterior and interior edges
(ExteriorEdgeReferences, InteriorEdgeReferences). The
Architecture page explains how features, edges, nodes and
faces relate.
Follow relationships between features
Features refer to other features, such as a beacon to the lights on it, with
feature pointers (the FFPT field). A pointer names its target by long name
(S57LongName: agency, feature ID and subdivision), the identifier a feature
keeps across editions and updates. GetFeature finds the target, and
GetReferencingFeatures finds the features that point to a given one:
foreach (S57TypedFeature feature in chart.AllFeatures.Values)
{
foreach (S57FeaturePointer pointer in feature.RelatedFeatures)
{
S57TypedFeature? related = chart.GetFeature(pointer.Name);
Console.WriteLine($"{feature.ObjectCode} -> {related?.ObjectCode} ({pointer.Relationship})");
}
}
S57Document.GetFeatureRecord(S57LongName) does the same on a document
without building a chart, but searches every record on each call.
Apply update cells
A base cell (.000) is followed by update cells (.001, .002, and so on)
that insert, delete and modify records. Apply them in order with
ApplyChanges(S57Document), which
returns a new document and leaves the original unchanged:
using EncDotNet.S57;
S57Document document = S57DocumentReader.ReadFromFile("path/to/cell.000");
foreach (string updatePath in new[] { "path/to/cell.001", "path/to/cell.002" })
{
document = document.ApplyChanges(S57DocumentReader.ReadFromFile(updatePath));
}
Console.WriteLine($"Now at update {document.DataSetIdentification?.UpdateNumber}");
When the cell is in an exchange set, S57ExchangeSet.ReadDocument finds and
applies its updates for you; see the next section.
Read an exchange set
An exchange set is a folder, usually named ENC_ROOT, with a CATALOG.031
file that lists every file in it.
S57ExchangeSetReader finds the catalog, the
base cell and its update cells under that folder, without parsing them:
using EncDotNet.S57;
using EncDotNet.S57.Charts;
using EncDotNet.S57.ExchangeSets;
S57ExchangeSet exchangeSet = S57ExchangeSetReader.Read("path/to/ENC_ROOT");
Console.WriteLine($"Base cell: {exchangeSet.BaseCellFileName}");
Console.WriteLine($"Updates: {string.Join(", ", exchangeSet.UpdateFileNames)}");
// The base cell with every update applied.
S57Chart chart = exchangeSet.ReadChart("path/to/ENC_ROOT");
Note
S57ExchangeSetReader describes one cell: it uses the first .000 file it
finds under the folder. That matches the single-cell exchange sets NOAA
distributes. For an exchange set with several cells, read CATALOG.031 and
open each cell with S57DocumentReader.
To list the catalog's entries and their coverage, read CATALOG.031 with
S57CatalogReader:
using EncDotNet.S57.ExchangeSets;
S57Catalog catalog = S57CatalogReader.ReadFromFile("path/to/ENC_ROOT/CATALOG.031");
foreach (S57CatalogEntry entry in catalog.Entries)
{
Console.WriteLine($"{entry.FileName}: {entry.SouthernmostLatitude}, {entry.WesternmostLongitude} to {entry.NorthernmostLatitude}, {entry.EasternmostLongitude}");
}
Verify an exchange set
The catalog records a CRC-32 checksum for each file. VerifyAsync checks
every file against it and reports a result per file instead of throwing:
using EncDotNet.S57.ExchangeSets;
S57ExchangeSet exchangeSet = S57ExchangeSetReader.Read("path/to/ENC_ROOT");
S57ExchangeSetVerificationResult result = await exchangeSet.VerifyAsync("path/to/ENC_ROOT");
foreach (S57FileVerificationResult file in result.FileResults)
{
Console.WriteLine($"{file.FileName}: checksum {file.ChecksumOutcome}, signature {file.SignatureOutcome}");
}
Console.WriteLine(result.AllValid ? "All files verified." : "Verification failed.");
Checksum and signature outcomes are reported separately. Checksums are
verified in full. S-63 signature verification isn't implemented yet: the
IS63SignatureVerifier extension point exists, and files report
NotSigned until it does.