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
4 changes: 3 additions & 1 deletion .github/skills/s57-enc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ out breaking changes and their EncDotNet.S100 impact in the PR description.
- **Attributes** (`ImmutableArray<S57AttributeValue>`): ATTL/ATVL pairs from
ATTF and NATF.
- **Spatial pointers** (FSPT, `S57SpatialPointer`) and **feature pointers**
(FFPT, `S57FeaturePointer`).
(FFPT, `S57FeaturePointer`). A feature pointer names its target by long name
(`S57LongName`: AGEN, FIDN, FIDS from the target's FOID), not by RCNM/RCID;
resolve it with `S57Chart.GetFeature(S57LongName)`.

**Vector records** (`S57VectorRecord`), by RCNM:

Expand Down
22 changes: 22 additions & 0 deletions docs/reading-s57-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ for cells without full topology, their exterior and interior edges
[Architecture](architecture.md) 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:

```csharp
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)
Expand Down
65 changes: 60 additions & 5 deletions src/EncDotNet.S57/Charts/S57Chart.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;

namespace EncDotNet.S57.Charts;

Expand Down Expand Up @@ -56,10 +57,16 @@ public sealed record S57Chart
public IReadOnlyDictionary<S57RecordName, S57TypedFeature> AllFeatures { get; }

/// <summary>
/// Gets the reverse feature-pointer index: maps a feature's record name to all features
/// Gets the features that have a long name (LNAM), indexed by it. Feature pointers
/// (<see cref="S57TypedFeature.RelatedFeatures"/>) refer to features by long name.
/// </summary>
public IReadOnlyDictionary<S57LongName, S57TypedFeature> FeaturesByLongName { get; }

/// <summary>
/// Gets the reverse feature-pointer index: maps a feature's long name to all features
/// that reference it via FFPT (feature-to-feature pointers).
/// </summary>
public IReadOnlyDictionary<S57RecordName, IReadOnlyList<S57TypedFeature>> ReferencingFeatures { get; }
public IReadOnlyDictionary<S57LongName, IReadOnlyList<S57TypedFeature>> ReferencingFeatures { get; }

/// <summary>
/// Gets the spatial co-location index: maps a spatial record name to all point features
Expand Down Expand Up @@ -125,6 +132,7 @@ public S57Chart(
AreaFeatures = areaFeatures ?? [];
MetaFeatures = metaFeatures ?? [];
AllFeatures = BuildAllFeaturesIndex(PointFeatures, LineFeatures, AreaFeatures, MetaFeatures);
FeaturesByLongName = BuildFeaturesByLongNameIndex(AllFeatures);
ReferencingFeatures = BuildReferencingFeaturesIndex(AllFeatures);
ColocatedPointFeatures = BuildColocatedPointFeaturesIndex(PointFeatures);
FeaturesByObjectCode = BuildFeaturesByObjectCodeIndex(PointFeatures, LineFeatures, AreaFeatures, MetaFeatures);
Expand Down Expand Up @@ -162,10 +170,28 @@ private static ImmutableDictionary<S57RecordName, S57TypedFeature> BuildAllFeatu
return builder.ToImmutable();
}

private static ImmutableDictionary<S57RecordName, IReadOnlyList<S57TypedFeature>> BuildReferencingFeaturesIndex(
private static ReadOnlyDictionary<S57LongName, S57TypedFeature> BuildFeaturesByLongNameIndex(
IReadOnlyDictionary<S57RecordName, S57TypedFeature> allFeatures)
{
var builder = new Dictionary<S57RecordName, ImmutableArray<S57TypedFeature>.Builder>();
// A hash table, not an ImmutableDictionary: it is read far more than built, and a node per
// entry would add about a twentieth to the chart's memory. Nothing mutates it once built.
var index = new Dictionary<S57LongName, S57TypedFeature>(allFeatures.Count);
foreach (var feature in allFeatures.Values)
{
if (feature.LongName is { } longName)
{
index[longName] = feature;
}
}

index.TrimExcess();
return index.AsReadOnly();
}

private static ImmutableDictionary<S57LongName, IReadOnlyList<S57TypedFeature>> BuildReferencingFeaturesIndex(
IReadOnlyDictionary<S57RecordName, S57TypedFeature> allFeatures)
{
var builder = new Dictionary<S57LongName, ImmutableArray<S57TypedFeature>.Builder>();

foreach (var feature in allFeatures.Values)
{
Expand All @@ -182,7 +208,7 @@ private static ImmutableDictionary<S57RecordName, IReadOnlyList<S57TypedFeature>
}
}

var result = ImmutableDictionary.CreateBuilder<S57RecordName, IReadOnlyList<S57TypedFeature>>();
var result = ImmutableDictionary.CreateBuilder<S57LongName, IReadOnlyList<S57TypedFeature>>();
foreach (var (name, list) in builder)
{
result[name] = list.ToImmutable();
Expand Down Expand Up @@ -358,6 +384,7 @@ private S57Chart(S57Document document)
AreaFeatures = areaFeatures.ToImmutable();
MetaFeatures = metaFeatures.ToImmutable();
AllFeatures = allFeatures.ToImmutable();
FeaturesByLongName = BuildFeaturesByLongNameIndex(AllFeatures);
ReferencingFeatures = BuildReferencingFeaturesIndex(AllFeatures);
ColocatedPointFeatures = BuildColocatedPointFeaturesIndex(PointFeatures);
FeaturesByObjectCode = BuildFeaturesByObjectCodeIndex(PointFeatures, LineFeatures, AreaFeatures, MetaFeatures);
Expand Down Expand Up @@ -461,6 +488,34 @@ public static async Task<S57Chart> FromStreamAsync(Stream stream, CancellationTo
return Faces.TryGetValue(name, out var face) ? face : null;
}

/// <summary>
/// Gets the feature with the specified long name, such as the target of a feature pointer.
/// </summary>
/// <param name="longName">The long name (LNAM) to find.</param>
/// <returns>The feature, or <c>null</c> if the chart has none with that long name.</returns>
/// <example>
/// <code>
/// foreach (var pointer in feature.RelatedFeatures)
/// {
/// S57TypedFeature? related = chart.GetFeature(pointer.Name);
/// }
/// </code>
/// </example>
public S57TypedFeature? GetFeature(S57LongName longName)
{
return FeaturesByLongName.TryGetValue(longName, out var feature) ? feature : null;
}

/// <summary>
/// Gets the features whose feature pointers refer to the specified feature.
/// </summary>
/// <param name="longName">The long name (LNAM) of the referenced feature.</param>
/// <returns>The referencing features, or an empty list if there are none.</returns>
public IReadOnlyList<S57TypedFeature> GetReferencingFeatures(S57LongName longName)
{
return ReferencingFeatures.TryGetValue(longName, out var features) ? features : [];
}

/// <summary>
/// Gets all point features that reference the specified spatial record (co-located features).
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/EncDotNet.S57/Charts/S57TypedFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public abstract record S57TypedFeature
/// <summary>Gets the record name that uniquely identifies this feature.</summary>
public S57RecordName RecordName { get; }

/// <summary>
/// Gets the feature's long name (LNAM), which other features' <see cref="RelatedFeatures"/>
/// pointers use to refer to it, or <c>null</c> when the feature has none.
/// </summary>
public S57LongName? LongName => RecordName.LongName;

/// <summary>Gets the object code (OBJL) identifying the feature type.</summary>
public S57ObjectCode ObjectCode { get; }

Expand Down
32 changes: 32 additions & 0 deletions src/EncDotNet.S57/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:EncDotNet.S57.Charts.S57Chart.get_ReferencingFeatures</Target>
<Left>lib/net10.0/EncDotNet.S57.dll</Left>
<Right>lib/net10.0/EncDotNet.S57.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:EncDotNet.S57.S57FeaturePointer.get_Name</Target>
<Left>lib/net10.0/EncDotNet.S57.dll</Left>
<Right>lib/net10.0/EncDotNet.S57.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:EncDotNet.S57.Charts.S57Chart.get_ReferencingFeatures</Target>
<Left>lib/net8.0/EncDotNet.S57.dll</Left>
<Right>lib/net8.0/EncDotNet.S57.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:EncDotNet.S57.S57FeaturePointer.get_Name</Target>
<Left>lib/net8.0/EncDotNet.S57.dll</Left>
<Right>lib/net8.0/EncDotNet.S57.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
23 changes: 23 additions & 0 deletions src/EncDotNet.S57/S57Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@ public sealed record S57Document
/// <summary>Gets all vector (spatial) records.</summary>
public IReadOnlyList<S57VectorRecord> VectorRecords { get; init; } = [];

/// <summary>
/// Gets a feature record by its long name (LNAM), such as the target of a feature pointer.
/// </summary>
/// <param name="longName">The long name to find.</param>
/// <returns>The first feature record with that long name, or <c>null</c> if there is none.</returns>
/// <remarks>
/// This searches every feature record. To resolve many pointers, build an
/// <see cref="Charts.S57Chart"/> and use <see cref="Charts.S57Chart.GetFeature(S57LongName)"/>,
/// which uses an index.
/// </remarks>
public S57FeatureRecord? GetFeatureRecord(S57LongName longName)
{
foreach (var record in FeatureRecords)
{
if (record.LongName == longName)
{
return record;
}
}

return null;
}

/// <summary>
/// Gets a feature record by its record name.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions src/EncDotNet.S57/S57DocumentReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -875,9 +875,9 @@ private static S57RecordName DecomposeNameField(ReadOnlySpan<byte> data)
}

/// <summary>
/// Decomposes a LNAM composite subfield (8 bytes: AGEN(2) + FIDN(4) + FIDS(2)) into an S57RecordName.
/// Decomposes a LNAM composite subfield (8 bytes: AGEN(2) + FIDN(4) + FIDS(2)) into an S57LongName.
/// </summary>
private static S57RecordName DecomposeLongNameField(ReadOnlySpan<byte> data)
private static S57LongName DecomposeLongNameField(ReadOnlySpan<byte> data)
{
if (data.Length < 8)
{
Expand All @@ -887,7 +887,7 @@ private static S57RecordName DecomposeLongNameField(ReadOnlySpan<byte> data)
var agen = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(0, 2));
var fidn = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(2, 4));
var fids = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2));
return S57RecordName.FromLongName(agen, (int)fidn, fids);
return new S57LongName(agen, fidn, fids);
}

/// <summary>
Expand Down
8 changes: 6 additions & 2 deletions src/EncDotNet.S57/S57FeaturePointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ namespace EncDotNet.S57;
/// </summary>
public readonly record struct S57FeaturePointer
{
/// <summary>Gets the name of the related feature record.</summary>
public S57RecordName Name { get; init; }
/// <summary>
/// Gets the long name (LNAM) of the related feature object (S-57 Part 3, 4.6). Look the
/// feature up with <see cref="Charts.S57Chart.GetFeature(S57LongName)"/> or
/// <see cref="S57Document.GetFeatureRecord(S57LongName)"/>.
/// </summary>
public S57LongName Name { get; init; }

/// <summary>Gets the relationship indicator.</summary>
public S57RelationshipIndicator Relationship { get; init; }
Expand Down
6 changes: 6 additions & 0 deletions src/EncDotNet.S57/S57FeatureRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public sealed record S57FeatureRecord
/// <summary>Gets the record name.</summary>
public S57RecordName RecordName { get; init; }

/// <summary>
/// Gets the feature's long name (LNAM) from its FOID field, which feature pointers use to
/// refer to it, or <c>null</c> when the record has no FOID field.
/// </summary>
public S57LongName? LongName => RecordName.LongName;

/// <summary>Gets the object geometric primitive (PRIM).</summary>
public S57GeometricPrimitive Primitive { get; init; }

Expand Down
42 changes: 42 additions & 0 deletions src/EncDotNet.S57/S57LongName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Globalization;

namespace EncDotNet.S57;

/// <summary>
/// Represents a feature object identifier, the "Long Name" (LNAM) that identifies a feature
/// object and that feature-to-feature pointers use to refer to it (S-57 Part 3, 4.3 and 4.6).
/// </summary>
/// <remarks>
/// A long name is the feature's producing agency (AGEN), feature identification number (FIDN)
/// and feature identification subdivision (FIDS), from its FOID field. Unlike a record's
/// <see cref="S57RecordName"/> (RCNM and RCID), it stays the same across editions and updates,
/// which is why FFPT pointers use it as their foreign key.
/// </remarks>
public readonly record struct S57LongName
{
/// <summary>
/// Initializes a new instance of the <see cref="S57LongName"/> struct.
/// </summary>
/// <param name="agencyCode">The producing agency (AGEN).</param>
/// <param name="featureId">The feature identification number (FIDN).</param>
/// <param name="featureSubdivision">The feature identification subdivision (FIDS).</param>
public S57LongName(ushort agencyCode, uint featureId, ushort featureSubdivision)
{
AgencyCode = agencyCode;
FeatureId = featureId;
FeatureSubdivision = featureSubdivision;
}

/// <summary>Gets the producing agency code (AGEN).</summary>
public ushort AgencyCode { get; init; }

/// <summary>Gets the feature identification number (FIDN).</summary>
public uint FeatureId { get; init; }

/// <summary>Gets the feature identification subdivision (FIDS).</summary>
public ushort FeatureSubdivision { get; init; }

/// <inheritdoc/>
public override string ToString() =>
string.Create(CultureInfo.InvariantCulture, $"AGEN={AgencyCode}, FIDN={FeatureId}, FIDS={FeatureSubdivision}");
}
9 changes: 9 additions & 0 deletions src/EncDotNet.S57/S57RecordName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ public static S57RecordName FromLongName(int agencyCode, int featureId, int feat
};
}

/// <summary>
/// Gets the feature's long name (AGEN, FIDN, FIDS), or <c>null</c> when the record has none,
/// such as a vector record or a feature record without a FOID field.
/// </summary>
public S57LongName? LongName =>
AgencyCode == 0 && FeatureId == 0 && FeatureSubdivision == 0
? null
: new S57LongName((ushort)AgencyCode, unchecked((uint)FeatureId), (ushort)FeatureSubdivision);

/// <inheritdoc/>
public override string ToString()
{
Expand Down
4 changes: 2 additions & 2 deletions tests/EncDotNet.PerformanceTests/PerformanceBudgets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ internal static class PerformanceBudgets
public const long Iso8211DocumentWriterWrite = 2_318_456;
public const long S57DocumentReaderRead = 3_713_184;
public const long S57DocumentApplyChanges = 685_440;
public const long S57ChartFromDocument = 1_030_312;
public const long S57ChartFromDocument = 1_063_752;

// Retained bytes held by the resulting model.
public const long Iso8211DocumentRetained = 2_048_712;
public const long S57DocumentRetained = 1_194_928;
public const long S57ChartRetained = 1_145_696;
public const long S57ChartRetained = 1_178_960;
}
Loading
Loading