Open Application Platform Docs

sMap integration

sMap

sMap integration

The map SDKs display Superroute basemaps in websites and apps: five colour schemes, labels in any language, points (clustered or not), lines, areas, and 3D buildings. Every SDK has the same API and reads from the nearest map node.

Version 1.1.0 adds standard style layers, basemap options and saved custom styles, data files (KML, GPX, CSV and others), layers from other map services, raster tiles, and adapters for other map tools.

The standards sMap follows, what each map tool can do in both directions, use cases and limits are described in sMap interoperability.

1. Access and billing

Maps use the capability map.tiles, enabled on the account by a Superroute administrator. Tiles are billed per thousand (tile_1k). Issuing a map token is not billed.

A map reaches the service with a map key, a map token, or both:

Map key Map token
Format smk_… sm1.…
Created Once, in the client portal By your server, per session
Placed in The page or app Fetched by the page or app at run time
Limited by Its allowlist Its expiry (60 seconds to 24 hours)

When both are given, both are sent and the token is checked first.

2. Map keys

Map keys are created on the Map keys page of the Open Platform in the client portal. Each key lists the websites and apps that may use it:

Allowlist Entry
Websites One host per line: maps.example.com or *.example.com
Android apps The package name, then the SHA-256 fingerprint of each signing certificate: com.example.app AB:CD:…
Apple apps (iOS, macOS) The bundle id: com.example.app
Windows apps The package family name of a packaged app, else its product name

The SDKs read the app's identity from the operating system and send it with the key. An app cannot set it itself.

For Android, list every certificate the app is signed with: debug, release, and the store's app signing key when the store re-signs the app.

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android | grep SHA256
apksigner verify --print-certs app-release.apk | grep SHA-256

A key is refused with 401 when it does not exist or is disabled, and with 403 when the site or app is not on its allowlist.

3. Map tokens

A map token is issued by your server with the Open Platform API token, and handed to the page or app. The API token is never placed in a page or app.

curl -sS https://open.superroute.ca/v1/map/token \
  -H "Authorization: Bearer $OPEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttl_seconds":3600}'
{
  "capability": "map.tiles",
  "billing": { "unit": "tile_1k", "quantity": 0, "counted": true },
  "data": {
    "token": "sm1.…",
    "expires_at": "2026-10-27T15:00:00Z",
    "base_url": "https://tile.srsr.ca",
    "subject": "open:1234"
  }
}

ttl_seconds is optional (default 3600, from 60 to 86400). The same token is issued by the GraphQL query openMapToken and the MCP tool open_map_token.

Every SDK accepts either token (a fixed token) or getToken (a function that returns one). With getToken, the SDK requests a new token two minutes before the current one expires; a failed request is retried after 30 seconds.

4. SDKs

Version 1.5.12. Swift packages, the Android Maven repository and example apps: github.com/Goodneo-Solutions/smap-sdk-releases.

Platform Package Installation
Web (JavaScript) @superrouteca/smap npm install @superrouteca/smap
React @superrouteca/smap-react npm install @superrouteca/smap-react @superrouteca/smap
React Native @superrouteca/smap-react-native npm install @superrouteca/smap-react-native
Flutter smap_flutter flutter pub add smap_flutter
Android ca.superroute:smap:1.5.12 Maven: https://goodneo-solutions.github.io/smap-sdk-releases/maven
iOS Swift package, product SMap https://github.com/Goodneo-Solutions/smap-sdk-releases
macOS Swift package, product SMapMac https://github.com/Goodneo-Solutions/smap-sdk-releases
Windows (WPF) Superroute.SMap.Wpf dotnet add package Superroute.SMap.Wpf
Windows (Windows Forms) Superroute.SMap.WinForms dotnet add package Superroute.SMap.WinForms
Leaflet @superrouteca/smap-leaflet npm install @superrouteca/smap-leaflet leaflet
OpenLayers @superrouteca/smap-openlayers npm install @superrouteca/smap-openlayers ol
Mapbox GL JS @superrouteca/smap-mapbox npm install @superrouteca/smap-mapbox mapbox-gl
deck.gl @superrouteca/smap-deckgl npm install @superrouteca/smap-deckgl @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @deck.gl/mapbox
ArcGIS Maps SDK for JavaScript @superrouteca/smap-arcgis npm install @superrouteca/smap-arcgis @arcgis/core
CesiumJS @superrouteca/smap-cesium npm install @superrouteca/smap-cesium cesium

The last six are adapters for other map tools (section 12). Each one installs the web SDK @superrouteca/smap with it.

The web SDK is also served by the Superroute Open Platform, without a bundler:

<link rel="stylesheet" href="https://open.superroute.ca/sdk/smap/v1/smap.css">
<script src="https://open.superroute.ca/sdk/smap/v1/smap.js"></script>

The files are listed under Downloads.

Each package can also be downloaded as a file (https://open.superroute.ca/sdk/smap/<file>) and installed without a package registry:

# Web, React, React Native
npm install ./smap-js-1.5.12.tgz
npm install ./smap-react-1.5.12.tgz ./smap-js-1.5.12.tgz
npm install ./smap-react-native-1.5.12.tgz

# Tool adapters: smap-leaflet, smap-openlayers, smap-mapbox, smap-deckgl, smap-arcgis, smap-cesium
npm install ./smap-leaflet-1.5.12.tgz ./smap-js-1.5.12.tgz

# Flutter (pubspec.yaml: smap_flutter: { path: smap_flutter })
mkdir smap_flutter && tar -xzf smap-flutter-1.5.12.tar.gz -C smap_flutter

# Android (settings.gradle.kts: repositories { maven { url = uri("smap-android-1.5.12") } }, same dependency)
unzip smap-android-1.5.12.zip

# iOS, macOS (Xcode: File > Add Package Dependencies > Add Local…, folder smap-apple-1.5.12)
unzip smap-apple-1.5.12.zip

# Windows (WPF, Windows Forms)
unzip smap-windows-1.5.12.zip
dotnet nuget add source "$PWD/smap-windows-1.5.12" --name smap-local
dotnet add package Superroute.SMap.Wpf

5. Examples

Each example shows a map of Toronto with one clustered point layer. Styles are light, white, grayscale, dark and black.

Web

<link rel="stylesheet" href="https://open.superroute.ca/sdk/smap/v1/smap.css">
<script src="https://open.superroute.ca/sdk/smap/v1/smap.js"></script>
<div id="map" style="height:480px"></div>
<script>
  const map = new sMap.Map({
    container: 'map',
    apiKey: 'smk_…',
    style: 'light',
    language: 'en',
    center: [-79.38, 43.65],
    zoom: 11,
  });
  map.ready.then(() => {
    map.addPoints('stops', [{ lng: -79.38, lat: 43.65, name: 'Depot' }], { cluster: true, label: 'name' });
    map.addLines('route', routeGeoJson, { color: '#16a34a', width: 5 });
    map.fitLayer('stops');
  });
</script>

With a bundler:

import { Map } from '@superrouteca/smap';
import '@superrouteca/smap/smap.css';

const map = new Map({
  container: 'map',
  getToken: async () => (await fetch('/map-token')).text(),
  style: 'dark',
  center: [-79.38, 43.65],
  zoom: 11,
});

In a server-rendered page, the map is created in the browser only (in an effect or an event handler).

React

import { SMap, Points, Lines } from '@superrouteca/smap-react';
import '@superrouteca/smap/smap.css';

export function Deliveries({ stops, route }) {
  return (
    <div style={{ height: 480 }}>
      <SMap apiKey="smk_…" style="light" language="en" center={[-79.38, 43.65]} zoom={11}
        onReady={(map) => map.fitLayer('stops')}>
        <Points id="stops" data={stops} options={{ cluster: true, label: 'name' }}
          onTap={(properties) => console.log(properties.name)} />
        <Lines id="route" data={route} options={{ color: '#16a34a', width: 5 }} />
      </SMap>
    </div>
  );
}

React Native

import { SMap, Points } from '@superrouteca/smap-react-native';

export function Deliveries({ stops }) {
  return (
    <SMap apiKey="smk_…" style="light" center={[-79.38, 43.65]} zoom={11} containerStyle={{ flex: 1 }}>
      <Points id="stops" data={stops} options={{ cluster: true, label: 'name' }} />
    </SMap>
  );
}

On iOS, add the SDK's post_install line to ios/Podfile as described in the package README, then run pod install.

Flutter

dependencies:
  smap_flutter: ^1.5.12
import 'package:smap_flutter/smap_flutter.dart';

SMap(
  options: const SMapOptions(
    apiKey: 'smk_…',
    style: SMapStyle.light,
    center: LatLng(43.6532, -79.3832),
    zoom: 11,
  ),
  onReady: (map) async {
    await map.addPoints(
      'stops',
      [const SMapPoint(43.65, -79.38, properties: {'name': 'Depot'})],
      const SMapPointOptions(cluster: true, label: 'name'),
    );
  },
)

Android

// settings.gradle.kts: repositories { maven { url = uri("https://goodneo-solutions.github.io/smap-sdk-releases/maven") } }
// app/build.gradle.kts:
dependencies {
    implementation("ca.superroute:smap:1.5.12")
}
map = SMap(this, SMapOptions(
    apiKey = "smk_…",
    style = SMapFlavor.LIGHT,
    center = LatLng(43.6532, -79.3832),
    zoom = 11.0,
))
map.onCreate(savedInstanceState)
setContentView(map)
map.addPoints("stops", listOf(SMapPoint(43.65, -79.38, mapOf("name" to "Depot"))),
    SMapPointOptions(cluster = SMapCluster(), label = "name"))

The activity forwards its lifecycle to the map (onStart, onResume, onPause, onStop, onDestroy, onLowMemory, onSaveInstanceState).

iOS

// Package.swift
.package(url: "https://github.com/Goodneo-Solutions/smap-sdk-releases", from: "1.5.12")
// target dependency
.product(name: "SMap", package: "smap-sdk-releases")
import SMap

let map = SMapView(frame: view.bounds, options: SMapOptions(
    style: .light,
    center: CLLocationCoordinate2D(latitude: 43.6532, longitude: -79.3832),
    zoom: 11,
    apiKey: "smk_…"
))
view.addSubview(map)

Task {
    await map.ready()
    try map.addPoints("stops", [SMapPoint(lat: 43.65, lng: -79.38, properties: ["name": "Depot"])],
                      options: SMapPointOptions(cluster: SMapCluster(), label: "name"))
}

SwiftUI apps use SMapSwiftUIView.

macOS

The same Swift package, product SMapMac. Requires macOS 12; a sandboxed app needs the Outgoing Connections (Client) capability.

import SMapMac

let map = SMapMacView(frame: window.contentView!.bounds, options: SMapOptions(apiKey: "smk_…"))
window.contentView!.addSubview(map)

SwiftUI apps use SMapMacSwiftUIView.

Windows

Requires .NET 8 and the WebView2 Runtime.

<Window xmlns:smap="clr-namespace:Superroute.SMap.Wpf;assembly=Superroute.SMap.Wpf">
    <smap:SMapView x:Name="Map" />
</Window>
using Superroute.SMap;

Map.Options = new SMapOptions
{
    ApiKey = "smk_…",
    Style = SMapStyle.Light,
    Center = new SMapLatLng(43.6532, -79.3832),
    Zoom = 11,
};
Map.AddPoints("stops", new[] { new SMapPoint(43.65, -79.38) },
    new SMapPointOptions { Cluster = new SMapCluster() });

Windows Forms apps use Superroute.SMap.WinForms.SMapView.

6. Style layers and slots

Version 1.1.0 adds the sources and layers of the Style Specification v8. They are passed to the map unchanged, so a layer copied from Mapbox Studio, Maputnik or a Mapbox GL JS page works as it is. They are kept when the scheme or the language changes.

map.addSource('parcels', { type: 'vector', tiles: ['https://tiles.example/{z}/{x}/{y}.pbf'], attribution: '© Example' });
map.addStyleLayer({ id: 'parcel-lines', type: 'line', source: 'parcels', 'source-layer': 'parcels',
  paint: { 'line-color': '#64748b' } }, { slot: 'roads' });
map.setPaint('parcel-lines', 'line-width', 2);
map.setFilter('parcel-lines', ['==', ['get', 'zoning'], 'R1']);
map.removeSource('parcels');         // with the layers that read it
Call Does
addSource(id, source) / removeSource(id) Adds or removes a source
addStyleLayer(layer, { slot, before }) Adds a style layer
setPaint, setLayout, setFilter Change a layer, the basemap's own layers included
getStyleLayer(id) Gives a layer as a Style Specification object
removeLayer(id) Removes a layer of any kind

A slot places a layer relative to the basemap. addPoints, addLines, addPolygons, addData and every external layer take slot too.

Slot Where
bottom Over land and water, under everything else
roads Under the roads
labels Under the labels
top On top

before: '<layer id>' places a layer under a named layer instead. A layer without a slot or before is drawn as in 1.0: on top, in the order added. Maps built with 1.0 look the same. zIndex orders layers inside one slot.

map.addPolygons('zones', zones, { fill: '#f97316', opacity: 0.4, slot: 'labels' });   // street names stay readable

Every SDK has the same calls (Android, iOS, Flutter, React, React Native, macOS and Windows in their own language).

7. Basemap options and custom styles

setBasemap() shows, hides and recolours the basemap by class:

map.setBasemap({
  water: { color: '#9cc0e6' },
  roads: { color: '#ffffff', width: 1.2 },     // every roads.* class
  'roads.highway': { color: '#f6c26b' },        // a class's own values take precedence over roads
  pois: { visible: false },
  labels: { halo: '#ffffff' },
});
map.basemap();          // the overrides in force
map.setBasemap({});     // back to the scheme

Classes: land, landcover, parks, landuse, airports, water, roads (roads.highway, roads.major, roads.minor, roads.paths), rail, buildings, boundaries, labels (labels.places, labels.roads, labels.water, labels.addresses) and pois.

Field Meaning
visible Shown or hidden
color Fill, line or background colour; the text colour for labels
casing The outline colour of roads
halo The halo colour of labels
opacity 0 to 1
width A line width factor, 0.25 to 4, of the scheme's own width

Each call replaces the overrides set before. They are kept when the scheme or the language changes. An unknown class or field is refused with an error that names it.

// Android
map.setBasemap(mapOf("water" to SMapBasemapOverride(color = "#9cc0e6"), "pois" to SMapBasemapOverride(visible = false)))
// iOS, macOS
try map.setBasemap(["water": ["color": "#9cc0e6"], "pois": ["visible": false]])
// Flutter
await map.setBasemap({'water': {'color': '#9cc0e6'}, 'pois': {'visible': false}});

Saved custom styles

A custom style is one of the five schemes with class overrides, saved under an id (sms_…). Custom styles are kept on the Map styles page of the Open Platform in the client portal (/open-api/map-styles), with a live preview. Each save is kept as a new revision; the id never changes. Styles are served only while map.tiles is enabled for the account.

Every SDK takes the id:

const map = new sMap.Map({ container: 'map', apiKey: 'smk_…', style: 'custom:sms_…' });
map.setStyle('custom:sms_…');

A custom style is served in every label language, with the same map key or token as the five schemes. Other map tools read it by its style URL (Copy style URL on the Map styles page): https://tile.srsr.ca/v1/styles/custom/sms_…?lang=en.

Google Maps styles

The Map styles page imports a Google Maps style JSON (featureType, elementType, stylers, as shared by Snazzy Maps and similar sites). Each entry becomes a class override. Entries without a counterpart are listed with the reason and are not applied.

8. Data formats

simplestyle

GeoJSON properties of the simplestyle specification (marker-color, marker-size, stroke, stroke-width, stroke-opacity, fill, fill-opacity, title, description) are drawn when a layer gives no colour of its own. Files from geojson.io, GitHub or Mapbox look as they do there.

Reading files on the device

addData() reads a file and draws it: points as the layer <id>, lines as <id>:lines, areas as <id>:areas.

await map.addData('trip', file);                              // a File from <input type="file">
await map.addData('stores', csvText, { format: 'csv', lat: 'Latitude', lng: 'Longitude' });
await map.addData('route', '_p~iF~ps|U_ulLnnqC', { format: 'polyline', width: 5 });
const collection = sMap.formats.read(kmlText);                // a FeatureCollection, without a map
Format Reads
kml, kmz Google Earth and My Maps files: placemarks, extended data, styles as simplestyle colours, tracks
gpx Waypoints, routes and tracks (with elevation)
polyline, polyline6 Encoded polylines, precision 5 and 6
wkt, wkb Well-Known Text and Binary (also EWKB and hexadecimal text)
csv Latitude and longitude columns, named or found from common headings
topojson, geojson TopoJSON topologies and GeoJSON (RFC 7946)

The format is taken from the file name or the content when not given; polylines are always named. Every SDK reads the same file into the same features (Android, iOS and Flutter read files natively).

Converting on the map service

POST /v1/convert converts a file on the map service, with the same formats and the same answer. It is used for large files (up to 20 MB) and by callers that cannot read a format themselves. Nothing is stored.

curl -sS "https://tile.srsr.ca/v1/convert?from=gpx" -H "Authorization: Bearer sm1.…" \
  --data-binary @trip.gpx -o trip.geojson

In the SDKs: sMap.convert(file, { format }) or map.convert(…).

Export

map.toGeoJSON(id) and map.toKML(id) give a layer's features as GeoJSON or as a KML document.

9. External layers

Layers from other services are added as sources. The attribution of each source is shown beside the map's credit.

map.addRasterLayer('osm', { tiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap', opacity: 0.6 });
map.addRasterLayer('imagery', { url: 'https://tiles.example/imagery.json' });                  // TileJSON
map.addWmsLayer('flood', { url: 'https://wms.example/ows', layers: 'flood_zones', slot: 'labels' });
await map.addWmtsLayer('ortho', { url: 'https://wmts.example/WMTSCapabilities.xml', layer: 'ortho' });
map.addVectorTiles('parcels', { pmtiles: 'https://data.example/parcels.pmtiles',
  layers: [{ id: 'parcel-fill', type: 'fill', 'source-layer': 'parcels', paint: { 'fill-color': '#fde68a' } }] });
await map.addArcGisLayer('esri', { url: 'https://services.example/arcgis/rest/services/World/MapServer' });
Call Reads
addRasterLayer Raster XYZ templates (tiles) or a TileJSON address (url)
addWmsLayer OGC WMS 1.3.0 or 1.1.1, one GetMap request per tile in Web Mercator
addWmtsLayer OGC WMTS from its GetCapabilities address (KVP or REST), or a REST template
addVectorTiles Mapbox Vector Tiles from templates, a TileJSON address or a PMTiles archive, drawn by your style layers
addArcGisLayer ArcGIS REST MapServer and ImageServer (raster tiles) and VectorTileServer

Every external layer takes slot, before and zIndex. Google and Bing tiles are not offered: their licences do not allow it, and the SDK refuses their addresses.

10. A map as JSON

map.toJSON() writes the map's basemap (scheme, language, overrides), sources and layers as one JSON document, and map.loadLayers(json) loads it. Every SDK reads the same document, so a map built on the web opens the same in an app.

const saved = map.toJSON();
// { "type": "smap.layers", "version": 1, "basemap": { … }, "sources": { … }, "layers": [ … ] }
other.loadLayers(saved);

Popups are written as templates (popup: '<b>{name}</b>'). Popup functions, click handlers and deck.gl layers are code and are not written. A reader that cannot draw a layer skips that layer and loads the rest. The shape and its JSON Schema are in shared/LAYER-JSON.md of the SDK repository.

11. Raster tiles, TileJSON and WMTS

For tools that do not draw vector tiles, the map service also serves the map as images, drawn from the same styles:

Address Gives
/v1/raster/<style>/{z}/{x}/{y}.png A 256 px tile ({y}@2x.png: 512 px)
/v1/raster/<style>.json TileJSON 3 of the raster tiles
/v1/raster/wmts?SERVICE=WMTS&REQUEST=GetCapabilities OGC WMTS 1.0.0 capabilities (each scheme a layer, tile matrix set GoogleMapsCompatible)

<style> is a scheme or custom:<id>. lang sets the label language. Raster tiles have the same access rules as vector tiles and are billed in the same way under map.tiles.

sMap.rasterUrl('grayscale', { lang: 'de', retina: true });
// https://tile.srsr.ca/v1/raster/grayscale/{z}/{x}/{y}@2x.png?lang=de

A desktop program sends no website address, so a map key alone is not accepted there. Put a map token in the address (&token=sm1.…, valid up to 24 hours, section 3), or give the program's Referer field a website that is on the key's allowlist.

Office software and other programs that read XYZ, TileJSON or WMTS use the same addresses.

12. Other map tools

Each adapter is an npm package that shows sMap in the tool with what the sMap SDK offers there: the five schemes and custom styles, any label language, basemap options, map key or token with renewal, the nearest map node, and the credits. It also works the other way: the tool's layers on an sMap map. Each package README has the full list of calls.

Leaflet

npm install @superrouteca/smap-leaflet leaflet
import L from 'leaflet';
import { smapLayer, smapRasterLayer } from '@superrouteca/smap-leaflet';
import 'leaflet/dist/leaflet.css';
import '@superrouteca/smap/smap.css';

const map = L.map('map').setView([43.65, -79.38], 12);
const basemap = smapLayer({ apiKey: 'smk_…', style: 'light', language: 'en' }).addTo(map);
basemap.addPoints('stops', stops, { marker: { text: { property: 'stop_number' } } });
basemap.setBasemap({ water: { color: '#9cc0e6' } });
// Without WebGL: smapRasterLayer({ apiKey: 'smk_…', style: 'grayscale' }).addTo(map);

Leaflet's zoom levels are one above the sMap map's own. Leaflet-style calls (L.marker, L.polyline, L.geoJSON, L.tileLayer, bindPopup, …) also run on an sMap map through @superrouteca/smap-leaflet/compat.

OpenLayers

npm install @superrouteca/smap-openlayers ol
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import { fromLonLat } from 'ol/proj.js';
import { smapLayer } from '@superrouteca/smap-openlayers';

const basemap = smapLayer({ apiKey: 'smk_…', style: 'light', language: 'en' });
const map = new Map({ target: 'map', layers: [basemap], view: new View({ center: fromLonLat([-79.38, 43.65]), zoom: 12 }) });
basemap.addLines('routes', routes, { color: { property: 'color' }, width: 5 });

The view must be in Web Mercator (EPSG:3857). smapRasterLayer() gives raster tiles. OpenLayers sources (XYZ, TileWMS, WMTS, VectorTile, Vector) are added to an sMap map with addOpenLayersLayer() from @superrouteca/smap-openlayers/on-smap.

Mapbox GL JS

npm install @superrouteca/smap-mapbox mapbox-gl
import mapboxgl from 'mapbox-gl';
import { SMapMapbox } from '@superrouteca/smap-mapbox';

mapboxgl.accessToken = 'pk.…';           // required by Mapbox's own terms
const smap = new SMapMapbox(mapboxgl, {
  container: 'map', apiKey: 'smk_…', style: 'light', language: 'en',
  center: [-79.38, 43.65], zoom: 12, pitch: 50, buildings3D: true,
});
await smap.ready;
smap.addStyleLayer({ id: 'zones-fill', type: 'fill', source: 'zones', paint: { 'fill-color': '#f97316' } }, { slot: 'labels' });

An existing Mapbox GL JS map changes its basemap to sMap with switchBasemap() and keeps its own layers. Mapbox styles and layers are loaded on an sMap map with loadMapboxStyle() from @superrouteca/smap-mapbox/on-smap.

deck.gl and kepler.gl

npm install @superrouteca/smap-deckgl @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @deck.gl/mapbox
import { createSMapDeck } from '@superrouteca/smap-deckgl';
import { HexagonLayer } from '@deck.gl/aggregation-layers';

const { map, setLayers } = await createSMapDeck({
  container: 'map', apiKey: 'smk_…', style: 'dark', center: [-79.38, 43.65], zoom: 11, pitch: 45,
  layers: [new HexagonLayer({ id: 'orders', data, getPosition: (d) => d.position, extruded: true, radius: 200 })],
});

deck.gl layers are drawn among the map's own layers. For an app with its own Deck, createVectorBasemapLayer() and createRasterBasemapLayer() give sMap as a deck.gl layer. On any sMap map, map.addDeckLayer(layer, { slot }) adds a deck.gl layer.

kepler.gl reads sMap's styles as map styles:

import KeplerGl from '@kepler.gl/components';
import { keplerMapStyles, keplerTransformRequest } from '@superrouteca/smap-deckgl';

const access = { apiKey: 'smk_…' };
const mapStyles = await keplerMapStyles({ ...access, language: 'en', styles: ['light', 'dark', 'custom:sms_…'] });

<KeplerGl id="map" mapStyles={mapStyles} mapStylesReplaceDefault
  transformRequest={keplerTransformRequest(access)} width={width} height={height} />;

The styles are named smap-light, smap-dark and smap-custom-sms_….

ArcGIS Maps SDK for JavaScript

npm install @superrouteca/smap-arcgis @arcgis/core
import Map from '@arcgis/core/Map.js';
import MapView from '@arcgis/core/views/MapView.js';
import { createSMapBasemap, addAttribution } from '@superrouteca/smap-arcgis';

const basemap = await createSMapBasemap({ apiKey: 'smk_…', style: 'light', language: 'fr' });
const view = new MapView({ container: 'map', map: new Map({ basemap }), center: [2.35, 48.86], zoom: 12 });
addAttribution(view);

The vector basemap converts sMap's style into what ArcGIS draws; its limits are in Interoperability guide 5.

raster: true gives raster tiles (also draped in a SceneView). An existing ArcGIS app keeps its layers: map.basemap = await createSMapBasemap({ … }). ArcGIS REST services are added to an sMap map with addArcGisService() from @superrouteca/smap-arcgis/services.

CesiumJS

npm install @superrouteca/smap-cesium cesium
import { Viewer } from 'cesium';
import { createBaseLayer } from '@superrouteca/smap-cesium';

const viewer = new Viewer('globe', {
  baseLayer: createBaseLayer({ apiKey: 'smk_…', style: 'light', language: 'ja' }),
  baseLayerPicker: false,
  geocoder: false,
});

The globe's imagery is sMap's raster tiles (XYZ, or WMTS with wmts: true); no Cesium ion account is needed for it. addSMapData() draws sMap's points, lines and areas in the viewer; addCesiumData() shows CZML, KML and GeoJSON on an sMap map.

QGIS

QGIS reads sMap's raster tiles as an XYZ connection or as WMTS.

XYZ. In the Browser panel, right-click XYZ Tiles > New Connection:

Field Value
Name sMap light
URL https://tile.srsr.ca/v1/raster/light/{z}/{x}/{y}.png?lang=en&token=sm1.…
Tile Resolution Standard (256x256 / 96 DPI); for {y}@2x.png, High (512x512 / 192 DPI)

WMTS. Layer > Add Layer > Add WMS/WMTS Layer, New, with the URL https://tile.srsr.ca/v1/raster/wmts?SERVICE=WMTS&REQUEST=GetCapabilities&token=sm1.…. Every scheme is listed as a layer.

The token is valid for its own lifetime (up to 24 hours). When it expires, a new token is issued and the URL is changed in the connection.

Google Maps JavaScript API

deck.gl's Google Maps overlay draws sMap's points, lines and areas on a Google map. The basemap stays Google's, under Google's terms; sMap's basemap is not placed on Google Maps.

npm install @superrouteca/smap-deckgl @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @deck.gl/google-maps
import { GoogleMapsOverlay } from '@deck.gl/google-maps';
import { pointsLayers, linesLayers, popupTooltip } from '@superrouteca/smap-deckgl';

const map = new google.maps.Map(document.getElementById('map'), { center: { lat: 43.65, lng: -79.38 }, zoom: 12, mapId: '…' });
const overlay = new GoogleMapsOverlay({
  layers: [
    ...linesLayers('route', route, { color: '#16a34a', width: 5, casing: true }),
    ...pointsLayers('stops', stops, { marker: { text: { property: 'seq' } } }),
  ],
  getTooltip: popupTooltip(),
});
overlay.setMap(map);

13. Offline regions

An app on Android or iOS can keep an area of the map on the device and show it without a network. A region is a bounding box, a zoom range, a colour scheme or saved custom style and a label language, downloaded as one file of at most 500 MB. The web, macOS and Windows SDKs have no offline regions.

  • Who may download: a map token, or a map key whose Allow offline regions switch is on (on the Map keys page; off by default).
  • Billing: the region's tiles are billed as map.tiles when it is downloaded, once a day per region and key or token, however often the download resumes. Asking for the size first is free. Showing a region offline costs nothing.
  • Validity: 30 days from the download; then the region is not shown until it is refreshed. While online, the SDK downloads again the regions in their last 3 days (offlineAutoRefresh, on by default).
  • Resuming: calling the download again for the same region continues where it stopped.
  • Fallback: when no map node answers, the map shows a downloaded region containing its centre (offlineFallback, on by default). Your layers stay on the map.
  • Managing: list(), get(id), refresh(id), remove(id) and usage() list, refresh and remove the downloaded regions and give the space they use. After the 30 days a region's map data is removed and its definition is kept, so refresh(id) downloads it again.
  • iOS: downloads need iOS 15. Regions are excluded from device backups.
Platform Downloads On the map
Android SMapOffline(context, options), map.offline SMapOptions(offlineRegion, offlineFallback, offlineAutoRefresh), useOfflineRegion, useOnline
iOS SMapOffline(options:), map.offline SMapOfflineSettings(region:fallback:autoRefresh:), useOfflineRegion, useOnline
Flutter SMapOffline(options), controller.offline SMapOptions(offlineRegion, offlineFallback, offlineAutoRefresh), useOfflineRegion, useOnline
React Native offlineRegions(options) <SMap offlineRegion offlineFallback offlineAutoRefresh>, useOfflineRegion, useOnline, offlineRegionInUse
val region = SMapOfflineRegion("downtown", west = -79.42, south = 43.63, east = -79.35, north = 43.68, maxZoom = 15)
val estimate = map.offline.estimate(region)   // tiles, bytes, fits, allowed
if (estimate.allowed && estimate.fits) {
    map.offline.download(region) { done, total -> }
    map.useOfflineRegion("downtown")          // map.useOnline()
}
const offline = offlineRegions({ apiKey: 'smk_…' });
await offline.download(region, (done, total) => setProgress(done / total));   // refresh(id, onProgress) too

14. Map tools

The controls and panels on a map — zoom, compass, scale bar, locate, and your own panels such as a legend or an orders list — can be shown and hidden one by one or all at once. Every SDK has the same calls from 1.1.5:

const map = new sMap.Map({ container: 'map', apiKey: 'smk_…', tools: { hidden: saved } });
map.addTool('zoom', 'zoom');
map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' });
map.addTool('stops', { layer: ['stops', 'stop-labels'] }, { label: 'Stops' });   // data layers, from 1.1.6
map.onToolsChange((e) => save(e.hidden));
  • addTool(id, kind or view, options) registers a tool: a control the map makes (zoom, compass, scale, locate; on the web also navigation, geolocate, fullscreen) or a view of your own, in a corner or where you placed it (position: 'none').
  • setToolVisible(id, visible), setToolsVisible(visible) and tools() switch and read the tools; onToolsChange reports every change.
  • A tools control (one button) lists every tool with Show all and Hide all; tools.control: false leaves it out.
  • A hidden tool keeps its state. The map data credit and the sMap badge are never tools.
  • The map keeps no state: give the starting state in tools.hidden and keep the hidden ids where you like.
  • On each platform: React <Tool>, React Native <Tool> children of <SMap>, Flutter SMap(tools: [SMapTool …]), Android and iOS addTool, macOS and Windows addTool (your own view stays where you placed it).

14.1 Where the tools control sits

From 1.1.6 the tools control is by default the last button of the map's toolbar: the corner that holds the map's navigation control (the zoom buttons or the compass), top left when the map has none. It stays last when other controls join that corner later. In a bottom corner it is the lowest tool, just above the map data credit and the sMap badge, which keep the map's edge.

  • On the web the toolbar is the corner of the navigation control, whether it was added as a tool (navigation, zoom, compass) or by the page itself. On Android, iOS, Flutter and React Native it is the corner of the first zoom tool, else of the first compass tool, else the top left, and the control follows that tool when it is added, moved or removed. macOS and Windows follow the web SDK.
  • A corner given by the app places the control there, as in 1.1.5: tools: { control: 'top-right' } on the web. None (false, or null on the native SDKs) leaves it out.
  • The value for the default is 'toolbar' (Web, React, React Native), SMapToolPosition.TOOLBAR (Kotlin), .toolbar (Swift), SMapToolPosition.toolbar (Dart) and SMapToolPosition.Toolbar (C#). It is valid for the tools control only; addTool refuses it. An exhaustive switch or when over the position values needs the new case.
  • Nothing on the map covers the control or its panel: the control is drawn above the other tools of its corner, and while its panel is open that corner is drawn above the map's other corners. The panel opens towards the middle of the map.
const map = new sMap.Map({ container: 'map', apiKey: 'smk_…', tools: { control: 'bottom-right' } });   // a corner of your choice

14.2 Data layers as tools

From 1.1.6 an app can register its own data layers as a tool, so that the tools control shows and hides them: the points, lines and polygons of the SDK, addData layers, style layers, external layers, or the basemap's layers, by layer id.

Platform Call
Web map.addTool('stops', { layer: ['stops', 'stop-labels'] }, { label: 'Stops' })
React, React Native <Tool id="stops" label="Stops" layer={['stops', 'stop-labels']} />
Android (Kotlin) map.addTool("stops", listOf("stops", "stop-labels"), "Stops"), or one layer: addTool(id, layer, label)
iOS, macOS (Swift) try map.addTool("stops", layers: ["stops", "stop-labels"], label: "Stops"), or layer:
Flutter SMapTool.layers('stops', ['stops', 'stop-labels'], label: 'Stops') in the map's tools
Windows (C#) map.AddTool("stops", new[] { "stops", "stop-labels" }, "Stops")
  • Hiding the tool hides every layer it names, and showing it shows them. It has the same label, setToolVisible, setToolsVisible, tools(), change events and saved hidden ids as any other tool.
  • One tool may name several layers, such as a layer and its labels; a layer named twice counts once. An addData group is named by its layers' ids, not by the group's id.
  • A layer that is not on the map, removed or not added yet, is left alone and is no error. A layer added later under an id of a hidden tool starts hidden; one added under a shown tool keeps its own visibility.
  • Removing a hidden data-layer tool shows its layers again.
  • A data-layer tool has no view and no corner; a corner given with it is refused.

The plan editor adds its panel as the tool plan-editor.

15. Markers, drawing and more

From 1.2.0 every SDK has the same calls for what an operations map needs:

map.addCustomMarkers('vans', vans, { idProperty: 'id', anchor: 'bottom', draggable: true,
  render: (p) => `<div class="van">${p.name}</div>`, onDragEnd: (f, e) => save(f.properties.id, e.lngLat) });
map.addPoints('stops', stops, { tooltip: '{name}', cluster: { ring: 'color' } });
map.addHeatmap('density', stops, { radius: 18 });
const draw = map.drawTool({ onChange: (e) => save(e.features) });
const picked = await map.selectArea({ mode: 'lasso', layers: ['stops'] });
  • Custom markers (addCustomMarkers): your own content at each point — HTML on the web, macOS and Windows, a view on Android and iOS, a widget in Flutter, a component in React and React Native.
  • Dragging points and markers (draggable, onDragStart / onDrag / onDragEnd): the map does not pan meanwhile and the layer keeps the new position. On a phone a drag starts after a long press.
  • Tooltips (tooltip) on points, lines and areas, labels along lines (label), and clusters drawn as a ring of their points' colours (cluster.ring) or as your own content.
  • Heat maps (addHeatmap), glTF 3D models at points (addModels), base imagery (setBaseImagery), relief (setTerrain), the layer switcher (the layers tool), tiles you paint (addCanvasTiles) and maps that follow each other (syncWith).
  • Google's base maps (setGoogleBase) are read straight from Google with a Map Tiles API session your server makes with your own Google key. Other imagery never reads Google or Bing tiles.
  • Drawing and editing (drawTool: polygons, rectangles, circles, lines, points and holes, with undo and redo) and area selection (selectArea: a box or a free shape).
  • Geometry helpers (sMap.geo, SMapGeo): distances, lengths, bounds, points in areas and the nearest point on a line.
  • Without WebGL 2 the web map still shows the map and your layers; what it cannot draw (terrain, 3D models) is reported once as unsupported.

16. Map data attribution

The map data is © OpenStreetMap contributors. The credit is shown on every map and must remain visible. In other map tools, the adapters add it to the tool's own attribution.