Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/EncDotNet.S57/S57DataSetIdentification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed record S57DataSetIdentification
/// <summary>Gets the edition date (STED).</summary>
public string S57EditionNumber { get; init; } = string.Empty;

/// <summary>Gets the producing agency code (PRSP).</summary>
/// <summary>Gets the producing agency code (AGEN).</summary>
public int ProducingAgency { get; init; }

/// <summary>Gets the data structure (DSTR).</summary>
Expand All @@ -43,4 +43,27 @@ public sealed record S57DataSetIdentification

/// <summary>Gets the comment (COMT).</summary>
public string Comment { get; init; } = string.Empty;

/// <summary>Gets the product specification code (PRSP).</summary>
/// <remarks>
/// S-57 Edition 3.1, Part 3 §7.3.1.1 enumerates 1 = ENC (Electronic Navigational Chart) and
/// 2 = ODD (IHO Object Catalogue Data Dictionary). Inland ENC producers declare 10.
/// When the data set uses the ASCII lexical form, the mnemonics <c>ENC</c> and <c>ODD</c> are
/// mapped to 1 and 2. The value is 0 when the subfield is absent.
/// </remarks>
public int ProductSpecification { get; init; }

/// <summary>Gets the product specification description (PSDN).</summary>
public string ProductSpecificationDescription { get; init; } = string.Empty;

/// <summary>Gets the product specification edition number (PRED), for example "2.0".</summary>
public string ProductSpecificationEdition { get; init; } = string.Empty;

/// <summary>Gets the application profile identification code (PROF).</summary>
/// <remarks>
/// S-57 Edition 3.1, Part 3 §7.3.1.1 enumerates 1 = EN (ENC new), 2 = ER (ENC revision) and
/// 3 = DD (IHO data dictionary). When the data set uses the ASCII lexical form, those mnemonics
/// are mapped to their numeric codes. The value is 0 when the subfield is absent.
/// </remarks>
public int ApplicationProfile { get; init; }
}
48 changes: 47 additions & 1 deletion src/EncDotNet.S57/S57DocumentReader.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Globalization;
using EncDotNet.Iso8211;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -186,6 +187,8 @@ private static S57DataSetIdentification ParseDataSetIdentification(Iso8211Record
string dsnm, edtn, updn, uadt, isdt, sted;
ushort agen = 0;
string comt = "";
int prsp = 0, prof = 0;
string psdn = "", pred = "";

var fieldDef = ddr?.GetFieldDefinition(S57FieldTags.DSID)
?? throw new InvalidOperationException("DDR is required but not available. DSID field definition not found.");
Expand All @@ -204,6 +207,24 @@ private static S57DataSetIdentification ParseDataSetIdentification(Iso8211Record
isdt = reader.GetSubfield<string>(S57SubfieldNames.ISDT);
sted = reader.GetSubfield<string>(S57SubfieldNames.STED);

// PRSP and PROF are b11 in the binary form but mnemonics (A(3) / A(2)) in the ASCII form,
// so read them as strings (binary values convert to their decimal text) and map to codes.
if (reader.TryGetSubfield<string>(S57SubfieldNames.PRSP, out var prspValue))
{
prsp = ParseCodedSubfield(prspValue, ProductSpecificationMnemonics);
}
if (reader.TryGetSubfield<string>(S57SubfieldNames.PSDN, out var psdnValue))
{
psdn = psdnValue;
}
if (reader.TryGetSubfield<string>(S57SubfieldNames.PRED, out var predValue))
{
pred = predValue;
}
if (reader.TryGetSubfield<string>(S57SubfieldNames.PROF, out var profValue))
{
prof = ParseCodedSubfield(profValue, ApplicationProfileMnemonics);
}
if (reader.TryGetSubfield<ushort>(S57SubfieldNames.AGEN, out var agenValue))
{
agen = agenValue;
Expand Down Expand Up @@ -240,10 +261,35 @@ private static S57DataSetIdentification ParseDataSetIdentification(Iso8211Record
DataStructure = dstr,
AttfLexicalLevel = aall,
NatfLexicalLevel = nall,
Comment = comt
Comment = comt,
ProductSpecification = prsp,
ProductSpecificationDescription = psdn,
ProductSpecificationEdition = pred,
ApplicationProfile = prof
};
}

private static readonly string[] ProductSpecificationMnemonics = ["ENC", "ODD"];

private static readonly string[] ApplicationProfileMnemonics = ["EN", "ER", "DD"];

/// <summary>
/// Converts a coded DSID subfield value to its numeric code. Accepts either the decimal text of
/// a binary value or an ASCII mnemonic (whose 1-based position in <paramref name="mnemonics"/>
/// is its code). Returns 0 for empty or unrecognized values.
/// </summary>
private static int ParseCodedSubfield(string value, string[] mnemonics)
{
var trimmed = value.Trim();
if (int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out var code))
{
return code;
}

var index = Array.IndexOf(mnemonics, trimmed.ToUpperInvariant());
return index >= 0 ? index + 1 : 0;
}

/// <summary>
/// Parses a Data Set Parameters record.
/// </summary>
Expand Down
120 changes: 112 additions & 8 deletions tests/EndDotNet.UnitTests/S57DocumentReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private static byte[] CreateS57Document(params byte[][] dataRecords)
/// <summary>
/// Creates an S-57 Data Descriptive Record (DDR) with all required field definitions.
/// </summary>
private static byte[] CreateS57Ddr()
private static byte[] CreateS57Ddr(string? dsidDescriptors = null, string? dsidFormats = null)
{
// Build all field definitions for DDR
var fields = new List<(string tag, byte[] data)>();
Expand All @@ -60,8 +60,8 @@ private static byte[] CreateS57Ddr()
// Format: RCNM(b11), RCID(b14), EXPP(b11), INTU(b11), DSNM(A), EDTN(A), UPDN(A), UADT(A), ISDT(A), STED(A), PRSP(b11), PSDN(A), PRED(A), PROF(b11), AGEN(b12), COMT(A)
fields.Add(("DSID", CreateDdrFieldData(
"DSID",
"RCNM!RCID!EXPP!INTU!DSNM!EDTN!UPDN!UADT!ISDT!STED!PRSP!PSDN!PRED!PROF!AGEN!COMT",
"(b11,b14,b11,b11,A,A,A,A,A,A,b11,A,A,b11,b12,A)")));
dsidDescriptors ?? "RCNM!RCID!EXPP!INTU!DSNM!EDTN!UPDN!UADT!ISDT!STED!PRSP!PSDN!PRED!PROF!AGEN!COMT",
dsidFormats ?? "(b11,b14,b11,b11,A,A,A,A,A,A,b11,A,A,b11,b12,A)")));

// DSSI - Data Set Structure Information Field
// Format: DSTR(b11), AALL(b11), NALL(b11), NOMR(b14), NOCR(b14), NOGR(b14), NOLR(b14), NOIN(b14), NOCN(b14), NOED(b14), NOFA(b14)
Expand Down Expand Up @@ -290,7 +290,11 @@ private static byte[] CreateDsidRecord(
string uadt = "20250101",
string isdt = "20250101",
string sted = "03.1",
ushort agen = 540)
ushort agen = 540,
byte prsp = 1,
string psdn = "",
string pred = "",
byte prof = 1)
{
// Build DSID field data
using var ms = new MemoryStream();
Expand All @@ -306,10 +310,10 @@ private static byte[] CreateDsidRecord(
WriteString(writer, uadt); // UADT
WriteString(writer, isdt); // ISDT
WriteString(writer, sted); // STED
writer.Write((byte)1); // PRSP
WriteString(writer, ""); // PSDN
WriteString(writer, ""); // PRED
writer.Write((byte)1); // PROF
writer.Write(prsp); // PRSP
WriteString(writer, psdn); // PSDN
WriteString(writer, pred); // PRED
writer.Write(prof); // PROF
writer.Write(agen); // AGEN
WriteString(writer, ""); // COMT
writer.Write((byte)0x1E); // Field terminator
Expand Down Expand Up @@ -739,6 +743,106 @@ public void Read_DocumentWithDsid_ParsesDataSetIdentification()
Assert.Equal(540, document.DataSetIdentification.ProducingAgency);
}

[Theory]
[InlineData(1, "", "2.0", 1)]
[InlineData(10, "Inland ENC", "2.4", 2)]
public void Read_DocumentWithDsid_ParsesProductSpecification(byte prsp, string psdn, string pred, byte prof)
{
// Arrange
var dsidRecord = CreateDsidRecord(prsp: prsp, psdn: psdn, pred: pred, prof: prof);
var data = CreateS57Document(dsidRecord);

// Act
var document = S57DocumentReader.Read(data);

// Assert
Assert.NotNull(document.DataSetIdentification);
Assert.Equal(prsp, document.DataSetIdentification.ProductSpecification);
Assert.Equal(psdn, document.DataSetIdentification.ProductSpecificationDescription);
Assert.Equal(pred, document.DataSetIdentification.ProductSpecificationEdition);
Assert.Equal(prof, document.DataSetIdentification.ApplicationProfile);
Assert.Equal(540, document.DataSetIdentification.ProducingAgency);
}

[Theory]
[InlineData("ENC", "EN", 1, 1)]
[InlineData("ODD", "DD", 2, 3)]
[InlineData("10", "ER", 10, 2)]
public void Read_DocumentWithAsciiDsid_MapsProductSpecificationMnemonics(string prsp, string prof, int expectedPrsp, int expectedProf)
{
// Arrange: PRSP and PROF use their ASCII (A) form instead of b11
var ddr = CreateS57Ddr(dsidFormats: "(b11,b14,b11,b11,A,A,A,A,A,A,A,A,A,A,b12,A)");

using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
writer.Write((byte)10); // RCNM
writer.Write(1u); // RCID
writer.Write((byte)1); // EXPP
writer.Write((byte)5); // INTU
WriteString(writer, "TEST"); // DSNM
WriteString(writer, "1"); // EDTN
WriteString(writer, "0"); // UPDN
WriteString(writer, ""); // UADT
WriteString(writer, ""); // ISDT
WriteString(writer, "03.1"); // STED
WriteString(writer, prsp); // PRSP
WriteString(writer, ""); // PSDN
WriteString(writer, "2.4"); // PRED
WriteString(writer, prof); // PROF
writer.Write((ushort)540); // AGEN
WriteString(writer, ""); // COMT
writer.Write((byte)0x1E); // Field terminator

var data = ddr.Concat(CreateDataRecord("DSID", ms.ToArray())).ToArray();

// Act
var document = S57DocumentReader.Read(data);

// Assert
Assert.NotNull(document.DataSetIdentification);
Assert.Equal(expectedPrsp, document.DataSetIdentification.ProductSpecification);
Assert.Equal("2.4", document.DataSetIdentification.ProductSpecificationEdition);
Assert.Equal(expectedProf, document.DataSetIdentification.ApplicationProfile);
}

[Fact]
public void Read_DocumentWithDsidWithoutProductSpecification_DefaultsToEmpty()
{
// Arrange: DSID definition omits PRSP, PSDN, PRED and PROF
var ddr = CreateS57Ddr(
dsidDescriptors: "RCNM!RCID!EXPP!INTU!DSNM!EDTN!UPDN!UADT!ISDT!STED!AGEN!COMT",
dsidFormats: "(b11,b14,b11,b11,A,A,A,A,A,A,b12,A)");

using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
writer.Write((byte)10); // RCNM
writer.Write(1u); // RCID
writer.Write((byte)1); // EXPP
writer.Write((byte)5); // INTU
WriteString(writer, "TEST"); // DSNM
WriteString(writer, "1"); // EDTN
WriteString(writer, "0"); // UPDN
WriteString(writer, ""); // UADT
WriteString(writer, ""); // ISDT
WriteString(writer, "03.1"); // STED
writer.Write((ushort)540); // AGEN
WriteString(writer, ""); // COMT
writer.Write((byte)0x1E); // Field terminator

var data = ddr.Concat(CreateDataRecord("DSID", ms.ToArray())).ToArray();

// Act
var document = S57DocumentReader.Read(data);

// Assert
Assert.NotNull(document.DataSetIdentification);
Assert.Equal(0, document.DataSetIdentification.ProductSpecification);
Assert.Equal(string.Empty, document.DataSetIdentification.ProductSpecificationDescription);
Assert.Equal(string.Empty, document.DataSetIdentification.ProductSpecificationEdition);
Assert.Equal(0, document.DataSetIdentification.ApplicationProfile);
Assert.Equal(540, document.DataSetIdentification.ProducingAgency);
}

[Fact]
public void Read_DocumentWithDspm_ParsesDataSetParameters()
{
Expand Down
Loading