@superrouteca/smap 0.0.0-stage → 1.0.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 +9 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/SOURCE.json +18 -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.mjs +308 -0
- package/dist/smap.cjs +635 -0
- package/dist/smap.css +1 -0
- package/dist/smap.mjs +631 -0
- package/dist/vendor-licenses.txt +116 -0
- package/package.json +55 -4
- package/types/smap.d.cts +182 -0
- package/types/smap.d.mts +182 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/* Generated by tools/sync-js.sh from the sMap repository (see SOURCE.json). Do not edit. */
|
|
2
|
+
var smapClientModule = { exports: {} };
|
|
3
|
+
(function (module, exports, define) {
|
|
4
|
+
/*! sMap JS client 1.0.1 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
|
|
5
|
+
/*
|
|
6
|
+
* Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
|
|
7
|
+
* own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
|
|
8
|
+
*
|
|
9
|
+
* const client = new SMapClient({ baseUrl: 'https://tile.srsr.ca', getToken: () => myServer.mapToken() });
|
|
10
|
+
* const keyed = new SMapClient({ baseUrl: 'https://tile.srsr.ca', apiKey: 'smk_…' }); // or a map key
|
|
11
|
+
* const tile = await client.tile('basemap', 12, 1143, 1493); // Uint8Array (vector tile)
|
|
12
|
+
* const style = await client.style('light', 'en'); // style document
|
|
13
|
+
*
|
|
14
|
+
* Every request goes to the nearest node that answers (asked of baseUrl, remembered for an hour) and moves to the
|
|
15
|
+
* next one when a node fails.
|
|
16
|
+
*/
|
|
17
|
+
(function (root, factory) {
|
|
18
|
+
if (typeof define === 'function' && define.amd) { define([], factory); }
|
|
19
|
+
else if (typeof module === 'object' && module.exports) { module.exports = factory(); }
|
|
20
|
+
else { root.SMapClient = factory(); }
|
|
21
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
var VERSION = '1.0.1';
|
|
25
|
+
var NODE_TTL = 3600 * 1000; // the measured order
|
|
26
|
+
var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
|
|
27
|
+
|
|
28
|
+
function unique(list) {
|
|
29
|
+
var seen = {};
|
|
30
|
+
return list.filter(function (u) { if (!u || seen[u]) { return false; } seen[u] = true; return true; });
|
|
31
|
+
}
|
|
32
|
+
var memory = {};
|
|
33
|
+
|
|
34
|
+
function store() {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof localStorage !== 'undefined') { localStorage.getItem('smap'); return localStorage; }
|
|
37
|
+
} catch (e) { /* storage unavailable */ }
|
|
38
|
+
return { getItem: function (k) { return memory[k] || null; }, setItem: function (k, v) { memory[k] = v; } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decodeBase64Url(s) {
|
|
42
|
+
s = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
43
|
+
while (s.length % 4) { s += '='; }
|
|
44
|
+
if (typeof atob === 'function') { return atob(s); }
|
|
45
|
+
return Buffer.from(s, 'base64').toString('binary');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Seconds since 1970 when a token expires; 0 when it cannot be read. */
|
|
49
|
+
function tokenExpiry(token) {
|
|
50
|
+
try { return JSON.parse(decodeBase64Url(String(token).split('.')[1])).exp || 0; } catch (e) { return 0; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SMapError(message, status, url) {
|
|
54
|
+
var e = new Error(message);
|
|
55
|
+
e.name = 'SMapError';
|
|
56
|
+
e.status = status || 0;
|
|
57
|
+
e.url = url || '';
|
|
58
|
+
return e;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Object} options
|
|
63
|
+
* @param {string} options.baseUrl the public sMap address (e.g. https://tile.srsr.ca)
|
|
64
|
+
* @param {string} [options.token] a map token; or
|
|
65
|
+
* @param {function(): (string|Promise<string>)} [options.getToken] called at start and before each token expires
|
|
66
|
+
* @param {string} [options.apiKey] a map key (`smk_…`); with a token both are sent (token first)
|
|
67
|
+
* @param {string[]} [options.nodes] node URLs known in advance (used while baseUrl is down)
|
|
68
|
+
* @param {boolean} [options.nearest=true] read from the nearest node
|
|
69
|
+
* @param {function} [options.fetch] a fetch implementation (default: the global one)
|
|
70
|
+
* @param {number} [options.timeoutMs=15000] per request
|
|
71
|
+
*/
|
|
72
|
+
function SMapClient(options) {
|
|
73
|
+
if (!(this instanceof SMapClient)) { return new SMapClient(options); }
|
|
74
|
+
options = options || {};
|
|
75
|
+
if (!options.baseUrl) { throw SMapError('sMap: baseUrl is required'); }
|
|
76
|
+
this.baseUrl = String(options.baseUrl).replace(/\/$/, '');
|
|
77
|
+
this._fetch = options.fetch || (typeof fetch === 'function' ? fetch.bind(typeof self !== 'undefined' ? self : globalThis) : null);
|
|
78
|
+
if (!this._fetch) { throw SMapError('sMap: no fetch available; pass options.fetch'); }
|
|
79
|
+
this._token = options.token || null;
|
|
80
|
+
this._getToken = options.getToken || null;
|
|
81
|
+
this._apiKey = options.apiKey || null;
|
|
82
|
+
this._nearest = options.nearest !== false;
|
|
83
|
+
this._seeds = (options.nodes || []).map(function (u) { return String(u).replace(/\/$/, ''); });
|
|
84
|
+
this._timeout = options.timeoutMs || 15000;
|
|
85
|
+
this._nodes = null; // ordered: nearest first
|
|
86
|
+
this._picking = null;
|
|
87
|
+
this._tokening = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
SMapClient.version = VERSION;
|
|
91
|
+
SMapClient.tokenExpiry = tokenExpiry;
|
|
92
|
+
|
|
93
|
+
SMapClient.prototype._timed = function (url, init, ms) {
|
|
94
|
+
var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
95
|
+
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
|
|
96
|
+
init = init || {};
|
|
97
|
+
if (ctrl) { init.signal = ctrl.signal; }
|
|
98
|
+
return this._fetch(url, init).then(function (res) { clearTimeout(timer); return res; }, function (e) { clearTimeout(timer); throw e; });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** The token to send now, asking getToken again two minutes before it expires. */
|
|
102
|
+
SMapClient.prototype.token = function () {
|
|
103
|
+
var self = this;
|
|
104
|
+
var exp = this._token ? tokenExpiry(this._token) : 0;
|
|
105
|
+
var fresh = this._token && (!exp || exp - Date.now() / 1000 > 120);
|
|
106
|
+
if (fresh || !this._getToken) { return Promise.resolve(this._token); }
|
|
107
|
+
if (!this._tokening) {
|
|
108
|
+
this._tokening = Promise.resolve(this._getToken()).then(function (t) {
|
|
109
|
+
self._token = t;
|
|
110
|
+
self._tokening = null;
|
|
111
|
+
return t;
|
|
112
|
+
}, function (e) { self._tokening = null; throw e; });
|
|
113
|
+
}
|
|
114
|
+
return this._tokening;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The nodes in the order they are tried: nearest first, then the others; baseUrl alone when nearest is off or the
|
|
119
|
+
* list cannot be read.
|
|
120
|
+
*
|
|
121
|
+
* @returns {Promise<string[]>}
|
|
122
|
+
*/
|
|
123
|
+
SMapClient.prototype.nodes = function () {
|
|
124
|
+
var self = this;
|
|
125
|
+
if (this._nodes) { return Promise.resolve(this._nodes); }
|
|
126
|
+
if (!this._nearest) { this._nodes = [this.baseUrl]; return Promise.resolve(this._nodes); }
|
|
127
|
+
if (this._picking) { return this._picking; }
|
|
128
|
+
var key = 'smap.nodes:' + this.baseUrl;
|
|
129
|
+
var storage = store();
|
|
130
|
+
var saved = null;
|
|
131
|
+
try { saved = JSON.parse(storage.getItem(key) || 'null'); } catch (e) { saved = null; }
|
|
132
|
+
if (saved && saved.urls && saved.urls.length && Date.now() - saved.at < NODE_TTL) {
|
|
133
|
+
this._nodes = saved.urls;
|
|
134
|
+
return Promise.resolve(this._nodes);
|
|
135
|
+
}
|
|
136
|
+
// Every node this client knows of: the kept list (30 days), the seed list, then baseUrl.
|
|
137
|
+
var kept = saved && saved.known && Date.now() - (saved.knownAt || 0) < KNOWN_TTL ? saved.known : [];
|
|
138
|
+
var known = unique([this.baseUrl].concat(this._seeds, kept));
|
|
139
|
+
this._picking = this._discover(known)
|
|
140
|
+
.then(function (list) {
|
|
141
|
+
var urls = list || unique(self._seeds.concat(kept));
|
|
142
|
+
if (urls.length === 0) { return { order: [self.baseUrl], known: null }; }
|
|
143
|
+
return self._measure(urls).then(function (order) { return { order: order, known: list }; });
|
|
144
|
+
})
|
|
145
|
+
.then(function (r) {
|
|
146
|
+
self._nodes = r.order;
|
|
147
|
+
self._picking = null;
|
|
148
|
+
// An order is remembered for the hour only when a node list was read; a guess made while nothing answered
|
|
149
|
+
// is tried again by the next client instead of being kept.
|
|
150
|
+
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 };
|
|
151
|
+
try { storage.setItem(key, JSON.stringify(record)); } catch (e) { /* ignore */ }
|
|
152
|
+
return r.order;
|
|
153
|
+
});
|
|
154
|
+
return this._picking;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Asks each node in turn for the node list (any node answers the whole list); null when none answers. */
|
|
158
|
+
SMapClient.prototype._discover = function (candidates) {
|
|
159
|
+
var self = this;
|
|
160
|
+
function ask(i) {
|
|
161
|
+
if (i >= candidates.length) { return Promise.resolve(null); }
|
|
162
|
+
return self._timed(candidates[i] + '/v1/nodes', {}, 2000)
|
|
163
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
164
|
+
.then(function (body) {
|
|
165
|
+
var urls = ((body && body.nodes) || []).map(function (n) { return String(n.url).replace(/\/$/, ''); });
|
|
166
|
+
return urls.length ? urls : ask(i + 1);
|
|
167
|
+
}, function () { return ask(i + 1); });
|
|
168
|
+
}
|
|
169
|
+
return ask(0);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** Times one small request to each node: fastest first, silent ones after, baseUrl last when not listed. */
|
|
173
|
+
SMapClient.prototype._measure = function (urls) {
|
|
174
|
+
var self = this;
|
|
175
|
+
return Promise.all(urls.map(function (url) {
|
|
176
|
+
var t0 = Date.now();
|
|
177
|
+
return self._timed(url + '/v1/ping', { cache: 'no-store' }, 1500)
|
|
178
|
+
.then(function (res) { return { url: url, ms: res.ok ? Date.now() - t0 : Infinity }; }, function () { return { url: url, ms: Infinity }; });
|
|
179
|
+
})).then(function (timed) {
|
|
180
|
+
timed.sort(function (a, b) { return a.ms - b.ms; });
|
|
181
|
+
var order = timed.map(function (t) { return t.url; });
|
|
182
|
+
if (order.indexOf(self.baseUrl) < 0) { order.push(self.baseUrl); }
|
|
183
|
+
return order;
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Forgets the chosen node order, so the next request measures again. */
|
|
188
|
+
SMapClient.prototype.resetNodes = function () {
|
|
189
|
+
this._nodes = null;
|
|
190
|
+
var key = 'smap.nodes:' + this.baseUrl;
|
|
191
|
+
try {
|
|
192
|
+
var saved = JSON.parse(store().getItem(key) || 'null');
|
|
193
|
+
// The kept node list survives a reset; only the measured order is forgotten.
|
|
194
|
+
store().setItem(key, JSON.stringify(saved ? { urls: [], at: 0, known: saved.known, knownAt: saved.knownAt } : null));
|
|
195
|
+
} catch (e) { /* ignore */ }
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* GET `path` from the nearest node that answers, with the token; a node that fails (network error, 5xx) is moved
|
|
200
|
+
* to the end and the next one is tried. 4xx answers are final.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} path starting with /
|
|
203
|
+
* @param {Object} [headers]
|
|
204
|
+
* @returns {Promise<Response>}
|
|
205
|
+
*/
|
|
206
|
+
SMapClient.prototype.request = function (path, headers) {
|
|
207
|
+
var self = this;
|
|
208
|
+
return Promise.all([this.nodes(), this.token()]).then(function (r) {
|
|
209
|
+
var nodes = r[0].slice();
|
|
210
|
+
var token = r[1];
|
|
211
|
+
var last = null;
|
|
212
|
+
function attempt(i) {
|
|
213
|
+
if (i >= nodes.length) { throw last || SMapError('sMap: no node answered', 0, path); }
|
|
214
|
+
var h = {};
|
|
215
|
+
for (var k in (headers || {})) { if (Object.prototype.hasOwnProperty.call(headers, k)) { h[k] = headers[k]; } }
|
|
216
|
+
if (token) { h.Authorization = 'Bearer ' + token; }
|
|
217
|
+
if (self._apiKey) { h['X-SMap-Key'] = self._apiKey; }
|
|
218
|
+
var url = nodes[i] + path;
|
|
219
|
+
return self._timed(url, { headers: h }, self._timeout).then(function (res) {
|
|
220
|
+
if (res.status >= 500) {
|
|
221
|
+
last = SMapError('sMap: HTTP ' + res.status, res.status, url);
|
|
222
|
+
self._demote(nodes[i]);
|
|
223
|
+
return attempt(i + 1);
|
|
224
|
+
}
|
|
225
|
+
if (!res.ok && res.status !== 206) {
|
|
226
|
+
return res.text().then(function (text) { throw SMapError('sMap: ' + (text || ('HTTP ' + res.status)), res.status, url); });
|
|
227
|
+
}
|
|
228
|
+
return res;
|
|
229
|
+
}, function (e) {
|
|
230
|
+
last = SMapError('sMap: ' + (e && e.message ? e.message : 'network error'), 0, url);
|
|
231
|
+
self._demote(nodes[i]);
|
|
232
|
+
return attempt(i + 1);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return attempt(0);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
SMapClient.prototype._demote = function (url) {
|
|
240
|
+
if (!this._nodes || this._nodes.length < 2) { return; }
|
|
241
|
+
var i = this._nodes.indexOf(url);
|
|
242
|
+
if (i >= 0) { this._nodes.splice(i, 1); this._nodes.push(url); }
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function bytes(res) { return res.arrayBuffer().then(function (b) { return new Uint8Array(b); }); }
|
|
246
|
+
|
|
247
|
+
// A map key also goes in the query of styles and TileJSON: the service carries it into their tile URLs, so a
|
|
248
|
+
// renderer given the document reads its tiles with the key.
|
|
249
|
+
SMapClient.prototype._withKey = function (path) {
|
|
250
|
+
if (!this._apiKey) { return path; }
|
|
251
|
+
return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'key=' + encodeURIComponent(this._apiKey);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A style document ('light', 'white', 'grayscale', 'dark', 'black') with labels in `language`.
|
|
256
|
+
* Its tile URLs point at the node that answered.
|
|
257
|
+
*/
|
|
258
|
+
SMapClient.prototype.style = function (flavor, language) {
|
|
259
|
+
var q = language ? '?lang=' + encodeURIComponent(language) : '';
|
|
260
|
+
return this.request(this._withKey('/v1/styles/' + encodeURIComponent(flavor || 'light') + q)).then(function (r) { return r.json(); });
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** TileJSON of a source (bounds, zooms, layers, tile URL template). */
|
|
264
|
+
SMapClient.prototype.tileJSON = function (source) {
|
|
265
|
+
return this.request(this._withKey('/v1/tiles/' + encodeURIComponent(source || 'basemap'))).then(function (r) { return r.json(); });
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** One vector tile as bytes; null where the source has no tile (204 / 404). */
|
|
269
|
+
SMapClient.prototype.tile = function (source, z, x, y) {
|
|
270
|
+
var path = '/v1/tiles/' + encodeURIComponent(source) + '/' + (z | 0) + '/' + (x | 0) + '/' + (y | 0);
|
|
271
|
+
return this.request(path).then(function (r) { return r.status === 204 ? null : bytes(r); }, function (e) {
|
|
272
|
+
if (e && e.status === 404) { return null; }
|
|
273
|
+
throw e;
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** Raw bytes `[start, end]` (inclusive) of a source's archive, where your access allows raw reads. */
|
|
278
|
+
SMapClient.prototype.range = function (source, start, end) {
|
|
279
|
+
return this.request('/' + encodeURIComponent(source) + '.pmtiles', { Range: 'bytes=' + start + '-' + end }).then(bytes);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/** A font glyph range (`0-255.pbf` style), for renderers that load fonts themselves. */
|
|
283
|
+
SMapClient.prototype.glyphs = function (fontstack, range) {
|
|
284
|
+
return this.request('/v1/fonts/' + encodeURIComponent(fontstack) + '/' + range + '.pbf').then(bytes);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Headers and URL rewriting for a renderer that fetches by itself: returns the URL unchanged plus the
|
|
289
|
+
* Authorization (and map key) headers to add for sMap URLs; none for any other host.
|
|
290
|
+
*/
|
|
291
|
+
SMapClient.prototype.authorize = function (url) {
|
|
292
|
+
var self = this;
|
|
293
|
+
return this.token().then(function (token) {
|
|
294
|
+
var ours = url.indexOf(self.baseUrl) === 0 || self._seeds.concat(self._nodes || []).some(function (n) { return url.indexOf(n) === 0; });
|
|
295
|
+
var headers = {};
|
|
296
|
+
if (ours && token) { headers.Authorization = 'Bearer ' + token; }
|
|
297
|
+
if (ours && self._apiKey) { headers['X-SMap-Key'] = self._apiKey; }
|
|
298
|
+
return { url: url, headers: headers };
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return SMapClient;
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
}).call(typeof globalThis !== 'undefined' ? globalThis : undefined, smapClientModule, smapClientModule.exports, undefined);
|
|
306
|
+
var SMapClient = smapClientModule.exports;
|
|
307
|
+
export { SMapClient };
|
|
308
|
+
export default SMapClient;
|