@superrouteca/smap-mapbox 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.
@@ -0,0 +1,166 @@
1
+ // sMap tool adapters — an sMap map (the Web SDK) drawn inside another tool. The tool owns the camera and the input;
2
+ // the embedded map draws the basemap and sMap's layers. Its own camera moves (Auto Zoom, fitLayer, flyTo, a popup
3
+ // panning into view) are handed to the tool, and the tool's clicks and pointer moves are handed to it, so popups,
4
+ // Auto Zoom, hover and onClick work as on an sMap map. Kept identical in leaflet/, openlayers/ and mapbox/
5
+ // (test/common.test.mjs checks).
6
+ import { injectCss, mergeAttributions } from './badge.js';
7
+
8
+ /** The engine's camera calls that are sent to the tool instead of moving the embedded map on its own. */
9
+ export const CAMERA_CALLS = ['jumpTo', 'easeTo', 'flyTo', 'fitBounds', 'panBy', 'panTo', 'setCenter', 'setZoom', 'zoomTo',
10
+ 'zoomIn', 'zoomOut', 'setBearing', 'rotateTo', 'setPitch'];
11
+ const INSTANT = ['jumpTo', 'setCenter', 'setZoom', 'setBearing', 'setPitch'];
12
+
13
+ /** The SDK options the embedding sets itself. */
14
+ const OWN_OPTIONS = { interactive: false, attributionControl: false, keyboard3D: false };
15
+
16
+ function lngLatOf(engineModule, v) {
17
+ if (v == null) { return null; }
18
+ if (engineModule && engineModule.LngLat && engineModule.LngLat.convert) {
19
+ const c = engineModule.LngLat.convert(v);
20
+ return { lng: c.lng, lat: c.lat };
21
+ }
22
+ if (Array.isArray(v)) { return { lng: +v[0], lat: +v[1] }; }
23
+ return { lng: +(v.lng !== undefined ? v.lng : v.lon), lat: +v.lat };
24
+ }
25
+
26
+ /**
27
+ * Where a camera call would take the engine: { center: {lng, lat}, zoom, bearing, pitch } and how to get there
28
+ * ({ animate, duration, fly }). Pure over the engine's read-only calls (getCenter, project, cameraForBounds, …).
29
+ */
30
+ export function cameraTarget(engine, engineModule, call, args) {
31
+ const c = engine.getCenter();
32
+ const t = { center: { lng: c.lng, lat: c.lat }, zoom: engine.getZoom(), bearing: engine.getBearing ? engine.getBearing() : 0, pitch: engine.getPitch ? engine.getPitch() : 0 };
33
+ let o = {};
34
+ const take = function (cam) {
35
+ if (!cam) { return; }
36
+ if (cam.center !== undefined) { t.center = lngLatOf(engineModule, cam.center); }
37
+ if (typeof cam.zoom === 'number') { t.zoom = cam.zoom; }
38
+ if (typeof cam.bearing === 'number') { t.bearing = cam.bearing; }
39
+ if (typeof cam.pitch === 'number') { t.pitch = cam.pitch; }
40
+ };
41
+ switch (call) {
42
+ case 'jumpTo': case 'easeTo': case 'flyTo': o = args[0] || {}; take(o); break;
43
+ case 'fitBounds': o = args[1] || {}; take(engine.cameraForBounds ? engine.cameraForBounds(args[0], o) : null); break;
44
+ case 'panBy': {
45
+ o = args[1] || {};
46
+ const off = Array.isArray(args[0]) ? args[0] : [args[0].x, args[0].y];
47
+ const p = engine.project([t.center.lng, t.center.lat]);
48
+ take({ center: engine.unproject([p.x + off[0], p.y + off[1]]) });
49
+ break;
50
+ }
51
+ case 'panTo': o = args[1] || {}; take({ center: args[0] }); break;
52
+ case 'setCenter': take({ center: args[0] }); break;
53
+ case 'setZoom': take({ zoom: args[0] }); break;
54
+ case 'zoomTo': o = args[1] || {}; take({ zoom: args[0] }); break;
55
+ case 'zoomIn': o = args[0] || {}; t.zoom += 1; break;
56
+ case 'zoomOut': o = args[0] || {}; t.zoom -= 1; break;
57
+ case 'setBearing': take({ bearing: args[0] }); break;
58
+ case 'rotateTo': o = args[1] || {}; take({ bearing: args[0] }); break;
59
+ case 'setPitch': take({ pitch: args[0] }); break;
60
+ default: break;
61
+ }
62
+ const animate = INSTANT.indexOf(call) < 0 && o.animate !== false && o.duration !== 0;
63
+ return { target: t, how: { animate: animate, duration: typeof o.duration === 'number' ? o.duration : undefined, fly: call === 'flyTo' } };
64
+ }
65
+
66
+ /**
67
+ * Creates the embedded sMap map.
68
+ *
69
+ * @param {Object} sdk the sMap Web SDK module (`Map`, `getEngine`)
70
+ * @param {HTMLElement} el the element it draws in (sized and placed by the caller)
71
+ * @param {Object} options sMap map options (style, language, apiKey, token, getToken, baseUrl, …)
72
+ * @param {Object} hooks move(target, how): move the tool's camera; attribution(list): the credits changed
73
+ */
74
+ export function createEmbedded(sdk, el, options, hooks) {
75
+ injectCss(el.ownerDocument);
76
+ el.classList.add('smap-embedded');
77
+ const map = new sdk.Map(Object.assign({ minZoom: -2, maxZoom: 24 }, options, OWN_OPTIONS, { container: el }));
78
+ const engineModule = typeof sdk.getEngine === 'function' ? sdk.getEngine() : null;
79
+ const state = { jumpTo: null, engine: null, credits: [], removed: false };
80
+
81
+ function credits() {
82
+ const engine = state.engine;
83
+ const list = [];
84
+ try {
85
+ const caches = engine.style && (engine.style.sourceCaches || engine.style._sourceCaches);
86
+ const ids = caches ? Object.keys(caches) : Object.keys((engine.getStyle() || {}).sources || {});
87
+ ids.forEach(function (id) {
88
+ const s = engine.getSource(id);
89
+ if (s && typeof s.attribution === 'string') { list.push(s.attribution); }
90
+ });
91
+ } catch (e) { /* no style yet */ }
92
+ const merged = mergeAttributions(list);
93
+ if (merged.join('\n') !== state.credits.join('\n')) {
94
+ state.credits = merged;
95
+ if (hooks.attribution) { hooks.attribution(merged.slice()); }
96
+ }
97
+ }
98
+
99
+ function adopt(engine) {
100
+ if (state.engine || !engine) { return; }
101
+ state.engine = engine;
102
+ state.jumpTo = engine.jumpTo.bind(engine);
103
+ CAMERA_CALLS.forEach(function (call) {
104
+ if (typeof engine[call] !== 'function') { return; }
105
+ engine[call] = function () {
106
+ const r = cameraTarget(engine, engineModule, call, Array.prototype.slice.call(arguments));
107
+ if (hooks.move) { hooks.move(r.target, r.how); }
108
+ return engine;
109
+ };
110
+ });
111
+ if (engine.on) {
112
+ engine.on('sourcedata', function (e) { if (!e || e.sourceDataType === 'metadata' || e.isSourceLoaded) { credits(); } });
113
+ engine.on('styledata', credits);
114
+ }
115
+ credits();
116
+ if (hooks.adopted) { hooks.adopted(engine); }
117
+ }
118
+
119
+ const ready = map.ready.then(function (m) { adopt(m.engine); return m; });
120
+
121
+ const api = {
122
+ map: map,
123
+ ready: ready,
124
+ /** The engine once created (null before). */
125
+ engine: function () { return state.engine || (map.engine && (adopt(map.engine), state.engine)) || null; },
126
+ /** Puts the embedded camera where the tool's is (zoom in the engine's 512 px scale). */
127
+ sync: function (camera) {
128
+ const engine = api.engine();
129
+ if (!engine || state.removed) { return; }
130
+ state.jumpTo({ center: [camera.center.lng, camera.center.lat], zoom: camera.zoom, bearing: camera.bearing || 0, pitch: camera.pitch || 0 });
131
+ if (camera.redraw && typeof engine.redraw === 'function') { engine.redraw(); }
132
+ },
133
+ resize: function () { const engine = api.engine(); if (engine && engine.resize) { engine.resize(); } },
134
+ /**
135
+ * Hands one of the tool's pointer events to the embedded map (click, dblclick, mousemove, mouseout, contextmenu):
136
+ * `x`, `y` in pixels of the embedded map's element, the position, and the browser event.
137
+ */
138
+ forward: function (type, x, y, lngLat, originalEvent) {
139
+ const engine = api.engine();
140
+ if (!engine || state.removed) { return; }
141
+ const point = engineModule && engineModule.Point ? new engineModule.Point(x, y) : { x: x, y: y };
142
+ const ll = engineModule && engineModule.LngLat ? new engineModule.LngLat(lngLat.lng, lngLat.lat) : lngLat;
143
+ engine.fire(type, { point: point, lngLat: ll, originalEvent: originalEvent, type: type });
144
+ // The tool's element shows the pointer the embedded map asks for (a hand over a clickable feature).
145
+ if (type === 'mousemove' && hooks.cursor && engine.getCanvas) { hooks.cursor(engine.getCanvas().style.cursor || ''); }
146
+ },
147
+ /** True when the browser event happened inside one of the embedded map's popups (the popup handles it). */
148
+ inPopup: function (originalEvent) {
149
+ const t = originalEvent && originalEvent.target;
150
+ return !!(t && typeof t.closest === 'function' && t.closest('.maplibregl-popup'));
151
+ },
152
+ credits: function () { return state.credits.slice(); },
153
+ remove: function () { state.removed = true; map.remove(); },
154
+ };
155
+ return api;
156
+ }
157
+
158
+ /** True where the browser can draw the vector map (WebGL 2). */
159
+ export function hasWebGL2(doc) {
160
+ try {
161
+ const d = doc || (typeof document !== 'undefined' ? document : null);
162
+ if (!d) { return false; }
163
+ const c = d.createElement('canvas');
164
+ return !!(c.getContext && c.getContext('webgl2'));
165
+ } catch (e) { return false; }
166
+ }
package/src/index.js ADDED
@@ -0,0 +1,475 @@
1
+ // @superrouteca/smap-mapbox — sMap in Mapbox GL JS: sMap's style drawn by Mapbox GL JS itself (schemes, custom
2
+ // styles, labels in any language, basemap overrides, 3D buildings), map key or token with renewal on every request,
3
+ // the nearest node with failover, the credits and the sMap badge; sMap's pins, lines and areas (popups, Auto Zoom)
4
+ // on a layer above, drawn by the sMap Web SDK and following Mapbox GL JS' camera. Mapbox GL JS styles and layers on an
5
+ // sMap map are in './on-smap.js' (`@superrouteca/smap-mapbox/on-smap`).
6
+ //
7
+ // Mapbox GL JS is passed in (`new SMapMapbox(mapboxgl, options)`), so this package never bundles or imports it.
8
+ import * as sMapModule from '@superrouteca/smap';
9
+ import { SMapAccess, checkStyle, rasterPath, stylePath, FLAVORS, DEFAULT_BASE } from './common/access.js';
10
+ import { OSM_ATTRIBUTION, createBadge, injectCss } from './common/badge.js';
11
+ import { createEmbedded } from './common/embed.js';
12
+
13
+ const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
14
+
15
+ export const version = '1.1.0';
16
+ export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION };
17
+
18
+ const ACCESS_OPTIONS = ['baseUrl', 'token', 'getToken', 'apiKey', 'nodes', 'nearest', 'fetch', 'timeoutMs', 'client', 'access'];
19
+ const OWN_OPTIONS = ['style', 'language', 'basemap', 'buildings3D', 'sdk', 'popup', 'popupLoading', 'autoZoom', 'overlay'];
20
+ const BUILDINGS_LAYER = 'smap-buildings-3d';
21
+ const BASEMAP_SOURCE = 'basemap';
22
+ /** Mapbox GL JS v3 slots, as sMap slots. */
23
+ const MAPBOX_SLOTS = { bottom: 'bottom', middle: 'labels', top: 'top' };
24
+
25
+ function pick(o, keys, inside) {
26
+ const out = {};
27
+ Object.keys(o || {}).forEach(function (k) { if ((keys.indexOf(k) >= 0) === inside) { out[k] = o[k]; } });
28
+ return out;
29
+ }
30
+
31
+ function sdkCall(sdk, name) {
32
+ if (typeof sdk[name] !== 'function') { throw new Error('sMap: the adapter needs @superrouteca/smap 1.1.0 or later (' + name + ')'); }
33
+ return sdk[name];
34
+ }
35
+
36
+ /**
37
+ * A Mapbox GL JS `transformRequest` for sMap: sMap URLs go to the node requests go to now and carry the token (renewed
38
+ * through getToken) and the map key; every other URL is passed on unchanged (or to your own `next(url, type)`).
39
+ */
40
+ export function smapTransformRequest(access, next) {
41
+ return function (url, resourceType) {
42
+ if (!access.isOurs(url)) { return next ? next(url, resourceType) : { url: url }; }
43
+ const to = access.rewrite(url);
44
+ return { url: to, headers: access.headers(to) };
45
+ };
46
+ }
47
+
48
+ /** The sMap badge as a Mapbox GL JS control. */
49
+ export class SMapBadgeControl {
50
+ onAdd(map) {
51
+ this._el = createBadge(map.getContainer().ownerDocument);
52
+ this._el.classList.add('mapboxgl-ctrl');
53
+ this._el.style.clear = 'both';
54
+ return this._el;
55
+ }
56
+ onRemove() { if (this._el && this._el.parentNode) { this._el.parentNode.removeChild(this._el); } this._el = null; }
57
+ }
58
+
59
+ /** The layer ids of a style that come from its basemap (all of them, for a style document of sMap's). */
60
+ function idsOf(doc) { return ((doc && doc.layers) || []).map(function (l) { return l.id; }); }
61
+
62
+ /**
63
+ * What is the app's in a live style: the sources and layers not in the basemap document, each layer with the basemap
64
+ * layer it stood under (to put it back at the same place in a new basemap).
65
+ */
66
+ export function userPart(style, basemapDoc, extraBasemapLayers) {
67
+ const baseSources = Object.keys((basemapDoc && basemapDoc.sources) || {});
68
+ const baseLayers = idsOf(basemapDoc).concat(extraBasemapLayers || []);
69
+ const sources = {};
70
+ Object.keys((style && style.sources) || {}).forEach(function (id) { if (baseSources.indexOf(id) < 0) { sources[id] = style.sources[id]; } });
71
+ const layers = [];
72
+ const list = (style && style.layers) || [];
73
+ list.forEach(function (l, i) {
74
+ if (baseLayers.indexOf(l.id) >= 0) { return; }
75
+ let before = null;
76
+ for (let j = i + 1; j < list.length; j++) { if (baseLayers.indexOf(list[j].id) >= 0 && (extraBasemapLayers || []).indexOf(list[j].id) < 0) { before = list[j].id; break; } }
77
+ layers.push({ layer: l, before: before });
78
+ });
79
+ return { sources: sources, layers: layers };
80
+ }
81
+
82
+ /** A basemap document with the app's sources and layers put back in (each under the basemap layer it stood under). */
83
+ export function mergeUserPart(doc, part) {
84
+ const out = Object.assign({}, doc, { sources: Object.assign({}, doc.sources, part.sources), layers: (doc.layers || []).slice() });
85
+ part.layers.forEach(function (x) {
86
+ const at = x.before ? out.layers.findIndex(function (l) { return l.id === x.before; }) : -1;
87
+ if (at >= 0) { out.layers.splice(at, 0, x.layer); } else { out.layers.push(x.layer); }
88
+ });
89
+ return out;
90
+ }
91
+
92
+ /** The style's sMap URLs moved onto the node requests go to now (sources, glyphs, sprite). */
93
+ function onNode(doc, access) {
94
+ const out = JSON.parse(JSON.stringify(doc));
95
+ const move = function (u) { return typeof u === 'string' ? access.rewrite(u) : u; };
96
+ if (out.glyphs) { out.glyphs = move(out.glyphs); }
97
+ if (typeof out.sprite === 'string') { out.sprite = move(out.sprite); }
98
+ Object.keys(out.sources || {}).forEach(function (id) {
99
+ const s = out.sources[id];
100
+ if (s.url) { s.url = move(s.url); }
101
+ if (Array.isArray(s.tiles)) { s.tiles = s.tiles.map(move); }
102
+ });
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * sMap in Mapbox GL JS.
108
+ *
109
+ * const smap = new SMapMapbox(mapboxgl, { container: 'map', apiKey: 'smk_…', style: 'light', language: 'en' });
110
+ * await smap.ready; // smap.map is the Mapbox GL JS map
111
+ *
112
+ * Options: the access options (apiKey, token, getToken, baseUrl, nodes, nearest), `style` (a scheme or
113
+ * `custom:<id>`), `language`, `basemap` (class overrides), `buildings3D` (true or { color, opacity }), `popup` /
114
+ * `popupLoading` / `autoZoom` for sMap's layers, and every Mapbox GL JS map option (container, center, zoom, pitch,
115
+ * bearing, …; `projection` is Web Mercator unless given). `map` instead of `container` attaches to a Mapbox GL JS map
116
+ * you made (see attach()).
117
+ */
118
+ export class SMapMapbox {
119
+ constructor(mapboxgl, options) {
120
+ options = options || {};
121
+ if (!mapboxgl || (!mapboxgl.Map && !options.map)) { throw new Error('sMap: pass the Mapbox GL JS module: new SMapMapbox(mapboxgl, options)'); }
122
+ this.mapboxgl = mapboxgl;
123
+ this.access = options.access || new SMapAccess(pick(options, ACCESS_OPTIONS, true));
124
+ this._sdk = options.sdk || SDK;
125
+ this._style = checkStyle(options.style || 'light');
126
+ this._language = options.language || (typeof navigator !== 'undefined' && navigator.language) || 'en';
127
+ this._overrides = options.basemap || {};
128
+ this._buildings = options.buildings3D ? (options.buildings3D === true ? {} : options.buildings3D) : null;
129
+ this._overlayOptions = pick(options, ['popup', 'popupLoading', 'autoZoom'], true);
130
+ this._doc = null; // the basemap document as served (before overrides)
131
+ this._shown = null; // the basemap document as shown (overrides applied)
132
+ this._overlay = null;
133
+ this._queue = [];
134
+ this.map = null;
135
+ const self = this;
136
+ const mapOptions = pick(options, ACCESS_OPTIONS.concat(OWN_OPTIONS).concat(['map']), false);
137
+ this.ready = this.access.start()
138
+ .then(function () { return self._fetchDoc(); })
139
+ .then(function () {
140
+ if (options.map) { self._attachTo(options.map, options); } else { self._create(mapOptions); }
141
+ return new Promise(function (resolve) {
142
+ const done = function () { resolve(self); };
143
+ if (self.map.isStyleLoaded && self.map.isStyleLoaded()) { done(); } else { self.map.once('style.load', done); }
144
+ });
145
+ });
146
+ }
147
+
148
+ /** Attaches to a Mapbox GL JS map you made: its basemap becomes sMap's and its own layers stay (see switchBasemap). */
149
+ static attach(mapboxgl, map, options) { return new SMapMapbox(mapboxgl, Object.assign({}, options, { map: map })); }
150
+
151
+ _fetchDoc() {
152
+ const self = this;
153
+ return this.access.style(this._style, this._language).then(function (doc) { self._doc = doc; return doc; });
154
+ }
155
+
156
+ _shownDoc() {
157
+ const apply = sdkCall(this._sdk, 'applyBasemapOverrides');
158
+ const doc = onNode(apply(this._doc, this._overrides), this.access);
159
+ // 3D buildings are part of the basemap as shown, under its labels.
160
+ if (this._buildings && doc.sources && doc.sources[BASEMAP_SOURCE]) {
161
+ const anchors = typeof this._sdk.slotAnchors === 'function' ? this._sdk.slotAnchors(idsOf(doc)) : {};
162
+ const at = anchors.labels ? doc.layers.findIndex(function (l) { return l.id === anchors.labels; }) : -1;
163
+ doc.layers.splice(at >= 0 ? at : doc.layers.length, 0, this._buildingsLayer());
164
+ }
165
+ this._shown = doc;
166
+ return doc;
167
+ }
168
+
169
+ _create(mapOptions) {
170
+ const o = Object.assign({ projection: 'mercator' }, mapOptions, {
171
+ style: this._shownDoc(),
172
+ transformRequest: smapTransformRequest(this.access, mapOptions.transformRequest),
173
+ });
174
+ this.map = new this.mapboxgl.Map(o);
175
+ this._wire();
176
+ }
177
+
178
+ _attachTo(map, options) {
179
+ this.map = map;
180
+ // Without this adapter's transformRequest (it can only be given when the map is made) the map key reaches the
181
+ // tiles through the style's URLs; a token cannot.
182
+ if ((options.token || options.getToken) && typeof console !== 'undefined') {
183
+ console.warn('sMap: a map made without smapTransformRequest() cannot send tokens; make it with transformRequest: smapTransformRequest(access) or use a map key.');
184
+ }
185
+ const keep = options.keep;
186
+ const style = map.getStyle() || { sources: {}, layers: [] };
187
+ const part = userPart(style, { sources: {}, layers: [] });
188
+ part.sources = pick(part.sources, Object.keys(part.sources).filter(function (id) { return !isMapboxSource(style.sources[id]); }), true);
189
+ part.layers = part.layers.filter(function (x) {
190
+ if (keep) { return keep(x.layer); }
191
+ if (!x.layer.source) { return false; } // background, sky: the old basemap's
192
+ return typeof x.layer.source === 'object' || !!part.sources[x.layer.source];
193
+ }).map(function (x) { return { layer: x.layer, before: null }; });
194
+ map.setStyle(mergeUserPart(this._shownDoc(), part));
195
+ this._wire();
196
+ }
197
+
198
+ _wire() {
199
+ const map = this.map;
200
+ const self = this;
201
+ if (map.addControl) { this._badge = new SMapBadgeControl(); map.addControl(this._badge, 'bottom-right'); }
202
+ // A tile or style request to a node that failed: that node is tried last from now on.
203
+ map.on('error', function (e) {
204
+ const err = e && e.error;
205
+ const url = err && (err.url || (err.message && (/https?:\/\/\S+/.exec(err.message) || [])[0]));
206
+ const status = err && err.status;
207
+ if (url && self.access.isOurs(url) && (!status || status >= 500)) { self.access.failed(url); }
208
+ });
209
+ ['move', 'resize'].forEach(function (type) { map.on(type, function () { self._syncOverlay(type); }); });
210
+ ['click', 'dblclick', 'contextmenu', 'mousemove', 'mouseout'].forEach(function (type) {
211
+ map.on(type, function (e) {
212
+ if (!self._overlay || !e || !e.point) { return; }
213
+ if (self._overlay.inPopup(e.originalEvent)) { return; }
214
+ self._overlay.forward(type, e.point.x, e.point.y, { lng: e.lngLat.lng, lat: e.lngLat.lat }, e.originalEvent);
215
+ });
216
+ });
217
+ }
218
+
219
+ /** Sets the basemap again, keeping the app's sources and layers where they were. */
220
+ _restyle() {
221
+ const map = this.map;
222
+ const part = userPart(map.getStyle(), this._shown);
223
+ const doc = mergeUserPart(this._shownDoc(), part);
224
+ map.setStyle(doc, { diff: true });
225
+ }
226
+
227
+ _whenReady(fn) { const self = this; return this.ready.then(function () { return fn.call(self); }); }
228
+
229
+ /** Switches to a scheme or `custom:<id>`; the app's layers and sMap's layers stay. */
230
+ setStyle(style) {
231
+ this._style = checkStyle(style);
232
+ const self = this;
233
+ this._pending = this._whenReady(function () { return self._fetchDoc().then(function () { self._restyle(); }); });
234
+ return this;
235
+ }
236
+
237
+ /** Switches the label language; the app's layers and sMap's layers stay. */
238
+ setLanguage(language) {
239
+ this._language = language;
240
+ const self = this;
241
+ this._pending = this._whenReady(function () { return self._fetchDoc().then(function () { self._restyle(); }); });
242
+ return this;
243
+ }
244
+
245
+ /** Shows, hides and recolours the basemap by class (contract §1); replaces the previous overrides ({} clears). */
246
+ setBasemap(overrides) {
247
+ // Checked now (an unknown class or field throws here, not later).
248
+ sdkCall(this._sdk, 'applyBasemapOverrides')({ layers: [] }, overrides || {});
249
+ this._overrides = overrides || {};
250
+ const self = this;
251
+ this._pending = this._whenReady(function () { self._restyle(); });
252
+ return this;
253
+ }
254
+
255
+ /** The basemap overrides in force. */
256
+ basemap() { return JSON.parse(JSON.stringify(this._overrides)); }
257
+
258
+ /** 3D buildings from the basemap's buildings (Mapbox GL JS draws them; the map tilts to show them). */
259
+ setBuildings3D(on, options) {
260
+ this._buildings = on ? (options || {}) : null;
261
+ const self = this;
262
+ this._whenReady(function () {
263
+ self._restyle();
264
+ if (on && self.map.getPitch && self.map.getPitch() < 30) { self.map.easeTo({ pitch: 55, duration: 800 }); }
265
+ });
266
+ return this;
267
+ }
268
+
269
+ _buildingsLayer() {
270
+ const o = this._buildings || {};
271
+ const dark = this._style === 'dark' || this._style === 'black';
272
+ return {
273
+ id: BUILDINGS_LAYER,
274
+ type: 'fill-extrusion',
275
+ source: BASEMAP_SOURCE,
276
+ 'source-layer': 'buildings',
277
+ minzoom: 13,
278
+ paint: {
279
+ 'fill-extrusion-color': o.color || (dark ? '#3b4252' : '#d9dee7'),
280
+ 'fill-extrusion-height': ['coalesce', ['get', 'height'], 8],
281
+ 'fill-extrusion-base': ['coalesce', ['get', 'min_height'], 0],
282
+ 'fill-extrusion-opacity': o.opacity !== undefined ? o.opacity : 0.85,
283
+ },
284
+ };
285
+ }
286
+
287
+ /** Where a slot begins in the basemap now (a layer id to add before; undefined for `top`). */
288
+ slotLayer(slot) {
289
+ if (!slot) { return undefined; }
290
+ const anchors = sdkCall(this._sdk, 'slotAnchors')(idsOf(this._shown));
291
+ return anchors[slot];
292
+ }
293
+
294
+ /** Adds a Style Specification v8 source to the Mapbox GL JS map (kept across style and language changes). */
295
+ addSource(id, source) { const self = this; this._whenReady(function () { self.map.addSource(id, source); }); return this; }
296
+
297
+ /** Removes a source and the layers that read it. */
298
+ removeSource(id) {
299
+ const self = this;
300
+ this._whenReady(function () {
301
+ (self.map.getStyle().layers || []).forEach(function (l) { if (l.source === id) { self.map.removeLayer(l.id); } });
302
+ if (self.map.getSource(id)) { self.map.removeSource(id); }
303
+ });
304
+ return this;
305
+ }
306
+
307
+ /**
308
+ * Adds a Style Specification v8 layer to the Mapbox GL JS map as it is: under `before`, at a slot (`bottom`,
309
+ * `roads`, `labels`, `top`, or Mapbox's own `middle`), or on top when neither is given.
310
+ */
311
+ addStyleLayer(layer, placement) {
312
+ const p = placement || {};
313
+ const self = this;
314
+ this._whenReady(function () {
315
+ const slot = p.slot || (layer.slot && MAPBOX_SLOTS[layer.slot]);
316
+ const before = p.before || self.slotLayer(MAPBOX_SLOTS[slot] || slot);
317
+ const copy = Object.assign({}, layer);
318
+ delete copy.slot; // a Mapbox slot of the sMap basemap is resolved here, not by Mapbox GL JS
319
+ self.map.addLayer(copy, before && self.map.getLayer(before) ? before : undefined);
320
+ });
321
+ return this;
322
+ }
323
+
324
+ setPaint(id, name, value) { const self = this; this._whenReady(function () { self.map.setPaintProperty(id, name, value); }); return this; }
325
+ setLayout(id, name, value) { const self = this; this._whenReady(function () { self.map.setLayoutProperty(id, name, value); }); return this; }
326
+ setFilter(id, filter) { const self = this; this._whenReady(function () { self.map.setFilter(id, filter); }); return this; }
327
+
328
+ /** Raster tiles (XYZ templates or a TileJSON address) as a Mapbox GL JS raster source and layer. */
329
+ addRasterLayer(id, o) {
330
+ o = o || {};
331
+ const source = { type: 'raster', tileSize: o.tileSize || 256 };
332
+ if (o.tiles) { source.tiles = Array.isArray(o.tiles) ? o.tiles : [o.tiles]; }
333
+ else if (o.url && /\{z\}/.test(o.url)) { source.tiles = [o.url]; }
334
+ else if (o.url) { source.url = o.url; }
335
+ ['attribution', 'minzoom', 'maxzoom', 'bounds', 'scheme'].forEach(function (k) { if (o[k] !== undefined) { source[k] = o[k]; } });
336
+ const layer = { id: id, type: 'raster', source: id, paint: {} };
337
+ if (o.opacity !== undefined) { layer.paint['raster-opacity'] = o.opacity; }
338
+ return this.addSource(id, source).addStyleLayer(layer, { slot: o.slot, before: o.before });
339
+ }
340
+
341
+ /** Vector tiles (templates or a TileJSON address) drawn by your Style Specification layers. */
342
+ addVectorTiles(id, o) {
343
+ o = o || {};
344
+ const source = { type: 'vector' };
345
+ if (o.tiles) { source.tiles = Array.isArray(o.tiles) ? o.tiles : [o.tiles]; } else if (o.url) { source.url = o.url; }
346
+ ['attribution', 'minzoom', 'maxzoom', 'bounds', 'promoteId'].forEach(function (k) { if (o[k] !== undefined) { source[k] = o[k]; } });
347
+ this.addSource(id, source);
348
+ const self = this;
349
+ (o.layers || []).forEach(function (l) { self.addStyleLayer(Object.assign({ source: id }, l), { slot: o.slot, before: o.before }); });
350
+ return this;
351
+ }
352
+
353
+ // ---------------------------------------------------------------- sMap's own layers (the overlay)
354
+
355
+ /**
356
+ * The sMap map drawing sMap's own layers (pins, lines, areas, popups, Auto Zoom, files, WMS / WMTS / ArcGIS
357
+ * layers) above the Mapbox GL JS map, following its camera; made on first use. Resolves to the sMap map.
358
+ */
359
+ overlay() {
360
+ const self = this;
361
+ return this.ready.then(function () {
362
+ if (self._overlay) { return self._overlay.ready; }
363
+ const map = self.map;
364
+ const container = map.getContainer();
365
+ injectCss(container.ownerDocument);
366
+ const el = container.ownerDocument.createElement('div');
367
+ el.className = 'smap-mapbox-overlay';
368
+ el.style.width = '100%';
369
+ el.style.height = '100%';
370
+ const controls = container.querySelector && container.querySelector('.mapboxgl-control-container');
371
+ if (controls) { container.insertBefore(el, controls); } else { container.appendChild(el); }
372
+ const head = self.access.head();
373
+ const c = map.getCenter();
374
+ self._overlay = createEmbedded(self._sdk, el, Object.assign({}, self._overlayOptions, {
375
+ // An empty style: no basemap of its own, the basemap's glyphs for labels; its own requests go to the node
376
+ // the Mapbox GL JS map reads from, with the same key and token.
377
+ style: { version: 8, glyphs: (self._shown && self._shown.glyphs) || head + '/v1/fonts/{fontstack}/{range}', sources: {}, layers: [] },
378
+ baseUrl: head,
379
+ nearest: false,
380
+ apiKey: self.access.apiKey || undefined,
381
+ token: self.access.token() || undefined,
382
+ getToken: self.access._getToken ? function () { return self.access.refresh(); } : undefined,
383
+ center: [c.lng, c.lat],
384
+ zoom: map.getZoom(),
385
+ bearing: map.getBearing(),
386
+ pitch: map.getPitch(),
387
+ }), {
388
+ move: function (t, how) {
389
+ const cam = { center: [t.center.lng, t.center.lat], zoom: t.zoom, bearing: t.bearing, pitch: t.pitch };
390
+ if (!how.animate) { map.jumpTo(cam); return; }
391
+ if (how.duration !== undefined) { cam.duration = how.duration; }
392
+ map[how.fly ? 'flyTo' : 'easeTo'](cam);
393
+ },
394
+ cursor: function (cursor) { map.getCanvas().style.cursor = cursor; },
395
+ attribution: function (list) { self._overlayCredits(list); },
396
+ });
397
+ return self._overlay.ready.then(function (m) { self._syncOverlay(); return m; });
398
+ });
399
+ }
400
+
401
+ _syncOverlay(type) {
402
+ if (!this._overlay) { return; }
403
+ if (type === 'resize') { this._overlay.resize(); }
404
+ const map = this.map;
405
+ const c = map.getCenter();
406
+ this._overlay.sync({ center: { lng: c.lng, lat: c.lat }, zoom: map.getZoom(), bearing: map.getBearing(), pitch: map.getPitch() });
407
+ }
408
+
409
+ // Credits of layers drawn by the overlay (beyond the data credit, which the basemap's own source carries) are
410
+ // added to the Mapbox GL JS attribution through a hidden source.
411
+ _overlayCredits(list) {
412
+ const map = this.map;
413
+ const extra = list.slice(1).join(' | ');
414
+ const id = 'smap-overlay-credits';
415
+ if (map.getLayer(id)) { map.removeLayer(id); }
416
+ if (map.getSource(id)) { map.removeSource(id); }
417
+ // An empty source a layer reads: the attribution control shows the credits of sources in use only.
418
+ if (extra) {
419
+ map.addSource(id, { type: 'geojson', data: { type: 'FeatureCollection', features: [] }, attribution: extra });
420
+ map.addLayer({ id: id, type: 'circle', source: id });
421
+ }
422
+ }
423
+
424
+ /** An sMap map call on the overlay, resolving to its answer. */
425
+ call(name) {
426
+ const args = Array.prototype.slice.call(arguments, 1);
427
+ return this.overlay().then(function (m) {
428
+ if (typeof m[name] !== 'function') { throw new Error('sMap: this version of @superrouteca/smap has no ' + name + '()'); }
429
+ return m[name].apply(m, args);
430
+ });
431
+ }
432
+
433
+ /** Removes the Mapbox GL JS map and the overlay. */
434
+ remove() {
435
+ this.access.stop();
436
+ if (this._overlay) { this._overlay.remove(); this._overlay = null; }
437
+ if (this.map) { this.map.remove(); }
438
+ }
439
+ }
440
+
441
+ /** sMap map calls made on the overlay (each returns the SMapMapbox). */
442
+ export const OVERLAY_CALLS = ['addPoints', 'addLines', 'addPolygons', 'addData', 'setData', 'setVisible', 'removeLayer', 'fitLayer',
443
+ 'openPopup', 'closePopup', 'setAutoZoom', 'addWmsLayer', 'addWmtsLayer', 'addArcGisLayer', 'addDeckLayer', 'removeDeckLayer', 'loadLayers'];
444
+
445
+ OVERLAY_CALLS.forEach(function (name) {
446
+ SMapMapbox.prototype[name] = function () {
447
+ const args = Array.prototype.slice.call(arguments);
448
+ this.call.apply(this, [name].concat(args)).catch(function (e) { if (typeof console !== 'undefined') { console.error(e); } });
449
+ return this;
450
+ };
451
+ });
452
+
453
+ function isMapboxSource(s) {
454
+ if (!s) { return false; }
455
+ const urls = [s.url].concat(s.tiles || []).filter(Boolean);
456
+ return urls.some(function (u) { return /^mapbox:\/\//.test(u) || /(^|\.)mapbox\.com\//.test(u.replace(/^https?:\/\//, '')); });
457
+ }
458
+
459
+ /**
460
+ * Switches an existing Mapbox GL JS map's basemap to sMap without touching its own layers: the old basemap (Mapbox
461
+ * sources and the layers reading them, background, sky) goes, every other source and layer stays.
462
+ * `keep(layer)` decides instead which layers stay. Resolves to the SMapMapbox.
463
+ */
464
+ export function switchBasemap(mapboxgl, map, options) { return SMapMapbox.attach(mapboxgl, map, options).ready; }
465
+
466
+ /** Creates sMap in Mapbox GL JS; resolves once the style has loaded. */
467
+ export function createMap(mapboxgl, options) { return new SMapMapbox(mapboxgl, options).ready; }
468
+
469
+ /** The service access (key or token, nearest node), to share with smapTransformRequest(). */
470
+ export function smapAccess(options) { return new SMapAccess(options); }
471
+
472
+ export { isMapboxSource, OSM_ATTRIBUTION as attribution };
473
+
474
+ const SMapMapboxModule = { version, styles: FLAVORS, SMapMapbox, createMap, switchBasemap, smapAccess, smapTransformRequest, SMapBadgeControl, SMapAccess, checkStyle, rasterPath };
475
+ export default SMapMapboxModule;