EncDotNet.S100.Renderers.Mapsui
0.13.0
See the version list below for details.
dotnet add package EncDotNet.S100.Renderers.Mapsui --version 0.13.0
NuGet\Install-Package EncDotNet.S100.Renderers.Mapsui -Version 0.13.0
<PackageReference Include="EncDotNet.S100.Renderers.Mapsui" Version="0.13.0" />
<PackageVersion Include="EncDotNet.S100.Renderers.Mapsui" Version="0.13.0" />
<PackageReference Include="EncDotNet.S100.Renderers.Mapsui" />
paket add EncDotNet.S100.Renderers.Mapsui --version 0.13.0
#r "nuget: EncDotNet.S100.Renderers.Mapsui, 0.13.0"
#:package EncDotNet.S100.Renderers.Mapsui@0.13.0
#addin nuget:?package=EncDotNet.S100.Renderers.Mapsui&version=0.13.0
#tool nuget:?package=EncDotNet.S100.Renderers.Mapsui&version=0.13.0
EncDotNet.S100.Renderers.Mapsui
Rendering of S-100 data into Mapsui map layers with CRS projection.
Overview
This library bridges the S-100 portrayal pipeline output to Mapsui map layers, including full CRS projection support (EPSG:3857 Web Mercator). Key types include:
MapsuiCoverageRenderer—ICoverageRenderer<ILayer>implementation that renders coverage data as a georeferenced raster overlay (S-102 / S-104 / S-111).MapsuiCoverageArrowRenderer— renders current arrows (e.g. from S-111 data) as a georeferenced raster layer.MapsuiDisplayListRenderer— product-agnostic vector renderer that consumes a list ofDrawingInstructions plus anIFeatureGeometryProviderand produces aMemoryLayerof styled point/line/area/text features. Used by every S-100 vector product (S-101, S-124, S-129, S-421); no per-spec subclass is required.ProjNetCrsTransformFactory—ICrsTransformFactoryimplementation using ProjNet for coordinate transformations between UTM, WGS84, and Web Mercator.
MapsuiDisplayListRenderer honours the relevant S-100 Part 9 conventions:
- Pen widths and text/symbol offsets specified in millimetres on the nominal display surface are converted to screen pixels using the standard
1 px = 0.32 mmratio (S-100 Part 9 §3.10.4). <foreground>/<background>colours accept either a palette token or a literal#RRGGBB/RRGGBBAAhex value, with the optionaltransparencyattribute applied as alpha attenuation.- Text alignment, mm offsets, and
textLinestart/end offsets (Relative or Absolute) are honoured per S-100 Part 9 §11.4. LineStyleProvider,SymbolProvider, andAreaFillProvidercallbacks let the host project plug in a portrayal catalogue without coupling the renderer to a specific dataset library.
Sharing processed-SVG and pattern-tile work across renders
MapsuiDisplayListRenderer resolves SVG symbols and rasterises area-fill pattern tiles lazily on first reference. The processed-SVG output depends on the active ColorPalette (fill/stroke colours are recoloured against the palette), and pattern-tile rasterisation is comparatively expensive.
When a single dataset is re-rendered repeatedly — typical when toggling palettes, scrubbing time-steps, or changing mariner settings — assign a single MapsuiRenderAssetCache instance to the renderer's AssetCache property on every Render() call:
private readonly MapsuiRenderAssetCache _renderAssetCache = new();
// per Render():
var renderer = new MapsuiDisplayListRenderer
{
Palette = palette,
AssetCache = _renderAssetCache,
SymbolProvider = name => catalogue.GetSymbol(name).SvgContent,
AreaFillProvider = name => catalogue.GetAreaFill(name),
};
The cache segments entries per palette (Day / Dusk / Night) so flipping back and forth keeps every palette warm. When AssetCache is unset, the renderer falls back to a per-instance cache, which preserves legacy behaviour for ad-hoc / one-shot callers.
Dynamic feature sources
EncDotNet.S100.Renderers.Mapsui.DynamicSources hosts the Mapsui-bound side of the dynamic-feature-source abstraction defined in EncDotNet.S100.Core (see docs/design/dynamic-feature-source.md). Renderers turn DynamicFeature snapshots into Mapsui IFeature + IStyle instances that the viewer's DynamicSourceOverlayHost attaches to a MemoryLayer on the overlay tier of IMapHost.
IDynamicFeatureRenderer—CanRender+Rendercontract. Implementations are stateless functions of one feature; the overlay host owns the layer-level state and UI-thread marshalling.DefaultDynamicFeatureRenderer— geometry-kind-dispatching fallback: coloured disc + optional speed-scaled heading line (six-minute predictor capped at 10 nm) forPoint, stroked polyline forCurve, translucent fill + outline forSurface. Also the safety-net renderer when a source'sRendererKeyisnullor unregistered.OwnShipRenderer— own-ship symbology under key"ownship". Draws a true-scale 5-vertex hull polygon when the on-screen vessel length exceedsMinVesselPixels(22 px ≈ 6 mm @ 96 dpi), a coloured disc otherwise, plus a heading vector with filled-triangle arrowhead in both modes and a CCRP cross at the GPS antenna in outline mode. UsesDynamicFeature.VesselGeometry(CCRP offsets) to place the hull around the antenna and gates the outline / pictogram via mutually-exclusiveMinVisible/MaxVisiblestyles so the renderer stays viewport-agnostic. Falls back to pictogram-only when noVesselGeometryis supplied (e.g. AIS targets with unknown dimensions). Seedocs/design/own-ship-symbology.md.KindMatchingRenderer— dispatches byDynamicFeature.Kindvia exact match or dot-namespaced prefix match (e.g."vessel"matches"vessel.cargo"). Longest-key-first ordering keeps prefix matching deterministic.CompositeDynamicFeatureRenderer— first-CanRender-wins fallthrough over an ordered list. Conventional ordering: per-kind specialists first,DefaultDynamicFeatureRendererlast.DynamicFeatureRendererServiceCollectionExtensions— DI helpers that register renderers under the same string key a source advertises viaDynamicSourceMetadata.RendererKey:// Register a source and its renderer in one call: services.AddDynamicFeatureSource<MyAisFeed, MyVesselRenderer>("vessel"); // Or just a renderer, for cross-source sharing: services.AddDynamicFeatureRenderer<MyVesselRenderer>("vessel");The viewer's overlay host resolves the renderer at registration time via
IServiceProvider.GetKeyedService<IDynamicFeatureRenderer>(source.Metadata.RendererKey).
Performance instrumentation
The renderer ships with optional OpenTelemetry instrumentation that attributes paint cost down to the style-renderer, layer, and geometry vertex count. All instruments are sub-millisecond per paint when no OTel listener is attached, so they are safe to leave in production builds.
| Instrument | Unit | Tags | Purpose |
|---|---|---|---|
s100.map.paint.duration |
ms | — | Compositor-thread paint wall-time per frame |
s100.map.paint.interval |
ms | — | Time between paints (idle gaps > 500 ms dropped) |
s100.map.paint.style.calls |
count | style, layer, points |
Style-renderer Draw calls per paint |
s100.map.paint.style.duration |
ms | style, layer, points |
Cumulative Draw duration per paint |
s100.layer.get_features.duration |
ms | layer |
Layer-level filter cost per GetFeatures call |
s100.layer.get_features.visible / total |
count | layer |
Visible / total feature counts per call |
s100.layer.get_features.fps |
gauge | layer |
Effective GetFeatures rate per layer |
s100.pattern_fill.draw.duration |
ms | — | AnchoredPatternFillRenderer per-call cost |
The points tag is bucketed (1-9, 10-99, 100-999, 1k-10k,
10k-100k, 100k+) to keep histogram cardinality bounded while
still revealing whether a layer's cost is driven by many cheap draws
or a few expensive ones.
To capture a measurement session, run the viewer with the OTel console exporter enabled:
ENC_DOTNET_OTEL_CONSOLE=1 OTEL_METRIC_EXPORT_INTERVAL=2000 \
dotnet run -c Release --project src/EncDotNet.S100.Viewer
Histograms are emitted every 2 s with cumulative counts and per-bucket
distributions. Aggregate by (layer, points) to identify which
geometries are dominating paint time — empirically, ~93% of paint cost
on real-world S-101 datasets is spent on geometries with ≥100 vertices,
with per-vertex cost ~1 µs. See
docs/design/mapsui-performance.md
for the full investigation and optimization plan.
Resolution-aware geometry simplification
Issue #164 adds an opt-in resolution-aware Douglas-Peucker simplification path that reduces the vertex count Skia tessellates per frame. Polylines in the 1k–10k bucket on real S-101 datasets typically simplify by 5–10× at typical pan zooms with no visible quality regression at the default 0.5-pixel tolerance.
Pipeline placement
Simplification lives on InstrumentedMemoryLayer.GetFeatures,
because that is the only seam in the pipeline that has access to the
current zoom (resolution, m/px in EPSG:3857). When a layer has
simplification enabled, every visible feature is routed through a
per-layer SimplificationCache:
- Cache key:
(original-feature reference, half-octave bucket). - Bucket:
round(log2(resolution) × 2). Tolerance for a bucket ispixelTolerance × 2^(bucket / 2)metres. - Algorithm: NTS'
DouglasPeuckerSimplifier, lines and multi-lines only in v1. Polygons, points, and other types pass through unchanged. (Polygon support is deferred until topology preservation + validation is wired in.) - Eviction: on bucket transition, drop entries from buckets outside
[active − 1, active + 1]. If the cache's tracked coordinate count still exceedsMaxCachedCoordinates(default 5 M ≈ 80 MB), the bucket farthest from the active one is dropped next, until under budget. - Simplified clones share style instances by reference and copy all
fields (including
S100.FeatureRef); they also carry anS100.OriginalFeatureback-reference. UseSimplification.GetOriginal(feature)to recover the unsimplified feature for picking / info-on-click.
Wiring
using EncDotNet.S100.Renderers.Mapsui;
using EncDotNet.S100.Renderers.Mapsui.Simplification;
if (layer is InstrumentedMemoryLayer iml)
{
iml.EnableSimplification(
DouglasPeuckerLineSimplifier.Instance,
SimplificationOptions.Default);
}
In the desktop viewer this is driven by the
Simplify line geometry (experimental) setting, applied in
DatasetLoaderService before the optional rasterization wrap.
Telemetry
| Instrument | Unit | Tags | Purpose |
|---|---|---|---|
s100.simplify.cache.hit.count |
count | s100.product |
Simplified clone served from cache |
s100.simplify.cache.miss.count |
count | s100.product |
DP invocation triggered |
s100.simplify.duration |
ms | s100.product |
Per-feature DP cost (miss only) |
s100.simplify.coords.in |
count | s100.product |
Original-geometry vertex count (miss) |
s100.simplify.coords.out |
count | s100.product |
Simplified-geometry vertex count (miss) |
s100.simplify.cache.coords.tracked |
count | s100.product |
Live coords in cache across all buckets |
The acceptance bar from issue #164 is steady-state hit rate ≥ 95%
and ≥ 50% reduction in s100.map.paint.duration mean on the
multi-S-101 workload from the perf review.
Known limits
- v1 simplifies only line geometry; polygons (e.g. depth areas) and points are unaffected. The perf review shows lines dominate the paint cost in real datasets, so this still hits the projected budget.
- The miss path runs synchronously on the render thread. After a
zoom-band transition, the first paint at the new bucket may stall
briefly while the visible set is simplified; subsequent frames hit
the cache. An async / pre-warm path is documented as future work
in
docs/design/mapsui-performance.md. - The cache is sized by coordinate count, not entry count, so a handful of very dense polylines and many small features have comparable budget cost.
Pattern-fill clip generalization
Independently of the resolution-aware line simplification above,
MapsuiDisplayListRenderer generalizes the polygon geometry used when
clipping tiled pattern fills against each other (display priority)
and against non-patterned solid fills such as land. S-101
quality/coverage areas (e.g. M_QUAL) can follow the coastline with
tens of thousands of vertices, the bulk of which are sub-pixel at chart
display scales. The NetTopologySuite Difference/Union overlay
operations these geometries feed are super-linear in vertex count, so a
single pathological area could dominate the whole frame (observed:
~10 s of an ~11 s frame on one 64k-vertex pattern zone in a real 2.35 MB
cell).
Before the overlay, each merged pattern geometry and the land exclusion
mask are passed through NTS TopologyPreservingSimplifier at a fixed
1 m (EPSG:3857) tolerance (PatternClipSimplifyToleranceMetres).
Topology-preserving simplification keeps the inputs valid for overlay;
the result is buffer(0)-repaired if it still validates as invalid, and
falls back to the original geometry on any failure. Because the clipped
boundary only bounds a tiled raster pattern fill, the generalization is
visually negligible (the S-101 visual-regression snapshot is unchanged).
An envelope-intersection test also short-circuits Difference when the
clip mask is disjoint from the entry. Together these cut the pattern
clip from ~11 s to well under 1 s on the affected cell, shaving ~6 s off
every S-101 frame (not just re-renders).
Installation
dotnet add package EncDotNet.S100.Renderers.Mapsui
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- EncDotNet.S100.Core (>= 0.13.0)
- EncDotNet.S100.Datasets.S101 (>= 0.13.0)
- EncDotNet.S100.Datasets.S421 (>= 0.13.0)
- EncDotNet.S100.Portrayals (>= 0.13.0)
- EncDotNet.S100.Renderers.Skia (>= 0.13.0)
- Mapsui (>= 5.0.2)
- Mapsui.Nts (>= 5.0.2)
- Mapsui.Rendering.Skia (>= 5.0.2)
- Mapsui.Tiling (>= 5.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- ProjNet (>= 2.0.0)
- SkiaSharp (>= 3.119.1)
-
net8.0
- EncDotNet.S100.Core (>= 0.13.0)
- EncDotNet.S100.Datasets.S101 (>= 0.13.0)
- EncDotNet.S100.Datasets.S421 (>= 0.13.0)
- EncDotNet.S100.Portrayals (>= 0.13.0)
- EncDotNet.S100.Renderers.Skia (>= 0.13.0)
- Mapsui (>= 5.0.2)
- Mapsui.Nts (>= 5.0.2)
- Mapsui.Rendering.Skia (>= 5.0.2)
- Mapsui.Tiling (>= 5.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- ProjNet (>= 2.0.0)
- SkiaSharp (>= 3.119.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.21.0 | 37 | 7/10/2026 |
| 0.20.0 | 107 | 6/29/2026 |
| 0.19.0 | 97 | 6/27/2026 |
| 0.18.0 | 113 | 6/17/2026 |
| 0.17.1 | 105 | 6/16/2026 |
| 0.17.0 | 94 | 6/15/2026 |
| 0.16.0 | 109 | 6/8/2026 |
| 0.15.0 | 110 | 6/6/2026 |
| 0.14.0 | 105 | 6/6/2026 |
| 0.13.0 | 100 | 6/3/2026 |
| 0.12.0 | 103 | 5/29/2026 |
| 0.11.0 | 105 | 5/19/2026 |
| 0.10.0 | 99 | 5/16/2026 |
| 0.9.0 | 99 | 5/15/2026 |
| 0.8.0 | 102 | 5/13/2026 |
| 0.7.0 | 99 | 5/12/2026 |
| 0.6.0 | 119 | 5/8/2026 |
| 0.5.0 | 105 | 5/4/2026 |
| 0.4.0 | 101 | 5/1/2026 |
| 0.3.0 | 107 | 4/29/2026 |