Open Application Platform Docs

sMap interoperability cases

sMap

sMap interoperability cases

Each case below is one use case of the interoperability guide, worked through: the situation, the steps, complete code, what to check afterwards and the limits that apply. The code is the code of the playground example named under it, which runs on that page as written.

  • Install the adapter of your tool from npm (npm install @superrouteca/smap-leaflet leaflet, and so on) or download it from the downloads page. The examples import them as ES modules, as an app built with a bundler does.
  • On your own site, give each map or adapter a map key whose allowlist names the site (apiKey: 'smk_…') or a token from your server (getToken). The playground is an allowed origin, so its examples read the map without either (maps guide 2).
  • In the playground, addButton(), print(), twoMaps(), MAPBOX_TOKEN and ARCGIS_ASSETS are provided by the page. In your app, use your own buttons, output and values instead.

1. A Leaflet app moves its basemap to sMap

Interoperability guide 4.1

Situation. A dispatch page is built with Leaflet. Its markers, route lines and GeoJSON layers stay; only the tile layer under them is replaced by sMap.

Steps.

  1. Install @superrouteca/smap-leaflet beside leaflet, and load leaflet/dist/leaflet.css and @superrouteca/smap/smap.css.
  2. Replace the previous tile layer with smapLayer({ apiKey, style, language }).addTo(map).
  3. Add sMap's own pins and route lines through the layer (addPoints, addLines) where they are wanted; the Leaflet layers stay as they are.
  4. For browsers without WebGL, use smapRasterLayer(), or smapBasemap(), which chooses by itself.
  5. To run Leaflet code on an sMap map instead, create an sMap map and call createLeafletCompat(smap); the app's L.* calls then draw on it.

Code.

Leaflet: sMap as the base layer

import * as L from 'leaflet';
import { popupTemplate } from '@superrouteca/smap';
import { smapLayer } from '@superrouteca/smap-leaflet';

// A Leaflet map with sMap as its base layer. On your own site, add apiKey: 'smk_…' or getToken.
const map = L.map('map').setView([43.655, -79.387], 14);
const basemap = smapLayer({ style: 'light', language: 'en' }).addTo(map);

const stops = [
  { lng: -79.3832, lat: 43.6532, n: 1, name: 'City Hall', color: '#16a34a' },
  { lng: -79.3871, lat: 43.6426, n: 2, name: 'CN Tower', color: '#f97316' },
  { lng: -79.3957, lat: 43.6677, n: 3, name: "Queen's Park", color: '#2563eb' },
  { lng: -79.3806, lat: 43.6629, n: 4, name: 'Church St', color: '#e11d48' },
];

// sMap's own pins, route line and popups, drawn by the sMap map inside Leaflet.
basemap.addLines('route', { type: 'LineString', coordinates: stops.map((s) => [s.lng, s.lat]) },
  { color: '#2563eb', width: 5, casing: true, arrows: true });
basemap.addPoints('stops', stops, {
  marker: { fill: { property: 'color' }, border: '#1e293b', text: { property: 'n' } },
  popup: popupTemplate('<b>{n}. {name}</b>'),
});

// The app's own Leaflet layers stay as they are.
L.marker([43.6487, -79.3817]).addTo(map).bindPopup('A Leaflet marker over sMap');

['light', 'grayscale', 'dark'].forEach((style) => addButton(style, () => basemap.setStyle(style)));
['en', 'fr', 'zh-Hans'].forEach((tag) => addButton(tag, () => basemap.setLanguage(tag)));

Leaflet: Leaflet code on an sMap map

import { Map } from '@superrouteca/smap';
import { createLeafletCompat } from '@superrouteca/smap-leaflet/compat';

// An sMap map, and Leaflet-style calls on it: code written for Leaflet runs as it is.
const smap = new Map({ container: 'map', style: 'light', center: [-79.387, 43.655], zoom: 13 });
await smap.ready;
const L = createLeafletCompat(smap);
const map = L.map;

const stops = [
  { lat: 43.6532, lng: -79.3832, n: 1, name: 'City Hall', color: '#16a34a' },
  { lat: 43.6426, lng: -79.3871, n: 2, name: 'CN Tower', color: '#f97316' },
  { lat: 43.6677, lng: -79.3957, n: 3, name: "Queen's Park", color: '#2563eb' },
  { lat: 43.6629, lng: -79.3806, n: 4, name: 'Church St', color: '#e11d48' },
];

L.marker([43.6487, -79.3817]).addTo(map).bindPopup('<b>Depot</b>');
L.polyline(stops.map((s) => [s.lat, s.lng]), { color: '#16a34a', weight: 5, dashArray: '10, 6' }).addTo(map);
L.geoJSON({
  type: 'FeatureCollection',
  features: stops.map((s) => ({ type: 'Feature', properties: s, geometry: { type: 'Point', coordinates: [s.lng, s.lat] } })),
}, {
  pointToLayer: (f, latlng) => L.circleMarker(latlng, { radius: 9, color: '#ffffff', weight: 2, fillColor: f.properties.color, fillOpacity: 1 }),
  onEachFeature: (f, layer) => layer.bindPopup('<b>' + f.properties.n + '.</b> ' + f.properties.name),
}).addTo(map);
L.circle([43.6532, -79.3832], { radius: 300, color: '#f97316' }).addTo(map);

map.on('click', (e) => print('click at ' + e.latlng.lat.toFixed(5) + ', ' + e.latlng.lng.toFixed(5)));

What to check.

  • The Leaflet marker and the sMap pins are both on the map, and a click on a pin opens its pop-up.
  • The buttons change the colour scheme and the label language without removing any layer.
  • The OpenStreetMap credit and the sMap badge are in the bottom-right corner.

Limits. The basemap is an embedded second map: Leaflet is flat and shows no 3D buildings. L.divIcon is drawn as the default pin. (Interoperability guide 5)

Run in the playground: Leaflet: sMap as the base layer · Leaflet: Leaflet code on an sMap map · Leaflet: raster tiles

2. A Mapbox GL JS app moves to sMap's basemap

Interoperability guide 4.2

Situation. An app shows its own sources and layers on a Mapbox-hosted style. The basemap changes to sMap's; the app's layers stay.

Steps.

  1. Install @superrouteca/smap-mapbox beside mapbox-gl. Mapbox GL JS stays under Mapbox's terms and keeps its access token.
  2. Create the map with transformRequest: smapTransformRequest(access), where access = smapAccess({ apiKey }) or smapAccess({ getToken }).
  3. Call switchBasemap(mapboxgl, map, { access, style: 'light' }) once the map has loaded.
  4. For a new map, use new SMapMapbox(mapboxgl, { … }), which draws sMap's style with Mapbox GL JS and keeps sMap's pins on a layer above.
  5. To take a Mapbox style's own layers to an sMap map, use copyMapboxLayers(mapboxMap, smap) or loadMapboxStyle(smap, style).

The Mapbox examples run in the playground only with your own Mapbox access token, typed in the field above the code. It stays in the browser tab and is sent only to Mapbox.

Code.

Mapbox GL JS: switch an existing map to sMap

import mapboxgl from 'mapbox-gl';
import { switchBasemap, smapAccess, smapTransformRequest } from '@superrouteca/smap-mapbox';

mapboxgl.accessToken = MAPBOX_TOKEN;   // your Mapbox access token (the field above the editor)

// An existing Mapbox GL JS app: a Mapbox style and the app's own source and layer.
const access = smapAccess({});         // on your own site: { apiKey: 'smk_…' } or { getToken }
const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v12',
  center: [-79.387, 43.652],
  zoom: 13.5,
  transformRequest: smapTransformRequest(access),
});
await new Promise((resolve) => map.on('load', resolve));
map.addSource('zone', { type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [[
  [-79.395, 43.646], [-79.375, 43.646], [-79.375, 43.658], [-79.395, 43.658], [-79.395, 43.646],
]] } } });
map.addLayer({ id: 'zone-fill', type: 'fill', source: 'zone', paint: { 'fill-color': '#f97316', 'fill-opacity': 0.4 } });

// The Mapbox-hosted basemap goes; the app's source and layer stay, on top of sMap's basemap.
addButton('Switch to sMap', async () => {
  await switchBasemap(mapboxgl, map, { access, style: 'light', language: 'en' });
  print('zone-fill kept: ' + Boolean(map.getLayer('zone-fill')));
});

Mapbox GL JS: sMap's style

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

// Mapbox GL JS is used under Mapbox's terms, which ask for your own access token (the field above the editor).
mapboxgl.accessToken = MAPBOX_TOKEN;

// sMap's style, drawn by Mapbox GL JS itself, with 3D buildings and tilting.
const smap = new SMapMapbox(mapboxgl, {
  container: 'map',
  style: 'light',
  language: 'en',
  center: [-79.3832, 43.6487],
  zoom: 15,
  pitch: 55,
  buildings3D: true,
});
await smap.ready;

// sMap's own pins, on a layer above.
smap.addPoints('stops', [
  { lng: -79.3832, lat: 43.6532, n: 1, name: 'City Hall' },
  { lng: -79.3871, lat: 43.6426, n: 2, name: 'CN Tower' },
], { marker: { fill: '#2563eb', text: { property: 'n' } }, popup: (p) => '<b>' + p.name + '</b>' });

['en', 'fr', 'zh-Hans', 'ja'].forEach((tag) => addButton(tag, () => smap.setLanguage(tag)));
['light', 'dark'].forEach((style) => addButton(style, () => smap.setStyle(style)));

What to check.

  • After the switch, the app's layer (zone-fill) is still on the map, over sMap's basemap.
  • setLanguage() changes the labels and keeps the app's layers.
  • The report of copyMapboxLayers() lists the layers left out because they read Mapbox-hosted sources.

Limits. sMap's own layers are drawn on an overlay above the Mapbox labels; the map stays in Web Mercator; a token works only when the map was created with smapTransformRequest(); Mapbox sprite icons are not loaded. (Interoperability guide 5)

Run in the playground: Mapbox GL JS: switch an existing map to sMap · Mapbox GL JS: sMap's style · Mapbox GL JS: copy layers to sMap

3. deck.gl or kepler.gl analytics over sMap

Interoperability guide 4.3

Situation. An analyst draws a heat map, points and arcs of a day's deliveries over an sMap basemap, under the street names.

Steps.

  1. Install @superrouteca/smap-deckgl and the deck.gl packages it names (@deck.gl/core, @deck.gl/layers, @deck.gl/geo-layers, @deck.gl/mapbox, and @deck.gl/aggregation-layers for heat maps).
  2. On an sMap map, call deckLayers(map) and add each deck.gl layer with a slot (roads, labels, …).
  3. Update a layer by adding a new one with the same id; set([]) removes them all.
  4. For a deck.gl app with its own Deck, use createVectorBasemapLayer() (sMap's vector tiles drawn by deck.gl) or createRasterBasemapLayer().
  5. For kepler.gl, pass keplerMapStyles() as mapStyles and keplerTransformRequest() as transformRequest.

Code.

deck.gl: layers on an sMap map

import * as sMap from '@superrouteca/smap';
import { deckLayers } from '@superrouteca/smap-deckgl';
import { ScatterplotLayer, ArcLayer } from '@deck.gl/layers';
import { HeatmapLayer } from '@deck.gl/aggregation-layers';

const map = new sMap.Map({ container: 'map', style: 'dark', center: [-79.42, 43.70], zoom: 10, pitch: 40 });
await map.ready;

// About 10,000 Toronto deliveries of a stored plan (ROUTES_URL): each stop with the depot it leaves from.
const plan = await (await fetch(ROUTES_URL)).json();
const stops = [];
plan.routes.forEach((route) => {
  sMap.formats.decodePolyline(route.stops, plan.precision).forEach((position) => {
    stops.push({ position, depot: plan.depots[route.depot] });
  });
});

// deck.gl layers drawn among the map's own layers: the heat map under the roads, the rest under the labels.
const deck = deckLayers(map);
const layers = {
  heat: () => deck.add(new HeatmapLayer({ id: 'heat', data: stops, getPosition: (d) => d.position, radiusPixels: 35 }), { slot: 'roads' }),
  points: () => deck.add(new ScatterplotLayer({ id: 'points', data: stops, getPosition: (d) => d.position,
    getRadius: 30, radiusMinPixels: 1.5, getFillColor: [56, 189, 248] }), { slot: 'labels' }),
  arcs: () => deck.add(new ArcLayer({ id: 'arcs', data: stops.filter((d, i) => i % 40 === 0),
    getSourcePosition: (d) => d.depot, getTargetPosition: (d) => d.position,
    getSourceColor: [249, 115, 22], getTargetColor: [56, 189, 248], getWidth: 1.5 }), { slot: 'labels' }),
};
layers.heat();
layers.arcs();
addButton('Heat map', () => { deck.set([]); layers.heat(); });
addButton('Points', () => { deck.set([]); layers.points(); });
addButton('Arcs', () => { deck.set([]); layers.arcs(); });
addButton('All', () => { deck.set([]); layers.heat(); layers.points(); layers.arcs(); });
print(stops.length.toLocaleString() + ' deliveries, ' + plan.depots.length + ' depots');

deck.gl: sMap vector tiles drawn by deck.gl

import { Deck } from '@deck.gl/core';
import { MVTLoader } from '@loaders.gl/mvt';
import { createVectorBasemapLayer, addAttribution, pointsLayers, popupTooltip } from '@superrouteca/smap-deckgl';

// sMap's vector tiles drawn by deck.gl itself (an MVTLayer coloured by sMap's style), for a deck.gl app with its
// own Deck. On your own site, add apiKey: 'smk_…' or getToken.
const basemap = await createVectorBasemapLayer({ style: 'light', language: 'en', buildings3D: true });
const container = document.getElementById('map');
const [r, g, b, a] = basemap.smap.background;
container.style.background = `rgba(${r}, ${g}, ${b}, ${a / 255})`;

const stops = [
  { lng: -79.3832, lat: 43.6532, n: 1, name: 'City Hall', color: '#16a34a' },
  { lng: -79.3871, lat: 43.6426, n: 2, name: 'CN Tower', color: '#f97316' },
  { lng: -79.3957, lat: 43.6677, n: 3, name: "Queen's Park", color: '#2563eb' },
];

new Deck({
  parent: container,
  initialViewState: { longitude: -79.388, latitude: 43.652, zoom: 13.6, pitch: 40 },
  controller: true,
  // MVTLoader with worker: false reads the tiles on the page itself, with no worker script to download.
  layers: [basemap.clone({ loaders: [MVTLoader], loadOptions: { ...basemap.props.loadOptions, worker: false } }),
    ...pointsLayers('stops', stops, { marker: { border: { property: 'color' }, text: { property: 'n' } }, label: 'name', popup: (p) => '<b>' + p.name + '</b>' })],
  getTooltip: popupTooltip(),
});
addAttribution(container);   // the sMap badge and the OpenStreetMap credit

kepler.gl: sMap styles as map styles

import * as sMap from '@superrouteca/smap';
import { keplerMapStyles, keplerTransformRequest } from '@superrouteca/smap-deckgl';

// kepler.gl reads Style Specification styles from a URL, so sMap's styles are kepler.gl map styles.
const access = {};   // on your own site: { apiKey: 'smk_…' } (it goes in the style URL) or { getToken }
const mapStyles = await keplerMapStyles({ ...access, language: 'en', styles: ['light', 'dark'] });
const transformRequest = keplerTransformRequest(access);

// In a kepler.gl app (React), these are the props of <KeplerGl>:
//   <KeplerGl id="map" mapStyles={mapStyles} mapStylesReplaceDefault transformRequest={transformRequest}
//             width={width} height={height} />
// and a style is chosen by its id:
//   dispatch(addDataToMap({ datasets, config: { version: 'v1', config: { mapStyle: { styleType: 'smap-dark' } } } }));
print(JSON.stringify(mapStyles, null, 2));

// The same style on an sMap map, for comparison.
new sMap.Map({ container: 'map', style: 'dark', center: [-79.387, 43.653], zoom: 12 });

What to check.

  • The heat map is under the roads and the arcs are under the labels; street names stay readable.
  • The buttons replace the layers without reloading the map.
  • With createVectorBasemapLayer(), the page background is set from basemap.smap.background, and the sMap badge and the OpenStreetMap credit are shown.

Limits. A basemap drawn by deck.gl has no sprite icons, no labels along lines and no label collision; sMap's clustering, Auto Zoom, badges, arrows and dashes are not drawn as deck.gl layers; deck.gl is for the web only. (Interoperability guide 5)

Run in the playground: deck.gl: layers on an sMap map · deck.gl: sMap vector tiles drawn by deck.gl · kepler.gl: sMap styles as map styles

4. ArcGIS services and ArcGIS apps

Interoperability guide 4.4

Situation. An organisation publishes its data as ArcGIS REST services and wants them on an sMap map; another team has an ArcGIS app that should use sMap as its basemap.

Steps.

  1. For services on sMap, install @superrouteca/smap-arcgis (the ArcGIS SDK is not needed for this part) and call addArcGisService(map, id, url) with a MapServer, ImageServer, VectorTileServer or one FeatureServer or MapServer layer.
  2. Pass { token } for a secured service and layerOptions (for example a pop-up) for a feature layer.
  3. For sMap in an ArcGIS app, install @superrouteca/smap-arcgis beside @arcgis/core, and set map.basemap = await createSMapBasemap({ apiKey, style, language }).
  4. Call addAttribution(view) for the sMap badge and the OpenStreetMap credit.

The examples use two public Esri services that need no key: World Hillshade (services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer) and the Trailheads sample (services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trailheads/FeatureServer/0).

Code.

ArcGIS: ArcGIS services on sMap

import * as sMap from '@superrouteca/smap';
import { addArcGisService } from '@superrouteca/smap-arcgis/services';

const map = new sMap.Map({ container: 'map', style: 'light', center: [-118.62, 34.12], zoom: 10.5 });
await map.ready;

// A cached MapServer: Esri's public World Hillshade service (no key), over land and water.
await addArcGisService(map, 'hillshade',
  'https://services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer', { slot: 'bottom' });

// One FeatureServer layer, queried as GeoJSON and coloured by its own renderer: Esri's public sample of
// trailheads in the Santa Monica Mountains (no key). Secured services take an ArcGIS token: { token }.
await addArcGisService(map, 'trailheads',
  'https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trailheads/FeatureServer/0',
  { layerOptions: { popup: sMap.popupTemplate('<b>{TRL_NAME}</b><br>{PARK_NAME}') } });

addButton('Hide hillshade', () => map.setVisible('hillshade', false));
addButton('Show hillshade', () => map.setVisible('hillshade', true));

ArcGIS: sMap as the basemap

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

// Where the ArcGIS SDK finds its assets (workers, styles). This page serves them itself (ARCGIS_ASSETS).
esriConfig.assetsPath = ARCGIS_ASSETS;

// sMap as the ArcGIS basemap: a VectorTileLayer of sMap's vector tiles in sMap's style, converted into what ArcGIS
// draws, in any colour scheme and label language. raster: true gives sMap's raster tiles instead (a WebTileLayer).
// On your own site, add apiKey: 'smk_…' (a map key works in ArcGIS workers too).
const basemap = await createSMapBasemap({ style: 'light', language: 'en' });

const stops = [
  { lng: -79.3832, lat: 43.6532, name: 'City Hall', color: '#16a34a' },
  { lng: -79.3871, lat: 43.6426, name: 'CN Tower', color: '#f97316' },
  { lng: -79.3957, lat: 43.6677, name: "Queen's Park", color: '#2563eb' },
];
const map = new Map({ basemap, layers: [
  createGraphicsLayer('route', 'lines', { type: 'LineString', coordinates: stops.map((s) => [s.lng, s.lat]) }, { color: '#2563eb', width: 4 }),
  createGraphicsLayer('stops', 'points', stops, { marker: { border: { property: 'color' } }, label: 'name',
    popup: (p) => '<b>' + p.name + '</b>', popupTitle: 'Stop' }),
] });
const view = new MapView({ container: 'map', map, center: [-79.388, 43.655], zoom: 13 });
addAttribution(view);   // the sMap badge and the OpenStreetMap credit

// The vector basemap changes its colour scheme and label language in place and keeps every layer.
['light', 'dark', 'grayscale'].forEach((style) => addButton(style, () => basemap.smap.setStyle(style)));
['en', 'fr', 'zh-Hans', 'ja', 'ko'].forEach((tag) => addButton(tag, () => basemap.smap.setLanguage(tag)));

// An existing ArcGIS app switches its basemap and keeps every layer: here to sMap's raster tiles and back.
// Only the newest click sets the basemap, and each raster basemap (one per scheme and language) is made once.
const rasters = {};
let clicks = 0;
addButton('raster', async () => {
  const click = ++clicks;
  const { style, language } = basemap.smap;
  rasters[style + '/' + language] ??= createSMapBasemap({ style, language, raster: true });
  const raster = await rasters[style + '/' + language];
  if (click === clicks) { map.basemap = raster; }
});
addButton('vector', () => { clicks++; map.basemap = basemap; });

What to check.

  • The hillshade is under the roads and the trailheads are drawn in the colours of the layer's own renderer.
  • A click on a trailhead opens its pop-up with the trail and park names.
  • In the ArcGIS app, the colour scheme and language buttons change the vector basemap in place, the raster button switches to raster tiles, and the graphics stay.

Limits. The vector basemap is converted into what ArcGIS draws: labels in one font, the local name on a second line for a name in another script, the first label position where sMap allows several, 3D buildings drawn flat, and Han characters in their Simplified Chinese forms in every language; tile requests made in ArcGIS workers may skip request interceptors, so use a map key rather than a token; a feature layer's attribution is returned and not drawn. (Interoperability guide 5)

Run in the playground: ArcGIS: ArcGIS services on sMap · ArcGIS: sMap as the basemap

5. CesiumJS globe imagery from sMap

Interoperability guide 4.5

Situation. A 3D viewer built with CesiumJS shows sMap's map on the globe, without a Cesium ion account for the imagery, and sMap shows data written for Cesium.

Steps.

  1. Install @superrouteca/smap-cesium beside cesium, and set CESIUM_BASE_URL to where CesiumJS's Workers, Assets and Widgets are served.
  2. Create the viewer with baseLayer: false and call setBasemap(viewer, { apiKey, style, language }); keep the returned handle for setStyle() and setLanguage().
  3. Add sMap's points, lines and areas with addSMapData(viewer, id, data, options).
  4. For Cesium data on an sMap map, call addCesiumData(map, id, czml) from @superrouteca/smap-cesium/data, which does not need the cesium package.

Code.

CesiumJS: sMap imagery on the globe

import { Viewer, Cartesian3 } from 'cesium';
import { setBasemap, addSMapData } from '@superrouteca/smap-cesium';

// CESIUM_BASE_URL (set on this page) tells CesiumJS where its workers and assets are served.
// No Cesium ion account is needed: the globe's imagery is sMap's raster tiles.
const viewer = new Viewer('map', {
  baseLayer: false, baseLayerPicker: false, geocoder: false, timeline: false, animation: false,
  homeButton: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false,
});
// On your own site, add apiKey: 'smk_…' or getToken.
const basemap = setBasemap(viewer, { style: 'light', language: 'en' });

await addSMapData(viewer, 'stops', [
  { lng: -79.3832, lat: 43.6532, name: 'City Hall', n: 1, color: '#16a34a' },
  { lng: -79.3871, lat: 43.6426, name: 'CN Tower', n: 2, color: '#f97316' },
  { lng: -79.3957, lat: 43.6677, name: "Queen's Park", n: 3, color: '#2563eb' },
], { marker: { text: { property: 'n' }, border: { property: 'color' } }, label: 'name', popup: (p) => '<b>' + p.name + '</b>' });
viewer.camera.flyTo({ destination: Cartesian3.fromDegrees(-79.39, 43.615, 5000), orientation: { pitch: -0.6 }, duration: 0 });

['en', 'fr', 'zh-Hans', 'ja'].forEach((tag) => addButton(tag, () => basemap.setLanguage(tag)));
['light', 'dark'].forEach((style) => addButton(style, () => basemap.setStyle(style)));
addButton('Globe', () => viewer.camera.flyTo({ destination: Cartesian3.fromDegrees(-79.39, 43.65, 12000000), duration: 2 }));

CesiumJS: CZML and GeoJSON on sMap

import * as sMap from '@superrouteca/smap';
import { addCesiumData } from '@superrouteca/smap-cesium/data';   // without the cesium package

const map = new sMap.Map({ container: 'map', style: 'light', center: [-79.385, 43.652], zoom: 13.5 });
await map.ready;

// CZML as a Cesium app writes it: a van with a time-dynamic position, a delivery zone, a road and a 3D model.
const czml = [
  { id: 'document', name: 'Fleet', version: '1.0' },
  { id: 'van-1', name: 'Van 1', description: 'Shown at its first sample',
    position: { epoch: '2026-10-27T14:00:00Z', cartographicDegrees: [0, -79.3957, 43.6426, 0, 600, -79.3832, 43.6532, 0] },
    point: { pixelSize: 14, color: { rgba: [37, 99, 235, 255] } } },
  { id: 'zone', name: 'Delivery zone',
    polygon: { positions: { cartographicDegrees: [-79.395, 43.646, 0, -79.375, 43.646, 0, -79.375, 43.658, 0, -79.395, 43.658, 0] },
      material: { solidColor: { color: { rgba: [249, 115, 22, 90] } } }, extrudedHeight: 120 } },
  { id: 'road', name: 'Queen Street',
    polyline: { positions: { cartographicDegrees: [-79.400, 43.6485, 0, -79.370, 43.6545, 0] }, width: 5,
      material: { solidColor: { color: { rgba: [22, 163, 74, 255] } } } } },
  { id: 'truck', name: 'Truck model', position: { cartographicDegrees: [-79.38, 43.645, 0] }, model: { gltf: 'truck.glb' } },
];
const { skipped } = await addCesiumData(map, 'fleet', czml, { slot: 'top' });

// GeoJSON goes the same way (KML and KMZ too).
await addCesiumData(map, 'depot', { type: 'Feature', properties: { title: 'Depot', 'marker-color': '#7c3aed' },
  geometry: { type: 'Point', coordinates: [-79.3817, 43.6487] } }, { format: 'geojson' });

print('skipped: ' + JSON.stringify(skipped, null, 2));

What to check.

  • The globe shows sMap's raster tiles, and the language buttons reload them with other labels.
  • On the sMap map, the CZML zone, road and van are drawn, and skipped lists the 3D model and the time-dynamic position.

Limits. In Cesium, sMap is imagery only: no 3D buildings and no 3D Tiles. On sMap, 3D models, volumes and positions given by reference are skipped and listed, and a time-dynamic position shows its first sample. (Interoperability guide 5)

Run in the playground: CesiumJS: sMap imagery on the globe · CesiumJS: CZML and GeoJSON on sMap

6. Planners use sMap in QGIS

Interoperability guide 4.6

Situation. Planners open sMap as a basemap in QGIS, under their own layers.

Steps.

  1. Have your server issue a token (POST /v1/map/token, valid up to 24 hours).
  2. In QGIS, add an XYZ connection (Browser panel, XYZ Tiles, New Connection) with /v1/raster/<style>/{z}/{x}/{y}.png?lang=<language>&token=sm1.….
  3. Or add a WMTS connection (Browser panel, WMS/WMTS, New Connection) with /v1/raster/wmts?SERVICE=WMTS&REQUEST=GetCapabilities&token=sm1.… and add the layer of a scheme.
  4. When the token expires, replace it in the connection with a new one from your server.

Code.

QGIS: XYZ and WMTS URLs

import * as sMap from '@superrouteca/smap';

// QGIS and other desktop programs read sMap's raster tiles.
// - XYZ:  Browser panel > XYZ Tiles > New Connection, URL below.
// - WMTS: Browser panel > WMS/WMTS > New Connection, URL below, then add the layer of a scheme.
// Desktop programs send no origin, so a map key alone is refused: put a token from your server in the URL
// (POST /v1/map/token, valid up to 24 hours) and ask your server for a new one when it expires.
const TOKEN = 'sm1.…';   // the token your server issued
const style = 'light';
const lang = 'en';

const xyz = sMap.rasterUrl(style, { lang }) + '&token=' + TOKEN;
const base = new URL(sMap.rasterUrl(style)).origin;
const wmts = base + '/v1/raster/wmts?SERVICE=WMTS&REQUEST=GetCapabilities&token=' + TOKEN;
const custom = base + '/v1/raster/custom:sms_…/{z}/{x}/{y}.png?lang=' + lang + '&token=' + TOKEN;   // a saved custom style
print(['XYZ:  ' + xyz, 'WMTS: ' + wmts, 'Custom style (XYZ): ' + custom].join('\n\n'));

// The same raster tiles QGIS will draw, shown here (this page needs no token).
const map = new sMap.Map({ container: 'map', style: 'white', center: [-79.385, 43.654], zoom: 12 });
await map.ready;
map.addRasterLayer('qgis-preview', { tiles: [sMap.rasterUrl(style, { lang })], tileSize: 256 });

What to check.

  • The layer draws in QGIS at every zoom level, with the labels in the language of the URL.
  • Without a token the tiles are refused: desktop programs send no origin.

Limits. A map key alone is refused for desktop programs; use a token in the URL, or a Referer on the key's allowlist. sMap's WMTS lists the five schemes only; custom styles use the XYZ template. (Interoperability guide 5)

Run in the playground: QGIS: XYZ and WMTS URLs

7. Government and weather WMS overlays

Interoperability guide 4.7

Situation. A weather radar or a government flood map is drawn over the basemap, under the street names.

Steps.

  1. Call map.addWmsLayer(id, { url, layers, slot: 'labels', opacity }) with the service's GetMap address and layer names.
  2. For a WMTS service, call await map.addWmtsLayer(id, { url: '…/WMTSCapabilities.xml', layer }).
  3. For an OpenLayers app that already has the source, pass its TileWMS or XYZ layer to addOpenLayersLayer(smap, id, layer, { slot }).
  4. Give the source's attribution; it is shown beside the OpenStreetMap credit.

The examples use the public GeoMet service of Environment and Climate Change Canada (geo.weather.gc.ca/geomet) and Esri's World Hillshade, neither of which needs a key.

Code.

A WMS layer

const map = new sMap.Map({ container: 'map', style: 'grayscale', center: [-79.4, 44.2], zoom: 5.5 });

// An OGC WMS layer: one GetMap request per tile. Here the weather service of Environment and
// Climate Change Canada. The source's attribution is shown beside the map's credit.
const GEOMET = 'https://geo.weather.gc.ca/geomet';
function show(layers) {
  map.removeLayer('weather');
  map.addWmsLayer('weather', {
    url: GEOMET, layers, opacity: 0.7, slot: 'labels',
    attribution: 'Environment and Climate Change Canada',
  });
}

map.ready.then(() => show('GDPS_15km_AirTemp_2m'));
addButton('Temperature', () => show('GDPS_15km_AirTemp_2m'));
addButton('Radar', () => show('RADAR_1KM_RRAI'));

OpenLayers: OpenLayers sources on sMap

import { Map } from '@superrouteca/smap';
import TileLayer from 'ol/layer/Tile.js';
import XYZ from 'ol/source/XYZ.js';
import TileWMS from 'ol/source/TileWMS.js';
import { addOpenLayersLayer } from '@superrouteca/smap-openlayers/on-smap';

const smap = new Map({ container: 'map', style: 'light', center: [-79.4, 44.2], zoom: 5.5 });
await smap.ready;

// An OpenLayers XYZ source as it is in an OpenLayers app: Esri's public World Hillshade service, under the roads.
await addOpenLayersLayer(smap, 'hillshade', new TileLayer({
  source: new XYZ({
    url: 'https://services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer/tile/{z}/{y}/{x}',
    attributions: 'Esri',
  }),
  opacity: 0.45,
}), { slot: 'roads' });

// An OpenLayers TileWMS source: the radar layer of Environment and Climate Change Canada's weather service,
// under the labels.
await addOpenLayersLayer(smap, 'radar', new TileLayer({
  source: new TileWMS({
    url: 'https://geo.weather.gc.ca/geomet',
    params: { LAYERS: 'RADAR_1KM_RRAI', TILED: true },
    attributions: 'Environment and Climate Change Canada',
  }),
  opacity: 0.7,
}), { slot: 'labels' });

const shown = { hillshade: true, radar: true };
Object.keys(shown).forEach((id) => addButton(id, () => { shown[id] = !shown[id]; smap.setVisible(id, shown[id]); }));

What to check.

  • The overlay is under the labels, and the buttons switch between its layers.
  • The service's credit is shown beside the map's credit.

Limits. sMap reads only Web Mercator WMTS tile matrix sets whose matrix ids are the zoom number, and a capabilities document is read only from a service that allows cross-origin requests. (Interoperability guide 5)

Run in the playground: A WMS layer · OpenLayers: OpenLayers sources on sMap

8. KML from Google My Maps and GPX tracks from drivers

Interoperability guide 4.8

Situation. A customer sends a My Maps export and drivers upload GPX tracks. Both are shown on the map and sent on as KML or GeoJSON to other tools.

Steps.

  1. Read each file with await map.addData(id, file); points become the layer <id>, lines <id>:lines and areas <id>:areas.
  2. Give the format when it is not in the file name ({ format: 'kml' }).
  3. Export with map.toKML(id) or map.toGeoJSON(id); sMap.formats.read(text, { format }) gives GeoJSON for a tool that does not read KML.
  4. Send files over 20 MB to POST /v1/convert, which gives the same answer.

Code.

Formats: one KML file in two tools

import * as sMap from '@superrouteca/smap';
import * as L from 'leaflet';
import { smapLayer } from '@superrouteca/smap-leaflet';

const [left, right] = twoMaps();   // left: the sMap web SDK, right: Leaflet

// One KML file, as Google Earth or My Maps exports it (a style, two placemarks and a path).
const kml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"><Document><name>Stores</name>
  <Style id="red"><IconStyle><color>ff2b39e1</color></IconStyle><LineStyle><color>ffeb6325</color><width>4</width></LineStyle></Style>
  <Placemark><name>City Hall</name><description>Open 8:00-20:00</description><styleUrl>#red</styleUrl>
    <Point><coordinates>-79.3832,43.6532,0</coordinates></Point></Placemark>
  <Placemark><name>Harbourfront</name><description>Open 9:00-21:00</description><styleUrl>#red</styleUrl>
    <Point><coordinates>-79.3957,43.6426,0</coordinates></Point></Placemark>
  <Placemark><name>Delivery path</name><styleUrl>#red</styleUrl>
    <LineString><coordinates>-79.3832,43.6532,0 -79.3880,43.6480,0 -79.3957,43.6426,0</coordinates></LineString></Placemark>
</Document></kml>`;

// 1. The KML on an sMap map: points as 'stores', lines as 'stores:lines'.
const smap = new sMap.Map({ container: left, style: 'light', center: [-79.389, 43.648], zoom: 13.5 });
await smap.ready;
await smap.addData('stores', kml, { format: 'kml', label: 'name' });

// 2. Exported again from the map, and loaded into Leaflet as GeoJSON (Leaflet reads GeoJSON, not KML).
const leaflet = L.map(right).setView([43.648, -79.389], 14);
smapLayer({ style: 'grayscale', language: 'en' }).addTo(leaflet);
addButton('Export KML to Leaflet', () => {
  const exported = smap.toKML('stores');
  const geojson = sMap.formats.read(exported, { format: 'kml' });
  L.geoJSON(geojson, { onEachFeature: (f, layer) => layer.bindPopup(f.properties.name || '') }).addTo(leaflet);
  print(exported);
});

Reading a GPX file

const map = new sMap.Map({ container: 'map', style: 'light', center: [-79.464, 43.646], zoom: 14 });

// A GPX file (a track and two waypoints), read on the device. addData() also reads KML, KMZ, CSV,
// WKT, WKB, TopoJSON and encoded polylines; the format comes from the content when not given.
const gpx = `<?xml version="1.0"?>
<gpx version="1.1" creator="example" xmlns="http://www.topografix.com/GPX/1/1">
  <wpt lat="43.6505" lon="-79.4637"><name>Grenadier Pond</name></wpt>
  <wpt lat="43.6413" lon="-79.4628"><name>Colborne Lodge</name></wpt>
  <trk><name>High Park loop</name><trkseg>
    <trkpt lat="43.6531" lon="-79.4655"/><trkpt lat="43.6505" lon="-79.4637"/>
    <trkpt lat="43.6478" lon="-79.4649"/><trkpt lat="43.6446" lon="-79.4641"/>
    <trkpt lat="43.6413" lon="-79.4628"/><trkpt lat="43.6422" lon="-79.4585"/>
    <trkpt lat="43.6460" lon="-79.4571"/><trkpt lat="43.6500" lon="-79.4590"/>
    <trkpt lat="43.6531" lon="-79.4655"/>
  </trkseg></trk>
</gpx>`;

map.ready.then(async () => {
  // Points become the layer 'walk', lines 'walk:lines', areas 'walk:areas'.
  await map.addData('walk', gpx, { format: 'gpx', color: '#dc2626', width: 4, label: 'name' });
  map.fitLayer('walk:lines', { padding: 50 });

  // Export: the same layer as KML.
  addButton('Download KML', () => {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(new Blob([map.toKML('walk')], { type: 'application/vnd.google-earth.kml+xml' }));
    link.download = 'walk.kml';
    link.click();
  });
});

What to check.

  • The placemarks and the path keep their colours from the KML styles.
  • The exported KML opens in Google Earth, and its GeoJSON draws in Leaflet.

Limits. Rings are not closed and coordinates are not rounded; /v1/convert takes up to 20 MB and a KMZ unpacks to at most 100 MB. (Interoperability guide 5)

Run in the playground: Formats: one KML file in two tools · Reading a GPX file

9. A brand style from a Google Maps style JSON

Interoperability guide 4.9

Situation. A style shared on Snazzy Maps, or one already used on Google Maps, becomes an sMap custom style for the website and the apps.

Steps.

  1. Open the Map styles page of the client portal (/open-api/map-styles), import the Google Maps style JSON and choose the base scheme.
  2. Read the list of entries that have no counterpart, adjust the overrides in the preview, and save. The style gets an id sms_… that never changes.
  3. Use style: 'custom:sms_…' in every SDK, and the style URL (/v1/styles/custom/sms_…) or the raster template (/v1/raster/custom:sms_…/{z}/{x}/{y}.png) in other tools.

Code.

A custom style

// A custom style is one of the five schemes with class overrides, saved on the Map styles page
// of the client portal under an id. Every SDK takes it as style: 'custom:<id>'.
const STYLE_ID = '';   // for example 'sms_…' from the Map styles page

// The overrides of a saved style, applied here to its base scheme so the example runs without an id.
const saved = { base: 'dark', overrides: {
  water: { color: '#0b3d5c' }, parks: { color: '#12382a' },
  roads: { color: '#3a4a5c' }, 'roads.highway': { color: '#c08a2e' },
  labels: { halo: '#000000' }, pois: { visible: false },
} };

const map = new sMap.Map({
  container: 'map',
  style: STYLE_ID ? 'custom:' + STYLE_ID : saved.base,
  center: [-79.385, 43.654], zoom: 12.5,
});
map.ready.then(() => { if (!STYLE_ID) { map.setBasemap(saved.overrides); } });

// The same style in other label languages.
['en', 'fr', 'zh-Hans'].forEach((tag) => addButton(tag, () => map.setLanguage(tag)));

What to check.

  • The website and an app show the same colours for the same id.
  • Every label language works with the custom style.

Limits. Partial feature types (for example poi.business), labels.icon, simplified, weight and invert_lightness are skipped and listed; hue, saturation, lightness and gamma apply only to a colour in the same rule. (Interoperability guide 5)

Run in the playground: A custom style · Basemap options

10. The same layers on web, Android, iOS, Flutter and desktop

Interoperability guide 4.10

Situation. Layers built once on the web are loaded in the Android, iOS and Flutter apps and in the desktop apps.

Steps.

  1. On the web, call map.toJSON() and store the document on your server.
  2. In each app, load it with loadLayers(json): map.loadLayers(json) on Android, try map.loadLayers(data) on iOS, await controller.loadLayers(description) in Flutter, <Layers json={json} /> in React Native.
  3. A layer a platform cannot draw is left out and reported; the rest are loaded.

Code.

Formats: one layer JSON for every SDK

import * as sMap from '@superrouteca/smap';

const [left, right] = twoMaps();   // left: the map built on the web, right: the same layers loaded again

const web = new sMap.Map({ container: left, style: 'light', center: [-79.385, 43.654], zoom: 13 });
await web.ready;
web.setBasemap({ water: { color: '#9cc0e6' } });
web.addPolygons('zone', { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [[
  [-79.395, 43.647], [-79.375, 43.647], [-79.375, 43.660], [-79.395, 43.660], [-79.395, 43.647],
]] } }, { fill: '#16a34a', opacity: 0.3, slot: 'labels' });
web.addPoints('stops', [
  { lng: -79.3832, lat: 43.6532, name: 'City Hall' },
  { lng: -79.3871, lat: 43.6577, name: "Queen's Park" },
], { color: '#2563eb', radius: 8, label: 'name', popup: '<b>{name}</b>' });

// One layer description, read by every sMap SDK. Your server stores it; each app loads it.
const saved = web.toJSON();
const app = new sMap.Map({ container: right, style: 'dark', center: [-79.385, 43.654], zoom: 13 });
await app.ready;
app.loadLayers(saved);

const json = JSON.stringify(saved);
print([
  '// Web (this page)       map.loadLayers(json);',
  '// Android (Kotlin)      map.loadLayers(json)',
  '// iOS (Swift)           try map.loadLayers(Data(json.utf8))',
  '// Flutter (Dart)        await controller.loadLayers(jsonDecode(json));',
  '// React Native          <Layers json={json} />',
  '',
  'json = ' + JSON.stringify(saved, null, 2),
].join('\n'));
addButton('Copy JSON', () => navigator.clipboard && navigator.clipboard.writeText(json));

What to check.

  • The second map shows the same basemap overrides, area and points as the first.
  • The document has the same shape on every platform: toJSON() of an app gives a document the web reads back.

Limits. On iOS and Flutter, addData draws no pop-up; video sources are not supported in the native SDKs; the macOS and Windows SDKs run the web SDK without deck.gl. (Interoperability guide 5)

Run in the playground: Formats: one layer JSON for every SDK · A map as JSON