@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.
- package/README.md +295 -51
- package/dist/charts/node.d.ts +1 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +6 -2
- package/dist/charts/node.js.map +1 -1
- package/dist/color-picker/ColorPicker.d.ts.map +1 -1
- package/dist/color-picker/ColorPicker.js +43 -33
- package/dist/color-picker/ColorPicker.js.map +1 -1
- package/dist/embed/client.d.ts +48 -3
- package/dist/embed/client.d.ts.map +1 -1
- package/dist/embed/client.js +79 -5
- package/dist/embed/client.js.map +1 -1
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.d.ts.map +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/index.js.map +1 -1
- package/dist/html/node.d.ts +3 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +7 -5
- package/dist/html/node.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -0
- package/dist/internal/markdown/ast.js +22 -0
- package/dist/internal/markdown/ast.js.map +1 -0
- package/dist/internal/markdown/parse.d.ts.map +1 -0
- package/dist/{markdown → internal/markdown}/parse.js +112 -22
- package/dist/internal/markdown/parse.js.map +1 -0
- package/dist/internal/markdown/stringify.d.ts +4 -0
- package/dist/internal/markdown/stringify.d.ts.map +1 -0
- package/dist/internal/markdown/stringify.js +684 -0
- package/dist/internal/markdown/stringify.js.map +1 -0
- package/dist/internal/markdown/tags.d.ts.map +1 -0
- package/dist/internal/markdown/tags.js.map +1 -0
- package/dist/internal/scroll.d.ts +3 -2
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +33 -15
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/units.d.ts +2 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +35 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +4 -2
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +98 -0
- package/dist/maps/anchors.d.ts.map +1 -0
- package/dist/maps/anchors.js +531 -0
- package/dist/maps/anchors.js.map +1 -0
- package/dist/maps/controller.d.ts +283 -0
- package/dist/maps/controller.d.ts.map +1 -0
- package/dist/maps/controller.js +743 -0
- package/dist/maps/controller.js.map +1 -0
- package/dist/maps/gl/buckets.d.ts +95 -0
- package/dist/maps/gl/buckets.d.ts.map +1 -0
- package/dist/maps/gl/buckets.js +362 -0
- package/dist/maps/gl/buckets.js.map +1 -0
- package/dist/maps/gl/build-worker.d.ts +13 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -0
- package/dist/maps/gl/build-worker.js +44 -0
- package/dist/maps/gl/build-worker.js.map +1 -0
- package/dist/maps/gl/color.d.ts +8 -0
- package/dist/maps/gl/color.d.ts.map +1 -0
- package/dist/maps/gl/color.js +57 -0
- package/dist/maps/gl/color.js.map +1 -0
- package/dist/maps/gl/cover.d.ts +51 -0
- package/dist/maps/gl/cover.d.ts.map +1 -0
- package/dist/maps/gl/cover.js +124 -0
- package/dist/maps/gl/cover.js.map +1 -0
- package/dist/maps/gl/index.d.ts +3 -0
- package/dist/maps/gl/index.d.ts.map +1 -0
- package/dist/maps/gl/index.js +20 -0
- package/dist/maps/gl/index.js.map +1 -0
- package/dist/maps/gl/markers.d.ts +28 -0
- package/dist/maps/gl/markers.d.ts.map +1 -0
- package/dist/maps/gl/markers.js +60 -0
- package/dist/maps/gl/markers.js.map +1 -0
- package/dist/maps/gl/overlays.d.ts +68 -0
- package/dist/maps/gl/overlays.d.ts.map +1 -0
- package/dist/maps/gl/overlays.js +300 -0
- package/dist/maps/gl/overlays.js.map +1 -0
- package/dist/maps/gl/pane.d.ts +12 -0
- package/dist/maps/gl/pane.d.ts.map +1 -0
- package/dist/maps/gl/pane.js +37 -0
- package/dist/maps/gl/pane.js.map +1 -0
- package/dist/maps/gl/placement.d.ts +111 -0
- package/dist/maps/gl/placement.d.ts.map +1 -0
- package/dist/maps/gl/placement.js +567 -0
- package/dist/maps/gl/placement.js.map +1 -0
- package/dist/maps/gl/renderer.d.ts +352 -0
- package/dist/maps/gl/renderer.d.ts.map +1 -0
- package/dist/maps/gl/renderer.js +1127 -0
- package/dist/maps/gl/renderer.js.map +1 -0
- package/dist/maps/gl/shaders.d.ts +120 -0
- package/dist/maps/gl/shaders.d.ts.map +1 -0
- package/dist/maps/gl/shaders.js +451 -0
- package/dist/maps/gl/shaders.js.map +1 -0
- package/dist/maps/gl/store.d.ts +115 -0
- package/dist/maps/gl/store.d.ts.map +1 -0
- package/dist/maps/gl/store.js +469 -0
- package/dist/maps/gl/store.js.map +1 -0
- package/dist/maps/gl/text.d.ts +154 -0
- package/dist/maps/gl/text.d.ts.map +1 -0
- package/dist/maps/gl/text.js +382 -0
- package/dist/maps/gl/text.js.map +1 -0
- package/dist/maps/gl/view.d.ts +54 -0
- package/dist/maps/gl/view.d.ts.map +1 -0
- package/dist/maps/gl/view.js +974 -0
- package/dist/maps/gl/view.js.map +1 -0
- package/dist/maps/index.d.ts +1 -1
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +216 -52
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +37 -16
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +188 -119
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +162 -56
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +607 -515
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/overlay.d.ts +72 -9
- package/dist/maps/overlay.d.ts.map +1 -1
- package/dist/maps/overlay.js +108 -28
- package/dist/maps/overlay.js.map +1 -1
- package/dist/maps/paint.d.ts +3 -0
- package/dist/maps/paint.d.ts.map +1 -1
- package/dist/maps/paint.js.map +1 -1
- package/dist/maps/proj.d.ts +14 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +28 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/renderer.d.ts +69 -0
- package/dist/maps/renderer.d.ts.map +1 -0
- package/dist/maps/renderer.js +66 -0
- package/dist/maps/renderer.js.map +1 -0
- package/dist/maps/sources.d.ts +25 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +17 -0
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/theme.d.ts +19 -0
- package/dist/maps/theme.d.ts.map +1 -0
- package/dist/maps/theme.js +44 -0
- package/dist/maps/theme.js.map +1 -0
- package/dist/maps/tiles.d.ts +117 -17
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +163 -30
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +188 -20
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/expressions.d.ts +1 -1
- package/dist/markdown/expressions.d.ts.map +1 -1
- package/dist/markdown/expressions.js.map +1 -1
- package/dist/markdown/index.d.ts +7 -6
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +9 -7
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/media-player/index.d.ts +11 -3
- package/dist/media-player/index.d.ts.map +1 -1
- package/dist/media-player/index.js +13 -2
- package/dist/media-player/index.js.map +1 -1
- package/dist/rich-text-editor/clipboard.d.ts +23 -0
- package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
- package/dist/rich-text-editor/clipboard.js +307 -0
- package/dist/rich-text-editor/clipboard.js.map +1 -0
- package/dist/rich-text-editor/collab.d.ts +32 -0
- package/dist/rich-text-editor/collab.d.ts.map +1 -0
- package/dist/rich-text-editor/collab.js +55 -0
- package/dist/rich-text-editor/collab.js.map +1 -0
- package/dist/rich-text-editor/commands.d.ts +50 -0
- package/dist/rich-text-editor/commands.d.ts.map +1 -0
- package/dist/rich-text-editor/commands.js +376 -0
- package/dist/rich-text-editor/commands.js.map +1 -0
- package/dist/rich-text-editor/drag.d.ts +68 -0
- package/dist/rich-text-editor/drag.d.ts.map +1 -0
- package/dist/rich-text-editor/drag.js +148 -0
- package/dist/rich-text-editor/drag.js.map +1 -0
- package/dist/rich-text-editor/html.d.ts +145 -0
- package/dist/rich-text-editor/html.d.ts.map +1 -0
- package/dist/rich-text-editor/html.js +516 -0
- package/dist/rich-text-editor/html.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +263 -0
- package/dist/rich-text-editor/index.d.ts.map +1 -0
- package/dist/rich-text-editor/index.js +822 -0
- package/dist/rich-text-editor/index.js.map +1 -0
- package/dist/rich-text-editor/inline.d.ts +94 -0
- package/dist/rich-text-editor/inline.d.ts.map +1 -0
- package/dist/rich-text-editor/inline.js +252 -0
- package/dist/rich-text-editor/inline.js.map +1 -0
- package/dist/rich-text-editor/keymap.d.ts +71 -0
- package/dist/rich-text-editor/keymap.d.ts.map +1 -0
- package/dist/rich-text-editor/keymap.js +393 -0
- package/dist/rich-text-editor/keymap.js.map +1 -0
- package/dist/rich-text-editor/keys.d.ts +29 -0
- package/dist/rich-text-editor/keys.d.ts.map +1 -0
- package/dist/rich-text-editor/keys.js +93 -0
- package/dist/rich-text-editor/keys.js.map +1 -0
- package/dist/rich-text-editor/look.d.ts +71 -0
- package/dist/rich-text-editor/look.d.ts.map +1 -0
- package/dist/rich-text-editor/look.js +342 -0
- package/dist/rich-text-editor/look.js.map +1 -0
- package/dist/rich-text-editor/markdown.d.ts +18 -0
- package/dist/rich-text-editor/markdown.d.ts.map +1 -0
- package/dist/rich-text-editor/markdown.js +496 -0
- package/dist/rich-text-editor/markdown.js.map +1 -0
- package/dist/rich-text-editor/nodes.d.ts +135 -0
- package/dist/rich-text-editor/nodes.d.ts.map +1 -0
- package/dist/rich-text-editor/nodes.js +397 -0
- package/dist/rich-text-editor/nodes.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts +68 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -0
- package/dist/rich-text-editor/render.js +500 -0
- package/dist/rich-text-editor/render.js.map +1 -0
- package/dist/rich-text-editor/schema.d.ts +16 -0
- package/dist/rich-text-editor/schema.d.ts.map +1 -0
- package/dist/rich-text-editor/schema.js +386 -0
- package/dist/rich-text-editor/schema.js.map +1 -0
- package/dist/rich-text-editor/store.d.ts +11 -0
- package/dist/rich-text-editor/store.d.ts.map +1 -0
- package/dist/rich-text-editor/store.js +43 -0
- package/dist/rich-text-editor/store.js.map +1 -0
- package/dist/rich-text-editor/suggest.d.ts +127 -0
- package/dist/rich-text-editor/suggest.d.ts.map +1 -0
- package/dist/rich-text-editor/suggest.js +0 -0
- package/dist/rich-text-editor/suggest.js.map +1 -0
- package/dist/rich-text-editor/tables.d.ts +41 -0
- package/dist/rich-text-editor/tables.d.ts.map +1 -0
- package/dist/rich-text-editor/tables.js +204 -0
- package/dist/rich-text-editor/tables.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts +58 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
- package/dist/rich-text-editor/toolbar.js +548 -0
- package/dist/rich-text-editor/toolbar.js.map +1 -0
- package/dist/rich-text-editor/view.d.ts +368 -0
- package/dist/rich-text-editor/view.d.ts.map +1 -0
- package/dist/rich-text-editor/view.js +1968 -0
- package/dist/rich-text-editor/view.js.map +1 -0
- package/dist/rich-text-editor/virtual.d.ts +54 -0
- package/dist/rich-text-editor/virtual.d.ts.map +1 -0
- package/dist/rich-text-editor/virtual.js +262 -0
- package/dist/rich-text-editor/virtual.js.map +1 -0
- package/dist/richtext/node.d.ts +25 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +46 -24
- package/dist/richtext/node.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +14 -5
- package/dist/table/index.js.map +1 -1
- package/dist/terminal/backends.d.ts +4 -1
- package/dist/terminal/backends.d.ts.map +1 -1
- package/dist/terminal/backends.js.map +1 -1
- package/dist/terminal/index.d.ts +18 -7
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +28 -11
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +56 -3
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +106 -26
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts +4 -0
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +14 -5
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +6 -4
- package/dist/tree/index.js.map +1 -1
- package/package.json +23 -4
- package/src/charts/node.ts +6 -2
- package/src/color-picker/ColorPicker.ts +107 -81
- package/src/embed/client.ts +102 -8
- package/src/embed/index.ts +2 -0
- package/src/html/node.ts +7 -5
- package/src/index.ts +2 -0
- package/src/{markdown → internal/markdown}/ast.ts +24 -7
- package/src/{markdown → internal/markdown}/parse.ts +134 -24
- package/src/internal/markdown/stringify.ts +772 -0
- package/src/internal/scroll.ts +39 -18
- package/src/internal/units.ts +34 -0
- package/src/internal/window.ts +4 -2
- package/src/maps/anchors.ts +607 -0
- package/src/maps/controller.ts +999 -0
- package/src/maps/gl/buckets.ts +457 -0
- package/src/maps/gl/build-worker.ts +55 -0
- package/src/maps/gl/color.ts +62 -0
- package/src/maps/gl/cover.ts +170 -0
- package/src/maps/gl/index.ts +23 -0
- package/src/maps/gl/markers.ts +83 -0
- package/src/maps/gl/overlays.ts +379 -0
- package/src/maps/gl/pane.ts +42 -0
- package/src/maps/gl/placement.ts +816 -0
- package/src/maps/gl/renderer.ts +1576 -0
- package/src/maps/gl/shaders.ts +486 -0
- package/src/maps/gl/store.ts +566 -0
- package/src/maps/gl/text.ts +495 -0
- package/src/maps/gl/view.ts +1224 -0
- package/src/maps/index.ts +270 -81
- package/src/maps/labels.ts +214 -121
- package/src/maps/node.ts +763 -629
- package/src/maps/overlay.ts +148 -43
- package/src/maps/paint.ts +3 -0
- package/src/maps/proj.ts +31 -0
- package/src/maps/renderer.ts +155 -0
- package/src/maps/sources.ts +38 -3
- package/src/maps/theme.ts +51 -0
- package/src/maps/tiles.ts +191 -33
- package/src/maps/types.ts +188 -20
- package/src/markdown/expressions.ts +5 -1
- package/src/markdown/index.ts +14 -11
- package/src/markdown/spans.ts +1 -1
- package/src/media-player/index.ts +18 -4
- package/src/rich-text-editor/clipboard.ts +439 -0
- package/src/rich-text-editor/collab.ts +91 -0
- package/src/rich-text-editor/commands.ts +448 -0
- package/src/rich-text-editor/dom-globals.d.ts +23 -0
- package/src/rich-text-editor/drag.ts +211 -0
- package/src/rich-text-editor/html.ts +619 -0
- package/src/rich-text-editor/index.ts +1331 -0
- package/src/rich-text-editor/inline.ts +345 -0
- package/src/rich-text-editor/keymap.ts +510 -0
- package/src/rich-text-editor/keys.ts +119 -0
- package/src/rich-text-editor/look.ts +427 -0
- package/src/rich-text-editor/markdown.ts +653 -0
- package/src/rich-text-editor/nodes.ts +497 -0
- package/src/rich-text-editor/render.ts +800 -0
- package/src/rich-text-editor/schema.ts +431 -0
- package/src/rich-text-editor/store.ts +39 -0
- package/src/rich-text-editor/suggest.ts +597 -0
- package/src/rich-text-editor/tables.ts +272 -0
- package/src/rich-text-editor/toolbar.ts +724 -0
- package/src/rich-text-editor/view.ts +2287 -0
- package/src/rich-text-editor/virtual.ts +347 -0
- package/src/richtext/node.ts +59 -30
- package/src/table/index.ts +14 -5
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +43 -15
- package/src/terminal/vt/node.ts +128 -29
- package/src/terminal/vt/renderer.ts +27 -5
- package/src/tree/index.ts +6 -4
- package/dist/markdown/ast.d.ts.map +0 -1
- package/dist/markdown/ast.js +0 -17
- package/dist/markdown/ast.js.map +0 -1
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/tags.d.ts.map +0 -1
- package/dist/markdown/tags.js.map +0 -1
- /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.js +0 -0
- /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
|
+
}
|