Writing S-57 charts
EncDotNet.S57 writes an S57Document as an S-57 Edition 3.1 base cell
(.000), and an S57Catalog as the CATALOG.031 file of an exchange set.
The writers are the inverses of the readers in
Reading S-57 charts:
S57DocumentWriter writes what
S57DocumentReader reads, and
S57CatalogWriter writes what
S57CatalogReader reads. Use them to save a
modified cell, to package cells you've built or translated, or to check a
pipeline by writing a cell and reading it back.
Write a cell
Pass the document to S57DocumentWriter. It writes to a byte array, a stream
or a file:
using EncDotNet.S57;
S57Document document = S57DocumentReader.ReadFromFile("path/to/cell.000");
S57DocumentWriter.WriteToFile("path/to/copy.000", document);
The copy reads back as an equal document. The writer checks the whole document
before it writes anything, so a document it can't encode throws
ArgumentException and leaves the file untouched. The message names the
record and subfield at fault.
The writer puts the records in a fixed order:
- The data descriptive record (DDR)
DSIDwithDSSIDSPM- The vector records
- The feature records
Within each record, the fields follow the S-57 tree structures. Vector and
feature records keep the order they have in the document. DSSI's record
counts are computed from the records.
The writer reads no clock, so the same document always produces the same
bytes. Dates such as the issue date (ISDT) are written as the document
holds them.
Build a cell from scratch
Every type in the document model has public init properties, so you can
build a cell without reading one. A base cell needs a DSID, set as
DataSetIdentification, and a DSPM, set as DataSetParameters. Every
record must be an insert (S57UpdateInstruction.Insert) with no update
control fields:
using EncDotNet.S57;
var document = new S57Document
{
DataSetIdentification = new S57DataSetIdentification
{
RecordName = S57RecordName.FromRcnmRcid(S57RecordNameCodes.DataSetGeneralInfo, 1),
ExchangePurpose = (int)S57ExchangePurpose.New,
IntendedUsage = 5,
DataSetName = "XX5EXAMP.000",
EditionNumber = "1",
UpdateNumber = "0",
IssueDate = "20260101",
S57EditionNumber = "03.1",
ProductSpecification = (int)S57ProductSpecification.Enc,
ApplicationProfile = (int)S57ApplicationProfile.EncNew,
ProducingAgency = 550,
DataStructure = (int)S57DataStructure.ChainNode,
AttfLexicalLevel = (int)S57LexicalLevel.Latin1,
NatfLexicalLevel = (int)S57LexicalLevel.Ucs2
},
DataSetParameters = new S57DataSetParameters
{
RecordName = S57RecordName.FromRcnmRcid(S57RecordNameCodes.DataSetGeoReference, 1),
HorizontalDatum = (int)S57HorizontalDatum.WGS84,
VerticalDatum = (int)S57VerticalDatum.MeanSeaLevel,
SoundingDatum = (int)S57VerticalDatum.LowestAstronomicalTide,
CompilationScale = 22000,
DepthUnits = (int)S57DepthUnits.Metres,
HeightUnits = (int)S57HeightUnits.Metres,
PositionalUnits = (int)S57PositionalAccuracyUnits.Metres,
CoordinateUnits = (int)S57CoordinateUnits.LatitudeLongitude,
CoordinateMultiplicationFactor = 10_000_000,
SoundingMultiplicationFactor = 10
},
VectorRecords =
[
new S57VectorRecord
{
RecordName = S57RecordName.FromRcnmRcid(S57RecordNameCodes.IsolatedNode, 1),
RecordVersion = 1,
UpdateInstruction = S57UpdateInstruction.Insert,
Coordinates2D = [new S57Coordinate2D { Y = 501_234_567, X = -12_345_678 }]
}
],
FeatureRecords =
[
new S57FeatureRecord
{
RecordName = new S57RecordName
{
RecordNameCode = S57RecordNameCodes.Feature,
RecordId = 1,
AgencyCode = 550,
FeatureId = 1,
FeatureSubdivision = 1
},
Primitive = S57GeometricPrimitive.Point,
Group = 2,
ObjectCode = S57ObjectCode.BOYLAT,
RecordVersion = 1,
UpdateInstruction = S57UpdateInstruction.Insert,
Attributes = [new S57AttributeValue(116, "Mer du Nord 1")],
SpatialPointers =
[
new S57SpatialPointer
{
Name = S57RecordName.FromRcnmRcid(S57RecordNameCodes.IsolatedNode, 1),
Orientation = S57Orientation.NotApplicable,
Usage = S57UsageIndicator.NotApplicable,
Mask = S57MaskingIndicator.NotApplicable
}
]
}
]
};
byte[] cell = S57DocumentWriter.Write(document);
The coded DSID and DSPM subfields are int properties. Enums name their
values, and each value is the S-57 code, so cast in either direction:
| Subfield | Property | Enum |
|---|---|---|
EXPP |
ExchangePurpose |
S57ExchangePurpose |
PRSP |
ProductSpecification |
S57ProductSpecification |
PROF |
ApplicationProfile |
S57ApplicationProfile |
DSTR |
DataStructure |
S57DataStructure |
AALL, NALL |
AttfLexicalLevel, NatfLexicalLevel |
S57LexicalLevel |
HDAT |
HorizontalDatum |
S57HorizontalDatum |
VDAT, SDAT |
VerticalDatum, SoundingDatum |
S57VerticalDatum |
DUNI |
DepthUnits |
S57DepthUnits |
HUNI |
HeightUnits |
S57HeightUnits |
PUNI |
PositionalUnits |
S57PositionalAccuracyUnits |
COUN |
CoordinateUnits |
S57CoordinateUnits |
A code an enum doesn't name, such as an AML product code, casts to an unnamed
value. To interpret the product of a cell you've read, use
DataSetIdentification.Product instead.
Coordinates and soundings are integers scaled by COMF and SOMF, as the
reader gives them. A feature's long name (AGEN, FIDN, FIDS) goes in its
RecordName and is written as its FOID field. A feature without one gets a
FOID of zeros.
Choose lexical levels
DSSI's AALL and NALL set how attribute values are encoded:
| Field | Level 0 | Level 1 | Level 2 |
|---|---|---|---|
ATTF |
ASCII | ISO 8859-1 (Latin-1) | Not allowed |
NATF |
ASCII | ISO 8859-1 (Latin-1) | UCS-2 |
The COMT comments of DSID, DSPM and FFPT are ISO 8859-1. S-57 makes
them ASCII, but some producers, such as NATO AML cells, put Latin-1 characters
like the degree sign in them, and ISO 8859-1 writes ASCII comments unchanged.
Every other text value is ASCII. A value that its level can't encode, such as
Greek in an ATTF value at level 1, throws ArgumentException rather than
being replaced. Put text like that in a national attribute with NALL set to
2. UCS-2 covers the Basic Multilingual Plane only, so characters outside it,
such as emoji, can't be written at any level.
The writer uses the levels the document sets. To use the lowest levels that
encode the document's attribute values, pass the document through
S57LexicalLevels.WithLowestLevels before writing it. It throws
ArgumentException if an ATTF value needs UCS-2:
byte[] cell = S57DocumentWriter.Write(S57LexicalLevels.WithLowestLevels(document));
For other text, S57LexicalLevels.Lowest returns the lowest level that encodes
a set of strings, and S57LexicalLevels.CanEncode checks one string. They make
the same checks as the writer.
The writer declares the levels in the DDR, as S-57 requires. It writes a
level 2 NATF field with two-byte terminators.
Package an exchange set
An exchange set holds the cells and a CATALOG.031 file that lists them
with their CRCs. Compute each cell's CRC with
S57CatalogWriter.ComputeCrc, then write the catalog:
using EncDotNet.S57;
using EncDotNet.S57.ExchangeSets;
byte[] cell = S57DocumentWriter.Write(document);
Directory.CreateDirectory("path/to/ENC_ROOT/XX5EXAMP");
File.WriteAllBytes("path/to/ENC_ROOT/XX5EXAMP/XX5EXAMP.000", cell);
var catalog = new S57Catalog
{
Entries =
[
new S57CatalogEntry { RecordId = 1, FileName = "CATALOG.031", Volume = "V01X01", Implementation = "ASC" },
new S57CatalogEntry
{
RecordId = 2,
FileName = "XX5EXAMP\\XX5EXAMP.000",
Volume = "V01X01",
Implementation = "BIN",
SouthernmostLatitude = 50.1,
WesternmostLongitude = -1.25,
NorthernmostLatitude = 50.15,
EasternmostLongitude = -1.2,
CrcChecksum = S57CatalogWriter.ComputeCrc(cell)
}
]
};
S57CatalogWriter.WriteToFile("path/to/ENC_ROOT/CATALOG.031", catalog);
File names in the catalog use \ as the path separator. An entry with an
empty RecordName is written as CD. Bounds are written with six decimal
places when that holds them exactly, and an absent bound is written empty.
Verify an exchange set to
check the result.
Limits
- Base cells only. The writer doesn't write update cells (
.001and later): a record whose update instruction isn'tInsert, or that has an update control field (FSPC,FFPC,VRPCorSGCC), throwsArgumentException. - No validation against S-58. The writer encodes the document as it is. It doesn't check topology, mandatory attributes or other product specification rules.
- Fields the model doesn't hold aren't written. These include
DSPR,DSRCandARCC. - Text is trimmed. The reader drops trailing spaces from text values, so a value that ended with spaces doesn't keep them.