@superrouteca/smap-leaflet 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 — 2026-10-27
4
+
5
+ - First version. sMap in Leaflet: `smapLayer()` draws the sMap vector map in a Leaflet pane with the five colour
6
+ schemes and saved custom styles (`custom:<id>`), any label language, basemap overrides, sMap's pins, lines and
7
+ areas with their options, popups and Auto Zoom; Leaflet keeps the view and the input, and the sMap map's own moves
8
+ are made by Leaflet. `smapRasterLayer()` gives raster tiles for browsers without WebGL; `smapBasemap()` picks one.
9
+ - Map key or token with renewal through `getToken`, the nearest map node and the next one when a node fails.
10
+ - The data credit and every source credit appear in Leaflet's attribution, with the sMap badge beside it.
11
+ - Leaflet on sMap: `createLeafletCompat(smapMap)` gives Leaflet-style calls on an sMap map (`L.map`, `L.marker`,
12
+ `L.circleMarker`, `L.circle`, `L.polyline`, `L.polygon`, `L.rectangle`, `L.geoJSON`, `L.tileLayer`,
13
+ `L.tileLayer.wms`, `L.layerGroup`, `L.featureGroup`, `L.popup`, `bindPopup`, `addTo`, events).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Goodneo Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,159 @@
1
- # Temporary Holding Version
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Goodneo-Solutions/smap-sdk/main/assets/smap-logo-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/Goodneo-Solutions/smap-sdk/main/assets/smap-logo.svg" alt="sMap" height="56">
4
+ </picture>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # sMap for Leaflet: `@superrouteca/smap-leaflet`
7
+
8
+ Two ways round:
9
+
10
+ - **sMap in Leaflet.** The sMap map as a Leaflet layer: the five colour schemes and your saved custom styles, labels
11
+ in any language, basemap overrides, sMap's pins, lines and areas with all their options, popups and Auto Zoom, map
12
+ key or token with renewal, the nearest map node. Vector (drawn with WebGL inside a Leaflet pane) or raster tiles for
13
+ browsers without WebGL.
14
+ - **Leaflet on sMap.** Leaflet-style calls (`L.marker`, `L.circleMarker`, `L.polyline`, `L.polygon`, `L.geoJSON`,
15
+ `L.tileLayer`, `bindPopup`, `L.layerGroup`, `addTo`, events) on an sMap map, so code written for Leaflet runs on
16
+ sMap.
17
+
18
+ - Version 1.1.0, MIT licence (see `LICENSE` at the repository root)
19
+ - Leaflet 1.9 (a peer dependency; not needed for the Leaflet-style calls on an sMap map)
20
+ - ES module with types
21
+
22
+ ## Installation
23
+
24
+ ```sh
25
+ npm install @superrouteca/smap-leaflet leaflet
26
+ ```
27
+
28
+ Load Leaflet's stylesheet and the sMap stylesheet (popups, Auto Zoom chooser):
29
+
30
+ ```js
31
+ import 'leaflet/dist/leaflet.css';
32
+ import '@superrouteca/smap/smap.css';
33
+ ```
34
+
35
+ ## sMap in Leaflet
36
+
37
+ ```js
38
+ import L from 'leaflet';
39
+ import { smapLayer } from '@superrouteca/smap-leaflet';
40
+
41
+ const map = L.map('map').setView([43.65, -79.38], 12);
42
+
43
+ const basemap = smapLayer({
44
+ apiKey: 'smk_…', // a map key that allows this site, or token / getToken
45
+ style: 'light', // light, white, grayscale, dark, black, or 'custom:sms_…'
46
+ language: 'en',
47
+ }).addTo(map);
48
+
49
+ // Your Leaflet layers stay as they are:
50
+ L.marker([43.65, -79.38]).addTo(map);
51
+
52
+ // sMap's own layers, on the sMap map (drawn on the GPU, popups and Auto Zoom included):
53
+ basemap.addPoints('stops', stops, {
54
+ marker: { fill: { property: 'status_color' }, border: { property: 'route_color' }, text: { property: 'stop_number' } },
55
+ popup: (p) => `<b>${p.stop_number}</b>`,
56
+ });
57
+ basemap.addLines('routes', routes, { color: { property: 'color' }, width: 5, casing: true, arrows: true });
58
+ basemap.addPolygons('zones', zones, { fill: '#f97316', opacity: 0.3 });
59
+ basemap.setBasemap({ water: { color: '#9cc0e6' }, pois: { visible: false } });
60
+ basemap.setStyle('dark');
61
+ basemap.setLanguage('fr');
62
+ ```
63
+
64
+ The layer forwards every sMap map call once its map is ready (`setStyle`, `setLanguage`, `setBasemap`,
65
+ `setAutoZoom`, `addPoints`, `addLines`, `addPolygons`, `addData`, `setData`, `setVisible`, `fitLayer`, `addSource`,
66
+ `addStyleLayer`, `setPaint`, `setLayout`, `setFilter`, `addRasterLayer`, `addWmsLayer`, `addWmtsLayer`,
67
+ `addVectorTiles`, `addArcGisLayer`, `loadLayers`); `openSMapPopup` / `closeSMapPopup` open and close the sMap map's
68
+ popup and `removeSMapLayer(id)` removes one of its layers (Leaflet keeps its own `openPopup` and `removeLayer`). Calls
69
+ that answer a value: `await basemap.call('basemap')`, `await basemap.call('toJSON')`. `await basemap.ready` (or
70
+ `basemap.getSMap()`) gives the sMap map itself.
71
+
72
+ How it works with Leaflet:
73
+
74
+ - Leaflet owns the view and the input. Every move and zoom of Leaflet moves the sMap map; every move the sMap map
75
+ asks for (Auto Zoom on piled-up pins, `fitLayer`, a popup panning into view) is made by Leaflet.
76
+ - Clicks and pointer moves on the map reach sMap's layers: popups, Auto Zoom, `onClick`, the pin under the pointer
77
+ coming to the front, the pointer cursor. `interactive: false` turns this off.
78
+ - The data credit and the credit of every source you add appear in Leaflet's attribution; the sMap badge sits above
79
+ it in the bottom-right corner (one badge per map).
80
+ - Leaflet's zoom levels are one above the sMap map's own (`map.getZoom()` 12 is sMap zoom 11).
81
+ - Leaflet draws flat maps: 3D buildings and tilting are for the tools that draw 3D (see the Mapbox GL JS adapter).
82
+ - `pane` (default `tilePane`) chooses the Leaflet pane.
83
+
84
+ ### Raster tiles
85
+
86
+ For browsers without WebGL, or wherever images are wanted:
87
+
88
+ ```js
89
+ import { smapRasterLayer, smapBasemap } from '@superrouteca/smap-leaflet';
90
+
91
+ smapRasterLayer({ apiKey: 'smk_…', style: 'grayscale', language: 'de' }).addTo(map); // 256 px tiles, 512 on high-density screens
92
+ smapBasemap({ apiKey: 'smk_…', style: 'light' }).addTo(map); // vector where WebGL 2 exists, raster elsewhere
93
+ ```
94
+
95
+ Tiles are read with the map key or token from the nearest node and move to the next node when one fails.
96
+ `setStyle()` and `setLanguage()` reload them. Every Leaflet grid layer option (`opacity`, `zIndex`, `minZoom`, …)
97
+ applies.
98
+
99
+ ### Map keys and tokens
100
+
101
+ - **Map key** (`apiKey: 'smk_…'`): created in the Superroute developer center with the sites it may be used from.
102
+ - **Token** (`token: 'sm1.…'`, or `getToken: async () => …`): signed by your own server; with `getToken` a new token
103
+ is asked for two minutes before the current one expires.
104
+ - `baseUrl` (default `https://tile.srsr.ca`), `nodes` (node addresses known in advance), `nearest` (default true).
105
+
106
+ ## Leaflet on sMap
107
+
108
+ ```js
109
+ import { Map } from '@superrouteca/smap';
110
+ import { createLeafletCompat } from '@superrouteca/smap-leaflet/compat';
111
+
112
+ const smap = new Map({ container: 'map', apiKey: 'smk_…', style: 'light', center: [-79.38, 43.65], zoom: 11 });
113
+ await smap.ready;
114
+ const L = createLeafletCompat(smap);
115
+ const map = L.map; // a Leaflet-style map over the sMap map
116
+
117
+ L.marker([43.65, -79.38]).addTo(map).bindPopup('<b>Depot</b>');
118
+ L.circleMarker([43.66, -79.4], { radius: 8, color: '#e11d48' }).addTo(map);
119
+ L.polyline(latlngs, { color: '#16a34a', weight: 5, dashArray: '10, 5' }).addTo(map);
120
+ L.polygon(ring, { color: '#2563eb', fillOpacity: 0.3 }).addTo(map);
121
+ L.geoJSON(zones, {
122
+ style: (f) => ({ color: f.properties.color }),
123
+ onEachFeature: (f, layer) => layer.bindPopup(f.properties.name),
124
+ }).addTo(map);
125
+ L.tileLayer('https://{s}.tiles.example/{z}/{x}/{y}.png', { attribution: '© Example' }).addTo(map);
126
+ L.layerGroup([L.marker([43.7, -79.4])]).addTo(map);
127
+
128
+ map.setView([43.65, -79.38], 13);
129
+ map.on('click', (e) => console.log(e.latlng));
130
+ ```
131
+
132
+ What is there: `L.map` (`setView`, `getCenter`, `getZoom`, `setZoom`, `zoomIn`, `zoomOut`, `panTo`, `panBy`,
133
+ `flyTo`, `fitBounds`, `getBounds`, `addLayer`, `removeLayer`, `hasLayer`, `eachLayer`, `on` / `off` with `latlng`,
134
+ `openPopup`, `closePopup`, `invalidateSize`, `project`, `unproject`), `L.marker` (with `L.icon`), `L.circleMarker`,
135
+ `L.circle` (radius in metres), `L.polyline`, `L.polygon` (holes and multi-polygons), `L.rectangle`, `L.geoJSON`
136
+ (`style`, `pointToLayer`, `onEachFeature`, `filter`, `addData`, `resetStyle`), `L.tileLayer` and `L.tileLayer.wms`,
137
+ `L.layerGroup`, `L.featureGroup`, `L.popup`, `L.latLng`, `L.latLngBounds`. Layers have `addTo`, `remove`,
138
+ `bindPopup` (text, an element or a function of the layer), `openPopup`, `setStyle`, `setLatLng` / `setLatLngs`,
139
+ `getBounds`, `toGeoJSON`, and `on` / `off` / `fire` (a click carries `latlng` and `layer`; a feature group hears its
140
+ layers' clicks).
141
+
142
+ Each layer added to the map draws on top of the ones before, as in Leaflet. Layers that share their look are drawn
143
+ together, so a GeoJSON layer of thousands of features costs a few map layers. Moving a marker only replaces its data.
144
+ `L.divIcon` has no counterpart (the default pin is drawn); for many points use `smap.addPoints` with its pins and
145
+ clustering.
146
+
147
+ `addTo()` takes `L.map`, a layer group, or the sMap map itself.
148
+
149
+ ## Example
150
+
151
+ `examples/index.html` shows both ways. Serve the repository root (for example `npx serve .`) and open
152
+ `/leaflet/examples/?key=smk_…` with a map key that allows your host.
153
+
154
+ ## Tests
155
+
156
+ ```sh
157
+ npm test
158
+ npm run typecheck
159
+ ```
@@ -0,0 +1,94 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>sMap for Leaflet</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
8
+ <link rel="stylesheet" href="../../js/dist/smap.css">
9
+ <style>
10
+ body { margin: 0; font: 14px/1.4 system-ui, sans-serif; }
11
+ .bar { display: flex; gap: 8px; align-items: center; padding: 8px 12px; flex-wrap: wrap; }
12
+ .maps { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 0 12px 12px; }
13
+ .maps > div { height: 70vh; border: 1px solid #cbd5e1; border-radius: 6px; }
14
+ h2 { font-size: 14px; margin: 0 12px; }
15
+ @media (max-width: 800px) { .maps { grid-template-columns: 1fr; } }
16
+ </style>
17
+ <!-- Serve the repository root (npx serve .) and open /leaflet/examples/?key=smk_… -->
18
+ <script type="importmap">
19
+ {
20
+ "imports": {
21
+ "leaflet": "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js",
22
+ "@superrouteca/smap": "../../js/dist/smap.mjs",
23
+ "@superrouteca/smap/client": "../../js/dist/smap-client.mjs",
24
+ "@superrouteca/smap-leaflet": "../src/index.js",
25
+ "@superrouteca/smap-leaflet/compat": "../src/compat.js"
26
+ }
27
+ }
28
+ </script>
29
+ </head>
30
+ <body>
31
+ <div class="bar">
32
+ <label>Scheme <select id="scheme"><option>light</option><option>white</option><option>grayscale</option><option>dark</option><option>black</option></select></label>
33
+ <label>Language <select id="lang"><option>en</option><option>fr</option><option>de</option><option>zh-Hans</option></select></label>
34
+ <label><input type="checkbox" id="raster"> Raster tiles</label>
35
+ <button id="water">Blue water</button>
36
+ </div>
37
+ <h2>sMap in Leaflet (left) · Leaflet calls on an sMap map (right)</h2>
38
+ <div class="maps"><div id="leaflet"></div><div id="smap"></div></div>
39
+ <script type="module">
40
+ import * as Leaflet from 'leaflet';
41
+ import { Map as SMap, popupTemplate } from '@superrouteca/smap';
42
+ import { smapLayer, smapRasterLayer } from '@superrouteca/smap-leaflet';
43
+ import { createLeafletCompat } from '@superrouteca/smap-leaflet/compat';
44
+
45
+ const L = Leaflet.default || Leaflet;
46
+ const apiKey = new URLSearchParams(location.search).get('key') || undefined;
47
+ const stops = [
48
+ { lng: -79.3832, lat: 43.6532, n: 1, status: '#16a34a', name: 'City Hall' },
49
+ { lng: -79.3871, lat: 43.6426, n: 2, status: '#f97316', name: 'CN Tower' },
50
+ { lng: -79.3957, lat: 43.6677, n: 3, status: '#2563eb', name: 'Queen’s Park' },
51
+ { lng: -79.3806, lat: 43.6629, n: 4, status: '#e11d48', name: 'Church St' },
52
+ ];
53
+ const route = { type: 'LineString', coordinates: stops.map((s) => [s.lng, s.lat]) };
54
+
55
+ // ① sMap in Leaflet
56
+ const map = L.map('leaflet').setView([43.655, -79.387], 14);
57
+ let base = smapLayer({ apiKey, style: 'light', language: 'en' }).addTo(map);
58
+ function decorate(layer) {
59
+ layer.addLines('route', route, { color: '#2563eb', width: 5, casing: true, arrows: true });
60
+ layer.addPoints('stops', stops, {
61
+ marker: { fill: { property: 'status' }, border: '#1e293b', text: { property: 'n' } },
62
+ popup: popupTemplate('<b>{n}</b> {name}'),
63
+ });
64
+ }
65
+ decorate(base);
66
+ L.marker([43.6487, -79.3817]).addTo(map).bindPopup('A Leaflet marker over sMap');
67
+
68
+ const scheme = document.getElementById('scheme');
69
+ const lang = document.getElementById('lang');
70
+ scheme.onchange = () => base.setStyle(scheme.value);
71
+ lang.onchange = () => base.setLanguage(lang.value);
72
+ document.getElementById('water').onclick = () => base.setBasemap && base.setBasemap({ water: { color: '#60a5fa' } });
73
+ document.getElementById('raster').onchange = (e) => {
74
+ map.removeLayer(base);
75
+ base = e.target.checked
76
+ ? smapRasterLayer({ apiKey, style: scheme.value, language: lang.value }).addTo(map)
77
+ : smapLayer({ apiKey, style: scheme.value, language: lang.value }).addTo(map);
78
+ if (!e.target.checked) { decorate(base); }
79
+ };
80
+
81
+ // ② Leaflet calls on an sMap map
82
+ const smap = new SMap({ container: 'smap', apiKey, style: 'light', language: 'en', center: [-79.387, 43.655], zoom: 13 });
83
+ await smap.ready;
84
+ const C = createLeafletCompat(smap);
85
+ C.polyline(stops.map((s) => [s.lat, s.lng]), { color: '#16a34a', weight: 5, dashArray: '10, 6' }).addTo(C.map);
86
+ C.geoJSON({ type: 'FeatureCollection', features: stops.map((s) => ({ type: 'Feature', properties: s, geometry: { type: 'Point', coordinates: [s.lng, s.lat] } })) }, {
87
+ pointToLayer: (f, latlng) => C.circleMarker(latlng, { radius: 9, color: '#fff', weight: 2, fillColor: f.properties.status, fillOpacity: 1 }),
88
+ onEachFeature: (f, layer) => layer.bindPopup(`<b>${f.properties.n}</b> ${f.properties.name}`),
89
+ }).addTo(C.map);
90
+ C.circle([43.6532, -79.3832], { radius: 300, color: '#f97316' }).addTo(C.map);
91
+ C.map.on('click', (e) => console.log('clicked', e.latlng));
92
+ </script>
93
+ </body>
94
+ </html>
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@superrouteca/smap-leaflet",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "sMap for Leaflet: maps from Superroute's sMap service as a Leaflet layer (vector or raster), and Leaflet-style calls on an sMap map.",
5
+ "license": "MIT",
6
+ "author": "Goodneo Solutions",
7
+ "homepage": "https://github.com/Goodneo-Solutions/smap-sdk/tree/main/leaflet#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Goodneo-Solutions/smap-sdk.git",
11
+ "directory": "leaflet"
12
+ },
13
+ "keywords": [
14
+ "map",
15
+ "maps",
16
+ "leaflet",
17
+ "vector-tiles",
18
+ "smap",
19
+ "superroute"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "main": "./src/index.js",
24
+ "types": "./types/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./types/index.d.ts",
28
+ "default": "./src/index.js"
29
+ },
30
+ "./compat": {
31
+ "types": "./types/compat.d.ts",
32
+ "default": "./src/compat.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "src",
38
+ "types",
39
+ "examples",
40
+ "README.md",
41
+ "CHANGELOG.md",
42
+ "LICENSE"
43
+ ],
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "scripts": {
48
+ "prepack": "cp ../LICENSE LICENSE",
49
+ "typecheck": "tsc -p test/tsconfig.json",
50
+ "test": "node --test test/*.test.mjs"
51
+ },
52
+ "peerDependencies": {
53
+ "leaflet": "^1.9.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "leaflet": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "dependencies": {
61
+ "@superrouteca/smap": "1.1.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/leaflet": "1.9.22",
65
+ "jsdom": "30.1.3",
66
+ "leaflet": "1.9.4",
67
+ "typescript": "6.0.3"
68
+ }
69
+ }
@@ -0,0 +1,198 @@
1
+ // sMap tool adapters — access to the service: map key or token (renewed through getToken), the nearest node and the
2
+ // next one when a node fails. Kept identical in leaflet/, openlayers/ and mapbox/ (test/common.test.mjs checks); the
3
+ // node choice, the token renewal and the failover of requests are the JS client's (@superrouteca/smap/client).
4
+ import SMapClient from '@superrouteca/smap/client';
5
+ import * as sMapModule from '@superrouteca/smap';
6
+
7
+ const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
8
+ const PLACEHOLDER = 'https://smap.invalid';
9
+
10
+ /** The SDK's own URL builder (rasterUrl, styleUrl of @superrouteca/smap 1.1.0); throws when the SDK is older. */
11
+ function builder(name) {
12
+ if (typeof SDK[name] !== 'function') { throw new Error('sMap: the adapters need @superrouteca/smap 1.1.0 or later (' + name + ')'); }
13
+ return SDK[name];
14
+ }
15
+
16
+ /** A path of the SDK's URL builders, without their address. */
17
+ function pathOf(url) { return url.slice(PLACEHOLDER.length); }
18
+
19
+ export const DEFAULT_BASE = 'https://tile.srsr.ca';
20
+ export const FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
21
+ const CUSTOM = /^custom:([A-Za-z0-9_-]+)$/;
22
+
23
+ /** Checks a style name: a scheme or `custom:<id>`; anything else throws with the name in the message. */
24
+ export function checkStyle(style) {
25
+ const s = style == null ? 'light' : String(style);
26
+ if (FLAVORS.indexOf(s) >= 0 || CUSTOM.test(s)) { return s; }
27
+ throw new Error('sMap: unknown style "' + s + '" (use ' + FLAVORS.join(', ') + ' or custom:<id>)');
28
+ }
29
+
30
+ /** The path of one raster tile on an sMap node (interop contract §5, the SDK's rasterUrl): 256 px, or 512 px with `retina`. */
31
+ export function rasterPath(style, z, x, y, options) {
32
+ const o = options || {};
33
+ const url = builder('rasterUrl')(checkStyle(style), { lang: o.lang || o.language, retina: !!o.retina, baseUrl: PLACEHOLDER });
34
+ return pathOf(url).replace('{z}', String(z | 0)).replace('{x}', String(x | 0)).replace('{y}', String(y | 0));
35
+ }
36
+
37
+ /** The path of a style document (the SDK's styleUrl): a scheme or a saved custom style, with labels in `lang`. */
38
+ export function stylePath(style, lang) {
39
+ return pathOf(builder('styleUrl')(checkStyle(style), { lang: lang, baseUrl: PLACEHOLDER }));
40
+ }
41
+
42
+ function strip(url) { return String(url).replace(/\/$/, ''); }
43
+
44
+ export class SMapAccess {
45
+ /**
46
+ * @param {Object} [o] baseUrl, token, getToken, apiKey, nodes, nearest, fetch, timeoutMs (as the JS client), or
47
+ * `client`: a JS client of your own.
48
+ */
49
+ constructor(o) {
50
+ o = o || {};
51
+ this.client = o.client || new SMapClient({
52
+ baseUrl: o.baseUrl || DEFAULT_BASE,
53
+ token: o.token,
54
+ getToken: o.getToken,
55
+ apiKey: o.apiKey,
56
+ nodes: o.nodes,
57
+ nearest: o.nearest,
58
+ fetch: o.fetch,
59
+ timeoutMs: o.timeoutMs,
60
+ });
61
+ this.baseUrl = strip(this.client.baseUrl);
62
+ this.apiKey = o.apiKey || null;
63
+ this._getToken = o.getToken || null;
64
+ this._token = o.token || null;
65
+ this._seeds = (o.nodes || []).map(strip);
66
+ this._order = null;
67
+ this._timer = null;
68
+ this._starting = null;
69
+ this._stopped = false;
70
+ this._listeners = [];
71
+ }
72
+
73
+ /** Reads the first token and the node order; resolves once both are known (never rejects for the nodes). */
74
+ start() {
75
+ if (!this._starting) {
76
+ const self = this;
77
+ this._starting = Promise.all([
78
+ this.refresh(),
79
+ this.client.nodes().then(function (order) { self._order = order; }, function () { self._order = [self.baseUrl]; }),
80
+ ]).then(function () { return self; });
81
+ }
82
+ return this._starting;
83
+ }
84
+
85
+ /** Asks the client for the current token (getToken is called when it is missing or expires within 2 minutes). */
86
+ refresh() {
87
+ const self = this;
88
+ return this.client.token().then(function (t) {
89
+ const changed = t !== self._token;
90
+ self._token = t || null;
91
+ self._schedule();
92
+ if (changed) { self._emit('token', self._token); }
93
+ return self._token;
94
+ });
95
+ }
96
+
97
+ _schedule() {
98
+ clearTimeout(this._timer);
99
+ this._timer = null;
100
+ if (this._stopped || !this._getToken || !this._token) { return; }
101
+ const exp = SMapClient.tokenExpiry(this._token);
102
+ if (!exp) { return; }
103
+ // One second past the client's renewal point (2 minutes before expiry), so the client asks getToken again.
104
+ const wait = Math.max(5, exp - Math.floor(Date.now() / 1000) - 119) * 1000;
105
+ const self = this;
106
+ this._timer = setTimeout(function () {
107
+ self.refresh().catch(function () {
108
+ clearTimeout(self._timer);
109
+ self._timer = setTimeout(function () { self._schedule(); self.refresh().catch(function () {}); }, 30000);
110
+ });
111
+ }, Math.min(wait, 2147483000));
112
+ if (this._timer && typeof this._timer.unref === 'function') { this._timer.unref(); }
113
+ }
114
+
115
+ /** The token to send now (null without one). */
116
+ token() { return this._token; }
117
+
118
+ /** The nodes in the order they are tried, nearest first (baseUrl alone before start() resolves). */
119
+ nodes() { return this._order ? this._order.slice() : [this.baseUrl]; }
120
+
121
+ /** The node requests go to now. */
122
+ head() { return (this._order && this._order[0]) || this.baseUrl; }
123
+
124
+ _nodeOf(url) {
125
+ const u = String(url);
126
+ const all = [this.baseUrl].concat(this._seeds, this._order || []);
127
+ for (let i = 0; i < all.length; i++) {
128
+ const n = all[i];
129
+ if (u === n || u.indexOf(n + '/') === 0 || u.indexOf(n + '?') === 0) { return n; }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /** True for a URL on the sMap service (baseUrl, a seed or a listed node). */
135
+ isOurs(url) { return this._nodeOf(url) !== null; }
136
+
137
+ /** The headers an sMap request carries (token, map key); none for any other host. */
138
+ headers(url) {
139
+ const h = {};
140
+ if (!this.isOurs(url)) { return h; }
141
+ if (this._token) { h.Authorization = 'Bearer ' + this._token; }
142
+ if (this.apiKey) { h['X-SMap-Key'] = this.apiKey; }
143
+ return h;
144
+ }
145
+
146
+ /** An sMap URL moved onto the node requests go to now; other URLs unchanged. */
147
+ rewrite(url) {
148
+ const n = this._nodeOf(url);
149
+ const head = this.head();
150
+ if (!n || n === head) { return url; }
151
+ return head + String(url).slice(n.length);
152
+ }
153
+
154
+ /** Reports that a request to `url` failed: its node is tried last from now on. */
155
+ failed(url) {
156
+ const n = this._nodeOf(url);
157
+ if (!n || !this._order || this._order.length < 2) { return; }
158
+ // The client's own order is the one moved, so its requests and these rewrites agree.
159
+ if (typeof this.client.demote === 'function') { this.client.demote(n); }
160
+ else {
161
+ const i = this._order.indexOf(n);
162
+ if (i >= 0) { this._order.splice(i, 1); this._order.push(n); }
163
+ }
164
+ this._emit('nodes', this.nodes());
165
+ }
166
+
167
+ /** A path appended with the map key (styles and TileJSON: the service writes it into the tile URLs). */
168
+ withKey(path) {
169
+ if (!this.apiKey) { return path; }
170
+ return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'key=' + encodeURIComponent(this.apiKey);
171
+ }
172
+
173
+ /** A style document (a scheme or `custom:<id>`) from the nearest node that answers. */
174
+ style(style, lang) {
175
+ return this.client.request(this.withKey(stylePath(style, lang))).then(function (r) { return r.json(); });
176
+ }
177
+
178
+ /** One raster tile as a Blob, from the nearest node that answers (contract §5). */
179
+ rasterTile(style, z, x, y, options) {
180
+ return this.client.request(rasterPath(style, z, x, y, options)).then(function (r) { return r.blob(); });
181
+ }
182
+
183
+ /** A `{z}/{x}/{y}` raster URL on the current node, for tools that load images by URL (map key in the query). */
184
+ rasterTemplate(style, options) {
185
+ const o = options || {};
186
+ return builder('rasterUrl')(checkStyle(style), { lang: o.lang || o.language, retina: !!o.retina, baseUrl: this.head(), apiKey: this.apiKey || undefined });
187
+ }
188
+
189
+ /** Called with ('token', token) after a renewal and ('nodes', order) after a node failed. */
190
+ on(fn) { this._listeners.push(fn); return this; }
191
+
192
+ _emit(type, value) { this._listeners.slice().forEach(function (fn) { try { fn(type, value); } catch (e) { /* listener error */ } }); }
193
+
194
+ /** Stops renewing the token. */
195
+ stop() { this._stopped = true; clearTimeout(this._timer); this._timer = null; this._listeners = []; }
196
+ }
197
+
198
+ export { SMapClient };
@@ -0,0 +1,56 @@
1
+ // sMap tool adapters — the sMap badge and the data credit shown in another tool's corner. Kept identical in leaflet/,
2
+ // openlayers/ and mapbox/ (test/common.test.mjs checks).
3
+
4
+ /** The data credit every sMap map keeps visible (the data licence requires it). */
5
+ export const OSM_ATTRIBUTION = '<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">&copy; OpenStreetMap contributors</a>';
6
+
7
+ const ICON = '<svg viewBox="0 0 56 56" width="14" height="14" aria-hidden="true" focusable="false">' +
8
+ '<rect x="2" y="2" width="52" height="52" rx="13" fill="#2563eb"/>' +
9
+ '<path d="M38 15H22a8 8 0 0 0 0 16h12a8 8 0 0 1 0 16H18" fill="none" stroke="#fff" stroke-width="7" ' +
10
+ 'stroke-linecap="round" stroke-linejoin="round"/></svg>';
11
+
12
+ const CSS = [
13
+ '.smap-adapter-badge{display:flex;align-items:center;gap:4px;height:20px;padding:0 7px 0 3px;border-radius:10px;',
14
+ 'background:rgba(255,255,255,.82);box-shadow:0 0 0 1px rgba(15,23,42,.12);color:#1e293b;',
15
+ 'font:600 11px/20px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;',
16
+ 'letter-spacing:.01em;pointer-events:none;-webkit-user-select:none;user-select:none;margin:4px 6px;width:max-content}',
17
+ '.smap-adapter-badge svg{display:block;flex:none;width:14px;height:14px}',
18
+ // An sMap map drawn inside another tool: the tool takes the input; only popups take clicks; the tool's corner
19
+ // shows the badge and the credit, so the embedded map's own are hidden.
20
+ '.smap-embedded{position:absolute;top:0;left:0;pointer-events:none;overflow:hidden}',
21
+ '.smap-embedded .smap-badge,.smap-embedded .maplibregl-ctrl-attrib{display:none!important}',
22
+ '.smap-embedded .maplibregl-popup{pointer-events:auto}',
23
+ ].join('');
24
+
25
+ /** Adds the adapters' stylesheet to a document once. */
26
+ export function injectCss(doc) {
27
+ if (!doc || !doc.head || doc.getElementById('smap-adapter-css')) { return; }
28
+ const style = doc.createElement('style');
29
+ style.id = 'smap-adapter-css';
30
+ style.textContent = CSS;
31
+ doc.head.appendChild(style);
32
+ }
33
+
34
+ /** The badge element (not a link): the sMap icon and name on a semi-transparent pill. */
35
+ export function createBadge(doc) {
36
+ injectCss(doc);
37
+ const el = doc.createElement('div');
38
+ el.className = 'smap-adapter-badge';
39
+ el.setAttribute('aria-label', 'sMap');
40
+ el.innerHTML = ICON + '<span>sMap</span>';
41
+ return el;
42
+ }
43
+
44
+ /**
45
+ * The credits to show: the data credit first, then every other credit a source names (once each; a source's own
46
+ * OpenStreetMap credit is the data credit and is not repeated).
47
+ */
48
+ export function mergeAttributions(list) {
49
+ const out = [OSM_ATTRIBUTION];
50
+ (list || []).forEach(function (a) {
51
+ if (!a || typeof a !== 'string') { return; }
52
+ if (/openstreetmap/i.test(a)) { return; }
53
+ if (out.indexOf(a) < 0) { out.push(a); }
54
+ });
55
+ return out;
56
+ }