isard 1.2.0 → 1.4.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 +124 -0
- package/README.md +46 -39
- package/dist/basemap/basemaps.js +1 -0
- package/dist/basemap/basemaps.js.map +1 -1
- package/dist/basemap/pmtileslayer.d.ts +33 -0
- package/dist/basemap/pmtileslayer.js +97 -0
- package/dist/basemap/pmtileslayer.js.map +1 -0
- package/dist/basemap/trees.d.ts +26 -0
- package/dist/basemap/trees.js +141 -0
- package/dist/basemap/trees.js.map +1 -0
- package/dist/basemap/vectorstyle.d.ts +10 -0
- package/dist/basemap/vectorstyle.js +35 -0
- package/dist/basemap/vectorstyle.js.map +1 -1
- package/dist/basemap/vectortiles.d.ts +93 -44
- package/dist/basemap/vectortiles.js +511 -303
- package/dist/basemap/vectortiles.js.map +1 -1
- package/dist/basemap/workerpool.d.ts +17 -0
- package/dist/basemap/workerpool.js +203 -0
- package/dist/basemap/workerpool.js.map +1 -0
- package/dist/basemap.worker.js +2 -2
- package/dist/basemap.worker.js.map +18 -8
- package/dist/bind/layers.js +10 -0
- package/dist/bind/layers.js.map +1 -1
- package/dist/decoder.worker.js +4 -4
- package/dist/decoder.worker.js.map +3 -3
- package/dist/engine-loader.d.ts +8 -1
- package/dist/engine-loader.js +20 -1
- package/dist/engine-loader.js.map +1 -1
- package/dist/geoplateforme.d.ts +16 -0
- package/dist/geoplateforme.js +23 -0
- package/dist/geoplateforme.js.map +1 -1
- package/dist/grid-module.d.ts +44 -0
- package/dist/grid-module.js +63 -0
- package/dist/grid-module.js.map +1 -0
- package/dist/grids.d.ts +51 -0
- package/dist/grids.js +88 -0
- package/dist/grids.js.map +1 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/isard_bg.wasm +0 -0
- package/dist/isard_core_bg.wasm +0 -0
- package/dist/isard_grid_bg.wasm +0 -0
- package/dist/isard_tess_bg.wasm +0 -0
- package/dist/layers/flow.d.ts +111 -0
- package/dist/layers/flow.js +222 -0
- package/dist/layers/flow.js.map +1 -0
- package/dist/layers/layers.js +9 -0
- package/dist/layers/layers.js.map +1 -1
- package/dist/layers/symbols.d.ts +2 -0
- package/dist/layers/symbols.js +2 -1
- package/dist/layers/symbols.js.map +1 -1
- package/dist/layers/vectors.d.ts +60 -0
- package/dist/layers/vectors.js +130 -0
- package/dist/layers/vectors.js.map +1 -0
- package/dist/map/frame-loop.d.ts +3 -0
- package/dist/map/frame-loop.js +6 -0
- package/dist/map/frame-loop.js.map +1 -1
- package/dist/map/layer-spec.d.ts +4 -1
- package/dist/map/layer-spec.js +2 -0
- package/dist/map/layer-spec.js.map +1 -1
- package/dist/map/raster-source.js +1 -1
- package/dist/map/raster-source.js.map +1 -1
- package/dist/map/workers.d.ts +5 -1
- package/dist/map/workers.js +36 -1
- package/dist/map/workers.js.map +1 -1
- package/dist/map.d.ts +35 -6
- package/dist/map.js +83 -11
- package/dist/map.js.map +1 -1
- package/dist/react/IsardMap.js +1 -0
- package/dist/react/IsardMap.js.map +1 -1
- package/dist/react/Layer.d.ts +2 -0
- package/dist/react/Layer.js.map +1 -1
- package/dist/react/Swipe.d.ts +11 -4
- package/dist/react/Swipe.js +26 -15
- package/dist/react/Swipe.js.map +1 -1
- package/dist/react/VectorGeometry.d.ts +35 -0
- package/dist/react/VectorGeometry.js +82 -0
- package/dist/react/VectorGeometry.js.map +1 -0
- package/dist/react/context.d.ts +2 -1
- package/dist/react/context.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/types.d.ts +30 -3
- package/dist/types.js.map +1 -1
- package/dist/ui/minimap.d.ts +25 -0
- package/dist/ui/minimap.js +90 -0
- package/dist/ui/minimap.js.map +1 -0
- package/dist/ui/scale.d.ts +19 -0
- package/dist/ui/scale.js +44 -0
- package/dist/ui/scale.js.map +1 -0
- package/dist/ui/search.d.ts +25 -0
- package/dist/ui/search.js +119 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/style.js +17 -0
- package/dist/ui/style.js.map +1 -1
- package/dist/util/sun.d.ts +11 -0
- package/dist/util/sun.js +26 -0
- package/dist/util/sun.js.map +1 -0
- package/dist/wasm/isard.d.ts +124 -7
- package/dist/wasm/isard.js +319 -8
- package/dist/wasm/isard_core.d.ts +124 -7
- package/dist/wasm/isard_core.js +319 -8
- package/dist/wasm/isard_grid.d.ts +101 -0
- package/dist/wasm/isard_grid.js +375 -0
- package/dist/wasm/isard_tess.d.ts +109 -0
- package/dist/wasm/isard_tess.js +398 -0
- package/dist/workers/basemap/geometry.d.ts +72 -0
- package/dist/workers/basemap/labels.d.ts +8 -0
- package/dist/workers/basemap/raster.d.ts +10 -0
- package/dist/workers/basemap/street.d.ts +19 -0
- package/dist/workers/basemap/tiles.d.ts +65 -0
- package/dist/workers/basemap/trees.d.ts +19 -0
- package/dist/workers/basemap.worker.d.ts +147 -22
- package/dist/workers/basemap.worker.js +93 -553
- package/dist/workers/basemap.worker.js.map +1 -1
- package/package.json +6 -4
- package/vite.js +11 -2
|
@@ -8,22 +8,24 @@
|
|
|
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
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import { assetUrl } from "../engine-loader.js";
|
|
11
|
+
import { withHeaders } from "../engine/requests.js";
|
|
13
12
|
import { IGN_ATTRIBUTION } from "../geoplateforme.js";
|
|
14
13
|
import { loadSprites, readPixels } from "../layers/icons.js";
|
|
15
|
-
import { mercatorX, mercatorY } from "../util/geo.js";
|
|
14
|
+
import { latitude, longitude, mercatorX, mercatorY } from "../util/geo.js";
|
|
16
15
|
import { isRecord } from "../util/guards.js";
|
|
17
16
|
import { Lru } from "../util/lru.js";
|
|
18
17
|
import { OPENFREEMAP_ATTRIBUTION } from "./basemaps.js";
|
|
19
18
|
import { pmtiles, pmtilesUrl } from "./pmtiles.js";
|
|
20
|
-
import {
|
|
21
|
-
import { isStyle, labelFont, } from "./vectorstyle.js";
|
|
19
|
+
import { isWoodland, TREE_NUMBERS, treeModel, treeSpacing } from "./trees.js";
|
|
20
|
+
import { evaluate, isExpression, isStyle, labelFont, vectorKind, zoomOnly, } from "./vectorstyle.js";
|
|
21
|
+
import { rendererKey, tilePriority } from "./workerpool.js";
|
|
22
22
|
/** OpenFreeMap's styles: OpenStreetMap data in the OpenMapTiles schema, worldwide, free and without a key */
|
|
23
23
|
export const OPENFREEMAP_STYLES = {
|
|
24
24
|
positron: 'https://tiles.openfreemap.org/styles/positron',
|
|
25
25
|
liberty: 'https://tiles.openfreemap.org/styles/liberty',
|
|
26
26
|
bright: 'https://tiles.openfreemap.org/styles/bright',
|
|
27
|
+
dark: 'https://tiles.openfreemap.org/styles/dark',
|
|
28
|
+
fiord: 'https://tiles.openfreemap.org/styles/fiord',
|
|
27
29
|
};
|
|
28
30
|
/** PLAN.IGN vector styles of the Géoplateforme (France) */
|
|
29
31
|
export const PLAN_STYLES = {
|
|
@@ -35,6 +37,7 @@ export const VECTOR_BASEMAPS = {
|
|
|
35
37
|
'osm-positron': OPENFREEMAP_STYLES.positron,
|
|
36
38
|
'osm-liberty': OPENFREEMAP_STYLES.liberty,
|
|
37
39
|
'osm-bright': OPENFREEMAP_STYLES.bright,
|
|
40
|
+
'osm-dark': OPENFREEMAP_STYLES.dark,
|
|
38
41
|
'ign-plan-vector': PLAN_STYLES.standard,
|
|
39
42
|
'ign-plan-grey-vector': PLAN_STYLES.grey,
|
|
40
43
|
};
|
|
@@ -120,193 +123,26 @@ const resolveSource = async (source) => {
|
|
|
120
123
|
};
|
|
121
124
|
/** Pause before the labels of tiles that failed are asked for again, when no move brings an update (ms) */
|
|
122
125
|
const RETRY_LABELS_MS = 3000;
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
127
|
-
/** Default URL of the basemap worker: in the page's assets folder (`isard/vite`), else next to the module */
|
|
128
|
-
export const defaultWorkerUrl = (assetsUrl) => {
|
|
129
|
-
return assetUrl('basemap.worker.js', undefined, assetsUrl);
|
|
130
|
-
};
|
|
131
|
-
/** Fetch priority of a raster tile drawn by the workers: 'high' near the center of the view, at the current zoom or
|
|
132
|
-
* coarser; 'low' far from it or finer (prefetch); 'auto' between */
|
|
133
|
-
export const tilePriority = (z, x, y, camera) => {
|
|
134
|
-
// A raster tile of level z covers the map at zoom z - 1
|
|
135
|
-
if (z - 1 > camera.zoom + 1)
|
|
136
|
-
return 'low';
|
|
137
|
-
const n = 2 ** z;
|
|
138
|
-
const [lon, lat] = camera.center;
|
|
139
|
-
const cx = mercatorX(lon) * n;
|
|
140
|
-
const cy = mercatorY(lat) * n;
|
|
141
|
-
// Across the antimeridian, the short way
|
|
142
|
-
const along = Math.abs(x + 0.5 - cx) % n;
|
|
143
|
-
const dx = Math.min(along, n - along);
|
|
144
|
-
const d = Math.hypot(dx, y + 0.5 - cy);
|
|
145
|
-
if (d <= 1.5)
|
|
146
|
-
return 'high';
|
|
147
|
-
return d >= 4 ? 'low' : 'auto';
|
|
148
|
-
};
|
|
149
|
-
/** The worker of a vector tile among `count`: always the same one */
|
|
150
|
-
export const workerIndex = (vz, vx, vy, count) => {
|
|
151
|
-
// Unsigned: every worker gets its share
|
|
152
|
-
return ((Math.imul(vx, 73856093) ^ Math.imul(vy, 19349663) ^ vz) >>> 0) % Math.max(1, count);
|
|
153
|
-
};
|
|
154
|
-
/** The worker-side key of a renderer: unique across the `VectorTiles` sharing a pool */
|
|
155
|
-
export const rendererKey = (namespace, n) => {
|
|
156
|
-
return `${namespace}:r${n}`;
|
|
157
|
-
};
|
|
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'] ?? '');
|
|
158
129
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* owner, so that one `VectorTiles` going away cancels its own requests only.
|
|
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
|
|
162
132
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const start = () => {
|
|
177
|
-
const worker = create(url);
|
|
178
|
-
worker.onmessage = ({ data }) => {
|
|
179
|
-
const p = this.pending.get(data.id);
|
|
180
|
-
if (!p)
|
|
181
|
-
return;
|
|
182
|
-
this.pending.delete(data.id);
|
|
183
|
-
if ('error' in data)
|
|
184
|
-
p.reject(new Error(data.error));
|
|
185
|
-
else
|
|
186
|
-
p.resolve(data);
|
|
187
|
-
};
|
|
188
|
-
// A worker that fails to load (a bundler serving it from elsewhere) or to read a message fails its requests
|
|
189
|
-
worker.onerror = (event) => {
|
|
190
|
-
event.preventDefault();
|
|
191
|
-
this.fail(worker, `the vector basemap worker ${url} failed${event.message ? `: ${event.message}` : ''}`);
|
|
192
|
-
};
|
|
193
|
-
worker.onmessageerror = () => this.fail(worker, `the vector basemap worker ${url} sent an unreadable message`);
|
|
194
|
-
const rules = headerRules();
|
|
195
|
-
if (rules.length > 0)
|
|
196
|
-
worker.postMessage({ type: 'headers', rules });
|
|
197
|
-
return worker;
|
|
198
|
-
};
|
|
199
|
-
this.workers = [start(), ...Array.from({ length: Math.max(1, count) - 1 }, start)];
|
|
200
|
-
this.unfollow = onHeaderRules((rules) => {
|
|
201
|
-
this.workers.forEach((w) => {
|
|
202
|
-
w.postMessage({ type: 'headers', rules });
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
/** A prefix for the renderer keys of one `VectorTiles` */
|
|
207
|
-
namespace() {
|
|
208
|
-
return `v${this.namespaces++}`;
|
|
209
|
-
}
|
|
210
|
-
/** Requests in progress */
|
|
211
|
-
get size() {
|
|
212
|
-
return this.pending.size;
|
|
213
|
-
}
|
|
214
|
-
/** Rejects the requests of a failed worker */
|
|
215
|
-
fail(worker, message) {
|
|
216
|
-
const error = new Error(`isard: ${message}. If a bundler moves the worker, give its URL in the \`basemapWorkerUrl\` option (see "With a bundler" in the guide)`);
|
|
217
|
-
this.pending.forEach((p, id) => {
|
|
218
|
-
if (p.worker !== worker)
|
|
219
|
-
return;
|
|
220
|
-
this.pending.delete(id);
|
|
221
|
-
p.reject(error);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/** The worker of a vector tile */
|
|
225
|
-
worker(vz, vx, vy) {
|
|
226
|
-
return this.workers[workerIndex(vz, vx, vy, this.workers.length)] ?? this.workers[0];
|
|
227
|
-
}
|
|
228
|
-
/** Sends a message to every worker (a renderer, or its removal) */
|
|
229
|
-
broadcast(message) {
|
|
230
|
-
if (this.terminated)
|
|
231
|
-
return;
|
|
232
|
-
// A renderer tells the workers how many they are: they share the decoded tiles' budget
|
|
233
|
-
const sent = message.type === 'init' ? { ...message, workers: this.workers.length } : message;
|
|
234
|
-
// A sprite image is copied to each worker (structured clone)
|
|
235
|
-
this.workers.forEach((w) => {
|
|
236
|
-
w.postMessage(sent);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
/** Asks a worker for a tile's image or labels, on behalf of `owner` */
|
|
240
|
-
request(worker, work, owner, signal) {
|
|
241
|
-
return new Promise((resolve, reject) => {
|
|
242
|
-
if (this.terminated)
|
|
243
|
-
return reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
|
|
244
|
-
if (signal?.aborted)
|
|
245
|
-
return reject(new DOMException('aborted', 'AbortError'));
|
|
246
|
-
const id = this.next++;
|
|
247
|
-
const abort = () => {
|
|
248
|
-
if (!this.pending.delete(id))
|
|
249
|
-
return;
|
|
250
|
-
worker.postMessage({ type: 'abort', id });
|
|
251
|
-
reject(new DOMException('aborted', 'AbortError'));
|
|
252
|
-
};
|
|
253
|
-
// The listener goes once the request is settled
|
|
254
|
-
const settle = () => signal?.removeEventListener('abort', abort);
|
|
255
|
-
this.pending.set(id, {
|
|
256
|
-
resolve: (r) => {
|
|
257
|
-
settle();
|
|
258
|
-
resolve(r);
|
|
259
|
-
},
|
|
260
|
-
reject: (e) => {
|
|
261
|
-
settle();
|
|
262
|
-
reject(e);
|
|
263
|
-
},
|
|
264
|
-
worker,
|
|
265
|
-
owner,
|
|
266
|
-
});
|
|
267
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
268
|
-
worker.postMessage({ ...work, id });
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
/** The altitudes of a PNG relief tile `z`/`x`/`y` at `url`, decoded by a worker (see `fetchHeights`) */
|
|
272
|
-
heights(url, encoding, floor, [z, x, y], signal, owner) {
|
|
273
|
-
const work = { type: 'heights', url, encoding, ...(floor === undefined ? {} : { floor }) };
|
|
274
|
-
return this.request(this.worker(z, x, y), work, owner, signal).then((r) => ('heights' in r ? r.heights : null));
|
|
275
|
-
}
|
|
276
|
-
/** Mercator tile `z`/`x`/`y` reprojected by a worker from the source images of its plan (see `resampler`) */
|
|
277
|
-
reproject(job, [z, x, y], signal, owner) {
|
|
278
|
-
// Blocks of 4 × 4 Mercator tiles go to the same worker: neighbours share source tiles, each worker keeps its share
|
|
279
|
-
const worker = this.worker(Math.max(0, z - 2), x >> 2, y >> 2);
|
|
280
|
-
const cached = Math.max(16, Math.ceil(SOURCE_CACHE / this.workers.length));
|
|
281
|
-
const work = { type: 'reproject', job, cached };
|
|
282
|
-
return this.request(worker, work, owner, signal).then((r) => ('image' in r ? r.image : null));
|
|
283
|
-
}
|
|
284
|
-
/** Cancels the requests of an owner (the workers stop them) */
|
|
285
|
-
cancel(owner) {
|
|
286
|
-
this.pending.forEach((p, id) => {
|
|
287
|
-
if (p.owner !== owner)
|
|
288
|
-
return;
|
|
289
|
-
this.pending.delete(id);
|
|
290
|
-
p.worker.postMessage({ type: 'abort', id });
|
|
291
|
-
p.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
/** Stops the workers; their requests fail */
|
|
295
|
-
terminate() {
|
|
296
|
-
if (this.terminated)
|
|
297
|
-
return;
|
|
298
|
-
this.terminated = true;
|
|
299
|
-
this.unfollow();
|
|
300
|
-
this.workers.forEach((w) => {
|
|
301
|
-
w.terminate();
|
|
302
|
-
});
|
|
303
|
-
const pending = [...this.pending.values()];
|
|
304
|
-
this.pending.clear();
|
|
305
|
-
pending.forEach((p) => {
|
|
306
|
-
p.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
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
|
+
};
|
|
310
146
|
/** A tile's label candidates as features, by style layer */
|
|
311
147
|
export const tileLabels = (candidates) => {
|
|
312
148
|
const byLayer = new Map();
|
|
@@ -425,10 +261,10 @@ export class VectorTiles {
|
|
|
425
261
|
return Promise.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
|
|
426
262
|
return this.pool.request(this.pool.worker(vz, vx, vy), work, this, signal);
|
|
427
263
|
}
|
|
428
|
-
/** Sends a renderer of these layers to every worker; returns its key */
|
|
429
|
-
async renderer(select, extra) {
|
|
264
|
+
/** Sends a renderer of these layers to every worker (again under `again`, restyled); returns its key */
|
|
265
|
+
async renderer(select, extra, again) {
|
|
430
266
|
const { style, tiles, maxzoom, rasters } = await this.ready;
|
|
431
|
-
const key = rendererKey(this.namespace, this.keys++);
|
|
267
|
+
const key = again ?? rendererKey(this.namespace, this.keys++);
|
|
432
268
|
const layers = [...style.layers.filter(select), ...extra];
|
|
433
269
|
const patterns = layers.some((l) => l.paint?.['fill-pattern'] !== undefined);
|
|
434
270
|
// The images are downloaded once, copied to each worker
|
|
@@ -436,8 +272,9 @@ export class VectorTiles {
|
|
|
436
272
|
if (this.destroyed)
|
|
437
273
|
throw new DOMException('isard: vector tiles destroyed', 'AbortError');
|
|
438
274
|
this.pool.broadcast({ type: 'init', key, tiles, maxzoom, layers, rasters, ...(sprites.length ? { sprites } : {}) });
|
|
439
|
-
|
|
440
|
-
|
|
275
|
+
if (!again)
|
|
276
|
+
this.renderers.push(key);
|
|
277
|
+
return { key, maxzoom, layers };
|
|
441
278
|
}
|
|
442
279
|
/**
|
|
443
280
|
* A raster layer drawing the style's fills, lines and circles (or those `layers` keeps, then `extraLayers`, of the
|
|
@@ -472,6 +309,431 @@ export class VectorTiles {
|
|
|
472
309
|
this.ground ??= layer;
|
|
473
310
|
return layer;
|
|
474
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* An extrusion layer of the polygons of the style's fill layers that `layers` keeps (by default its buildings: the
|
|
314
|
+
* fill or fill-extrusion layers of PLAN IGN's `bati_surf` or OpenMapTiles' `building`), raised by the style
|
|
315
|
+
* given (`height`, `base`, `color`, `ramp`…; the properties of the tiles' features). Its tiles follow the view: at
|
|
316
|
+
* the source's deepest level, the `maxTiles` nearest to the center, from `minZoom` on (15 by default); the tiles
|
|
317
|
+
* leaving the view are dropped.
|
|
318
|
+
*/
|
|
319
|
+
extrusions(options = {}) {
|
|
320
|
+
const { layers = isBuildings, minZoom = 15, maxTiles = 36, ...style } = options;
|
|
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;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The wind through the streets around the view, as particles at street level (`height`, default 3 metres) between
|
|
368
|
+
* the style's buildings (`layers`, default its `building` and `bati_surf` layers): their footprints laid on a grid of
|
|
369
|
+
* `cell` metres (default 4) over a square of `size` metres (default 1200) around the view's center, the flow of the
|
|
370
|
+
* air round them a potential flow (it goes round the blocks and faster through the narrow streets; no wake behind
|
|
371
|
+
* them), computed in a worker by the grid module from street level (`minZoom`, default 15) and again when the view
|
|
372
|
+
* leaves the square's middle. `wind` is the wind far away (east and north, m/s, default a light breeze from the
|
|
373
|
+
* south-west); `setWind` changes it at once (the flow is linear in it: nothing computed again), from a model's hour
|
|
374
|
+
* (`windAt`) for instance; `computed` resolves once the first square's flow is there. The other options are the flow
|
|
375
|
+
* layer's (`addFlow`)
|
|
376
|
+
*/
|
|
377
|
+
streetWind(options = {}) {
|
|
378
|
+
const { size = 1200, cell = 4, iterations = 300, layers = isBuildings, minZoom = 15, ...flow } = options;
|
|
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
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Trees on the style's woods, forests and grass (or the fill layers `layers` keeps) as 3D models, from street level
|
|
489
|
+
* (`minZoom`, default 15): the workers scatter them over the polygons of the vector tiles around the view, `spacing`
|
|
490
|
+
* metres apart (by style layer: 9 in woods, 25 on grass, by default) on a jittered grid, the same trees whenever a
|
|
491
|
+
* tile comes back; the engine draws them all in one instanced draw, on the relief. A low-poly tree by default (its
|
|
492
|
+
* `colors`), or your own glTF (`url`, `data`, about 10 metres high, standing on its origin). Dense around the
|
|
493
|
+
* view's center, fewer and fewer away from it (as the camera moves too), at most `maxInstances` drawn (default
|
|
494
|
+
* 25000); at most `maxTiles` tiles (default 9) and `maxTrees` trees in each (default 30000). The models load the
|
|
495
|
+
* engine with 3D Tiles.
|
|
496
|
+
*/
|
|
497
|
+
trees(options = {}) {
|
|
498
|
+
const { layers = isWoodland, spacing = treeSpacing, minZoom = 15, maxTiles = 9, maxTrees = 30000, maxInstances = 25000, } = options;
|
|
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;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* A layer drawing the style's fills, lines and buildings (or those `layers` keeps; backgrounds as fills, the
|
|
582
|
+
* `fill-extrusion` layers raised into buildings by their height and base height) as geometry instead of images: the
|
|
583
|
+
* workers cut the vector tiles in view into polygons and lines, which the engine draws on the globe and the relief,
|
|
584
|
+
* in the style's order, each style layer over its zoom range. Sharp at any zoom
|
|
585
|
+
* and pitch, picked by feature (`pick` gives its properties and style layer), restyled without cutting the tiles
|
|
586
|
+
* again (`setPaintProperty`, `setLayoutProperty`). Its tiles follow the view, while it moves too, coarser far away in a
|
|
587
|
+
* tilted view, and those leaving it stay until the ones replacing them are there; past the source's deepest level,
|
|
588
|
+
* cut again from its tiles down to level 18 (overzoom: their filters and styles those of their level). The fills' outlines are drawn one
|
|
589
|
+
* pixel wide, the lines' ends as their `line-cap` says (their joins round), the colors follow the camera's zoom. The
|
|
590
|
+
* layers drawn with a pattern and the circles are left out (the raster path draws them), as are `line-offset`,
|
|
591
|
+
* `line-blur` and the translations (`fill-translate`, `line-translate`); `raster` stays the lighter path for heavy
|
|
592
|
+
* styles and small devices. At night (`setSun`) the line layers `streets` keeps glow (by default those whose id names
|
|
593
|
+
* a road, not its casing, a railway, a path or a tunnel).
|
|
594
|
+
*/
|
|
595
|
+
vectors(options = {}) {
|
|
596
|
+
const { layers = () => true, streets = isStreet, before } = options;
|
|
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;
|
|
736
|
+
}
|
|
475
737
|
/**
|
|
476
738
|
* A symbol layer with the labels of the style's symbol layers (or those `layers` keeps), updated as the view
|
|
477
739
|
* moves: the labels of the vector tiles in view, the higher style layers placed first. With a raster layer of
|
|
@@ -565,27 +827,9 @@ export class VectorTiles {
|
|
|
565
827
|
}
|
|
566
828
|
else {
|
|
567
829
|
const z = Math.min(zoom, maxzoom);
|
|
568
|
-
const
|
|
569
|
-
const [west, south, east, north] = this.map.getBounds();
|
|
570
|
-
const tx = (lon) => Math.floor(mercatorX(lon) * n);
|
|
571
|
-
const ty = (lat) => Math.floor(mercatorY(lat) * n);
|
|
572
|
-
const [cx, cy] = [mercatorX(camera.center[0]) * n, ty(camera.center[1])];
|
|
573
|
-
// The tiles of the bounds, within a window around the center: a tilted view's bounds reach far, and only
|
|
574
|
-
// the nearest `maxTiles` are kept anyway
|
|
575
|
-
const reach = Math.ceil(Math.sqrt(maxTiles)) + 2;
|
|
576
|
-
const [x0, x1] = [
|
|
577
|
-
Math.max(0, tx(west), Math.floor(cx) - reach),
|
|
578
|
-
Math.min(n - 1, tx(east), Math.floor(cx) + reach),
|
|
579
|
-
];
|
|
580
|
-
const [y0, y1] = [
|
|
581
|
-
Math.max(0, ty(north), Math.floor(cy) - reach),
|
|
582
|
-
Math.min(n - 1, ty(south), Math.floor(cy) + reach),
|
|
583
|
-
];
|
|
584
|
-
const wanted = Array.from({ length: y1 - y0 + 1 }, (_, j) => y0 + j).flatMap((y) => Array.from({ length: x1 - x0 + 1 }, (_, i) => [x0 + i, y]));
|
|
585
|
-
// Nearest to the center first, at most `maxTiles` (tilted views reach far)
|
|
586
|
-
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));
|
|
830
|
+
const wanted = tilesAround(this.map, camera, z, maxTiles);
|
|
587
831
|
// Past the source's deepest level, its tiles' labels at the zoom of the view (the layers of the deeper zooms)
|
|
588
|
-
keys.push(...wanted.
|
|
832
|
+
keys.push(...wanted.map(([x, y]) => `${z}/${x}/${y}${zoom > z ? `@${zoom}` : ''}`));
|
|
589
833
|
}
|
|
590
834
|
// The same tiles as the labels shown: nothing to do
|
|
591
835
|
const set = keys.join(' ');
|
|
@@ -653,6 +897,32 @@ export class VectorTiles {
|
|
|
653
897
|
refresh();
|
|
654
898
|
return symbol;
|
|
655
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* 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 `held` aborted when destroyed
|
|
903
|
+
*/
|
|
904
|
+
followView(update, held, moving) {
|
|
905
|
+
const refresh = () => {
|
|
906
|
+
update().catch(() => undefined);
|
|
907
|
+
};
|
|
908
|
+
const pace = { timer: 0 };
|
|
909
|
+
const follow = () => {
|
|
910
|
+
if (pace.timer)
|
|
911
|
+
return;
|
|
912
|
+
pace.timer = window.setTimeout(() => {
|
|
913
|
+
pace.timer = 0;
|
|
914
|
+
Promise.resolve(moving()).catch(() => undefined);
|
|
915
|
+
}, VECTOR_MOVE_REFRESH_MS);
|
|
916
|
+
};
|
|
917
|
+
this.cleanup.push(this.map.on('move', follow), this.map.on('moveend', refresh), () => {
|
|
918
|
+
clearTimeout(pace.timer);
|
|
919
|
+
held.forEach((controller) => {
|
|
920
|
+
controller.abort();
|
|
921
|
+
});
|
|
922
|
+
held.clear();
|
|
923
|
+
});
|
|
924
|
+
refresh();
|
|
925
|
+
}
|
|
656
926
|
/** Stops its workers' work and removes its raster and label layers from the map (those still there) */
|
|
657
927
|
destroy() {
|
|
658
928
|
if (this.destroyed)
|
|
@@ -690,96 +960,34 @@ export class VectorTiles {
|
|
|
690
960
|
this.renderers.length = 0;
|
|
691
961
|
}
|
|
692
962
|
}
|
|
693
|
-
/** Colors of the layers of `basicStyle`, in turn */
|
|
694
|
-
const BASIC_COLORS = ['#2f6fd1', '#d1495b', '#2a9d8f', '#e9a03b', '#7b5ea7', '#5c7a29', '#c8553d', '#3d8eb9'];
|
|
695
963
|
/**
|
|
696
|
-
* A
|
|
697
|
-
*
|
|
698
|
-
* layer, the `name` of points as labels. `source` is the style's vector source (`url`: TileJSON or `pmtiles://…`, or
|
|
699
|
-
* `tiles`)
|
|
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
|
|
700
966
|
*/
|
|
701
|
-
export const
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
id: `${name}-circle`,
|
|
708
|
-
type: 'circle',
|
|
709
|
-
...base,
|
|
710
|
-
paint: { 'circle-color': color, 'circle-radius': 3.5, 'circle-stroke-color': '#fff', 'circle-stroke-width': 1 },
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
|
-
layers.forEach((name) => {
|
|
714
|
-
out.push({
|
|
715
|
-
id: `${name}-label`,
|
|
716
|
-
type: 'symbol',
|
|
717
|
-
source: 'data',
|
|
718
|
-
'source-layer': name,
|
|
719
|
-
filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'name']],
|
|
720
|
-
layout: { 'text-field': ['get', 'name'], 'text-size': 12, 'text-offset': [0, 0.9], 'text-anchor': 'top' },
|
|
721
|
-
paint: { 'text-color': '#1d2433', 'text-halo-color': '#fff', 'text-halo-width': 1.5 },
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
return { version: 8, sources: { data: { type: 'vector', ...source } }, layers: out };
|
|
725
|
-
};
|
|
726
|
-
/** HTML of a file reduced to its text, escaped: credits from an archive's metadata cannot run in the page */
|
|
727
|
-
const plainText = (html) => {
|
|
728
|
-
return html
|
|
729
|
-
.replace(/<[^>]*>/g, '')
|
|
730
|
-
.replace(/&(?!(amp|lt|gt|quot|copy|#\d+);)/g, '&')
|
|
731
|
-
.replace(/</g, '<')
|
|
732
|
-
.replace(/>/g, '>');
|
|
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];
|
|
733
973
|
};
|
|
734
|
-
/**
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const attribution = options.attribution ?? (typeof credit === 'string' && credit ? plainText(credit) : undefined);
|
|
745
|
-
const kind = options.kind ?? (header.tileType === 'mvt' ? 'vector' : 'raster');
|
|
746
|
-
const credits = attribution ? { attribution } : {};
|
|
747
|
-
if (kind === 'raster') {
|
|
748
|
-
return map.addRaster({
|
|
749
|
-
url: `pmtiles://${archiveUrl}`,
|
|
750
|
-
minZoom: header.minZoom,
|
|
751
|
-
maxZoom: Math.max(header.maxZoom, 19),
|
|
752
|
-
...(options.opacity === undefined ? {} : { opacity: options.opacity }),
|
|
753
|
-
...(options.size === undefined ? {} : { size: options.size }),
|
|
754
|
-
...(options.before ? { before: options.before } : {}),
|
|
755
|
-
...credits,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
const layers = metadata.vector_layers;
|
|
759
|
-
const names = Array.isArray(layers)
|
|
760
|
-
? layers.map((l) => (isRecord(l) ? l.id : undefined)).filter((id) => typeof id === 'string')
|
|
761
|
-
: [];
|
|
762
|
-
const style = options.style ?? basicStyle({ url: `pmtiles://${archiveUrl}` }, names);
|
|
763
|
-
const tiles = map.vectorTiles({ style, ...credits });
|
|
764
|
-
tiles.raster({
|
|
765
|
-
...(options.opacity === undefined ? {} : { opacity: options.opacity }),
|
|
766
|
-
...(options.before ? { before: options.before } : {}),
|
|
767
|
-
});
|
|
768
|
-
tiles.labels();
|
|
769
|
-
return tiles;
|
|
770
|
-
};
|
|
771
|
-
/** A raster loader reading the tiles of a `pmtiles://` archive URL (images of any size, drawn at `size`); undefined
|
|
772
|
-
* for another URL */
|
|
773
|
-
export const pmtilesRasterLoader = (url, size) => {
|
|
774
|
-
const archiveUrl = pmtilesUrl(url);
|
|
775
|
-
if (!archiveUrl)
|
|
776
|
-
return undefined;
|
|
777
|
-
const archive = pmtiles(archiveUrl);
|
|
778
|
-
return async (z, x, y, signal) => {
|
|
779
|
-
const bytes = await archive.tile(z, x, y, signal);
|
|
780
|
-
if (!bytes)
|
|
781
|
-
return null;
|
|
782
|
-
return createImageBitmap(new Blob([bytes]), { resizeWidth: size, resizeHeight: size });
|
|
783
|
-
};
|
|
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';
|
|
784
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;
|
|
785
993
|
//# sourceMappingURL=vectortiles.js.map
|