@superrouteca/smap-deckgl 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 +19 -0
- package/LICENSE +21 -0
- package/README.md +174 -2
- package/package.json +62 -4
- package/src/basemap.js +129 -0
- package/src/index.js +8 -0
- package/src/kepler.js +31 -0
- package/src/layers.js +227 -0
- package/src/overlay.js +133 -0
- package/src/session.js +198 -0
- package/src/style.js +378 -0
- package/types/index.d.ts +168 -0
package/src/style.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// Reads a Style Specification v8 document (sMap's schemes and custom styles) far enough to draw its vector tiles with
|
|
2
|
+
// deck.gl layers: fills, lines, extrusions and point labels, each feature coloured by the first matching style layer
|
|
3
|
+
// in style order, values evaluated at the tile's zoom. Sprites (icons) and line-placed labels are not drawn.
|
|
4
|
+
|
|
5
|
+
const NAMED = {
|
|
6
|
+
black: [0, 0, 0, 255], white: [255, 255, 255, 255], transparent: [0, 0, 0, 0], red: [255, 0, 0, 255],
|
|
7
|
+
green: [0, 128, 0, 255], blue: [0, 0, 255, 255], gray: [128, 128, 128, 255], grey: [128, 128, 128, 255],
|
|
8
|
+
yellow: [255, 255, 0, 255], orange: [255, 165, 0, 255],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function hsl2rgb(h, s, l) {
|
|
12
|
+
h = ((h % 360) + 360) % 360 / 360; s /= 100; l /= 100;
|
|
13
|
+
if (s === 0) { return [l * 255, l * 255, l * 255]; }
|
|
14
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
15
|
+
const p = 2 * l - q;
|
|
16
|
+
const f = (t) => {
|
|
17
|
+
t = (t + 1) % 1;
|
|
18
|
+
if (t < 1 / 6) { return p + (q - p) * 6 * t; }
|
|
19
|
+
if (t < 1 / 2) { return q; }
|
|
20
|
+
if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; }
|
|
21
|
+
return p;
|
|
22
|
+
};
|
|
23
|
+
return [f(h + 1 / 3) * 255, f(h) * 255, f(h - 1 / 3) * 255];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A CSS colour as [r, g, b, a] (0–255 each); null when it cannot be read. */
|
|
27
|
+
export function parseColor(value) {
|
|
28
|
+
if (Array.isArray(value) && value.length >= 3 && value.every((n) => typeof n === 'number')) {
|
|
29
|
+
return [value[0], value[1], value[2], value.length > 3 ? value[3] : 255];
|
|
30
|
+
}
|
|
31
|
+
if (typeof value !== 'string') { return null; }
|
|
32
|
+
const s = value.trim().toLowerCase();
|
|
33
|
+
if (NAMED[s]) { return NAMED[s].slice(); }
|
|
34
|
+
let m = /^#([0-9a-f]{3,8})$/.exec(s);
|
|
35
|
+
if (m) {
|
|
36
|
+
let h = m[1];
|
|
37
|
+
if (h.length === 3 || h.length === 4) { h = h.split('').map((c) => c + c).join(''); }
|
|
38
|
+
if (h.length !== 6 && h.length !== 8) { return null; }
|
|
39
|
+
const n = (i) => parseInt(h.slice(i, i + 2), 16);
|
|
40
|
+
return [n(0), n(2), n(4), h.length === 8 ? n(6) : 255];
|
|
41
|
+
}
|
|
42
|
+
m = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(s);
|
|
43
|
+
if (!m) { return null; }
|
|
44
|
+
const parts = m[2].split(/[\s,/]+/).filter(Boolean);
|
|
45
|
+
const num = (x) => parseFloat(x);
|
|
46
|
+
const alpha = parts.length > 3 ? (parts[3].endsWith('%') ? num(parts[3]) / 100 : num(parts[3])) : 1;
|
|
47
|
+
let rgb;
|
|
48
|
+
if (m[1].startsWith('rgb')) {
|
|
49
|
+
rgb = parts.slice(0, 3).map((x) => (x.endsWith('%') ? num(x) * 2.55 : num(x)));
|
|
50
|
+
} else {
|
|
51
|
+
rgb = hsl2rgb(num(parts[0]), num(parts[1]), num(parts[2]));
|
|
52
|
+
}
|
|
53
|
+
if (rgb.some((x) => Number.isNaN(x))) { return null; }
|
|
54
|
+
return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), Math.round(alpha * 255)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function baseType(geometry) {
|
|
58
|
+
const t = geometry && geometry.type;
|
|
59
|
+
return t ? t.replace(/^Multi/, '') : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getProp(feature, key) {
|
|
63
|
+
if (key === '$type') { return baseType(feature.geometry); }
|
|
64
|
+
if (key === '$id') { return feature.id; }
|
|
65
|
+
const p = feature.properties || {};
|
|
66
|
+
return p[key];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function interpolate(kind, input, stops) {
|
|
70
|
+
if (input <= stops[0][0]) { return stops[0][1]; }
|
|
71
|
+
const last = stops[stops.length - 1];
|
|
72
|
+
if (input >= last[0]) { return last[1]; }
|
|
73
|
+
for (let i = 0; i < stops.length - 1; i++) {
|
|
74
|
+
const [z0, v0] = stops[i];
|
|
75
|
+
const [z1, v1] = stops[i + 1];
|
|
76
|
+
if (input >= z0 && input <= z1) {
|
|
77
|
+
const base = kind && kind[0] === 'exponential' ? kind[1] : 1;
|
|
78
|
+
const d = z1 - z0;
|
|
79
|
+
const prog = input - z0;
|
|
80
|
+
const t = d === 0 ? 0 : (base === 1 ? prog / d : (Math.pow(base, prog) - 1) / (Math.pow(base, d) - 1));
|
|
81
|
+
return mix(v0, v1, t);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return last[1];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mix(a, b, t) {
|
|
88
|
+
if (typeof a === 'number' && typeof b === 'number') { return a + (b - a) * t; }
|
|
89
|
+
const ca = parseColor(a);
|
|
90
|
+
const cb = parseColor(b);
|
|
91
|
+
if (ca && cb) { return ca.map((x, i) => Math.round(x + (cb[i] - x) * t)); }
|
|
92
|
+
return t < 0.5 ? a : b;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LEGACY_FILTERS = new Set(['==', '!=', '<', '<=', '>', '>=', 'in', '!in', 'has', '!has', 'none']);
|
|
96
|
+
|
|
97
|
+
function isLegacyFilter(f) {
|
|
98
|
+
if (!Array.isArray(f)) { return false; }
|
|
99
|
+
const op = f[0];
|
|
100
|
+
if (op === 'all' || op === 'any') { return f.slice(1).some(isLegacyFilter); }
|
|
101
|
+
if (op === 'none' || op === '!in' || op === '!has') { return true; }
|
|
102
|
+
return LEGACY_FILTERS.has(op) && typeof f[1] === 'string';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function legacyFilter(f, feature) {
|
|
106
|
+
const op = f[0];
|
|
107
|
+
switch (op) {
|
|
108
|
+
case 'all': return f.slice(1).every((x) => legacyFilter(x, feature));
|
|
109
|
+
case 'any': return f.slice(1).some((x) => legacyFilter(x, feature));
|
|
110
|
+
case 'none': return !f.slice(1).some((x) => legacyFilter(x, feature));
|
|
111
|
+
case 'has': return getProp(feature, f[1]) !== undefined;
|
|
112
|
+
case '!has': return getProp(feature, f[1]) === undefined;
|
|
113
|
+
case 'in': return f.slice(2).includes(getProp(feature, f[1]));
|
|
114
|
+
case '!in': return !f.slice(2).includes(getProp(feature, f[1]));
|
|
115
|
+
default: {
|
|
116
|
+
if (!Array.isArray(f) || typeof f[1] !== 'string') { return Boolean(evaluate(f, feature, 0)); }
|
|
117
|
+
const v = getProp(feature, f[1]);
|
|
118
|
+
return compare(op, v, f[2]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compare(op, a, b) {
|
|
124
|
+
switch (op) {
|
|
125
|
+
case '==': return a === b;
|
|
126
|
+
case '!=': return a !== b;
|
|
127
|
+
case '<': return a < b;
|
|
128
|
+
case '<=': return a <= b;
|
|
129
|
+
case '>': return a > b;
|
|
130
|
+
case '>=': return a >= b;
|
|
131
|
+
default: return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Evaluates a style value for a feature at a zoom: constants, the old function objects (`{ stops, base, property }`)
|
|
137
|
+
* and the common expressions (get, has, comparisons, all / any / !, case, match, coalesce, interpolate, step, zoom,
|
|
138
|
+
* literal, to-number / to-string / to-boolean, concat, upcase / downcase, in, geometry-type, rgb / rgba, format).
|
|
139
|
+
*/
|
|
140
|
+
export function evaluate(expr, feature, zoom) {
|
|
141
|
+
if (expr === null || expr === undefined) { return expr; }
|
|
142
|
+
if (!Array.isArray(expr)) {
|
|
143
|
+
if (typeof expr === 'object' && Array.isArray(expr.stops)) {
|
|
144
|
+
const input = expr.property ? getProp(feature, expr.property) : zoom;
|
|
145
|
+
const type = expr.type || (expr.property ? 'categorical' : 'exponential');
|
|
146
|
+
const stops = expr.stops;
|
|
147
|
+
if (type === 'categorical') {
|
|
148
|
+
const hit = stops.find((s) => s[0] === input);
|
|
149
|
+
return hit ? hit[1] : expr.default;
|
|
150
|
+
}
|
|
151
|
+
if (type === 'interval') {
|
|
152
|
+
let out = stops[0][1];
|
|
153
|
+
for (const s of stops) { if (input >= s[0]) { out = s[1]; } }
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
if (type === 'identity') { return input; }
|
|
157
|
+
return interpolate(['exponential', expr.base || 1], input, stops);
|
|
158
|
+
}
|
|
159
|
+
return expr;
|
|
160
|
+
}
|
|
161
|
+
const op = expr[0];
|
|
162
|
+
const ev = (e) => evaluate(e, feature, zoom);
|
|
163
|
+
switch (op) {
|
|
164
|
+
case 'literal': return expr[1];
|
|
165
|
+
case 'zoom': return zoom;
|
|
166
|
+
case 'get': return expr.length > 2 ? (ev(expr[2]) || {})[ev(expr[1])] : getProp(feature, ev(expr[1]));
|
|
167
|
+
case 'has': return getProp(feature, ev(expr[1])) !== undefined;
|
|
168
|
+
case '!has': return getProp(feature, ev(expr[1])) === undefined;
|
|
169
|
+
case 'id': return feature.id;
|
|
170
|
+
case 'geometry-type': return baseType(feature.geometry);
|
|
171
|
+
case '!': return !ev(expr[1]);
|
|
172
|
+
case 'all': return expr.slice(1).every((e) => (isLegacyFilter(e) ? legacyFilter(e, feature) : ev(e)));
|
|
173
|
+
case 'any': return expr.slice(1).some((e) => (isLegacyFilter(e) ? legacyFilter(e, feature) : ev(e)));
|
|
174
|
+
case '==': case '!=': case '<': case '<=': case '>': case '>=':
|
|
175
|
+
if (typeof expr[1] === 'string') { return legacyFilter(expr, feature); }
|
|
176
|
+
return compare(op, ev(expr[1]), ev(expr[2]));
|
|
177
|
+
case 'in': {
|
|
178
|
+
if (typeof expr[1] === 'string' && !Array.isArray(expr[2])) { return legacyFilter(expr, feature); }
|
|
179
|
+
const hay = ev(expr[2]);
|
|
180
|
+
const needle = ev(expr[1]);
|
|
181
|
+
return Array.isArray(hay) ? hay.includes(needle) : typeof hay === 'string' ? hay.includes(String(needle)) : false;
|
|
182
|
+
}
|
|
183
|
+
case '!in': case 'none': return legacyFilter(expr, feature);
|
|
184
|
+
case 'case': {
|
|
185
|
+
for (let i = 1; i < expr.length - 1; i += 2) { if (ev(expr[i])) { return ev(expr[i + 1]); } }
|
|
186
|
+
return ev(expr[expr.length - 1]);
|
|
187
|
+
}
|
|
188
|
+
case 'match': {
|
|
189
|
+
const input = ev(expr[1]);
|
|
190
|
+
for (let i = 2; i < expr.length - 1; i += 2) {
|
|
191
|
+
const labels = Array.isArray(expr[i]) ? expr[i] : [expr[i]];
|
|
192
|
+
if (labels.includes(input)) { return ev(expr[i + 1]); }
|
|
193
|
+
}
|
|
194
|
+
return ev(expr[expr.length - 1]);
|
|
195
|
+
}
|
|
196
|
+
case 'coalesce': {
|
|
197
|
+
for (const e of expr.slice(1)) { const v = ev(e); if (v !== null && v !== undefined) { return v; } }
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
case 'interpolate': case 'interpolate-hcl': case 'interpolate-lab': {
|
|
201
|
+
const input = ev(expr[2]);
|
|
202
|
+
const stops = [];
|
|
203
|
+
for (let i = 3; i < expr.length - 1; i += 2) { stops.push([expr[i], ev(expr[i + 1])]); }
|
|
204
|
+
return typeof input === 'number' && stops.length ? interpolate(expr[1], input, stops) : (stops[0] || [])[1];
|
|
205
|
+
}
|
|
206
|
+
case 'step': {
|
|
207
|
+
const input = ev(expr[1]);
|
|
208
|
+
let out = ev(expr[2]);
|
|
209
|
+
for (let i = 3; i < expr.length - 1; i += 2) { if (input >= expr[i]) { out = ev(expr[i + 1]); } }
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
case 'to-number': { for (const e of expr.slice(1)) { const n = Number(ev(e)); if (!Number.isNaN(n)) { return n; } } return 0; }
|
|
213
|
+
case 'to-string': { const v = ev(expr[1]); return v === null || v === undefined ? '' : String(v); }
|
|
214
|
+
case 'to-boolean': return Boolean(ev(expr[1]));
|
|
215
|
+
case 'to-color': return ev(expr[1]);
|
|
216
|
+
case 'number': case 'string': case 'boolean': return ev(expr[1]);
|
|
217
|
+
case 'concat': return expr.slice(1).map(ev).map((v) => (v === null || v === undefined ? '' : v)).join('');
|
|
218
|
+
case 'upcase': return String(ev(expr[1]) || '').toUpperCase();
|
|
219
|
+
case 'downcase': return String(ev(expr[1]) || '').toLowerCase();
|
|
220
|
+
case 'rgb': return [ev(expr[1]), ev(expr[2]), ev(expr[3]), 255];
|
|
221
|
+
case 'rgba': return [ev(expr[1]), ev(expr[2]), ev(expr[3]), Math.round(ev(expr[4]) * 255)];
|
|
222
|
+
case 'format': {
|
|
223
|
+
let out = '';
|
|
224
|
+
for (let i = 1; i < expr.length; i++) {
|
|
225
|
+
const v = expr[i];
|
|
226
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) { continue; }
|
|
227
|
+
const s = ev(v);
|
|
228
|
+
if (s !== null && s !== undefined) { out += s; }
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
case '+': return expr.slice(1).map(ev).reduce((a, b) => a + b, 0);
|
|
233
|
+
case '*': return expr.slice(1).map(ev).reduce((a, b) => a * b, 1);
|
|
234
|
+
case '-': return expr.length === 2 ? -ev(expr[1]) : ev(expr[1]) - ev(expr[2]);
|
|
235
|
+
case '/': return ev(expr[1]) / ev(expr[2]);
|
|
236
|
+
case 'min': return Math.min(...expr.slice(1).map(ev));
|
|
237
|
+
case 'max': return Math.max(...expr.slice(1).map(ev));
|
|
238
|
+
default:
|
|
239
|
+
// An array of numbers (a colour or an offset) is a value.
|
|
240
|
+
return expr.every((x) => typeof x !== 'object') && typeof op !== 'string' ? expr : null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `{name}` placeholders of an old text-field, or an expression. */
|
|
245
|
+
export function textOf(field, feature, zoom) {
|
|
246
|
+
if (typeof field === 'string') {
|
|
247
|
+
return field.replace(/\{([^}]+)\}/g, (_, k) => {
|
|
248
|
+
const v = getProp(feature, k.trim());
|
|
249
|
+
return v === undefined || v === null ? '' : String(v);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const v = evaluate(field, feature, zoom);
|
|
253
|
+
return v === null || v === undefined ? '' : String(v);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function filterPasses(filter, feature, zoom) {
|
|
257
|
+
if (filter === undefined || filter === null) { return true; }
|
|
258
|
+
return isLegacyFilter(filter) ? legacyFilter(filter, feature) : Boolean(evaluate(filter, feature, zoom));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function withOpacity(color, opacity) {
|
|
262
|
+
const c = parseColor(color);
|
|
263
|
+
if (!c) { return null; }
|
|
264
|
+
const o = typeof opacity === 'number' ? Math.max(0, Math.min(1, opacity)) : 1;
|
|
265
|
+
return [c[0], c[1], c[2], Math.round(c[3] * o)];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The style layers of a document that read a vector source, grouped by source layer, in style order. */
|
|
269
|
+
export function compileStyle(doc, { source } = {}) {
|
|
270
|
+
const sources = doc && doc.sources ? doc.sources : {};
|
|
271
|
+
const vectorIds = Object.keys(sources).filter((id) => sources[id].type === 'vector' && (!source || id === source));
|
|
272
|
+
const bySourceLayer = new Map();
|
|
273
|
+
let background = null;
|
|
274
|
+
(doc && doc.layers ? doc.layers : []).forEach((layer, index) => {
|
|
275
|
+
if (layer.type === 'background') {
|
|
276
|
+
background = withOpacity((layer.paint || {})['background-color'], (layer.paint || {})['background-opacity']);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!vectorIds.includes(layer.source) || !layer['source-layer']) { return; }
|
|
280
|
+
if (!['fill', 'line', 'symbol', 'fill-extrusion'].includes(layer.type)) { return; }
|
|
281
|
+
if ((layer.layout || {}).visibility === 'none') { return; }
|
|
282
|
+
const list = bySourceLayer.get(layer['source-layer']) || [];
|
|
283
|
+
list.push({ index, layer });
|
|
284
|
+
bySourceLayer.set(layer['source-layer'], list);
|
|
285
|
+
});
|
|
286
|
+
return { sources: vectorIds, bySourceLayer, background };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function outlineOf(geometry) {
|
|
290
|
+
if (geometry.type === 'Polygon') { return { type: 'MultiLineString', coordinates: geometry.coordinates }; }
|
|
291
|
+
if (geometry.type === 'MultiPolygon') { return { type: 'MultiLineString', coordinates: geometry.coordinates.flat() }; }
|
|
292
|
+
return geometry;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function midpoint(geometry) {
|
|
296
|
+
const t = geometry.type;
|
|
297
|
+
if (t === 'Point') { return geometry.coordinates; }
|
|
298
|
+
if (t === 'MultiPoint') { return geometry.coordinates[0]; }
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Turns one tile's GeoJSON features into what is drawn: `fills`, `lines`, `extrusions` and `labels`, each item
|
|
304
|
+
* `{ geometry, properties, ... }` carrying its resolved colour and size, sorted in style order.
|
|
305
|
+
*
|
|
306
|
+
* @param {ReturnType<typeof compileStyle>} compiled
|
|
307
|
+
* @param {object[]} features GeoJSON features with `properties.layerName` = the tile layer
|
|
308
|
+
* @param {number} zoom
|
|
309
|
+
* @param {{ labels?: boolean, buildings3D?: boolean, buildingsLayer?: string }} [options]
|
|
310
|
+
*/
|
|
311
|
+
export function drawTile(compiled, features, zoom, options = {}) {
|
|
312
|
+
const fills = []; const lines = []; const extrusions = []; const labels = [];
|
|
313
|
+
const buildingsLayer = options.buildingsLayer || 'buildings';
|
|
314
|
+
for (const f of features || []) {
|
|
315
|
+
if (!f || !f.geometry) { continue; }
|
|
316
|
+
const layerName = (f.properties || {}).layerName;
|
|
317
|
+
const candidates = compiled.bySourceLayer.get(layerName);
|
|
318
|
+
if (!candidates) { continue; }
|
|
319
|
+
const type = baseType(f.geometry);
|
|
320
|
+
for (const { index, layer } of candidates) {
|
|
321
|
+
if (layer.minzoom !== undefined && zoom < layer.minzoom) { continue; }
|
|
322
|
+
if (layer.maxzoom !== undefined && zoom >= layer.maxzoom) { continue; }
|
|
323
|
+
if (!filterPasses(layer.filter, f, zoom)) { continue; }
|
|
324
|
+
const paint = layer.paint || {};
|
|
325
|
+
const layout = layer.layout || {};
|
|
326
|
+
const value = (v) => evaluate(v, f, zoom);
|
|
327
|
+
if (layer.type === 'fill' && type === 'Polygon') {
|
|
328
|
+
if (options.buildings3D && layerName === buildingsLayer) { continue; }
|
|
329
|
+
const color = withOpacity(value(paint['fill-color'] ?? '#000000'), value(paint['fill-opacity']));
|
|
330
|
+
if (color && color[3] > 0) { fills.push({ index, geometry: f.geometry, properties: f.properties, color }); }
|
|
331
|
+
const outline = paint['fill-outline-color'] !== undefined ? withOpacity(value(paint['fill-outline-color']), value(paint['fill-opacity'])) : null;
|
|
332
|
+
if (outline && outline[3] > 0) { lines.push({ index: index + 0.5, geometry: outlineOf(f.geometry), properties: f.properties, color: outline, width: 1 }); }
|
|
333
|
+
} else if (layer.type === 'fill-extrusion' && type === 'Polygon') {
|
|
334
|
+
if (!options.buildings3D) { continue; }
|
|
335
|
+
const color = withOpacity(value(paint['fill-extrusion-color'] ?? '#cccccc'), value(paint['fill-extrusion-opacity']));
|
|
336
|
+
const height = Number(value(paint['fill-extrusion-height'] ?? ['get', 'height'])) || 0;
|
|
337
|
+
const base = Number(value(paint['fill-extrusion-base'] ?? 0)) || 0;
|
|
338
|
+
if (color) { extrusions.push({ index, geometry: f.geometry, properties: f.properties, color, height, base }); }
|
|
339
|
+
} else if (layer.type === 'line' && (type === 'LineString' || type === 'Polygon')) {
|
|
340
|
+
const color = withOpacity(value(paint['line-color'] ?? '#000000'), value(paint['line-opacity']));
|
|
341
|
+
const width = Number(value(paint['line-width'] ?? 1));
|
|
342
|
+
if (color && color[3] > 0 && width > 0) {
|
|
343
|
+
lines.push({ index, geometry: outlineOf(f.geometry), properties: f.properties, color, width, dash: value(paint['line-dasharray']) || null });
|
|
344
|
+
}
|
|
345
|
+
} else if (layer.type === 'symbol' && options.labels !== false && type === 'Point' && layout['text-field'] !== undefined) {
|
|
346
|
+
if ((layout['symbol-placement'] || 'point') !== 'point') { continue; }
|
|
347
|
+
const text = textOf(layout['text-field'], f, zoom).trim();
|
|
348
|
+
const position = midpoint(f.geometry);
|
|
349
|
+
if (!text || !position) { continue; }
|
|
350
|
+
const transform = value(layout['text-transform']);
|
|
351
|
+
labels.push({
|
|
352
|
+
index,
|
|
353
|
+
position,
|
|
354
|
+
properties: f.properties,
|
|
355
|
+
text: transform === 'uppercase' ? text.toUpperCase() : transform === 'lowercase' ? text.toLowerCase() : text,
|
|
356
|
+
color: withOpacity(value(paint['text-color'] ?? '#000000'), value(paint['text-opacity'])) || [0, 0, 0, 255],
|
|
357
|
+
size: Number(value(layout['text-size'] ?? 16)) || 16,
|
|
358
|
+
halo: withOpacity(value(paint['text-halo-color'] ?? 'rgba(0,0,0,0)'), value(paint['text-opacity'])) || [0, 0, 0, 0],
|
|
359
|
+
haloWidth: Number(value(paint['text-halo-width'] ?? 0)) || 0,
|
|
360
|
+
anchor: value(layout['text-anchor']) || 'center',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (options.buildings3D) {
|
|
366
|
+
// Buildings the style draws flat are raised from their own height (or 10 m) when the style has no extrusion.
|
|
367
|
+
for (const f of features || []) {
|
|
368
|
+
if ((f.properties || {}).layerName !== buildingsLayer || baseType(f.geometry) !== 'Polygon') { continue; }
|
|
369
|
+
if (extrusions.some((e) => e.geometry === f.geometry)) { continue; }
|
|
370
|
+
const p = f.properties || {};
|
|
371
|
+
const flat = (compiled.bySourceLayer.get(buildingsLayer) || []).find((c) => c.layer.type === 'fill');
|
|
372
|
+
const color = flat ? withOpacity(evaluate((flat.layer.paint || {})['fill-color'], f, zoom), 1) : [204, 204, 204, 255];
|
|
373
|
+
extrusions.push({ index: flat ? flat.index : 0, geometry: f.geometry, properties: p, color: color || [204, 204, 204, 255], height: Number(p.height) || 10, base: Number(p.min_height) || 0 });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const byIndex = (a, b) => a.index - b.index;
|
|
377
|
+
return { fills: fills.sort(byIndex), lines: lines.sort(byIndex), extrusions: extrusions.sort(byIndex), labels: labels.sort(byIndex) };
|
|
378
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Types of @superrouteca/smap-deckgl.
|
|
2
|
+
import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
3
|
+
import type { MVTLayer, TileLayer } from '@deck.gl/geo-layers';
|
|
4
|
+
import type { MapboxOverlay, MapboxOverlayProps } from '@deck.gl/mapbox';
|
|
5
|
+
import type SMapClient from '@superrouteca/smap/client';
|
|
6
|
+
import type { Map as SMapMap, MapOptions as SMapMapOptions, LayerData, PointOptions, LineOptions, PolygonOptions, GeoJSONFeature, BasemapOverrides, Slot } from '@superrouteca/smap';
|
|
7
|
+
|
|
8
|
+
/** A colour scheme, or a saved custom style as `custom:<id>`. */
|
|
9
|
+
export type StyleName = 'light' | 'white' | 'grayscale' | 'dark' | 'black' | `custom:${string}`;
|
|
10
|
+
export type RGBA = [number, number, number, number];
|
|
11
|
+
|
|
12
|
+
/** How the adapter reaches sMap: the JS client's options, or a client of your own. */
|
|
13
|
+
export interface SessionOptions {
|
|
14
|
+
/** The public sMap address (default https://tile.srsr.ca). */
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/** A map key (`smk_…`). */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
token?: string;
|
|
19
|
+
/** Called at start and again before each token expires. */
|
|
20
|
+
getToken?: () => string | Promise<string>;
|
|
21
|
+
/** Node URLs known in advance, used while baseUrl is down. */
|
|
22
|
+
nodes?: string[];
|
|
23
|
+
/** Read from the nearest node (default true). */
|
|
24
|
+
nearest?: boolean;
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
/** A client to use instead of making one. */
|
|
28
|
+
client?: SMapClient;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One connection to sMap: nearest node, failover, key and token (through the JS client), and the URLs tools need. */
|
|
32
|
+
export declare class Session {
|
|
33
|
+
constructor(options?: SessionOptions);
|
|
34
|
+
readonly client: SMapClient;
|
|
35
|
+
readonly baseUrl: string;
|
|
36
|
+
readonly apiKey: string | null;
|
|
37
|
+
ready(): Promise<this>;
|
|
38
|
+
node(): string;
|
|
39
|
+
nodes(): string[];
|
|
40
|
+
token(): string | null;
|
|
41
|
+
headers(): Record<string, string>;
|
|
42
|
+
origins(): string[];
|
|
43
|
+
nodeOf(url: string): string | null;
|
|
44
|
+
pathOf(url: string): string | null;
|
|
45
|
+
rebase(url: string): string;
|
|
46
|
+
failed(urlOrNode: string): boolean;
|
|
47
|
+
styleUrl(style?: StyleName, language?: string): string;
|
|
48
|
+
tileJsonUrl(source?: string): string;
|
|
49
|
+
rasterTemplate(style?: StyleName, options?: { language?: string; retina?: boolean; placeholders?: { z?: string; x?: string; y?: string } }): string;
|
|
50
|
+
rasterTileJsonUrl(style?: StyleName, options?: { language?: string }): string;
|
|
51
|
+
wmtsCapabilitiesUrl(): string;
|
|
52
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
53
|
+
style(style?: StyleName, language?: string): Promise<Record<string, unknown>>;
|
|
54
|
+
on(type: 'nodechange' | 'token', fn: (value: string | null) => void): () => void;
|
|
55
|
+
off(type: 'nodechange' | 'token', fn: (value: string | null) => void): void;
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
export declare function createSession(options?: SessionOptions | Session): Session;
|
|
59
|
+
export declare const FLAVORS: Array<'light' | 'white' | 'grayscale' | 'dark' | 'black'>;
|
|
60
|
+
export declare const ATTRIBUTION: string;
|
|
61
|
+
export declare const version: string;
|
|
62
|
+
|
|
63
|
+
interface BasemapCommon extends SessionOptions {
|
|
64
|
+
/** A session shared with other layers, instead of the options above. */
|
|
65
|
+
session?: Session;
|
|
66
|
+
style?: StyleName;
|
|
67
|
+
/** Label language (BCP 47). */
|
|
68
|
+
language?: string;
|
|
69
|
+
id?: string;
|
|
70
|
+
/** Other deck.gl props of the layer. */
|
|
71
|
+
layerProps?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface VectorBasemapOptions extends BasemapCommon {
|
|
75
|
+
/** A style document to draw instead of reading `style` from sMap. */
|
|
76
|
+
styleDocument?: Record<string, unknown>;
|
|
77
|
+
/** The vector source of the style to draw (default its first). */
|
|
78
|
+
source?: string;
|
|
79
|
+
/** Point labels (default true). */
|
|
80
|
+
labels?: boolean;
|
|
81
|
+
/** Buildings raised by their height. */
|
|
82
|
+
buildings3D?: boolean;
|
|
83
|
+
/** Basemap options by class (contract §1), as the sMap map's setBasemap. */
|
|
84
|
+
basemap?: BasemapOverrides;
|
|
85
|
+
fontFamily?: string;
|
|
86
|
+
pickable?: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface RasterBasemapOptions extends BasemapCommon {
|
|
89
|
+
/** 512 px tiles. */
|
|
90
|
+
retina?: boolean;
|
|
91
|
+
maxZoom?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface BasemapInfo {
|
|
94
|
+
session: Session;
|
|
95
|
+
style: StyleName;
|
|
96
|
+
background?: RGBA | null;
|
|
97
|
+
attribution: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export declare function createVectorBasemapLayer(options?: VectorBasemapOptions): Promise<MVTLayer & { smap: BasemapInfo }>;
|
|
101
|
+
export declare function createRasterBasemapLayer(options?: RasterBasemapOptions): Promise<TileLayer & { smap: BasemapInfo }>;
|
|
102
|
+
export declare function tileSubLayers(props: Record<string, any>, compiled: CompiledStyle, options?: { labels?: boolean; buildings3D?: boolean; fontFamily?: string; pickable?: boolean }): Layer[];
|
|
103
|
+
|
|
104
|
+
export interface SMapDeckOptions extends SMapMapOptions {
|
|
105
|
+
/** deck.gl layers, drawn interleaved with the basemap. */
|
|
106
|
+
layers?: Layer[];
|
|
107
|
+
/** Other deck.gl props of the map's overlay (getTooltip, onClick, pickingRadius, …). */
|
|
108
|
+
deck?: MapboxOverlayProps;
|
|
109
|
+
/** The @superrouteca/smap module (imported when not given). */
|
|
110
|
+
sMap?: { Map: typeof SMapMap; useDeck?: (module: unknown) => unknown };
|
|
111
|
+
}
|
|
112
|
+
export interface SMapDeck {
|
|
113
|
+
map: SMapMap;
|
|
114
|
+
/** The map's deck.gl overlay (null until a layer or a prop needs it). */
|
|
115
|
+
readonly overlay: MapboxOverlay | null;
|
|
116
|
+
layers: DeckLayers;
|
|
117
|
+
setLayers(layers: Layer[]): this;
|
|
118
|
+
setProps(props: MapboxOverlayProps): this;
|
|
119
|
+
pickObject(options: { x: number; y: number; radius?: number; layerIds?: string[] }): PickingInfo | null;
|
|
120
|
+
remove(): void;
|
|
121
|
+
}
|
|
122
|
+
export declare function createSMapDeck(options: SMapDeckOptions): Promise<SMapDeck>;
|
|
123
|
+
|
|
124
|
+
/** Where a deck.gl layer sits on the basemap; a layer without one draws on top, in the order added. */
|
|
125
|
+
export interface DeckPlacement { slot?: Slot; before?: string }
|
|
126
|
+
export interface DeckLayers {
|
|
127
|
+
add(layers: Layer | Array<Layer | null | undefined | false>, placement?: DeckPlacement): this;
|
|
128
|
+
remove(...ids: Array<string | string[]>): this;
|
|
129
|
+
set(layers: Array<Layer | null | undefined | false>, placement?: DeckPlacement): this;
|
|
130
|
+
get(id: string): Layer | null;
|
|
131
|
+
ids(): string[];
|
|
132
|
+
clear(): this;
|
|
133
|
+
}
|
|
134
|
+
/** deck.gl layers on an sMap map (1.1.0 or later), kept by id, through its addDeckLayer / removeDeckLayer. */
|
|
135
|
+
export declare function deckLayers(map: SMapMap): DeckLayers;
|
|
136
|
+
/** Hands deck.gl's map overlay module to the SDK (done by createSMapDeck and deckLayers). */
|
|
137
|
+
export declare function registerDeck(sMap?: { useDeck?: (module: unknown) => unknown }): void;
|
|
138
|
+
export declare function addAttribution(container: HTMLElement, options?: { attribution?: string | string[] }): HTMLElement;
|
|
139
|
+
|
|
140
|
+
export interface DeckLayerExtras {
|
|
141
|
+
pickable?: boolean;
|
|
142
|
+
fontFamily?: string;
|
|
143
|
+
}
|
|
144
|
+
export declare function pointsLayers(id: string, data: LayerData, options?: PointOptions & DeckLayerExtras): Layer[];
|
|
145
|
+
export declare function linesLayers(id: string, data: LayerData, options?: LineOptions & DeckLayerExtras): Layer[];
|
|
146
|
+
export declare function polygonsLayers(id: string, data: LayerData, options?: PolygonOptions & DeckLayerExtras): Layer[];
|
|
147
|
+
export declare function popupTooltip(fallback?: (info: PickingInfo) => unknown): (info: PickingInfo) => { html: string; className: string } | unknown;
|
|
148
|
+
export declare function toFeatures(data: LayerData): GeoJSONFeature[];
|
|
149
|
+
export declare function accessor<T = unknown>(value: unknown, fallback?: T, simplestyleKey?: string): (feature: GeoJSONFeature) => T;
|
|
150
|
+
|
|
151
|
+
export interface KeplerMapStyle { id: string; label: string; url: string; icon: string }
|
|
152
|
+
export declare function keplerMapStyles(options?: SessionOptions & { session?: Session; styles?: StyleName[]; language?: string; labels?: Record<string, string> }): Promise<KeplerMapStyle[]>;
|
|
153
|
+
export declare function keplerTransformRequest(options?: SessionOptions & { session?: Session }): (url: string) => { url: string; headers?: Record<string, string> };
|
|
154
|
+
|
|
155
|
+
export interface CompiledStyle {
|
|
156
|
+
sources: string[];
|
|
157
|
+
bySourceLayer: Map<string, Array<{ index: number; layer: Record<string, any> }>>;
|
|
158
|
+
background: RGBA | null;
|
|
159
|
+
}
|
|
160
|
+
export declare function parseColor(value: unknown): RGBA | null;
|
|
161
|
+
export declare function evaluate(expression: unknown, feature: GeoJSONFeature, zoom: number): any;
|
|
162
|
+
export declare function compileStyle(doc: Record<string, any>, options?: { source?: string }): CompiledStyle;
|
|
163
|
+
export declare function drawTile(compiled: CompiledStyle, features: GeoJSONFeature[], zoom: number, options?: { labels?: boolean; buildings3D?: boolean; buildingsLayer?: string }): {
|
|
164
|
+
fills: Array<{ geometry: unknown; color: RGBA }>;
|
|
165
|
+
lines: Array<{ geometry: unknown; color: RGBA; width: number }>;
|
|
166
|
+
extrusions: Array<{ geometry: unknown; color: RGBA; height: number; base: number }>;
|
|
167
|
+
labels: Array<{ position: [number, number]; text: string; color: RGBA; size: number }>;
|
|
168
|
+
};
|