S-100 map host sample (Map.AddS100)
This sample is a small Avalonia and Mapsui desktop app that shows S-100 data
on its own map. It adds S-100 rendering to a stock Mapsui MapControl through
the public AddS100(...) extension and the IS100MapSession it returns. It
has its own window, controls and pointer handling, and doesn't reference
SoundCharts (EncDotNet.S100.Viewer).
Use it as a starting point for adding S-100 layers to your own Mapsui application.
Prerequisites
- The .NET 10 SDK.
The sample includes an S-101 cell, sample-cell.000. It's the IHO S-101 test
cell 101AA00DS0008.000, linked from tests/datasets/.
Run the sample
From the repository root:
dotnet run --project samples/EncDotNet.S100.Samples.MapHost
In the window:
- Choose Load cell to load and render the bundled S-101 cell. The map zooms to it.
- Choose Day, Dusk or Night to change the palette, and clear Visible to hide the cell.
- Drag to pan and use the mouse wheel to zoom.
- Click the map to pick features. The topmost feature is outlined, and the status bar names it.
- Choose Zoom to cell to frame the cell again, or Unload to remove it.
Run the headless check
The --smoke option drives the same session without a window, so it runs in
CI:
dotnet run --project samples/EncDotNet.S100.Samples.MapHost -- --smoke
It attaches a session to a bare Map, loads the cell, zooms to it, picks at
the cell's centre and removes the cell. It ends with:
PASS: reusable S-100 session loaded, framed, picked, and tore down headlessly.
On failure it prints a FAIL: line to standard error and exits with code 1.
How it works
The integration is in the
MainWindow
constructor and event handlers. In outline:
// Create a processor factory with the bundled catalogues for every product.
// It holds catalogue caches, so dispose it with the session.
using var factory = BundledDatasetProcessorFactory.Create();
// Attach a session to the control's map. AddS100 returns the session, which
// owns the S-100 layers, processors and rendering, and an adapter for pointer
// picks and snapshots. It also makes background renders repaint the control.
var map = new Map { CRS = "EPSG:3857" };
mapControl.Map = map;
var (session, adapter) = mapControl.AddS100(new S100MapsuiOptions
{
CrsTransformFactory = new ProjNetCrsTransformFactory(),
DatasetPipelineFactory = factory,
});
// Drive the session. Mapsui still handles navigation.
var id = await session.Datasets.LoadAsync("cell.000");
session.ZoomToDataset(id);
await session.SetPresentationAsync(MapPresentationState.Default.WithPalette(PaletteType.Night));
var picks = await adapter.PickAtScreenAsync(session.Query, x, y);
// Release every processor, layer, subscription and cache.
session.Dispose();
Each button and pointer handler in the code-behind calls one part of the API, with comments that explain why the API works that way:
| Task | API |
|---|---|
| Attach a session to an existing map | mapControl.AddS100(options), or map.AddS100(options) for a bare Map |
| Load or unload a dataset | session.Datasets.LoadAsync(path), session.RemoveDataset(id) |
| Change the palette | session.SetPresentationAsync(state.WithPalette(...)) |
| Show or hide a dataset | session.SetVisible(id, visible) |
| Zoom to a dataset | session.ZoomToDataset(id), alongside normal MapControl gestures |
| Pick the features under the pointer | adapter.PickAtScreenAsync(session.Query, x, y) |
| Pick at a position, without a UI | session.Query.PickAsync(new GeographicPickQuery { ... }) |
| Add a host overlay layer | session.Layers.AddOverlayLayer(layer) |
| Outline a picked feature | S100PickHighlightLayer.Show(pick) |
| Show a cell's extent when it's zoomed out of view | S100DatasetExtentIndicatorLayer.Show(...) |
| Clean up | adapter.Dispose(), session.Dispose() |
| File | What it shows |
|---|---|
MainWindow.axaml.cs |
The integration: create, attach, drive and dispose. Start here. |
MainWindow.axaml |
The toolbar and a stock Mapsui MapControl. AddS100 doesn't need a special subclass. |
SmokeTest.cs |
The same session without a window, for --smoke. |
Wiring notes
- Projection. The map's
CRSisEPSG:3857. The renderer projects datasets to Web Mercator, and the pick adapter converts pointer pixels back to WGS-84. - CRS transforms. The rendering assembly has no CRS implementation, so the
host supplies one:
ProjNetCrsTransformFactoryfromEncDotNet.S100.Crs.ProjNet. - Processor factory.
Datasets.LoadAsyncneeds anIDatasetProcessorFactory.BundledDatasetProcessorFactory.Create(), from theEncDotNet.S100package, returns one set up with the bundled catalogues for every product, so you don't set up catalogue managers, the Lua engine, the CRS factory or the product registry yourself. Dispose it with the session. - Overlays.
S100DatasetExtentIndicatorLayerandS100PickHighlightLayerare optional. Add the ones you want withsession.Layers.AddOverlayLayer(...), which places them above the dataset layers, and drive them withShowandClear. Leaving them out doesn't change dataset rendering. - Repaints. When a background render finishes, such as after a palette
change, the session signals a redraw.
mapControl.AddS100turns that signal into aRefreshGraphicscall on the UI thread. With a bareMap, supply your ownS100MapsuiOptions.RedrawMarshal; without one, the session redraws inline, which is fine headlessly.
Use S100MapControl instead
This sample attaches the session by hand, with explicit fields and disposal,
to show each step. S100MapControl is a MapControl subclass that creates and
owns the session for you: setup is one Configure(options) call and cleanup is
one Dispose(). See
One-call control
in the EncDotNet.S100.Renderers.Mapsui.Avalonia README.
Package references
The sample uses project references so that it builds in this repository. In
your own app, reference the published packages; the code in MainWindow stays
the same. It needs:
EncDotNet.S100.Renderers.MapsuiEncDotNet.S100.Renderers.Mapsui.AvaloniaEncDotNet.S100.Crs.ProjNetEncDotNet.S100
AddS100 itself doesn't depend on any S-100 product. It takes an
IDatasetProcessorFactory from EncDotNet.S100.Core. EncDotNet.S100 brings in
every product through BundledDatasetProcessorFactory. For a smaller app, build
your own IDatasetProcessorFactory, or a DatasetPipelineFactory with a
smaller ProductRegistry, and reference only the product packages you
need.