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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<PackageVersion Include="ModelContextProtocol" Version="1.3.0" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="1.3.0" />
<!-- EncDotNet -->
<PackageVersion Include="EncDotNet.Iso8211" Version="0.4.1" />
<PackageVersion Include="EncDotNet.Iso8211" Version="0.4.3" />
<PackageVersion Include="EncDotNet.S57" Version="0.4.0" />
<!-- Mapping / rendering -->
<PackageVersion Include="FluentIcons.Avalonia.Fluent" Version="2.0.323" />
Expand Down
176 changes: 155 additions & 21 deletions src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
/// <summary>
/// Pipeline processor for S-111 surface-currents datasets. Branches
/// between dcf2 (regular grid → coverage + arrow layers, see existing
/// portrayal catalogue) and dcf8 (time series at fixed stations →
/// station-arrow point layer; see S-111 Edition 2.0.0 §10.2.3 /
/// §10.2.7).
/// portrayal catalogue), dcf3 (ungeorectified grid → station-arrow
/// point layer; S-100 Part 10c §10.2.1), and dcf8 (time series at
/// fixed stations → station-arrow point layer; see S-111 Edition 2.0.0
/// §10.2.3 / §10.2.7).
/// </summary>
public sealed class S111DatasetProcessor : IDatasetProcessor
{
Expand Down Expand Up @@ -138,8 +139,17 @@
{
_stationsById[station.Identifier] = station;
}
// dcf8 uses an inline arrow glyph — no portrayal
// catalogue required.

// DCF 3 (ungeorectified grid) uses the portrayal catalogue
// for PC-faithful color/symbol rendering. DCF 8 (time series
// at fixed stations) uses inline arrow glyphs — no PC required.
if (s.Dataset.DataCodingFormat == 3
&& catalogueManager.HasCatalogue("S-111"))
{
_provider = catalogueManager.GetProvider("S-111");
_catalogue = new S111PortrayalCatalogue(_provider);
Diagnostics.CatalogueResolutionDiagnostics.Report(this, Spec, _catalogue.CatalogueRef, "portrayal");
}
break;
}
}
Expand Down Expand Up @@ -242,9 +252,12 @@
// ---- dcf8 station series rendering ---------------------------------

/// <summary>
/// Projects each station to a single point feature with an arrow
/// Projects each station/node to a single point feature with an arrow
/// glyph oriented along <c>DirectionsDegreesTrue</c> and scaled by
/// speed magnitude. Rebuilt per <see cref="S111RenderContext.TimeStep"/>.
/// speed magnitude. When the portrayal catalogue is loaded (DCF 3),
/// colors and scale factors are resolved from the PC's speed-band
/// table; otherwise (DCF 8) a hardcoded palette is used.
/// Rebuilt per <see cref="S111RenderContext.TimeStep"/>.
/// </summary>
private DatasetResult RenderStationSeries(S111StationSeriesDataset ds, RenderContext? context)
{
Expand All @@ -260,6 +273,19 @@

_stationSelectedTime = selectedTime;

// Resolve PC schemes if available (DCF 3 with catalogue loaded)
CoverageColorScheme? colorScheme = null;
CoverageSymbolScheme? symbolScheme = null;
Dictionary<string, string>? svgCache = null;
if (_catalogue is not null && _provider is not null)
{
_catalogue.SwitchPalette(context?.Palette ?? PaletteType.Day);
var mariner = context?.Mariner ?? MarinerSettings.Default;
colorScheme = _catalogue.ResolveColorScheme(mariner);
symbolScheme = _catalogue.ResolveSymbolScheme(mariner);
svgCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}

var nativeToMerc = _crsTransformFactory.Create($"EPSG:{ds.HorizontalCRS ?? 4326}", "EPSG:3857");

var features = new List<IFeature>(ds.Stations.Count);
Expand Down Expand Up @@ -302,21 +328,98 @@
feature["Latitude"] = station.Latitude;
feature["Longitude"] = station.Longitude;

var arrowColour = ColorByMagnitude(speed);
Color arrowColour;
double symbolScale;
string? svgSource = null;
string? symbolRef = null;

// Symbol orientation in Mapsui follows screen rotation
// (counter-clockwise positive); compass bearing increases
// clockwise from north, so negate. Geographic north on
// screen is "up", which corresponds to a zero-rotation
// arrow whose default orientation we treat as pointing up.
feature.Styles.Add(new SymbolStyle
if (colorScheme is not null)
{
SymbolType = SymbolType.Triangle,
Fill = new Brush(arrowColour),
Outline = new Pen(arrowColour, 1.0),
SymbolScale = SymbolScaleForSpeed(speed),
SymbolRotation = -direction,
});
// PC-faithful rendering: resolve color from speed bands
var hex = colorScheme.Resolve(speed);
arrowColour = hex is not null
? ParseHexColor(hex)
: new Color(0x80, 0x80, 0x80); // grey fallback for out-of-range

// Use PC symbol scheme for scaling and SVG symbol if available
if (symbolScheme is not null)
{
var band = symbolScheme.Resolve(speed);
if (band is not null)
{
symbolRef = band.SymbolRef;
symbolScale = band.ScaleByValue
? band.ScaleFactor * speed
: band.ScaleFactor;
// Clamp to reasonable visual range
symbolScale = Math.Clamp(symbolScale, 0.20, 2.0);
}
else
{
symbolScale = 0.30; // minimum visible
}
}
else
{
symbolScale = SymbolScaleForSpeed(speed);
}

// Load SVG from PC if symbol ref resolved
if (symbolRef is not null && svgCache is not null && _provider is not null)
{
if (!svgCache.TryGetValue(symbolRef, out svgSource))
{
var item = _provider.Catalogue.Symbols
.FirstOrDefault(s => s.Id.Equals(symbolRef, StringComparison.OrdinalIgnoreCase));
if (item is not null)
{
using var stream = _provider.FetchAssetAsync(item, "Symbols").GetAwaiter().GetResult();
using var reader = new StreamReader(stream);
var rawSvg = reader.ReadToEnd();
// Process SVG through palette color resolver and
// wrap with the svg-content:// URI scheme that
// Mapsui's ImageStyle expects.
var processed = SvgProcessor.Process(rawSvg, _catalogue.ActivePalette);

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / test-arm64 (windows-11-arm, win-arm64)

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / test-arm64 (windows-11-arm, win-arm64)

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / perf-gate

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / publish (windows-11-arm, win-arm64, Viewer-win-arm64)

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / publish (macos-latest, osx-arm64, Viewer-osx-arm64)

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / publish (windows-latest, win-x64, Viewer-win-x64)

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / publish (ubuntu-latest, linux-x64, Viewer-linux-x64)

Dereference of a possibly null reference.

Check warning on line 382 in src/EncDotNet.S100.Datasets.Pipelines/S111DatasetProcessor.cs

View workflow job for this annotation

GitHub Actions / publish (ubuntu-22.04-arm, linux-arm64, Viewer-linux-arm64)

Dereference of a possibly null reference.
svgSource = "svg-content://" + processed;
}
svgCache[symbolRef] = svgSource ?? "";
}
if (string.IsNullOrEmpty(svgSource))
svgSource = null;
}
}
else
{
// Fallback (DCF 8): hardcoded palette
arrowColour = ColorByMagnitude(speed);
symbolScale = SymbolScaleForSpeed(speed);
}

// Symbol orientation: Mapsui rotation is counter-clockwise
// from east; compass bearing is clockwise from north. Negate
// to convert.
if (svgSource is not null)
{
// PC SVG arrow symbol
feature.Styles.Add(new ImageStyle
{
Image = new Image { Source = svgSource, RasterizeSvg = true },
SymbolScale = symbolScale * 0.6,
SymbolRotation = -direction,
});
}
else
{
// Triangle fallback
feature.Styles.Add(new SymbolStyle
{
SymbolType = SymbolType.Triangle,
Fill = new Brush(arrowColour),
Outline = new Pen(arrowColour, 1.0),
SymbolScale = symbolScale,
SymbolRotation = -direction,
});
}

features.Add(feature);
}
Expand All @@ -332,7 +435,8 @@
? new MRect(0, 0, 0, 0)
: new MRect(mercMinX, mercMinY, mercMaxX, mercMaxY);

var info = $"{ds.GeographicIdentifier ?? _fileName} — {ds.Stations.Count} stations, " +
var dcfLabel = ds.DataCodingFormat == 3 ? "nodes" : "stations";
var info = $"{ds.GeographicIdentifier ?? _fileName} — {ds.Stations.Count} {dcfLabel}, " +
$"time: {selectedTime:u}";

return new DatasetResult
Expand All @@ -344,6 +448,36 @@
};
}

/// <summary>
/// Parses a hex color string (e.g. "#RRGGBB" or "#AARRGGBB") into
/// a Mapsui <see cref="Color"/>.
/// </summary>
private static Color ParseHexColor(string hex)
{
var span = hex.AsSpan();
if (span.Length > 0 && span[0] == '#')
span = span[1..];

if (span.Length == 6)
{
int r = int.Parse(span[0..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int g = int.Parse(span[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int b = int.Parse(span[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
return new Color(r, g, b);
}

if (span.Length == 8)
{
int a = int.Parse(span[0..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int r = int.Parse(span[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int g = int.Parse(span[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int b = int.Parse(span[6..8], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
return new Color(r, g, b, a);
}

return new Color(0x80, 0x80, 0x80);
}

private static IReadOnlyList<DateTime> ComputeStationUnionTimes(S111StationSeriesDataset ds)
{
var set = new SortedSet<DateTime>();
Expand Down
Loading
Loading