@superrouteca/smap-openlayers 0.0.0-stage → 1.1.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +148 -2
- package/examples/index.html +97 -0
- package/package.json +67 -4
- package/src/common/access.js +198 -0
- package/src/common/badge.js +56 -0
- package/src/common/embed.js +166 -0
- package/src/index.js +326 -0
- package/src/on-smap.js +273 -0
- package/types/index.d.ts +183 -0
- package/types/on-smap.d.ts +31 -0
|
@@ -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,326 @@
|
|
|
1
|
+
// @superrouteca/smap-openlayers — sMap in OpenLayers: the vector map (an sMap map drawn as an OpenLayers layer, with
|
|
2
|
+
// sMap's layers, popups and Auto Zoom) or raster tiles (an XYZ source), and the sMap badge and data credit.
|
|
3
|
+
// OpenLayers sources and layers on an sMap map are in './on-smap.js' (`@superrouteca/smap-openlayers/on-smap`).
|
|
4
|
+
import Layer from 'ol/layer/Layer.js';
|
|
5
|
+
import TileLayer from 'ol/layer/Tile.js';
|
|
6
|
+
import Source from 'ol/source/Source.js';
|
|
7
|
+
import XYZ from 'ol/source/XYZ.js';
|
|
8
|
+
import Control from 'ol/control/Control.js';
|
|
9
|
+
import TileState from 'ol/TileState.js';
|
|
10
|
+
import { toLonLat, fromLonLat } from 'ol/proj.js';
|
|
11
|
+
import * as sMapModule from '@superrouteca/smap';
|
|
12
|
+
import { SMapAccess, checkStyle, rasterPath, stylePath, FLAVORS, DEFAULT_BASE } from './common/access.js';
|
|
13
|
+
import { OSM_ATTRIBUTION, createBadge } from './common/badge.js';
|
|
14
|
+
import { createEmbedded, hasWebGL2 } from './common/embed.js';
|
|
15
|
+
|
|
16
|
+
const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
|
|
17
|
+
|
|
18
|
+
export const version = '1.1.1';
|
|
19
|
+
export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION, hasWebGL2 };
|
|
20
|
+
|
|
21
|
+
/** OpenLayers layer options; everything else of SMapLayer's options goes to the sMap map. */
|
|
22
|
+
const LAYER_OPTIONS = ['className', 'opacity', 'visible', 'extent', 'zIndex', 'minResolution', 'maxResolution', 'minZoom', 'maxZoom', 'properties', 'map'];
|
|
23
|
+
const ACCESS_OPTIONS = ['baseUrl', 'token', 'getToken', 'apiKey', 'nodes', 'nearest', 'fetch', 'timeoutMs', 'client'];
|
|
24
|
+
/** Web Mercator: metres per pixel at OpenLayers zoom 0 (256 px tiles). */
|
|
25
|
+
const RESOLUTION_0 = 2 * Math.PI * 6378137 / 256;
|
|
26
|
+
|
|
27
|
+
function pick(o, keys, inside) {
|
|
28
|
+
const out = {};
|
|
29
|
+
Object.keys(o || {}).forEach(function (k) { if ((keys.indexOf(k) >= 0) === inside) { out[k] = o[k]; } });
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ------------------------------------------------------------------ badge (one per map)
|
|
34
|
+
|
|
35
|
+
/** The sMap badge as an OpenLayers control, in the bottom-right corner above the attribution. */
|
|
36
|
+
export class SMapBadge extends Control {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
const doc = (options && options.document) || document;
|
|
39
|
+
const el = createBadge(doc);
|
|
40
|
+
el.classList.add('ol-unselectable');
|
|
41
|
+
el.style.position = 'absolute';
|
|
42
|
+
el.style.right = '.5em';
|
|
43
|
+
el.style.bottom = '2.6em';
|
|
44
|
+
el.style.margin = '0';
|
|
45
|
+
super({ element: el });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function holdBadge(map) {
|
|
50
|
+
const held = map.get('smap:badge') || { control: null, count: 0 };
|
|
51
|
+
held.count += 1;
|
|
52
|
+
if (!held.control) { held.control = new SMapBadge(); map.addControl(held.control); }
|
|
53
|
+
map.set('smap:badge', held, true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function releaseBadge(map) {
|
|
57
|
+
const held = map.get('smap:badge');
|
|
58
|
+
if (!held) { return; }
|
|
59
|
+
held.count -= 1;
|
|
60
|
+
if (held.count <= 0 && held.control) { map.removeControl(held.control); held.control = null; held.count = 0; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ------------------------------------------------------------------ vector: an sMap map as a layer
|
|
64
|
+
|
|
65
|
+
/** sMap map calls a layer forwards once its map is ready (each returns the layer). */
|
|
66
|
+
export const FORWARDED = ['setStyle', 'setLanguage', 'setBasemap', 'setAutoZoom', 'addPoints', 'addLines', 'addPolygons',
|
|
67
|
+
'addData', 'setData', 'setVisible', 'fitLayer', 'openPopup', 'closePopup', 'addSource', 'removeSource', 'addStyleLayer',
|
|
68
|
+
'setPaint', 'setLayout', 'setFilter', 'addRasterLayer', 'addWmsLayer', 'addWmtsLayer', 'addVectorTiles', 'addArcGisLayer',
|
|
69
|
+
'loadLayers', 'setBuildings3D'];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The sMap vector map as an OpenLayers layer. Options: every sMap map option (style incl. `custom:<id>`, language,
|
|
73
|
+
* apiKey, token, getToken, baseUrl, nodes, nearest, popup, popupLoading, autoZoom), OpenLayers layer options
|
|
74
|
+
* (opacity, visible, zIndex, minZoom, …) and `interactive` (OpenLayers' clicks and pointer moves reach sMap's
|
|
75
|
+
* layers; default true). The view must be in Web Mercator (EPSG:3857, OpenLayers' default).
|
|
76
|
+
*/
|
|
77
|
+
export class SMapLayer extends Layer {
|
|
78
|
+
constructor(options) {
|
|
79
|
+
options = options || {};
|
|
80
|
+
const credits = { list: [OSM_ATTRIBUTION] };
|
|
81
|
+
const source = new Source({ attributions: function () { return credits.list; } });
|
|
82
|
+
const self = { ref: null };
|
|
83
|
+
super(Object.assign(pick(options, LAYER_OPTIONS, true), {
|
|
84
|
+
source: source,
|
|
85
|
+
render: function (frameState) { return self.ref._render(frameState); },
|
|
86
|
+
}));
|
|
87
|
+
self.ref = this;
|
|
88
|
+
this._credits = credits;
|
|
89
|
+
this._sdk = options.sdk || SDK;
|
|
90
|
+
this._interactive = options.interactive !== false;
|
|
91
|
+
this._smapOptions = pick(options, LAYER_OPTIONS.concat(['sdk', 'interactive']), false);
|
|
92
|
+
if (typeof this._smapOptions.style === 'string' && this._smapOptions.style.indexOf('://') < 0) { checkStyle(this._smapOptions.style); }
|
|
93
|
+
const me = this;
|
|
94
|
+
this.ready = new Promise(function (resolve) { me._resolveReady = resolve; });
|
|
95
|
+
this._embedded = null;
|
|
96
|
+
this._container = null;
|
|
97
|
+
this._keys = [];
|
|
98
|
+
this._placeholder = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setMapInternal(map) {
|
|
102
|
+
const before = this.getMapInternal();
|
|
103
|
+
super.setMapInternal(map);
|
|
104
|
+
if (before === map) { return; }
|
|
105
|
+
if (before) { this._detach(before); }
|
|
106
|
+
if (map) { this._attach(map); }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_attach(map) {
|
|
110
|
+
const doc = map.getViewport().ownerDocument;
|
|
111
|
+
if (!this._container) {
|
|
112
|
+
this._container = doc.createElement('div');
|
|
113
|
+
this._container.className = 'ol-smap-layer';
|
|
114
|
+
this._container.style.width = '100%';
|
|
115
|
+
this._container.style.height = '100%';
|
|
116
|
+
}
|
|
117
|
+
// Inside the stop-event overlay: clicks on an open popup stay with the popup and never reach OpenLayers.
|
|
118
|
+
map.getOverlayContainerStopEvent().appendChild(this._container);
|
|
119
|
+
if (!this._embedded) {
|
|
120
|
+
const self = this;
|
|
121
|
+
// While the map is being built its view may not be set yet: the first render puts the sMap map in place.
|
|
122
|
+
const view = map.getView && map.getView();
|
|
123
|
+
const known = view && view.getCenter() && view.getZoom() !== undefined;
|
|
124
|
+
this._embedded = createEmbedded(this._sdk, this._container, Object.assign({}, this._smapOptions, {
|
|
125
|
+
center: known ? toLonLat(view.getCenter(), view.getProjection()) : [0, 0],
|
|
126
|
+
zoom: known ? view.getZoom() - 1 : 0,
|
|
127
|
+
}), {
|
|
128
|
+
move: function (t, how) { self._moveView(t, how); },
|
|
129
|
+
attribution: function (list) { self._credits.list = list; self.getSource().changed(); },
|
|
130
|
+
cursor: function (cursor) { const m = self.getMapInternal(); if (m) { m.getViewport().style.cursor = cursor; } },
|
|
131
|
+
});
|
|
132
|
+
this._embedded.ready.then(function (m) { self.changed(); self._resolveReady(m); });
|
|
133
|
+
} else {
|
|
134
|
+
this._embedded.resize();
|
|
135
|
+
}
|
|
136
|
+
const self = this;
|
|
137
|
+
const forward = function (type) {
|
|
138
|
+
return function (e) {
|
|
139
|
+
if (!self._interactive || !self._embedded || (e.dragging && type === 'mousemove')) { return; }
|
|
140
|
+
if (self._embedded.inPopup(e.originalEvent)) { return; }
|
|
141
|
+
const ll = toLonLat(e.coordinate, map.getView().getProjection());
|
|
142
|
+
self._embedded.forward(type, e.pixel[0], e.pixel[1], { lng: ll[0], lat: ll[1] }, e.originalEvent);
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
this._keys = [
|
|
146
|
+
[map, 'click', forward('click')],
|
|
147
|
+
[map, 'dblclick', forward('dblclick')],
|
|
148
|
+
[map, 'pointermove', forward('mousemove')],
|
|
149
|
+
[map, 'change:size', function () { if (self._embedded) { self._embedded.resize(); } }],
|
|
150
|
+
];
|
|
151
|
+
this._keys.forEach(function (k) { k[0].on(k[1], k[2]); });
|
|
152
|
+
this._out = function (e) {
|
|
153
|
+
if (!self._interactive || !self._embedded) { return; }
|
|
154
|
+
self._embedded.forward('mouseout', -1, -1, { lng: 0, lat: 0 }, e);
|
|
155
|
+
};
|
|
156
|
+
map.getViewport().addEventListener('pointerleave', this._out);
|
|
157
|
+
holdBadge(map);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_detach(map) {
|
|
161
|
+
this._keys.forEach(function (k) { k[0].un(k[1], k[2]); });
|
|
162
|
+
this._keys = [];
|
|
163
|
+
map.getViewport().removeEventListener('pointerleave', this._out);
|
|
164
|
+
releaseBadge(map);
|
|
165
|
+
if (this._container && this._container.parentNode) { this._container.parentNode.removeChild(this._container); }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_render(frameState) {
|
|
169
|
+
const engine = this._embedded && this._embedded.engine();
|
|
170
|
+
if (!engine) {
|
|
171
|
+
if (!this._placeholder) { this._placeholder = document.createElement('div'); }
|
|
172
|
+
return this._placeholder;
|
|
173
|
+
}
|
|
174
|
+
const vs = frameState.viewState;
|
|
175
|
+
const ll = toLonLat(vs.center, vs.projection);
|
|
176
|
+
// OpenLayers counts zoom in 256 px tiles, the sMap map in 512 px tiles: one level apart.
|
|
177
|
+
const zoom = Math.log2(RESOLUTION_0 / vs.resolution) - 1;
|
|
178
|
+
this._embedded.sync({ center: { lng: ll[0], lat: ll[1] }, zoom: zoom, bearing: (-vs.rotation * 180) / Math.PI, redraw: true });
|
|
179
|
+
const canvas = engine.getCanvas();
|
|
180
|
+
canvas.style.position = 'absolute';
|
|
181
|
+
canvas.style.left = '0';
|
|
182
|
+
canvas.style.top = '0';
|
|
183
|
+
canvas.style.opacity = String(this.getOpacity());
|
|
184
|
+
return canvas;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The sMap map asked to move (Auto Zoom, fitLayer, flyTo, a popup panning into view): the OpenLayers view moves.
|
|
188
|
+
_moveView(t, how) {
|
|
189
|
+
const map = this.getMapInternal();
|
|
190
|
+
if (!map) { return; }
|
|
191
|
+
const view = map.getView();
|
|
192
|
+
const center = fromLonLat([t.center.lng, t.center.lat], view.getProjection());
|
|
193
|
+
const resolution = RESOLUTION_0 / Math.pow(2, t.zoom + 1);
|
|
194
|
+
const rotation = (-(t.bearing || 0) * Math.PI) / 180;
|
|
195
|
+
if (how.animate) {
|
|
196
|
+
view.animate({ center: center, resolution: resolution, rotation: rotation, duration: how.duration !== undefined ? how.duration : 400 });
|
|
197
|
+
} else {
|
|
198
|
+
view.setCenter(center);
|
|
199
|
+
view.setResolution(resolution);
|
|
200
|
+
view.setRotation(rotation);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The sMap map (null until it is ready); `await layer.ready` gives it once it is. */
|
|
205
|
+
getSMap() { return this._embedded && this._embedded.engine() ? this._embedded.map : null; }
|
|
206
|
+
|
|
207
|
+
/** sMap map calls whose answer is a value: `layer.call('basemap')` resolves to it. */
|
|
208
|
+
call(name) {
|
|
209
|
+
const args = Array.prototype.slice.call(arguments, 1);
|
|
210
|
+
return this.ready.then(function (m) {
|
|
211
|
+
if (typeof m[name] !== 'function') { throw new Error('sMap: this version of @superrouteca/smap has no ' + name + '()'); }
|
|
212
|
+
return m[name].apply(m, args);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Removes one of the sMap map's layers (OpenLayers' own removeLayer belongs to the map). */
|
|
217
|
+
removeSMapLayer(id) { this.call('removeLayer', id).catch(function () {}); return this; }
|
|
218
|
+
|
|
219
|
+
disposeInternal() {
|
|
220
|
+
if (this._embedded) { this._embedded.remove(); this._embedded = null; }
|
|
221
|
+
super.disposeInternal();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
FORWARDED.forEach(function (name) {
|
|
226
|
+
// OpenLayers layers have no method of these names, except setVisible (visibility of the OpenLayers layer), which
|
|
227
|
+
// stays OpenLayers'; the sMap map's is setSMapVisible.
|
|
228
|
+
const own = name === 'setVisible' ? 'setSMapVisible' : name;
|
|
229
|
+
SMapLayer.prototype[own] = function () {
|
|
230
|
+
const args = Array.prototype.slice.call(arguments);
|
|
231
|
+
if (name === 'setStyle' && typeof args[0] === 'string' && args[0].indexOf('://') < 0) { checkStyle(args[0]); }
|
|
232
|
+
this.call.apply(this, [name].concat(args)).catch(function (e) { if (typeof console !== 'undefined') { console.error(e); } });
|
|
233
|
+
return this;
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ------------------------------------------------------------------ raster tiles
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* sMap raster tiles as an OpenLayers XYZ source: read with the map key or token from the nearest node, moving to the
|
|
241
|
+
* next node when one fails. Options: style (a scheme or `custom:<id>`), language, retina (512 px tiles; default the
|
|
242
|
+
* screen's), the access options, and XYZ source options.
|
|
243
|
+
*/
|
|
244
|
+
export class SMapRasterSource extends XYZ {
|
|
245
|
+
constructor(options) {
|
|
246
|
+
options = options || {};
|
|
247
|
+
const access = options.access || new SMapAccess(pick(options, ACCESS_OPTIONS, true));
|
|
248
|
+
const state = {
|
|
249
|
+
style: checkStyle(options.style || 'light'),
|
|
250
|
+
language: options.language || (typeof navigator !== 'undefined' && navigator.language) || 'en',
|
|
251
|
+
retina: options.retina !== undefined ? !!options.retina : (typeof window !== 'undefined' && window.devicePixelRatio > 1),
|
|
252
|
+
};
|
|
253
|
+
const rest = pick(options, ACCESS_OPTIONS.concat(['style', 'language', 'retina', 'access']), false);
|
|
254
|
+
super(Object.assign({
|
|
255
|
+
attributions: OSM_ATTRIBUTION,
|
|
256
|
+
attributionsCollapsible: true,
|
|
257
|
+
maxZoom: 22,
|
|
258
|
+
crossOrigin: 'anonymous',
|
|
259
|
+
}, rest, {
|
|
260
|
+
url: access.rasterTemplate(state.style, { lang: state.language, retina: state.retina }),
|
|
261
|
+
tileSize: 256,
|
|
262
|
+
tilePixelRatio: state.retina ? 2 : 1,
|
|
263
|
+
tileLoadFunction: function (tile) {
|
|
264
|
+
const c = tile.getTileCoord();
|
|
265
|
+
access.start()
|
|
266
|
+
.then(function () { return access.rasterTile(state.style, c[0], c[1], c[2], { lang: state.language, retina: state.retina }); })
|
|
267
|
+
.then(function (blob) {
|
|
268
|
+
const img = tile.getImage();
|
|
269
|
+
const url = URL.createObjectURL(blob);
|
|
270
|
+
const done = function () { URL.revokeObjectURL(url); };
|
|
271
|
+
img.addEventListener('load', done, { once: true });
|
|
272
|
+
img.addEventListener('error', done, { once: true });
|
|
273
|
+
img.src = url;
|
|
274
|
+
})
|
|
275
|
+
.catch(function () { tile.setState(TileState.ERROR); });
|
|
276
|
+
},
|
|
277
|
+
}));
|
|
278
|
+
this._access = access;
|
|
279
|
+
this._state = state;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_reload() { this.setUrl(this._access.rasterTemplate(this._state.style, { lang: this._state.language, retina: this._state.retina })); }
|
|
283
|
+
|
|
284
|
+
/** Switches the scheme (or `custom:<id>`) and reloads the tiles. */
|
|
285
|
+
setStyle(style) { this._state.style = checkStyle(style); this._reload(); return this; }
|
|
286
|
+
/** Switches the label language and reloads the tiles. */
|
|
287
|
+
setLanguage(language) { this._state.language = language; this._reload(); return this; }
|
|
288
|
+
/** The access in use (token, nodes). */
|
|
289
|
+
getAccess() { return this._access; }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** sMap raster tiles as an OpenLayers tile layer (with the sMap badge). */
|
|
293
|
+
export class SMapRasterLayer extends TileLayer {
|
|
294
|
+
constructor(options) {
|
|
295
|
+
options = options || {};
|
|
296
|
+
super(Object.assign(pick(options, LAYER_OPTIONS.concat(['preload', 'useInterimTilesOnError']), true), {
|
|
297
|
+
source: options.source || new SMapRasterSource(pick(options, LAYER_OPTIONS.concat(['preload', 'useInterimTilesOnError', 'source']), false)),
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
setMapInternal(map) {
|
|
301
|
+
const before = this.getMapInternal();
|
|
302
|
+
super.setMapInternal(map);
|
|
303
|
+
if (before === map) { return; }
|
|
304
|
+
if (before) { releaseBadge(before); }
|
|
305
|
+
if (map) { holdBadge(map); }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function smapRasterSource(options) { return new SMapRasterSource(options); }
|
|
310
|
+
export function smapRasterLayer(options) { return new SMapRasterLayer(options); }
|
|
311
|
+
export function smapLayer(options) { return new SMapLayer(options); }
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The sMap basemap the browser can draw: the vector map where WebGL 2 exists, raster tiles elsewhere.
|
|
315
|
+
* `mode: 'vector' | 'raster' | 'auto'` (default auto).
|
|
316
|
+
*/
|
|
317
|
+
export function smapBasemap(options) {
|
|
318
|
+
const o = Object.assign({}, options);
|
|
319
|
+
const mode = o.mode || 'auto';
|
|
320
|
+
delete o.mode;
|
|
321
|
+
if (mode === 'raster' || (mode === 'auto' && !hasWebGL2())) { return new SMapRasterLayer(o); }
|
|
322
|
+
return new SMapLayer(o);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const SMapOpenLayers = { version, styles: FLAVORS, SMapLayer, smapLayer, SMapRasterSource, smapRasterSource, SMapRasterLayer, smapRasterLayer, smapBasemap, SMapBadge, SMapAccess, hasWebGL2, rasterPath, checkStyle };
|
|
326
|
+
export default SMapOpenLayers;
|