@react-x11/components 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/README.md +131 -4
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +11 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +26 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/qml/containers.d.ts +3 -0
  140. package/dist/qml/containers.d.ts.map +1 -0
  141. package/dist/qml/containers.js +331 -0
  142. package/dist/qml/containers.js.map +1 -0
  143. package/dist/qml/controls.d.ts +12 -0
  144. package/dist/qml/controls.d.ts.map +1 -0
  145. package/dist/qml/controls.js +21 -0
  146. package/dist/qml/controls.js.map +1 -0
  147. package/dist/qml/globals.d.ts +6 -0
  148. package/dist/qml/globals.d.ts.map +1 -0
  149. package/dist/qml/globals.js +25 -0
  150. package/dist/qml/globals.js.map +1 -0
  151. package/dist/qml/index.d.ts +15 -0
  152. package/dist/qml/index.d.ts.map +1 -0
  153. package/dist/qml/index.js +25 -0
  154. package/dist/qml/index.js.map +1 -0
  155. package/dist/qml/interaction.d.ts +12 -0
  156. package/dist/qml/interaction.d.ts.map +1 -0
  157. package/dist/qml/interaction.js +246 -0
  158. package/dist/qml/interaction.js.map +1 -0
  159. package/dist/qml/ir.d.ts +106 -0
  160. package/dist/qml/ir.d.ts.map +1 -0
  161. package/dist/qml/ir.js +7 -0
  162. package/dist/qml/ir.js.map +1 -0
  163. package/dist/qml/layouts.d.ts +3 -0
  164. package/dist/qml/layouts.d.ts.map +1 -0
  165. package/dist/qml/layouts.js +122 -0
  166. package/dist/qml/layouts.js.map +1 -0
  167. package/dist/qml/models.d.ts +14 -0
  168. package/dist/qml/models.d.ts.map +1 -0
  169. package/dist/qml/models.js +80 -0
  170. package/dist/qml/models.js.map +1 -0
  171. package/dist/qml/objects.d.ts +228 -0
  172. package/dist/qml/objects.d.ts.map +1 -0
  173. package/dist/qml/objects.js +1103 -0
  174. package/dist/qml/objects.js.map +1 -0
  175. package/dist/qml/parse.d.ts +10 -0
  176. package/dist/qml/parse.d.ts.map +1 -0
  177. package/dist/qml/parse.js +607 -0
  178. package/dist/qml/parse.js.map +1 -0
  179. package/dist/qml/qtquick.d.ts +7 -0
  180. package/dist/qml/qtquick.d.ts.map +1 -0
  181. package/dist/qml/qtquick.js +347 -0
  182. package/dist/qml/qtquick.js.map +1 -0
  183. package/dist/qml/react.d.ts +83 -0
  184. package/dist/qml/react.d.ts.map +1 -0
  185. package/dist/qml/react.js +323 -0
  186. package/dist/qml/react.js.map +1 -0
  187. package/dist/qml/resolver.d.ts +38 -0
  188. package/dist/qml/resolver.d.ts.map +1 -0
  189. package/dist/qml/resolver.js +64 -0
  190. package/dist/qml/resolver.js.map +1 -0
  191. package/dist/qml/slots.d.ts +65 -0
  192. package/dist/qml/slots.d.ts.map +1 -0
  193. package/dist/qml/slots.js +230 -0
  194. package/dist/qml/slots.js.map +1 -0
  195. package/dist/qml/states.d.ts +5 -0
  196. package/dist/qml/states.d.ts.map +1 -0
  197. package/dist/qml/states.js +276 -0
  198. package/dist/qml/states.js.map +1 -0
  199. package/dist/qml/view-utils.d.ts +42 -0
  200. package/dist/qml/view-utils.d.ts.map +1 -0
  201. package/dist/qml/view-utils.js +121 -0
  202. package/dist/qml/view-utils.js.map +1 -0
  203. package/dist/reorder/hx.d.ts +18 -0
  204. package/dist/reorder/hx.d.ts.map +1 -0
  205. package/dist/reorder/hx.js +10 -0
  206. package/dist/reorder/hx.js.map +1 -0
  207. package/dist/reorder/index.d.ts +314 -0
  208. package/dist/reorder/index.d.ts.map +1 -0
  209. package/dist/reorder/index.js +1539 -0
  210. package/dist/reorder/index.js.map +1 -0
  211. package/dist/reorder/model.d.ts +107 -0
  212. package/dist/reorder/model.d.ts.map +1 -0
  213. package/dist/reorder/model.js +162 -0
  214. package/dist/reorder/model.js.map +1 -0
  215. package/dist/richtext/node.d.ts +15 -5
  216. package/dist/richtext/node.d.ts.map +1 -1
  217. package/dist/richtext/node.js +40 -11
  218. package/dist/richtext/node.js.map +1 -1
  219. package/dist/richtext/runs.d.ts +35 -13
  220. package/dist/richtext/runs.d.ts.map +1 -1
  221. package/dist/richtext/runs.js +83 -27
  222. package/dist/richtext/runs.js.map +1 -1
  223. package/dist/tabs/hx.d.ts +18 -0
  224. package/dist/tabs/hx.d.ts.map +1 -0
  225. package/dist/tabs/hx.js +10 -0
  226. package/dist/tabs/hx.js.map +1 -0
  227. package/dist/tabs/index.d.ts +160 -0
  228. package/dist/tabs/index.d.ts.map +1 -0
  229. package/dist/tabs/index.js +1265 -0
  230. package/dist/tabs/index.js.map +1 -0
  231. package/dist/terminal/vt/fonts.d.ts +45 -15
  232. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  233. package/dist/terminal/vt/fonts.js +89 -19
  234. package/dist/terminal/vt/fonts.js.map +1 -1
  235. package/dist/terminal/vt/index.d.ts +1 -1
  236. package/dist/terminal/vt/index.d.ts.map +1 -1
  237. package/dist/terminal/vt/index.js +3 -3
  238. package/dist/terminal/vt/index.js.map +1 -1
  239. package/dist/terminal/vt/node.d.ts +42 -0
  240. package/dist/terminal/vt/node.d.ts.map +1 -1
  241. package/dist/terminal/vt/node.js +95 -11
  242. package/dist/terminal/vt/node.js.map +1 -1
  243. package/dist/terminal/vt/pty.d.ts +16 -0
  244. package/dist/terminal/vt/pty.d.ts.map +1 -1
  245. package/dist/terminal/vt/pty.js +186 -0
  246. package/dist/terminal/vt/pty.js.map +1 -1
  247. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  248. package/dist/terminal/vt/renderer.js +13 -3
  249. package/dist/terminal/vt/renderer.js.map +1 -1
  250. package/package.json +23 -3
  251. package/src/charts/components.ts +14 -7
  252. package/src/charts/node.ts +81 -26
  253. package/src/charts/render.ts +16 -6
  254. package/src/code-editor/node.ts +147 -53
  255. package/src/flow/draw.ts +104 -35
  256. package/src/flow/index.ts +33 -13
  257. package/src/flow/model.ts +4 -2
  258. package/src/flow/node.ts +158 -59
  259. package/src/flow/types.ts +46 -21
  260. package/src/formula/index.ts +4 -2
  261. package/src/formula/node.ts +24 -5
  262. package/src/html/controls.ts +4 -1
  263. package/src/html/css/cascade.ts +18 -7
  264. package/src/html/css/parse.ts +46 -21
  265. package/src/html/css/style.ts +6 -1
  266. package/src/html/css/values.ts +25 -9
  267. package/src/html/index.ts +1 -0
  268. package/src/html/layout/boxes.ts +19 -9
  269. package/src/html/layout/inline.ts +23 -1
  270. package/src/html/node.ts +110 -17
  271. package/src/html/paint.ts +17 -2
  272. package/src/index.ts +142 -0
  273. package/src/maps/clip.ts +184 -0
  274. package/src/maps/gzip.ts +66 -0
  275. package/src/maps/index.ts +316 -0
  276. package/src/maps/labels.ts +442 -0
  277. package/src/maps/mvt.ts +857 -0
  278. package/src/maps/node.ts +1819 -0
  279. package/src/maps/overlay.ts +639 -0
  280. package/src/maps/paint.ts +978 -0
  281. package/src/maps/proj.ts +641 -0
  282. package/src/maps/sources.ts +425 -0
  283. package/src/maps/style.ts +309 -0
  284. package/src/maps/styles.ts +1040 -0
  285. package/src/maps/tiles.ts +676 -0
  286. package/src/maps/types.ts +260 -0
  287. package/src/qml/containers.tsx +380 -0
  288. package/src/qml/controls.tsx +47 -0
  289. package/src/qml/globals.ts +33 -0
  290. package/src/qml/index.ts +67 -0
  291. package/src/qml/interaction.tsx +317 -0
  292. package/src/qml/ir.ts +80 -0
  293. package/src/qml/layouts.tsx +141 -0
  294. package/src/qml/models.ts +101 -0
  295. package/src/qml/objects.ts +1431 -0
  296. package/src/qml/parse.ts +624 -0
  297. package/src/qml/qtquick.tsx +413 -0
  298. package/src/qml/react.tsx +454 -0
  299. package/src/qml/resolver.ts +99 -0
  300. package/src/qml/slots.ts +249 -0
  301. package/src/qml/states.ts +318 -0
  302. package/src/qml/view-utils.ts +140 -0
  303. package/src/reorder/hx.ts +31 -0
  304. package/src/reorder/index.ts +2223 -0
  305. package/src/reorder/model.ts +232 -0
  306. package/src/richtext/node.ts +49 -14
  307. package/src/richtext/runs.ts +115 -30
  308. package/src/tabs/hx.ts +31 -0
  309. package/src/tabs/index.ts +1686 -0
  310. package/src/terminal/vt/fonts.ts +128 -36
  311. package/src/terminal/vt/index.ts +4 -2
  312. package/src/terminal/vt/node.ts +105 -12
  313. package/src/terminal/vt/pty.ts +243 -0
  314. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,676 @@
1
+ // The tile cache: what has been loaded, what has been drawn, and what may
2
+ // be thrown away.
3
+ //
4
+ // Two caches in one, because they are invalidated by different things, are
5
+ // keyed on different tiles, and a map that conflated them would redraw far
6
+ // too much:
7
+ //
8
+ // - **Data** — the parsed {@link VectorTile}, or a raster tile's pixels.
9
+ // Keyed on the tile the *source* cuts and valid forever: a tile's
10
+ // contents do not depend on where the camera is. Past a source's
11
+ // `maxZoom` many renderings share one of these — 256 zoom-20 tiles are
12
+ // one zoom-14 fetch — which is what makes overzoom sharp instead of a
13
+ // stretched bitmap.
14
+ // - **A rendered `Surface`** — the tile's features rasterized once, at a
15
+ // size, in a style. Also independent of the camera's *position*, which
16
+ // is the whole point: panning composites the same surfaces at new
17
+ // offsets and rasterizes nothing, and a fractional zoom composites them
18
+ // scaled. Only an integer zoom change, a style change or a display-scale
19
+ // change invalidates one.
20
+ //
21
+ // The surface is the expensive half — on the corpus in `scripts/bench/`, a
22
+ // dense city tile is 50-140 ms to rasterize and about 0.05 ms to composite
23
+ // — so the cache's job is to make sure that cost is paid once per tile per
24
+ // zoom level, and never during a gesture.
25
+ import type { PreparedStyle } from './paint.js';
26
+ import type { VectorTile } from './mvt.js';
27
+ import { parseTile } from './mvt.js';
28
+ import { tileKey } from './proj.js';
29
+ import type { TileId } from './proj.js';
30
+ import { pyramidOf } from './sources.js';
31
+ import type { MapSource, TileData } from './sources.js';
32
+
33
+ /** The slice of `react-x11/ntk`'s `Surface` this uses. Structural because
34
+ * the same object is an X pixmap on one backend and a CoreGraphics bitmap
35
+ * on the other, and neither name appears here. */
36
+ export interface SurfaceLike {
37
+ readonly width: number;
38
+ readonly height: number;
39
+ getContext(name: '2d', ...args: unknown[]): unknown;
40
+ clear(): unknown;
41
+ destroy(): void;
42
+ }
43
+
44
+ /** How a surface is made — handed in rather than imported, so a test can
45
+ * run the whole cache with no display. */
46
+ export type SurfaceFactory = (size: number) => SurfaceLike | null;
47
+
48
+ /** Where a tile has got to. */
49
+ export type TileStatus =
50
+ /** Never asked for. */
51
+ | 'idle'
52
+ /** The source was asked and has not answered. */
53
+ | 'loading'
54
+ /** Data is here. */
55
+ | 'ready'
56
+ /** The source said there is nothing here — an ordinary answer, not a
57
+ * failure, and the reason a map over an ocean is not full of errors. */
58
+ | 'empty'
59
+ /** The source threw. Retried on the next camera change that wants it. */
60
+ | 'error';
61
+
62
+ /**
63
+ * One rendering of a tile: a surface, the context that draws into it, and
64
+ * what it was drawn for.
65
+ *
66
+ * A tile has up to two — the one on screen and the one being drawn — which
67
+ * is what stops a re-rasterization blanking the map. See
68
+ * {@link CachedTile.shown}.
69
+ */
70
+ export interface TileRender {
71
+ surface: SurfaceLike;
72
+ /** Held for the life of the surface rather than made per frame: a context
73
+ * is a real resource on X11 (a GC, a Picture and a listener on the
74
+ * pixmap), and rasterization is resumable, so a tile that fills in over a
75
+ * dozen frames would otherwise make and destroy a dozen of them. */
76
+ context: unknown;
77
+ /** Edge length in device pixels. */
78
+ size: number;
79
+ /** The zoom its style was resolved at. */
80
+ zoom: number;
81
+ /** The cache generation it was drawn under. */
82
+ generation: number;
83
+ /** The next style run to draw, or `-1` when it is finished. */
84
+ progress: number;
85
+ /** …and how far into that run's active layers, because a run is not a
86
+ * small enough unit on its own: a road network is one run of fourteen
87
+ * layers and one of those layers alone measured 90 ms. */
88
+ progressLayer: number;
89
+ }
90
+
91
+ /**
92
+ * What a source answered for one tile, shared by every rendering of it.
93
+ *
94
+ * Separate from {@link CachedTile} because past a source's `maxZoom` the
95
+ * two are not the same tile: a zoom-20 rendering draws a zoom-14 tile's
96
+ * data, and its two hundred and fifty-five neighbours draw the same one.
97
+ */
98
+ export interface TileDataEntry {
99
+ readonly key: string;
100
+ readonly tile: TileId;
101
+ status: TileStatus;
102
+ error: unknown;
103
+ vector: VectorTile | null;
104
+ raster: { width: number; height: number; data: Uint8Array } | null;
105
+ /** Cancels an in-flight load. */
106
+ abort: (() => void) | null;
107
+ /** How many times this tile's load has failed in a row. */
108
+ attempts: number;
109
+ /** `Date.now()` before which a failed tile is not asked for again. */
110
+ retryAt: number;
111
+ /** Frame counter of the last frame that wanted it. */
112
+ lastUsed: number;
113
+ }
114
+
115
+ /**
116
+ * One tile as the cover asked for it, and its renderings.
117
+ *
118
+ * The data behind it is `data`, which is this tile until the cover goes
119
+ * deeper than the source does. The status accessors read through, so
120
+ * everything that only wants "is there anything to draw" reads the same
121
+ * names it always did.
122
+ */
123
+ export class CachedTile {
124
+ readonly key: string;
125
+ readonly sourceId: string;
126
+ readonly tile: TileId;
127
+ /** The source tile whose bytes this draws. Itself, or an ancestor. */
128
+ readonly data: TileDataEntry;
129
+ /** Where this tile sits inside `data`: cell `(x, y)` of `span × span`. */
130
+ readonly sub: { x: number; y: number; span: number };
131
+
132
+ /**
133
+ * The rendering that is on screen. **Always finished**, which is the
134
+ * whole point of the pair.
135
+ *
136
+ * A tile is re-rasterized whenever its size, its style zoom or the cache
137
+ * generation moves. Drawn in place, each of those clears the surface and
138
+ * leaves the tile blank for the six or so frames it takes to redraw: a
139
+ * visible flash, and many more of them on a backend that paints more
140
+ * frames. So the new rendering goes into {@link drawing} and this one
141
+ * keeps being composited until it is ready to be replaced.
142
+ */
143
+ shown: TileRender | null = null;
144
+ /** The rendering being drawn, or null when there is nothing to draw. */
145
+ drawing: TileRender | null = null;
146
+ /** Frame counter of the last frame that wanted it — what eviction sorts
147
+ * on. */
148
+ lastUsed = 0;
149
+
150
+ constructor(
151
+ key: string,
152
+ sourceId: string,
153
+ tile: TileId,
154
+ data: TileDataEntry,
155
+ sub: { x: number; y: number; span: number },
156
+ ) {
157
+ this.key = key;
158
+ this.sourceId = sourceId;
159
+ this.tile = tile;
160
+ this.data = data;
161
+ this.sub = sub;
162
+ }
163
+
164
+ get status(): TileStatus {
165
+ return this.data.status;
166
+ }
167
+ get error(): unknown {
168
+ return this.data.error;
169
+ }
170
+ get vector(): VectorTile | null {
171
+ return this.data.vector;
172
+ }
173
+ get raster(): { width: number; height: number; data: Uint8Array } | null {
174
+ return this.data.raster;
175
+ }
176
+ }
177
+
178
+ export interface TileCacheOptions {
179
+ /**
180
+ * How many bytes of rendered surfaces to keep. An argb32 surface is
181
+ * `4 × size²`, so a 1024-pixel tile is 4 MB and a retina screenful is
182
+ * about ten of them; the default keeps roughly three screenfuls, which is
183
+ * what makes zooming out and back in free.
184
+ */
185
+ surfaceBudget?: number;
186
+ /** How many tiles' worth of parsed data to keep. Cheap next to the
187
+ * surfaces (a dense tile is a few hundred KB) and worth keeping longer,
188
+ * because re-parsing is 5-12 ms. */
189
+ dataBudget?: number;
190
+ /** Called when a load finishes, so the element can ask for a repaint. */
191
+ onChange?: (entry: TileDataEntry) => void;
192
+ /** Called once per failed load. A map whose tiles all fail looks exactly
193
+ * like a map that is still loading, so somebody has to be told. */
194
+ onError?: (entry: TileDataEntry) => void;
195
+ }
196
+
197
+ const DEFAULT_SURFACE_BUDGET = 128 * 1024 * 1024;
198
+ const DEFAULT_DATA_BUDGET = 192;
199
+
200
+ /**
201
+ * The cache.
202
+ *
203
+ * One per element. Nothing in it knows about the camera: it is asked for
204
+ * tiles by name, and told once a frame which ones are still wanted.
205
+ */
206
+ export class TileCache {
207
+ private readonly _entries = new Map<string, CachedTile>();
208
+ /** The source's own tiles, shared by every rendering that draws one. */
209
+ private readonly _data = new Map<string, TileDataEntry>();
210
+ private readonly _options: TileCacheOptions;
211
+ private _frame = 0;
212
+ private _surfaceBytes = 0;
213
+ /** Bumped by a style change, which retires every surface without
214
+ * touching the data behind it. */
215
+ private _generation = 0;
216
+
217
+ constructor(options: TileCacheOptions = {}) {
218
+ this._options = options;
219
+ }
220
+
221
+ /** Bumped once per paint; `lastUsed` is stamped from it. */
222
+ beginFrame(): number {
223
+ return ++this._frame;
224
+ }
225
+
226
+ get generation(): number {
227
+ return this._generation;
228
+ }
229
+
230
+ /** Every rendered surface is stale — a new style, a new display scale. */
231
+ invalidateStyle(): void {
232
+ this._generation++;
233
+ }
234
+
235
+ /** How much the rendered surfaces are holding, in bytes. */
236
+ get surfaceBytes(): number {
237
+ return this._surfaceBytes;
238
+ }
239
+
240
+ get size(): number {
241
+ return this._entries.size;
242
+ }
243
+
244
+ key(sourceId: string, tile: TileId): string {
245
+ return `${sourceId}:${tileKey(tile)}`;
246
+ }
247
+
248
+ peek(sourceId: string, tile: TileId): CachedTile | undefined {
249
+ return this._entries.get(this.key(sourceId, tile));
250
+ }
251
+
252
+ /**
253
+ * The entry for a tile the cover asked for, and the data behind it,
254
+ * asking the source for that data if nothing has yet.
255
+ *
256
+ * `dataTile` is the tile whose bytes serve this one — itself, until the
257
+ * cover goes deeper than the source cuts. Marks both used by the current
258
+ * frame, which is what keeps them out of the next eviction.
259
+ */
260
+ want(
261
+ source: MapSource,
262
+ sourceId: string,
263
+ tile: TileId,
264
+ dataTile: TileId = tile,
265
+ sub: { x: number; y: number; span: number } = { x: 0, y: 0, span: 1 },
266
+ ): CachedTile {
267
+ const data = this._wantData(source, sourceId, dataTile);
268
+ const key = this.key(sourceId, tile);
269
+ let entry = this._entries.get(key);
270
+ if (!entry) {
271
+ entry = new CachedTile(key, sourceId, tile, data, sub);
272
+ this._entries.set(key, entry);
273
+ }
274
+ entry.lastUsed = this._frame;
275
+ return entry;
276
+ }
277
+
278
+ private _wantData(
279
+ source: MapSource,
280
+ sourceId: string,
281
+ tile: TileId,
282
+ ): TileDataEntry {
283
+ const key = this.key(sourceId, tile);
284
+ let data = this._data.get(key);
285
+ if (!data) {
286
+ data = {
287
+ key,
288
+ tile,
289
+ status: 'idle',
290
+ error: null,
291
+ vector: null,
292
+ raster: null,
293
+ abort: null,
294
+ attempts: 0,
295
+ retryAt: 0,
296
+ lastUsed: 0,
297
+ };
298
+ this._data.set(key, data);
299
+ }
300
+ data.lastUsed = this._frame;
301
+ if (data.status === 'idle') {
302
+ this._load(source, sourceId, data);
303
+ } else if (data.status === 'error' && Date.now() >= data.retryAt) {
304
+ // A failed tile is retried, but on a backoff rather than on every
305
+ // frame. Without it a source that is down — or an application whose
306
+ // `load` throws on the first call, which is how this was found — is
307
+ // asked for every visible tile sixty times a second, which is a
308
+ // retry storm pointed at somebody else's servers.
309
+ this._load(source, sourceId, data);
310
+ }
311
+ return data;
312
+ }
313
+
314
+ private _load(
315
+ source: MapSource,
316
+ sourceId: string,
317
+ entry: TileDataEntry,
318
+ ): void {
319
+ entry.status = 'loading';
320
+ entry.error = null;
321
+ // A **real** `AbortController` where the runtime has one. A look-alike
322
+ // is not good enough: `fetch` checks `instanceof AbortSignal` and
323
+ // throws `TypeError` on anything else, so handing a source a plain
324
+ // object with an `aborted` getter fails every load a source makes the
325
+ // documented way — which is exactly what it did.
326
+ const Controller = (
327
+ globalThis as {
328
+ AbortController?: new () => {
329
+ abort(): void;
330
+ signal: { readonly aborted: boolean };
331
+ };
332
+ }
333
+ ).AbortController;
334
+ let aborted = false;
335
+ const controller = Controller ? new Controller() : null;
336
+ const signal = controller
337
+ ? controller.signal
338
+ : {
339
+ get aborted(): boolean {
340
+ return aborted;
341
+ },
342
+ };
343
+ entry.abort = (): void => {
344
+ aborted = true;
345
+ controller?.abort();
346
+ };
347
+ const fail = (error: unknown): void => {
348
+ entry.status = 'error';
349
+ entry.error = error;
350
+ entry.attempts++;
351
+ // 0.5s, 1, 2, 4, 8, 16, then 30s — long enough that a dead source
352
+ // costs nothing, short enough that a blip repairs itself.
353
+ entry.retryAt =
354
+ Date.now() + Math.min(30_000, 500 * 2 ** (entry.attempts - 1));
355
+ };
356
+ const settle = (fn: () => void): void => {
357
+ // A load that finished after its tile was evicted must not resurrect
358
+ // it: the entry is gone from the map, so writing to it would leak a
359
+ // surface nothing composites.
360
+ if (aborted || this._data.get(entry.key) !== entry) return;
361
+ entry.abort = null;
362
+ fn();
363
+ if (entry.status === 'error') this._options.onError?.(entry);
364
+ this._options.onChange?.(entry);
365
+ };
366
+ let result: TileData | Promise<TileData>;
367
+ try {
368
+ result = source.load({ ...entry.tile, sourceId, signal });
369
+ } catch (error) {
370
+ settle(() => fail(error));
371
+ return;
372
+ }
373
+ const accept = (data: TileData): void =>
374
+ settle(() => {
375
+ entry.attempts = 0;
376
+ entry.retryAt = 0;
377
+ if (data === null) {
378
+ entry.status = 'empty';
379
+ return;
380
+ }
381
+ if (data.kind === 'vector') {
382
+ try {
383
+ entry.vector = parseTile(data.data);
384
+ entry.status = 'ready';
385
+ } catch (error) {
386
+ fail(error);
387
+ }
388
+ return;
389
+ }
390
+ entry.raster = data;
391
+ entry.status = 'ready';
392
+ });
393
+ if (result && typeof (result as Promise<TileData>).then === 'function') {
394
+ (result as Promise<TileData>).then(accept, (error: unknown) =>
395
+ settle(() => fail(error)),
396
+ );
397
+ } else {
398
+ accept(result as TileData);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * The nearest ancestor of `tile` that already has a drawn surface, or
404
+ * null.
405
+ *
406
+ * What covers a hole. A zoom in has the previous level's tiles in hand
407
+ * and the new level's on the way, so the coarse picture is scaled up
408
+ * under the fine one until it arrives — which is the difference between a
409
+ * map that fills in and a map that flashes empty. Bounded, because
410
+ * scaling one tile over sixteen screenfuls is worse than the hole.
411
+ */
412
+ ancestorWithSurface(
413
+ sourceId: string,
414
+ tile: TileId,
415
+ levels = 5,
416
+ ): CachedTile | null {
417
+ let z = tile.z - 1;
418
+ let x = tile.x >> 1;
419
+ let y = tile.y >> 1;
420
+ for (let up = 0; up < levels && z >= 0; up++, z--, x >>= 1, y >>= 1) {
421
+ const entry = this._entries.get(this.key(sourceId, { z, x, y }));
422
+ // `shown` is finished by construction, so an ancestor is never a
423
+ // half-drawn picture.
424
+ if (entry?.shown) {
425
+ // Stamped, because for as long as it is covering a hole it is on
426
+ // screen — and an unstamped entry is the *first* thing eviction
427
+ // takes, which would drop the only picture the map is showing.
428
+ entry.lastUsed = this._frame;
429
+ return entry;
430
+ }
431
+ }
432
+ return null;
433
+ }
434
+
435
+ /**
436
+ * The finished **descendants** of a tile, and where each sits inside it.
437
+ *
438
+ * The mirror of {@link ancestorWithSurface}, and the half a map needs
439
+ * when it zooms *out*: the tiles already in hand are then the target's
440
+ * children, and walking up the pyramid finds nothing. Without this a
441
+ * zoom-out shows the background — with the labels and markers still drawn
442
+ * over it, which is exactly as odd as it sounds — until the coarser tile
443
+ * has been fetched, rasterized and composited.
444
+ *
445
+ * `depth` is how many levels down to look, and it is small on purpose: a
446
+ * level down is 4 tiles, two is 16, and beyond that the pieces are too
447
+ * small to be worth the composites.
448
+ */
449
+ descendantsWithSurface(
450
+ sourceId: string,
451
+ tile: TileId,
452
+ depth = 2,
453
+ ): { entry: CachedTile; x: number; y: number; span: number }[] {
454
+ for (let down = 1; down <= depth; down++) {
455
+ const span = 1 << down;
456
+ const found: { entry: CachedTile; x: number; y: number; span: number }[] =
457
+ [];
458
+ for (let dy = 0; dy < span; dy++) {
459
+ for (let dx = 0; dx < span; dx++) {
460
+ const entry = this._entries.get(
461
+ this.key(sourceId, {
462
+ z: tile.z + down,
463
+ x: (tile.x << down) + dx,
464
+ y: (tile.y << down) + dy,
465
+ }),
466
+ );
467
+ if (entry?.shown) {
468
+ entry.lastUsed = this._frame;
469
+ found.push({ entry, x: dx, y: dy, span });
470
+ }
471
+ }
472
+ }
473
+ // The nearest level that has anything wins: one level down is both
474
+ // the sharpest and the fewest composites.
475
+ if (found.length > 0) return found;
476
+ }
477
+ return [];
478
+ }
479
+
480
+ /**
481
+ * The rendering this tile should be drawn into, or null when what is on
482
+ * screen is already right.
483
+ *
484
+ * The size, the zoom and the cache generation are all part of what makes
485
+ * a rendering valid: one drawn at the wrong size composites blurry, and
486
+ * one drawn at another zoom carries the wrong road widths and the wrong
487
+ * layers.
488
+ *
489
+ * When something is already on screen and a *new* rendering is needed,
490
+ * this hands back a second surface and leaves the first one composited
491
+ * until {@link promote} swaps them. That is the whole of the
492
+ * double-buffering: without it a re-rasterization clears the surface and
493
+ * the tile is blank for the several frames it takes to redraw, which
494
+ * above a source's `maxZoom` happens at every integer zoom — the same
495
+ * z14 tile serves 15, 16, 17 and on.
496
+ */
497
+ beginRender(
498
+ entry: CachedTile,
499
+ size: number,
500
+ zoom: number,
501
+ make: SurfaceFactory,
502
+ ): TileRender | null {
503
+ if (this._matches(entry.shown, size, zoom)) return null;
504
+ if (this._matches(entry.drawing, size, zoom)) return entry.drawing;
505
+ // A draft for something else — an intermediate zoom the camera swept
506
+ // past — is thrown away rather than finished.
507
+ if (entry.drawing) this._release(entry.drawing);
508
+ entry.drawing = null;
509
+ const surface = make(size);
510
+ if (!surface) return null;
511
+ this._surfaceBytes += size * size * 4;
512
+ entry.drawing = {
513
+ surface,
514
+ context: surface.getContext('2d'),
515
+ size,
516
+ zoom,
517
+ generation: this._generation,
518
+ progress: 0,
519
+ progressLayer: 0,
520
+ };
521
+ return entry.drawing;
522
+ }
523
+
524
+ private _matches(
525
+ render: TileRender | null,
526
+ size: number,
527
+ zoom: number,
528
+ ): boolean {
529
+ return (
530
+ render !== null &&
531
+ render.size === size &&
532
+ render.zoom === zoom &&
533
+ render.generation === this._generation
534
+ );
535
+ }
536
+
537
+ /** The finished draft becomes what is on screen, and the old one goes.
538
+ * True when that happened, which is the one moment a tile's pixels
539
+ * change and therefore the one moment worth claiming damage for. */
540
+ promote(entry: CachedTile): boolean {
541
+ if (!entry.drawing || entry.drawing.progress !== -1) return false;
542
+ if (entry.shown) this._release(entry.shown);
543
+ entry.shown = entry.drawing;
544
+ entry.drawing = null;
545
+ return true;
546
+ }
547
+
548
+ /** Note where a tile's rasterization stopped: at run `run`, having
549
+ * finished `layer` of its active layers (`-1` for all of them). */
550
+ advance(
551
+ render: TileRender,
552
+ run: number,
553
+ layer: number,
554
+ style: PreparedStyle,
555
+ ): void {
556
+ render.progress = run >= style.runs.length ? -1 : run;
557
+ render.progressLayer = layer < 0 ? 0 : layer;
558
+ }
559
+
560
+ /**
561
+ * Drop what is no longer worth keeping.
562
+ *
563
+ * Surfaces go first and by least-recently-wanted, because they are two
564
+ * orders of magnitude larger than the data behind them and much cheaper
565
+ * to lose: a dropped surface is one re-rasterization, a dropped tile is a
566
+ * network request.
567
+ */
568
+ sweep(): void {
569
+ if (
570
+ this._surfaceBytes >
571
+ (this._options.surfaceBudget ?? DEFAULT_SURFACE_BUDGET)
572
+ ) {
573
+ const withSurfaces = [...this._entries.values()]
574
+ .filter((entry) => entry.shown !== null || entry.drawing !== null)
575
+ .sort((a, b) => a.lastUsed - b.lastUsed);
576
+ const budget = this._options.surfaceBudget ?? DEFAULT_SURFACE_BUDGET;
577
+ for (const entry of withSurfaces) {
578
+ if (this._surfaceBytes <= budget) break;
579
+ // Never the frame being drawn: a surface evicted this frame is one
580
+ // that will be rebuilt this frame.
581
+ if (entry.lastUsed >= this._frame) continue;
582
+ this._dropSurface(entry);
583
+ }
584
+ }
585
+ const dataBudget = this._options.dataBudget ?? DEFAULT_DATA_BUDGET;
586
+ if (this._entries.size > dataBudget) {
587
+ const all = [...this._entries.values()].sort(
588
+ (a, b) => a.lastUsed - b.lastUsed,
589
+ );
590
+ for (const entry of all) {
591
+ if (this._entries.size <= dataBudget) break;
592
+ if (entry.lastUsed >= this._frame) continue;
593
+ this._drop(entry);
594
+ }
595
+ }
596
+ if (this._data.size > dataBudget) {
597
+ const all = [...this._data.values()].sort(
598
+ (a, b) => a.lastUsed - b.lastUsed,
599
+ );
600
+ for (const data of all) {
601
+ if (this._data.size <= dataBudget) break;
602
+ if (data.lastUsed >= this._frame) continue;
603
+ this._dropData(data);
604
+ }
605
+ }
606
+ }
607
+
608
+ private _dropSurface(entry: CachedTile): void {
609
+ if (entry.shown) this._release(entry.shown);
610
+ if (entry.drawing) this._release(entry.drawing);
611
+ entry.shown = null;
612
+ entry.drawing = null;
613
+ }
614
+
615
+ private _release(render: TileRender): void {
616
+ this._surfaceBytes -= render.size * render.size * 4;
617
+ (render.context as { destroy?(): void } | null)?.destroy?.();
618
+ render.surface.destroy();
619
+ }
620
+
621
+ private _drop(entry: CachedTile): void {
622
+ this._dropSurface(entry);
623
+ this._entries.delete(entry.key);
624
+ }
625
+
626
+ /** Drop a source tile's bytes, and abort a load still in flight for it.
627
+ * Only when no rendering still points at it. */
628
+ private _dropData(data: TileDataEntry): void {
629
+ for (const entry of this._entries.values()) {
630
+ if (entry.data === data) return;
631
+ }
632
+ data.abort?.();
633
+ this._data.delete(data.key);
634
+ }
635
+
636
+ /** Release everything. Called from the element's `destroySubtree`. */
637
+ destroy(): void {
638
+ for (const entry of [...this._entries.values()]) this._drop(entry);
639
+ this._entries.clear();
640
+ for (const data of this._data.values()) data.abort?.();
641
+ this._data.clear();
642
+ this._surfaceBytes = 0;
643
+ }
644
+
645
+ /** Every entry — for a test, and for the `onTiles` diagnostic. */
646
+ entries(): IterableIterator<CachedTile> {
647
+ return this._entries.values();
648
+ }
649
+
650
+ /** Every source tile whose bytes are held. What labels are collected
651
+ * from: a label belongs to the tile the data came from, and past a
652
+ * source's depth many renderings share one of those. */
653
+ dataEntries(): IterableIterator<TileDataEntry> {
654
+ return this._data.values();
655
+ }
656
+ }
657
+
658
+ /** A source's pyramid, memoized per source object so the defaults are not
659
+ * refilled per frame. */
660
+ const pyramids = new WeakMap<
661
+ MapSource,
662
+ { minZoom: number; maxZoom: number; tileSize: number }
663
+ >();
664
+
665
+ export function pyramid(source: MapSource): {
666
+ minZoom: number;
667
+ maxZoom: number;
668
+ tileSize: number;
669
+ } {
670
+ let found = pyramids.get(source);
671
+ if (!found) {
672
+ found = pyramidOf(source);
673
+ pyramids.set(source, found);
674
+ }
675
+ return found;
676
+ }