Superroute.SMap.WinForms
1.5.15
dotnet add package Superroute.SMap.WinForms --version 1.5.15
NuGet\Install-Package Superroute.SMap.WinForms -Version 1.5.15
<PackageReference Include="Superroute.SMap.WinForms" Version="1.5.15" />
<PackageVersion Include="Superroute.SMap.WinForms" Version="1.5.15" />
<PackageReference Include="Superroute.SMap.WinForms" />
paket add Superroute.SMap.WinForms --version 1.5.15
#r "nuget: Superroute.SMap.WinForms, 1.5.15"
#:package Superroute.SMap.WinForms@1.5.15
#addin nuget:?package=Superroute.SMap.WinForms&version=1.5.15
#tool nuget:?package=Superroute.SMap.WinForms&version=1.5.15

sMap for Windows
Maps from Superroute's sMap service for Windows desktop apps: vector basemaps in five colour schemes, labels in any language, large point sets with clustering, lines, flat and extruded areas, 3D buildings, and access tokens that renew themselves. A WPF control and a Windows Forms control are included, with the same API as the other sMap SDKs.
The map is drawn by the system web view (WebView2). Everything it needs to start is inside the package, so nothing is downloaded to show it except the map data itself.
Requires .NET 8 and the WebView2 Runtime, which is part of Windows 11 and of current Windows 10 installations (apps that run on older systems can ship the runtime's installer, see Microsoft's WebView2 distribution guide).
Installation
| Package | For |
|---|---|
Superroute.SMap.Wpf |
WPF apps |
Superroute.SMap.WinForms |
Windows Forms apps |
Superroute.SMap |
the shared core (options, tokens, layers); installed with either control |
dotnet add package Superroute.SMap.Wpf
# or
dotnet add package Superroute.SMap.WinForms
The app's project targets net8.0-windows (or later) with UseWPF or UseWindowsForms.
A minimal map
WPF:
<Window xmlns:smap="clr-namespace:Superroute.SMap.Wpf;assembly=Superroute.SMap.Wpf" …>
<smap:SMapView x:Name="Map" />
</Window>
using Superroute.SMap;
using Superroute.SMap.Wpf;
public MainWindow()
{
InitializeComponent();
// Read when the control is first shown.
Map.Options = new SMapOptions
{
ApiKey = "smk_…",
Style = SMapStyle.Light,
Language = "en",
Center = new SMapLatLng(43.6532, -79.3832),
Zoom = 11,
};
Map.AddPoints("stops", stops.Select(s => new SMapPoint(s.Lat, s.Lng)
{
Properties = new Dictionary<string, object?> { ["name"] = s.Name },
}), new SMapPointOptions { Cluster = new SMapCluster(), Label = "name" });
Map.OnTap("stops", (properties, at) => Title = $"{properties["name"]} {at.Lat:F5}, {at.Lng:F5}");
}
Windows Forms:
using Superroute.SMap;
using Superroute.SMap.WinForms;
var map = new SMapView(new SMapOptions { ApiKey = "smk_…" }) { Dock = DockStyle.Fill };
Controls.Add(map);
map.AddLines("route", new[] { new SMapLine(routeCoordinates) }, new SMapLineOptions { Width = 5 });
map.Ready += (_, _) => map.FitLayer("route");
map.Error += (_, e) => Debug.WriteLine(e.Exception.Message);
Layers can be added before the map is ready; they are drawn once it is. await map.ReadyAsync() waits for the
first style. Call the map from the UI thread; its events are raised there.
Map keys
A map key (smk_…) can ship inside the app. Create one in the developer center and list your app on its allowlist
as a Windows app:
- a packaged app (MSIX): its package family name, e.g.
Contoso.Dispatch_8wekyb3d8bbwe; - an unpackaged app: the product name it declares — the
<Product>property of the app's project (AssemblyProductAttributeof the entry assembly), else the name of its entry assembly.
Entries are compared in lower case. A product name must use ASCII letters, digits, spaces and punctuation only; a name with other characters cannot be sent.
The SDK reads the identity from the system and sends it, with the key, on every request to the map service's nodes — map data, styles, fonts and icons included — and to no other host; the app cannot set it itself. With a token too, both are sent and the token is checked first.
Tokens
Give a token directly (Token), or let the map ask your server for one with GetToken. It is called at start when
no token is given, and again two minutes before each token expires.
var options = new SMapOptions
{
GetToken = async () => await http.GetStringAsync("https://api.example.com/map-token"),
};
Tokens are always fetched by your app's code, never by the map's page. Keep the signing secret on your server; the app only ever sees tokens.
Styles and languages
SetStyle(SMapStyle.Light | White | Grayscale | Dark | Black) and SetLanguage("fr") (any BCP-47 tag, e.g.
zh-Hans; the system's language by default). Both keep every layer you added. MapStyle and MapLanguage give
what is shown.
A custom style saved in the Open Platform portal is used by name: SetStyle("custom:sms_…") (or
SMapStyleName.Custom(id)), or StyleName = "custom:sms_…" in the options; MapStyleName gives the name shown.
Basemap
SetBasemap(overrides) shows, hides and recolours the basemap by class, in any scheme and language:
map.SetBasemap(JsonNode.Parse("""
{"water": {"color": "#9cc0e6"}, "pois": {"visible": false}, "roads": {"color": "#ffffff", "width": 1.2}}
""")!.AsObject());
Classes: land, landcover, parks, landuse, airports, water, roads (and roads.highway, roads.major,
roads.minor, roads.paths), buildings, rail, boundaries, labels (and labels.places, labels.roads,
labels.water, labels.addresses) and pois. Fields: visible, color, casing (road casings), halo (labels),
opacity (0–1), width (a factor, 0.25–4). A class such as roads.major wins over its parent roads. Each call
replaces the previous overrides ({} clears them); Basemap() returns them. An unknown class or field, or a value
out of range, throws an SMapException naming it.
SMapBasemap.Apply(styleDocument, overrides) applies overrides to a style document without a map (a preview), as the
service does for custom styles; SMapBasemap.Check(overrides) lists what is wrong with them.
Layers
| Method | Options |
|---|---|
AddPoints(id, points or SMapData, options) |
Cluster (radius 50, max zoom 14; click a cluster to zoom in), Color, Radius, StrokeColor, StrokeWidth, Icon (a name given to RegisterImage, registered before the layer is added), IconSize, Label (a property name), MinZoom, MaxZoom, ZIndex |
AddLines(id, lines or SMapData, options) |
Color, Width, Dash, Opacity, MinZoom, MaxZoom, ZIndex |
AddPolygons(id, polygons or SMapData, options) |
Fill, Opacity, Outline, OutlineWidth, Extrude (Height, Base: a number or a property name), MinZoom, MaxZoom, ZIndex |
Every layer also takes a Slot: Bottom (over land and water), Roads (under the roads), Labels (under the
labels) or Top. A layer without a slot draws as before: on top, in the order added.
SMapData is GeoJSON (SMapData.FromGeoJson(text or JsonNode): a FeatureCollection, a Feature or a geometry) or
lists of points, lines or areas (FromPoints, FromLines, FromPolygons). Colours are CSS colours ("#2563eb",
"rgba(37, 99, 235, 0.8)"). Properties are strings, numbers, booleans, null, lists, dictionaries or JsonNode
values.
Also: SetData(id, data), SetVisible(id, visible), RemoveLayer(id), Layers() (ids in draw order),
FitLayer(id, padding: 40, maxZoom: 15), OnTap(id, (properties, position) => …) (a click on a feature),
FlyTo(center, zoom), SetBuildings3D(true, color, opacity) and RegisterImage(name, png bytes / BitmapSource / Image).
A higher ZIndex draws on top; equal values keep the order added. 3D buildings are drawn under your layers.
Style layers and sources
Style Specification v8 sources and layers are passed through unchanged, so a layer copied from a style editor or another map page works as it is:
map.AddSource("parcels", JsonNode.Parse("""{"type": "vector", "url": "https://tiles.example/parcels.json"}""")!.AsObject());
map.AddStyleLayer(JsonNode.Parse("""
{"id": "parcel-lines", "type": "line", "source": "parcels", "source-layer": "parcels",
"paint": {"line-color": "#64748b"}}
""")!.AsObject(), new SMapLayerPosition { Slot = SMapSlot.Roads });
map.SetPaint("parcel-lines", "line-width", 2);
SetPaint, SetLayout, SetFilter, GetStyleLayerAsync work on any layer, the basemap's included;
RemoveLayer removes either kind and RemoveSource a source. SMapLayerPosition takes Before (a layer id) or
Slot.
Files
KML, KMZ, GPX, encoded polylines (polyline precision 5, polyline6), WKT, WKB, CSV with latitude / longitude
columns, TopoJSON and GeoJSON:
map.AddData("trip", File.ReadAllBytes("trip.kmz"), new SMapDataOptions { Read = new SMapReadOptions { Name = "trip.kmz" } });
var features = await map.ReadFormatAsync(File.ReadAllText("stops.csv"), new SMapReadOptions { Format = "csv", Lat = "y", Lng = "x" });
var kml = await map.ToKmlAsync("trip");
AddData draws the points as the layer trip, the lines as trip:lines and the areas as trip:areas; OnTap("trip", …) receives clicks on all three. The format is guessed from the file name or the content when not given (encoded
polylines are never guessed). Features without colours of their own are drawn with their simplestyle properties
(marker-color, stroke, fill, …), as geojson.io draws them; KML styles become those properties. Files are read
in the map by the same code as the JS SDK, so every sMap SDK gives the same features. ToGeoJsonAsync and
ToKmlAsync write a layer out.
ConvertAsync(bytes, options) reads a file on the service instead (at most 20 MB, Format needed), and
SMapConvertClient does the same without a map.
External layers
| Method | Options |
|---|---|
AddRasterLayer(id, options) |
Tiles ({z}/{x}/{y} templates) or Url (TileJSON), TileSize, Opacity, Bounds |
AddWmsLayer(id, options) |
Url, Layers, Styles, Format, Transparent, Version (1.3.0 / 1.1.1), Params, TileSize, Opacity |
AddWmtsLayer(id, options) |
Url (capabilities or a REST template) or Capabilities (the document's text), Layer, TileMatrixSet, Style, Format, Encoding, Opacity |
AddVectorTiles(id, options) |
Tiles, Url (TileJSON or PMTiles) or PmTiles, Layers (style layers), Bounds |
AddArcGisLayer(id, options) |
Url (MapServer, ImageServer or VectorTileServer), Layers (vector tiles; the service's own style when none), Opacity |
Every one also takes Attribution, Slot or Before, ZIndex, MinZoom and MaxZoom.
Each layer's attribution is shown beside the map data attribution. Google and Bing tiles are refused (their licences
do not allow it). A WMTS capabilities document is read by the map's page, so its server must allow cross-origin
reads; otherwise read it in the app and pass its text as Capabilities. A REST template needs no read. A WMTS or
ArcGIS service that cannot be read is reported through the Error event.
RasterUrl(style, language, retina) gives the service's own raster tiles of a scheme or custom style as a
{z}/{x}/{y} template on the map's node with the map key, for tools that cannot draw vector tiles
(SMapRasterUrl builds it without a map, with TileJson and WmtsCapabilities beside it).
Layer descriptions
await map.ToJsonAsync() describes the map's basemap, sources and layers in the JSON every sMap SDK reads;
LoadLayers(json) loads one on another map (web, Android, iOS, Flutter, macOS, Windows). SMapLayerDescription
reads and writes it in C#, and SMapCanonicalJson serialises JSON the way the SDKs compare it.
Markers, drawing and more (1.2.0)
map.AddCustomMarkers("vans", SMapData.FromGeoJson(vans), new SMapMarkerOptions(SMapMarkerOptions.Template("<b class=\"van\">{name}</b>"))
{
IdProperty = "id",
Anchor = SMapAnchor.Bottom,
Drag = new SMapDragOptions { Draggable = true, OnDragEnd = (f, e) => Save(f.Key, e.Position) },
});
map.AddPoints("stops", SMapData.FromGeoJson(stops), new SMapPointOptions
{
Cluster = new SMapCluster { Ring = new SMapClusterRing("color") },
Tooltip = SMapTooltip.Template("{name}"),
});
map.AddHeatmap("density", SMapData.FromGeoJson(stops), new SMapHeatmapOptions { Radius = 18 });
var draw = map.DrawTool(new SMapDrawOptions { OnChange = e => Console.WriteLine(e.Type) });
var picked = await map.SelectAreaAsync(lasso: true, layers: new[] { "stops" });
Markers attached to points (1.5.15): set AttachTo to a point layer's id and each marker shows only while the point
with its key is drawn on its own (not grouped into a cluster, its layer visible, in the data, not being dragged); a
marker without handlers of its own takes no pointer events. await map.DrawnPointsAsync("stops") gives the keys of the
points drawn on their own now.
A marker's HTML is inserted as it is: escape the feature's text (a template does). AddModels, SetBaseImagery,
SetGoogleBase, SetTerrain, AddTool(id, SMapLayersTool), AddCanvasTiles and SyncWith complete the set.
The camera, events, states and more (1.5.0)
The map gaps G15–G37 of every sMap SDK, done by the Web SDK the map's page carries (so they follow the web exactly, pointer hover and right click included). Calls that read the map are asynchronous; camera moves and listeners given before the map is ready wait for it. Points on the view are device-independent pixels from its top-left corner.
// Fitting and the camera (G15, G16)
map.FitBounds(new SMapBounds(-79.6, 43.6, -79.2, 43.8), new SMapFitOptions { Padding = SMapPadding.All(60), MaxZoom = 14 });
map.FitLayer(new[] { "stops", "route" });
map.EaseTo(new SMapCameraOptions { Zoom = 15, Bearing = 30 }, TimeSpan.FromMilliseconds(800));
var camera = await map.GetCameraAsync();
map.On(SMapEventType.MoveEnd, e => Console.WriteLine(e.Zoom));
map.On(SMapEventType.DoubleClick, e => Zoomed(e.Position), preventDefault: true); // the map does not zoom
var pixel = await map.ProjectAsync(new SMapLatLng(43.65, -79.38));
// Styles per feature, states, pointer events, popups (G19–G22)
map.AddPoints("stops", stops, new SMapPointOptions
{
IdProperty = "code",
ColorValue = SMapFeatureValue<string>.Computed(p => (bool?)p["late"] == true ? "#dc2626" : "#16a34a"),
States = new[] { new SMapState<SMapPointLook>("hover", new SMapPointLook { Radius = 9 }),
new SMapState<SMapPointLook>("selected", new SMapPointLook { Color = "#111827" }) },
HoverState = true,
OnContextMenu = (f, e) => ShowMenu(f.Key, e.Point),
Popup = SMapMarkerOptions.Template("<b>{name}</b>"), // escapes the feature's text
});
map.SetFeatureState("stops", "A12", new JsonObject { ["selected"] = true });
var under = await map.FeaturesAtAsync(new SMapPixel(120, 80));
map.OpenFeaturePopup("stops", "A12");
// Moving one feature, headings (G23, G24)
map.MoveFeature("vans", "v7", new SMapLatLng(43.66, -79.39), TimeSpan.FromSeconds(1));
map.UpdateFeature("vans", "v7", new JsonObject { ["status"] = "late" });
// Images, circles, the picture (G25, G26, G29)
await map.AddImageAsync("arrow", File.ReadAllBytes("arrow-sdf.png"), new SMapImageOptions { Sdf = true });
map.AddPoints("heading", vans, new SMapPointOptions { Icon = "arrow", IconColor = SMapFeatureValue<string>.FromProperty("color", "#2563eb"),
Rotation = SMapFeatureValue<double>.FromProperty("heading", 0) });
map.AddCircles("reach", depots, new SMapCircleOptions { Radius = 500, Area = new SMapPolygonOptions { Fill = "#0ea5e9" } });
byte[] png = await map.ExportAsync(new SMapExportOptions { PixelRatio = 2 });
// The search tool: your own results, the SDK never calls a geocoder (G35)
map.AddTool("search", new SMapSearchOptions(async text => await MyPlaces(text)) { OnPick = r => Picked(r.Data) });
- Geometry (G14, G27):
SMapGeo.Distance,Length,Bounds,PointInPolygon,NearestPointOnLine,DistanceToLine,DecodePolyline,EncodePolyline,Area,Centroid,PointOnFeature,Circle,Union,Buffer,SimplifyandSelfIntersects, in C# with the answers of every other sMap SDK. - Drawing (G28):
SMapDrawOptions.OnDrawing(length, area, radius, validity while a shape is drawn or edited),Measure,AllowSelfIntersection = false,SnapLayers, a look per shape withStyleProperty+Styles, andSMapDrawTool.SetProperties(undoable) and itsDrawingevent. - Lines and labels (G34):
Arrows = new SMapLineArrows { Share = 0.15 }(or a pixelSpacing);AllowOverlap,PriorityandLabelSizeon point and line labels. Heat maps takeBlurandMinOpacity(G33), models a tintColorper feature (G30), a layer switcher base aGoogleentry (G37). - When the map cannot be shown (G32):
map.On(SMapEventType.Unavailable, e => …)withe.Reason(Forbidden,Unreachable,Unsupported), told at once to a listener added after;map.Unavailableholds the reason. A notice in the map's language covers the map unlessSMapOptions.UnavailableNotice = false. - Small maps in a scrolling window (G36):
SMapOptions.CooperativeGestures = true.
Differences from the web, as the page answers over the web view's channel: preventDefault is asked for when the
listener is added; a drawing tool's per-shape look is a table by a property value (StyleProperty + Styles), and
its measure label uses the map's own text (no format function); MoveFeature samples the easing into a table;
FeatureState answers the states the app set (the page's hover is the page's); the web's resize() is
ResizeMap() (beside the controls' own Resize). Your own three.js objects (addThreeLayer, G31) are not wrapped:
the page carries the Web SDK, so a page script run with CallJavaScriptAsync can call smap.map.addThreeLayer.
Map tools
map.Options = new SMapOptions { ApiKey = "smk_…", Tools = new SMapToolsOptions { Hidden = savedHidden } };
map.AddTool("zoom", SMapToolKind.Zoom);
map.AddTool("scale", SMapToolKind.Scale);
map.AddTool("legend", legendPanel, "Legend"); // your own element or control, placed by you
map.AddTool("stops", new[] { "stops", "stop-labels" }, "Stops"); // data layers: shown and hidden with the tool
map.ToolsChanged += (_, e) => Save(e.Hidden);
map.SetToolsVisible(true);
- The map draws
Zoom,Compass,ScaleandLocatein their corners (or the one you give), with a tools control (one button) that lists every tool with Show all and Hide all. The tools control is the last button of the map's toolbar by default (SMapToolsOptions.Control = SMapToolPosition.Toolbar: the corner holding the zoom or compass, top left without one, kept last when tools join it); give a corner to put it there, or null to leave it out. While its panel is open its corner is drawn above the map's other corners, and the panel opens towards the middle of the map. AddTool(id, layers, label)makes data layers a tool: layers of the map's layer calls or of the basemap, by id. Hiding the tool hides them (layout visibility), showing it shows them; a layer not on the map is left alone, one added later under the id of a hidden tool starts hidden, and removing a hidden tool shows its layers again.AddTool(id, view)takes a WPFUIElement(hidden =Collapsed) or a WinFormsControl(hidden =Visible = false) that you lay out yourself over or beside the map; it is not moved into the map's corners.- The attribution, the sMap badge and the tools control are never tools. The map keeps no state: give the starting
state in
Tools.Hiddenand keepToolsChanged'sHiddenwhere you like.
Plan editor (1.1.6)
A person edits a plan of routes on the map — drags a stop within or between routes, takes it off, places an
unassigned job, re-orders a route around locked stops — and every edit is one call of the Open Platform's plan
editing with the whole plan. The editor keeps no plan on any server: the edited plan comes back to the app through
its Changed event. It is the same editor as on the web, Android and Apple (it passes every case of
shared/plan-contract.json), written in C# in Superroute.SMap; the map's page only draws and drags.
using Superroute.SMap;
// /plan-token on your own server calls POST /v1/plan/token with its API token and hands the answer on.
var options = new SMapPlanEditorOptions(SMapPlanCredentials.Token(async () =>
SMapPlanToken.FromJson(await http.GetStringAsync("https://my.server/plan-token"))!))
{
Plan = SMapPlan.FromJson(planJson),
};
var editing = map.PlanEditor(options); // the layers, and the panel as the map tool "plan-editor"
editing.Editor.Changed += (_, e) => { if (e.Plan is { } plan) Save(plan.ToJson()); };
editing.Editor.Error += (_, e) => Log(e.Exception.Code, e.Exception.Reason);
- Credentials (one place, sent as
Authorization: Bearer …):SMapPlanCredentials.Token(provider), a provider of the short-lived plan editor token (sr_pet_…) that your server mints withPOST /v1/plan/token— it returns the token (a string converts) or the token answer (SMapPlanToken.FromJson), is called at first use and 30 seconds before the expiry it gives, and once more after a401 SESSION_EXPIRED; concurrent calls wait for one provider call. OrSMapPlanCredentials.Key("smk_…"), a map key with Allow plan editing switched on, never renewed. Never put an API token in the app. Every call also sendsX-SMap-App: windows:<id>, read from the system as for map requests (the package family name when packaged, else the product name). Other refusals (401 SESSION_REVOKED,TOKEN_INVALID,403 NOT_AUTHORIZEDwith their reasons) are raised asSMapPlanExceptionand never repeated. - Fast path (no code change): when the token answer carries
fast_path.url(SMapPlanToken.FastPathUrl, read bySMapPlanToken.FromJson), or the map key's configuration on sMap names one (GET <SmapUrl>/plan/v1/config, kept 10 minutes;SMapView.PlanEditoruses the map'sBaseUrl, otherwisehttps://tile.srsr.ca), every call goes to<url>/<same path>with the same credential, headers and body. A409 USE_STANDARD_PATH(orX-Plan-Path: standard), a 5xx, a network error or no answer within 2 s (FastPathTimeout; refine 5 s,FastPathRefineTimeout) repeats the same call once on the standard path; the next call tries the fast path again;402,429and every other 4xx are final.State().Path,SMapPlanChangeEventArgs.PathandSMapPlanEditResult.PathgiveSMapPlanPath.FastorStandard;FastPath = falsekeeps every call on the standard path. - Compressed answers: sMap gzips plan answers when the request allows it. The shared client asks for them
(
Accept-Encoding: gzip, deflate, br) and decompresses them. A handler you give throughSMapPlanEditorOptions.HttpHandleris used as it is: set itsAutomaticDecompression(for examplenew SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.All }) to get the same; without it the answers come uncompressed. - Edits (
Task<SMapPlanEditResult?>):ResequenceAsync,MoveAsync,RemoveAsyncandDropStopAsyncare drawn at once and priced when the answer comes;InsertAsync(new jobs asJsonObject, or the id of an unassigned job — both convert toSMapPlanJob) andRefineAsyncchange the plan when answered;Choices/ChooseAsynctake another position an insert answered;Lock/Unlockkeep stops in place for refine;UndoAsync/RedoAsync;RecostAsync;PreviewAsyncprices other routes without changing the plan. A task gives null when a newer edit took its place (its request is cancelled), and throwsSMapPlanException(Status,Code,Reason) when the service refused it — the edit is then taken back and kept asLastError. - Events:
Changed(Kind,Plan,Result,Answer,Pending,CanUndo,CanRedo),BusyChanged,Error,Snapshot(the distance snapshot:opened,used,expired, …),PreviewChanged,Destroyed.State()gives everything at once. Call the editor on the UI thread; its events are raised there. - Distance snapshot: opened before the first call and named in every call, opened again by itself when it
expired; a snapshot that cannot be opened never stops a call.
Snapshot = falseturns it off. - On the map (
map.AttachPlanEditor(editor)on its own): the legs, each vehicle's start, a numbered pin per stop (border dark red with a violation, near black when locked) and the unassigned jobs, drawn with the map's own points and lines (plan:legs,plan:stops, …). A stop or an unassigned job is picked up with the pointer (the map does not pan meanwhile); it drops before the stop under the pointer, into a route's line, or first after a start. While it is dragged, the routes the drop would make are priced as a preview (250 ms after the target last changed). - The panel (
SMapPlanPanel, a WPFUserControlor a WinFormsUserControl, also made on its own withnew SMapPlanPanel(editor, options)): Undo, Redo, Refine and the busy status, the totals, each route with its stops (arrival, violations, Lock, Remove), the unassigned jobs with Place and the positions an insert answered. Stops are dragged within and between routes in the list, and onto the unassigned jobs to take them off their route. Texts in 14 languages fromshared/plan-texts.json(the map's language unlessSMapPlanPanelOptions.Languagesays otherwise;Labelsreplaces any text). In WinForms the panel lies over the map in the cornerSMapPlanPanelOptions.Positionnames; in WPF it docks beside the map on that corner's side (WPF cannot draw over the web view). The tools control shows and hides it as the toolplan-editor;withPanel: falseleaves it out. editing.Destroy()ends it: calls cancelled, the snapshot closed, layers, panel and tool removed.- Drop points (OPEN-ROUTE-EDIT E25 / E26):
editor.PrepareInsert(points)(orPrepareInsert(lat, lng)/PrepareInsert(jobId)) sends points about to be inserted to the open snapshot at once, so their legs are measured before the insert is confirmed. Call it when a new stop is dropped on the map (on release) or an address is picked in your own insert form; the editor does it itself when a job of no route is dropped on a route (drops on the page included). The call isPOST <BaseUrl>/v1/plan/matrix/<id>/points, always on the standard path, each point once per snapshot, nothing without a snapshot; it never waits and never reports an error. An insert of such a point prepared less thanInsertWaitMinAge(300 ms) before is sent at once; later, while the point is not known to be measured, it first asks the server to wait for it (wait_ms=InsertWaitBudget, 1100 ms, minus the time since the point was sent, within 100-1000 ms; the call given up afterwait_ms+ 200 ms, at mostInsertWaitTimeout, 1.2 s) and is sent then either way.
Options
| Option | Default | What |
|---|---|---|
BaseUrl |
https://tile.srsr.ca |
the public service address |
Nodes |
none | seed node URLs, known before the first node list arrives |
Token / GetToken |
none | a token, or an async function returning one |
ApiKey |
none | a map key, see Map keys |
Style |
Light |
colour scheme |
Language |
system language | label language (BCP-47) |
Nearest |
true |
read from the nearest node |
Center, Zoom, Bearing, Pitch |
initial camera | |
CooperativeGestures |
false |
one finger scrolls the window, Ctrl + wheel zooms (G36) |
UnavailableNotice |
true |
the notice over a map that cannot be shown (G32) |
HttpHandler, Store |
shared client, a file under %LOCALAPPDATA%\Superroute\SMap |
networking for the node list and where node choices are remembered |
The control's UserDataFolder sets where the web view keeps its data (%LOCALAPPDATA%\Superroute\SMap\WebView2\<app>
by default, so it works when the app is installed in a folder it cannot write to).
Nearest node
By default the map asks the service for its nodes, measures each one and reads from the fastest; the choice is remembered for an hour. When the first style cannot be loaded from a node, the next one is used.
The map keeps working while the public address is down:
- the last node list received is kept for 30 days;
- the list is asked of
BaseUrlfirst, then of every other known node (the kept list and theNodesseeds) in turn — any node answers the complete list; - when no node answers, the kept list and the seeds are measured and used.
Pass Nodes so that even a first start works while BaseUrl is down.
Anything else
The web view is available as map.WebView. CallJavaScriptAsync runs script on the map's page and returns its
result as JSON text; there the map is smap.map and its full map object smap.map.engine, for any map capability
not wrapped here:
var zoom = double.Parse(await map.CallJavaScriptAsync("smap.map.engine.getZoom()"), CultureInfo.InvariantCulture);
The map shows its data attribution; its links open in the browser. Every map shows a small sMap badge in its bottom-right corner, left of the attribution (above it when the map is too narrow for both).
Example apps
windows/examples/WpfExample and windows/examples/WinFormsExample show one window with the colour scheme and
language switchers, 2,000 clustered points, a line, an extruded area and a 3D buildings toggle. Set SMAP_TOKEN or
SMAP_API_KEY (and optionally SMAP_BASE_URL) in the environment; list smap wpf example or
smap winforms example (their product names) on the key's allowlist.
dotnet run --project windows/examples/WpfExample
Development
From windows/:
dotnet build Superroute.SMap.sln -c Release # on macOS or Linux add -p:EnableWindowsTargeting=true
dotnet test tests/Superroute.SMap.Tests
dotnet pack Superroute.SMap.sln -c Release -p:Version=1.1.0 -o artifacts
The core (Superroute.SMap) has no Windows dependency, so its tests run anywhere; the page bridge is checked with
Node.js when it is installed. The page's files are embedded in Superroute.SMap: smap.js, smap.css and
vendor-licenses.txt in src/Superroute.SMap/Web/ are the built Web SDK, copied by tools/bundle-web.sh (which
records their origin in Web/SOURCE.txt); index.html, bridge.js and bridge-interop.js (the 1.1.0 calls, loaded right
after bridge.js) are shared with the macOS SDK and live in shared/web/. The basemap class table is shared/basemap-classes.json, embedded as it is. The WebView2 host is
one set of sources (src/Shared/) compiled into both control packages.
Releases are built on GitHub Actions (.github/workflows/windows.yml): a tag 1.2.0 builds, tests, packs the three
packages as version 1.2.0 and publishes them to NuGet.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. |
-
net8.0-windows7.0
- Microsoft.Web.WebView2 (= 1.0.4258.31)
- Superroute.SMap (>= 1.5.15)
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 |
|---|---|---|
| 1.5.15 | 0 | 11/1/2026 |
| 1.5.12 | 0 | 10/31/2026 |
| 1.5.11 | 0 | 10/31/2026 |
| 1.5.10 | 0 | 10/31/2026 |
| 1.5.9 | 33 | 10/31/2026 |
| 1.5.8 | 33 | 10/31/2026 |
| 1.5.7 | 38 | 10/31/2026 |
| 1.5.6 | 42 | 10/31/2026 |
| 1.5.5 | 41 | 10/31/2026 |
| 1.5.2 | 47 | 10/31/2026 |
| 1.5.1 | 37 | 10/30/2026 |
| 1.5.0 | 41 | 10/30/2026 |
| 1.2.16 | 37 | 10/30/2026 |
| 1.2.15 | 44 | 10/29/2026 |
| 1.2.12 | 38 | 10/29/2026 |
| 1.2.11 | 41 | 10/29/2026 |
| 1.2.10 | 44 | 10/29/2026 |
| 1.2.9 | 42 | 10/29/2026 |
| 1.2.8 | 44 | 10/29/2026 |
| 1.2.7 | 45 | 10/29/2026 |