isard 1.4.0 → 1.5.0
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 +55 -1
- package/README.md +22 -7
- package/dist/basemap/vectortiles/context.d.ts +43 -0
- package/dist/basemap/vectortiles/context.js +6 -0
- package/dist/basemap/vectortiles/context.js.map +1 -0
- package/dist/basemap/vectortiles/extrusions.d.ts +11 -0
- package/dist/basemap/vectortiles/extrusions.js +46 -0
- package/dist/basemap/vectortiles/extrusions.js.map +1 -0
- package/dist/basemap/vectortiles/labels.d.ts +22 -0
- package/dist/basemap/vectortiles/labels.js +225 -0
- package/dist/basemap/vectortiles/labels.js.map +1 -0
- package/dist/basemap/vectortiles/requests.d.ts +24 -0
- package/dist/basemap/vectortiles/requests.js +61 -0
- package/dist/basemap/vectortiles/requests.js.map +1 -0
- package/dist/basemap/vectortiles/street-wind.d.ts +19 -0
- package/dist/basemap/vectortiles/street-wind.js +133 -0
- package/dist/basemap/vectortiles/street-wind.js.map +1 -0
- package/dist/basemap/vectortiles/style.d.ts +47 -0
- package/dist/basemap/vectortiles/style.js +141 -0
- package/dist/basemap/vectortiles/style.js.map +1 -0
- package/dist/basemap/vectortiles/tiles.d.ts +34 -0
- package/dist/basemap/vectortiles/tiles.js +62 -0
- package/dist/basemap/vectortiles/tiles.js.map +1 -0
- package/dist/basemap/vectortiles/trees.d.ts +19 -0
- package/dist/basemap/vectortiles/trees.js +93 -0
- package/dist/basemap/vectortiles/trees.js.map +1 -0
- package/dist/basemap/vectortiles/vectors.d.ts +20 -0
- package/dist/basemap/vectortiles/vectors.js +167 -0
- package/dist/basemap/vectortiles/vectors.js.map +1 -0
- package/dist/basemap/vectortiles.d.ts +23 -90
- package/dist/basemap/vectortiles.js +57 -765
- package/dist/basemap/vectortiles.js.map +1 -1
- package/dist/basemap/workerpool.js +25 -6
- package/dist/basemap/workerpool.js.map +1 -1
- package/dist/basemap.worker.js +2 -2
- package/dist/basemap.worker.js.map +4 -4
- package/dist/decoder.worker.js +5 -4
- package/dist/decoder.worker.js.map +8 -6
- package/dist/engine/decoders.d.ts +4 -8
- package/dist/engine/decoders.js +29 -2
- package/dist/engine/decoders.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/isard_bg.wasm +0 -0
- package/dist/isard_core_bg.wasm +0 -0
- package/dist/layers/flow.d.ts +9 -2
- package/dist/layers/flow.js +51 -10
- package/dist/layers/flow.js.map +1 -1
- package/dist/layers/panoramas.d.ts +2 -0
- package/dist/layers/panoramas.js +27 -6
- package/dist/layers/panoramas.js.map +1 -1
- package/dist/map/workers.js +3 -0
- package/dist/map/workers.js.map +1 -1
- package/dist/util/read.d.ts +4 -0
- package/dist/util/read.js +28 -0
- package/dist/util/read.js.map +1 -0
- package/dist/wasm/isard.d.ts +18 -5
- package/dist/wasm/isard.js +25 -4
- package/dist/wasm/isard_core.d.ts +18 -5
- package/dist/wasm/isard_core.js +25 -4
- package/dist/workers/basemap/street.d.ts +1 -1
- package/package.json +1 -1
|
@@ -7,167 +7,21 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Any subset of the style can be drawn on its own raster layer (`raster({ layers })`), at its own place in the stack:
|
|
9
9
|
* rivers from the basemap's data over a flood layer, for instance, restyled with `extraLayers`.
|
|
10
|
+
*
|
|
11
|
+
* The layers other than the raster ones are made in `vectortiles/` (one module each), from what they use of the
|
|
12
|
+
* instance (`VectorTilesContext`); the style's loading is in `vectortiles/style.ts`.
|
|
10
13
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { isWoodland, TREE_NUMBERS, treeModel, treeSpacing } from "./trees.js";
|
|
20
|
-
import { evaluate, isExpression, isStyle, labelFont, vectorKind, zoomOnly, } from "./vectorstyle.js";
|
|
14
|
+
import { loadSprites, spriteSources } from "../layers/icons.js";
|
|
15
|
+
import { extrusions } from "./vectortiles/extrusions.js";
|
|
16
|
+
import { labels, size, tileLabels } from "./vectortiles/labels.js";
|
|
17
|
+
import { streetWind } from "./vectortiles/street-wind.js";
|
|
18
|
+
import { loadStyle, OPENFREEMAP_STYLES, PLAN_STYLES, styleAttribution, VECTOR_BASEMAPS, } from "./vectortiles/style.js";
|
|
19
|
+
import { VECTOR_MOVE_REFRESH_MS } from "./vectortiles/tiles.js";
|
|
20
|
+
import { trees } from "./vectortiles/trees.js";
|
|
21
|
+
import { dashes, vectors } from "./vectortiles/vectors.js";
|
|
21
22
|
import { rendererKey, tilePriority } from "./workerpool.js";
|
|
22
|
-
|
|
23
|
-
export
|
|
24
|
-
positron: 'https://tiles.openfreemap.org/styles/positron',
|
|
25
|
-
liberty: 'https://tiles.openfreemap.org/styles/liberty',
|
|
26
|
-
bright: 'https://tiles.openfreemap.org/styles/bright',
|
|
27
|
-
dark: 'https://tiles.openfreemap.org/styles/dark',
|
|
28
|
-
fiord: 'https://tiles.openfreemap.org/styles/fiord',
|
|
29
|
-
};
|
|
30
|
-
/** PLAN.IGN vector styles of the Géoplateforme (France) */
|
|
31
|
-
export const PLAN_STYLES = {
|
|
32
|
-
standard: 'https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json',
|
|
33
|
-
grey: 'https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/gris.json',
|
|
34
|
-
};
|
|
35
|
-
/** Vector basemaps by name (see `BasemapName`) */
|
|
36
|
-
export const VECTOR_BASEMAPS = {
|
|
37
|
-
'osm-positron': OPENFREEMAP_STYLES.positron,
|
|
38
|
-
'osm-liberty': OPENFREEMAP_STYLES.liberty,
|
|
39
|
-
'osm-bright': OPENFREEMAP_STYLES.bright,
|
|
40
|
-
'osm-dark': OPENFREEMAP_STYLES.dark,
|
|
41
|
-
'ign-plan-vector': PLAN_STYLES.standard,
|
|
42
|
-
'ign-plan-grey-vector': PLAN_STYLES.grey,
|
|
43
|
-
};
|
|
44
|
-
/** The credits of a known style: OpenFreeMap's, PLAN.IGN's */
|
|
45
|
-
export const styleAttribution = (style) => {
|
|
46
|
-
if (typeof style !== 'string')
|
|
47
|
-
return undefined;
|
|
48
|
-
if (Object.values(OPENFREEMAP_STYLES).some((s) => s === style))
|
|
49
|
-
return OPENFREEMAP_ATTRIBUTION;
|
|
50
|
-
if (Object.values(PLAN_STYLES).some((s) => s === style))
|
|
51
|
-
return IGN_ATTRIBUTION;
|
|
52
|
-
return undefined;
|
|
53
|
-
};
|
|
54
|
-
/** Text or icon size of a style (a number, legacy stops, or the stops of a zoom expression made by the worker) as a
|
|
55
|
-
* size for the symbol layer */
|
|
56
|
-
export const size = (value, fallback = 16) => {
|
|
57
|
-
const stop = (s) => {
|
|
58
|
-
const [z, v] = Array.isArray(s) ? s : [];
|
|
59
|
-
return [Number(z), Number(v)];
|
|
60
|
-
};
|
|
61
|
-
if (typeof value === 'number')
|
|
62
|
-
return value;
|
|
63
|
-
if (Array.isArray(value) && value.length > 0 && value.every(Array.isArray))
|
|
64
|
-
return value.map(stop);
|
|
65
|
-
if (isRecord(value) && Array.isArray(value.stops))
|
|
66
|
-
return value.stops.map(stop);
|
|
67
|
-
return fallback;
|
|
68
|
-
};
|
|
69
|
-
/** The anchors of the symbol layer */
|
|
70
|
-
const ANCHORS = new Set([
|
|
71
|
-
'center',
|
|
72
|
-
'top',
|
|
73
|
-
'bottom',
|
|
74
|
-
'left',
|
|
75
|
-
'right',
|
|
76
|
-
'top-left',
|
|
77
|
-
'top-right',
|
|
78
|
-
'bottom-left',
|
|
79
|
-
'bottom-right',
|
|
80
|
-
]);
|
|
81
|
-
const isAnchor = (value) => value !== undefined && ANCHORS.has(value);
|
|
82
|
-
/** A style's anchor (`text-anchor`, `icon-anchor`) for the symbol layer: `center` if it is none */
|
|
83
|
-
const anchor = (value) => (isAnchor(value) ? value : 'center');
|
|
84
|
-
/** Fetches a JSON document (a style, a TileJSON) */
|
|
85
|
-
const json = async (url) => {
|
|
86
|
-
const response = await fetch(url, withHeaders(url));
|
|
87
|
-
if (!response.ok)
|
|
88
|
-
throw new Error(`${url}: ${response.status}`);
|
|
89
|
-
return response.json();
|
|
90
|
-
};
|
|
91
|
-
/** A style fetched: a MapLibre style, or an error */
|
|
92
|
-
const fetchStyle = async (url) => {
|
|
93
|
-
const style = await json(url);
|
|
94
|
-
if (!isStyle(style))
|
|
95
|
-
throw new Error(`${url}: not a MapLibre style`);
|
|
96
|
-
return style;
|
|
97
|
-
};
|
|
98
|
-
const isTileJson = (value) => isRecord(value) &&
|
|
99
|
-
Array.isArray(value.tiles) &&
|
|
100
|
-
value.tiles.every((t) => typeof t === 'string') &&
|
|
101
|
-
(value.maxzoom === undefined || typeof value.maxzoom === 'number');
|
|
102
|
-
/** A TileJSON fetched, or an error */
|
|
103
|
-
const fetchTileJson = async (url) => {
|
|
104
|
-
const tilejson = await json(url);
|
|
105
|
-
if (!isTileJson(tilejson))
|
|
106
|
-
throw new Error(`${url}: not a TileJSON`);
|
|
107
|
-
return tilejson;
|
|
108
|
-
};
|
|
109
|
-
/**
|
|
110
|
-
* A style source's tiles and deepest level: a `pmtiles://` archive (its `url`, or a `tiles` template) read for its
|
|
111
|
-
* header, its tiles then named `pmtiles://<absolute archive URL>/{z}/{x}/{y}` for the workers; other sources as they are
|
|
112
|
-
*/
|
|
113
|
-
const resolveSource = async (source) => {
|
|
114
|
-
const given = source.url ?? source.tiles?.[0] ?? '';
|
|
115
|
-
const archive = pmtilesUrl(given.replace(/\/\{z\}\/\{x\}\/\{y\}$/, ''));
|
|
116
|
-
if (!archive)
|
|
117
|
-
return {
|
|
118
|
-
...(source.tiles ? { tiles: source.tiles } : {}),
|
|
119
|
-
...(source.maxzoom === undefined ? {} : { maxzoom: source.maxzoom }),
|
|
120
|
-
};
|
|
121
|
-
const header = await pmtiles(archive).header();
|
|
122
|
-
return { tiles: [`pmtiles://${archive}/{z}/{x}/{y}`], maxzoom: source.maxzoom ?? header.maxZoom };
|
|
123
|
-
};
|
|
124
|
-
/** Pause before the labels of tiles that failed are asked for again, when no move brings an update (ms) */
|
|
125
|
-
const RETRY_LABELS_MS = 3000;
|
|
126
|
-
/** The style layers of buildings, by default: PLAN IGN's `bati_surf` (not its built-up areas nor its parks), the
|
|
127
|
-
* OpenMapTiles' `building` */
|
|
128
|
-
const isBuildings = (layer) => ['bati_surf', 'building'].includes(layer['source-layer'] ?? '');
|
|
129
|
-
/**
|
|
130
|
-
* The tiles of level `z` in view, within a window around the center (a tilted view's bounds reach far), the
|
|
131
|
-
* `maxTiles` nearest to the center first
|
|
132
|
-
*/
|
|
133
|
-
const tilesAround = (map, camera, z, maxTiles) => {
|
|
134
|
-
const n = 2 ** z;
|
|
135
|
-
const [west, south, east, north] = map.getBounds();
|
|
136
|
-
const tx = (lon) => Math.floor(mercatorX(lon) * n);
|
|
137
|
-
const ty = (lat) => Math.floor(mercatorY(lat) * n);
|
|
138
|
-
const [cx, cy] = [mercatorX(camera.center[0]) * n, mercatorY(camera.center[1]) * n];
|
|
139
|
-
const reach = Math.ceil(Math.sqrt(maxTiles)) + 2;
|
|
140
|
-
const [x0, x1] = [Math.max(0, tx(west), Math.floor(cx) - reach), Math.min(n - 1, tx(east), Math.floor(cx) + reach)];
|
|
141
|
-
const [y0, y1] = [Math.max(0, ty(north), Math.floor(cy) - reach), Math.min(n - 1, ty(south), Math.floor(cy) + reach)];
|
|
142
|
-
const wanted = Array.from({ length: Math.max(0, y1 - y0 + 1) }, (_, j) => y0 + j).flatMap((y) => Array.from({ length: Math.max(0, x1 - x0 + 1) }, (_, i) => [x0 + i, y]));
|
|
143
|
-
wanted.sort((a, b) => Math.hypot(a[0] + 0.5 - cx, a[1] + 0.5 - cy) - Math.hypot(b[0] + 0.5 - cx, b[1] + 0.5 - cy));
|
|
144
|
-
return wanted.slice(0, maxTiles);
|
|
145
|
-
};
|
|
146
|
-
/** A tile's label candidates as features, by style layer */
|
|
147
|
-
export const tileLabels = (candidates) => {
|
|
148
|
-
const byLayer = new Map();
|
|
149
|
-
candidates.forEach((p) => {
|
|
150
|
-
const feature = {
|
|
151
|
-
type: 'Feature',
|
|
152
|
-
properties: p,
|
|
153
|
-
geometry: p.along
|
|
154
|
-
? {
|
|
155
|
-
type: 'LineString',
|
|
156
|
-
coordinates: [
|
|
157
|
-
[p.lon - p.along[0] / 2, p.lat - p.along[1] / 2],
|
|
158
|
-
[p.lon + p.along[0] / 2, p.lat + p.along[1] / 2],
|
|
159
|
-
],
|
|
160
|
-
}
|
|
161
|
-
: { type: 'Point', coordinates: [p.lon, p.lat] },
|
|
162
|
-
};
|
|
163
|
-
const list = byLayer.get(p.layer);
|
|
164
|
-
if (list)
|
|
165
|
-
list.push(feature);
|
|
166
|
-
else
|
|
167
|
-
byLayer.set(p.layer, [feature]);
|
|
168
|
-
});
|
|
169
|
-
return [...byLayer].sort((a, b) => b[0] - a[0]);
|
|
170
|
-
};
|
|
23
|
+
// The names the package and its tests import from here, now made in `vectortiles/`
|
|
24
|
+
export { dashes, OPENFREEMAP_STYLES, PLAN_STYLES, size, styleAttribution, tileLabels, VECTOR_BASEMAPS };
|
|
171
25
|
export class VectorTiles {
|
|
172
26
|
map;
|
|
173
27
|
pool;
|
|
@@ -186,6 +40,8 @@ export class VectorTiles {
|
|
|
186
40
|
layers = [];
|
|
187
41
|
/** Its first raster layer: the ground its labels follow */
|
|
188
42
|
ground;
|
|
43
|
+
/** What its layers made in `vectortiles/` use of it */
|
|
44
|
+
context;
|
|
189
45
|
/** The style once loaded, with its source resolved */
|
|
190
46
|
ready;
|
|
191
47
|
/**
|
|
@@ -200,57 +56,49 @@ export class VectorTiles {
|
|
|
200
56
|
this.namespace = pool.namespace();
|
|
201
57
|
const style = options.style ?? OPENFREEMAP_STYLES.positron;
|
|
202
58
|
this.attribution = options.attribution ?? styleAttribution(style);
|
|
203
|
-
this.ready =
|
|
59
|
+
this.ready = loadStyle(style);
|
|
204
60
|
// Reported once here; `ready`, the raster tiles and the labels fail with it
|
|
205
61
|
this.ready.catch((error) => {
|
|
206
62
|
if (!this.destroyed)
|
|
207
63
|
console.warn('isard: vector tiles', error);
|
|
208
64
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const r = await resolveSource(s);
|
|
228
|
-
const url = r.tiles?.[0];
|
|
229
|
-
if (url)
|
|
230
|
-
rasters[key] = { tiles: url, maxzoom: r.maxzoom ?? 22, tileSize: s.tileSize ?? 512 };
|
|
231
|
-
}
|
|
232
|
-
// Only the layers of those sources (and the background); other types (hillshade, heatmap…) are left out. The
|
|
233
|
-
// application's style object stays as it is
|
|
234
|
-
const layers = style.layers.filter((l) => l.type === 'background' ||
|
|
235
|
-
(l.source === name && l.type !== 'raster') ||
|
|
236
|
-
(l.type === 'raster' && l.source !== undefined && rasters[l.source] !== undefined));
|
|
237
|
-
return { style: { ...style, layers }, tiles: template, maxzoom: maxzoom ?? 14, rasters };
|
|
238
|
-
}
|
|
239
|
-
/** The fonts of the style's labels, for its glyphs (the first of each `text-font`, as the workers take it) */
|
|
240
|
-
static fonts(style) {
|
|
241
|
-
const fonts = new Set();
|
|
242
|
-
style.layers.forEach((layer) => {
|
|
243
|
-
const font = layer.layout?.['text-font'];
|
|
244
|
-
if (layer.type === 'symbol' && Array.isArray(font) && typeof font[0] === 'string')
|
|
245
|
-
fonts.add(labelFont(font[0]));
|
|
246
|
-
});
|
|
247
|
-
return [...fonts];
|
|
65
|
+
this.context = {
|
|
66
|
+
map,
|
|
67
|
+
ready: this.ready,
|
|
68
|
+
layers: this.layers,
|
|
69
|
+
cleanup: this.cleanup,
|
|
70
|
+
iconPrefix: this.iconPrefix,
|
|
71
|
+
destroyed: () => this.destroyed,
|
|
72
|
+
ground: () => this.ground,
|
|
73
|
+
broadcast: (message) => {
|
|
74
|
+
this.pool.broadcast(message);
|
|
75
|
+
},
|
|
76
|
+
request: (vz, vx, vy, work, signal) => this.request(vz, vx, vy, work, signal),
|
|
77
|
+
renderer: (select, extra, again) => this.renderer(select, extra, again),
|
|
78
|
+
loadSprites: (sprite) => this.loadSprites(sprite),
|
|
79
|
+
followView: (update, requests, moving) => {
|
|
80
|
+
this.followView(update, requests, moving);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
248
83
|
}
|
|
249
84
|
/** The style's sprites (their JSON and their images), loaded once, for the fill patterns and the icons */
|
|
250
85
|
sprites;
|
|
86
|
+
/** The style's sprites, kept once all of them came: one that failed is asked for again by the next layer */
|
|
251
87
|
loadSprites(sprite) {
|
|
252
|
-
this.sprites
|
|
253
|
-
|
|
88
|
+
if (this.sprites)
|
|
89
|
+
return this.sprites;
|
|
90
|
+
const wanted = spriteSources(sprite).length;
|
|
91
|
+
const loading = loadSprites(sprite)
|
|
92
|
+
.catch(() => [])
|
|
93
|
+
.then((loaded) => {
|
|
94
|
+
if (loaded.length < wanted && this.sprites === loading) {
|
|
95
|
+
console.warn(`isard: ${wanted - loaded.length} of the style's sprites not loaded, its icons and patterns missing`);
|
|
96
|
+
this.sprites = undefined;
|
|
97
|
+
}
|
|
98
|
+
return loaded;
|
|
99
|
+
});
|
|
100
|
+
this.sprites = loading;
|
|
101
|
+
return loading;
|
|
254
102
|
}
|
|
255
103
|
/** Prefix of the names of its sprites' images among the map's icons */
|
|
256
104
|
get iconPrefix() {
|
|
@@ -317,51 +165,7 @@ export class VectorTiles {
|
|
|
317
165
|
* leaving the view are dropped.
|
|
318
166
|
*/
|
|
319
167
|
extrusions(options = {}) {
|
|
320
|
-
|
|
321
|
-
const extrusions = this.map.addExtrusions(style);
|
|
322
|
-
const renderer = this.renderer((l) => (l.type === 'fill' || l.type === 'fill-extrusion') && layers(l), []);
|
|
323
|
-
renderer.catch(() => undefined);
|
|
324
|
-
this.layers.push(extrusions);
|
|
325
|
-
/** The tiles held or asked for, by key */
|
|
326
|
-
const held = new Map();
|
|
327
|
-
const update = async () => {
|
|
328
|
-
if (this.destroyed)
|
|
329
|
-
return;
|
|
330
|
-
const { key, maxzoom } = await renderer;
|
|
331
|
-
const camera = this.map.getCamera();
|
|
332
|
-
const z = maxzoom;
|
|
333
|
-
const wanted = camera.zoom < minZoom ? [] : tilesAround(this.map, camera, z, maxTiles);
|
|
334
|
-
const keys = new Set(wanted.map(([x, y]) => `${z}/${x}/${y}`));
|
|
335
|
-
// Out of view: dropped, or no longer asked for
|
|
336
|
-
[...held].forEach(([k, controller]) => {
|
|
337
|
-
if (keys.has(k))
|
|
338
|
-
return;
|
|
339
|
-
controller.abort();
|
|
340
|
-
held.delete(k);
|
|
341
|
-
extrusions.removeTile(k);
|
|
342
|
-
});
|
|
343
|
-
wanted.forEach(([x, y], index) => {
|
|
344
|
-
const k = `${z}/${x}/${y}`;
|
|
345
|
-
if (held.has(k))
|
|
346
|
-
return;
|
|
347
|
-
const controller = new AbortController();
|
|
348
|
-
const work = { type: 'polygons', key, z, x, y, priority: index < 4 ? 'auto' : 'low' };
|
|
349
|
-
held.set(k, controller);
|
|
350
|
-
this.request(z, x, y, work, controller.signal).then((r) => {
|
|
351
|
-
if (controller.signal.aborted || !('polygons' in r))
|
|
352
|
-
return;
|
|
353
|
-
const tile = r.polygons;
|
|
354
|
-
// The features' properties as the style reads them
|
|
355
|
-
extrusions.setTile(k, { arrays: tile.arrays, properties: tile.properties });
|
|
356
|
-
}, () => {
|
|
357
|
-
// A failure is asked for again on the next update
|
|
358
|
-
if (held.get(k) === controller)
|
|
359
|
-
held.delete(k);
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
};
|
|
363
|
-
this.followView(update, held, update);
|
|
364
|
-
return extrusions;
|
|
168
|
+
return extrusions(this.context, options);
|
|
365
169
|
}
|
|
366
170
|
/**
|
|
367
171
|
* The wind through the streets around the view, as particles at street level (`height`, default 3 metres) between
|
|
@@ -375,114 +179,7 @@ export class VectorTiles {
|
|
|
375
179
|
* layer's (`addFlow`)
|
|
376
180
|
*/
|
|
377
181
|
streetWind(options = {}) {
|
|
378
|
-
|
|
379
|
-
const renderer = this.renderer((l) => (l.type === 'fill' || l.type === 'fill-extrusion') && layers(l), []);
|
|
380
|
-
const [lon, lat] = this.map.getCamera().center;
|
|
381
|
-
// Nothing yet: a field of no value, where no particle is born
|
|
382
|
-
const empty = { width: 2, height: 2, u: [NaN, NaN, NaN, NaN], v: [NaN, NaN, NaN, NaN] };
|
|
383
|
-
const layer = this.map.addFlow({
|
|
384
|
-
height: 3,
|
|
385
|
-
...flow,
|
|
386
|
-
field: { ...empty, bounds: [lon, lat, lon + 1e-4, lat + 1e-4] },
|
|
387
|
-
});
|
|
388
|
-
const state = { wind: options.wind ?? [3, 2], asked: 0, wait: 1000 };
|
|
389
|
-
/** The flow of the wind far away: `u` times the flow towards the east plus `v` times the one towards the north */
|
|
390
|
-
const send = () => {
|
|
391
|
-
const { flows, grid, wind } = state;
|
|
392
|
-
if (!flows || !grid)
|
|
393
|
-
return;
|
|
394
|
-
const [wu, wv] = wind;
|
|
395
|
-
const u = flows.eastU.map((e, i) => wu * e + wv * (flows.northU[i] ?? 0));
|
|
396
|
-
const v = flows.eastV.map((e, i) => wu * e + wv * (flows.northV[i] ?? 0));
|
|
397
|
-
layer.setField({ ...grid, u, v });
|
|
398
|
-
};
|
|
399
|
-
const held = new Map();
|
|
400
|
-
const first = {};
|
|
401
|
-
const computed = new Promise((resolve) => {
|
|
402
|
-
first.resolve = resolve;
|
|
403
|
-
});
|
|
404
|
-
const update = async () => {
|
|
405
|
-
if (this.destroyed)
|
|
406
|
-
return;
|
|
407
|
-
const camera = this.map.getCamera();
|
|
408
|
-
const [x, y] = [mercatorX(camera.center[0]), mercatorY(camera.center[1])];
|
|
409
|
-
// Half the square in Mercator, at its latitude
|
|
410
|
-
const half = size / 2 / (40_075_016.686 * Math.cos((camera.center[1] * Math.PI) / 180));
|
|
411
|
-
const square = state.square;
|
|
412
|
-
// Off street level, or still in the square's middle: nothing to compute
|
|
413
|
-
if (camera.zoom < minZoom)
|
|
414
|
-
return;
|
|
415
|
-
if (square &&
|
|
416
|
-
Math.abs(x - (square[0] + square[2]) / 2) < half / 3 &&
|
|
417
|
-
Math.abs(y - (square[1] + square[3]) / 2) < half / 3) {
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const { key, maxzoom } = await renderer;
|
|
421
|
-
const bounds = [x - half, y - half, x + half, y + half];
|
|
422
|
-
const n = 2 ** maxzoom;
|
|
423
|
-
const tiles = [];
|
|
424
|
-
for (let ty = Math.floor(bounds[1] * n); ty <= Math.floor(bounds[3] * n); ty++) {
|
|
425
|
-
for (let tx = Math.floor(bounds[0] * n); tx <= Math.floor(bounds[2] * n); tx++)
|
|
426
|
-
tiles.push(tx, ty);
|
|
427
|
-
}
|
|
428
|
-
const sides = Math.max(8, Math.round(size / cell));
|
|
429
|
-
const work = {
|
|
430
|
-
type: 'street',
|
|
431
|
-
key,
|
|
432
|
-
module: this.map.gridUrl,
|
|
433
|
-
z: maxzoom,
|
|
434
|
-
tiles,
|
|
435
|
-
bounds,
|
|
436
|
-
width: sides,
|
|
437
|
-
height: sides,
|
|
438
|
-
iterations,
|
|
439
|
-
};
|
|
440
|
-
const asked = ++state.asked;
|
|
441
|
-
held.forEach((c) => {
|
|
442
|
-
c.abort();
|
|
443
|
-
});
|
|
444
|
-
held.clear();
|
|
445
|
-
const controller = new AbortController();
|
|
446
|
-
held.set('square', controller);
|
|
447
|
-
state.square = bounds;
|
|
448
|
-
const answer = await this.request(maxzoom, Math.floor(x * n), Math.floor(y * n), work, controller.signal).catch((error) => ({ error }));
|
|
449
|
-
if (asked !== state.asked || controller.signal.aborted)
|
|
450
|
-
return;
|
|
451
|
-
// Failed (a tile or the grid module not fetched, a worker of an older build): the square asked for again later,
|
|
452
|
-
// a while after each failure
|
|
453
|
-
if (!('street' in answer)) {
|
|
454
|
-
console.warn("isard: the streets' wind not computed, tried again", 'error' in answer ? answer.error : answer);
|
|
455
|
-
state.square = undefined;
|
|
456
|
-
clearTimeout(state.retry);
|
|
457
|
-
state.retry = window.setTimeout(() => {
|
|
458
|
-
update().catch(() => undefined);
|
|
459
|
-
}, state.wait);
|
|
460
|
-
state.wait = Math.min(state.wait * 2, 30_000);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
state.wait = 1000;
|
|
464
|
-
state.flows = answer.street;
|
|
465
|
-
state.grid = {
|
|
466
|
-
width: sides,
|
|
467
|
-
height: sides,
|
|
468
|
-
bounds: [longitude(bounds[0]), latitude(bounds[3]), longitude(bounds[2]), latitude(bounds[1])],
|
|
469
|
-
};
|
|
470
|
-
send();
|
|
471
|
-
first.resolve?.();
|
|
472
|
-
};
|
|
473
|
-
renderer.catch(() => undefined);
|
|
474
|
-
this.layers.push(layer);
|
|
475
|
-
this.followView(update, held, () => undefined);
|
|
476
|
-
this.cleanup.push(() => {
|
|
477
|
-
clearTimeout(state.retry);
|
|
478
|
-
});
|
|
479
|
-
return Object.assign(layer, {
|
|
480
|
-
computed,
|
|
481
|
-
setWind: (wind) => {
|
|
482
|
-
state.wind = wind;
|
|
483
|
-
send();
|
|
484
|
-
},
|
|
485
|
-
});
|
|
182
|
+
return streetWind(this.context, options);
|
|
486
183
|
}
|
|
487
184
|
/**
|
|
488
185
|
* Trees on the style's woods, forests and grass (or the fill layers `layers` keeps) as 3D models, from street level
|
|
@@ -495,87 +192,7 @@ export class VectorTiles {
|
|
|
495
192
|
* engine with 3D Tiles.
|
|
496
193
|
*/
|
|
497
194
|
trees(options = {}) {
|
|
498
|
-
|
|
499
|
-
const model = options.url ? { url: options.url } : { data: options.data ?? treeModel(options.colors) };
|
|
500
|
-
const trees = this.map.addModels({
|
|
501
|
-
...model,
|
|
502
|
-
instances: [],
|
|
503
|
-
...(options.before ? { before: options.before } : {}),
|
|
504
|
-
});
|
|
505
|
-
const renderer = this.renderer((l) => l.type === 'fill' && layers(l), []);
|
|
506
|
-
renderer.catch(() => undefined);
|
|
507
|
-
this.layers.push(trees);
|
|
508
|
-
/** The tiles held or asked for, by key, and the trees of those that came */
|
|
509
|
-
const held = new Map();
|
|
510
|
-
const found = new Map();
|
|
511
|
-
/** The trees drawn to the engine (longitude, latitude, altitude, heading, pitch, roll, scale each): all of them
|
|
512
|
-
* near the view's center, past `near` metres those of a rank under (near / distance)², at most `maxInstances` */
|
|
513
|
-
const send = () => {
|
|
514
|
-
const { center, zoom } = this.map.getCamera();
|
|
515
|
-
const near = TREES_NEAR_METRES * 2 ** (16 - zoom);
|
|
516
|
-
const metresPerDegree = 111320;
|
|
517
|
-
const cos = Math.cos((center[1] * Math.PI) / 180);
|
|
518
|
-
const chosen = [];
|
|
519
|
-
found.forEach((t) => {
|
|
520
|
-
for (let i = 0; i < t.length / TREE_NUMBERS; i += 1) {
|
|
521
|
-
const dx = ((t[i * TREE_NUMBERS] ?? 0) - center[0]) * metresPerDegree * cos;
|
|
522
|
-
const dy = ((t[i * TREE_NUMBERS + 1] ?? 0) - center[1]) * metresPerDegree;
|
|
523
|
-
const far = (dx * dx + dy * dy) / (near * near);
|
|
524
|
-
const score = (t[i * TREE_NUMBERS + 4] ?? 0) * Math.max(1, far);
|
|
525
|
-
if (score < 1)
|
|
526
|
-
chosen.push([score, t, i]);
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
if (chosen.length > maxInstances)
|
|
530
|
-
chosen.sort((a, b) => a[0] - b[0]).splice(maxInstances);
|
|
531
|
-
const out = new Float64Array(chosen.length * 7);
|
|
532
|
-
chosen.forEach(([, t, i], k) => {
|
|
533
|
-
const at = i * TREE_NUMBERS;
|
|
534
|
-
out.set([t[at] ?? 0, t[at + 1] ?? 0, 0, t[at + 3] ?? 0, 0, 0, t[at + 2] ?? 1], k * 7);
|
|
535
|
-
});
|
|
536
|
-
trees.setInstanceNumbers(out);
|
|
537
|
-
};
|
|
538
|
-
const update = async () => {
|
|
539
|
-
if (this.destroyed)
|
|
540
|
-
return;
|
|
541
|
-
const { key, maxzoom, layers: list } = await renderer;
|
|
542
|
-
const camera = this.map.getCamera();
|
|
543
|
-
const z = maxzoom;
|
|
544
|
-
const wanted = camera.zoom < minZoom ? [] : tilesAround(this.map, camera, z, maxTiles);
|
|
545
|
-
const keys = new Set(wanted.map(([x, y]) => `${z}/${x}/${y}`));
|
|
546
|
-
const metres = list.map((l) => (typeof spacing === 'number' ? spacing : spacing(l)));
|
|
547
|
-
const dropped = [...held.keys()].filter((k) => !keys.has(k));
|
|
548
|
-
// Out of view: dropped, or no longer asked for
|
|
549
|
-
dropped.forEach((k) => {
|
|
550
|
-
held.get(k)?.abort();
|
|
551
|
-
held.delete(k);
|
|
552
|
-
found.delete(k);
|
|
553
|
-
});
|
|
554
|
-
// Dense around the new center
|
|
555
|
-
send();
|
|
556
|
-
wanted.forEach(([x, y], index) => {
|
|
557
|
-
const k = `${z}/${x}/${y}`;
|
|
558
|
-
if (held.has(k))
|
|
559
|
-
return;
|
|
560
|
-
const controller = new AbortController();
|
|
561
|
-
const priority = index < 4 ? 'auto' : 'low';
|
|
562
|
-
const work = { type: 'trees', key, z, x, y, spacing: metres, maxTrees, priority };
|
|
563
|
-
held.set(k, controller);
|
|
564
|
-
this.request(z, x, y, work, controller.signal).then((r) => {
|
|
565
|
-
if (controller.signal.aborted || !('trees' in r))
|
|
566
|
-
return;
|
|
567
|
-
found.set(k, r.trees);
|
|
568
|
-
send();
|
|
569
|
-
}, () => {
|
|
570
|
-
// A failure is asked for again on the next update
|
|
571
|
-
if (held.get(k) === controller)
|
|
572
|
-
held.delete(k);
|
|
573
|
-
});
|
|
574
|
-
});
|
|
575
|
-
};
|
|
576
|
-
// While the camera moves, the trees drawn follow its center (dense near it)
|
|
577
|
-
this.followView(update, held, send);
|
|
578
|
-
return trees;
|
|
195
|
+
return trees(this.context, options);
|
|
579
196
|
}
|
|
580
197
|
/**
|
|
581
198
|
* A layer drawing the style's fills, lines and buildings (or those `layers` keeps; backgrounds as fills, the
|
|
@@ -593,146 +210,7 @@ export class VectorTiles {
|
|
|
593
210
|
* a road, not its casing, a railway, a path or a tunnel).
|
|
594
211
|
*/
|
|
595
212
|
vectors(options = {}) {
|
|
596
|
-
|
|
597
|
-
const vector = this.map.addVectorLayer(before);
|
|
598
|
-
const state = { renderer: this.renderer((l) => vectorKind(l) !== undefined && layers(l), []), update: 0, style: 0 };
|
|
599
|
-
state.renderer.catch(() => undefined);
|
|
600
|
-
this.layers.push(vector);
|
|
601
|
-
const sublayers = (list) => {
|
|
602
|
-
const zoom = this.map.getCamera().zoom;
|
|
603
|
-
return list.map((l) => ({
|
|
604
|
-
id: l.id,
|
|
605
|
-
sourceLayer: l['source-layer'],
|
|
606
|
-
kind: vectorKind(l) ?? 'fill',
|
|
607
|
-
minZoom: l.minzoom ?? 0,
|
|
608
|
-
maxZoom: l.maxzoom ?? 24,
|
|
609
|
-
visible: l.layout?.visibility !== 'none',
|
|
610
|
-
opacity: zoomOnly(l.paint?.[`${l.type}-opacity`], 1) ?? 1,
|
|
611
|
-
cap: lineCap(l.layout?.['line-cap'], zoom),
|
|
612
|
-
lit: l.type === 'line' && streets(l),
|
|
613
|
-
dash: l.type === 'line' ? dashes(l.paint?.['line-dasharray'], zoom) : [0, 0, 0, 0],
|
|
614
|
-
}));
|
|
615
|
-
};
|
|
616
|
-
/** The tiles held or asked for, by `z/x/y` */
|
|
617
|
-
const held = new Map();
|
|
618
|
-
/** A tile's geometry, or its styles alone, asked of the worker of its tile of the source's level (which keeps the
|
|
619
|
-
* decoded tile its deeper tiles are cut from) */
|
|
620
|
-
const ask = ({ key, maxzoom }, [z = 0, x = 0, y = 0], styles, priority, signal) => {
|
|
621
|
-
const d = Math.max(0, z - maxzoom);
|
|
622
|
-
const work = { type: 'vectors', key, z, x, y, deepest: deepestLevel(maxzoom), priority };
|
|
623
|
-
// The geometry made ready for the GPU in the workers, with the prisms the relief needs
|
|
624
|
-
return this.request(z - d, Math.floor(x / 2 ** d), Math.floor(y / 2 ** d), styles ? { ...work, styles } : { ...work, prism: this.map.hasRelief }, signal);
|
|
625
|
-
};
|
|
626
|
-
// The workers tessellate the tiles (the main thread only uploads them), once they know the module
|
|
627
|
-
this.pool.broadcast({ type: 'tess', url: this.map.tessUrl });
|
|
628
|
-
/** The styles of a tile held asked for again, the tile kept as it is */
|
|
629
|
-
const restyleTile = async (renderer, k) => {
|
|
630
|
-
const r = await ask(renderer, k.split('/').map(Number), true, 'auto').catch(() => undefined);
|
|
631
|
-
if (r && 'styles' in r)
|
|
632
|
-
vector.setTileStyles(k, r.styles);
|
|
633
|
-
};
|
|
634
|
-
const update = async () => {
|
|
635
|
-
if (this.destroyed)
|
|
636
|
-
return;
|
|
637
|
-
const generation = ++state.update;
|
|
638
|
-
const { key, maxzoom, layers: list } = await state.renderer;
|
|
639
|
-
vector.setSublayers(sublayers(state.own ?? list));
|
|
640
|
-
const view = this.map.engine.vectorTilesInView(0, deepestLevel(maxzoom));
|
|
641
|
-
const wanted = Array.from({ length: Math.floor(view.length / 3) }, (_, i) => [
|
|
642
|
-
...view.subarray(i * 3, i * 3 + 3),
|
|
643
|
-
]);
|
|
644
|
-
const keys = new Set(wanted.map((t) => t.join('/')));
|
|
645
|
-
const loads = wanted.flatMap((t, index) => {
|
|
646
|
-
const k = t.join('/');
|
|
647
|
-
if (held.has(k))
|
|
648
|
-
return [];
|
|
649
|
-
const controller = new AbortController();
|
|
650
|
-
const style = state.style;
|
|
651
|
-
held.set(k, controller);
|
|
652
|
-
return [
|
|
653
|
-
ask({ key, maxzoom }, t, false, index < 4 ? 'high' : 'auto', controller.signal).then(async (r) => {
|
|
654
|
-
if (controller.signal.aborted || !('vectors' in r))
|
|
655
|
-
return;
|
|
656
|
-
vector.setTile(k, r.vectors);
|
|
657
|
-
// Cut under a style changed since: its styles asked for again
|
|
658
|
-
if (style !== state.style)
|
|
659
|
-
await restyleTile(await state.renderer, k);
|
|
660
|
-
}, () => {
|
|
661
|
-
// A failure is asked for again on the next update
|
|
662
|
-
if (held.get(k) === controller)
|
|
663
|
-
held.delete(k);
|
|
664
|
-
}),
|
|
665
|
-
];
|
|
666
|
-
});
|
|
667
|
-
// Out of view: those still asked for go at once, those drawn once their replacements are there (no hole)
|
|
668
|
-
const drop = (drawn) => {
|
|
669
|
-
const shown = new Set(vector.tileKeys);
|
|
670
|
-
[...held].forEach(([k, controller]) => {
|
|
671
|
-
if (keys.has(k) || shown.has(k) !== drawn)
|
|
672
|
-
return;
|
|
673
|
-
controller.abort();
|
|
674
|
-
held.delete(k);
|
|
675
|
-
vector.removeTile(k);
|
|
676
|
-
});
|
|
677
|
-
};
|
|
678
|
-
drop(false);
|
|
679
|
-
await Promise.all(loads);
|
|
680
|
-
// A later update (the view moved again meanwhile) drops for its own view: these keys are no longer the view
|
|
681
|
-
if (!this.destroyed && generation === state.update)
|
|
682
|
-
drop(true);
|
|
683
|
-
};
|
|
684
|
-
const refresh = () => {
|
|
685
|
-
update().catch(() => undefined);
|
|
686
|
-
};
|
|
687
|
-
/** The style layers changed: the workers' copy, then the styles of the tiles held */
|
|
688
|
-
const restyle = async (list) => {
|
|
689
|
-
state.own = list;
|
|
690
|
-
state.style++;
|
|
691
|
-
state.renderer = state.renderer.then(({ key }) => this.renderer(() => false, list, key));
|
|
692
|
-
const renderer = await state.renderer;
|
|
693
|
-
vector.setSublayers(sublayers(list));
|
|
694
|
-
await Promise.all(vector.tileKeys.map((k) => restyleTile(renderer, k)));
|
|
695
|
-
};
|
|
696
|
-
vector.restyle = (id, group, name, value) => {
|
|
697
|
-
state.renderer
|
|
698
|
-
.then(({ layers: list }) => {
|
|
699
|
-
const next = (state.own ?? list).map((l) => l.id === id ? { ...l, [group]: { ...l[group], [name]: value } } : l);
|
|
700
|
-
// Visibility and dashes are the engine's: the tiles stay as they are
|
|
701
|
-
if ((group === 'layout' && name === 'visibility') || name === 'line-dasharray') {
|
|
702
|
-
state.own = next;
|
|
703
|
-
vector.setSublayers(sublayers(next));
|
|
704
|
-
return undefined;
|
|
705
|
-
}
|
|
706
|
-
// An outline added or taken away: a feature more or less in each tile, cut again
|
|
707
|
-
if (name === 'fill-outline-color' || name === 'fill-antialias') {
|
|
708
|
-
state.own = next;
|
|
709
|
-
state.style++;
|
|
710
|
-
state.renderer = state.renderer.then(({ key }) => this.renderer(() => false, next, key));
|
|
711
|
-
vector.setSublayers(sublayers(next));
|
|
712
|
-
vector.recut?.();
|
|
713
|
-
return undefined;
|
|
714
|
-
}
|
|
715
|
-
return restyle(next);
|
|
716
|
-
})
|
|
717
|
-
.catch(() => undefined);
|
|
718
|
-
};
|
|
719
|
-
vector.recut = () => {
|
|
720
|
-
held.forEach((controller) => {
|
|
721
|
-
controller.abort();
|
|
722
|
-
});
|
|
723
|
-
held.clear();
|
|
724
|
-
refresh();
|
|
725
|
-
};
|
|
726
|
-
vector.styled = state.renderer.then(({ layers: list }) => {
|
|
727
|
-
if (!this.destroyed)
|
|
728
|
-
vector.setSublayers(sublayers(state.own ?? list));
|
|
729
|
-
});
|
|
730
|
-
// Reported by the style's `ready`
|
|
731
|
-
vector.styled.catch(() => undefined);
|
|
732
|
-
// While the camera moves, the tiles follow it (as the labels do: a long drag or flight does not leave the view of
|
|
733
|
-
// its start drawn)
|
|
734
|
-
this.followView(update, held, update);
|
|
735
|
-
return vector;
|
|
213
|
+
return vectors(this.context, options);
|
|
736
214
|
}
|
|
737
215
|
/**
|
|
738
216
|
* A symbol layer with the labels of the style's symbol layers (or those `layers` keeps), updated as the view
|
|
@@ -742,166 +220,13 @@ export class VectorTiles {
|
|
|
742
220
|
* the center at the zoom of the view. It stays above the layers added after it unless one is moved over it.
|
|
743
221
|
*/
|
|
744
222
|
labels(options = {}) {
|
|
745
|
-
|
|
746
|
-
const symbol = this.map.addSymbols({
|
|
747
|
-
data: [],
|
|
748
|
-
text: (p) => p.text,
|
|
749
|
-
font: (p) => p.font,
|
|
750
|
-
size: (p) => size(p.size),
|
|
751
|
-
color: (p) => p.color,
|
|
752
|
-
haloColor: (p) => p.haloColor,
|
|
753
|
-
haloWidth: (p) => p.haloWidth,
|
|
754
|
-
anchor: (p) => anchor(p.anchor),
|
|
755
|
-
offset: (p) => p.offset,
|
|
756
|
-
maxWidth: (p) => p.maxWidth,
|
|
757
|
-
lineHeight: (p) => p.lineHeight,
|
|
758
|
-
overlap: (p) => p.overlap,
|
|
759
|
-
placement: (p) => (p.textAlong ? 'line' : 'point'),
|
|
760
|
-
textOptional: (p) => p.textOptional === true,
|
|
761
|
-
icon: (p) => (p.icon ? this.iconPrefix + p.icon.image : ''),
|
|
762
|
-
iconSize: (p) => size(p.icon?.size ?? 1, 1),
|
|
763
|
-
iconAnchor: (p) => anchor(p.icon?.anchor),
|
|
764
|
-
iconOffset: (p) => p.icon?.offset ?? [0, 0],
|
|
765
|
-
iconRotate: (p) => p.icon?.rotate ?? 0,
|
|
766
|
-
iconRotationAlignment: (p) => p.icon?.alignment ?? 'viewport',
|
|
767
|
-
iconOverlap: (p) => p.icon?.overlap ?? false,
|
|
768
|
-
iconOptional: (p) => p.icon?.optional ?? false,
|
|
769
|
-
iconTextFit: (p) => p.icon?.fit ?? 'none',
|
|
770
|
-
iconTextFitPadding: (p) => p.icon?.padding ?? [0, 0, 0, 0],
|
|
771
|
-
iconColor: (p) => p.icon?.color ?? '#000',
|
|
772
|
-
iconOpacity: (p) => p.icon?.opacity ?? 1,
|
|
773
|
-
});
|
|
774
|
-
this.map.keepOnTop(symbol);
|
|
775
|
-
this.layers.push(symbol);
|
|
776
|
-
const renderer = this.ready.then(async ({ style }) => {
|
|
777
|
-
// The style's fonts from its own glyphs
|
|
778
|
-
if (style.glyphs)
|
|
779
|
-
this.map.glyphs.register(style.glyphs, VectorTiles.fonts(style));
|
|
780
|
-
// Its icons from its sprites, before the first labels
|
|
781
|
-
const icons = style.layers.some((l) => l.type === 'symbol' && l.layout?.['icon-image'] !== undefined);
|
|
782
|
-
if (icons && style.sprite) {
|
|
783
|
-
(await this.loadSprites(style.sprite)).forEach((sprite) => {
|
|
784
|
-
if (!this.destroyed)
|
|
785
|
-
this.map.icons.addSprite(this.iconPrefix, sprite.icons, () => readPixels(sprite.image));
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
return this.renderer((l) => l.type === 'symbol' && layers(l), []);
|
|
789
|
-
});
|
|
790
|
-
renderer.catch(() => undefined);
|
|
791
|
-
/** The labels of the tiles, as features by style layer, the higher layers first: each list is given to the symbol
|
|
792
|
-
* layer as a group, shaped once while the tile is kept */
|
|
793
|
-
const tiles = new Lru(256);
|
|
794
|
-
let generation = 0;
|
|
795
|
-
/** The tiles whose labels are shown */
|
|
796
|
-
let shown = '';
|
|
797
|
-
/** The update asked again after tiles failed */
|
|
798
|
-
let retry = 0;
|
|
799
|
-
const update = async () => {
|
|
800
|
-
if (this.destroyed)
|
|
801
|
-
return;
|
|
802
|
-
const { key, maxzoom } = await renderer;
|
|
803
|
-
const run = ++generation;
|
|
804
|
-
const camera = this.map.getCamera();
|
|
805
|
-
const zoom = Math.max(0, Math.min(Math.floor(camera.zoom), 24));
|
|
806
|
-
const ground = this.ground && this.map.hasLayer(this.ground) ? this.map.engine.wantedTiles(this.ground.id) : [];
|
|
807
|
-
const keys = [];
|
|
808
|
-
if (ground.length > 0) {
|
|
809
|
-
// The ground's tiles, the nearest first: a tile so many levels coarser than the finest in view has its
|
|
810
|
-
// labels of a zoom as many under the view's, past the source's deepest level those of that level at the zoom
|
|
811
|
-
const groundTiles = Array.from({ length: Math.ceil(ground.length / 3) }, (_, k) => [
|
|
812
|
-
ground[3 * k] ?? 0,
|
|
813
|
-
ground[3 * k + 1] ?? 0,
|
|
814
|
-
ground[3 * k + 2] ?? 0,
|
|
815
|
-
]);
|
|
816
|
-
const top = groundTiles.reduce((t, [z]) => Math.max(t, z), 0);
|
|
817
|
-
groundTiles.forEach(([z, x, y]) => {
|
|
818
|
-
if (keys.length >= maxTiles)
|
|
819
|
-
return;
|
|
820
|
-
const at = Math.max(0, zoom - (top - z));
|
|
821
|
-
const tz = Math.min(at, maxzoom, z);
|
|
822
|
-
const shift = 2 ** (z - tz);
|
|
823
|
-
const key = `${tz}/${Math.floor(x / shift)}/${Math.floor(y / shift)}${at > tz ? `@${at}` : ''}`;
|
|
824
|
-
if (!keys.includes(key))
|
|
825
|
-
keys.push(key);
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
else {
|
|
829
|
-
const z = Math.min(zoom, maxzoom);
|
|
830
|
-
const wanted = tilesAround(this.map, camera, z, maxTiles);
|
|
831
|
-
// Past the source's deepest level, its tiles' labels at the zoom of the view (the layers of the deeper zooms)
|
|
832
|
-
keys.push(...wanted.map(([x, y]) => `${z}/${x}/${y}${zoom > z ? `@${zoom}` : ''}`));
|
|
833
|
-
}
|
|
834
|
-
// The same tiles as the labels shown: nothing to do
|
|
835
|
-
const set = keys.join(' ');
|
|
836
|
-
if (set === shown)
|
|
837
|
-
return;
|
|
838
|
-
const lists = await Promise.all(keys.map((k, index) => {
|
|
839
|
-
const cached = tiles.get(k);
|
|
840
|
-
if (cached)
|
|
841
|
-
return cached;
|
|
842
|
-
const [tile, deeper] = k.split('@');
|
|
843
|
-
const [tz = 0, x = 0, y = 0] = (tile ?? '').split('/').map(Number);
|
|
844
|
-
// The nearest tiles first: the others wait for the raster tiles
|
|
845
|
-
const priority = index < 4 ? 'auto' : 'low';
|
|
846
|
-
const work = {
|
|
847
|
-
type: 'labels',
|
|
848
|
-
key,
|
|
849
|
-
z: tz,
|
|
850
|
-
x,
|
|
851
|
-
y,
|
|
852
|
-
priority,
|
|
853
|
-
...(deeper === undefined ? {} : { zoom: Number(deeper) }),
|
|
854
|
-
};
|
|
855
|
-
const p = this.request(tz, x, y, work).then((r) => tileLabels('labels' in r ? r.labels : []), () => {
|
|
856
|
-
// A network error is not kept: the tile is asked for again on the next update
|
|
857
|
-
tiles.delete(k);
|
|
858
|
-
return [];
|
|
859
|
-
});
|
|
860
|
-
// A bounded cache of tiles' labels, the least recently used dropped
|
|
861
|
-
tiles.set(k, p);
|
|
862
|
-
return p;
|
|
863
|
-
}));
|
|
864
|
-
if (run !== generation || this.destroyed)
|
|
865
|
-
return;
|
|
866
|
-
// A tile that failed is missing: the next update asks for it again, after a pause if no move brings one
|
|
867
|
-
shown = keys.every((k) => tiles.has(k)) ? set : '';
|
|
868
|
-
if (shown === '' && !retry) {
|
|
869
|
-
retry = window.setTimeout(() => {
|
|
870
|
-
retry = 0;
|
|
871
|
-
refresh();
|
|
872
|
-
}, RETRY_LABELS_MS);
|
|
873
|
-
}
|
|
874
|
-
// The labels of the higher style layers first: they win the collisions; for each style layer, the tiles nearest
|
|
875
|
-
// to the center first
|
|
876
|
-
const layerIds = [...new Set(lists.flatMap((list) => list.map(([layer]) => layer)))].sort((a, b) => b - a);
|
|
877
|
-
const groups = layerIds.flatMap((layer) => lists.map((list) => list.find(([l]) => l === layer)?.[1]).filter((group) => group !== undefined));
|
|
878
|
-
symbol.setGroups(groups);
|
|
879
|
-
};
|
|
880
|
-
// The style failed to load: reported by `ready`
|
|
881
|
-
const refresh = () => {
|
|
882
|
-
update().catch(() => undefined);
|
|
883
|
-
};
|
|
884
|
-
let timer = 0;
|
|
885
|
-
const schedule = () => {
|
|
886
|
-
if (timer)
|
|
887
|
-
return;
|
|
888
|
-
timer = window.setTimeout(() => {
|
|
889
|
-
timer = 0;
|
|
890
|
-
refresh();
|
|
891
|
-
}, 250);
|
|
892
|
-
};
|
|
893
|
-
this.cleanup.push(this.map.on('move', schedule), this.map.on('moveend', refresh), () => {
|
|
894
|
-
clearTimeout(timer);
|
|
895
|
-
clearTimeout(retry);
|
|
896
|
-
});
|
|
897
|
-
refresh();
|
|
898
|
-
return symbol;
|
|
223
|
+
return labels(this.context, options);
|
|
899
224
|
}
|
|
900
225
|
/**
|
|
901
226
|
* Makes a layer's tiles follow the view: `update` now and where the camera stops (`moveend`), `moving` every
|
|
902
|
-
* `VECTOR_MOVE_REFRESH_MS` while it moves; the requests
|
|
227
|
+
* `VECTOR_MOVE_REFRESH_MS` while it moves; the `requests` aborted when destroyed
|
|
903
228
|
*/
|
|
904
|
-
followView(update,
|
|
229
|
+
followView(update, requests, moving) {
|
|
905
230
|
const refresh = () => {
|
|
906
231
|
update().catch(() => undefined);
|
|
907
232
|
};
|
|
@@ -916,10 +241,7 @@ export class VectorTiles {
|
|
|
916
241
|
};
|
|
917
242
|
this.cleanup.push(this.map.on('move', follow), this.map.on('moveend', refresh), () => {
|
|
918
243
|
clearTimeout(pace.timer);
|
|
919
|
-
|
|
920
|
-
controller.abort();
|
|
921
|
-
});
|
|
922
|
-
held.clear();
|
|
244
|
+
requests.abortAll();
|
|
923
245
|
});
|
|
924
246
|
refresh();
|
|
925
247
|
}
|
|
@@ -960,34 +282,4 @@ export class VectorTiles {
|
|
|
960
282
|
this.renderers.length = 0;
|
|
961
283
|
}
|
|
962
284
|
}
|
|
963
|
-
/**
|
|
964
|
-
* A `line-dasharray` as the engine takes it: two dashes and gaps in line widths (an odd count repeated, as the
|
|
965
|
-
* canvas's `setLineDash`; past four, the first four), zeros for a solid line
|
|
966
|
-
*/
|
|
967
|
-
export const dashes = (value, zoom) => {
|
|
968
|
-
const v = isExpression(value) ? evaluate(value, { zoom, properties: {}, type: 'Unknown' }) : value;
|
|
969
|
-
const list = Array.isArray(v) ? v.map(Number).filter((d) => Number.isFinite(d) && d >= 0) : [];
|
|
970
|
-
const even = list.length % 2 === 1 ? [...list, ...list] : list;
|
|
971
|
-
const [a = 0, b = 0, c = 0, d = 0] = even.length >= 2 && even.some((x) => x > 0) ? even : [];
|
|
972
|
-
return [a, b, c, d];
|
|
973
|
-
};
|
|
974
|
-
/** Metres around the view's center where every tree is drawn at zoom 16 (twice as far a zoom level out) */
|
|
975
|
-
const TREES_NEAR_METRES = 350;
|
|
976
|
-
/** Whether a style layer draws streets, which glow at night: its id names a road (OpenMapTiles' styles, PLAN IGN's),
|
|
977
|
-
* not its casing, a railway, a path or a tunnel */
|
|
978
|
-
const isStreet = (layer) => /road|highway|street|motorway|trunk|primary|secondary|tertiary|minor|routier/i.test(layer.id) &&
|
|
979
|
-
!/casing|outline|rail|path|track|ferry|tunnel|pier|area|pattern|label/i.test(layer.id);
|
|
980
|
-
/** A `line-cap` as the engine draws it, at the camera's zoom (MapLibre's default: butt) */
|
|
981
|
-
const lineCap = (value, zoom) => {
|
|
982
|
-
const v = isExpression(value) ? evaluate(value, { zoom, properties: {}, type: 'Unknown' }) : value;
|
|
983
|
-
return v === 'round' || v === 'square' ? v : 'butt';
|
|
984
|
-
};
|
|
985
|
-
/** The deepest level `vectors()` cuts its tiles at: past the source's deepest level, its tiles cut again into deeper
|
|
986
|
-
* ones (overzoom, as MapLibre does) down to level 18, their filters and styles those of their level, smaller tiles
|
|
987
|
-
* drawn in a closer view; past it, the tiles of that level stretched */
|
|
988
|
-
const OVERZOOM_LEVEL = 18;
|
|
989
|
-
/** The deepest level of the tiles of `vectors()` over a source whose deepest is `maxzoom` */
|
|
990
|
-
const deepestLevel = (maxzoom) => Math.max(maxzoom, OVERZOOM_LEVEL);
|
|
991
|
-
/** Milliseconds between two updates of the tiles of `vectors()` while the camera moves */
|
|
992
|
-
const VECTOR_MOVE_REFRESH_MS = 250;
|
|
993
285
|
//# sourceMappingURL=vectortiles.js.map
|