@react-x11/components 0.7.1 → 0.9.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 (354) hide show
  1. package/README.md +295 -51
  2. package/dist/charts/node.d.ts +1 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +6 -2
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/color-picker/ColorPicker.d.ts.map +1 -1
  7. package/dist/color-picker/ColorPicker.js +43 -33
  8. package/dist/color-picker/ColorPicker.js.map +1 -1
  9. package/dist/embed/client.d.ts +48 -3
  10. package/dist/embed/client.d.ts.map +1 -1
  11. package/dist/embed/client.js +79 -5
  12. package/dist/embed/client.js.map +1 -1
  13. package/dist/embed/index.d.ts +1 -1
  14. package/dist/embed/index.d.ts.map +1 -1
  15. package/dist/embed/index.js +1 -1
  16. package/dist/embed/index.js.map +1 -1
  17. package/dist/html/node.d.ts +3 -1
  18. package/dist/html/node.d.ts.map +1 -1
  19. package/dist/html/node.js +7 -5
  20. package/dist/html/node.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
  26. package/dist/internal/markdown/ast.d.ts.map +1 -0
  27. package/dist/internal/markdown/ast.js +22 -0
  28. package/dist/internal/markdown/ast.js.map +1 -0
  29. package/dist/internal/markdown/parse.d.ts.map +1 -0
  30. package/dist/{markdown → internal/markdown}/parse.js +112 -22
  31. package/dist/internal/markdown/parse.js.map +1 -0
  32. package/dist/internal/markdown/stringify.d.ts +4 -0
  33. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  34. package/dist/internal/markdown/stringify.js +684 -0
  35. package/dist/internal/markdown/stringify.js.map +1 -0
  36. package/dist/internal/markdown/tags.d.ts.map +1 -0
  37. package/dist/internal/markdown/tags.js.map +1 -0
  38. package/dist/internal/scroll.d.ts +3 -2
  39. package/dist/internal/scroll.d.ts.map +1 -1
  40. package/dist/internal/scroll.js +33 -15
  41. package/dist/internal/scroll.js.map +1 -1
  42. package/dist/internal/units.d.ts +2 -0
  43. package/dist/internal/units.d.ts.map +1 -0
  44. package/dist/internal/units.js +35 -0
  45. package/dist/internal/units.js.map +1 -0
  46. package/dist/internal/window.d.ts.map +1 -1
  47. package/dist/internal/window.js +4 -2
  48. package/dist/internal/window.js.map +1 -1
  49. package/dist/maps/anchors.d.ts +98 -0
  50. package/dist/maps/anchors.d.ts.map +1 -0
  51. package/dist/maps/anchors.js +531 -0
  52. package/dist/maps/anchors.js.map +1 -0
  53. package/dist/maps/controller.d.ts +283 -0
  54. package/dist/maps/controller.d.ts.map +1 -0
  55. package/dist/maps/controller.js +743 -0
  56. package/dist/maps/controller.js.map +1 -0
  57. package/dist/maps/gl/buckets.d.ts +95 -0
  58. package/dist/maps/gl/buckets.d.ts.map +1 -0
  59. package/dist/maps/gl/buckets.js +362 -0
  60. package/dist/maps/gl/buckets.js.map +1 -0
  61. package/dist/maps/gl/build-worker.d.ts +13 -0
  62. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  63. package/dist/maps/gl/build-worker.js +44 -0
  64. package/dist/maps/gl/build-worker.js.map +1 -0
  65. package/dist/maps/gl/color.d.ts +8 -0
  66. package/dist/maps/gl/color.d.ts.map +1 -0
  67. package/dist/maps/gl/color.js +57 -0
  68. package/dist/maps/gl/color.js.map +1 -0
  69. package/dist/maps/gl/cover.d.ts +51 -0
  70. package/dist/maps/gl/cover.d.ts.map +1 -0
  71. package/dist/maps/gl/cover.js +124 -0
  72. package/dist/maps/gl/cover.js.map +1 -0
  73. package/dist/maps/gl/index.d.ts +3 -0
  74. package/dist/maps/gl/index.d.ts.map +1 -0
  75. package/dist/maps/gl/index.js +20 -0
  76. package/dist/maps/gl/index.js.map +1 -0
  77. package/dist/maps/gl/markers.d.ts +28 -0
  78. package/dist/maps/gl/markers.d.ts.map +1 -0
  79. package/dist/maps/gl/markers.js +60 -0
  80. package/dist/maps/gl/markers.js.map +1 -0
  81. package/dist/maps/gl/overlays.d.ts +68 -0
  82. package/dist/maps/gl/overlays.d.ts.map +1 -0
  83. package/dist/maps/gl/overlays.js +300 -0
  84. package/dist/maps/gl/overlays.js.map +1 -0
  85. package/dist/maps/gl/pane.d.ts +12 -0
  86. package/dist/maps/gl/pane.d.ts.map +1 -0
  87. package/dist/maps/gl/pane.js +37 -0
  88. package/dist/maps/gl/pane.js.map +1 -0
  89. package/dist/maps/gl/placement.d.ts +111 -0
  90. package/dist/maps/gl/placement.d.ts.map +1 -0
  91. package/dist/maps/gl/placement.js +567 -0
  92. package/dist/maps/gl/placement.js.map +1 -0
  93. package/dist/maps/gl/renderer.d.ts +352 -0
  94. package/dist/maps/gl/renderer.d.ts.map +1 -0
  95. package/dist/maps/gl/renderer.js +1127 -0
  96. package/dist/maps/gl/renderer.js.map +1 -0
  97. package/dist/maps/gl/shaders.d.ts +120 -0
  98. package/dist/maps/gl/shaders.d.ts.map +1 -0
  99. package/dist/maps/gl/shaders.js +451 -0
  100. package/dist/maps/gl/shaders.js.map +1 -0
  101. package/dist/maps/gl/store.d.ts +115 -0
  102. package/dist/maps/gl/store.d.ts.map +1 -0
  103. package/dist/maps/gl/store.js +469 -0
  104. package/dist/maps/gl/store.js.map +1 -0
  105. package/dist/maps/gl/text.d.ts +154 -0
  106. package/dist/maps/gl/text.d.ts.map +1 -0
  107. package/dist/maps/gl/text.js +382 -0
  108. package/dist/maps/gl/text.js.map +1 -0
  109. package/dist/maps/gl/view.d.ts +54 -0
  110. package/dist/maps/gl/view.d.ts.map +1 -0
  111. package/dist/maps/gl/view.js +974 -0
  112. package/dist/maps/gl/view.js.map +1 -0
  113. package/dist/maps/index.d.ts +1 -1
  114. package/dist/maps/index.d.ts.map +1 -1
  115. package/dist/maps/index.js +216 -52
  116. package/dist/maps/index.js.map +1 -1
  117. package/dist/maps/labels.d.ts +37 -16
  118. package/dist/maps/labels.d.ts.map +1 -1
  119. package/dist/maps/labels.js +188 -119
  120. package/dist/maps/labels.js.map +1 -1
  121. package/dist/maps/node.d.ts +162 -56
  122. package/dist/maps/node.d.ts.map +1 -1
  123. package/dist/maps/node.js +607 -515
  124. package/dist/maps/node.js.map +1 -1
  125. package/dist/maps/overlay.d.ts +72 -9
  126. package/dist/maps/overlay.d.ts.map +1 -1
  127. package/dist/maps/overlay.js +108 -28
  128. package/dist/maps/overlay.js.map +1 -1
  129. package/dist/maps/paint.d.ts +3 -0
  130. package/dist/maps/paint.d.ts.map +1 -1
  131. package/dist/maps/paint.js.map +1 -1
  132. package/dist/maps/proj.d.ts +14 -0
  133. package/dist/maps/proj.d.ts.map +1 -1
  134. package/dist/maps/proj.js +28 -0
  135. package/dist/maps/proj.js.map +1 -1
  136. package/dist/maps/renderer.d.ts +69 -0
  137. package/dist/maps/renderer.d.ts.map +1 -0
  138. package/dist/maps/renderer.js +66 -0
  139. package/dist/maps/renderer.js.map +1 -0
  140. package/dist/maps/sources.d.ts +25 -3
  141. package/dist/maps/sources.d.ts.map +1 -1
  142. package/dist/maps/sources.js +17 -0
  143. package/dist/maps/sources.js.map +1 -1
  144. package/dist/maps/theme.d.ts +19 -0
  145. package/dist/maps/theme.d.ts.map +1 -0
  146. package/dist/maps/theme.js +44 -0
  147. package/dist/maps/theme.js.map +1 -0
  148. package/dist/maps/tiles.d.ts +117 -17
  149. package/dist/maps/tiles.d.ts.map +1 -1
  150. package/dist/maps/tiles.js +163 -30
  151. package/dist/maps/tiles.js.map +1 -1
  152. package/dist/maps/types.d.ts +188 -20
  153. package/dist/maps/types.d.ts.map +1 -1
  154. package/dist/markdown/expressions.d.ts +1 -1
  155. package/dist/markdown/expressions.d.ts.map +1 -1
  156. package/dist/markdown/expressions.js.map +1 -1
  157. package/dist/markdown/index.d.ts +7 -6
  158. package/dist/markdown/index.d.ts.map +1 -1
  159. package/dist/markdown/index.js +9 -7
  160. package/dist/markdown/index.js.map +1 -1
  161. package/dist/markdown/spans.d.ts +1 -1
  162. package/dist/markdown/spans.d.ts.map +1 -1
  163. package/dist/media-player/index.d.ts +11 -3
  164. package/dist/media-player/index.d.ts.map +1 -1
  165. package/dist/media-player/index.js +13 -2
  166. package/dist/media-player/index.js.map +1 -1
  167. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  168. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  169. package/dist/rich-text-editor/clipboard.js +307 -0
  170. package/dist/rich-text-editor/clipboard.js.map +1 -0
  171. package/dist/rich-text-editor/collab.d.ts +32 -0
  172. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  173. package/dist/rich-text-editor/collab.js +55 -0
  174. package/dist/rich-text-editor/collab.js.map +1 -0
  175. package/dist/rich-text-editor/commands.d.ts +50 -0
  176. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  177. package/dist/rich-text-editor/commands.js +376 -0
  178. package/dist/rich-text-editor/commands.js.map +1 -0
  179. package/dist/rich-text-editor/drag.d.ts +68 -0
  180. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  181. package/dist/rich-text-editor/drag.js +148 -0
  182. package/dist/rich-text-editor/drag.js.map +1 -0
  183. package/dist/rich-text-editor/html.d.ts +145 -0
  184. package/dist/rich-text-editor/html.d.ts.map +1 -0
  185. package/dist/rich-text-editor/html.js +516 -0
  186. package/dist/rich-text-editor/html.js.map +1 -0
  187. package/dist/rich-text-editor/index.d.ts +263 -0
  188. package/dist/rich-text-editor/index.d.ts.map +1 -0
  189. package/dist/rich-text-editor/index.js +822 -0
  190. package/dist/rich-text-editor/index.js.map +1 -0
  191. package/dist/rich-text-editor/inline.d.ts +94 -0
  192. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  193. package/dist/rich-text-editor/inline.js +252 -0
  194. package/dist/rich-text-editor/inline.js.map +1 -0
  195. package/dist/rich-text-editor/keymap.d.ts +71 -0
  196. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  197. package/dist/rich-text-editor/keymap.js +393 -0
  198. package/dist/rich-text-editor/keymap.js.map +1 -0
  199. package/dist/rich-text-editor/keys.d.ts +29 -0
  200. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  201. package/dist/rich-text-editor/keys.js +93 -0
  202. package/dist/rich-text-editor/keys.js.map +1 -0
  203. package/dist/rich-text-editor/look.d.ts +71 -0
  204. package/dist/rich-text-editor/look.d.ts.map +1 -0
  205. package/dist/rich-text-editor/look.js +342 -0
  206. package/dist/rich-text-editor/look.js.map +1 -0
  207. package/dist/rich-text-editor/markdown.d.ts +18 -0
  208. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  209. package/dist/rich-text-editor/markdown.js +496 -0
  210. package/dist/rich-text-editor/markdown.js.map +1 -0
  211. package/dist/rich-text-editor/nodes.d.ts +135 -0
  212. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  213. package/dist/rich-text-editor/nodes.js +397 -0
  214. package/dist/rich-text-editor/nodes.js.map +1 -0
  215. package/dist/rich-text-editor/render.d.ts +68 -0
  216. package/dist/rich-text-editor/render.d.ts.map +1 -0
  217. package/dist/rich-text-editor/render.js +500 -0
  218. package/dist/rich-text-editor/render.js.map +1 -0
  219. package/dist/rich-text-editor/schema.d.ts +16 -0
  220. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  221. package/dist/rich-text-editor/schema.js +386 -0
  222. package/dist/rich-text-editor/schema.js.map +1 -0
  223. package/dist/rich-text-editor/store.d.ts +11 -0
  224. package/dist/rich-text-editor/store.d.ts.map +1 -0
  225. package/dist/rich-text-editor/store.js +43 -0
  226. package/dist/rich-text-editor/store.js.map +1 -0
  227. package/dist/rich-text-editor/suggest.d.ts +127 -0
  228. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  229. package/dist/rich-text-editor/suggest.js +0 -0
  230. package/dist/rich-text-editor/suggest.js.map +1 -0
  231. package/dist/rich-text-editor/tables.d.ts +41 -0
  232. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  233. package/dist/rich-text-editor/tables.js +204 -0
  234. package/dist/rich-text-editor/tables.js.map +1 -0
  235. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  236. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  237. package/dist/rich-text-editor/toolbar.js +548 -0
  238. package/dist/rich-text-editor/toolbar.js.map +1 -0
  239. package/dist/rich-text-editor/view.d.ts +368 -0
  240. package/dist/rich-text-editor/view.d.ts.map +1 -0
  241. package/dist/rich-text-editor/view.js +1968 -0
  242. package/dist/rich-text-editor/view.js.map +1 -0
  243. package/dist/rich-text-editor/virtual.d.ts +54 -0
  244. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  245. package/dist/rich-text-editor/virtual.js +262 -0
  246. package/dist/rich-text-editor/virtual.js.map +1 -0
  247. package/dist/richtext/node.d.ts +25 -2
  248. package/dist/richtext/node.d.ts.map +1 -1
  249. package/dist/richtext/node.js +46 -24
  250. package/dist/richtext/node.js.map +1 -1
  251. package/dist/table/index.d.ts.map +1 -1
  252. package/dist/table/index.js +14 -5
  253. package/dist/table/index.js.map +1 -1
  254. package/dist/terminal/backends.d.ts +4 -1
  255. package/dist/terminal/backends.d.ts.map +1 -1
  256. package/dist/terminal/backends.js.map +1 -1
  257. package/dist/terminal/index.d.ts +18 -7
  258. package/dist/terminal/index.d.ts.map +1 -1
  259. package/dist/terminal/index.js +28 -11
  260. package/dist/terminal/index.js.map +1 -1
  261. package/dist/terminal/vt/node.d.ts +56 -3
  262. package/dist/terminal/vt/node.d.ts.map +1 -1
  263. package/dist/terminal/vt/node.js +106 -26
  264. package/dist/terminal/vt/node.js.map +1 -1
  265. package/dist/terminal/vt/renderer.d.ts +4 -0
  266. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  267. package/dist/terminal/vt/renderer.js +14 -5
  268. package/dist/terminal/vt/renderer.js.map +1 -1
  269. package/dist/tree/index.d.ts.map +1 -1
  270. package/dist/tree/index.js +6 -4
  271. package/dist/tree/index.js.map +1 -1
  272. package/package.json +23 -4
  273. package/src/charts/node.ts +6 -2
  274. package/src/color-picker/ColorPicker.ts +107 -81
  275. package/src/embed/client.ts +102 -8
  276. package/src/embed/index.ts +2 -0
  277. package/src/html/node.ts +7 -5
  278. package/src/index.ts +2 -0
  279. package/src/{markdown → internal/markdown}/ast.ts +24 -7
  280. package/src/{markdown → internal/markdown}/parse.ts +134 -24
  281. package/src/internal/markdown/stringify.ts +772 -0
  282. package/src/internal/scroll.ts +39 -18
  283. package/src/internal/units.ts +34 -0
  284. package/src/internal/window.ts +4 -2
  285. package/src/maps/anchors.ts +607 -0
  286. package/src/maps/controller.ts +999 -0
  287. package/src/maps/gl/buckets.ts +457 -0
  288. package/src/maps/gl/build-worker.ts +55 -0
  289. package/src/maps/gl/color.ts +62 -0
  290. package/src/maps/gl/cover.ts +170 -0
  291. package/src/maps/gl/index.ts +23 -0
  292. package/src/maps/gl/markers.ts +83 -0
  293. package/src/maps/gl/overlays.ts +379 -0
  294. package/src/maps/gl/pane.ts +42 -0
  295. package/src/maps/gl/placement.ts +816 -0
  296. package/src/maps/gl/renderer.ts +1576 -0
  297. package/src/maps/gl/shaders.ts +486 -0
  298. package/src/maps/gl/store.ts +566 -0
  299. package/src/maps/gl/text.ts +495 -0
  300. package/src/maps/gl/view.ts +1224 -0
  301. package/src/maps/index.ts +270 -81
  302. package/src/maps/labels.ts +214 -121
  303. package/src/maps/node.ts +763 -629
  304. package/src/maps/overlay.ts +148 -43
  305. package/src/maps/paint.ts +3 -0
  306. package/src/maps/proj.ts +31 -0
  307. package/src/maps/renderer.ts +155 -0
  308. package/src/maps/sources.ts +38 -3
  309. package/src/maps/theme.ts +51 -0
  310. package/src/maps/tiles.ts +191 -33
  311. package/src/maps/types.ts +188 -20
  312. package/src/markdown/expressions.ts +5 -1
  313. package/src/markdown/index.ts +14 -11
  314. package/src/markdown/spans.ts +1 -1
  315. package/src/media-player/index.ts +18 -4
  316. package/src/rich-text-editor/clipboard.ts +439 -0
  317. package/src/rich-text-editor/collab.ts +91 -0
  318. package/src/rich-text-editor/commands.ts +448 -0
  319. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  320. package/src/rich-text-editor/drag.ts +211 -0
  321. package/src/rich-text-editor/html.ts +619 -0
  322. package/src/rich-text-editor/index.ts +1331 -0
  323. package/src/rich-text-editor/inline.ts +345 -0
  324. package/src/rich-text-editor/keymap.ts +510 -0
  325. package/src/rich-text-editor/keys.ts +119 -0
  326. package/src/rich-text-editor/look.ts +427 -0
  327. package/src/rich-text-editor/markdown.ts +653 -0
  328. package/src/rich-text-editor/nodes.ts +497 -0
  329. package/src/rich-text-editor/render.ts +800 -0
  330. package/src/rich-text-editor/schema.ts +431 -0
  331. package/src/rich-text-editor/store.ts +39 -0
  332. package/src/rich-text-editor/suggest.ts +597 -0
  333. package/src/rich-text-editor/tables.ts +272 -0
  334. package/src/rich-text-editor/toolbar.ts +724 -0
  335. package/src/rich-text-editor/view.ts +2287 -0
  336. package/src/rich-text-editor/virtual.ts +347 -0
  337. package/src/richtext/node.ts +59 -30
  338. package/src/table/index.ts +14 -5
  339. package/src/terminal/backends.ts +4 -1
  340. package/src/terminal/index.ts +43 -15
  341. package/src/terminal/vt/node.ts +128 -29
  342. package/src/terminal/vt/renderer.ts +27 -5
  343. package/src/tree/index.ts +6 -4
  344. package/dist/markdown/ast.d.ts.map +0 -1
  345. package/dist/markdown/ast.js +0 -17
  346. package/dist/markdown/ast.js.map +0 -1
  347. package/dist/markdown/parse.d.ts.map +0 -1
  348. package/dist/markdown/parse.js.map +0 -1
  349. package/dist/markdown/tags.d.ts.map +0 -1
  350. package/dist/markdown/tags.js.map +0 -1
  351. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  352. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  353. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  354. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -0,0 +1,999 @@
1
+ // The camera and the input, which both of `<Map>`'s renderers drive.
2
+ //
3
+ // `<Map>` draws through one of two renderers — the retained one in
4
+ // `./node.ts`, which composites rasterized tile surfaces, and the GL one in
5
+ // `./gl/`, which draws every frame from vector buckets — and chooses between
6
+ // them at run time (`./renderer.ts`). What neither may own is the camera. A
7
+ // map that falls back from GL to the retained renderer in the middle of a pan
8
+ // has to keep the camera it was panned to, the handle an application holds
9
+ // has to keep working, and a gesture has to mean the same thing on both —
10
+ // which is only true if both renderers answer to one object. This is it: the
11
+ // camera and its controlled/uncontrolled fork, the settle window,
12
+ // `onCameraChange` and `onMoveEnd`, the gestures (drag, wheel, keys, double
13
+ // click, a press on a marker, the hover), the marker hit test, and the
14
+ // handle `<Map>`'s ref hands out. A renderer attaches to it as a
15
+ // {@link MapView} — where its pane is, what a camera move costs it — and
16
+ // holds no camera of its own.
17
+ //
18
+ // The camera lives on an object rather than in React state for the reason
19
+ // `AGENTS.md` gives under "A map, and the three caches under it": that is
20
+ // what makes a drag step two numbers and a blit, with no render and no commit
21
+ // in the loop. `<Map>` makes one controller per mount and hands it to
22
+ // whichever renderer is drawing; a bare `<mapview>` makes its own.
23
+ import type { A11ySceneItem } from 'react-x11/node';
24
+
25
+ import { markerAt, markerRect } from './overlay.js';
26
+ import type { MapMarker } from './overlay.js';
27
+ import {
28
+ DEFAULT_TILE_SIZE,
29
+ boundsOf,
30
+ cameraForBounds,
31
+ projectLngLat,
32
+ transformFor,
33
+ unprojectPoint,
34
+ visibleBounds,
35
+ } from './proj.js';
36
+ import type {
37
+ LngLat,
38
+ LngLatBounds,
39
+ MapCamera,
40
+ ScreenRect,
41
+ Transform,
42
+ } from './proj.js';
43
+ import type {
44
+ FitBoundsOptions,
45
+ MapFrameStats,
46
+ MapHandle,
47
+ MapPointerEvent,
48
+ } from './types.js';
49
+
50
+ /** Where a map with no camera given starts. */
51
+ export const DEFAULT_CAMERA: MapCamera = {
52
+ center: { lon: 0, lat: 20 },
53
+ zoom: 2,
54
+ };
55
+
56
+ /** Logical pixels the pointer may travel before a press becomes a drag. */
57
+ export const DRAG_THRESHOLD = 3;
58
+
59
+ /**
60
+ * How long after the last camera change the map counts as moving.
61
+ *
62
+ * One window for both renderers, and both ask it the same question — is this
63
+ * a gesture? The retained renderer rasterizes nothing inside it, so a drag or
64
+ * a wheel is composites only; the GL renderer draws at reduced detail inside
65
+ * it when adaptive quality asks, and admits no new labels. `onMoveEnd` fires
66
+ * when it closes. Long enough that a wheel zoom's many notches count as one
67
+ * gesture, short enough that the map sharpens before the user has finished
68
+ * looking. (The retained renderer had 140 ms and the GL proof of concept
69
+ * 180; the retained number was the one its tests had pinned.)
70
+ */
71
+ export const SETTLE_MS = 140;
72
+
73
+ /**
74
+ * Zoom is quantized to this, for the reason react-x11's docs/scale.md gives
75
+ * about a gesture-driven `scale`: every distinct value is a distinct set of
76
+ * font sizes to shape, and a wheel feeding a raw accumulator makes a new set
77
+ * per frame. A sixteenth of a level is finer than the eye reads as stepping.
78
+ */
79
+ const ZOOM_STEP = 1 / 16;
80
+
81
+ /** A zoom on the {@link ZOOM_STEP} grid — also what a label placement is
82
+ * keyed on, so a placement and the zooms a wheel produces agree. */
83
+ export function quantizeZoom(zoom: number): number {
84
+ return Math.round(zoom / ZOOM_STEP) * ZOOM_STEP;
85
+ }
86
+
87
+ /** A wheel notch is this much zoom. */
88
+ const WHEEL_ZOOM = 1 / 2.5;
89
+
90
+ /**
91
+ * How fast a wheel's zoom closes on where the notches asked it to go — the
92
+ * time constant of the exponential the glide follows, so it is all but over
93
+ * after three of these.
94
+ *
95
+ * A notch is 0.384 of a level, which is six of {@link ZOOM_STEP}, and
96
+ * applying it the moment it arrives is a jump: the map is one size, and a
97
+ * frame later it is another. The device cannot help with that. A wheel
98
+ * *clicks* — the core protocol reports it as a button, and even over XI2 a
99
+ * mouse reports one notch at a time — so the smoothness has to come from
100
+ * the map spreading a notch over the frames after it, which is what every
101
+ * map client does with one. `ev.smooth` tells that device from the one that
102
+ * measured the scroll; see {@link MapController.wheel}.
103
+ */
104
+ const WHEEL_EASE_MS = 55;
105
+
106
+ /** What the wheel event's own step is worth, in the absence of a frame to
107
+ * measure: one at 60Hz. The gesture has to move *now* — a wheel that waits
108
+ * for the next frame to do anything reads as lag — and this is the distance
109
+ * a frame would have moved it. */
110
+ const EASE_FIRST_MS = 16;
111
+
112
+ /**
113
+ * How often a glide steps: a 60Hz frame.
114
+ *
115
+ * A timer rather than the frame the step is drawn in, because a step *is* a
116
+ * camera move and the views already know what to do with one — the retained
117
+ * renderer claims the pane, the GL one asks for a frame — and that is the
118
+ * path a drag's every pointer step takes. Claiming from inside a frame is
119
+ * not: the claim lands on the frame being painted and brings nothing after
120
+ * it, which left a notch delivering its first step and then waiting for the
121
+ * settle timer to wake the map up.
122
+ *
123
+ * Nothing here outruns the display. The step is exponential in the *time*
124
+ * since the last one, so a view that paints at half this rate sees every
125
+ * other step and the same glide; what the timer decides is the finest the
126
+ * map can move, not how fast it gets there.
127
+ */
128
+ const GLIDE_TICK_MS = 16;
129
+
130
+ /**
131
+ * A zoom the wheel asked for and the frames are still delivering.
132
+ *
133
+ * The **target** is the whole of what the wheel has asked for, kept apart
134
+ * from the camera because the camera cannot hold it: the zoom is quantized
135
+ * to {@link ZOOM_STEP}, and a touchpad's fraction of a notch is smaller than
136
+ * that. Accumulating here is what stops a slow two-finger scroll rounding to
137
+ * nothing, event after event, and never moving the map at all.
138
+ */
139
+ interface ZoomGlide {
140
+ /** Where the zoom is going: every delta of this gesture, unrounded. */
141
+ target: number;
142
+ /** The camera zoom the last step produced. If the camera is not there any
143
+ * more, something else has moved it — an application that owns it,
144
+ * `setCamera`, a fit — and this gesture is no longer the authority on
145
+ * where the zoom goes. */
146
+ applied: number;
147
+ /** The point the zoom is about, in pane-local logical pixels: where the
148
+ * pointer was for the last notch, which is what must not move. */
149
+ x: number;
150
+ y: number;
151
+ /** When the last step was applied — what the next one's distance is
152
+ * measured from, so the glide is the same speed at any frame rate. */
153
+ at: number;
154
+ /**
155
+ * Whether the frames have anything left to deliver, or this is only the
156
+ * accumulator.
157
+ *
158
+ * `false` for a device that *measured* the scroll: a touchpad's stream of
159
+ * fractions is already as smooth as the hand that made it, and easing it
160
+ * would add lag to a gesture that has none. The record stays behind
161
+ * anyway, because the fractions still have to accumulate somewhere.
162
+ */
163
+ easing: boolean;
164
+ }
165
+
166
+ /** The props the controller reads — `<Map>`'s, whichever renderer draws. */
167
+ export interface MapControllerProps {
168
+ camera?: MapCamera;
169
+ onCameraChange?: (camera: MapCamera) => void;
170
+ onMoveEnd?: (camera: MapCamera) => void;
171
+ minZoom?: number;
172
+ maxZoom?: number;
173
+ interactive?: boolean;
174
+ markers?: readonly MapMarker[];
175
+ onMapClick?: (event: MapPointerEvent) => void;
176
+ onMarkerClick?: (marker: MapMarker, event: MapPointerEvent) => void;
177
+ onMarkerHover?: (
178
+ marker: MapMarker | null,
179
+ event: MapPointerEvent | null,
180
+ ) => void;
181
+ }
182
+
183
+ /**
184
+ * A renderer, as the controller sees it: where its pane is, and what a camera
185
+ * move costs it. Nothing about how it draws.
186
+ */
187
+ export interface MapView {
188
+ /** The pane, in the window's logical pixels — empty before layout. */
189
+ pane(): ScreenRect;
190
+ /** Device pixels per logical pixel. */
191
+ scale(): number;
192
+ /**
193
+ * The camera moved, and the view should show it. `blit` says whether the
194
+ * move is a translation of what is already on screen — a pan — rather
195
+ * than a zoom or a jump: the retained renderer's difference between a
196
+ * blit and a repaint.
197
+ */
198
+ moved(previous: MapCamera, next: MapCamera, blit: boolean): void;
199
+ /** The settle window closed: the map is still, and may sharpen. */
200
+ settled(): void;
201
+ /** `refresh()`: throw away everything derived from the style. */
202
+ refresh(): void;
203
+ stats(): MapFrameStats | null;
204
+ /** Take the keyboard focus — a pan is where the arrows start working. */
205
+ focus(): void;
206
+ }
207
+
208
+ /** A pointer event as the controller reads it: pane-local logical pixels,
209
+ * the button, the click count and the modifiers. */
210
+ export interface MapPointerInput {
211
+ x: number;
212
+ y: number;
213
+ /** `1` left, `2` middle, `3` right. */
214
+ button?: number;
215
+ /** The click count: `2` is the second press of a double click. */
216
+ detail?: number;
217
+ shiftKey?: boolean;
218
+ ctrlKey?: boolean;
219
+ altKey?: boolean;
220
+ metaKey?: boolean;
221
+ }
222
+
223
+ /** What a press began — see {@link MapController.pointerDown}. */
224
+ export type PressKind = 'marker' | 'pan' | 'frozen';
225
+
226
+ type Gesture =
227
+ | {
228
+ kind: 'pan';
229
+ startX: number;
230
+ startY: number;
231
+ lastX: number;
232
+ lastY: number;
233
+ moved: boolean;
234
+ }
235
+ | { kind: 'marker'; id: string; startX: number; startY: number };
236
+
237
+ const timers = globalThis as {
238
+ setTimeout?(fn: () => void, ms: number): unknown;
239
+ clearTimeout?(id: unknown): void;
240
+ performance?: { now(): number };
241
+ };
242
+
243
+ /**
244
+ * The clock a glide is measured on.
245
+ *
246
+ * `performance.now()` rather than the `Date.now()` the settle window uses:
247
+ * this one is compared across two frames of a 60Hz display, and `Date.now()`
248
+ * counts whole milliseconds, so a sixth of the step would be error. Reached
249
+ * through `globalThis` because `src/` compiles with `types: []`.
250
+ */
251
+ function now(): number {
252
+ return timers.performance?.now() ?? Date.now();
253
+ }
254
+
255
+ /**
256
+ * The settle timer, unref'd where the runtime allows it.
257
+ *
258
+ * A map that has just been panned holds a timer, and an unref'd one does not
259
+ * keep a process alive on its own — which matters for a script or a test that
260
+ * renders a map and expects to exit, and is the call core's caret blink makes
261
+ * for the same reason.
262
+ */
263
+ function arm(tick: () => void, ms: number): unknown {
264
+ const handle = timers.setTimeout?.(tick, ms) ?? null;
265
+ (handle as { unref?(): void } | null)?.unref?.();
266
+ return handle;
267
+ }
268
+
269
+ function clamp(value: number, low: number, high: number): number {
270
+ return value < low ? low : value > high ? high : value;
271
+ }
272
+
273
+ const EMPTY_PANE: ScreenRect = { x: 0, y: 0, width: 0, height: 0 };
274
+
275
+ export class MapController {
276
+ /**
277
+ * What `<Map>`'s ref hands out. Made once, so it is the **same object**
278
+ * for the life of the mount — across a renderer switch too, which is
279
+ * what lets an application hold it in a ref and never look again.
280
+ */
281
+ readonly handle: MapHandle;
282
+
283
+ /**
284
+ * Set by the view for the length of a frame. A camera change made inside
285
+ * one — the fit that a `fitBounds` asked for before layout, applied at
286
+ * the first frame with a size — is already what the frame draws, so it
287
+ * claims nothing and starts no gesture.
288
+ */
289
+ painting = false;
290
+
291
+ private _props: MapControllerProps = {};
292
+ /** The camera, when the application does not control it. */
293
+ private _camera: MapCamera;
294
+ private _view: MapView | null = null;
295
+ private _gesture: Gesture | null = null;
296
+ private _hover: string | null = null;
297
+ /** Wall-clock time the settle window closes at. */
298
+ private _settleAt = 0;
299
+ private _settleTimer: unknown = null;
300
+ private _glide: ZoomGlide | null = null;
301
+ private _glideTimer: unknown = null;
302
+ private _pendingFit: {
303
+ bounds: LngLatBounds;
304
+ options?: FitBoundsOptions;
305
+ } | null = null;
306
+
307
+ constructor(seed: MapCamera = DEFAULT_CAMERA) {
308
+ this._camera = { center: { ...seed.center }, zoom: seed.zoom };
309
+ this.handle = {
310
+ getCamera: () => this.getCamera(),
311
+ setCamera: (camera) => this.setCamera(camera),
312
+ panBy: (dx, dy) => this.panBy(dx, dy),
313
+ zoomIn: (step) => this.zoomIn(step),
314
+ zoomOut: (step) => this.zoomOut(step),
315
+ zoomTo: (zoom) => this.zoomTo(zoom),
316
+ fitBounds: (bounds, options) => this.fitBounds(bounds, options),
317
+ fitMarkers: (ids, options) => this.fitMarkers(ids, options),
318
+ getBounds: () => this.getBounds(),
319
+ project: (position) => this.project(position),
320
+ unproject: (x, y) => this.unproject(x, y),
321
+ markerAt: (x, y) => this.markerAt(x, y),
322
+ refresh: () => this._view?.refresh(),
323
+ stats: () => this._view?.stats() ?? null,
324
+ };
325
+ }
326
+
327
+ // --- props and views --------------------------------------------------------
328
+
329
+ /** The props to read from here on. Read lazily, at event and frame time,
330
+ * so a new object per render costs an assignment. */
331
+ setProps(props: MapControllerProps): void {
332
+ this._props = props;
333
+ }
334
+
335
+ get props(): MapControllerProps {
336
+ return this._props;
337
+ }
338
+
339
+ /** The renderer drawing the map from now on. */
340
+ attach(view: MapView): void {
341
+ this._view = view;
342
+ }
343
+
344
+ /** …and letting go, if it still is: during a renderer switch the new
345
+ * view can attach before the old one is torn down. */
346
+ detach(view: MapView): void {
347
+ if (this._view === view) this._view = null;
348
+ }
349
+
350
+ get view(): MapView | null {
351
+ return this._view;
352
+ }
353
+
354
+ /**
355
+ * Stop the timers — the settle window's, and a wheel glide in flight.
356
+ * They are the only things held that outlive a frame. The camera and the
357
+ * view stay, so a mount that React only pretends to tear down (a
358
+ * strict-mode double effect) comes back to a working controller; the next
359
+ * camera move arms the settle timer again, and the next notch a glide.
360
+ */
361
+ dispose(): void {
362
+ if (this._settleTimer !== null) timers.clearTimeout?.(this._settleTimer);
363
+ this._settleTimer = null;
364
+ if (this._glideTimer !== null) timers.clearTimeout?.(this._glideTimer);
365
+ this._glideTimer = null;
366
+ this._glide = null;
367
+ }
368
+
369
+ private _minZoom(): number {
370
+ return this._props.minZoom ?? 0;
371
+ }
372
+
373
+ private _maxZoom(): number {
374
+ return this._props.maxZoom ?? 22;
375
+ }
376
+
377
+ private _interactive(): boolean {
378
+ return this._props.interactive !== false;
379
+ }
380
+
381
+ private _markers(): readonly MapMarker[] {
382
+ return this._props.markers ?? [];
383
+ }
384
+
385
+ // --- the camera ----------------------------------------------------------
386
+
387
+ /** The camera in effect: the application's when it controls one. */
388
+ camera(): MapCamera {
389
+ return this._props.camera ?? this._camera;
390
+ }
391
+
392
+ /** The camera this controller keeps for itself — the one in effect
393
+ * whenever the application is not controlling it. */
394
+ ownCamera(): MapCamera {
395
+ return this._camera;
396
+ }
397
+
398
+ /** The pane, in the window's logical pixels — empty with no view. */
399
+ pane(): ScreenRect {
400
+ return this._view?.pane() ?? EMPTY_PANE;
401
+ }
402
+
403
+ /** A camera resolved against the pane. */
404
+ transform(camera: MapCamera = this.camera()): Transform {
405
+ const pane = this.pane();
406
+ return transformFor(
407
+ camera,
408
+ { width: pane.width, height: pane.height },
409
+ DEFAULT_TILE_SIZE,
410
+ );
411
+ }
412
+
413
+ /**
414
+ * Move the camera — the one place it changes.
415
+ *
416
+ * Clamped (the zoom to the range, the latitude to what Web Mercator can
417
+ * represent; the longitude is not, because the map wraps and a camera just
418
+ * past the antimeridian is a camera in the next copy of the world),
419
+ * reported, and the settle window restarted: every camera move is a
420
+ * gesture, not only a pointer one, because an application animating the
421
+ * camera with `panBy` in a loop wants what a drag wants.
422
+ *
423
+ * A camera the application controls is only *asked* to move:
424
+ * `onCameraChange` is the request, and the view hears about the move when
425
+ * the new `camera` prop arrives. Telling it here as well was a double
426
+ * shift — the retained renderer blitted the pane for the request and then
427
+ * again for the prop, and `scrollContents` adds two shifts in a frame
428
+ * together.
429
+ */
430
+ apply(next: MapCamera, blit: boolean): void {
431
+ const previous = this.camera();
432
+ const camera: MapCamera = {
433
+ center: { lon: next.center.lon, lat: clamp(next.center.lat, -85, 85) },
434
+ zoom: clamp(next.zoom, this._minZoom(), this._maxZoom()),
435
+ };
436
+ if (
437
+ camera.zoom === previous.zoom &&
438
+ camera.center.lon === previous.center.lon &&
439
+ camera.center.lat === previous.center.lat
440
+ ) {
441
+ return;
442
+ }
443
+ const controlled = this._props.camera !== undefined;
444
+ if (!controlled) this._camera = camera;
445
+ this._props.onCameraChange?.(camera);
446
+ if (this.painting) return;
447
+ this.touch();
448
+ if (!controlled) this._view?.moved(previous, camera, blit);
449
+ }
450
+
451
+ getCamera(): MapCamera {
452
+ const camera = this.camera();
453
+ return { center: { ...camera.center }, zoom: camera.zoom };
454
+ }
455
+
456
+ setCamera(camera: Partial<MapCamera>): void {
457
+ const current = this.camera();
458
+ this.apply(
459
+ {
460
+ center: camera.center ?? current.center,
461
+ zoom: camera.zoom ?? current.zoom,
462
+ },
463
+ false,
464
+ );
465
+ }
466
+
467
+ /** Move by a distance in pane-local logical pixels. */
468
+ panBy(dx: number, dy: number): void {
469
+ const t = this.transform();
470
+ this.apply(
471
+ {
472
+ zoom: t.zoom,
473
+ center: unprojectPoint(t, t.paneX + dx, t.paneY + dy),
474
+ },
475
+ true,
476
+ );
477
+ }
478
+
479
+ zoomIn(step = 1): void {
480
+ const pane = this.pane();
481
+ this.zoomAbout(step, pane.width / 2, pane.height / 2);
482
+ }
483
+
484
+ zoomOut(step = 1): void {
485
+ this.zoomIn(-step);
486
+ }
487
+
488
+ zoomTo(zoom: number): void {
489
+ this.setCamera({ zoom });
490
+ }
491
+
492
+ /**
493
+ * Zoom by `delta` levels about a pane-local point that must not move —
494
+ * the pointer under a wheel, the pane's centre for a key.
495
+ */
496
+ zoomAbout(delta: number, x: number, y: number): void {
497
+ const camera = this.camera();
498
+ const zoom = clamp(
499
+ quantizeZoom(camera.zoom + delta),
500
+ this._minZoom(),
501
+ this._maxZoom(),
502
+ );
503
+ if (zoom === camera.zoom) return;
504
+ const before = this.transform(camera);
505
+ const anchor = unprojectPoint(before, x, y);
506
+ const after = this.transform({ center: camera.center, zoom });
507
+ // Where the anchor would land at the new zoom, and how far the centre
508
+ // has to move so it lands where it already is.
509
+ const moved = projectLngLat(after, anchor);
510
+ const dx = (moved.x - x) / after.world;
511
+ const dy = (moved.y - y) / after.world;
512
+ this.apply(
513
+ {
514
+ zoom,
515
+ center: unprojectPoint(
516
+ {
517
+ ...after,
518
+ centerX: after.centerX + dx,
519
+ centerY: after.centerY + dy,
520
+ },
521
+ after.paneX,
522
+ after.paneY,
523
+ ),
524
+ },
525
+ false,
526
+ );
527
+ }
528
+
529
+ /**
530
+ * A scroll, as a zoom: where the wheel wants the camera, and how the
531
+ * frames after it get there.
532
+ *
533
+ * **Two devices arrive here and they want opposite things.** A wheel
534
+ * *clicks*: one event carrying a whole notch, with nothing in between, so
535
+ * the notch becomes a target the frames after it glide towards. A touchpad
536
+ * *measures*: a stream of fractions that is already as smooth as the hand
537
+ * making it, which wants applying the moment it arrives and only needs
538
+ * keeping from rounding away. `ev.smooth` is core's word for which device
539
+ * this is — "a whole notch is worth easing towards, a stream of measured
540
+ * pixels is not" (react-x11's docs/events.md).
541
+ *
542
+ * Either way the *target* accumulates. A second notch arriving mid-glide
543
+ * lengthens the one gesture instead of starting another, and a fraction
544
+ * too small to move the quantized camera is still on the books when the
545
+ * next one arrives.
546
+ */
547
+ private _wheelZoom(
548
+ delta: number,
549
+ x: number,
550
+ y: number,
551
+ smooth: boolean,
552
+ ): void {
553
+ const at = now();
554
+ const camera = this.camera();
555
+ const glide = this._glide;
556
+ // Where this delta counts from: the target the wheel has already asked
557
+ // for, or the camera when there is nothing outstanding. A record whose
558
+ // `applied` is not where the camera is belongs to a camera something
559
+ // else has since moved, and is not this gesture's to add to.
560
+ //
561
+ // Nothing expires it by age. A glide that is still owed frames is still
562
+ // owed them however long the frames take, and what is left of a
563
+ // touchpad's is under half a step of zoom — too little to be a jump
564
+ // whenever it is spent.
565
+ const from =
566
+ glide && glide.applied === camera.zoom ? glide.target : camera.zoom;
567
+ // Clamped here as well as in `apply`: an accumulator allowed past the
568
+ // limit records notches the map cannot answer, and the scroll back out
569
+ // then does nothing until they are spent.
570
+ this._glide = {
571
+ target: clamp(from + delta, this._minZoom(), this._maxZoom()),
572
+ applied: camera.zoom,
573
+ x,
574
+ y,
575
+ at,
576
+ easing: !smooth,
577
+ };
578
+ // The event's own step, so the map moves under the notch that asked
579
+ // rather than on the frame after it — and then the rest, a frame at a
580
+ // time, until the target is reached.
581
+ this.stepZoom(at, EASE_FIRST_MS);
582
+ this._armGlide();
583
+ }
584
+
585
+ /** Keep stepping a glide until it arrives. Idempotent: a notch arriving
586
+ * mid-glide moves the target and finds the timer already running. */
587
+ private _armGlide(): void {
588
+ if (this._glideTimer !== null || this._glide?.easing !== true) return;
589
+ const tick = (): void => {
590
+ this._glideTimer = null;
591
+ const glide = this._glide;
592
+ if (glide?.easing !== true) return;
593
+ const at = now();
594
+ // `stepZoom` clears the record when the target is reached or the
595
+ // camera refuses a step, and this re-arms only while one is left.
596
+ this.stepZoom(at, at - glide.at);
597
+ this._armGlide();
598
+ };
599
+ this._glideTimer = arm(tick, GLIDE_TICK_MS);
600
+ }
601
+
602
+ /**
603
+ * One step of a glide: how far the camera moves this frame.
604
+ *
605
+ * Exponential rather than a curve with a duration, because the distance is
606
+ * not known when it starts — a second notch moves the target while the
607
+ * first is still being delivered — and because `elapsed` being the real
608
+ * gap between two frames is what makes the glide the same speed on a
609
+ * display that paints 120 frames a second and one that manages 30.
610
+ *
611
+ * Returns whether the camera moved. The move itself is what brings the
612
+ * frame that shows it: `apply` tells the view, exactly as a drag's every
613
+ * pointer step does.
614
+ */
615
+ stepZoom(at: number, elapsed: number): boolean {
616
+ const glide = this._glide;
617
+ if (!glide) return false;
618
+ const camera = this.camera();
619
+ if (glide.applied !== camera.zoom) {
620
+ // The camera moved without this gesture: an application that owns it
621
+ // answering a wheel with something of its own, a `setCamera`, a fit.
622
+ // Whatever it was is more current than a notch from before it.
623
+ this._glide = null;
624
+ return false;
625
+ }
626
+ const remaining = glide.target - camera.zoom;
627
+ let step = glide.easing
628
+ ? remaining * (1 - Math.exp(-elapsed / WHEEL_EASE_MS))
629
+ : remaining;
630
+ // Never a step the quantized camera cannot show. The tail of an
631
+ // exponential is arbitrarily small, and a step that rounds to nothing
632
+ // would leave the glide asking for frames it has no use for — so the
633
+ // tail is walked a grid step at a time, and "the camera did not move" is
634
+ // left meaning what it says below.
635
+ if (Math.abs(step) < ZOOM_STEP) {
636
+ step =
637
+ Math.abs(remaining) < ZOOM_STEP
638
+ ? remaining
639
+ : Math.sign(remaining) * ZOOM_STEP;
640
+ }
641
+ const before = camera.zoom;
642
+ this.zoomAbout(step, glide.x, glide.y);
643
+ const zoom = this.camera().zoom;
644
+ glide.applied = zoom;
645
+ glide.at = at;
646
+ if (zoom === before) {
647
+ // The camera refused a step it cannot have rounded away — an
648
+ // application owns it and did not answer, or it is against its limit.
649
+ // A wheel's glide has nothing further to deliver; a touchpad's record
650
+ // stays behind as the accumulator, and the next fraction moves it on.
651
+ if (glide.easing) this._glide = null;
652
+ return false;
653
+ }
654
+ return true;
655
+ }
656
+
657
+ fitBounds(bounds: LngLatBounds, options?: FitBoundsOptions): void {
658
+ const pane = this.pane();
659
+ if (pane.width <= 0 || pane.height <= 0) {
660
+ // Asked before layout has run — which `fitBounds` in an effect always
661
+ // is. Remembered, and applied at the first frame that has a size.
662
+ this._pendingFit = { bounds, options };
663
+ return;
664
+ }
665
+ this._pendingFit = null;
666
+ this.apply(
667
+ cameraForBounds(
668
+ bounds,
669
+ { width: pane.width, height: pane.height },
670
+ {
671
+ padding: options?.padding ?? 24,
672
+ tileSize: DEFAULT_TILE_SIZE,
673
+ minZoom: this._minZoom(),
674
+ maxZoom: options?.maxZoom ?? this._maxZoom(),
675
+ },
676
+ ),
677
+ false,
678
+ );
679
+ }
680
+
681
+ fitMarkers(ids?: readonly string[], options?: FitBoundsOptions): void {
682
+ const wanted = ids ? new Set(ids) : null;
683
+ const positions: LngLat[] = [];
684
+ for (const marker of this._markers()) {
685
+ if (wanted && !wanted.has(marker.id)) continue;
686
+ positions.push(marker.position);
687
+ }
688
+ const bounds = boundsOf(positions);
689
+ if (bounds) this.fitBounds(bounds, options);
690
+ }
691
+
692
+ /** What the pane can see — the whole world before it has a size. */
693
+ getBounds(): LngLatBounds {
694
+ const pane = this.pane();
695
+ if (pane.width <= 0 || pane.height <= 0) {
696
+ return { west: -180, south: -85, east: 180, north: 85 };
697
+ }
698
+ return visibleBounds(this.transform());
699
+ }
700
+
701
+ project(position: LngLat): { x: number; y: number } {
702
+ return projectLngLat(this.transform(), position);
703
+ }
704
+
705
+ unproject(x: number, y: number): LngLat {
706
+ return unprojectPoint(this.transform(), x, y);
707
+ }
708
+
709
+ markerAt(x: number, y: number): MapMarker | null {
710
+ return markerAt(this._markers(), this.transform(), x, y);
711
+ }
712
+
713
+ /**
714
+ * A view is about to draw a frame: a `fitBounds` that arrived before the
715
+ * pane had a size is applied now, so the frame draws it.
716
+ */
717
+ beginFrame(): void {
718
+ const fit = this._pendingFit;
719
+ if (!fit) return;
720
+ const pane = this.pane();
721
+ if (pane.width <= 0 || pane.height <= 0) return;
722
+ this.fitBounds(fit.bounds, fit.options);
723
+ }
724
+
725
+ // --- the settle window ---------------------------------------------------
726
+
727
+ /** Whether the camera moved within the settle window. */
728
+ get moving(): boolean {
729
+ return Date.now() < this._settleAt;
730
+ }
731
+
732
+ /** …or a press is still down: what the retained renderer holds
733
+ * rasterization off for. */
734
+ get gesturing(): boolean {
735
+ return this._gesture !== null || this.moving;
736
+ }
737
+
738
+ /** Whether a pan gesture is under way — a press that may yet drag. */
739
+ get dragging(): boolean {
740
+ return this._gesture?.kind === 'pan';
741
+ }
742
+
743
+ /**
744
+ * Restart the settle window. When it closes the camera has held still for
745
+ * {@link SETTLE_MS}: `onMoveEnd` fires — the moment an application fetches
746
+ * what is now on screen — and the view is told it may sharpen.
747
+ *
748
+ * Wall-clock time rather than a frame clock: this is a window a hundred
749
+ * and forty milliseconds long compared inside a timer callback, and a
750
+ * timer is the one thing that runs when no frame does.
751
+ */
752
+ touch(): void {
753
+ this._settleAt = Date.now() + SETTLE_MS;
754
+ if (this._settleTimer !== null) return;
755
+ const tick = (): void => {
756
+ this._settleTimer = null;
757
+ const left = this._settleAt - Date.now();
758
+ if (left > 0) {
759
+ this._settleTimer = arm(tick, left);
760
+ return;
761
+ }
762
+ this._props.onMoveEnd?.(this.camera());
763
+ this._view?.settled();
764
+ };
765
+ this._settleTimer = arm(tick, SETTLE_MS);
766
+ }
767
+
768
+ // --- input ---------------------------------------------------------------
769
+
770
+ /**
771
+ * A press. Answers what it began, for the view to capture the pointer — and
772
+ * for a pan, to take the focus — or null when nothing is.
773
+ *
774
+ * A press on a marker is a marker gesture: its release is a marker click.
775
+ * A press anywhere else is a pan, or on a map that does not move a
776
+ * `'frozen'` press, which pans nothing and is still a click when it is
777
+ * released — an application listening for clicks on a frozen map gets
778
+ * them.
779
+ */
780
+ pointerDown(input: MapPointerInput): PressKind {
781
+ const marker = markerAt(
782
+ this._markers(),
783
+ this.transform(),
784
+ input.x,
785
+ input.y,
786
+ );
787
+ if (marker) {
788
+ this._gesture = {
789
+ kind: 'marker',
790
+ id: marker.id,
791
+ startX: input.x,
792
+ startY: input.y,
793
+ };
794
+ return 'marker';
795
+ }
796
+ this._gesture = {
797
+ kind: 'pan',
798
+ startX: input.x,
799
+ startY: input.y,
800
+ lastX: input.x,
801
+ lastY: input.y,
802
+ moved: false,
803
+ };
804
+ return this._interactive() ? 'pan' : 'frozen';
805
+ }
806
+
807
+ /** The pointer moved with the press still down. */
808
+ pointerDrag(input: MapPointerInput): void {
809
+ const gesture = this._gesture;
810
+ if (!gesture || gesture.kind !== 'pan') return;
811
+ if (!this._interactive()) return;
812
+ if (
813
+ !gesture.moved &&
814
+ Math.abs(input.x - gesture.startX) < DRAG_THRESHOLD &&
815
+ Math.abs(input.y - gesture.startY) < DRAG_THRESHOLD
816
+ ) {
817
+ return;
818
+ }
819
+ gesture.moved = true;
820
+ // Whole device pixels, because that is what the retained renderer's
821
+ // blit can shift — a fractional pan would decline it every frame and
822
+ // repaint the pane. Invisible on the GL renderer, which draws any shift.
823
+ const scale = this._view?.scale() ?? 1;
824
+ const dx = Math.round((input.x - gesture.lastX) * scale) / scale;
825
+ const dy = Math.round((input.y - gesture.lastY) * scale) / scale;
826
+ if (dx === 0 && dy === 0) return;
827
+ gesture.lastX += dx;
828
+ gesture.lastY += dy;
829
+ this.touch();
830
+ this.panBy(-dx, -dy);
831
+ }
832
+
833
+ /**
834
+ * The press let go. A release within the drag threshold of a marker press
835
+ * is that marker's click, then the map's; one that ended no drag is the
836
+ * map's click. And the second release of a double click zooms in about the
837
+ * pointer, Shift for out — the convention every map client has.
838
+ */
839
+ pointerUp(input: MapPointerInput): void {
840
+ const gesture = this._gesture;
841
+ this._gesture = null;
842
+ if (!gesture) return;
843
+ if (gesture.kind === 'marker') {
844
+ const marker = this._markers().find((m) => m.id === gesture.id);
845
+ if (
846
+ marker &&
847
+ Math.abs(input.x - gesture.startX) < DRAG_THRESHOLD &&
848
+ Math.abs(input.y - gesture.startY) < DRAG_THRESHOLD
849
+ ) {
850
+ const event = this.pointerEvent(input, marker);
851
+ this._props.onMarkerClick?.(marker, event);
852
+ this._props.onMapClick?.(event);
853
+ }
854
+ return;
855
+ }
856
+ if (gesture.moved) {
857
+ this.touch();
858
+ return;
859
+ }
860
+ this._props.onMapClick?.(this.pointerEvent(input, null));
861
+ if (input.detail === 2 && this._interactive()) {
862
+ this.zoomAbout(input.shiftKey ? -1 : 1, input.x, input.y);
863
+ }
864
+ }
865
+
866
+ /** The pointer moved with nothing pressed: the marker under it, if that
867
+ * changed, is `onMarkerHover`'s. */
868
+ pointerMove(input: MapPointerInput): void {
869
+ const notify = this._props.onMarkerHover;
870
+ if (!notify) return;
871
+ const marker = markerAt(
872
+ this._markers(),
873
+ this.transform(),
874
+ input.x,
875
+ input.y,
876
+ );
877
+ const id = marker?.id ?? null;
878
+ if (id === this._hover) return;
879
+ this._hover = id;
880
+ notify(marker, this.pointerEvent(input, marker));
881
+ }
882
+
883
+ /**
884
+ * The pointer left the map. No event: there is no position on the map to
885
+ * report, and inventing one would be worse than saying so.
886
+ */
887
+ pointerLeave(): void {
888
+ if (this._hover === null) return;
889
+ this._hover = null;
890
+ this._props.onMarkerHover?.(null, null);
891
+ }
892
+
893
+ /**
894
+ * The wheel, which is a zoom about the pointer. True when it was consumed
895
+ * — always, on a map that moves: a wheel over a map is never meant for
896
+ * whatever is behind it.
897
+ */
898
+ wheel(input: MapPointerInput, deltaY: number, smooth = false): boolean {
899
+ if (!this._interactive()) return false;
900
+ this.touch();
901
+ this._wheelZoom(-deltaY * WHEEL_ZOOM * 0.02, input.x, input.y, smooth);
902
+ return true;
903
+ }
904
+
905
+ /** A key: the arrows pan, +/− zoom about the centre. True when it was one
906
+ * of those, on a map that moves. */
907
+ keyDown(keysym: number, shiftKey: boolean): boolean {
908
+ if (!this._interactive()) return false;
909
+ const pane = this.pane();
910
+ const step = shiftKey ? 200 : 60;
911
+ switch (keysym) {
912
+ case 0xff51: // XK_Left
913
+ this.panBy(-step, 0);
914
+ return true;
915
+ case 0xff53: // XK_Right
916
+ this.panBy(step, 0);
917
+ return true;
918
+ case 0xff52: // XK_Up
919
+ this.panBy(0, -step);
920
+ return true;
921
+ case 0xff54: // XK_Down
922
+ this.panBy(0, step);
923
+ return true;
924
+ case 0x002b: // XK_plus
925
+ case 0x003d: // XK_equal
926
+ case 0xffab: // XK_KP_Add
927
+ this.zoomAbout(1, pane.width / 2, pane.height / 2);
928
+ return true;
929
+ case 0x002d: // XK_minus
930
+ case 0xffad: // XK_KP_Subtract
931
+ this.zoomAbout(-1, pane.width / 2, pane.height / 2);
932
+ return true;
933
+ default:
934
+ return false;
935
+ }
936
+ }
937
+
938
+ /** Where a pointer event happened, in every space that could be wanted. */
939
+ pointerEvent(
940
+ input: MapPointerInput,
941
+ marker: MapMarker | null,
942
+ ): MapPointerEvent {
943
+ return {
944
+ lngLat: unprojectPoint(this.transform(), input.x, input.y),
945
+ x: input.x,
946
+ y: input.y,
947
+ marker,
948
+ shiftKey: input.shiftKey ?? false,
949
+ ctrlKey: input.ctrlKey ?? false,
950
+ altKey: input.altKey ?? false,
951
+ metaKey: input.metaKey ?? false,
952
+ button: input.button ?? 1,
953
+ };
954
+ }
955
+
956
+ /**
957
+ * What a screen reader meets: the markers in view, as buttons.
958
+ *
959
+ * A map is one painted rectangle to an assistive technology, and its
960
+ * markers are the only things in it that are *objects* rather than
961
+ * cartography — so those are the scene. Announcing every road would be
962
+ * worse than announcing none. The same whichever renderer draws.
963
+ */
964
+ markerScene(): A11ySceneItem[] {
965
+ const pane = this.pane();
966
+ if (pane.width <= 0 || pane.height <= 0) return [];
967
+ const transform = this.transform();
968
+ const scale = this._view?.scale() ?? 1;
969
+ const items: A11ySceneItem[] = [];
970
+ for (const marker of this._markers()) {
971
+ const rect = markerRect(marker, transform);
972
+ if (
973
+ rect.x + rect.width < 0 ||
974
+ rect.y + rect.height < 0 ||
975
+ rect.x > pane.width ||
976
+ rect.y > pane.height
977
+ ) {
978
+ continue;
979
+ }
980
+ items.push({
981
+ id: `marker:${marker.id}`,
982
+ // Device pixels in the owning window's coordinates, which is what
983
+ // an a11y scene rect is — the same space as `abs`.
984
+ rect: {
985
+ x: (pane.x + rect.x) * scale,
986
+ y: (pane.y + rect.y) * scale,
987
+ width: rect.width * scale,
988
+ height: rect.height * scale,
989
+ },
990
+ role: 'button',
991
+ name:
992
+ marker.title ??
993
+ `${marker.position.lat.toFixed(4)}, ${marker.position.lon.toFixed(4)}`,
994
+ states: { selected: marker.selected ?? false },
995
+ });
996
+ }
997
+ return items;
998
+ }
999
+ }