isard 1.1.0 → 1.3.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.
Files changed (254) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +44 -39
  3. package/dist/basemap/basemaps.d.ts +1 -1
  4. package/dist/basemap/basemaps.js +2 -2
  5. package/dist/basemap/basemaps.js.map +1 -1
  6. package/dist/basemap/grid.d.ts +5 -5
  7. package/dist/basemap/grid.js +59 -65
  8. package/dist/basemap/grid.js.map +1 -1
  9. package/dist/basemap/mvt.d.ts +1 -1
  10. package/dist/basemap/mvt.js +6 -6
  11. package/dist/basemap/mvt.js.map +1 -1
  12. package/dist/basemap/pmtiles.d.ts +10 -10
  13. package/dist/basemap/pmtiles.js +39 -31
  14. package/dist/basemap/pmtiles.js.map +1 -1
  15. package/dist/basemap/pmtileslayer.d.ts +33 -0
  16. package/dist/basemap/pmtileslayer.js +97 -0
  17. package/dist/basemap/pmtileslayer.js.map +1 -0
  18. package/dist/basemap/resample.d.ts +2 -2
  19. package/dist/basemap/resample.js +16 -14
  20. package/dist/basemap/resample.js.map +1 -1
  21. package/dist/basemap/terrain.d.ts +3 -3
  22. package/dist/basemap/terrain.js +6 -6
  23. package/dist/basemap/terrain.js.map +1 -1
  24. package/dist/basemap/trees.d.ts +26 -0
  25. package/dist/basemap/trees.js +141 -0
  26. package/dist/basemap/trees.js.map +1 -0
  27. package/dist/basemap/vectorstyle.d.ts +22 -10
  28. package/dist/basemap/vectorstyle.js +163 -110
  29. package/dist/basemap/vectorstyle.js.map +1 -1
  30. package/dist/basemap/vectortiles.d.ts +70 -47
  31. package/dist/basemap/vectortiles.js +480 -358
  32. package/dist/basemap/vectortiles.js.map +1 -1
  33. package/dist/basemap/wmts.d.ts +3 -3
  34. package/dist/basemap/wmts.js +16 -11
  35. package/dist/basemap/wmts.js.map +1 -1
  36. package/dist/basemap/workerpool.d.ts +17 -0
  37. package/dist/basemap/workerpool.js +194 -0
  38. package/dist/basemap/workerpool.js.map +1 -0
  39. package/dist/basemap.worker.js +2 -2
  40. package/dist/basemap.worker.js.map +21 -13
  41. package/dist/bind/layers.d.ts +13 -4
  42. package/dist/bind/layers.js +64 -40
  43. package/dist/bind/layers.js.map +1 -1
  44. package/dist/data/crs.d.ts +10 -10
  45. package/dist/data/crs.js +38 -34
  46. package/dist/data/crs.js.map +1 -1
  47. package/dist/data/gpx.d.ts +1 -1
  48. package/dist/data/gpx.js +20 -20
  49. package/dist/data/gpx.js.map +1 -1
  50. package/dist/data/kml.d.ts +1 -1
  51. package/dist/data/kml.js +42 -39
  52. package/dist/data/kml.js.map +1 -1
  53. package/dist/data/topojson.d.ts +1 -1
  54. package/dist/data/topojson.js +15 -10
  55. package/dist/data/topojson.js.map +1 -1
  56. package/dist/data/xml.d.ts +5 -5
  57. package/dist/data/xml.js +19 -18
  58. package/dist/data/xml.js.map +1 -1
  59. package/dist/decoder.worker.js +4 -4
  60. package/dist/decoder.worker.js.map +6 -6
  61. package/dist/engine/decoders.d.ts +2 -2
  62. package/dist/engine/decoders.js +28 -25
  63. package/dist/engine/decoders.js.map +1 -1
  64. package/dist/engine/loader.js +13 -14
  65. package/dist/engine/loader.js.map +1 -1
  66. package/dist/engine/requests.d.ts +1 -1
  67. package/dist/engine/requests.js +19 -16
  68. package/dist/engine/requests.js.map +1 -1
  69. package/dist/engine-loader.d.ts +13 -9
  70. package/dist/engine-loader.js +38 -28
  71. package/dist/engine-loader.js.map +1 -1
  72. package/dist/geoplateforme.d.ts +26 -10
  73. package/dist/geoplateforme.js +45 -20
  74. package/dist/geoplateforme.js.map +1 -1
  75. package/dist/index.d.ts +12 -4
  76. package/dist/index.js +9 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/isard_bg.wasm +0 -0
  79. package/dist/isard_core_bg.wasm +0 -0
  80. package/dist/isard_points_bg.wasm +0 -0
  81. package/dist/isard_tess_bg.wasm +0 -0
  82. package/dist/layers/extrusions.d.ts +1 -1
  83. package/dist/layers/extrusions.js +10 -5
  84. package/dist/layers/extrusions.js.map +1 -1
  85. package/dist/layers/features.d.ts +10 -10
  86. package/dist/layers/features.js +41 -42
  87. package/dist/layers/features.js.map +1 -1
  88. package/dist/layers/glyphs.d.ts +3 -3
  89. package/dist/layers/glyphs.js +34 -40
  90. package/dist/layers/glyphs.js.map +1 -1
  91. package/dist/layers/icons.d.ts +8 -8
  92. package/dist/layers/icons.js +76 -68
  93. package/dist/layers/icons.js.map +1 -1
  94. package/dist/layers/layers.d.ts +1 -1
  95. package/dist/layers/layers.js +42 -29
  96. package/dist/layers/layers.js.map +1 -1
  97. package/dist/layers/panoramas.d.ts +2 -2
  98. package/dist/layers/panoramas.js +52 -41
  99. package/dist/layers/panoramas.js.map +1 -1
  100. package/dist/layers/pointclouds.d.ts +3 -3
  101. package/dist/layers/pointclouds.js +25 -33
  102. package/dist/layers/pointclouds.js.map +1 -1
  103. package/dist/layers/symbols.d.ts +1 -1
  104. package/dist/layers/symbols.js +68 -71
  105. package/dist/layers/symbols.js.map +1 -1
  106. package/dist/layers/vectors.d.ts +60 -0
  107. package/dist/layers/vectors.js +130 -0
  108. package/dist/layers/vectors.js.map +1 -0
  109. package/dist/map/basemap-layers.d.ts +26 -0
  110. package/dist/map/basemap-layers.js +61 -0
  111. package/dist/map/basemap-layers.js.map +1 -0
  112. package/dist/map/canvas.d.ts +17 -0
  113. package/dist/map/canvas.js +56 -0
  114. package/dist/map/canvas.js.map +1 -0
  115. package/dist/map/checks.d.ts +11 -0
  116. package/dist/map/checks.js +39 -0
  117. package/dist/map/checks.js.map +1 -0
  118. package/dist/map/credits.d.ts +17 -0
  119. package/dist/map/credits.js +34 -0
  120. package/dist/map/credits.js.map +1 -0
  121. package/dist/map/engine-settings.d.ts +15 -0
  122. package/dist/map/engine-settings.js +45 -0
  123. package/dist/map/engine-settings.js.map +1 -0
  124. package/dist/map/engines.d.ts +41 -0
  125. package/dist/map/engines.js +77 -0
  126. package/dist/map/engines.js.map +1 -0
  127. package/dist/map/events.d.ts +14 -0
  128. package/dist/map/events.js +46 -0
  129. package/dist/map/events.js.map +1 -0
  130. package/dist/map/frame-loop.d.ts +64 -0
  131. package/dist/map/frame-loop.js +145 -0
  132. package/dist/map/frame-loop.js.map +1 -0
  133. package/dist/map/hover-pick.d.ts +30 -0
  134. package/dist/map/hover-pick.js +56 -0
  135. package/dist/map/hover-pick.js.map +1 -0
  136. package/dist/map/layer-spec.d.ts +42 -0
  137. package/dist/map/layer-spec.js +34 -0
  138. package/dist/map/layer-spec.js.map +1 -0
  139. package/dist/map/layer-stack.d.ts +36 -0
  140. package/dist/map/layer-stack.js +101 -0
  141. package/dist/map/layer-stack.js.map +1 -0
  142. package/dist/map/overlay-host.d.ts +36 -0
  143. package/dist/map/overlay-host.js +101 -0
  144. package/dist/map/overlay-host.js.map +1 -0
  145. package/dist/map/panorama-host.d.ts +5 -0
  146. package/dist/map/panorama-host.js +32 -0
  147. package/dist/map/panorama-host.js.map +1 -0
  148. package/dist/map/raster-source.d.ts +11 -0
  149. package/dist/map/raster-source.js +26 -0
  150. package/dist/map/raster-source.js.map +1 -0
  151. package/dist/map/symbol-atlases.d.ts +26 -0
  152. package/dist/map/symbol-atlases.js +59 -0
  153. package/dist/map/symbol-atlases.js.map +1 -0
  154. package/dist/map/viewshed.d.ts +5 -0
  155. package/dist/map/viewshed.js +28 -0
  156. package/dist/map/viewshed.js.map +1 -0
  157. package/dist/map/workers.d.ts +46 -0
  158. package/dist/map/workers.js +73 -0
  159. package/dist/map/workers.js.map +1 -0
  160. package/dist/map.d.ts +54 -126
  161. package/dist/map.js +227 -698
  162. package/dist/map.js.map +1 -1
  163. package/dist/react/Control.d.ts +11 -0
  164. package/dist/react/Control.js +18 -0
  165. package/dist/react/Control.js.map +1 -0
  166. package/dist/react/CustomLayer.d.ts +11 -0
  167. package/dist/react/CustomLayer.js +30 -0
  168. package/dist/react/CustomLayer.js.map +1 -0
  169. package/dist/react/IsardMap.d.ts +43 -0
  170. package/dist/react/IsardMap.js +147 -0
  171. package/dist/react/IsardMap.js.map +1 -0
  172. package/dist/react/Layer.d.ts +42 -0
  173. package/dist/react/Layer.js +74 -0
  174. package/dist/react/Layer.js.map +1 -0
  175. package/dist/react/Marker.d.ts +24 -0
  176. package/dist/react/Marker.js +53 -0
  177. package/dist/react/Marker.js.map +1 -0
  178. package/dist/react/Panel.d.ts +11 -0
  179. package/dist/react/Panel.js +37 -0
  180. package/dist/react/Panel.js.map +1 -0
  181. package/dist/react/Popup.d.ts +18 -0
  182. package/dist/react/Popup.js +60 -0
  183. package/dist/react/Popup.js.map +1 -0
  184. package/dist/react/Swipe.d.ts +16 -0
  185. package/dist/react/Swipe.js +52 -0
  186. package/dist/react/Swipe.js.map +1 -0
  187. package/dist/react/VectorGeometry.d.ts +35 -0
  188. package/dist/react/VectorGeometry.js +82 -0
  189. package/dist/react/VectorGeometry.js.map +1 -0
  190. package/dist/react/Viewshed.d.ts +5 -0
  191. package/dist/react/Viewshed.js +28 -0
  192. package/dist/react/Viewshed.js.map +1 -0
  193. package/dist/react/context.d.ts +33 -0
  194. package/dist/react/context.js +20 -0
  195. package/dist/react/context.js.map +1 -0
  196. package/dist/react/index.d.ts +16 -108
  197. package/dist/react/index.js +11 -397
  198. package/dist/react/index.js.map +1 -1
  199. package/dist/settings.d.ts +5 -9
  200. package/dist/settings.js +17 -15
  201. package/dist/settings.js.map +1 -1
  202. package/dist/types.d.ts +27 -4
  203. package/dist/types.js.map +1 -1
  204. package/dist/ui/controls.js +6 -4
  205. package/dist/ui/controls.js.map +1 -1
  206. package/dist/ui/interaction.d.ts +2 -2
  207. package/dist/ui/interaction.js +8 -7
  208. package/dist/ui/interaction.js.map +1 -1
  209. package/dist/ui/minimap.d.ts +25 -0
  210. package/dist/ui/minimap.js +90 -0
  211. package/dist/ui/minimap.js.map +1 -0
  212. package/dist/ui/overlay.js +18 -20
  213. package/dist/ui/overlay.js.map +1 -1
  214. package/dist/ui/scale.d.ts +19 -0
  215. package/dist/ui/scale.js +44 -0
  216. package/dist/ui/scale.js.map +1 -0
  217. package/dist/ui/search.d.ts +25 -0
  218. package/dist/ui/search.js +119 -0
  219. package/dist/ui/search.js.map +1 -0
  220. package/dist/ui/style.d.ts +1 -1
  221. package/dist/ui/style.js +19 -2
  222. package/dist/ui/style.js.map +1 -1
  223. package/dist/util/animation.d.ts +3 -3
  224. package/dist/util/animation.js +6 -6
  225. package/dist/util/animation.js.map +1 -1
  226. package/dist/util/color.d.ts +1 -1
  227. package/dist/util/color.js +14 -16
  228. package/dist/util/color.js.map +1 -1
  229. package/dist/util/geo.d.ts +6 -6
  230. package/dist/util/geo.js +12 -12
  231. package/dist/util/geo.js.map +1 -1
  232. package/dist/util/guards.d.ts +7 -0
  233. package/dist/util/guards.js +8 -0
  234. package/dist/util/guards.js.map +1 -0
  235. package/dist/util/lru.js.map +1 -1
  236. package/dist/util/sun.d.ts +11 -0
  237. package/dist/util/sun.js +26 -0
  238. package/dist/util/sun.js.map +1 -0
  239. package/dist/wasm/isard.d.ts +250 -7
  240. package/dist/wasm/isard.js +375 -8
  241. package/dist/wasm/isard_core.d.ts +226 -7
  242. package/dist/wasm/isard_core.js +351 -8
  243. package/dist/wasm/isard_tess.d.ts +109 -0
  244. package/dist/wasm/isard_tess.js +398 -0
  245. package/dist/workers/basemap/geometry.d.ts +72 -0
  246. package/dist/workers/basemap/labels.d.ts +8 -0
  247. package/dist/workers/basemap/raster.d.ts +10 -0
  248. package/dist/workers/basemap/tiles.d.ts +65 -0
  249. package/dist/workers/basemap/trees.d.ts +19 -0
  250. package/dist/workers/basemap.worker.d.ts +126 -22
  251. package/dist/workers/basemap.worker.js +76 -544
  252. package/dist/workers/basemap.worker.js.map +1 -1
  253. package/package.json +5 -4
  254. package/vite.js +10 -2
@@ -8,16 +8,17 @@
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 { headerRules, onHeaderRules, withHeaders } from "../engine/requests.js";
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
14
  import { mercatorX, mercatorY } from "../util/geo.js";
15
+ import { isRecord } from "../util/guards.js";
16
16
  import { Lru } from "../util/lru.js";
17
17
  import { OPENFREEMAP_ATTRIBUTION } from "./basemaps.js";
18
18
  import { pmtiles, pmtilesUrl } from "./pmtiles.js";
19
- import { SOURCE_CACHE } from "./resample.js";
20
- import { 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";
21
22
  /** OpenFreeMap's styles: OpenStreetMap data in the OpenMapTiles schema, worldwide, free and without a key */
22
23
  export const OPENFREEMAP_STYLES = {
23
24
  positron: 'https://tiles.openfreemap.org/styles/positron',
@@ -38,43 +39,75 @@ export const VECTOR_BASEMAPS = {
38
39
  'ign-plan-grey-vector': PLAN_STYLES.grey,
39
40
  };
40
41
  /** The credits of a known style: OpenFreeMap's, PLAN.IGN's */
41
- export function styleAttribution(style) {
42
+ export const styleAttribution = (style) => {
42
43
  if (typeof style !== 'string')
43
44
  return undefined;
44
- if (Object.values(OPENFREEMAP_STYLES).includes(style))
45
+ if (Object.values(OPENFREEMAP_STYLES).some((s) => s === style))
45
46
  return OPENFREEMAP_ATTRIBUTION;
46
- if (Object.values(PLAN_STYLES).includes(style))
47
+ if (Object.values(PLAN_STYLES).some((s) => s === style))
47
48
  return IGN_ATTRIBUTION;
48
49
  return undefined;
49
- }
50
+ };
50
51
  /** Text or icon size of a style (a number, legacy stops, or the stops of a zoom expression made by the worker) as a
51
52
  * size for the symbol layer */
52
- export function size(value, fallback = 16) {
53
+ export const size = (value, fallback = 16) => {
54
+ const stop = (s) => {
55
+ const [z, v] = Array.isArray(s) ? s : [];
56
+ return [Number(z), Number(v)];
57
+ };
53
58
  if (typeof value === 'number')
54
59
  return value;
55
- if (Array.isArray(value) && value.length > 0 && value.every(Array.isArray)) {
56
- return value.map((s) => [Number(s[0]), Number(s[1])]);
57
- }
58
- if (value && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.stops)) {
59
- return value.stops.map((s) => {
60
- const [z, v] = s;
61
- return [Number(z), Number(v)];
62
- });
63
- }
60
+ if (Array.isArray(value) && value.length > 0 && value.every(Array.isArray))
61
+ return value.map(stop);
62
+ if (isRecord(value) && Array.isArray(value.stops))
63
+ return value.stops.map(stop);
64
64
  return fallback;
65
- }
65
+ };
66
+ /** The anchors of the symbol layer */
67
+ const ANCHORS = new Set([
68
+ 'center',
69
+ 'top',
70
+ 'bottom',
71
+ 'left',
72
+ 'right',
73
+ 'top-left',
74
+ 'top-right',
75
+ 'bottom-left',
76
+ 'bottom-right',
77
+ ]);
78
+ const isAnchor = (value) => value !== undefined && ANCHORS.has(value);
79
+ /** A style's anchor (`text-anchor`, `icon-anchor`) for the symbol layer: `center` if it is none */
80
+ const anchor = (value) => (isAnchor(value) ? value : 'center');
66
81
  /** Fetches a JSON document (a style, a TileJSON) */
67
- async function json(url) {
82
+ const json = async (url) => {
68
83
  const response = await fetch(url, withHeaders(url));
69
84
  if (!response.ok)
70
85
  throw new Error(`${url}: ${response.status}`);
71
- return (await response.json());
72
- }
86
+ return response.json();
87
+ };
88
+ /** A style fetched: a MapLibre style, or an error */
89
+ const fetchStyle = async (url) => {
90
+ const style = await json(url);
91
+ if (!isStyle(style))
92
+ throw new Error(`${url}: not a MapLibre style`);
93
+ return style;
94
+ };
95
+ const isTileJson = (value) => isRecord(value) &&
96
+ Array.isArray(value.tiles) &&
97
+ value.tiles.every((t) => typeof t === 'string') &&
98
+ (value.maxzoom === undefined || typeof value.maxzoom === 'number');
99
+ /** A TileJSON fetched, or an error */
100
+ const fetchTileJson = async (url) => {
101
+ const tilejson = await json(url);
102
+ if (!isTileJson(tilejson))
103
+ throw new Error(`${url}: not a TileJSON`);
104
+ return tilejson;
105
+ };
73
106
  /**
74
107
  * A style source's tiles and deepest level: a `pmtiles://` archive (its `url`, or a `tiles` template) read for its
75
108
  * header, its tiles then named `pmtiles://<absolute archive URL>/{z}/{x}/{y}` for the workers; other sources as they are
76
109
  */
77
- async function resolveSource(source) {
110
+ const resolveSource = async (source) => {
78
111
  const given = source.url ?? source.tiles?.[0] ?? '';
79
112
  const archive = pmtilesUrl(given.replace(/\/\{z\}\/\{x\}\/\{y\}$/, ''));
80
113
  if (!archive)
@@ -84,195 +117,33 @@ async function resolveSource(source) {
84
117
  };
85
118
  const header = await pmtiles(archive).header();
86
119
  return { tiles: [`pmtiles://${archive}/{z}/{x}/{y}`], maxzoom: source.maxzoom ?? header.maxZoom };
87
- }
120
+ };
88
121
  /** Pause before the labels of tiles that failed are asked for again, when no move brings an update (ms) */
89
122
  const RETRY_LABELS_MS = 3000;
90
- /** Default size of a basemap worker pool: cores - 1, 1 to 4 */
91
- export function defaultWorkerCount() {
92
- return Math.min(4, Math.max(1, (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 2 : 2) - 1));
93
- }
94
- /** Default URL of the basemap worker: in the page's assets folder (`isard/vite`), else next to the module */
95
- export function defaultWorkerUrl(assetsUrl) {
96
- return assetUrl('basemap.worker.js', undefined, assetsUrl);
97
- }
98
- /** Fetch priority of a raster tile drawn by the workers: 'high' near the center of the view, at the current zoom or
99
- * coarser; 'low' far from it or finer (prefetch); 'auto' between */
100
- export function tilePriority(z, x, y, camera) {
101
- // A raster tile of level z covers the map at zoom z - 1
102
- if (z - 1 > camera.zoom + 1)
103
- return 'low';
104
- const n = 2 ** z;
105
- const [lon, lat] = camera.center;
106
- const cx = mercatorX(lon) * n;
107
- const cy = mercatorY(lat) * n;
108
- // Across the antimeridian, the short way
109
- let dx = Math.abs(x + 0.5 - cx) % n;
110
- dx = Math.min(dx, n - dx);
111
- const d = Math.hypot(dx, y + 0.5 - cy);
112
- if (d <= 1.5)
113
- return 'high';
114
- return d >= 4 ? 'low' : 'auto';
115
- }
116
- /** The worker of a vector tile among `count`: always the same one */
117
- export function workerIndex(vz, vx, vy, count) {
118
- // Unsigned: every worker gets its share
119
- return ((Math.imul(vx, 73856093) ^ Math.imul(vy, 19349663) ^ vz) >>> 0) % Math.max(1, count);
120
- }
121
- /** The worker-side key of a renderer: unique across the `VectorTiles` sharing a pool */
122
- export function rendererKey(namespace, n) {
123
- return `${namespace}:r${n}`;
124
- }
123
+ /** The style layers of buildings, by default: PLAN IGN's `bati_surf` (not its built-up areas nor its parks), the
124
+ * OpenMapTiles' `building` */
125
+ const isBuildings = (layer) => ['bati_surf', 'building'].includes(layer['source-layer'] ?? '');
125
126
  /**
126
- * @internal Workers drawing vector tiles, shared by the `VectorTiles` of a map: each vector tile always goes to the
127
- * same worker, which keeps it decoded for every style drawn from it. Requests are tracked one by one, with their
128
- * owner, so that one `VectorTiles` going away cancels its own requests only.
127
+ * The tiles of level `z` in view, within a window around the center (a tilted view's bounds reach far), the
128
+ * `maxTiles` nearest to the center first
129
129
  */
130
- export class BasemapWorkers {
131
- count;
132
- url;
133
- workers;
134
- pending = new Map();
135
- next = 1;
136
- namespaces = 0;
137
- terminated = false;
138
- /** Stops following the page's request headers */
139
- unfollow;
140
- constructor(count, url, create = (u) => new Worker(u, { type: 'module' })) {
141
- this.count = count;
142
- this.url = url;
143
- this.workers = Array.from({ length: Math.max(1, count) }, () => {
144
- const worker = create(url);
145
- worker.onmessage = ({ data }) => {
146
- const p = this.pending.get(data.id);
147
- if (!p)
148
- return;
149
- this.pending.delete(data.id);
150
- if ('error' in data)
151
- p.reject(new Error(data.error));
152
- else
153
- p.resolve(data);
154
- };
155
- // A worker that fails to load (a bundler serving it from elsewhere) or to read a message fails its requests
156
- worker.onerror = (event) => {
157
- event.preventDefault();
158
- this.fail(worker, `the vector basemap worker ${url} failed${event.message ? `: ${event.message}` : ''}`);
159
- };
160
- worker.onmessageerror = () => this.fail(worker, `the vector basemap worker ${url} sent an unreadable message`);
161
- const rules = headerRules();
162
- if (rules.length > 0)
163
- worker.postMessage({ type: 'headers', rules });
164
- return worker;
165
- });
166
- this.unfollow = onHeaderRules((rules) => {
167
- for (const w of this.workers)
168
- w.postMessage({ type: 'headers', rules });
169
- });
170
- }
171
- /** A prefix for the renderer keys of one `VectorTiles` */
172
- namespace() {
173
- return `v${this.namespaces++}`;
174
- }
175
- /** Requests in progress */
176
- get size() {
177
- return this.pending.size;
178
- }
179
- /** Rejects the requests of a failed worker */
180
- fail(worker, message) {
181
- 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)`);
182
- for (const [id, p] of this.pending) {
183
- if (p.worker !== worker)
184
- continue;
185
- this.pending.delete(id);
186
- p.reject(error);
187
- }
188
- }
189
- /** The worker of a vector tile */
190
- worker(vz, vx, vy) {
191
- return this.workers[workerIndex(vz, vx, vy, this.workers.length)] ?? this.workers[0];
192
- }
193
- /** Sends a message to every worker (a renderer, or its removal) */
194
- broadcast(message) {
195
- if (this.terminated)
196
- return;
197
- // A renderer tells the workers how many they are: they share the decoded tiles' budget
198
- const sent = message.type === 'init' ? { ...message, workers: this.workers.length } : message;
199
- // A sprite image is copied to each worker (structured clone)
200
- for (const w of this.workers)
201
- w.postMessage(sent);
202
- }
203
- /** Asks a worker for a tile's image or labels, on behalf of `owner` */
204
- request(worker, work, owner, signal) {
205
- return new Promise((resolve, reject) => {
206
- if (this.terminated)
207
- return reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
208
- if (signal?.aborted)
209
- return reject(new DOMException('aborted', 'AbortError'));
210
- const id = this.next++;
211
- const abort = () => {
212
- if (!this.pending.delete(id))
213
- return;
214
- worker.postMessage({ type: 'abort', id });
215
- reject(new DOMException('aborted', 'AbortError'));
216
- };
217
- // The listener goes once the request is settled
218
- const settle = () => signal?.removeEventListener('abort', abort);
219
- this.pending.set(id, {
220
- resolve: (r) => {
221
- settle();
222
- resolve(r);
223
- },
224
- reject: (e) => {
225
- settle();
226
- reject(e);
227
- },
228
- worker,
229
- owner,
230
- });
231
- signal?.addEventListener('abort', abort, { once: true });
232
- worker.postMessage({ ...work, id });
233
- });
234
- }
235
- /** The altitudes of a PNG relief tile `z`/`x`/`y` at `url`, decoded by a worker (see `fetchHeights`) */
236
- heights(url, encoding, floor, [z, x, y], signal, owner) {
237
- const work = { type: 'heights', url, encoding, ...(floor === undefined ? {} : { floor }) };
238
- return this.request(this.worker(z, x, y), work, owner, signal).then((r) => ('heights' in r ? r.heights : null));
239
- }
240
- /** Mercator tile `z`/`x`/`y` reprojected by a worker from the source images of its plan (see `resampler`) */
241
- reproject(job, [z, x, y], signal, owner) {
242
- // Blocks of 4 × 4 Mercator tiles go to the same worker: neighbours share source tiles, each worker keeps its share
243
- const worker = this.worker(Math.max(0, z - 2), x >> 2, y >> 2);
244
- const cached = Math.max(16, Math.ceil(SOURCE_CACHE / this.workers.length));
245
- const work = { type: 'reproject', job, cached };
246
- return this.request(worker, work, owner, signal).then((r) => ('image' in r ? r.image : null));
247
- }
248
- /** Cancels the requests of an owner (the workers stop them) */
249
- cancel(owner) {
250
- for (const [id, p] of this.pending) {
251
- if (p.owner !== owner)
252
- continue;
253
- this.pending.delete(id);
254
- p.worker.postMessage({ type: 'abort', id });
255
- p.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
256
- }
257
- }
258
- /** Stops the workers; their requests fail */
259
- terminate() {
260
- if (this.terminated)
261
- return;
262
- this.terminated = true;
263
- this.unfollow();
264
- for (const w of this.workers)
265
- w.terminate();
266
- const pending = [...this.pending.values()];
267
- this.pending.clear();
268
- for (const p of pending)
269
- p.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
270
- }
271
- }
130
+ const tilesAround = (map, camera, z, maxTiles) => {
131
+ const n = 2 ** z;
132
+ const [west, south, east, north] = map.getBounds();
133
+ const tx = (lon) => Math.floor(mercatorX(lon) * n);
134
+ const ty = (lat) => Math.floor(mercatorY(lat) * n);
135
+ const [cx, cy] = [mercatorX(camera.center[0]) * n, mercatorY(camera.center[1]) * n];
136
+ const reach = Math.ceil(Math.sqrt(maxTiles)) + 2;
137
+ const [x0, x1] = [Math.max(0, tx(west), Math.floor(cx) - reach), Math.min(n - 1, tx(east), Math.floor(cx) + reach)];
138
+ const [y0, y1] = [Math.max(0, ty(north), Math.floor(cy) - reach), Math.min(n - 1, ty(south), Math.floor(cy) + reach)];
139
+ 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]));
140
+ 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));
141
+ return wanted.slice(0, maxTiles);
142
+ };
272
143
  /** A tile's label candidates as features, by style layer */
273
- export function tileLabels(candidates) {
144
+ export const tileLabels = (candidates) => {
274
145
  const byLayer = new Map();
275
- for (const p of candidates) {
146
+ candidates.forEach((p) => {
276
147
  const feature = {
277
148
  type: 'Feature',
278
149
  properties: p,
@@ -291,9 +162,9 @@ export function tileLabels(candidates) {
291
162
  list.push(feature);
292
163
  else
293
164
  byLayer.set(p.layer, [feature]);
294
- }
165
+ });
295
166
  return [...byLayer].sort((a, b) => b[0] - a[0]);
296
- }
167
+ };
297
168
  export class VectorTiles {
298
169
  map;
299
170
  pool;
@@ -334,16 +205,14 @@ export class VectorTiles {
334
205
  });
335
206
  }
336
207
  async load(input) {
337
- const style = typeof input === 'string' ? await json(input) : input;
208
+ const style = typeof input === 'string' ? await fetchStyle(input) : input;
338
209
  const [name, source] = Object.entries(style.sources).find(([, s]) => s.type === 'vector') ?? [];
339
210
  if (!name || !source)
340
211
  throw new Error('the style has no vector source');
341
- let { tiles, maxzoom } = await resolveSource(source);
342
- if (!tiles && source.url) {
343
- const tilejson = await json(source.url);
344
- tiles = tilejson.tiles;
345
- maxzoom ??= tilejson.maxzoom;
346
- }
212
+ const resolved = await resolveSource(source);
213
+ const tilejson = !resolved.tiles && source.url ? await fetchTileJson(source.url) : undefined;
214
+ const tiles = tilejson === undefined ? resolved.tiles : tilejson.tiles;
215
+ const maxzoom = resolved.maxzoom ?? tilejson?.maxzoom;
347
216
  const template = tiles?.[0];
348
217
  if (!template)
349
218
  throw new Error(`source ${name} has no tiles`);
@@ -367,11 +236,11 @@ export class VectorTiles {
367
236
  /** The fonts of the style's labels, for its glyphs (the first of each `text-font`, as the workers take it) */
368
237
  static fonts(style) {
369
238
  const fonts = new Set();
370
- for (const layer of style.layers) {
239
+ style.layers.forEach((layer) => {
371
240
  const font = layer.layout?.['text-font'];
372
241
  if (layer.type === 'symbol' && Array.isArray(font) && typeof font[0] === 'string')
373
242
  fonts.add(labelFont(font[0]));
374
- }
243
+ });
375
244
  return [...fonts];
376
245
  }
377
246
  /** The style's sprites (their JSON and their images), loaded once, for the fill patterns and the icons */
@@ -389,10 +258,10 @@ export class VectorTiles {
389
258
  return Promise.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
390
259
  return this.pool.request(this.pool.worker(vz, vx, vy), work, this, signal);
391
260
  }
392
- /** Sends a renderer of these layers to every worker; returns its key */
393
- async renderer(select, extra) {
261
+ /** Sends a renderer of these layers to every worker (again under `again`, restyled); returns its key */
262
+ async renderer(select, extra, again) {
394
263
  const { style, tiles, maxzoom, rasters } = await this.ready;
395
- const key = rendererKey(this.namespace, this.keys++);
264
+ const key = again ?? rendererKey(this.namespace, this.keys++);
396
265
  const layers = [...style.layers.filter(select), ...extra];
397
266
  const patterns = layers.some((l) => l.paint?.['fill-pattern'] !== undefined);
398
267
  // The images are downloaded once, copied to each worker
@@ -400,8 +269,9 @@ export class VectorTiles {
400
269
  if (this.destroyed)
401
270
  throw new DOMException('isard: vector tiles destroyed', 'AbortError');
402
271
  this.pool.broadcast({ type: 'init', key, tiles, maxzoom, layers, rasters, ...(sprites.length ? { sprites } : {}) });
403
- this.renderers.push(key);
404
- return { key, maxzoom };
272
+ if (!again)
273
+ this.renderers.push(key);
274
+ return { key, maxzoom, layers };
405
275
  }
406
276
  /**
407
277
  * A raster layer drawing the style's fills, lines and circles (or those `layers` keeps, then `extraLayers`, of the
@@ -436,6 +306,310 @@ export class VectorTiles {
436
306
  this.ground ??= layer;
437
307
  return layer;
438
308
  }
309
+ /**
310
+ * An extrusion layer of the polygons of the style's fill layers that `layers` keeps (by default its buildings: the
311
+ * fill or fill-extrusion layers of PLAN IGN's `bati_surf` or OpenMapTiles' `building`), raised by the style
312
+ * given (`height`, `base`, `color`, `ramp`…; the properties of the tiles' features). Its tiles follow the view: at
313
+ * the source's deepest level, the `maxTiles` nearest to the center, from `minZoom` on (15 by default); the tiles
314
+ * leaving the view are dropped.
315
+ */
316
+ extrusions(options = {}) {
317
+ const { layers = isBuildings, minZoom = 15, maxTiles = 36, ...style } = options;
318
+ const extrusions = this.map.addExtrusions(style);
319
+ const renderer = this.renderer((l) => (l.type === 'fill' || l.type === 'fill-extrusion') && layers(l), []);
320
+ renderer.catch(() => undefined);
321
+ this.layers.push(extrusions);
322
+ /** The tiles held or asked for, by key */
323
+ const held = new Map();
324
+ const update = async () => {
325
+ if (this.destroyed)
326
+ return;
327
+ const { key, maxzoom } = await renderer;
328
+ const camera = this.map.getCamera();
329
+ const z = maxzoom;
330
+ const wanted = camera.zoom < minZoom ? [] : tilesAround(this.map, camera, z, maxTiles);
331
+ const keys = new Set(wanted.map(([x, y]) => `${z}/${x}/${y}`));
332
+ // Out of view: dropped, or no longer asked for
333
+ [...held].forEach(([k, controller]) => {
334
+ if (keys.has(k))
335
+ return;
336
+ controller.abort();
337
+ held.delete(k);
338
+ extrusions.removeTile(k);
339
+ });
340
+ wanted.forEach(([x, y], index) => {
341
+ const k = `${z}/${x}/${y}`;
342
+ if (held.has(k))
343
+ return;
344
+ const controller = new AbortController();
345
+ const work = { type: 'polygons', key, z, x, y, priority: index < 4 ? 'auto' : 'low' };
346
+ held.set(k, controller);
347
+ this.request(z, x, y, work, controller.signal).then((r) => {
348
+ if (controller.signal.aborted || !('polygons' in r))
349
+ return;
350
+ const tile = r.polygons;
351
+ // The features' properties as the style reads them
352
+ extrusions.setTile(k, { arrays: tile.arrays, properties: tile.properties });
353
+ }, () => {
354
+ // A failure is asked for again on the next update
355
+ if (held.get(k) === controller)
356
+ held.delete(k);
357
+ });
358
+ });
359
+ };
360
+ this.followView(update, held, update);
361
+ return extrusions;
362
+ }
363
+ /**
364
+ * Trees on the style's woods, forests and grass (or the fill layers `layers` keeps) as 3D models, from street level
365
+ * (`minZoom`, default 15): the workers scatter them over the polygons of the vector tiles around the view, `spacing`
366
+ * metres apart (by style layer: 9 in woods, 25 on grass, by default) on a jittered grid, the same trees whenever a
367
+ * tile comes back; the engine draws them all in one instanced draw, on the relief. A low-poly tree by default (its
368
+ * `colors`), or your own glTF (`url`, `data`, about 10 metres high, standing on its origin). Dense around the
369
+ * view's center, fewer and fewer away from it (as the camera moves too), at most `maxInstances` drawn (default
370
+ * 25000); at most `maxTiles` tiles (default 9) and `maxTrees` trees in each (default 30000). The models load the
371
+ * engine with 3D Tiles.
372
+ */
373
+ trees(options = {}) {
374
+ const { layers = isWoodland, spacing = treeSpacing, minZoom = 15, maxTiles = 9, maxTrees = 30000, maxInstances = 25000, } = options;
375
+ const model = options.url ? { url: options.url } : { data: options.data ?? treeModel(options.colors) };
376
+ const trees = this.map.addModels({
377
+ ...model,
378
+ instances: [],
379
+ ...(options.before ? { before: options.before } : {}),
380
+ });
381
+ const renderer = this.renderer((l) => l.type === 'fill' && layers(l), []);
382
+ renderer.catch(() => undefined);
383
+ this.layers.push(trees);
384
+ /** The tiles held or asked for, by key, and the trees of those that came */
385
+ const held = new Map();
386
+ const found = new Map();
387
+ /** The trees drawn to the engine (longitude, latitude, altitude, heading, pitch, roll, scale each): all of them
388
+ * near the view's center, past `near` metres those of a rank under (near / distance)², at most `maxInstances` */
389
+ const send = () => {
390
+ const { center, zoom } = this.map.getCamera();
391
+ const near = TREES_NEAR_METRES * 2 ** (16 - zoom);
392
+ const metresPerDegree = 111320;
393
+ const cos = Math.cos((center[1] * Math.PI) / 180);
394
+ const chosen = [];
395
+ found.forEach((t) => {
396
+ for (let i = 0; i < t.length / TREE_NUMBERS; i += 1) {
397
+ const dx = ((t[i * TREE_NUMBERS] ?? 0) - center[0]) * metresPerDegree * cos;
398
+ const dy = ((t[i * TREE_NUMBERS + 1] ?? 0) - center[1]) * metresPerDegree;
399
+ const far = (dx * dx + dy * dy) / (near * near);
400
+ const score = (t[i * TREE_NUMBERS + 4] ?? 0) * Math.max(1, far);
401
+ if (score < 1)
402
+ chosen.push([score, t, i]);
403
+ }
404
+ });
405
+ if (chosen.length > maxInstances)
406
+ chosen.sort((a, b) => a[0] - b[0]).splice(maxInstances);
407
+ const out = new Float64Array(chosen.length * 7);
408
+ chosen.forEach(([, t, i], k) => {
409
+ const at = i * TREE_NUMBERS;
410
+ out.set([t[at] ?? 0, t[at + 1] ?? 0, 0, t[at + 3] ?? 0, 0, 0, t[at + 2] ?? 1], k * 7);
411
+ });
412
+ trees.setInstanceNumbers(out);
413
+ };
414
+ const update = async () => {
415
+ if (this.destroyed)
416
+ return;
417
+ const { key, maxzoom, layers: list } = await renderer;
418
+ const camera = this.map.getCamera();
419
+ const z = maxzoom;
420
+ const wanted = camera.zoom < minZoom ? [] : tilesAround(this.map, camera, z, maxTiles);
421
+ const keys = new Set(wanted.map(([x, y]) => `${z}/${x}/${y}`));
422
+ const metres = list.map((l) => (typeof spacing === 'number' ? spacing : spacing(l)));
423
+ const dropped = [...held.keys()].filter((k) => !keys.has(k));
424
+ // Out of view: dropped, or no longer asked for
425
+ dropped.forEach((k) => {
426
+ held.get(k)?.abort();
427
+ held.delete(k);
428
+ found.delete(k);
429
+ });
430
+ // Dense around the new center
431
+ send();
432
+ wanted.forEach(([x, y], index) => {
433
+ const k = `${z}/${x}/${y}`;
434
+ if (held.has(k))
435
+ return;
436
+ const controller = new AbortController();
437
+ const priority = index < 4 ? 'auto' : 'low';
438
+ const work = { type: 'trees', key, z, x, y, spacing: metres, maxTrees, priority };
439
+ held.set(k, controller);
440
+ this.request(z, x, y, work, controller.signal).then((r) => {
441
+ if (controller.signal.aborted || !('trees' in r))
442
+ return;
443
+ found.set(k, r.trees);
444
+ send();
445
+ }, () => {
446
+ // A failure is asked for again on the next update
447
+ if (held.get(k) === controller)
448
+ held.delete(k);
449
+ });
450
+ });
451
+ };
452
+ // While the camera moves, the trees drawn follow its center (dense near it)
453
+ this.followView(update, held, send);
454
+ return trees;
455
+ }
456
+ /**
457
+ * A layer drawing the style's fills, lines and buildings (or those `layers` keeps; backgrounds as fills, the
458
+ * `fill-extrusion` layers raised into buildings by their height and base height) as geometry instead of images: the
459
+ * workers cut the vector tiles in view into polygons and lines, which the engine draws on the globe and the relief,
460
+ * in the style's order, each style layer over its zoom range. Sharp at any zoom
461
+ * and pitch, picked by feature (`pick` gives its properties and style layer), restyled without cutting the tiles
462
+ * again (`setPaintProperty`, `setLayoutProperty`). Its tiles follow the view, while it moves too, coarser far away in a
463
+ * tilted view, and those leaving it stay until the ones replacing them are there; past the source's deepest level,
464
+ * cut again from its tiles down to level 18 (overzoom: their filters and styles those of their level). The fills' outlines are drawn one
465
+ * pixel wide, the lines' ends as their `line-cap` says (their joins round), the colors follow the camera's zoom. The
466
+ * layers drawn with a pattern and the circles are left out (the raster path draws them), as are `line-offset`,
467
+ * `line-blur` and the translations (`fill-translate`, `line-translate`); `raster` stays the lighter path for heavy
468
+ * styles and small devices. At night (`setSun`) the line layers `streets` keeps glow (by default those whose id names
469
+ * a road, not its casing, a railway, a path or a tunnel).
470
+ */
471
+ vectors(options = {}) {
472
+ const { layers = () => true, streets = isStreet, before } = options;
473
+ const vector = this.map.addVectorLayer(before);
474
+ const state = { renderer: this.renderer((l) => vectorKind(l) !== undefined && layers(l), []), update: 0, style: 0 };
475
+ state.renderer.catch(() => undefined);
476
+ this.layers.push(vector);
477
+ const sublayers = (list) => {
478
+ const zoom = this.map.getCamera().zoom;
479
+ return list.map((l) => ({
480
+ id: l.id,
481
+ sourceLayer: l['source-layer'],
482
+ kind: vectorKind(l) ?? 'fill',
483
+ minZoom: l.minzoom ?? 0,
484
+ maxZoom: l.maxzoom ?? 24,
485
+ visible: l.layout?.visibility !== 'none',
486
+ opacity: zoomOnly(l.paint?.[`${l.type}-opacity`], 1) ?? 1,
487
+ cap: lineCap(l.layout?.['line-cap'], zoom),
488
+ lit: l.type === 'line' && streets(l),
489
+ dash: l.type === 'line' ? dashes(l.paint?.['line-dasharray'], zoom) : [0, 0, 0, 0],
490
+ }));
491
+ };
492
+ /** The tiles held or asked for, by `z/x/y` */
493
+ const held = new Map();
494
+ /** A tile's geometry, or its styles alone, asked of the worker of its tile of the source's level (which keeps the
495
+ * decoded tile its deeper tiles are cut from) */
496
+ const ask = ({ key, maxzoom }, [z = 0, x = 0, y = 0], styles, priority, signal) => {
497
+ const d = Math.max(0, z - maxzoom);
498
+ const work = { type: 'vectors', key, z, x, y, deepest: deepestLevel(maxzoom), priority };
499
+ // The geometry made ready for the GPU in the workers, with the prisms the relief needs
500
+ return this.request(z - d, Math.floor(x / 2 ** d), Math.floor(y / 2 ** d), styles ? { ...work, styles } : { ...work, prism: this.map.hasRelief }, signal);
501
+ };
502
+ // The workers tessellate the tiles (the main thread only uploads them), once they know the module
503
+ this.pool.broadcast({ type: 'tess', url: this.map.tessUrl });
504
+ /** The styles of a tile held asked for again, the tile kept as it is */
505
+ const restyleTile = async (renderer, k) => {
506
+ const r = await ask(renderer, k.split('/').map(Number), true, 'auto').catch(() => undefined);
507
+ if (r && 'styles' in r)
508
+ vector.setTileStyles(k, r.styles);
509
+ };
510
+ const update = async () => {
511
+ if (this.destroyed)
512
+ return;
513
+ const generation = ++state.update;
514
+ const { key, maxzoom, layers: list } = await state.renderer;
515
+ vector.setSublayers(sublayers(state.own ?? list));
516
+ const view = this.map.engine.vectorTilesInView(0, deepestLevel(maxzoom));
517
+ const wanted = Array.from({ length: Math.floor(view.length / 3) }, (_, i) => [
518
+ ...view.subarray(i * 3, i * 3 + 3),
519
+ ]);
520
+ const keys = new Set(wanted.map((t) => t.join('/')));
521
+ const loads = wanted.flatMap((t, index) => {
522
+ const k = t.join('/');
523
+ if (held.has(k))
524
+ return [];
525
+ const controller = new AbortController();
526
+ const style = state.style;
527
+ held.set(k, controller);
528
+ return [
529
+ ask({ key, maxzoom }, t, false, index < 4 ? 'high' : 'auto', controller.signal).then(async (r) => {
530
+ if (controller.signal.aborted || !('vectors' in r))
531
+ return;
532
+ vector.setTile(k, r.vectors);
533
+ // Cut under a style changed since: its styles asked for again
534
+ if (style !== state.style)
535
+ await restyleTile(await state.renderer, k);
536
+ }, () => {
537
+ // A failure is asked for again on the next update
538
+ if (held.get(k) === controller)
539
+ held.delete(k);
540
+ }),
541
+ ];
542
+ });
543
+ // Out of view: those still asked for go at once, those drawn once their replacements are there (no hole)
544
+ const drop = (drawn) => {
545
+ const shown = new Set(vector.tileKeys);
546
+ [...held].forEach(([k, controller]) => {
547
+ if (keys.has(k) || shown.has(k) !== drawn)
548
+ return;
549
+ controller.abort();
550
+ held.delete(k);
551
+ vector.removeTile(k);
552
+ });
553
+ };
554
+ drop(false);
555
+ await Promise.all(loads);
556
+ // A later update (the view moved again meanwhile) drops for its own view: these keys are no longer the view
557
+ if (!this.destroyed && generation === state.update)
558
+ drop(true);
559
+ };
560
+ const refresh = () => {
561
+ update().catch(() => undefined);
562
+ };
563
+ /** The style layers changed: the workers' copy, then the styles of the tiles held */
564
+ const restyle = async (list) => {
565
+ state.own = list;
566
+ state.style++;
567
+ state.renderer = state.renderer.then(({ key }) => this.renderer(() => false, list, key));
568
+ const renderer = await state.renderer;
569
+ vector.setSublayers(sublayers(list));
570
+ await Promise.all(vector.tileKeys.map((k) => restyleTile(renderer, k)));
571
+ };
572
+ vector.restyle = (id, group, name, value) => {
573
+ state.renderer
574
+ .then(({ layers: list }) => {
575
+ const next = (state.own ?? list).map((l) => l.id === id ? { ...l, [group]: { ...l[group], [name]: value } } : l);
576
+ // Visibility and dashes are the engine's: the tiles stay as they are
577
+ if ((group === 'layout' && name === 'visibility') || name === 'line-dasharray') {
578
+ state.own = next;
579
+ vector.setSublayers(sublayers(next));
580
+ return undefined;
581
+ }
582
+ // An outline added or taken away: a feature more or less in each tile, cut again
583
+ if (name === 'fill-outline-color' || name === 'fill-antialias') {
584
+ state.own = next;
585
+ state.style++;
586
+ state.renderer = state.renderer.then(({ key }) => this.renderer(() => false, next, key));
587
+ vector.setSublayers(sublayers(next));
588
+ vector.recut?.();
589
+ return undefined;
590
+ }
591
+ return restyle(next);
592
+ })
593
+ .catch(() => undefined);
594
+ };
595
+ vector.recut = () => {
596
+ held.forEach((controller) => {
597
+ controller.abort();
598
+ });
599
+ held.clear();
600
+ refresh();
601
+ };
602
+ vector.styled = state.renderer.then(({ layers: list }) => {
603
+ if (!this.destroyed)
604
+ vector.setSublayers(sublayers(state.own ?? list));
605
+ });
606
+ // Reported by the style's `ready`
607
+ vector.styled.catch(() => undefined);
608
+ // While the camera moves, the tiles follow it (as the labels do: a long drag or flight does not leave the view of
609
+ // its start drawn)
610
+ this.followView(update, held, update);
611
+ return vector;
612
+ }
439
613
  /**
440
614
  * A symbol layer with the labels of the style's symbol layers (or those `layers` keeps), updated as the view
441
615
  * moves: the labels of the vector tiles in view, the higher style layers placed first. With a raster layer of
@@ -453,7 +627,7 @@ export class VectorTiles {
453
627
  color: (p) => p.color,
454
628
  haloColor: (p) => p.haloColor,
455
629
  haloWidth: (p) => p.haloWidth,
456
- anchor: (p) => p.anchor,
630
+ anchor: (p) => anchor(p.anchor),
457
631
  offset: (p) => p.offset,
458
632
  maxWidth: (p) => p.maxWidth,
459
633
  lineHeight: (p) => p.lineHeight,
@@ -462,7 +636,7 @@ export class VectorTiles {
462
636
  textOptional: (p) => p.textOptional === true,
463
637
  icon: (p) => (p.icon ? this.iconPrefix + p.icon.image : ''),
464
638
  iconSize: (p) => size(p.icon?.size ?? 1, 1),
465
- iconAnchor: (p) => (p.icon?.anchor ?? 'center'),
639
+ iconAnchor: (p) => anchor(p.icon?.anchor),
466
640
  iconOffset: (p) => p.icon?.offset ?? [0, 0],
467
641
  iconRotate: (p) => p.icon?.rotate ?? 0,
468
642
  iconRotationAlignment: (p) => p.icon?.alignment ?? 'viewport',
@@ -482,10 +656,10 @@ export class VectorTiles {
482
656
  // Its icons from its sprites, before the first labels
483
657
  const icons = style.layers.some((l) => l.type === 'symbol' && l.layout?.['icon-image'] !== undefined);
484
658
  if (icons && style.sprite) {
485
- for (const sprite of await this.loadSprites(style.sprite)) {
659
+ (await this.loadSprites(style.sprite)).forEach((sprite) => {
486
660
  if (!this.destroyed)
487
661
  this.map.icons.addSprite(this.iconPrefix, sprite.icons, () => readPixels(sprite.image));
488
- }
662
+ });
489
663
  }
490
664
  return this.renderer((l) => l.type === 'symbol' && layers(l), []);
491
665
  });
@@ -510,57 +684,39 @@ export class VectorTiles {
510
684
  if (ground.length > 0) {
511
685
  // The ground's tiles, the nearest first: a tile so many levels coarser than the finest in view has its
512
686
  // labels of a zoom as many under the view's, past the source's deepest level those of that level at the zoom
513
- let top = 0;
514
- for (let i = 0; i < ground.length; i += 3)
515
- top = Math.max(top, ground[i]);
516
- for (let i = 0; i < ground.length && keys.length < maxTiles; i += 3) {
517
- const [z, x, y] = [ground[i], ground[i + 1], ground[i + 2]];
687
+ const groundTiles = Array.from({ length: Math.ceil(ground.length / 3) }, (_, k) => [
688
+ ground[3 * k] ?? 0,
689
+ ground[3 * k + 1] ?? 0,
690
+ ground[3 * k + 2] ?? 0,
691
+ ]);
692
+ const top = groundTiles.reduce((t, [z]) => Math.max(t, z), 0);
693
+ groundTiles.forEach(([z, x, y]) => {
694
+ if (keys.length >= maxTiles)
695
+ return;
518
696
  const at = Math.max(0, zoom - (top - z));
519
697
  const tz = Math.min(at, maxzoom, z);
520
698
  const shift = 2 ** (z - tz);
521
699
  const key = `${tz}/${Math.floor(x / shift)}/${Math.floor(y / shift)}${at > tz ? `@${at}` : ''}`;
522
700
  if (!keys.includes(key))
523
701
  keys.push(key);
524
- }
702
+ });
525
703
  }
526
704
  else {
527
705
  const z = Math.min(zoom, maxzoom);
528
- const n = 2 ** z;
529
- const [west, south, east, north] = this.map.getBounds();
530
- const tx = (lon) => Math.floor(mercatorX(lon) * n);
531
- const ty = (lat) => Math.floor(mercatorY(lat) * n);
532
- const [cx, cy] = [mercatorX(camera.center[0]) * n, ty(camera.center[1])];
533
- // The tiles of the bounds, within a window around the center: a tilted view's bounds reach far, and only
534
- // the nearest `maxTiles` are kept anyway
535
- const reach = Math.ceil(Math.sqrt(maxTiles)) + 2;
536
- const [x0, x1] = [
537
- Math.max(0, tx(west), Math.floor(cx) - reach),
538
- Math.min(n - 1, tx(east), Math.floor(cx) + reach),
539
- ];
540
- const [y0, y1] = [
541
- Math.max(0, ty(north), Math.floor(cy) - reach),
542
- Math.min(n - 1, ty(south), Math.floor(cy) + reach),
543
- ];
544
- const wanted = [];
545
- for (let y = y0; y <= y1; y += 1)
546
- for (let x = x0; x <= x1; x += 1)
547
- wanted.push([x, y]);
548
- // Nearest to the center first, at most `maxTiles` (tilted views reach far)
549
- 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));
706
+ const wanted = tilesAround(this.map, camera, z, maxTiles);
550
707
  // Past the source's deepest level, its tiles' labels at the zoom of the view (the layers of the deeper zooms)
551
- for (const [x, y] of wanted.slice(0, maxTiles))
552
- keys.push(`${z}/${x}/${y}${zoom > z ? `@${zoom}` : ''}`);
708
+ keys.push(...wanted.map(([x, y]) => `${z}/${x}/${y}${zoom > z ? `@${zoom}` : ''}`));
553
709
  }
554
710
  // The same tiles as the labels shown: nothing to do
555
711
  const set = keys.join(' ');
556
712
  if (set === shown)
557
713
  return;
558
714
  const lists = await Promise.all(keys.map((k, index) => {
559
- let p = tiles.get(k);
560
- if (p)
561
- return p;
715
+ const cached = tiles.get(k);
716
+ if (cached)
717
+ return cached;
562
718
  const [tile, deeper] = k.split('@');
563
- const [tz, x, y] = (tile ?? '').split('/').map(Number);
719
+ const [tz = 0, x = 0, y = 0] = (tile ?? '').split('/').map(Number);
564
720
  // The nearest tiles first: the others wait for the raster tiles
565
721
  const priority = index < 4 ? 'auto' : 'low';
566
722
  const work = {
@@ -572,7 +728,7 @@ export class VectorTiles {
572
728
  priority,
573
729
  ...(deeper === undefined ? {} : { zoom: Number(deeper) }),
574
730
  };
575
- p = this.request(tz, x, y, work).then((r) => tileLabels('labels' in r ? r.labels : []), () => {
731
+ const p = this.request(tz, x, y, work).then((r) => tileLabels('labels' in r ? r.labels : []), () => {
576
732
  // A network error is not kept: the tile is asked for again on the next update
577
733
  tiles.delete(k);
578
734
  return [];
@@ -594,14 +750,7 @@ export class VectorTiles {
594
750
  // The labels of the higher style layers first: they win the collisions; for each style layer, the tiles nearest
595
751
  // to the center first
596
752
  const layerIds = [...new Set(lists.flatMap((list) => list.map(([layer]) => layer)))].sort((a, b) => b - a);
597
- const groups = [];
598
- for (const layer of layerIds) {
599
- for (const list of lists) {
600
- const group = list.find(([l]) => l === layer)?.[1];
601
- if (group)
602
- groups.push(group);
603
- }
604
- }
753
+ const groups = layerIds.flatMap((layer) => lists.map((list) => list.find(([l]) => l === layer)?.[1]).filter((group) => group !== undefined));
605
754
  symbol.setGroups(groups);
606
755
  };
607
756
  // The style failed to load: reported by `ready`
@@ -624,24 +773,53 @@ export class VectorTiles {
624
773
  refresh();
625
774
  return symbol;
626
775
  }
776
+ /**
777
+ * Makes a layer's tiles follow the view: `update` now and where the camera stops (`moveend`), `moving` every
778
+ * `VECTOR_MOVE_REFRESH_MS` while it moves; the requests `held` aborted when destroyed
779
+ */
780
+ followView(update, held, moving) {
781
+ const refresh = () => {
782
+ update().catch(() => undefined);
783
+ };
784
+ const pace = { timer: 0 };
785
+ const follow = () => {
786
+ if (pace.timer)
787
+ return;
788
+ pace.timer = window.setTimeout(() => {
789
+ pace.timer = 0;
790
+ Promise.resolve(moving()).catch(() => undefined);
791
+ }, VECTOR_MOVE_REFRESH_MS);
792
+ };
793
+ this.cleanup.push(this.map.on('move', follow), this.map.on('moveend', refresh), () => {
794
+ clearTimeout(pace.timer);
795
+ held.forEach((controller) => {
796
+ controller.abort();
797
+ });
798
+ held.clear();
799
+ });
800
+ refresh();
801
+ }
627
802
  /** Stops its workers' work and removes its raster and label layers from the map (those still there) */
628
803
  destroy() {
629
804
  if (this.destroyed)
630
805
  return;
631
806
  this.destroyed = true;
632
- for (const off of this.cleanup)
807
+ this.cleanup.forEach((off) => {
633
808
  off();
809
+ });
634
810
  this.cleanup.length = 0;
635
- for (const layer of this.layers)
811
+ this.layers.forEach((layer) => {
636
812
  if (this.map.hasLayer(layer))
637
813
  layer.remove();
814
+ });
638
815
  this.layers.length = 0;
639
816
  this.map.dropVectorTiles(this);
640
817
  // The page's copy of the sprites (the workers have theirs), and their icons
641
818
  this.sprites
642
819
  ?.then((sprites) => {
643
- for (const sprite of sprites)
820
+ sprites.forEach((sprite) => {
644
821
  sprite.image.close();
822
+ });
645
823
  this.map.icons.remove(this.iconPrefix, true);
646
824
  })
647
825
  .catch(() => undefined);
@@ -651,97 +829,41 @@ export class VectorTiles {
651
829
  else {
652
830
  // Shared workers: only its requests stop and its renderers go
653
831
  this.pool.cancel(this);
654
- for (const key of this.renderers)
832
+ this.renderers.forEach((key) => {
655
833
  this.pool.broadcast({ type: 'drop', key });
834
+ });
656
835
  }
657
836
  this.renderers.length = 0;
658
837
  }
659
838
  }
660
- /** Colors of the layers of `basicStyle`, in turn */
661
- const BASIC_COLORS = ['#2f6fd1', '#d1495b', '#2a9d8f', '#e9a03b', '#7b5ea7', '#5c7a29', '#c8553d', '#3d8eb9'];
662
839
  /**
663
- * A plain style for vector tiles known only by their layers' names (a PMTiles archive's `vector_layers`, an MBTiles'
664
- * metadata): each layer's polygons filled, its lines and outlines stroked and its points as circles, a color per
665
- * layer, the `name` of points as labels. `source` is the style's vector source (`url`: TileJSON or `pmtiles://…`, or
666
- * `tiles`)
840
+ * A `line-dasharray` as the engine takes it: two dashes and gaps in line widths (an odd count repeated, as the
841
+ * canvas's `setLineDash`; past four, the first four), zeros for a solid line
667
842
  */
668
- export function basicStyle(source, layers) {
669
- const out = [];
670
- layers.forEach((name, i) => {
671
- const color = BASIC_COLORS[i % BASIC_COLORS.length];
672
- const base = { source: 'data', 'source-layer': name };
673
- out.push({ id: `${name}-fill`, type: 'fill', ...base, paint: { 'fill-color': color, 'fill-opacity': 0.18 } }, { id: `${name}-line`, type: 'line', ...base, paint: { 'line-color': color, 'line-width': 1.2 } }, {
674
- id: `${name}-circle`,
675
- type: 'circle',
676
- ...base,
677
- paint: { 'circle-color': color, 'circle-radius': 3.5, 'circle-stroke-color': '#fff', 'circle-stroke-width': 1 },
678
- });
679
- });
680
- layers.forEach((name) => {
681
- out.push({
682
- id: `${name}-label`,
683
- type: 'symbol',
684
- source: 'data',
685
- 'source-layer': name,
686
- filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'name']],
687
- layout: { 'text-field': ['get', 'name'], 'text-size': 12, 'text-offset': [0, 0.9], 'text-anchor': 'top' },
688
- paint: { 'text-color': '#1d2433', 'text-halo-color': '#fff', 'text-halo-width': 1.5 },
689
- });
690
- });
691
- return { version: 8, sources: { data: { type: 'vector', ...source } }, layers: out };
692
- }
693
- /** HTML of a file reduced to its text, escaped: credits from an archive's metadata cannot run in the page */
694
- function plainText(html) {
695
- return html
696
- .replace(/<[^>]*>/g, '')
697
- .replace(/&(?!(amp|lt|gt|quot|copy|#\d+);)/g, '&amp;')
698
- .replace(/</g, '&lt;')
699
- .replace(/>/g, '&gt;');
700
- }
701
- /** @internal `GeoMap.addPmtiles`: a raster layer of the archive's images, or vector tiles with their raster layer and
702
- * labels */
703
- export async function pmtilesLayer(map, url, options) {
704
- const archiveUrl = pmtilesUrl(url.startsWith('pmtiles://') ? url : `pmtiles://${url}`);
705
- const archive = pmtiles(archiveUrl);
706
- const [header, metadata] = await Promise.all([archive.header(), archive.metadata().catch(() => ({}))]);
707
- const credit = metadata.attribution;
708
- const attribution = options.attribution ?? (typeof credit === 'string' && credit ? plainText(credit) : undefined);
709
- const kind = options.kind ?? (header.tileType === 'mvt' ? 'vector' : 'raster');
710
- const credits = attribution ? { attribution } : {};
711
- if (kind === 'raster') {
712
- return map.addRaster({
713
- url: `pmtiles://${archiveUrl}`,
714
- minZoom: header.minZoom,
715
- maxZoom: Math.max(header.maxZoom, 19),
716
- ...(options.opacity === undefined ? {} : { opacity: options.opacity }),
717
- ...(options.size === undefined ? {} : { size: options.size }),
718
- ...(options.before ? { before: options.before } : {}),
719
- ...credits,
720
- });
721
- }
722
- const layers = metadata.vector_layers;
723
- const names = Array.isArray(layers) ? layers.map((l) => l?.id).filter((id) => typeof id === 'string') : [];
724
- const style = options.style ?? basicStyle({ url: `pmtiles://${archiveUrl}` }, names);
725
- const tiles = map.vectorTiles({ style, ...credits });
726
- tiles.raster({
727
- ...(options.opacity === undefined ? {} : { opacity: options.opacity }),
728
- ...(options.before ? { before: options.before } : {}),
729
- });
730
- tiles.labels();
731
- return tiles;
732
- }
733
- /** A raster loader reading the tiles of a `pmtiles://` archive URL (images of any size, drawn at `size`); undefined
734
- * for another URL */
735
- export function pmtilesRasterLoader(url, size) {
736
- const archiveUrl = pmtilesUrl(url);
737
- if (!archiveUrl)
738
- return undefined;
739
- const archive = pmtiles(archiveUrl);
740
- return async (z, x, y, signal) => {
741
- const bytes = await archive.tile(z, x, y, signal);
742
- if (!bytes)
743
- return null;
744
- return createImageBitmap(new Blob([bytes]), { resizeWidth: size, resizeHeight: size });
745
- };
746
- }
843
+ export const dashes = (value, zoom) => {
844
+ const v = isExpression(value) ? evaluate(value, { zoom, properties: {}, type: 'Unknown' }) : value;
845
+ const list = Array.isArray(v) ? v.map(Number).filter((d) => Number.isFinite(d) && d >= 0) : [];
846
+ const even = list.length % 2 === 1 ? [...list, ...list] : list;
847
+ const [a = 0, b = 0, c = 0, d = 0] = even.length >= 2 && even.some((x) => x > 0) ? even : [];
848
+ return [a, b, c, d];
849
+ };
850
+ /** Metres around the view's center where every tree is drawn at zoom 16 (twice as far a zoom level out) */
851
+ const TREES_NEAR_METRES = 350;
852
+ /** Whether a style layer draws streets, which glow at night: its id names a road (OpenMapTiles' styles, PLAN IGN's),
853
+ * not its casing, a railway, a path or a tunnel */
854
+ const isStreet = (layer) => /road|highway|street|motorway|trunk|primary|secondary|tertiary|minor|routier/i.test(layer.id) &&
855
+ !/casing|outline|rail|path|track|ferry|tunnel|pier|area|pattern|label/i.test(layer.id);
856
+ /** A `line-cap` as the engine draws it, at the camera's zoom (MapLibre's default: butt) */
857
+ const lineCap = (value, zoom) => {
858
+ const v = isExpression(value) ? evaluate(value, { zoom, properties: {}, type: 'Unknown' }) : value;
859
+ return v === 'round' || v === 'square' ? v : 'butt';
860
+ };
861
+ /** The deepest level `vectors()` cuts its tiles at: past the source's deepest level, its tiles cut again into deeper
862
+ * ones (overzoom, as MapLibre does) down to level 18, their filters and styles those of their level, smaller tiles
863
+ * drawn in a closer view; past it, the tiles of that level stretched */
864
+ const OVERZOOM_LEVEL = 18;
865
+ /** The deepest level of the tiles of `vectors()` over a source whose deepest is `maxzoom` */
866
+ const deepestLevel = (maxzoom) => Math.max(maxzoom, OVERZOOM_LEVEL);
867
+ /** Milliseconds between two updates of the tiles of `vectors()` while the camera moves */
868
+ const VECTOR_MOVE_REFRESH_MS = 250;
747
869
  //# sourceMappingURL=vectortiles.js.map