@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 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.0",
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.0"
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
- // Basemap options (contract §1: show, hide and recolour by class), applied by the SDK's own function.
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
- return overrides && Object.keys(overrides).length ? applyBasemapOverrides(doc, overrides) : doc;
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 = async () => { await layer.loadStyle(await styled()); return layer; };
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.0';
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
- export declare function createVectorTileLayer(options?: LayerCommon & { basemap?: BasemapOverrides }): Promise<VectorTileLayer & { smap: SMapVectorInfo }>;
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; basemap?: BasemapOverrides }): Promise<Basemap & { smap: SMapLayerInfo | SMapVectorInfo }>;
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';