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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Superroute.SMap.WinForms" Version="1.5.15" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Superroute.SMap.WinForms" Version="1.5.15" />
                    
Directory.Packages.props
<PackageReference Include="Superroute.SMap.WinForms" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Superroute.SMap.WinForms --version 1.5.15
                    
#r "nuget: Superroute.SMap.WinForms, 1.5.15"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Superroute.SMap.WinForms@1.5.15
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Superroute.SMap.WinForms&version=1.5.15
                    
Install as a Cake Addin
#tool nuget:?package=Superroute.SMap.WinForms&version=1.5.15
                    
Install as a Cake Tool

sMap

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 (AssemblyProductAttribute of 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, Simplify and SelfIntersects, 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 with StyleProperty + Styles, and SMapDrawTool.SetProperties (undoable) and its Drawing event.
  • Lines and labels (G34): Arrows = new SMapLineArrows { Share = 0.15 } (or a pixel Spacing); AllowOverlap, Priority and LabelSize on point and line labels. Heat maps take Blur and MinOpacity (G33), models a tint Color per feature (G30), a layer switcher base a Google entry (G37).
  • When the map cannot be shown (G32): map.On(SMapEventType.Unavailable, e => …) with e.Reason (Forbidden, Unreachable, Unsupported), told at once to a listener added after; map.Unavailable holds the reason. A notice in the map's language covers the map unless SMapOptions.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, Scale and Locate in 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 WPF UIElement (hidden = Collapsed) or a WinForms Control (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.Hidden and keep ToolsChanged's Hidden where 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 with POST /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 a 401 SESSION_EXPIRED; concurrent calls wait for one provider call. Or SMapPlanCredentials.Key("smk_…"), a map key with Allow plan editing switched on, never renewed. Never put an API token in the app. Every call also sends X-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_AUTHORIZED with their reasons) are raised as SMapPlanException and never repeated.
  • Fast path (no code change): when the token answer carries fast_path.url (SMapPlanToken.FastPathUrl, read by SMapPlanToken.FromJson), or the map key's configuration on sMap names one (GET <SmapUrl>/plan/v1/config, kept 10 minutes; SMapView.PlanEditor uses the map's BaseUrl, otherwise https://tile.srsr.ca), every call goes to <url>/<same path> with the same credential, headers and body. A 409 USE_STANDARD_PATH (or X-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, 429 and every other 4xx are final. State().Path, SMapPlanChangeEventArgs.Path and SMapPlanEditResult.Path give SMapPlanPath.Fast or Standard; FastPath = false keeps 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 through SMapPlanEditorOptions.HttpHandler is used as it is: set its AutomaticDecompression (for example new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.All }) to get the same; without it the answers come uncompressed.
  • Edits (Task<SMapPlanEditResult?>): ResequenceAsync, MoveAsync, RemoveAsync and DropStopAsync are drawn at once and priced when the answer comes; InsertAsync (new jobs as JsonObject, or the id of an unassigned job — both convert to SMapPlanJob) and RefineAsync change the plan when answered; Choices / ChooseAsync take another position an insert answered; Lock / Unlock keep stops in place for refine; UndoAsync / RedoAsync; RecostAsync; PreviewAsync prices other routes without changing the plan. A task gives null when a newer edit took its place (its request is cancelled), and throws SMapPlanException (Status, Code, Reason) when the service refused it — the edit is then taken back and kept as LastError.
  • 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 = false turns 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 WPF UserControl or a WinForms UserControl, also made on its own with new 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 from shared/plan-texts.json (the map's language unless SMapPlanPanelOptions.Language says otherwise; Labels replaces any text). In WinForms the panel lies over the map in the corner SMapPlanPanelOptions.Position names; 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 tool plan-editor; withPanel: false leaves 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) (or PrepareInsert(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 is POST <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 than InsertWaitMinAge (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 after wait_ms + 200 ms, at most InsertWaitTimeout, 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 BaseUrl first, then of every other known node (the kept list and the Nodes seeds) 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 Compatible and additional computed target framework versions.
.NET net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed