@superrouteca/smap-arcgis 1.1.0 → 1.1.2
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 +24 -0
- package/README.md +20 -1
- package/package.json +3 -2
- package/src/basemap.js +129 -3
- package/src/index.js +2 -1
- package/src/style.js +251 -0
- package/types/index.d.ts +24 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.2 — 2026-10-27
|
|
4
|
+
|
|
5
|
+
- Restyling quickly no longer logs errors: `setStyle()`, `setLanguage()` and `setBasemap()` called in quick
|
|
6
|
+
succession are applied one at a time and the latest call wins. A call that a newer one replaced resolves quietly
|
|
7
|
+
instead of rejecting with "Request canceled"; a real failure (network, a style that does not load) still rejects.
|
|
8
|
+
- A restyle started while the map switches between the raster and the vector basemap waits until the vector layer is
|
|
9
|
+
shown again and then applies to it, and a basemap switched away from during a restyle no longer reports "Cannot read
|
|
10
|
+
properties of null (reading 'setStyleResources')". Removing the basemap during a restyle throws nothing.
|
|
11
|
+
|
|
12
|
+
## 1.1.1 — 2026-10-27
|
|
13
|
+
|
|
14
|
+
- The vector basemap draws: water, land, roads, labels and icons appear in every colour scheme and label language
|
|
15
|
+
(with 1.1.0, `createSMapBasemap()` and `createVectorTileLayer()` loaded without an error and drew nothing). The style
|
|
16
|
+
is converted into what ArcGIS's vector tile layer supports before it is handed over; sMap's own style is unchanged.
|
|
17
|
+
- What the conversion simplifies, compared with the sMap web map: a label shows the name in the label language and,
|
|
18
|
+
for a name in another script, the local name on a second line, in one font (the per-script fonts are not used);
|
|
19
|
+
labels that sMap places at one of several positions keep the first position; layer properties ArcGIS does not
|
|
20
|
+
have are left out, and 3D buildings are drawn flat. `layer.smap.simplified` lists every simplification of the
|
|
21
|
+
style shown.
|
|
22
|
+
- `toArcGISStyle()` converts any style the same way (for a `VectorTileLayer` of your own); `convertStyle: false`
|
|
23
|
+
hands sMap's style to ArcGIS unchanged.
|
|
24
|
+
- Chinese, Japanese and Korean labels show in the vector basemap: sMap 1.1.1 serves them in its fonts. Chinese
|
|
25
|
+
characters take the Simplified Chinese form in every language.
|
|
26
|
+
|
|
3
27
|
## 1.1.0 — 2026-10-27
|
|
4
28
|
|
|
5
29
|
- First version.
|
package/README.md
CHANGED
|
@@ -44,10 +44,29 @@ An existing ArcGIS app switches its basemap and keeps every layer: `map.basemap
|
|
|
44
44
|
- `basemap: { pois: { visible: false }, roads: { color: '#ffffff' } }` shows, hides and recolours the basemap by
|
|
45
45
|
class (vector), as the sMap map's `setBasemap`.
|
|
46
46
|
- `basemap.smap.setStyle(style)` / `setLanguage(language)` / `setBasemap(overrides)` (vector) restyle the layer in
|
|
47
|
-
place.
|
|
47
|
+
place. Calls in quick succession (buttons clicked fast) are applied one at a time and the latest call wins: a state
|
|
48
|
+
replaced before it loaded is skipped, and a style is loaded only once the layer view is ready (also right after the
|
|
49
|
+
map's basemap was switched back to it). Each call returns a promise of the layer: the latest call resolves when its
|
|
50
|
+
state is shown and rejects when it fails (network, a style that does not load); a call that a newer one superseded
|
|
51
|
+
resolves with the layer once the newer call has settled and never rejects, so handlers need no `catch` for it.
|
|
48
52
|
|
|
49
53
|
The layers on their own: `createVectorTileLayer(options)` and `createRasterTileLayer(options)`.
|
|
50
54
|
|
|
55
|
+
### The vector style in ArcGIS
|
|
56
|
+
|
|
57
|
+
ArcGIS's `VectorTileLayer` supports part of the Style Specification, so the vector basemap converts sMap's style
|
|
58
|
+
before handing it over (`toArcGISStyle(style)` does the same for a `VectorTileLayer` of your own; `convertStyle: false`
|
|
59
|
+
hands the style over unchanged). Compared with the sMap web map:
|
|
60
|
+
|
|
61
|
+
- a label shows the name in the label language and, for a name in another script, the local name on a second line,
|
|
62
|
+
in one font;
|
|
63
|
+
- a label that sMap places at one of several positions keeps the first one;
|
|
64
|
+
- layer properties ArcGIS does not have are left out, and 3D buildings are drawn flat;
|
|
65
|
+
- Chinese, Japanese and Korean labels are drawn with sMap's own fonts (sMap 1.1.1 or later); Chinese characters take
|
|
66
|
+
the Simplified Chinese form in every language.
|
|
67
|
+
|
|
68
|
+
`basemap.smap.simplified` lists each simplification of the style shown (`"<layer>: <what>"`).
|
|
69
|
+
|
|
51
70
|
### Keys, tokens and nodes
|
|
52
71
|
|
|
53
72
|
`createSMapBasemap` (and the layer functions) install one request interceptor per session
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superrouteca/smap-arcgis",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "sMap for the ArcGIS Maps SDK for JavaScript: sMap basemaps (vector and raster), sMap layers as graphics, and ArcGIS REST services on sMap maps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Goodneo Solutions",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"typecheck": "tsc -p test/tsconfig.json"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@superrouteca/smap": "1.1.
|
|
53
|
+
"@superrouteca/smap": "1.1.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@arcgis/core": "^5.0.0"
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@arcgis/core": "5.1.27",
|
|
65
|
+
"@maplibre/maplibre-gl-style-spec": "26.4.4",
|
|
65
66
|
"typescript": "6.0.3"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/src/basemap.js
CHANGED
|
@@ -8,6 +8,7 @@ import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js';
|
|
|
8
8
|
import WebTileLayer from '@arcgis/core/layers/WebTileLayer.js';
|
|
9
9
|
import { applyBasemapOverrides } from '@superrouteca/smap';
|
|
10
10
|
import { createSession, ATTRIBUTION } from './session.js';
|
|
11
|
+
import { toArcGISStyle } from './style.js';
|
|
11
12
|
|
|
12
13
|
const installed = new WeakMap();
|
|
13
14
|
|
|
@@ -61,6 +62,122 @@ export function installInterceptor(sessionOrOptions) {
|
|
|
61
62
|
return handle;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
function isAbort(error) {
|
|
66
|
+
return !!error && (error.name === 'AbortError' || error.name === 'cancel' || /^cancell?ed:/.test(String(error.name || '')));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The views that drew the layer (learnt from its layerview-create events), the layer views it has now, and a guard
|
|
71
|
+
* on each of those layer views: ArcGIS 5.x's VectorTileLayerView2D reads its tile container after awaiting the
|
|
72
|
+
* sprites of a new style, and the container is gone when the layer view was detached meanwhile (the basemap switched
|
|
73
|
+
* away during a restyle), which ArcGIS raised as an unhandled "Cannot read properties of null
|
|
74
|
+
* (reading 'setStyleResources')". A failure of a layer view that is no longer attached is dropped; any other is kept.
|
|
75
|
+
*/
|
|
76
|
+
function trackLayerViews(layer) {
|
|
77
|
+
const views = new Set();
|
|
78
|
+
const layerViews = new Set();
|
|
79
|
+
const guard = (lv) => {
|
|
80
|
+
if (!lv || lv.__smapGuarded || typeof lv._loadStyle !== 'function') { return; }
|
|
81
|
+
const own = lv._loadStyle;
|
|
82
|
+
lv._loadStyle = function smapGuardedLoadStyle(...args) {
|
|
83
|
+
const gone = () => this.destroyed || this.attached === false || !this._vectorTileContainer;
|
|
84
|
+
try {
|
|
85
|
+
return Promise.resolve(own.apply(this, args)).catch((error) => { if (gone()) { return undefined; } throw error; });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (gone()) { return Promise.resolve(); }
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
lv.__smapGuarded = true;
|
|
92
|
+
};
|
|
93
|
+
if (typeof layer.on === 'function') {
|
|
94
|
+
layer.on('layerview-create', (e) => {
|
|
95
|
+
if (e && e.view) { views.add(e.view); }
|
|
96
|
+
if (e && e.layerView) { layerViews.add(e.layerView); guard(e.layerView); }
|
|
97
|
+
});
|
|
98
|
+
layer.on('layerview-destroy', (e) => { if (e && e.layerView) { layerViews.delete(e.layerView); } });
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
/**
|
|
102
|
+
* Resolves once every view that shows the layer has its layer view (one being created after a basemap switch is
|
|
103
|
+
* waited for); a view that drops the layer meanwhile is left out. Never rejects.
|
|
104
|
+
*/
|
|
105
|
+
async settled() {
|
|
106
|
+
const waits = [];
|
|
107
|
+
for (const view of views) {
|
|
108
|
+
if (!view || view.destroyed) { views.delete(view); continue; }
|
|
109
|
+
const all = view.map && view.map.allLayers;
|
|
110
|
+
const shown = all && typeof all.includes === 'function' ? all.includes(layer) : false;
|
|
111
|
+
if (!shown || typeof view.whenLayerView !== 'function') { continue; }
|
|
112
|
+
waits.push(Promise.resolve().then(() => view.whenLayerView(layer)).then((lv) => {
|
|
113
|
+
guard(lv);
|
|
114
|
+
return typeof (lv && lv.when) === 'function' ? lv.when() : lv;
|
|
115
|
+
}).catch(() => undefined));
|
|
116
|
+
}
|
|
117
|
+
await Promise.all(waits);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Restyles `layer` with what `styled()` gives for the state wanted now. Calls are serialised and coalesced, so the
|
|
124
|
+
* latest call wins: one restyle runs at a time, a state that was replaced before it was loaded is not loaded, and the
|
|
125
|
+
* style is loaded only once the layer's layer views are ready (never into one being re-created after a basemap
|
|
126
|
+
* switch). Each call returns a promise of the layer: the latest call resolves when its state is shown and rejects
|
|
127
|
+
* when it fails (network, a style that does not load); a call that a newer one superseded resolves with the layer once
|
|
128
|
+
* the newer call has settled, and never rejects. A destroyed layer (the basemap removed) resolves every call.
|
|
129
|
+
*/
|
|
130
|
+
function restyler(layer, styled) {
|
|
131
|
+
const views = trackLayerViews(layer);
|
|
132
|
+
let wanted = 0;
|
|
133
|
+
let running = false;
|
|
134
|
+
const waiting = [];
|
|
135
|
+
const gone = () => !!layer.destroyed;
|
|
136
|
+
const settle = (upTo, error) => {
|
|
137
|
+
for (let i = waiting.length - 1; i >= 0; i--) {
|
|
138
|
+
const w = waiting[i];
|
|
139
|
+
if (w.gen > upTo) { continue; }
|
|
140
|
+
waiting.splice(i, 1);
|
|
141
|
+
if (error && w.gen === upTo) { w.reject(error); } else { w.resolve(layer); }
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const run = async () => {
|
|
145
|
+
running = true;
|
|
146
|
+
let aborts = 0;
|
|
147
|
+
try {
|
|
148
|
+
while (waiting.length) {
|
|
149
|
+
const gen = wanted;
|
|
150
|
+
if (gone()) { settle(gen); break; }
|
|
151
|
+
try {
|
|
152
|
+
const doc = await styled();
|
|
153
|
+
if (gen !== wanted || gone()) { continue; }
|
|
154
|
+
await views.settled();
|
|
155
|
+
if (gen !== wanted || gone()) { continue; }
|
|
156
|
+
await layer.loadStyle(doc);
|
|
157
|
+
if (gen !== wanted) { continue; }
|
|
158
|
+
aborts = 0;
|
|
159
|
+
settle(gen);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (gone()) { settle(wanted); break; }
|
|
162
|
+
if (gen !== wanted) { continue; }
|
|
163
|
+
// Cancelled by something else than this adapter (another loadStyle on the layer): try again, twice at most.
|
|
164
|
+
if (isAbort(error) && aborts++ < 2) { continue; }
|
|
165
|
+
aborts = 0;
|
|
166
|
+
settle(gen, error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
running = false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
return () => {
|
|
174
|
+
const gen = ++wanted;
|
|
175
|
+
const promise = new Promise((resolve, reject) => { waiting.push({ gen, resolve, reject }); });
|
|
176
|
+
if (!running) { run(); }
|
|
177
|
+
return promise;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
64
181
|
/**
|
|
65
182
|
* A VectorTileLayer drawing sMap's vector tiles in a scheme or custom style, labels in `language`. The style is read
|
|
66
183
|
* through the session (nearest node, failover, key, token); tiles, fonts and sprites through the interceptor.
|
|
@@ -76,10 +193,17 @@ export async function createVectorTileLayer(options = {}) {
|
|
|
76
193
|
let style = options.style || 'light';
|
|
77
194
|
let language = options.language;
|
|
78
195
|
let overrides = options.basemap || null;
|
|
79
|
-
|
|
196
|
+
let simplified = [];
|
|
197
|
+
// Basemap options (contract §1: show, hide and recolour by class), applied by the SDK's own function; then the
|
|
198
|
+
// style is converted into what ArcGIS's VectorTileLayer draws (style.js; `convertStyle: false` hands it over as is).
|
|
80
199
|
const styled = async () => {
|
|
81
200
|
const doc = await session.style(style, language);
|
|
82
|
-
|
|
201
|
+
const own = overrides && Object.keys(overrides).length ? applyBasemapOverrides(doc, overrides) : doc;
|
|
202
|
+
if (options.convertStyle === false) { simplified = []; return own; }
|
|
203
|
+
const changes = [];
|
|
204
|
+
const out = toArcGISStyle(own, { language, changes });
|
|
205
|
+
simplified = changes;
|
|
206
|
+
return out;
|
|
83
207
|
};
|
|
84
208
|
const layer = new VectorTileLayer({
|
|
85
209
|
id: options.id || 'smap-vector',
|
|
@@ -87,12 +211,14 @@ export async function createVectorTileLayer(options = {}) {
|
|
|
87
211
|
style: await styled(),
|
|
88
212
|
...(options.layerProps || {}),
|
|
89
213
|
});
|
|
90
|
-
const reload =
|
|
214
|
+
const reload = restyler(layer, styled);
|
|
91
215
|
layer.smap = {
|
|
92
216
|
session,
|
|
93
217
|
get style() { return style; },
|
|
94
218
|
get language() { return language; },
|
|
95
219
|
basemap() { return overrides ? { ...overrides } : {}; },
|
|
220
|
+
/** What the conversion for ArcGIS simplified in the style now shown, one line per layer and property. */
|
|
221
|
+
get simplified() { return simplified.slice(); },
|
|
96
222
|
setStyle(next) { style = next; return reload(); },
|
|
97
223
|
setLanguage(next) { language = next; return reload(); },
|
|
98
224
|
setBasemap(next) { overrides = next && Object.keys(next).length ? { ...next } : null; return reload(); },
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @superrouteca/smap-arcgis — sMap and the ArcGIS Maps SDK for JavaScript both ways.
|
|
2
2
|
export { Session, createSession, FLAVORS, ATTRIBUTION } from './session.js';
|
|
3
3
|
export { installInterceptor, createVectorTileLayer, createRasterTileLayer, createSMapBasemap, addAttribution } from './basemap.js';
|
|
4
|
+
export { toArcGISStyle, styleLanguage, nameExpression } from './style.js';
|
|
4
5
|
export { createGraphicsLayer, symbolFor, toArcGisGeometries, toFeatures, zoomToScale } from './graphics.js';
|
|
5
6
|
export { addArcGisService, queryFeatures, serviceInfo, parseServiceUrl, withToken, symbolToSimplestyle, rendererStyle } from './services.js';
|
|
6
|
-
export const version = '1.1.
|
|
7
|
+
export const version = '1.1.2';
|
package/src/style.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// sMap's styles made readable by the ArcGIS Maps SDK's VectorTileLayer, which implements only part of the style
|
|
2
|
+
// specification. Measured against @arcgis/core 5.1.27 (its views/2d/engine/vectorTiles: expression.js, Filter.js,
|
|
3
|
+
// StyleProperty.js, StyleDefinition.js) and checked in a browser:
|
|
4
|
+
//
|
|
5
|
+
// - `is-supported-script` is not implemented: a text-field that uses it fails to parse and the tiles holding such
|
|
6
|
+
// features draw nothing at all (with sMap's label expressions: the whole basemap stays empty). It becomes `true`.
|
|
7
|
+
// - `format` joins its sections with commas: one section becomes its text, several are joined with `concat`.
|
|
8
|
+
// - `coalesce` stops at the first value that is not null, but a missing property reads as undefined there, so
|
|
9
|
+
// `coalesce(get name:en, get name)` gives nothing for a feature without name:en: a chain of `get` becomes
|
|
10
|
+
// `case(has …)`.
|
|
11
|
+
// - a filter that mixes the legacy syntax and expressions (`["all", ["==", "kind", "x"], ["!", ["has", "y"]]]`) is
|
|
12
|
+
// converted as if it were all legacy and breaks: every filter is written as an expression.
|
|
13
|
+
// - properties it has no definition for (text-variable-anchor, text-radial-offset, the *-sort-key of fill / line /
|
|
14
|
+
// circle, line-gradient, …) are left out or approximated, layer types it cannot draw are left out (fill-extrusion
|
|
15
|
+
// becomes a flat fill).
|
|
16
|
+
//
|
|
17
|
+
// The server's style is never changed: the conversion works on a copy. `changes`, when given, receives one line per
|
|
18
|
+
// simplification.
|
|
19
|
+
|
|
20
|
+
const UNIMPLEMENTED = new Set(['collator', 'image', 'number-format', 'accumulated', 'feature-state', 'line-progress',
|
|
21
|
+
'within', 'heatmap-density', 'distance', 'global-state', 'elevation']);
|
|
22
|
+
const COMPARISONS = new Set(['==', '!=', '<', '<=', '>', '>=']);
|
|
23
|
+
const LAYER_TYPES = new Set(['background', 'fill', 'line', 'symbol', 'circle']);
|
|
24
|
+
// Layout and paint properties of the style specification that @arcgis/core 5.1.27 has no definition for.
|
|
25
|
+
const DROPPED = new Set(['fill-sort-key', 'fill-layer-opacity', 'line-sort-key', 'line-gradient', 'line-layer-opacity',
|
|
26
|
+
'icon-pitch-alignment', 'icon-text-fit', 'icon-text-fit-padding', 'symbol-height-anchor', 'symbol-height-offset',
|
|
27
|
+
'symbol-z-order', 'text-pitch-alignment', 'text-variable-anchor-offset', 'circle-sort-key', 'circle-pitch-alignment',
|
|
28
|
+
'circle-pitch-scale']);
|
|
29
|
+
const ANCHOR_OFFSET = { center: [0, 0], top: [0, 1], bottom: [0, -1], left: [1, 0], right: [-1, 0],
|
|
30
|
+
'top-left': [0.7, 0.7], 'top-right': [-0.7, 0.7], 'bottom-left': [0.7, -0.7], 'bottom-right': [-0.7, -0.7] };
|
|
31
|
+
|
|
32
|
+
class Unsupported extends Error {}
|
|
33
|
+
|
|
34
|
+
const isGet = (e) => Array.isArray(e) && e[0] === 'get' && e.length === 2 && typeof e[1] === 'string';
|
|
35
|
+
const clone = (v) => (v === undefined ? v : JSON.parse(JSON.stringify(v)));
|
|
36
|
+
|
|
37
|
+
/** The language of sMap's labels: the first `name:<lang>` the style reads. */
|
|
38
|
+
export function styleLanguage(style) {
|
|
39
|
+
const m = JSON.stringify((style && style.layers) || []).match(/"name:([A-Za-z-]+)"/);
|
|
40
|
+
return m ? m[1] : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A label in `language` (else the name in the local language), in expressions ArcGIS evaluates. */
|
|
44
|
+
export function nameExpression(language) {
|
|
45
|
+
const chain = [];
|
|
46
|
+
if (language) { chain.push(['get', 'name:' + language]); }
|
|
47
|
+
chain.push(['get', 'pgf:name'], ['get', 'name']);
|
|
48
|
+
return rewrite(['coalesce', ...chain], { note() {} });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Rewrites one expression into what ArcGIS evaluates; throws Unsupported where it cannot. */
|
|
52
|
+
function rewrite(e, ctx) {
|
|
53
|
+
if (!Array.isArray(e) || e.length === 0 || typeof e[0] !== 'string') {
|
|
54
|
+
return Array.isArray(e) ? e.map((x) => rewrite(x, ctx)) : e;
|
|
55
|
+
}
|
|
56
|
+
const op = e[0];
|
|
57
|
+
if (op === 'literal') { return e; }
|
|
58
|
+
if (op === 'is-supported-script') { ctx.note('is-supported-script read as true (local names are always shown)'); return true; }
|
|
59
|
+
if (op === 'resolved-locale') { return ctx.language || 'en'; }
|
|
60
|
+
if (op === 'image') { return rewrite(e[1], ctx); }
|
|
61
|
+
if (op === 'number-format') { return ['to-string', rewrite(e[1], ctx)]; }
|
|
62
|
+
if (op === 'format') {
|
|
63
|
+
const texts = [];
|
|
64
|
+
let options = false;
|
|
65
|
+
for (let i = 1; i < e.length; i++) {
|
|
66
|
+
const a = e[i];
|
|
67
|
+
if (a && typeof a === 'object' && !Array.isArray(a)) { if (Object.keys(a).length) { options = true; } continue; }
|
|
68
|
+
texts.push(rewrite(a, ctx));
|
|
69
|
+
}
|
|
70
|
+
if (texts.length > 1 || options) { ctx.note('format: sections joined into one text' + (options ? ', section fonts and sizes dropped' : '')); }
|
|
71
|
+
return texts.length === 1 ? texts[0] : ['concat', ...texts];
|
|
72
|
+
}
|
|
73
|
+
if (op === 'coalesce') {
|
|
74
|
+
const args = e.slice(1).map((x) => rewrite(x, ctx));
|
|
75
|
+
let out = args[args.length - 1];
|
|
76
|
+
for (let i = args.length - 2; i >= 0; i--) {
|
|
77
|
+
out = isGet(args[i]) ? ['case', ['has', args[i][1]], args[i], out] : ['coalesce', args[i], out];
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
if (COMPARISONS.has(op) && e.length === 4) { ctx.note(op + ': collator dropped'); return [op, rewrite(e[1], ctx), rewrite(e[2], ctx)]; }
|
|
82
|
+
if (UNIMPLEMENTED.has(op)) { throw new Unsupported(op); }
|
|
83
|
+
return [op, ...e.slice(1).map((x) => rewrite(x, ctx))];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// maplibre's test of whether a filter is written as an expression (the legacy syntax otherwise).
|
|
87
|
+
function isExpressionFilter(f) {
|
|
88
|
+
if (f === true || f === false) { return true; }
|
|
89
|
+
if (!Array.isArray(f) || f.length === 0) { return false; }
|
|
90
|
+
switch (f[0]) {
|
|
91
|
+
case 'has': return f.length >= 2 && f[1] !== '$id' && f[1] !== '$type';
|
|
92
|
+
case 'in': return f.length >= 3 && (typeof f[1] !== 'string' || Array.isArray(f[2]));
|
|
93
|
+
case '!in': case '!has': case 'none': return false;
|
|
94
|
+
case '==': case '!=': case '>': case '>=': case '<': case '<=': return f.length !== 3 || Array.isArray(f[1]) || Array.isArray(f[2]);
|
|
95
|
+
case 'any': case 'all':
|
|
96
|
+
for (const x of f.slice(1)) { if (!isExpressionFilter(x) && typeof x !== 'boolean') { return false; } }
|
|
97
|
+
return true;
|
|
98
|
+
default: return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function legacyKey(k) { return k === '$type' ? ['geometry-type'] : k === '$id' ? ['id'] : ['get', k]; }
|
|
103
|
+
|
|
104
|
+
// A legacy filter as an expression (the style specification's own conversion, without its type guards).
|
|
105
|
+
function legacyToExpression(f) {
|
|
106
|
+
if (!Array.isArray(f) || f.length === 0) { return true; }
|
|
107
|
+
const [op, ...a] = f;
|
|
108
|
+
switch (op) {
|
|
109
|
+
case 'any': case 'all': return [op, ...a.map(filterExpression)];
|
|
110
|
+
case 'none': return ['!', ['any', ...a.map(filterExpression)]];
|
|
111
|
+
case 'has': return a[0] === '$type' ? true : a[0] === '$id' ? ['has-id'] : ['has', a[0]];
|
|
112
|
+
case '!has': return ['!', legacyToExpression(['has', a[0]])];
|
|
113
|
+
case 'in': return ['in', legacyKey(a[0]), ['literal', a.slice(1)]];
|
|
114
|
+
case '!in': return ['!', ['in', legacyKey(a[0]), ['literal', a.slice(1)]]];
|
|
115
|
+
case '!=': return ['!', ['==', legacyKey(a[0]), a[1]]];
|
|
116
|
+
case '==': case '<': case '<=': case '>': case '>=': return [op, legacyKey(a[0]), a[1]];
|
|
117
|
+
default: return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function filterExpression(f) {
|
|
122
|
+
if (typeof f === 'boolean') { return f; }
|
|
123
|
+
return isExpressionFilter(f) ? f : legacyToExpression(f);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ArcGIS reads a bare boolean inside all / any as a legacy filter (and then the whole filter as legacy): booleans are
|
|
127
|
+
// folded away.
|
|
128
|
+
function foldBooleans(f) {
|
|
129
|
+
if (!Array.isArray(f) || (f[0] !== 'all' && f[0] !== 'any' && f[0] !== '!')) { return f; }
|
|
130
|
+
if (f[0] === '!') { const x = foldBooleans(f[1]); return typeof x === 'boolean' ? !x : ['!', x]; }
|
|
131
|
+
const all = f[0] === 'all';
|
|
132
|
+
const kept = [];
|
|
133
|
+
for (const x of f.slice(1).map(foldBooleans)) {
|
|
134
|
+
if (x === all) { continue; }
|
|
135
|
+
if (x === !all) { return !all; }
|
|
136
|
+
kept.push(x);
|
|
137
|
+
}
|
|
138
|
+
return kept.length === 0 ? all : kept.length === 1 ? kept[0] : [f[0], ...kept];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// text-variable-anchor (and text-radial-offset) as one text-anchor and text-offset: the first anchor of each list.
|
|
142
|
+
function fixedAnchor(layout, note) {
|
|
143
|
+
const first = (v) => (Array.isArray(v) && v[0] === 'literal' ? first(v[1]) : Array.isArray(v) ? v[0] : v);
|
|
144
|
+
const anchors = layout['text-variable-anchor'];
|
|
145
|
+
const radial = layout['text-radial-offset'];
|
|
146
|
+
delete layout['text-variable-anchor'];
|
|
147
|
+
delete layout['text-radial-offset'];
|
|
148
|
+
if (anchors === undefined) {
|
|
149
|
+
if (radial !== undefined) { note('text-radial-offset dropped'); }
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const r = typeof radial === 'number' ? radial : 0;
|
|
153
|
+
const offset = (a) => (ANCHOR_OFFSET[a] || [0, 0]).map((x) => x * r);
|
|
154
|
+
if (Array.isArray(anchors) && anchors[0] === 'step') {
|
|
155
|
+
const anchor = ['step', anchors[1]];
|
|
156
|
+
const off = ['step', anchors[1]];
|
|
157
|
+
for (let i = 2; i < anchors.length; i++) {
|
|
158
|
+
if (i % 2 === 1) { anchor.push(anchors[i]); off.push(anchors[i]); continue; }
|
|
159
|
+
const a = first(anchors[i]);
|
|
160
|
+
anchor.push(a);
|
|
161
|
+
off.push(['literal', offset(a)]);
|
|
162
|
+
}
|
|
163
|
+
if (layout['text-anchor'] === undefined) { layout['text-anchor'] = anchor; }
|
|
164
|
+
if (r && layout['text-offset'] === undefined) { layout['text-offset'] = off; }
|
|
165
|
+
} else {
|
|
166
|
+
const a = first(anchors);
|
|
167
|
+
if (layout['text-anchor'] === undefined && typeof a === 'string') { layout['text-anchor'] = a; }
|
|
168
|
+
if (r && layout['text-offset'] === undefined) { layout['text-offset'] = offset(a); }
|
|
169
|
+
}
|
|
170
|
+
note('text-variable-anchor: the first anchor only' + (radial !== undefined ? ', text-radial-offset as text-offset' : ''));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function convertLayer(layer, ctx) {
|
|
174
|
+
const l = clone(layer);
|
|
175
|
+
const note = (what) => ctx.changes.push(l.id + ': ' + what);
|
|
176
|
+
if (l.type === 'fill-extrusion') {
|
|
177
|
+
l.type = 'fill';
|
|
178
|
+
const p = l.paint || {};
|
|
179
|
+
l.paint = {};
|
|
180
|
+
if (p['fill-extrusion-color'] !== undefined) { l.paint['fill-color'] = p['fill-extrusion-color']; }
|
|
181
|
+
if (p['fill-extrusion-opacity'] !== undefined) { l.paint['fill-opacity'] = p['fill-extrusion-opacity']; }
|
|
182
|
+
if (p['fill-extrusion-pattern'] !== undefined) { l.paint['fill-pattern'] = p['fill-extrusion-pattern']; }
|
|
183
|
+
note('fill-extrusion drawn as a flat fill');
|
|
184
|
+
}
|
|
185
|
+
if (!LAYER_TYPES.has(l.type)) { note('layer type ' + l.type + ' left out'); return null; }
|
|
186
|
+
if (l.type !== 'background' && ctx.sources && !(ctx.sources[l.source] && ctx.sources[l.source].type === 'vector')) {
|
|
187
|
+
note('layer of a ' + ((ctx.sources[l.source] && ctx.sources[l.source].type) || 'missing') + ' source left out');
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const lctx = { language: ctx.language, note };
|
|
191
|
+
if (l.filter !== undefined) {
|
|
192
|
+
try {
|
|
193
|
+
l.filter = foldBooleans(rewrite(filterExpression(l.filter), lctx));
|
|
194
|
+
if (l.filter === true) { delete l.filter; } else if (l.filter === false) { l.filter = ['==', 1, 0]; }
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (!(e instanceof Unsupported)) { throw e; }
|
|
197
|
+
note('filter with ' + e.message + ' dropped');
|
|
198
|
+
delete l.filter;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (l.layout && (l.layout['text-variable-anchor'] !== undefined || l.layout['text-radial-offset'] !== undefined)) { fixedAnchor(l.layout, note); }
|
|
202
|
+
for (const kind of ['layout', 'paint']) {
|
|
203
|
+
const props = l[kind];
|
|
204
|
+
if (!props) { continue; }
|
|
205
|
+
for (const name of Object.keys(props)) {
|
|
206
|
+
if (DROPPED.has(name)) { delete props[name]; note(name + ' dropped'); continue; }
|
|
207
|
+
if (name === 'text-overlap' || name === 'icon-overlap') {
|
|
208
|
+
const v = props[name];
|
|
209
|
+
delete props[name];
|
|
210
|
+
const allow = name.replace('-overlap', '-allow-overlap');
|
|
211
|
+
if (props[allow] === undefined && typeof v === 'string') { props[allow] = v !== 'never'; }
|
|
212
|
+
note(name + ' as ' + allow);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
try { props[name] = rewrite(props[name], lctx); } catch (e) {
|
|
216
|
+
if (!(e instanceof Unsupported)) { throw e; }
|
|
217
|
+
if (name === 'text-field') {
|
|
218
|
+
props[name] = nameExpression(ctx.language);
|
|
219
|
+
note('text-field with ' + e.message + ' read as the name in the label language');
|
|
220
|
+
} else {
|
|
221
|
+
delete props[name];
|
|
222
|
+
note(name + ' with ' + e.message + ' dropped');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return l;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A copy of an sMap style (or any style of the specification) that ArcGIS's VectorTileLayer draws: filters written
|
|
232
|
+
* as expressions, labels without the operators ArcGIS lacks, and properties it has no definition for left out or
|
|
233
|
+
* approximated (see the top of this file).
|
|
234
|
+
*
|
|
235
|
+
* @param {object} style a style document
|
|
236
|
+
* @param {{ language?: string, changes?: string[] }} [options] `language` of the fallback label (read from the
|
|
237
|
+
* style when not given); `changes` receives one line per simplification
|
|
238
|
+
* @returns {object} the converted copy
|
|
239
|
+
*/
|
|
240
|
+
export function toArcGISStyle(style, options = {}) {
|
|
241
|
+
if (!style || !Array.isArray(style.layers)) { return style; }
|
|
242
|
+
const changes = [];
|
|
243
|
+
const ctx = { language: options.language || styleLanguage(style), sources: style.sources || null, changes };
|
|
244
|
+
const out = { ...style, layers: [] };
|
|
245
|
+
for (const layer of style.layers) {
|
|
246
|
+
const l = convertLayer(layer, ctx);
|
|
247
|
+
if (l) { out.layers.push(l); }
|
|
248
|
+
}
|
|
249
|
+
if (Array.isArray(options.changes)) { options.changes.push(...[...new Set(changes)]); }
|
|
250
|
+
return out;
|
|
251
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -81,15 +81,37 @@ export interface SMapLayerInfo {
|
|
|
81
81
|
readonly language?: string;
|
|
82
82
|
}
|
|
83
83
|
export interface SMapVectorInfo extends SMapLayerInfo {
|
|
84
|
+
/**
|
|
85
|
+
* setStyle / setLanguage / setBasemap are applied one at a time and the latest call wins. The latest call resolves
|
|
86
|
+
* when its state is shown and rejects when it fails; a call a newer one superseded resolves with the layer once the
|
|
87
|
+
* newer call has settled and never rejects.
|
|
88
|
+
*/
|
|
84
89
|
setStyle(style: StyleName): Promise<VectorTileLayer>;
|
|
85
90
|
setLanguage(language: string): Promise<VectorTileLayer>;
|
|
86
91
|
/** Basemap options by class (contract §1), replacing the previous ones; `{}` clears. */
|
|
87
92
|
setBasemap(overrides: BasemapOverrides): Promise<VectorTileLayer>;
|
|
88
93
|
basemap(): BasemapOverrides;
|
|
94
|
+
/** What the conversion for ArcGIS simplified in the style now shown ("<layer id>: <what>"), see toArcGISStyle. */
|
|
95
|
+
readonly simplified: string[];
|
|
89
96
|
}
|
|
90
|
-
|
|
97
|
+
interface VectorOptions {
|
|
98
|
+
basemap?: BasemapOverrides;
|
|
99
|
+
/** false hands sMap's style to ArcGIS unconverted (default true: see toArcGISStyle). */
|
|
100
|
+
convertStyle?: boolean;
|
|
101
|
+
}
|
|
102
|
+
export declare function createVectorTileLayer(options?: LayerCommon & VectorOptions): Promise<VectorTileLayer & { smap: SMapVectorInfo }>;
|
|
91
103
|
export declare function createRasterTileLayer(options?: LayerCommon & { retina?: boolean }): Promise<WebTileLayer & { smap: SMapLayerInfo }>;
|
|
92
|
-
export declare function createSMapBasemap(options?: LayerCommon & { raster?: boolean; retina?: boolean; basemapId?: string
|
|
104
|
+
export declare function createSMapBasemap(options?: LayerCommon & VectorOptions & { raster?: boolean; retina?: boolean; basemapId?: string }): Promise<Basemap & { smap: SMapLayerInfo | SMapVectorInfo }>;
|
|
105
|
+
/**
|
|
106
|
+
* A copy of a style that ArcGIS's VectorTileLayer draws: filters as expressions, labels without the operators ArcGIS
|
|
107
|
+
* lacks (is-supported-script, format, coalesce of properties), properties and layer types ArcGIS has no definition
|
|
108
|
+
* for left out or approximated. `changes` receives one line per simplification.
|
|
109
|
+
*/
|
|
110
|
+
export declare function toArcGISStyle<T extends { layers?: unknown[] } | null | undefined>(style: T, options?: { language?: string; changes?: string[] }): T;
|
|
111
|
+
/** The label language of an sMap style (the first name:<lang> it reads), or null. */
|
|
112
|
+
export declare function styleLanguage(style: { layers?: unknown[] }): string | null;
|
|
113
|
+
/** The label expression the conversion falls back to: the name in `language`, else the local name. */
|
|
114
|
+
export declare function nameExpression(language?: string | null): unknown[];
|
|
93
115
|
export declare function addAttribution(view: { ui: { add(el: HTMLElement, position: string): void }; container?: HTMLElement | null }, options?: { document?: Document }): HTMLElement;
|
|
94
116
|
|
|
95
117
|
export type GraphicsKind = 'points' | 'lines' | 'areas';
|