@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.1 — 2026-10-27
|
|
4
|
+
|
|
5
|
+
- The version follows the shared 1.1.1 release; no change of its own.
|
|
6
|
+
|
|
7
|
+
## 1.1.0 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
- First version. sMap in OpenLayers: `smapLayer()` draws the sMap vector map as an OpenLayers layer with the five
|
|
10
|
+
colour schemes and saved custom styles (`custom:<id>`), any label language, basemap overrides, sMap's pins, lines
|
|
11
|
+
and areas with their options, popups and Auto Zoom; the OpenLayers view keeps the camera (rotation included) and
|
|
12
|
+
the input, and the sMap map's own moves are made by the view. `smapRasterSource()` / `smapRasterLayer()` give raster
|
|
13
|
+
tiles as an XYZ source; `smapBasemap()` picks vector or raster.
|
|
14
|
+
- Map key or token with renewal through `getToken`, the nearest map node and the next one when a node fails.
|
|
15
|
+
- The data credit and every source credit appear in OpenLayers' attribution, with the sMap badge beside it.
|
|
16
|
+
- OpenLayers on sMap: `addOpenLayersLayer()` / `addOpenLayersLayers()` add XYZ, OSM, TileJSON, TileWMS, ImageWMS,
|
|
17
|
+
WMTS, VectorTile and Vector sources (or whole OpenLayers maps) to an sMap map.
|
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,149 @@
|
|
|
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 OpenLayers: `@superrouteca/smap-openlayers`
|
|
7
|
+
|
|
8
|
+
Two ways round:
|
|
9
|
+
|
|
10
|
+
- **sMap in OpenLayers.** The sMap map as an OpenLayers layer: the five colour schemes and your saved custom styles,
|
|
11
|
+
labels in any language, basemap overrides, sMap's pins, lines and areas with all their options, popups and Auto
|
|
12
|
+
Zoom, map key or token with renewal, the nearest map node. Vector (drawn with WebGL as an OpenLayers layer, rotation
|
|
13
|
+
included) or raster tiles as an XYZ source.
|
|
14
|
+
- **OpenLayers on sMap.** OpenLayers sources and layers — XYZ, OSM, TileJSON, TileWMS, ImageWMS, WMTS, VectorTile and
|
|
15
|
+
Vector — added to an sMap map as they are.
|
|
16
|
+
|
|
17
|
+
- Version 1.1.0, MIT licence (see `LICENSE` at the repository root)
|
|
18
|
+
- OpenLayers 10 (a peer dependency)
|
|
19
|
+
- ES module with types
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @superrouteca/smap-openlayers ol
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import 'ol/ol.css';
|
|
29
|
+
import '@superrouteca/smap/smap.css'; // popups and the Auto Zoom chooser
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## sMap in OpenLayers
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import Map from 'ol/Map.js';
|
|
36
|
+
import View from 'ol/View.js';
|
|
37
|
+
import { fromLonLat } from 'ol/proj.js';
|
|
38
|
+
import { smapLayer } from '@superrouteca/smap-openlayers';
|
|
39
|
+
|
|
40
|
+
const basemap = smapLayer({
|
|
41
|
+
apiKey: 'smk_…', // a map key that allows this site, or token / getToken
|
|
42
|
+
style: 'light', // light, white, grayscale, dark, black, or 'custom:sms_…'
|
|
43
|
+
language: 'en',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const map = new Map({
|
|
47
|
+
target: 'map',
|
|
48
|
+
layers: [basemap /* , your OpenLayers layers */],
|
|
49
|
+
view: new View({ center: fromLonLat([-79.38, 43.65]), zoom: 12 }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
basemap.addPoints('stops', stops, {
|
|
53
|
+
marker: { fill: { property: 'status_color' }, text: { property: 'stop_number' } },
|
|
54
|
+
popup: (p) => `<b>${p.stop_number}</b>`,
|
|
55
|
+
});
|
|
56
|
+
basemap.addLines('routes', routes, { color: { property: 'color' }, width: 5, casing: true, arrows: true });
|
|
57
|
+
basemap.setBasemap({ water: { color: '#9cc0e6' }, roads: { width: 1.5 } });
|
|
58
|
+
basemap.setStyle('dark');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The layer forwards every sMap map call once its map is ready (`setStyle`, `setLanguage`, `setBasemap`,
|
|
62
|
+
`setAutoZoom`, `addPoints`, `addLines`, `addPolygons`, `addData`, `setData`, `fitLayer`, `openPopup`, `closePopup`,
|
|
63
|
+
`addSource`, `addStyleLayer`, `setPaint`, `setLayout`, `setFilter`, `addRasterLayer`, `addWmsLayer`, `addWmtsLayer`,
|
|
64
|
+
`addVectorTiles`, `addArcGisLayer`, `loadLayers`). `setSMapVisible(id, on)` shows or hides one of the sMap map's
|
|
65
|
+
layers and `removeSMapLayer(id)` removes one (OpenLayers keeps its own `setVisible`). Calls that answer a value:
|
|
66
|
+
`await basemap.call('basemap')`. `await basemap.ready` (or `basemap.getSMap()`) gives the sMap map itself.
|
|
67
|
+
|
|
68
|
+
How it works with OpenLayers:
|
|
69
|
+
|
|
70
|
+
- The OpenLayers view owns the camera and the input; the sMap map follows it on every frame, rotation included.
|
|
71
|
+
Every move the sMap map asks for (Auto Zoom, `fitLayer`, a popup panning into view) is made by the view.
|
|
72
|
+
- Clicks and pointer moves reach sMap's layers (popups, Auto Zoom, `onClick`, hover); `interactive: false` turns
|
|
73
|
+
this off. Popups sit above the map and keep their own clicks.
|
|
74
|
+
- The credits are the layer's attributions in OpenLayers' attribution control (the data credit and every source you
|
|
75
|
+
add to the sMap map); the sMap badge sits above it (one per map).
|
|
76
|
+
- OpenLayers' zoom levels are one above the sMap map's own. The view must be in Web Mercator (EPSG:3857, the
|
|
77
|
+
default); for other projections use raster tiles from a reprojecting source.
|
|
78
|
+
- Opacity, visibility, `zIndex`, `minZoom` / `maxZoom` are OpenLayers layer options as usual.
|
|
79
|
+
|
|
80
|
+
### Raster tiles
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { smapRasterLayer, smapRasterSource, smapBasemap } from '@superrouteca/smap-openlayers';
|
|
84
|
+
|
|
85
|
+
map.addLayer(smapRasterLayer({ apiKey: 'smk_…', style: 'grayscale', language: 'de' })); // an XYZ tile layer
|
|
86
|
+
const source = smapRasterSource({ getToken, style: 'dark' }); // the source alone
|
|
87
|
+
map.addLayer(smapBasemap({ apiKey: 'smk_…' })); // vector where WebGL 2 exists, raster elsewhere
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Tiles are 256 px (512 px on high-density screens, `retina`), read with the map key or token from the nearest node,
|
|
91
|
+
moving to the next node when one fails. `source.setStyle()` and `source.setLanguage()` reload them. sMap also
|
|
92
|
+
publishes the same tiles as TileJSON and WMTS capabilities for tools that read those.
|
|
93
|
+
|
|
94
|
+
### Map keys and tokens
|
|
95
|
+
|
|
96
|
+
- **Map key** (`apiKey: 'smk_…'`): created in the Superroute developer center with the sites it may be used from.
|
|
97
|
+
- **Token** (`token: 'sm1.…'`, or `getToken: async () => …`): signed by your own server; with `getToken` a new token
|
|
98
|
+
is asked for two minutes before the current one expires.
|
|
99
|
+
- `baseUrl` (default `https://tile.srsr.ca`), `nodes`, `nearest` (default true).
|
|
100
|
+
|
|
101
|
+
## OpenLayers on sMap
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { Map } from '@superrouteca/smap';
|
|
105
|
+
import TileLayer from 'ol/layer/Tile.js';
|
|
106
|
+
import TileWMS from 'ol/source/TileWMS.js';
|
|
107
|
+
import VectorTileSource from 'ol/source/VectorTile.js';
|
|
108
|
+
import MVT from 'ol/format/MVT.js';
|
|
109
|
+
import { addOpenLayersLayer, addOpenLayersLayers } from '@superrouteca/smap-openlayers/on-smap';
|
|
110
|
+
|
|
111
|
+
const smap = new Map({ container: 'map', apiKey: 'smk_…', style: 'light' });
|
|
112
|
+
await smap.ready;
|
|
113
|
+
|
|
114
|
+
await addOpenLayersLayer(smap, 'radar', new TileLayer({
|
|
115
|
+
source: new TileWMS({ url: 'https://wms.example/ows', params: { LAYERS: 'radar' } }),
|
|
116
|
+
opacity: 0.6,
|
|
117
|
+
}), { slot: 'roads' });
|
|
118
|
+
|
|
119
|
+
await addOpenLayersLayer(smap, 'parcels', new VectorTileSource({ format: new MVT(), url: 'https://vt.example/{z}/{x}/{y}.pbf' }), {
|
|
120
|
+
layers: [{ id: 'parcels-line', type: 'line', 'source-layer': 'parcels', paint: { 'line-color': '#475569' } }],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Or every layer of an existing OpenLayers map, in order:
|
|
124
|
+
const { added, skipped } = await addOpenLayersLayers(smap, olMap);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| OpenLayers | On the sMap map |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `XYZ`, `OSM`, `TileJSON`, `TileImage` | raster layer (`{a-c}` subdomains expanded, `{-y}` as TMS, tile size kept) |
|
|
130
|
+
| `TileWMS`, `ImageWMS` | WMS layer with the source's parameters |
|
|
131
|
+
| `WMTS` | raster layer when the matrix set is Web Mercator by zoom (REST or KVP), otherwise a WMTS layer read from the service |
|
|
132
|
+
| `VectorTile` | vector tiles drawn by the Style Specification layers you give in `layers` (OpenLayers style functions have no counterpart) |
|
|
133
|
+
| `Vector` | the features as GeoJSON in WGS 84, points, lines and areas as `<id>`, `<id>:lines`, `<id>:areas`; a layer's fixed `Style` becomes their colours; sMap layer options (`popup`, `marker`, …) apply |
|
|
134
|
+
|
|
135
|
+
A layer's opacity, zoom range and the source's attributions come along. Without `slot` or `before` a layer draws on
|
|
136
|
+
top, in the order added. `addOpenLayersLayers` skips an sMap basemap already in the OpenLayers map and lists what it
|
|
137
|
+
could not add in `skipped`.
|
|
138
|
+
|
|
139
|
+
## Example
|
|
140
|
+
|
|
141
|
+
`examples/index.html` shows both ways. Serve the repository root (for example `npx serve .`) and open
|
|
142
|
+
`/openlayers/examples/?key=smk_…` with a map key that allows your host.
|
|
143
|
+
|
|
144
|
+
## Tests
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
npm test
|
|
148
|
+
npm run typecheck
|
|
149
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>sMap for OpenLayers</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@10.11.0/ol.css">
|
|
8
|
+
<link rel="stylesheet" href="../../js/dist/smap.css">
|
|
9
|
+
<style>
|
|
10
|
+
body { margin: 0; font: 14px/1.4 system-ui, sans-serif; }
|
|
11
|
+
.bar { display: flex; gap: 8px; align-items: center; padding: 8px 12px; flex-wrap: wrap; }
|
|
12
|
+
.maps { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 0 12px 12px; }
|
|
13
|
+
.maps > div { height: 70vh; border: 1px solid #cbd5e1; border-radius: 6px; position: relative; }
|
|
14
|
+
h2 { font-size: 14px; margin: 0 12px; }
|
|
15
|
+
@media (max-width: 800px) { .maps { grid-template-columns: 1fr; } }
|
|
16
|
+
</style>
|
|
17
|
+
<!-- Serve the repository root (npx serve .) and open /openlayers/examples/?key=smk_… -->
|
|
18
|
+
<script type="importmap">
|
|
19
|
+
{
|
|
20
|
+
"imports": {
|
|
21
|
+
"ol/": "https://cdn.jsdelivr.net/npm/ol@10.11.0/",
|
|
22
|
+
"@superrouteca/smap": "../../js/dist/smap.mjs",
|
|
23
|
+
"@superrouteca/smap/client": "../../js/dist/smap-client.mjs",
|
|
24
|
+
"@superrouteca/smap-openlayers": "../src/index.js",
|
|
25
|
+
"@superrouteca/smap-openlayers/on-smap": "../src/on-smap.js"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div class="bar">
|
|
32
|
+
<label>Scheme <select id="scheme"><option>light</option><option>white</option><option>grayscale</option><option>dark</option><option>black</option></select></label>
|
|
33
|
+
<label>Language <select id="lang"><option>en</option><option>fr</option><option>de</option><option>zh-Hans</option></select></label>
|
|
34
|
+
<label><input type="checkbox" id="raster"> Raster tiles</label>
|
|
35
|
+
<button id="rotate">Rotate</button>
|
|
36
|
+
</div>
|
|
37
|
+
<h2>sMap in OpenLayers (left) · OpenLayers sources on an sMap map (right)</h2>
|
|
38
|
+
<div class="maps"><div id="ol"></div><div id="smap"></div></div>
|
|
39
|
+
<script type="module">
|
|
40
|
+
import OlMap from 'ol/Map.js';
|
|
41
|
+
import View from 'ol/View.js';
|
|
42
|
+
import VectorLayer from 'ol/layer/Vector.js';
|
|
43
|
+
import VectorSource from 'ol/source/Vector.js';
|
|
44
|
+
import TileLayer from 'ol/layer/Tile.js';
|
|
45
|
+
import OSM from 'ol/source/OSM.js';
|
|
46
|
+
import Feature from 'ol/Feature.js';
|
|
47
|
+
import Point from 'ol/geom/Point.js';
|
|
48
|
+
import { Style, Circle, Fill, Stroke } from 'ol/style.js';
|
|
49
|
+
import { fromLonLat } from 'ol/proj.js';
|
|
50
|
+
import { Map as SMap, popupTemplate } from '@superrouteca/smap';
|
|
51
|
+
import { smapLayer, smapRasterLayer } from '@superrouteca/smap-openlayers';
|
|
52
|
+
import { addOpenLayersLayer } from '@superrouteca/smap-openlayers/on-smap';
|
|
53
|
+
|
|
54
|
+
const apiKey = new URLSearchParams(location.search).get('key') || undefined;
|
|
55
|
+
const stops = [
|
|
56
|
+
{ lng: -79.3832, lat: 43.6532, n: 1, status: '#16a34a', name: 'City Hall' },
|
|
57
|
+
{ lng: -79.3871, lat: 43.6426, n: 2, status: '#f97316', name: 'CN Tower' },
|
|
58
|
+
{ lng: -79.3957, lat: 43.6677, n: 3, status: '#2563eb', name: 'Queen’s Park' },
|
|
59
|
+
{ lng: -79.3806, lat: 43.6629, n: 4, status: '#e11d48', name: 'Church St' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// ① sMap in OpenLayers
|
|
63
|
+
let base = smapLayer({ apiKey, style: 'light', language: 'en' });
|
|
64
|
+
const decorate = (layer) => layer.addPoints('stops', stops, {
|
|
65
|
+
marker: { fill: { property: 'status' }, border: '#1e293b', text: { property: 'n' } },
|
|
66
|
+
popup: popupTemplate('<b>{n}</b> {name}'),
|
|
67
|
+
});
|
|
68
|
+
decorate(base);
|
|
69
|
+
const own = new VectorLayer({
|
|
70
|
+
source: new VectorSource({ features: [new Feature(new Point(fromLonLat([-79.3817, 43.6487])))] }),
|
|
71
|
+
style: new Style({ image: new Circle({ radius: 8, fill: new Fill({ color: '#7c3aed' }), stroke: new Stroke({ color: '#fff', width: 2 }) }) }),
|
|
72
|
+
});
|
|
73
|
+
const map = new OlMap({ target: 'ol', layers: [base, own], view: new View({ center: fromLonLat([-79.387, 43.655]), zoom: 14 }) });
|
|
74
|
+
|
|
75
|
+
const scheme = document.getElementById('scheme');
|
|
76
|
+
const lang = document.getElementById('lang');
|
|
77
|
+
scheme.onchange = () => base.setStyle(scheme.value);
|
|
78
|
+
lang.onchange = () => base.setLanguage(lang.value);
|
|
79
|
+
document.getElementById('rotate').onclick = () => map.getView().animate({ rotation: map.getView().getRotation() + Math.PI / 6 });
|
|
80
|
+
document.getElementById('raster').onchange = (e) => {
|
|
81
|
+
map.removeLayer(base);
|
|
82
|
+
base = e.target.checked ? smapRasterLayer({ apiKey, style: scheme.value, language: lang.value }) : smapLayer({ apiKey, style: scheme.value, language: lang.value });
|
|
83
|
+
if (!e.target.checked) { decorate(base); }
|
|
84
|
+
map.getLayers().insertAt(0, base);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ② OpenLayers sources on an sMap map
|
|
88
|
+
const smap = new SMap({ container: 'smap', apiKey, style: 'grayscale', language: 'en', center: [-79.387, 43.655], zoom: 12 });
|
|
89
|
+
await smap.ready;
|
|
90
|
+
await addOpenLayersLayer(smap, 'osm', new TileLayer({ source: new OSM(), opacity: 0.35 }), { slot: 'roads' });
|
|
91
|
+
await addOpenLayersLayer(smap, 'mine', new VectorLayer({
|
|
92
|
+
source: new VectorSource({ features: stops.map((s) => new Feature({ geometry: new Point(fromLonLat([s.lng, s.lat])), name: s.name })) }),
|
|
93
|
+
style: new Style({ image: new Circle({ radius: 7, fill: new Fill({ color: '#e11d48' }) }) }),
|
|
94
|
+
}), { popup: popupTemplate('{name}') });
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superrouteca/smap-openlayers",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "sMap for OpenLayers: maps from Superroute's sMap service as an OpenLayers layer (vector or raster), and OpenLayers sources and layers on an sMap map.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Goodneo Solutions",
|
|
7
|
+
"homepage": "https://github.com/Goodneo-Solutions/smap-sdk/tree/main/openlayers#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Goodneo-Solutions/smap-sdk.git",
|
|
11
|
+
"directory": "openlayers"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"map",
|
|
15
|
+
"maps",
|
|
16
|
+
"openlayers",
|
|
17
|
+
"ol",
|
|
18
|
+
"vector-tiles",
|
|
19
|
+
"smap",
|
|
20
|
+
"superroute"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "./src/index.js",
|
|
25
|
+
"types": "./types/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./types/index.d.ts",
|
|
29
|
+
"default": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./on-smap": {
|
|
32
|
+
"types": "./types/on-smap.d.ts",
|
|
33
|
+
"default": "./src/on-smap.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"types",
|
|
40
|
+
"examples",
|
|
41
|
+
"README.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"prepack": "cp ../LICENSE LICENSE",
|
|
50
|
+
"typecheck": "tsc -p test/tsconfig.json",
|
|
51
|
+
"test": "node --test test/*.test.mjs"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"ol": ">=10.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"ol": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@superrouteca/smap": "1.1.1"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"jsdom": "30.1.3",
|
|
66
|
+
"ol": "10.11.0",
|
|
67
|
+
"typescript": "6.0.3"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// sMap tool adapters — access to the service: map key or token (renewed through getToken), the nearest node and the
|
|
2
|
+
// next one when a node fails. Kept identical in leaflet/, openlayers/ and mapbox/ (test/common.test.mjs checks); the
|
|
3
|
+
// node choice, the token renewal and the failover of requests are the JS client's (@superrouteca/smap/client).
|
|
4
|
+
import SMapClient from '@superrouteca/smap/client';
|
|
5
|
+
import * as sMapModule from '@superrouteca/smap';
|
|
6
|
+
|
|
7
|
+
const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
|
|
8
|
+
const PLACEHOLDER = 'https://smap.invalid';
|
|
9
|
+
|
|
10
|
+
/** The SDK's own URL builder (rasterUrl, styleUrl of @superrouteca/smap 1.1.0); throws when the SDK is older. */
|
|
11
|
+
function builder(name) {
|
|
12
|
+
if (typeof SDK[name] !== 'function') { throw new Error('sMap: the adapters need @superrouteca/smap 1.1.0 or later (' + name + ')'); }
|
|
13
|
+
return SDK[name];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A path of the SDK's URL builders, without their address. */
|
|
17
|
+
function pathOf(url) { return url.slice(PLACEHOLDER.length); }
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_BASE = 'https://tile.srsr.ca';
|
|
20
|
+
export const FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
|
|
21
|
+
const CUSTOM = /^custom:([A-Za-z0-9_-]+)$/;
|
|
22
|
+
|
|
23
|
+
/** Checks a style name: a scheme or `custom:<id>`; anything else throws with the name in the message. */
|
|
24
|
+
export function checkStyle(style) {
|
|
25
|
+
const s = style == null ? 'light' : String(style);
|
|
26
|
+
if (FLAVORS.indexOf(s) >= 0 || CUSTOM.test(s)) { return s; }
|
|
27
|
+
throw new Error('sMap: unknown style "' + s + '" (use ' + FLAVORS.join(', ') + ' or custom:<id>)');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The path of one raster tile on an sMap node (interop contract §5, the SDK's rasterUrl): 256 px, or 512 px with `retina`. */
|
|
31
|
+
export function rasterPath(style, z, x, y, options) {
|
|
32
|
+
const o = options || {};
|
|
33
|
+
const url = builder('rasterUrl')(checkStyle(style), { lang: o.lang || o.language, retina: !!o.retina, baseUrl: PLACEHOLDER });
|
|
34
|
+
return pathOf(url).replace('{z}', String(z | 0)).replace('{x}', String(x | 0)).replace('{y}', String(y | 0));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The path of a style document (the SDK's styleUrl): a scheme or a saved custom style, with labels in `lang`. */
|
|
38
|
+
export function stylePath(style, lang) {
|
|
39
|
+
return pathOf(builder('styleUrl')(checkStyle(style), { lang: lang, baseUrl: PLACEHOLDER }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function strip(url) { return String(url).replace(/\/$/, ''); }
|
|
43
|
+
|
|
44
|
+
export class SMapAccess {
|
|
45
|
+
/**
|
|
46
|
+
* @param {Object} [o] baseUrl, token, getToken, apiKey, nodes, nearest, fetch, timeoutMs (as the JS client), or
|
|
47
|
+
* `client`: a JS client of your own.
|
|
48
|
+
*/
|
|
49
|
+
constructor(o) {
|
|
50
|
+
o = o || {};
|
|
51
|
+
this.client = o.client || new SMapClient({
|
|
52
|
+
baseUrl: o.baseUrl || DEFAULT_BASE,
|
|
53
|
+
token: o.token,
|
|
54
|
+
getToken: o.getToken,
|
|
55
|
+
apiKey: o.apiKey,
|
|
56
|
+
nodes: o.nodes,
|
|
57
|
+
nearest: o.nearest,
|
|
58
|
+
fetch: o.fetch,
|
|
59
|
+
timeoutMs: o.timeoutMs,
|
|
60
|
+
});
|
|
61
|
+
this.baseUrl = strip(this.client.baseUrl);
|
|
62
|
+
this.apiKey = o.apiKey || null;
|
|
63
|
+
this._getToken = o.getToken || null;
|
|
64
|
+
this._token = o.token || null;
|
|
65
|
+
this._seeds = (o.nodes || []).map(strip);
|
|
66
|
+
this._order = null;
|
|
67
|
+
this._timer = null;
|
|
68
|
+
this._starting = null;
|
|
69
|
+
this._stopped = false;
|
|
70
|
+
this._listeners = [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reads the first token and the node order; resolves once both are known (never rejects for the nodes). */
|
|
74
|
+
start() {
|
|
75
|
+
if (!this._starting) {
|
|
76
|
+
const self = this;
|
|
77
|
+
this._starting = Promise.all([
|
|
78
|
+
this.refresh(),
|
|
79
|
+
this.client.nodes().then(function (order) { self._order = order; }, function () { self._order = [self.baseUrl]; }),
|
|
80
|
+
]).then(function () { return self; });
|
|
81
|
+
}
|
|
82
|
+
return this._starting;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Asks the client for the current token (getToken is called when it is missing or expires within 2 minutes). */
|
|
86
|
+
refresh() {
|
|
87
|
+
const self = this;
|
|
88
|
+
return this.client.token().then(function (t) {
|
|
89
|
+
const changed = t !== self._token;
|
|
90
|
+
self._token = t || null;
|
|
91
|
+
self._schedule();
|
|
92
|
+
if (changed) { self._emit('token', self._token); }
|
|
93
|
+
return self._token;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_schedule() {
|
|
98
|
+
clearTimeout(this._timer);
|
|
99
|
+
this._timer = null;
|
|
100
|
+
if (this._stopped || !this._getToken || !this._token) { return; }
|
|
101
|
+
const exp = SMapClient.tokenExpiry(this._token);
|
|
102
|
+
if (!exp) { return; }
|
|
103
|
+
// One second past the client's renewal point (2 minutes before expiry), so the client asks getToken again.
|
|
104
|
+
const wait = Math.max(5, exp - Math.floor(Date.now() / 1000) - 119) * 1000;
|
|
105
|
+
const self = this;
|
|
106
|
+
this._timer = setTimeout(function () {
|
|
107
|
+
self.refresh().catch(function () {
|
|
108
|
+
clearTimeout(self._timer);
|
|
109
|
+
self._timer = setTimeout(function () { self._schedule(); self.refresh().catch(function () {}); }, 30000);
|
|
110
|
+
});
|
|
111
|
+
}, Math.min(wait, 2147483000));
|
|
112
|
+
if (this._timer && typeof this._timer.unref === 'function') { this._timer.unref(); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The token to send now (null without one). */
|
|
116
|
+
token() { return this._token; }
|
|
117
|
+
|
|
118
|
+
/** The nodes in the order they are tried, nearest first (baseUrl alone before start() resolves). */
|
|
119
|
+
nodes() { return this._order ? this._order.slice() : [this.baseUrl]; }
|
|
120
|
+
|
|
121
|
+
/** The node requests go to now. */
|
|
122
|
+
head() { return (this._order && this._order[0]) || this.baseUrl; }
|
|
123
|
+
|
|
124
|
+
_nodeOf(url) {
|
|
125
|
+
const u = String(url);
|
|
126
|
+
const all = [this.baseUrl].concat(this._seeds, this._order || []);
|
|
127
|
+
for (let i = 0; i < all.length; i++) {
|
|
128
|
+
const n = all[i];
|
|
129
|
+
if (u === n || u.indexOf(n + '/') === 0 || u.indexOf(n + '?') === 0) { return n; }
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** True for a URL on the sMap service (baseUrl, a seed or a listed node). */
|
|
135
|
+
isOurs(url) { return this._nodeOf(url) !== null; }
|
|
136
|
+
|
|
137
|
+
/** The headers an sMap request carries (token, map key); none for any other host. */
|
|
138
|
+
headers(url) {
|
|
139
|
+
const h = {};
|
|
140
|
+
if (!this.isOurs(url)) { return h; }
|
|
141
|
+
if (this._token) { h.Authorization = 'Bearer ' + this._token; }
|
|
142
|
+
if (this.apiKey) { h['X-SMap-Key'] = this.apiKey; }
|
|
143
|
+
return h;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** An sMap URL moved onto the node requests go to now; other URLs unchanged. */
|
|
147
|
+
rewrite(url) {
|
|
148
|
+
const n = this._nodeOf(url);
|
|
149
|
+
const head = this.head();
|
|
150
|
+
if (!n || n === head) { return url; }
|
|
151
|
+
return head + String(url).slice(n.length);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Reports that a request to `url` failed: its node is tried last from now on. */
|
|
155
|
+
failed(url) {
|
|
156
|
+
const n = this._nodeOf(url);
|
|
157
|
+
if (!n || !this._order || this._order.length < 2) { return; }
|
|
158
|
+
// The client's own order is the one moved, so its requests and these rewrites agree.
|
|
159
|
+
if (typeof this.client.demote === 'function') { this.client.demote(n); }
|
|
160
|
+
else {
|
|
161
|
+
const i = this._order.indexOf(n);
|
|
162
|
+
if (i >= 0) { this._order.splice(i, 1); this._order.push(n); }
|
|
163
|
+
}
|
|
164
|
+
this._emit('nodes', this.nodes());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A path appended with the map key (styles and TileJSON: the service writes it into the tile URLs). */
|
|
168
|
+
withKey(path) {
|
|
169
|
+
if (!this.apiKey) { return path; }
|
|
170
|
+
return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'key=' + encodeURIComponent(this.apiKey);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A style document (a scheme or `custom:<id>`) from the nearest node that answers. */
|
|
174
|
+
style(style, lang) {
|
|
175
|
+
return this.client.request(this.withKey(stylePath(style, lang))).then(function (r) { return r.json(); });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** One raster tile as a Blob, from the nearest node that answers (contract §5). */
|
|
179
|
+
rasterTile(style, z, x, y, options) {
|
|
180
|
+
return this.client.request(rasterPath(style, z, x, y, options)).then(function (r) { return r.blob(); });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A `{z}/{x}/{y}` raster URL on the current node, for tools that load images by URL (map key in the query). */
|
|
184
|
+
rasterTemplate(style, options) {
|
|
185
|
+
const o = options || {};
|
|
186
|
+
return builder('rasterUrl')(checkStyle(style), { lang: o.lang || o.language, retina: !!o.retina, baseUrl: this.head(), apiKey: this.apiKey || undefined });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Called with ('token', token) after a renewal and ('nodes', order) after a node failed. */
|
|
190
|
+
on(fn) { this._listeners.push(fn); return this; }
|
|
191
|
+
|
|
192
|
+
_emit(type, value) { this._listeners.slice().forEach(function (fn) { try { fn(type, value); } catch (e) { /* listener error */ } }); }
|
|
193
|
+
|
|
194
|
+
/** Stops renewing the token. */
|
|
195
|
+
stop() { this._stopped = true; clearTimeout(this._timer); this._timer = null; this._listeners = []; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { SMapClient };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// sMap tool adapters — the sMap badge and the data credit shown in another tool's corner. Kept identical in leaflet/,
|
|
2
|
+
// openlayers/ and mapbox/ (test/common.test.mjs checks).
|
|
3
|
+
|
|
4
|
+
/** The data credit every sMap map keeps visible (the data licence requires it). */
|
|
5
|
+
export const OSM_ATTRIBUTION = '<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a>';
|
|
6
|
+
|
|
7
|
+
const ICON = '<svg viewBox="0 0 56 56" width="14" height="14" aria-hidden="true" focusable="false">' +
|
|
8
|
+
'<rect x="2" y="2" width="52" height="52" rx="13" fill="#2563eb"/>' +
|
|
9
|
+
'<path d="M38 15H22a8 8 0 0 0 0 16h12a8 8 0 0 1 0 16H18" fill="none" stroke="#fff" stroke-width="7" ' +
|
|
10
|
+
'stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
11
|
+
|
|
12
|
+
const CSS = [
|
|
13
|
+
'.smap-adapter-badge{display:flex;align-items:center;gap:4px;height:20px;padding:0 7px 0 3px;border-radius:10px;',
|
|
14
|
+
'background:rgba(255,255,255,.82);box-shadow:0 0 0 1px rgba(15,23,42,.12);color:#1e293b;',
|
|
15
|
+
'font:600 11px/20px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;',
|
|
16
|
+
'letter-spacing:.01em;pointer-events:none;-webkit-user-select:none;user-select:none;margin:4px 6px;width:max-content}',
|
|
17
|
+
'.smap-adapter-badge svg{display:block;flex:none;width:14px;height:14px}',
|
|
18
|
+
// An sMap map drawn inside another tool: the tool takes the input; only popups take clicks; the tool's corner
|
|
19
|
+
// shows the badge and the credit, so the embedded map's own are hidden.
|
|
20
|
+
'.smap-embedded{position:absolute;top:0;left:0;pointer-events:none;overflow:hidden}',
|
|
21
|
+
'.smap-embedded .smap-badge,.smap-embedded .maplibregl-ctrl-attrib{display:none!important}',
|
|
22
|
+
'.smap-embedded .maplibregl-popup{pointer-events:auto}',
|
|
23
|
+
].join('');
|
|
24
|
+
|
|
25
|
+
/** Adds the adapters' stylesheet to a document once. */
|
|
26
|
+
export function injectCss(doc) {
|
|
27
|
+
if (!doc || !doc.head || doc.getElementById('smap-adapter-css')) { return; }
|
|
28
|
+
const style = doc.createElement('style');
|
|
29
|
+
style.id = 'smap-adapter-css';
|
|
30
|
+
style.textContent = CSS;
|
|
31
|
+
doc.head.appendChild(style);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The badge element (not a link): the sMap icon and name on a semi-transparent pill. */
|
|
35
|
+
export function createBadge(doc) {
|
|
36
|
+
injectCss(doc);
|
|
37
|
+
const el = doc.createElement('div');
|
|
38
|
+
el.className = 'smap-adapter-badge';
|
|
39
|
+
el.setAttribute('aria-label', 'sMap');
|
|
40
|
+
el.innerHTML = ICON + '<span>sMap</span>';
|
|
41
|
+
return el;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The credits to show: the data credit first, then every other credit a source names (once each; a source's own
|
|
46
|
+
* OpenStreetMap credit is the data credit and is not repeated).
|
|
47
|
+
*/
|
|
48
|
+
export function mergeAttributions(list) {
|
|
49
|
+
const out = [OSM_ATTRIBUTION];
|
|
50
|
+
(list || []).forEach(function (a) {
|
|
51
|
+
if (!a || typeof a !== 'string') { return; }
|
|
52
|
+
if (/openstreetmap/i.test(a)) { return; }
|
|
53
|
+
if (out.indexOf(a) < 0) { out.push(a); }
|
|
54
|
+
});
|
|
55
|
+
return out;
|
|
56
|
+
}
|