@superrouteca/smap 0.0.0-stage → 1.0.1
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 +28 -0
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/SOURCE.json +17 -0
- package/dist/smap-client.cjs +301 -0
- package/dist/smap-client.d.cts +43 -0
- package/dist/smap-client.d.mts +44 -0
- package/dist/smap-client.d.ts +42 -0
- package/dist/smap-client.js +300 -0
- package/dist/smap-client.mjs +308 -0
- package/dist/smap.cjs +27478 -0
- package/dist/smap.css +24 -0
- package/dist/smap.js +27452 -0
- package/dist/smap.mjs +27474 -0
- package/dist/vendor-licenses.txt +116 -0
- package/package.json +67 -4
- package/types/smap.d.cts +185 -0
- package/types/smap.d.mts +185 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.1 — 2026-10-27
|
|
4
|
+
|
|
5
|
+
- Every map shows a small sMap badge (the sMap icon and name on a semi-transparent pill that reads on all five colour
|
|
6
|
+
schemes) in its bottom-right corner, left of the attribution (above it when the map is too narrow for both). It is
|
|
7
|
+
always shown and is not a link; the map data attribution stays visible. The badge is styled by `smap.css` and loads
|
|
8
|
+
nothing.
|
|
9
|
+
- The package description starts with "sMap"; the README starts with the sMap logo.
|
|
10
|
+
- The browser build is in the package: `dist/smap.js` (global `sMap`), `dist/smap.css` and `dist/smap-client.js`
|
|
11
|
+
(global `SMapClient`, types in `dist/smap-client.d.ts`). The map service no longer serves the SDK.
|
|
12
|
+
- A map reads from `https://tile.srsr.ca` unless `baseUrl` is given, wherever the script was loaded from.
|
|
13
|
+
- The Web SDK reports the package version (1.0.1).
|
|
14
|
+
- The bundled map renderer is upgraded to its version 6.16.0, which fixes a security advisory in how it cleans the map
|
|
15
|
+
data credit (GHSA-jrc7-96c5-q579). Maps now need WebGL 2, and the renderer's worker starts from a `blob:` URL
|
|
16
|
+
(`worker-src blob:` under a Content Security Policy). Feature properties that hold objects reach `popup` and
|
|
17
|
+
`onClick` as objects instead of JSON text.
|
|
18
|
+
- A `popup` may return a DOM node as well as an HTML string; an HTML string is inserted as it is (never sanitized), so
|
|
19
|
+
it must be trusted.
|
|
20
|
+
- The browser build no longer sets a renderer global beside `sMap`; use `sMap.engine`.
|
|
21
|
+
|
|
22
|
+
## 1.0.0 — 2026-10-27
|
|
23
|
+
|
|
24
|
+
- The Web SDK as an npm package: ES module and CommonJS entry points with types, the map renderer bundled, safe to
|
|
25
|
+
import on a server.
|
|
26
|
+
- The JS client as `@superrouteca/smap/client` for browsers, Node.js 18+ and workers.
|
|
27
|
+
- The stylesheet as `@superrouteca/smap/smap.css`.
|
|
28
|
+
- Map keys (`apiKey`) beside tokens.
|
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,110 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
6
|
+
# sMap for the web: `@superrouteca/smap`
|
|
7
|
+
|
|
8
|
+
Maps from Superroute's sMap service for the web: five colour schemes, labels in any language, your points, lines and
|
|
9
|
+
areas drawn on the GPU (hundreds of thousands of points, clustered or not), 3D buildings and areas, and tokens that
|
|
10
|
+
renew themselves. Also the JS client (no map view) for servers and workers: tokens, the nearest node, styles, tiles
|
|
11
|
+
and raw reads.
|
|
12
|
+
|
|
13
|
+
- Version 1.0.1, MIT licence (see `LICENSE` at the repository root)
|
|
14
|
+
- Works with bundlers (Vite, webpack, Next.js and others): ES module and CommonJS entry points with types
|
|
15
|
+
- Nothing else to install: the map renderer is bundled in the package
|
|
16
|
+
- Importing is safe on a server; a map is created in the browser only
|
|
17
|
+
- Browsers with WebGL 2 (every current browser). The renderer's worker starts from a `blob:` URL: a page with a
|
|
18
|
+
Content Security Policy allows `worker-src blob:`
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @superrouteca/smap
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Without a bundler, use the browser build in the package (also on the downloads page of the Superroute Open Platform
|
|
27
|
+
developer center): `dist/smap.js` sets the global `sMap`, `dist/smap.css` is its stylesheet, and `dist/smap-client.js`
|
|
28
|
+
(types in `dist/smap-client.d.ts`) is the JS client as `window.SMapClient`. Host the files with your page:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<link rel="stylesheet" href="smap.css">
|
|
32
|
+
<script src="smap.js"></script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The map reads from the public sMap service (`https://tile.srsr.ca`) unless `baseUrl` names another.
|
|
36
|
+
|
|
37
|
+
## A minimal map
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { Map } from '@superrouteca/smap';
|
|
41
|
+
import '@superrouteca/smap/smap.css';
|
|
42
|
+
|
|
43
|
+
const map = new Map({
|
|
44
|
+
container: 'map', // an element or its id
|
|
45
|
+
apiKey: 'smk_…', // a map key that allows this site
|
|
46
|
+
style: 'light', // light, white, grayscale, dark, black
|
|
47
|
+
language: 'en',
|
|
48
|
+
center: [-79.38, 43.65], // [longitude, latitude]
|
|
49
|
+
zoom: 11,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await map.ready;
|
|
53
|
+
map.addPoints('stops', [{ lng: -79.38, lat: 43.65, name: 'Depot' }], { cluster: true, label: 'name' });
|
|
54
|
+
map.addLines('route', routeGeoJson, { color: '#16a34a', width: 5 });
|
|
55
|
+
map.addPolygons('zones', zonesGeoJson, { fill: '#f97316', extrude: { height: 'height' } });
|
|
56
|
+
map.setBuildings3D(true);
|
|
57
|
+
map.fitLayer('stops');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A `popup` option returns an HTML string, which is inserted as it is and never sanitized (escape any feature data you
|
|
61
|
+
put in it), or a DOM node.
|
|
62
|
+
|
|
63
|
+
`setStyle()` and `setLanguage()` keep your layers. Other calls: `setData`, `setVisible`, `removeLayer`, `layers`,
|
|
64
|
+
`flyTo`, `setProjection`, `on` / `off` / `once`, `remove`. `map.engine` is the renderer's own map for anything the
|
|
65
|
+
SDK does not wrap, and `getEngine()` gives the renderer itself.
|
|
66
|
+
|
|
67
|
+
In a server-rendered page (for example Next.js), create the map in an effect or event handler, never while rendering
|
|
68
|
+
on the server: `new Map()` throws outside a browser. Importing the package is fine anywhere.
|
|
69
|
+
|
|
70
|
+
## Map keys and tokens
|
|
71
|
+
|
|
72
|
+
- **Map key** (`apiKey: 'smk_…'`): created in the Superroute developer center, with the sites (and apps) it may be
|
|
73
|
+
used from. Safe to put in a page.
|
|
74
|
+
- **Token** (`token: 'sm1.…'`, or `getToken: async () => …`): signed by your own server for a limited time.
|
|
75
|
+
With `getToken` the SDK asks for a new token two minutes before the current one expires.
|
|
76
|
+
- With both, both are sent and the token is checked first. Superroute's own sites need neither.
|
|
77
|
+
|
|
78
|
+
Other options: `baseUrl` (default `https://tile.srsr.ca`), `nodes` (node addresses known in advance, used while
|
|
79
|
+
`baseUrl` cannot be reached), `nearest` (read from the nearest node, default true). Every other option (`pitch`,
|
|
80
|
+
`bearing`, `minZoom`, `maxZoom`, `maxBounds`, …) is passed to the renderer.
|
|
81
|
+
|
|
82
|
+
## The JS client
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import SMapClient from '@superrouteca/smap/client'; // or require('@superrouteca/smap/client')
|
|
86
|
+
|
|
87
|
+
const client = new SMapClient({ baseUrl: 'https://tile.srsr.ca', apiKey: 'smk_…' });
|
|
88
|
+
const style = await client.style('dark', 'fr'); // a style document
|
|
89
|
+
const tile = await client.tile('basemap', 12, 1143, 1493); // Uint8Array, or null where there is none
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Runs wherever `fetch` exists (browsers, Node.js 18+, workers); elsewhere pass `fetch` in the options.
|
|
93
|
+
|
|
94
|
+
## Data attribution
|
|
95
|
+
|
|
96
|
+
The map data credit stays visible on every map, as the data licence requires. Every map shows a small sMap badge in
|
|
97
|
+
its bottom-right corner, left of the attribution (above it when the map is too narrow for both).
|
|
98
|
+
|
|
99
|
+
## Where the code comes from
|
|
100
|
+
|
|
101
|
+
The sources are in `src/` (the Web SDK `smap.js` and `smap.css`, the JS client `smap-client.js` and its types).
|
|
102
|
+
`npm run build` writes `dist/` from them and the rendering engine (an exact devDependency); `dist/SOURCE.json`
|
|
103
|
+
records the version, the engine and the hash of every file. `dist/` is committed and never edited by hand. The type
|
|
104
|
+
declarations of the map SDK are in `types/`.
|
|
105
|
+
|
|
106
|
+
## Tests
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npm test
|
|
110
|
+
```
|
package/dist/SOURCE.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdkVersion": "1.0.1",
|
|
3
|
+
"engine": "maplibre-gl 6.16.0",
|
|
4
|
+
"files": {
|
|
5
|
+
"smap.js": "b48a84d65d3937c122f3f67b6cead8b9348913e2447782cca2a3f7a6c5d48ae7",
|
|
6
|
+
"smap.css": "558e15e5dfb3676e90f4cdfdb277c58f78c3d8de5527c8df1707f6c572241668",
|
|
7
|
+
"smap-client.js": "8029946bec62ce4806e47e2324f3020b3d049b0a73bb98352cdd307474ae776c",
|
|
8
|
+
"smap-client.d.ts": "ad701612630a290b2dc0e49250ae25cea965ecb3a145c016bb462cd30f108cf7",
|
|
9
|
+
"smap.mjs": "b59530982e07703ce3e3ffeaea328f18a86855e99076e737ee41c4dbd186104d",
|
|
10
|
+
"smap.cjs": "22f7b7248f40d5436cb1cd4c865f06e1a38ab8f9beed96ec3828abf058549a71",
|
|
11
|
+
"smap-client.cjs": "9956892bea8d367c1d278de11c3850293ad7d963d74d9692ce7194f6deff6f15",
|
|
12
|
+
"smap-client.mjs": "176a70faf2b4ecb3f998e5823202097b2a9d1bce546c41f6411f2567e2f7e5ae",
|
|
13
|
+
"smap-client.d.cts": "80b96f5a907caf6295ca073816dc526e8d285f22670c809adaab5242a018e686",
|
|
14
|
+
"smap-client.d.mts": "a66834cab868a6a0eaf943c1b185a236916cb00ecf32a871f972ab3213eddeba",
|
|
15
|
+
"vendor-licenses.txt": "ee5fc05a0677eaf69601d2c7db0d9ecd6cc27c3abc1d0733bc9ed34707cf8ef2"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
|
|
2
|
+
/*! sMap JS client 1.0.1 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
|
|
3
|
+
/*
|
|
4
|
+
* Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
|
|
5
|
+
* own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
|
|
6
|
+
*
|
|
7
|
+
* const client = new SMapClient({ baseUrl: 'https://tile.srsr.ca', getToken: () => myServer.mapToken() });
|
|
8
|
+
* const keyed = new SMapClient({ baseUrl: 'https://tile.srsr.ca', apiKey: 'smk_…' }); // or a map key
|
|
9
|
+
* const tile = await client.tile('basemap', 12, 1143, 1493); // Uint8Array (vector tile)
|
|
10
|
+
* const style = await client.style('light', 'en'); // style document
|
|
11
|
+
*
|
|
12
|
+
* Every request goes to the nearest node that answers (asked of baseUrl, remembered for an hour) and moves to the
|
|
13
|
+
* next one when a node fails.
|
|
14
|
+
*/
|
|
15
|
+
(function (root, factory) {
|
|
16
|
+
if (typeof define === 'function' && define.amd) { define([], factory); }
|
|
17
|
+
else if (typeof module === 'object' && module.exports) { module.exports = factory(); }
|
|
18
|
+
else { root.SMapClient = factory(); }
|
|
19
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
var VERSION = '1.0.1';
|
|
23
|
+
var NODE_TTL = 3600 * 1000; // the measured order
|
|
24
|
+
var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
|
|
25
|
+
|
|
26
|
+
function unique(list) {
|
|
27
|
+
var seen = {};
|
|
28
|
+
return list.filter(function (u) { if (!u || seen[u]) { return false; } seen[u] = true; return true; });
|
|
29
|
+
}
|
|
30
|
+
var memory = {};
|
|
31
|
+
|
|
32
|
+
function store() {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof localStorage !== 'undefined') { localStorage.getItem('smap'); return localStorage; }
|
|
35
|
+
} catch (e) { /* storage unavailable */ }
|
|
36
|
+
return { getItem: function (k) { return memory[k] || null; }, setItem: function (k, v) { memory[k] = v; } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeBase64Url(s) {
|
|
40
|
+
s = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
41
|
+
while (s.length % 4) { s += '='; }
|
|
42
|
+
if (typeof atob === 'function') { return atob(s); }
|
|
43
|
+
return Buffer.from(s, 'base64').toString('binary');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Seconds since 1970 when a token expires; 0 when it cannot be read. */
|
|
47
|
+
function tokenExpiry(token) {
|
|
48
|
+
try { return JSON.parse(decodeBase64Url(String(token).split('.')[1])).exp || 0; } catch (e) { return 0; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function SMapError(message, status, url) {
|
|
52
|
+
var e = new Error(message);
|
|
53
|
+
e.name = 'SMapError';
|
|
54
|
+
e.status = status || 0;
|
|
55
|
+
e.url = url || '';
|
|
56
|
+
return e;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Object} options
|
|
61
|
+
* @param {string} options.baseUrl the public sMap address (e.g. https://tile.srsr.ca)
|
|
62
|
+
* @param {string} [options.token] a map token; or
|
|
63
|
+
* @param {function(): (string|Promise<string>)} [options.getToken] called at start and before each token expires
|
|
64
|
+
* @param {string} [options.apiKey] a map key (`smk_…`); with a token both are sent (token first)
|
|
65
|
+
* @param {string[]} [options.nodes] node URLs known in advance (used while baseUrl is down)
|
|
66
|
+
* @param {boolean} [options.nearest=true] read from the nearest node
|
|
67
|
+
* @param {function} [options.fetch] a fetch implementation (default: the global one)
|
|
68
|
+
* @param {number} [options.timeoutMs=15000] per request
|
|
69
|
+
*/
|
|
70
|
+
function SMapClient(options) {
|
|
71
|
+
if (!(this instanceof SMapClient)) { return new SMapClient(options); }
|
|
72
|
+
options = options || {};
|
|
73
|
+
if (!options.baseUrl) { throw SMapError('sMap: baseUrl is required'); }
|
|
74
|
+
this.baseUrl = String(options.baseUrl).replace(/\/$/, '');
|
|
75
|
+
this._fetch = options.fetch || (typeof fetch === 'function' ? fetch.bind(typeof self !== 'undefined' ? self : globalThis) : null);
|
|
76
|
+
if (!this._fetch) { throw SMapError('sMap: no fetch available; pass options.fetch'); }
|
|
77
|
+
this._token = options.token || null;
|
|
78
|
+
this._getToken = options.getToken || null;
|
|
79
|
+
this._apiKey = options.apiKey || null;
|
|
80
|
+
this._nearest = options.nearest !== false;
|
|
81
|
+
this._seeds = (options.nodes || []).map(function (u) { return String(u).replace(/\/$/, ''); });
|
|
82
|
+
this._timeout = options.timeoutMs || 15000;
|
|
83
|
+
this._nodes = null; // ordered: nearest first
|
|
84
|
+
this._picking = null;
|
|
85
|
+
this._tokening = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
SMapClient.version = VERSION;
|
|
89
|
+
SMapClient.tokenExpiry = tokenExpiry;
|
|
90
|
+
|
|
91
|
+
SMapClient.prototype._timed = function (url, init, ms) {
|
|
92
|
+
var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
93
|
+
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
|
|
94
|
+
init = init || {};
|
|
95
|
+
if (ctrl) { init.signal = ctrl.signal; }
|
|
96
|
+
return this._fetch(url, init).then(function (res) { clearTimeout(timer); return res; }, function (e) { clearTimeout(timer); throw e; });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** The token to send now, asking getToken again two minutes before it expires. */
|
|
100
|
+
SMapClient.prototype.token = function () {
|
|
101
|
+
var self = this;
|
|
102
|
+
var exp = this._token ? tokenExpiry(this._token) : 0;
|
|
103
|
+
var fresh = this._token && (!exp || exp - Date.now() / 1000 > 120);
|
|
104
|
+
if (fresh || !this._getToken) { return Promise.resolve(this._token); }
|
|
105
|
+
if (!this._tokening) {
|
|
106
|
+
this._tokening = Promise.resolve(this._getToken()).then(function (t) {
|
|
107
|
+
self._token = t;
|
|
108
|
+
self._tokening = null;
|
|
109
|
+
return t;
|
|
110
|
+
}, function (e) { self._tokening = null; throw e; });
|
|
111
|
+
}
|
|
112
|
+
return this._tokening;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The nodes in the order they are tried: nearest first, then the others; baseUrl alone when nearest is off or the
|
|
117
|
+
* list cannot be read.
|
|
118
|
+
*
|
|
119
|
+
* @returns {Promise<string[]>}
|
|
120
|
+
*/
|
|
121
|
+
SMapClient.prototype.nodes = function () {
|
|
122
|
+
var self = this;
|
|
123
|
+
if (this._nodes) { return Promise.resolve(this._nodes); }
|
|
124
|
+
if (!this._nearest) { this._nodes = [this.baseUrl]; return Promise.resolve(this._nodes); }
|
|
125
|
+
if (this._picking) { return this._picking; }
|
|
126
|
+
var key = 'smap.nodes:' + this.baseUrl;
|
|
127
|
+
var storage = store();
|
|
128
|
+
var saved = null;
|
|
129
|
+
try { saved = JSON.parse(storage.getItem(key) || 'null'); } catch (e) { saved = null; }
|
|
130
|
+
if (saved && saved.urls && saved.urls.length && Date.now() - saved.at < NODE_TTL) {
|
|
131
|
+
this._nodes = saved.urls;
|
|
132
|
+
return Promise.resolve(this._nodes);
|
|
133
|
+
}
|
|
134
|
+
// Every node this client knows of: the kept list (30 days), the seed list, then baseUrl.
|
|
135
|
+
var kept = saved && saved.known && Date.now() - (saved.knownAt || 0) < KNOWN_TTL ? saved.known : [];
|
|
136
|
+
var known = unique([this.baseUrl].concat(this._seeds, kept));
|
|
137
|
+
this._picking = this._discover(known)
|
|
138
|
+
.then(function (list) {
|
|
139
|
+
var urls = list || unique(self._seeds.concat(kept));
|
|
140
|
+
if (urls.length === 0) { return { order: [self.baseUrl], known: null }; }
|
|
141
|
+
return self._measure(urls).then(function (order) { return { order: order, known: list }; });
|
|
142
|
+
})
|
|
143
|
+
.then(function (r) {
|
|
144
|
+
self._nodes = r.order;
|
|
145
|
+
self._picking = null;
|
|
146
|
+
// An order is remembered for the hour only when a node list was read; a guess made while nothing answered
|
|
147
|
+
// is tried again by the next client instead of being kept.
|
|
148
|
+
var record = { urls: r.known ? r.order : [], at: r.known ? Date.now() : 0, known: r.known || kept, knownAt: r.known ? Date.now() : (saved && saved.knownAt) || 0 };
|
|
149
|
+
try { storage.setItem(key, JSON.stringify(record)); } catch (e) { /* ignore */ }
|
|
150
|
+
return r.order;
|
|
151
|
+
});
|
|
152
|
+
return this._picking;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Asks each node in turn for the node list (any node answers the whole list); null when none answers. */
|
|
156
|
+
SMapClient.prototype._discover = function (candidates) {
|
|
157
|
+
var self = this;
|
|
158
|
+
function ask(i) {
|
|
159
|
+
if (i >= candidates.length) { return Promise.resolve(null); }
|
|
160
|
+
return self._timed(candidates[i] + '/v1/nodes', {}, 2000)
|
|
161
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
162
|
+
.then(function (body) {
|
|
163
|
+
var urls = ((body && body.nodes) || []).map(function (n) { return String(n.url).replace(/\/$/, ''); });
|
|
164
|
+
return urls.length ? urls : ask(i + 1);
|
|
165
|
+
}, function () { return ask(i + 1); });
|
|
166
|
+
}
|
|
167
|
+
return ask(0);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** Times one small request to each node: fastest first, silent ones after, baseUrl last when not listed. */
|
|
171
|
+
SMapClient.prototype._measure = function (urls) {
|
|
172
|
+
var self = this;
|
|
173
|
+
return Promise.all(urls.map(function (url) {
|
|
174
|
+
var t0 = Date.now();
|
|
175
|
+
return self._timed(url + '/v1/ping', { cache: 'no-store' }, 1500)
|
|
176
|
+
.then(function (res) { return { url: url, ms: res.ok ? Date.now() - t0 : Infinity }; }, function () { return { url: url, ms: Infinity }; });
|
|
177
|
+
})).then(function (timed) {
|
|
178
|
+
timed.sort(function (a, b) { return a.ms - b.ms; });
|
|
179
|
+
var order = timed.map(function (t) { return t.url; });
|
|
180
|
+
if (order.indexOf(self.baseUrl) < 0) { order.push(self.baseUrl); }
|
|
181
|
+
return order;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Forgets the chosen node order, so the next request measures again. */
|
|
186
|
+
SMapClient.prototype.resetNodes = function () {
|
|
187
|
+
this._nodes = null;
|
|
188
|
+
var key = 'smap.nodes:' + this.baseUrl;
|
|
189
|
+
try {
|
|
190
|
+
var saved = JSON.parse(store().getItem(key) || 'null');
|
|
191
|
+
// The kept node list survives a reset; only the measured order is forgotten.
|
|
192
|
+
store().setItem(key, JSON.stringify(saved ? { urls: [], at: 0, known: saved.known, knownAt: saved.knownAt } : null));
|
|
193
|
+
} catch (e) { /* ignore */ }
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* GET `path` from the nearest node that answers, with the token; a node that fails (network error, 5xx) is moved
|
|
198
|
+
* to the end and the next one is tried. 4xx answers are final.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} path starting with /
|
|
201
|
+
* @param {Object} [headers]
|
|
202
|
+
* @returns {Promise<Response>}
|
|
203
|
+
*/
|
|
204
|
+
SMapClient.prototype.request = function (path, headers) {
|
|
205
|
+
var self = this;
|
|
206
|
+
return Promise.all([this.nodes(), this.token()]).then(function (r) {
|
|
207
|
+
var nodes = r[0].slice();
|
|
208
|
+
var token = r[1];
|
|
209
|
+
var last = null;
|
|
210
|
+
function attempt(i) {
|
|
211
|
+
if (i >= nodes.length) { throw last || SMapError('sMap: no node answered', 0, path); }
|
|
212
|
+
var h = {};
|
|
213
|
+
for (var k in (headers || {})) { if (Object.prototype.hasOwnProperty.call(headers, k)) { h[k] = headers[k]; } }
|
|
214
|
+
if (token) { h.Authorization = 'Bearer ' + token; }
|
|
215
|
+
if (self._apiKey) { h['X-SMap-Key'] = self._apiKey; }
|
|
216
|
+
var url = nodes[i] + path;
|
|
217
|
+
return self._timed(url, { headers: h }, self._timeout).then(function (res) {
|
|
218
|
+
if (res.status >= 500) {
|
|
219
|
+
last = SMapError('sMap: HTTP ' + res.status, res.status, url);
|
|
220
|
+
self._demote(nodes[i]);
|
|
221
|
+
return attempt(i + 1);
|
|
222
|
+
}
|
|
223
|
+
if (!res.ok && res.status !== 206) {
|
|
224
|
+
return res.text().then(function (text) { throw SMapError('sMap: ' + (text || ('HTTP ' + res.status)), res.status, url); });
|
|
225
|
+
}
|
|
226
|
+
return res;
|
|
227
|
+
}, function (e) {
|
|
228
|
+
last = SMapError('sMap: ' + (e && e.message ? e.message : 'network error'), 0, url);
|
|
229
|
+
self._demote(nodes[i]);
|
|
230
|
+
return attempt(i + 1);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return attempt(0);
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
SMapClient.prototype._demote = function (url) {
|
|
238
|
+
if (!this._nodes || this._nodes.length < 2) { return; }
|
|
239
|
+
var i = this._nodes.indexOf(url);
|
|
240
|
+
if (i >= 0) { this._nodes.splice(i, 1); this._nodes.push(url); }
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function bytes(res) { return res.arrayBuffer().then(function (b) { return new Uint8Array(b); }); }
|
|
244
|
+
|
|
245
|
+
// A map key also goes in the query of styles and TileJSON: the service carries it into their tile URLs, so a
|
|
246
|
+
// renderer given the document reads its tiles with the key.
|
|
247
|
+
SMapClient.prototype._withKey = function (path) {
|
|
248
|
+
if (!this._apiKey) { return path; }
|
|
249
|
+
return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'key=' + encodeURIComponent(this._apiKey);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A style document ('light', 'white', 'grayscale', 'dark', 'black') with labels in `language`.
|
|
254
|
+
* Its tile URLs point at the node that answered.
|
|
255
|
+
*/
|
|
256
|
+
SMapClient.prototype.style = function (flavor, language) {
|
|
257
|
+
var q = language ? '?lang=' + encodeURIComponent(language) : '';
|
|
258
|
+
return this.request(this._withKey('/v1/styles/' + encodeURIComponent(flavor || 'light') + q)).then(function (r) { return r.json(); });
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/** TileJSON of a source (bounds, zooms, layers, tile URL template). */
|
|
262
|
+
SMapClient.prototype.tileJSON = function (source) {
|
|
263
|
+
return this.request(this._withKey('/v1/tiles/' + encodeURIComponent(source || 'basemap'))).then(function (r) { return r.json(); });
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** One vector tile as bytes; null where the source has no tile (204 / 404). */
|
|
267
|
+
SMapClient.prototype.tile = function (source, z, x, y) {
|
|
268
|
+
var path = '/v1/tiles/' + encodeURIComponent(source) + '/' + (z | 0) + '/' + (x | 0) + '/' + (y | 0);
|
|
269
|
+
return this.request(path).then(function (r) { return r.status === 204 ? null : bytes(r); }, function (e) {
|
|
270
|
+
if (e && e.status === 404) { return null; }
|
|
271
|
+
throw e;
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** Raw bytes `[start, end]` (inclusive) of a source's archive, where your access allows raw reads. */
|
|
276
|
+
SMapClient.prototype.range = function (source, start, end) {
|
|
277
|
+
return this.request('/' + encodeURIComponent(source) + '.pmtiles', { Range: 'bytes=' + start + '-' + end }).then(bytes);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** A font glyph range (`0-255.pbf` style), for renderers that load fonts themselves. */
|
|
281
|
+
SMapClient.prototype.glyphs = function (fontstack, range) {
|
|
282
|
+
return this.request('/v1/fonts/' + encodeURIComponent(fontstack) + '/' + range + '.pbf').then(bytes);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Headers and URL rewriting for a renderer that fetches by itself: returns the URL unchanged plus the
|
|
287
|
+
* Authorization (and map key) headers to add for sMap URLs; none for any other host.
|
|
288
|
+
*/
|
|
289
|
+
SMapClient.prototype.authorize = function (url) {
|
|
290
|
+
var self = this;
|
|
291
|
+
return this.token().then(function (token) {
|
|
292
|
+
var ours = url.indexOf(self.baseUrl) === 0 || self._seeds.concat(self._nodes || []).some(function (n) { return url.indexOf(n) === 0; });
|
|
293
|
+
var headers = {};
|
|
294
|
+
if (ours && token) { headers.Authorization = 'Bearer ' + token; }
|
|
295
|
+
if (ours && self._apiKey) { headers['X-SMap-Key'] = self._apiKey; }
|
|
296
|
+
return { url: url, headers: headers };
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return SMapClient;
|
|
301
|
+
}));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Generated by js/build.mjs. Do not edit.
|
|
2
|
+
// sMap JS client — type declarations (UMD: `require('smap-client')`, `import SMapClient from …`, `window.SMapClient`).
|
|
3
|
+
declare namespace SMapClient {
|
|
4
|
+
interface Options {
|
|
5
|
+
/** The public sMap address, e.g. https://tile.srsr.ca */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
getToken?: () => string | Promise<string>;
|
|
9
|
+
/** A map key (`smk_…`); with a token both are sent and the token is checked first. */
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/** Node URLs known in advance, used while baseUrl is down. */
|
|
12
|
+
nodes?: string[];
|
|
13
|
+
/** Read from the nearest node (default true). */
|
|
14
|
+
nearest?: boolean;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
/** Per request, default 15000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
type StyleFlavor = 'light' | 'white' | 'grayscale' | 'dark' | 'black';
|
|
20
|
+
interface SMapError extends Error {
|
|
21
|
+
status: number;
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare class SMapClient {
|
|
27
|
+
constructor(options: SMapClient.Options);
|
|
28
|
+
static readonly version: string;
|
|
29
|
+
static tokenExpiry(token: string): number;
|
|
30
|
+
readonly baseUrl: string;
|
|
31
|
+
token(): Promise<string | null>;
|
|
32
|
+
nodes(): Promise<string[]>;
|
|
33
|
+
resetNodes(): void;
|
|
34
|
+
request(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
35
|
+
style(flavor?: SMapClient.StyleFlavor, language?: string): Promise<Record<string, unknown>>;
|
|
36
|
+
tileJSON(source?: string): Promise<Record<string, unknown>>;
|
|
37
|
+
tile(source: string, z: number, x: number, y: number): Promise<Uint8Array | null>;
|
|
38
|
+
range(source: string, start: number, end: number): Promise<Uint8Array>;
|
|
39
|
+
glyphs(fontstack: string, range: string): Promise<Uint8Array>;
|
|
40
|
+
authorize(url: string): Promise<{ url: string; headers: Record<string, string> }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export = SMapClient;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by js/build.mjs. Do not edit.
|
|
2
|
+
// sMap JS client — type declarations (UMD: `require('smap-client')`, `import SMapClient from …`, `window.SMapClient`).
|
|
3
|
+
declare namespace SMapClient {
|
|
4
|
+
interface Options {
|
|
5
|
+
/** The public sMap address, e.g. https://tile.srsr.ca */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
getToken?: () => string | Promise<string>;
|
|
9
|
+
/** A map key (`smk_…`); with a token both are sent and the token is checked first. */
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/** Node URLs known in advance, used while baseUrl is down. */
|
|
12
|
+
nodes?: string[];
|
|
13
|
+
/** Read from the nearest node (default true). */
|
|
14
|
+
nearest?: boolean;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
/** Per request, default 15000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
type StyleFlavor = 'light' | 'white' | 'grayscale' | 'dark' | 'black';
|
|
20
|
+
interface SMapError extends Error {
|
|
21
|
+
status: number;
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare class SMapClient {
|
|
27
|
+
constructor(options: SMapClient.Options);
|
|
28
|
+
static readonly version: string;
|
|
29
|
+
static tokenExpiry(token: string): number;
|
|
30
|
+
readonly baseUrl: string;
|
|
31
|
+
token(): Promise<string | null>;
|
|
32
|
+
nodes(): Promise<string[]>;
|
|
33
|
+
resetNodes(): void;
|
|
34
|
+
request(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
35
|
+
style(flavor?: SMapClient.StyleFlavor, language?: string): Promise<Record<string, unknown>>;
|
|
36
|
+
tileJSON(source?: string): Promise<Record<string, unknown>>;
|
|
37
|
+
tile(source: string, z: number, x: number, y: number): Promise<Uint8Array | null>;
|
|
38
|
+
range(source: string, start: number, end: number): Promise<Uint8Array>;
|
|
39
|
+
glyphs(fontstack: string, range: string): Promise<Uint8Array>;
|
|
40
|
+
authorize(url: string): Promise<{ url: string; headers: Record<string, string> }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { SMapClient };
|
|
44
|
+
export default SMapClient;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// sMap JS client — type declarations (UMD: `require('smap-client')`, `import SMapClient from …`, `window.SMapClient`).
|
|
2
|
+
declare namespace SMapClient {
|
|
3
|
+
interface Options {
|
|
4
|
+
/** The public sMap address, e.g. https://tile.srsr.ca */
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
token?: string;
|
|
7
|
+
getToken?: () => string | Promise<string>;
|
|
8
|
+
/** A map key (`smk_…`); with a token both are sent and the token is checked first. */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** Node URLs known in advance, used while baseUrl is down. */
|
|
11
|
+
nodes?: string[];
|
|
12
|
+
/** Read from the nearest node (default true). */
|
|
13
|
+
nearest?: boolean;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
/** Per request, default 15000. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
type StyleFlavor = 'light' | 'white' | 'grayscale' | 'dark' | 'black';
|
|
19
|
+
interface SMapError extends Error {
|
|
20
|
+
status: number;
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare class SMapClient {
|
|
26
|
+
constructor(options: SMapClient.Options);
|
|
27
|
+
static readonly version: string;
|
|
28
|
+
static tokenExpiry(token: string): number;
|
|
29
|
+
readonly baseUrl: string;
|
|
30
|
+
token(): Promise<string | null>;
|
|
31
|
+
nodes(): Promise<string[]>;
|
|
32
|
+
resetNodes(): void;
|
|
33
|
+
request(path: string, headers?: Record<string, string>): Promise<Response>;
|
|
34
|
+
style(flavor?: SMapClient.StyleFlavor, language?: string): Promise<Record<string, unknown>>;
|
|
35
|
+
tileJSON(source?: string): Promise<Record<string, unknown>>;
|
|
36
|
+
tile(source: string, z: number, x: number, y: number): Promise<Uint8Array | null>;
|
|
37
|
+
range(source: string, start: number, end: number): Promise<Uint8Array>;
|
|
38
|
+
glyphs(fontstack: string, range: string): Promise<Uint8Array>;
|
|
39
|
+
authorize(url: string): Promise<{ url: string; headers: Record<string, string> }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export = SMapClient;
|