@react-x11/components 0.8.0 → 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 +95 -34
- 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/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 +1 -0
- package/dist/internal/scroll.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/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/package.json +23 -4
- package/src/charts/node.ts +6 -2
- 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 +5 -3
- 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/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +43 -15
- 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
package/dist/maps/node.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// Three caches, and the map's whole performance argument is the way they
|
|
12
12
|
// are layered:
|
|
13
13
|
//
|
|
14
|
-
// 1. **Tile data**, keyed on `
|
|
14
|
+
// 1. **Tile data**, keyed on the source object and `z/x/y`, and valid
|
|
15
|
+
// forever. The object, not its `id` — see `./tiles.ts`.
|
|
15
16
|
// 2. **A rendered `Surface` per tile**, valid for a zoom *level* and a
|
|
16
17
|
// style — not for a camera position. So a **pan** composites the same
|
|
17
18
|
// surfaces at new offsets (and blits, so most of them are not even
|
|
@@ -29,13 +30,15 @@
|
|
|
29
30
|
// nothing is ever rasterized during a drag.
|
|
30
31
|
import { Node } from 'react-x11/node';
|
|
31
32
|
import { Surface } from 'react-x11/ntk';
|
|
32
|
-
import {
|
|
33
|
+
import { MapController, quantizeZoom } from './controller.js';
|
|
33
34
|
import { BATCH_VERTICES, DrawScratch, drawTileRun, isMapCanvas, now, prepareStyle, } from './paint.js';
|
|
34
|
-
import { DEFAULT_TILE_SIZE,
|
|
35
|
-
import { TileCache, pyramid } from './tiles.js';
|
|
35
|
+
import { DEFAULT_TILE_SIZE, dataSquareOf, dataTileFor, subTileOf, rasterFor, tileCover, tileKey, transformFor, } from './proj.js';
|
|
36
|
+
import { TileCache, drawnFor, pyramid } from './tiles.js';
|
|
37
|
+
import { attributionOf } from './sources.js';
|
|
36
38
|
import { shortbreadStyle } from './styles.js';
|
|
39
|
+
import { isDarkTheme, overlayPalette } from './theme.js';
|
|
37
40
|
import { LabelShaper, collectLabels, drawLabels, placeLabels, } from './labels.js';
|
|
38
|
-
import { drawMarkers, drawOverlays,
|
|
41
|
+
import { ATTRIBUTION_OPACITY, ATTRIBUTION_SIZE, attributionLayout, drawMarkers, drawOverlays, markerRect, } from './overlay.js';
|
|
39
42
|
/** Registration key, `kind` and JSX tag, one string — react-x11 rejects a
|
|
40
43
|
* node whose `kind` is not the name it was registered under, because
|
|
41
44
|
* `kind` is what paint order, the test queries and the DEV style assertion
|
|
@@ -57,20 +60,6 @@ export const SELF_DAMAGED_PROPS = [
|
|
|
57
60
|
'sources',
|
|
58
61
|
'mapStyle',
|
|
59
62
|
];
|
|
60
|
-
/** Screen pixels the pointer may travel before a press becomes a drag. */
|
|
61
|
-
const DRAG_THRESHOLD = 3;
|
|
62
|
-
/** How long after the last gesture step the map goes back to rasterizing.
|
|
63
|
-
* Long enough that a wheel-zoom's many steps count as one gesture, short
|
|
64
|
-
* enough that the map sharpens before the user has finished looking. */
|
|
65
|
-
const SETTLE_MS = 140;
|
|
66
|
-
/** Zoom is quantized to this, for the reason react-x11's docs/scale.md
|
|
67
|
-
* gives about a gesture-driven `scale`: every distinct value is a distinct
|
|
68
|
-
* set of font sizes to shape, and a wheel feeding a raw accumulator makes
|
|
69
|
-
* a new set per frame. A sixteenth of a level is finer than the eye reads
|
|
70
|
-
* as stepping. */
|
|
71
|
-
const ZOOM_STEP = 1 / 16;
|
|
72
|
-
/** A wheel notch is this much zoom. */
|
|
73
|
-
const WHEEL_ZOOM = 1 / 2.5;
|
|
74
63
|
/** The largest tile surface, per edge, in device pixels. An argb32 surface
|
|
75
64
|
* is `4 × size²` bytes, so 2048 is 16 MB and is already more than any
|
|
76
65
|
* pyramid justifies. */
|
|
@@ -101,26 +90,6 @@ function intersectRects(a, b) {
|
|
|
101
90
|
return null;
|
|
102
91
|
return { x, y, width: right - x, height: bottom - y };
|
|
103
92
|
}
|
|
104
|
-
const timers = globalThis;
|
|
105
|
-
/**
|
|
106
|
-
* The settle timer, unref'd where the runtime allows it.
|
|
107
|
-
*
|
|
108
|
-
* A map that has just been panned holds a 140 ms timer, and an unref'd one
|
|
109
|
-
* does not keep a process alive on its own — which matters for a script or
|
|
110
|
-
* a test that renders a map and expects to exit, and is the call core's
|
|
111
|
-
* caret blink makes for the same reason.
|
|
112
|
-
*/
|
|
113
|
-
function arm(tick) {
|
|
114
|
-
const handle = timers.setTimeout?.(tick, SETTLE_MS) ?? null;
|
|
115
|
-
handle?.unref?.();
|
|
116
|
-
return handle;
|
|
117
|
-
}
|
|
118
|
-
function clamp(value, low, high) {
|
|
119
|
-
return value < low ? low : value > high ? high : value;
|
|
120
|
-
}
|
|
121
|
-
function quantize(zoom) {
|
|
122
|
-
return Math.round(zoom / ZOOM_STEP) * ZOOM_STEP;
|
|
123
|
-
}
|
|
124
93
|
/** A logical value put on the device grid — the same helper `src/flow/`
|
|
125
94
|
* keeps, and for the same reason: ntk's fast paths for a blit and a
|
|
126
95
|
* rounded box are gated on integral geometry, and `x * 1.5` is not always
|
|
@@ -136,38 +105,76 @@ function rectsOverlap(a, b) {
|
|
|
136
105
|
a.y < b.y + b.height &&
|
|
137
106
|
a.y + a.height > b.y);
|
|
138
107
|
}
|
|
108
|
+
/** Whether `outer` covers all of `inner`. */
|
|
109
|
+
function containsRect(outer, inner) {
|
|
110
|
+
return (outer.x <= inner.x &&
|
|
111
|
+
outer.y <= inner.y &&
|
|
112
|
+
outer.x + outer.width >= inner.x + inner.width &&
|
|
113
|
+
outer.y + outer.height >= inner.y + inner.height);
|
|
114
|
+
}
|
|
115
|
+
function sameSources(a, b) {
|
|
116
|
+
return a.length === b.length && a.every((source, i) => source === b[i]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* How long a restyle may keep the previous style up once the view moves,
|
|
120
|
+
* in milliseconds the map was free to draw in.
|
|
121
|
+
*
|
|
122
|
+
* A still view is never cut short: its redraw is a fixed amount of work,
|
|
123
|
+
* and the swap waits for all of it. A moving one can bring tiles into view
|
|
124
|
+
* as fast as the old ones are finished — a pan with pauses in it, a camera
|
|
125
|
+
* an application animates — and would never swap. So once the camera has
|
|
126
|
+
* moved the wait is bounded, and a tile not yet redrawn when it runs out
|
|
127
|
+
* shows the background until it is. Frames a gesture holds rasterization
|
|
128
|
+
* off in do not count, since nothing is drawn in them.
|
|
129
|
+
*/
|
|
130
|
+
const RESTYLE_WAIT_MS = 1500;
|
|
139
131
|
export class MapViewNode extends Node {
|
|
140
132
|
_cache;
|
|
141
133
|
_scratch = new DrawScratch();
|
|
142
|
-
_geometry = new GeometryBuffer();
|
|
143
134
|
_shaper = null;
|
|
144
135
|
/**
|
|
145
|
-
* The camera
|
|
136
|
+
* The camera and the gestures: the controller `<Map>` hands down, or this
|
|
137
|
+
* element's own when it is used bare. Both of `<Map>`'s renderers answer
|
|
138
|
+
* to one (`./controller.ts`), which is how a fallback from one to the
|
|
139
|
+
* other keeps the camera and the handle.
|
|
146
140
|
*
|
|
147
|
-
* The
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
141
|
+
* The camera being an object's rather than React state's is what makes a
|
|
142
|
+
* pan cost nothing but a blit: a drag step moves two numbers and claims a
|
|
143
|
+
* strip, and React is not involved at all. Routed through state instead,
|
|
144
|
+
* every pointer step would be a render, a commit and a full-pane claim,
|
|
145
|
+
* which is the shape `<Flow>` documents as "the content lags and catches
|
|
146
|
+
* up".
|
|
153
147
|
*/
|
|
154
|
-
|
|
148
|
+
_controller;
|
|
149
|
+
_ownsController;
|
|
150
|
+
/** This element, as its controller sees it. */
|
|
151
|
+
_view;
|
|
155
152
|
_prepared = null;
|
|
156
153
|
_preparedFrom = null;
|
|
157
154
|
_defaultStyle = null;
|
|
158
155
|
/** The placement, and what it was computed for. */
|
|
159
156
|
_labels = [];
|
|
160
157
|
_labelKey = '';
|
|
158
|
+
/** …and what it was placed from, all of which a restyle holds on to (see
|
|
159
|
+
* {@link Outgoing}): the quantized zoom, the sources on the map, their
|
|
160
|
+
* candidates per source, and the face they were shaped in. */
|
|
161
|
+
_labelZoom = 0;
|
|
162
|
+
_labelSources = [];
|
|
163
|
+
_labelCandidates = new Map();
|
|
164
|
+
_shapedFamily;
|
|
161
165
|
/** Candidates per tile, so a pan that brings a tile back does not redo
|
|
162
166
|
* the walk over its symbol layers. */
|
|
163
167
|
_candidates = new Map();
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
/** The previous style while a restyle draws the new one behind it, or
|
|
169
|
+
* null when what is on screen is the current generation. */
|
|
170
|
+
_outgoing = null;
|
|
171
|
+
/** The style background the last frame painted — what a restyle keeps
|
|
172
|
+
* painting. Remembered rather than recomputed, because `refresh()`
|
|
173
|
+
* follows an edit made to the very style object it would be read from. */
|
|
174
|
+
_paintedBackground;
|
|
175
|
+
/** Whether a frame has been painted: before one, a restyle has no
|
|
176
|
+
* previous picture to hold. */
|
|
177
|
+
_painted = false;
|
|
171
178
|
/** Whether any tile has been rasterized in the frame being painted — the
|
|
172
179
|
* forward-progress guarantee below. */
|
|
173
180
|
_rastered = false;
|
|
@@ -176,11 +183,37 @@ export class MapViewNode extends Node {
|
|
|
176
183
|
_sceneAnnounced = false;
|
|
177
184
|
constructor(props, app) {
|
|
178
185
|
super(ELEMENT, props, app);
|
|
179
|
-
|
|
180
|
-
//
|
|
186
|
+
const given = props.mapController;
|
|
187
|
+
// A bare element's own controller is seeded once: `defaultCamera` is
|
|
188
|
+
// read here and never again, which is what makes it a *default* rather
|
|
189
|
+
// than a second controlled prop. `<Map>` seeds the one it hands down.
|
|
181
190
|
const seed = (props.camera ?? props.defaultCamera);
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
this._controller = given ?? new MapController(seed);
|
|
192
|
+
this._ownsController = given === undefined;
|
|
193
|
+
if (this._ownsController) {
|
|
194
|
+
this._controller.setProps(props);
|
|
195
|
+
}
|
|
196
|
+
this._view = {
|
|
197
|
+
pane: () => this._pane(),
|
|
198
|
+
scale: () => this._scale,
|
|
199
|
+
moved: (previous, next, blit) => {
|
|
200
|
+
// A new pyramid level: different tiles, different labels.
|
|
201
|
+
if (Math.floor(next.zoom) !== Math.floor(previous.zoom)) {
|
|
202
|
+
this._labelKey = '';
|
|
203
|
+
}
|
|
204
|
+
if (!blit || !this._blitPan(previous, next))
|
|
205
|
+
this._repaint('scroll');
|
|
206
|
+
},
|
|
207
|
+
// The gesture is over: sharpen. A wake-up, not a repaint — nothing has
|
|
208
|
+
// moved since the last frame, so what is on screen is still right;
|
|
209
|
+
// what is needed is a frame to start rasterizing in, and each tile
|
|
210
|
+
// claims its own box as it lands.
|
|
211
|
+
settled: () => this._wake('content'),
|
|
212
|
+
refresh: () => this._restyle(),
|
|
213
|
+
stats: () => this._stats,
|
|
214
|
+
focus: () => this.focus(),
|
|
215
|
+
};
|
|
216
|
+
this._controller.attach(this._view);
|
|
184
217
|
// A map is a thing you drive with the keyboard as well as the mouse:
|
|
185
218
|
// arrows pan, +/- zoom. Without this it is never focused and no key
|
|
186
219
|
// arrives.
|
|
@@ -191,9 +224,7 @@ export class MapViewNode extends Node {
|
|
|
191
224
|
onError: (entry) => {
|
|
192
225
|
this._prop('onTileError')?.(entry.error, {
|
|
193
226
|
...entry.tile,
|
|
194
|
-
|
|
195
|
-
// from the key it was built with.
|
|
196
|
-
sourceId: entry.key.slice(0, entry.key.lastIndexOf(':')),
|
|
227
|
+
sourceId: entry.sourceId,
|
|
197
228
|
});
|
|
198
229
|
},
|
|
199
230
|
onChange: () => {
|
|
@@ -239,44 +270,20 @@ export class MapViewNode extends Node {
|
|
|
239
270
|
return this._prepared;
|
|
240
271
|
}
|
|
241
272
|
_isDark() {
|
|
242
|
-
|
|
243
|
-
const background = theme?.background;
|
|
244
|
-
if (typeof background !== 'string')
|
|
245
|
-
return false;
|
|
246
|
-
// The same reading `src/code-editor/`'s token themes make: luminance of
|
|
247
|
-
// the surface the widget sits on, not a flag nobody sets.
|
|
248
|
-
const hex = background.trim();
|
|
249
|
-
if (!hex.startsWith('#') || hex.length < 7)
|
|
250
|
-
return false;
|
|
251
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
252
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
253
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
254
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 128;
|
|
273
|
+
return isDarkTheme(this.theme);
|
|
255
274
|
}
|
|
256
275
|
_palette() {
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
accent: theme?.accent ?? '#2d6cdf',
|
|
260
|
-
background: theme?.background ?? '#ffffff',
|
|
261
|
-
text: theme?.text ?? '#111111',
|
|
262
|
-
};
|
|
276
|
+
return overlayPalette(this.theme);
|
|
263
277
|
}
|
|
264
278
|
_sources() {
|
|
265
279
|
const given = this._prop('sources');
|
|
266
280
|
return given ? [...given] : [];
|
|
267
281
|
}
|
|
282
|
+
/** What a source is called in its requests and its errors. Not what its
|
|
283
|
+
* tiles are cached under — the cache files them under the object. */
|
|
268
284
|
_sourceId(source, index) {
|
|
269
285
|
return source.id ?? `source-${index}`;
|
|
270
286
|
}
|
|
271
|
-
_minZoom() {
|
|
272
|
-
return this._prop('minZoom') ?? 0;
|
|
273
|
-
}
|
|
274
|
-
_maxZoom() {
|
|
275
|
-
return this._prop('maxZoom') ?? 22;
|
|
276
|
-
}
|
|
277
|
-
_interactive() {
|
|
278
|
-
return this._prop('interactive') !== false;
|
|
279
|
-
}
|
|
280
287
|
_markers() {
|
|
281
288
|
return this._prop('markers') ?? [];
|
|
282
289
|
}
|
|
@@ -301,8 +308,7 @@ export class MapViewNode extends Node {
|
|
|
301
308
|
return pane.width > 0 && pane.height > 0;
|
|
302
309
|
}
|
|
303
310
|
camera() {
|
|
304
|
-
|
|
305
|
-
return given ?? this._camera;
|
|
311
|
+
return this._controller.camera();
|
|
306
312
|
}
|
|
307
313
|
/** The camera resolved against the pane. */
|
|
308
314
|
_transform(camera = this.camera()) {
|
|
@@ -344,46 +350,6 @@ export class MapViewNode extends Node {
|
|
|
344
350
|
this.invalidate(false, { x: box.x, y: box.y, width: 1, height: 1 }, reason);
|
|
345
351
|
}
|
|
346
352
|
// --- camera --------------------------------------------------------------
|
|
347
|
-
/**
|
|
348
|
-
* Move the camera.
|
|
349
|
-
*
|
|
350
|
-
* The one place the camera changes, so the controlled/uncontrolled fork,
|
|
351
|
-
* the clamping, the notification and — the interesting part — the
|
|
352
|
-
* decision between a blit and a repaint all live together.
|
|
353
|
-
*/
|
|
354
|
-
_applyCamera(next, blit = true) {
|
|
355
|
-
const previous = this.camera();
|
|
356
|
-
const zoom = clamp(next.zoom, this._minZoom(), this._maxZoom());
|
|
357
|
-
// Latitude is clamped to what Web Mercator can represent; longitude is
|
|
358
|
-
// not, because the map wraps and a camera just past the antimeridian is
|
|
359
|
-
// a camera in the next copy of the world.
|
|
360
|
-
const camera = {
|
|
361
|
-
center: { lon: next.center.lon, lat: clamp(next.center.lat, -85, 85) },
|
|
362
|
-
zoom,
|
|
363
|
-
};
|
|
364
|
-
if (camera.zoom === previous.zoom &&
|
|
365
|
-
camera.center.lon === previous.center.lon &&
|
|
366
|
-
camera.center.lat === previous.center.lat) {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (this.props.camera === undefined)
|
|
370
|
-
this._camera = camera;
|
|
371
|
-
this._prop('onCameraChange')?.(camera);
|
|
372
|
-
if (this._painting)
|
|
373
|
-
return;
|
|
374
|
-
// Every camera move defers rasterization, not just a pointer gesture.
|
|
375
|
-
// An application animating a camera with `panBy` in a loop wants
|
|
376
|
-
// exactly what a drag wants — composites while it moves, a sharpen when
|
|
377
|
-
// it stops — and a single programmatic move only pays the settle delay,
|
|
378
|
-
// which is a seventh of a second.
|
|
379
|
-
this._touchGesture();
|
|
380
|
-
if (Math.floor(camera.zoom) !== Math.floor(previous.zoom)) {
|
|
381
|
-
// A new pyramid level: different tiles, different labels.
|
|
382
|
-
this._labelKey = '';
|
|
383
|
-
}
|
|
384
|
-
if (!blit || !this._blitPan(previous, camera))
|
|
385
|
-
this._repaint('scroll');
|
|
386
|
-
}
|
|
387
353
|
/**
|
|
388
354
|
* A pan is a scroll in every way but the bookkeeping, and react-x11#303
|
|
389
355
|
* made the bookkeeping public: `scrollContents` claims the pane, arms the
|
|
@@ -440,134 +406,108 @@ export class MapViewNode extends Node {
|
|
|
440
406
|
}
|
|
441
407
|
return true;
|
|
442
408
|
}
|
|
443
|
-
/**
|
|
444
|
-
*
|
|
445
|
-
|
|
446
|
-
const camera = this.camera();
|
|
447
|
-
const zoom = clamp(quantize(camera.zoom + delta), this._minZoom(), this._maxZoom());
|
|
448
|
-
if (zoom === camera.zoom)
|
|
449
|
-
return;
|
|
450
|
-
const before = this._transform(camera);
|
|
451
|
-
const anchor = unprojectPoint(before, screenX, screenY);
|
|
452
|
-
const after = this._transform({ center: camera.center, zoom });
|
|
453
|
-
// Where the anchor would land at the new zoom, and how far the centre
|
|
454
|
-
// has to move so it lands where it already is.
|
|
455
|
-
const moved = projectLngLat(after, anchor);
|
|
456
|
-
const dx = (moved.x - screenX) / after.world;
|
|
457
|
-
const dy = (moved.y - screenY) / after.world;
|
|
458
|
-
this._applyCamera({
|
|
459
|
-
zoom,
|
|
460
|
-
center: unprojectPoint({
|
|
461
|
-
...after,
|
|
462
|
-
centerX: after.centerX + dx,
|
|
463
|
-
centerY: after.centerY + dy,
|
|
464
|
-
}, after.paneX, after.paneY),
|
|
465
|
-
}, false);
|
|
466
|
-
}
|
|
467
|
-
/** Suspend rasterization for the length of a gesture, and arrange for it
|
|
468
|
-
* to resume. */
|
|
469
|
-
_touchGesture() {
|
|
470
|
-
// Wall-clock here, not the budget clock: this is a 140 ms window, and
|
|
471
|
-
// it is compared inside a timer callback.
|
|
472
|
-
this._settleAt = Date.now() + SETTLE_MS;
|
|
473
|
-
if (this._settleTimer !== null)
|
|
474
|
-
return;
|
|
475
|
-
const tick = () => {
|
|
476
|
-
this._settleTimer = null;
|
|
477
|
-
if (Date.now() < this._settleAt) {
|
|
478
|
-
this._settleTimer = arm(tick);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
// The gesture is over: sharpen. A wake-up, not a repaint — nothing
|
|
482
|
-
// has moved since the last frame, so what is on screen is still
|
|
483
|
-
// right; what is needed is a frame to start rasterizing in, and each
|
|
484
|
-
// tile claims its own box as it lands.
|
|
485
|
-
this._prop('onMoveEnd')?.(this.camera());
|
|
486
|
-
this._wake('content');
|
|
487
|
-
};
|
|
488
|
-
this._settleTimer = arm(tick);
|
|
489
|
-
}
|
|
409
|
+
/** A gesture in flight, or the settle window after a camera move: while
|
|
410
|
+
* it lasts nothing is rasterized, so a drag or a wheel is composites
|
|
411
|
+
* only. The window is the controller's, and one for both renderers. */
|
|
490
412
|
get _gesturing() {
|
|
491
|
-
return this.
|
|
413
|
+
return this._controller.gesturing;
|
|
492
414
|
}
|
|
493
415
|
// --- the imperative surface ----------------------------------------------
|
|
416
|
+
//
|
|
417
|
+
// The controller's, for an application holding the element itself rather
|
|
418
|
+
// than `<Map>`'s handle.
|
|
494
419
|
getCamera() {
|
|
495
|
-
|
|
496
|
-
return { center: { ...camera.center }, zoom: camera.zoom };
|
|
420
|
+
return this._controller.getCamera();
|
|
497
421
|
}
|
|
498
422
|
setCamera(camera) {
|
|
499
|
-
|
|
500
|
-
this._applyCamera({
|
|
501
|
-
center: camera.center ?? current.center,
|
|
502
|
-
zoom: camera.zoom ?? current.zoom,
|
|
503
|
-
}, false);
|
|
423
|
+
this._controller.setCamera(camera);
|
|
504
424
|
}
|
|
505
425
|
/** Move by a distance in pane-local logical pixels. */
|
|
506
426
|
panBy(dx, dy) {
|
|
507
|
-
|
|
508
|
-
this._applyCamera({
|
|
509
|
-
zoom: transform.zoom,
|
|
510
|
-
center: unprojectPoint(transform, transform.paneX + dx, transform.paneY + dy),
|
|
511
|
-
});
|
|
427
|
+
this._controller.panBy(dx, dy);
|
|
512
428
|
}
|
|
513
429
|
zoomIn(step = 1) {
|
|
514
|
-
|
|
515
|
-
this._zoomAbout(step, pane.width / 2, pane.height / 2);
|
|
430
|
+
this._controller.zoomIn(step);
|
|
516
431
|
}
|
|
517
432
|
zoomOut(step = 1) {
|
|
518
|
-
this.
|
|
433
|
+
this._controller.zoomOut(step);
|
|
519
434
|
}
|
|
520
435
|
zoomTo(zoom) {
|
|
521
|
-
this.
|
|
436
|
+
this._controller.zoomTo(zoom);
|
|
522
437
|
}
|
|
523
438
|
fitBounds(bounds, options) {
|
|
524
|
-
|
|
525
|
-
if (pane.width <= 0 || pane.height <= 0) {
|
|
526
|
-
// Asked before layout has run — which `fitBounds` in an effect always
|
|
527
|
-
// is. Remembered and applied at the first paint that has a size.
|
|
528
|
-
this._pendingFit = { bounds, options };
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
this._applyCamera(cameraForBounds(bounds, { width: pane.width, height: pane.height }, {
|
|
532
|
-
padding: options?.padding ?? 24,
|
|
533
|
-
tileSize: DEFAULT_TILE_SIZE,
|
|
534
|
-
minZoom: this._minZoom(),
|
|
535
|
-
maxZoom: options?.maxZoom ?? this._maxZoom(),
|
|
536
|
-
}), false);
|
|
439
|
+
this._controller.fitBounds(bounds, options);
|
|
537
440
|
}
|
|
538
441
|
fitMarkers(ids, options) {
|
|
539
|
-
|
|
540
|
-
const positions = [];
|
|
541
|
-
for (const marker of this._markers()) {
|
|
542
|
-
if (wanted && !wanted.has(marker.id))
|
|
543
|
-
continue;
|
|
544
|
-
positions.push(marker.position);
|
|
545
|
-
}
|
|
546
|
-
const bounds = boundsOf(positions);
|
|
547
|
-
if (bounds)
|
|
548
|
-
this.fitBounds(bounds, options);
|
|
442
|
+
this._controller.fitMarkers(ids, options);
|
|
549
443
|
}
|
|
550
444
|
getBounds() {
|
|
551
|
-
return
|
|
445
|
+
return this._controller.getBounds();
|
|
552
446
|
}
|
|
553
447
|
project(position) {
|
|
554
|
-
return
|
|
448
|
+
return this._controller.project(position);
|
|
555
449
|
}
|
|
556
450
|
unproject(x, y) {
|
|
557
|
-
return
|
|
451
|
+
return this._controller.unproject(x, y);
|
|
558
452
|
}
|
|
559
453
|
markerAt(x, y) {
|
|
560
|
-
return
|
|
454
|
+
return this._controller.markerAt(x, y);
|
|
561
455
|
}
|
|
562
456
|
refresh() {
|
|
457
|
+
this._restyle();
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Retire every rendered tile — for a new `mapStyle`, or `refresh()` after
|
|
461
|
+
* an edit made to the style in place. One path for both, and for anything
|
|
462
|
+
* else that ever needs the map redrawn in a new look.
|
|
463
|
+
*
|
|
464
|
+
* The compiled style and the label candidates go with the tiles: both are
|
|
465
|
+
* readings of the style, and `refresh()` exists because the object was
|
|
466
|
+
* edited under them. What does not go is the picture on screen. Unless
|
|
467
|
+
* there is none yet, or `progressive` asked to watch the repaint, it is
|
|
468
|
+
* held as {@link Outgoing} until the new one is ready to replace it.
|
|
469
|
+
*
|
|
470
|
+
* True when it is held. Then nothing on screen changes yet, so the only
|
|
471
|
+
* claim is the pixel that asks for a frame to start drawing in — and
|
|
472
|
+
* anything else a commit changed has to claim its own damage.
|
|
473
|
+
*/
|
|
474
|
+
_restyle() {
|
|
475
|
+
this._prepared = null;
|
|
476
|
+
this._preparedFrom = null;
|
|
477
|
+
this._candidates.clear();
|
|
478
|
+
if (this._outgoing === null &&
|
|
479
|
+
this._painted &&
|
|
480
|
+
this._prop('progressive') !== true) {
|
|
481
|
+
this._outgoing = {
|
|
482
|
+
generation: this._cache.generation,
|
|
483
|
+
background: this._paintedBackground,
|
|
484
|
+
labels: this._labels,
|
|
485
|
+
labelZoom: this._labelZoom,
|
|
486
|
+
labelSources: this._labelSources,
|
|
487
|
+
candidates: this._labelCandidates,
|
|
488
|
+
family: this._shapedFamily,
|
|
489
|
+
view: this._viewKey(this._transform(), this._pane()),
|
|
490
|
+
moved: false,
|
|
491
|
+
waited: 0,
|
|
492
|
+
lastDrawn: null,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
563
495
|
this._cache.invalidateStyle();
|
|
564
496
|
this._labelKey = '';
|
|
565
|
-
this.
|
|
497
|
+
if (this._outgoing === null) {
|
|
498
|
+
this._repaint('props');
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
this._wake('props');
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
/** The camera and the pane, as a string that changes when either does. */
|
|
505
|
+
_viewKey(transform, pane) {
|
|
506
|
+
return `${transform.centerX},${transform.centerY},${transform.zoom},${pane.width}x${pane.height}`;
|
|
566
507
|
}
|
|
567
508
|
stats() {
|
|
568
509
|
return this._stats;
|
|
569
510
|
}
|
|
570
|
-
_pendingFit = null;
|
|
571
511
|
// --- painting ------------------------------------------------------------
|
|
572
512
|
/** How tall the attribution strip is, in logical pixels — 0 when there is
|
|
573
513
|
* nothing to say. Read by the blit as well as the paint, so the band it
|
|
@@ -576,16 +516,7 @@ export class MapViewNode extends Node {
|
|
|
576
516
|
return this._attributionText() ? 16 : 0;
|
|
577
517
|
}
|
|
578
518
|
_attributionText() {
|
|
579
|
-
|
|
580
|
-
if (given !== undefined)
|
|
581
|
-
return given;
|
|
582
|
-
const parts = [];
|
|
583
|
-
for (const source of this._sources()) {
|
|
584
|
-
if (source.attribution && !parts.includes(source.attribution)) {
|
|
585
|
-
parts.push(source.attribution);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return parts.join(' · ');
|
|
519
|
+
return attributionOf(this._prop('attribution'), this._sources());
|
|
589
520
|
}
|
|
590
521
|
/** How large a tile is rasterized, and how many surface pixels one
|
|
591
522
|
* logical pixel is — the pair the rasterizer needs, and the pair that
|
|
@@ -619,7 +550,6 @@ export class MapViewNode extends Node {
|
|
|
619
550
|
if (!this._visible() || !isMapCanvas(ctx))
|
|
620
551
|
return;
|
|
621
552
|
const started = now();
|
|
622
|
-
this._painting = true;
|
|
623
553
|
const scale = this._scale;
|
|
624
554
|
this._frameClip = damage
|
|
625
555
|
? {
|
|
@@ -629,19 +559,17 @@ export class MapViewNode extends Node {
|
|
|
629
559
|
height: damage.height / scale,
|
|
630
560
|
}
|
|
631
561
|
: null;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
this.fitBounds(bounds, options);
|
|
637
|
-
this._painting = true;
|
|
638
|
-
}
|
|
562
|
+
// A fit asked for before layout had a size — `fitBounds` in an effect —
|
|
563
|
+
// lands in the first frame that has one, and this frame draws it.
|
|
564
|
+
this._controller.beginFrame();
|
|
565
|
+
this._controller.painting = true;
|
|
639
566
|
const pane = this._pane();
|
|
640
567
|
const camera = this.camera();
|
|
641
568
|
const transform = this._transform(camera);
|
|
642
569
|
const style = this._style();
|
|
643
570
|
const frame = this._cache.beginFrame();
|
|
644
571
|
const stats = {
|
|
572
|
+
renderer: 'retained',
|
|
645
573
|
rasterMs: 0,
|
|
646
574
|
drawMs: 0,
|
|
647
575
|
tiles: 0,
|
|
@@ -649,6 +577,7 @@ export class MapViewNode extends Node {
|
|
|
649
577
|
fromAncestor: 0,
|
|
650
578
|
fromDescendant: 0,
|
|
651
579
|
pending: 0,
|
|
580
|
+
restyling: false,
|
|
652
581
|
labels: 0,
|
|
653
582
|
errors: 0,
|
|
654
583
|
surfaceBytes: 0,
|
|
@@ -677,17 +606,44 @@ export class MapViewNode extends Node {
|
|
|
677
606
|
? intersectRects(box, this._deviceRect(this._frameClip))
|
|
678
607
|
: box;
|
|
679
608
|
if (!clip) {
|
|
680
|
-
this.
|
|
609
|
+
this._controller.painting = false;
|
|
681
610
|
this._frameClip = null;
|
|
682
611
|
return;
|
|
683
612
|
}
|
|
684
613
|
ctx.rect(clip.x, clip.y, clip.width, clip.height);
|
|
685
614
|
ctx.clip();
|
|
615
|
+
// Rasterization is suspended for the length of a gesture, so a drag or
|
|
616
|
+
// a wheel is composites only. `rasterBudgetMs` bounds the rest.
|
|
617
|
+
const budget = this._gesturing
|
|
618
|
+
? 0
|
|
619
|
+
: (this._prop('rasterBudgetMs') ?? 8);
|
|
620
|
+
const deadline = started + budget;
|
|
621
|
+
this._rastered = false;
|
|
622
|
+
const progressive = this._prop('progressive') === true;
|
|
623
|
+
// **The work first, then the picture.** Every tile the cover wants is
|
|
624
|
+
// asked for, and the ones in view are drawn as far as the budget goes —
|
|
625
|
+
// into their own surfaces, so none of it is on screen yet. Which style
|
|
626
|
+
// this frame shows is decided only after that, because it depends on
|
|
627
|
+
// what the work just finished: a restyle swaps in the frame that finds
|
|
628
|
+
// the view redrawn, and has to know before the first pixel goes down.
|
|
629
|
+
const sources = this._sources();
|
|
630
|
+
const holding = this._outgoing !== null && !progressive;
|
|
631
|
+
const views = [];
|
|
632
|
+
for (let i = 0; i < sources.length; i++) {
|
|
633
|
+
views.push(this._workSource(sources[i], this._sourceId(sources[i], i), transform, pane, style, stats, budget > 0, deadline, holding));
|
|
634
|
+
}
|
|
635
|
+
this._settleRestyle(transform, pane, stats, budget, started, !damage || containsRect(damage, box), progressive);
|
|
636
|
+
const outgoing = this._outgoing;
|
|
637
|
+
stats.restyling = outgoing !== null;
|
|
686
638
|
// The style's background under everything: it is what the parts of the
|
|
687
639
|
// world with no tile yet look like, so it is most of what a map looks
|
|
688
|
-
// like while it loads.
|
|
689
|
-
const
|
|
690
|
-
|
|
640
|
+
// like while it loads. The previous style's while a restyle holds it.
|
|
641
|
+
const styleBackground = outgoing
|
|
642
|
+
? outgoing.background
|
|
643
|
+
: this._preparedBackground();
|
|
644
|
+
if (!outgoing)
|
|
645
|
+
this._paintedBackground = styleBackground;
|
|
646
|
+
const background = this.style.backgroundColor ?? styleBackground;
|
|
691
647
|
if (background) {
|
|
692
648
|
ctx.fillStyle = background;
|
|
693
649
|
const region = this._frameClip
|
|
@@ -695,16 +651,16 @@ export class MapViewNode extends Node {
|
|
|
695
651
|
: { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
696
652
|
ctx.fillRect(region.x, region.y, region.width, region.height);
|
|
697
653
|
}
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
654
|
+
// Only pictures of the generation on screen are composited: the
|
|
655
|
+
// previous style's while a restyle holds it up, the current one's
|
|
656
|
+
// otherwise — so a tile last drawn before a switch is never shown in
|
|
657
|
+
// the style the map was switched away from. `progressive` shows
|
|
658
|
+
// whatever there is, which is its point.
|
|
659
|
+
const generation = progressive
|
|
660
|
+
? undefined
|
|
661
|
+
: (outgoing?.generation ?? this._cache.generation);
|
|
706
662
|
for (let i = 0; i < sources.length; i++) {
|
|
707
|
-
this.
|
|
663
|
+
this._drawSource(ctx, sources[i], views[i], stats, progressive, generation);
|
|
708
664
|
}
|
|
709
665
|
this._paintLabels(ctx, transform, pane, style, stats);
|
|
710
666
|
const palette = this._palette();
|
|
@@ -716,7 +672,8 @@ export class MapViewNode extends Node {
|
|
|
716
672
|
stats.surfaceBytes = this._cache.surfaceBytes;
|
|
717
673
|
stats.drawMs = now() - started - stats.rasterMs;
|
|
718
674
|
this._stats = stats;
|
|
719
|
-
this.
|
|
675
|
+
this._painted = true;
|
|
676
|
+
this._controller.painting = false;
|
|
720
677
|
this._frameClip = null;
|
|
721
678
|
void frame;
|
|
722
679
|
this._prop('onFrame')?.(stats);
|
|
@@ -762,10 +719,81 @@ export class MapViewNode extends Node {
|
|
|
762
719
|
height: Math.ceil((rect.y + rect.height) * s) - y,
|
|
763
720
|
};
|
|
764
721
|
}
|
|
765
|
-
|
|
722
|
+
/**
|
|
723
|
+
* Whether this frame keeps showing the previous style or swaps in the new
|
|
724
|
+
* one — see {@link Outgoing}.
|
|
725
|
+
*
|
|
726
|
+
* The swap waits for every tile in view that has data to have a finished
|
|
727
|
+
* rendering in the new style, which is `pending` reaching zero: a tile
|
|
728
|
+
* still loading has nothing to draw and does not hold it, and a tile that
|
|
729
|
+
* is done waits for the rest rather than going up alone. It is made only
|
|
730
|
+
* in a frame that repaints the whole pane, because a swap inside a partial
|
|
731
|
+
* frame would leave the rest of the pane in the old style — so a frame
|
|
732
|
+
* that finds the view redrawn but was clipped to its one pixel claims the
|
|
733
|
+
* pane, and the next frame swaps. That is the one full-pane claim a
|
|
734
|
+
* restyle makes.
|
|
735
|
+
*
|
|
736
|
+
* Or, once the view has moved, when {@link RESTYLE_WAIT_MS} of drawing
|
|
737
|
+
* time has gone by, with whatever is left undrawn.
|
|
738
|
+
*/
|
|
739
|
+
_settleRestyle(transform, pane, stats, budget, started, whole, progressive) {
|
|
740
|
+
const outgoing = this._outgoing;
|
|
741
|
+
if (!outgoing)
|
|
742
|
+
return;
|
|
743
|
+
if (progressive) {
|
|
744
|
+
// Turned on while a restyle was held: the opt-in holds nothing.
|
|
745
|
+
this._swapStyle();
|
|
746
|
+
if (!whole)
|
|
747
|
+
this._repaint('content');
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!outgoing.moved && this._viewKey(transform, pane) !== outgoing.view) {
|
|
751
|
+
outgoing.moved = true;
|
|
752
|
+
}
|
|
753
|
+
// The clock runs across frames that could draw, two in a row, so the
|
|
754
|
+
// gap a gesture made — which drew nothing — is not counted when the
|
|
755
|
+
// next frame after it begins.
|
|
756
|
+
if (budget > 0) {
|
|
757
|
+
if (outgoing.lastDrawn !== null) {
|
|
758
|
+
outgoing.waited += started - outgoing.lastDrawn;
|
|
759
|
+
}
|
|
760
|
+
outgoing.lastDrawn = started;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
outgoing.lastDrawn = null;
|
|
764
|
+
}
|
|
765
|
+
const redrawn = stats.pending === 0;
|
|
766
|
+
// Never by the clock in the middle of a gesture: nothing is drawn
|
|
767
|
+
// during one, so the view would swap to holes it cannot fill until the
|
|
768
|
+
// gesture ends.
|
|
769
|
+
const overdue = budget > 0 && outgoing.moved && outgoing.waited >= RESTYLE_WAIT_MS;
|
|
770
|
+
if (!redrawn && !overdue)
|
|
771
|
+
return;
|
|
772
|
+
if (whole)
|
|
773
|
+
this._swapStyle();
|
|
774
|
+
else
|
|
775
|
+
this._repaint('content');
|
|
776
|
+
}
|
|
777
|
+
/** The new style goes on screen: every finished tile, its background and
|
|
778
|
+
* a label placement of its own, all in the frame being painted. */
|
|
779
|
+
_swapStyle() {
|
|
780
|
+
this._outgoing = null;
|
|
781
|
+
this._cache.swap();
|
|
782
|
+
this._labelKey = '';
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* A frame's work, for one source: want every tile of its cover, so that
|
|
786
|
+
* each loads and stays cached, and draw the ones in view into their own
|
|
787
|
+
* surfaces as far as the budget goes. Returns the ones in view, for
|
|
788
|
+
* {@link _drawSource} to composite once the frame knows which style it is
|
|
789
|
+
* showing.
|
|
790
|
+
*/
|
|
791
|
+
_workSource(source, sourceId, transform, pane, style, stats,
|
|
766
792
|
/** False for the length of a gesture, when nothing is rasterized. */
|
|
767
|
-
mayRaster, deadline
|
|
768
|
-
|
|
793
|
+
mayRaster, deadline,
|
|
794
|
+
/** A restyle is holding the previous style up, so a tile that finishes
|
|
795
|
+
* waits for the swap rather than going on screen alone. */
|
|
796
|
+
holding) {
|
|
769
797
|
const p = pyramid(source);
|
|
770
798
|
// The cover goes **deeper than the source cuts**, up to
|
|
771
799
|
// `MAX_OVERZOOM` levels past it, and the data for those tiles comes
|
|
@@ -773,12 +801,18 @@ export class MapViewNode extends Node {
|
|
|
773
801
|
// overzoomed map sharp: instead of one tile rasterized onto a surface
|
|
774
802
|
// and stretched sixty-four times, there are two hundred and fifty-six
|
|
775
803
|
// tiles sharing one fetch, each drawn at its own natural size, with
|
|
776
|
-
// detail limited by the data rather than by a bitmap.
|
|
777
|
-
|
|
804
|
+
// detail limited by the data rather than by a bitmap. That is vector
|
|
805
|
+
// data. An image has no more detail than its pixels, so a raster tile
|
|
806
|
+
// past the cut is drawn as the whole of its data tile instead — see
|
|
807
|
+
// `_rasterSquares`.
|
|
808
|
+
const cover = this._rasterSquares(source, sourceId, tileCover({ ...transform, zoom: transform.zoom }, { ...p, maxZoom: p.maxZoom + MAX_OVERZOOM }, COVER_PADDING), p.maxZoom);
|
|
778
809
|
const zoom = transform.zoom;
|
|
779
810
|
const styleZoom = Math.floor(zoom);
|
|
780
|
-
const
|
|
811
|
+
const visible = [];
|
|
781
812
|
for (const entry of cover) {
|
|
813
|
+
// In the window's logical pixels, the space `pane` is in: what the
|
|
814
|
+
// overlap tests below, the claim when the tile lands and `_composite`
|
|
815
|
+
// all take. The pane's origin goes in here and nowhere after.
|
|
782
816
|
const box = {
|
|
783
817
|
x: pane.x + entry.x,
|
|
784
818
|
y: pane.y + entry.y,
|
|
@@ -788,22 +822,27 @@ export class MapViewNode extends Node {
|
|
|
788
822
|
// Two different questions, and conflating them was a bug worth
|
|
789
823
|
// spelling out. **Whether to work on a tile** is about the pane: the
|
|
790
824
|
// cover is padded, so some of it is off screen and those tiles are
|
|
791
|
-
// wanted (so they load) but never drawn.
|
|
825
|
+
// wanted (so they load) but never drawn. They are wanted on *every*
|
|
826
|
+
// frame, however small its damage rect, because the cache's `sweep`
|
|
827
|
+
// cancels any load a frame did not want. **Whether to composite it**
|
|
792
828
|
// is about this pass's damage rect, which may be far smaller —
|
|
793
829
|
// including the deliberately tiny claim a rasterization continuation
|
|
794
|
-
// makes, which must still let the rasterizer run.
|
|
830
|
+
// makes, which must still let the rasterizer run. The first is asked
|
|
831
|
+
// here, and the second in `_drawSource`.
|
|
795
832
|
const onScreen = rectsOverlap(box, pane);
|
|
796
|
-
const inPass = this._frameClip === null || rectsOverlap(box, this._frameClip);
|
|
797
833
|
const cached = this._cache.want(source, sourceId, entry.tile, dataTileFor(entry.tile, p.maxZoom), subTileOf(entry.tile, p.maxZoom));
|
|
798
834
|
if (!onScreen)
|
|
799
835
|
continue;
|
|
836
|
+
visible.push({ entry, box, cached });
|
|
800
837
|
stats.tiles++;
|
|
801
838
|
if (cached.status === 'error')
|
|
802
839
|
stats.errors++;
|
|
803
840
|
if (cached.status === 'ready') {
|
|
804
841
|
const plan = this._rasterPlan(entry, p, zoom);
|
|
805
842
|
const size = cached.raster ? cached.raster.width : plan.size;
|
|
806
|
-
const drawing = this._cache.beginRender(cached, size, cached.raster ? 0 : styleZoom, (edge) => this._makeSurface(edge)
|
|
843
|
+
const drawing = this._cache.beginRender(cached, size, cached.raster ? 0 : styleZoom, (edge) => this._makeSurface(edge),
|
|
844
|
+
// A raster tile is the provider's image, the same in every style.
|
|
845
|
+
!cached.raster);
|
|
807
846
|
if (drawing && drawing.progress !== -1) {
|
|
808
847
|
if (cached.raster) {
|
|
809
848
|
this._uploadRaster(cached, drawing);
|
|
@@ -823,18 +862,87 @@ export class MapViewNode extends Node {
|
|
|
823
862
|
// goes blank between them. Claim the box it occupies, because
|
|
824
863
|
// *that* is the pixel change this whole sequence of frames was
|
|
825
864
|
// for; the frames before it claimed almost nothing.
|
|
826
|
-
|
|
865
|
+
//
|
|
866
|
+
// Not while a restyle holds the previous style up, though: one
|
|
867
|
+
// tile in the new style among the rest in the old is the
|
|
868
|
+
// patchwork the hold is there to prevent. It waits, finished, for
|
|
869
|
+
// `_swapStyle` to put the whole view up at once. A raster tile
|
|
870
|
+
// does not wait — it is in no style, so it cannot make one.
|
|
871
|
+
if ((!holding || !drawing.styled) && this._cache.promote(cached)) {
|
|
827
872
|
this._claim(box, 'content');
|
|
873
|
+
}
|
|
828
874
|
}
|
|
829
875
|
// "Pending" means *there is work left that this map could still
|
|
830
876
|
// do*, and nothing weaker — because `paint` asks for another frame
|
|
831
|
-
// while it is non-zero
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
// repainting a map that cannot
|
|
835
|
-
|
|
877
|
+
// while it is non-zero, and a restyle swaps when it reaches zero. A
|
|
878
|
+
// tile whose surface could not be made (a backend that has none)
|
|
879
|
+
// never becomes drawable, and counting it would spin the frame
|
|
880
|
+
// clock at the refresh rate forever, repainting a map that cannot
|
|
881
|
+
// change. A finished tile waiting for a restyle's swap has nothing
|
|
882
|
+
// left to do either.
|
|
883
|
+
if (cached.drawing && cached.drawing.progress !== -1)
|
|
836
884
|
stats.pending++;
|
|
837
885
|
}
|
|
886
|
+
}
|
|
887
|
+
return visible;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* The cover, with every **raster** tile past the source's depth drawn as
|
|
891
|
+
* the whole of its data tile — once, however many of its cells there are.
|
|
892
|
+
*
|
|
893
|
+
* The cover goes deeper than the source cuts because that is what makes
|
|
894
|
+
* vector data sharp there: each cell is rasterized from its data tile's
|
|
895
|
+
* features, through the cell (`sub`, in `_rasterize`). An image has no
|
|
896
|
+
* detail finer than its pixels and no drawing it through a cell: a raster
|
|
897
|
+
* cell was drawn by uploading the image, all of it, so each cell showed
|
|
898
|
+
* its data tile whole, shrunk into its own square. Past a raster source's
|
|
899
|
+
* depth the map was a grid of miniatures of each tile — 2×2 one level
|
|
900
|
+
* past, 4×4 two — and a 256px source is read a level deeper than the
|
|
901
|
+
* view, so for `osmRasterSource`, which cuts at 19, that was every zoom
|
|
902
|
+
* from 19 up.
|
|
903
|
+
*
|
|
904
|
+
* A raster map past its provider's depth draws the deepest images larger,
|
|
905
|
+
* and so does this. The square is where the cover at the source's own
|
|
906
|
+
* depth puts the data tile, and it is drawn from that tile's own surface —
|
|
907
|
+
* the one the view at that depth draws, so crossing into overzoom uploads
|
|
908
|
+
* nothing.
|
|
909
|
+
*
|
|
910
|
+
* Which kind a tile is, is known once its data is in. Until then a cell
|
|
911
|
+
* stays a cell, and is covered from its neighbours in the pyramid like
|
|
912
|
+
* any other hole. Asking is a `want`, which the frame's own pass repeats
|
|
913
|
+
* for a cell kept here, and which is idempotent.
|
|
914
|
+
*/
|
|
915
|
+
_rasterSquares(source, sourceId, cover, maxZoom) {
|
|
916
|
+
// A cover is one level, so every entry in it is past the cut or none is.
|
|
917
|
+
if (cover.length === 0 || cover[0].tile.z <= maxZoom)
|
|
918
|
+
return cover;
|
|
919
|
+
const out = [];
|
|
920
|
+
const squares = new Set();
|
|
921
|
+
for (const cell of cover) {
|
|
922
|
+
const cached = this._cache.want(source, sourceId, cell.tile, dataTileFor(cell.tile, maxZoom), subTileOf(cell.tile, maxZoom));
|
|
923
|
+
if (!cached.raster) {
|
|
924
|
+
out.push(cell);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const square = dataSquareOf(cell, maxZoom);
|
|
928
|
+
const key = `${square.worldCopy}:${tileKey(square.tile)}`;
|
|
929
|
+
if (squares.has(key))
|
|
930
|
+
continue;
|
|
931
|
+
squares.add(key);
|
|
932
|
+
out.push(square);
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* A frame's picture, for one source: composite each tile in view from its
|
|
938
|
+
* own finished rendering or, for a tile with none, from a finished
|
|
939
|
+
* ancestor or descendants. Only renderings of `generation` count, and
|
|
940
|
+
* `undefined` counts any — which is `progressive`.
|
|
941
|
+
*/
|
|
942
|
+
_drawSource(ctx, source, visible, stats, progressive, generation) {
|
|
943
|
+
const scale = this._scale;
|
|
944
|
+
for (const { entry, box, cached } of visible) {
|
|
945
|
+
const inPass = this._frameClip === null || rectsOverlap(box, this._frameClip);
|
|
838
946
|
// What is composited is `shown`, which is **finished by
|
|
839
947
|
// construction** — a rendering only becomes `shown` when its last
|
|
840
948
|
// style run is done. So a tile appears whole rather than as water,
|
|
@@ -845,22 +953,16 @@ export class MapViewNode extends Node {
|
|
|
845
953
|
// `progressive` composites the draft instead, which is the old
|
|
846
954
|
// behaviour and is honest about what the renderer is doing.
|
|
847
955
|
const showing = progressive && cached.drawing ? cached.drawing : cached.shown;
|
|
848
|
-
if (showing) {
|
|
956
|
+
if (showing && drawnFor(showing, generation)) {
|
|
849
957
|
if (!inPass)
|
|
850
958
|
continue;
|
|
851
|
-
this._composite(ctx, showing.surface, showing.size, box,
|
|
959
|
+
this._composite(ctx, showing.surface, showing.size, box, scale, 0, 0, 1);
|
|
852
960
|
stats.ready++;
|
|
853
961
|
continue;
|
|
854
962
|
}
|
|
855
|
-
// Nothing of this tile
|
|
856
|
-
// help
|
|
857
|
-
//
|
|
858
|
-
// empty on every zoom.
|
|
859
|
-
if (!inPass)
|
|
860
|
-
continue;
|
|
861
|
-
// Nothing of this tile yet — a first load, which no buffering can
|
|
862
|
-
// help. Two ways to cover it, and which is available says which way
|
|
863
|
-
// the camera moved.
|
|
963
|
+
// Nothing of this tile to show — a first load, which no buffering can
|
|
964
|
+
// help, or a picture in a style the map is not showing. Two ways to
|
|
965
|
+
// cover it, and which is available says which way the camera moved.
|
|
864
966
|
//
|
|
865
967
|
// **Zooming in**, the tile already in hand is this one's *ancestor*:
|
|
866
968
|
// one composite, scaled up, blurry but complete. **Zooming out**, the
|
|
@@ -874,17 +976,26 @@ export class MapViewNode extends Node {
|
|
|
874
976
|
// sharper and they are the level the user is coming *from*; the
|
|
875
977
|
// ancestor wins when they do not, because a complete blurry picture
|
|
876
978
|
// beats a sharp one with holes in it.
|
|
877
|
-
|
|
979
|
+
//
|
|
980
|
+
// Looked up whether or not this pass draws the tile, because the
|
|
981
|
+
// lookup is what stamps a piece as in use: a piece covering a hole is
|
|
982
|
+
// on screen, and an unstamped entry is the first thing eviction takes
|
|
983
|
+
// — which, in the one-pixel frames a redraw runs in, was every piece
|
|
984
|
+
// but the one under that pixel. A restyle keeps such pieces on screen
|
|
985
|
+
// for as long as its redraw takes.
|
|
986
|
+
const kids = this._cache.descendantsWithSurface(source, entry.tile, undefined, generation);
|
|
878
987
|
const covered = kids.length > 0 && kids.length === kids[0].span * kids[0].span;
|
|
879
988
|
const ancestor = covered
|
|
880
989
|
? null
|
|
881
|
-
: this._cache.ancestorWithSurface(
|
|
990
|
+
: this._cache.ancestorWithSurface(source, entry.tile, undefined, generation);
|
|
991
|
+
if (!inPass)
|
|
992
|
+
continue;
|
|
882
993
|
if (ancestor?.shown) {
|
|
883
994
|
const up = entry.tile.z - ancestor.tile.z;
|
|
884
995
|
const span = 1 << up;
|
|
885
996
|
const fx = entry.tile.x - (ancestor.tile.x << up);
|
|
886
997
|
const fy = entry.tile.y - (ancestor.tile.y << up);
|
|
887
|
-
this._composite(ctx, ancestor.shown.surface, ancestor.shown.size, box,
|
|
998
|
+
this._composite(ctx, ancestor.shown.surface, ancestor.shown.size, box, scale, fx, fy, span);
|
|
888
999
|
stats.fromAncestor++;
|
|
889
1000
|
}
|
|
890
1001
|
else if (kids.length > 0) {
|
|
@@ -895,7 +1006,7 @@ export class MapViewNode extends Node {
|
|
|
895
1006
|
y: box.y + kid.y * piece,
|
|
896
1007
|
width: piece,
|
|
897
1008
|
height: piece,
|
|
898
|
-
},
|
|
1009
|
+
}, scale, 0, 0, 1);
|
|
899
1010
|
}
|
|
900
1011
|
stats.fromDescendant++;
|
|
901
1012
|
}
|
|
@@ -1030,18 +1141,28 @@ export class MapViewNode extends Node {
|
|
|
1030
1141
|
* point. Clipping the destination and moving the source rectangle to
|
|
1031
1142
|
* match keeps the scale factor `sw/dw` exactly what it was, so nothing
|
|
1032
1143
|
* about the picture changes.
|
|
1144
|
+
*
|
|
1145
|
+
* **`dest` is already in the window's space**, the one `pane` is in — a
|
|
1146
|
+
* tile's `box` is `pane.x + entry.x` — so nothing is added to it here.
|
|
1147
|
+
* The pane's origin used to be added a second time, which drew every
|
|
1148
|
+
* tile that far right of and below where the markers, labels and
|
|
1149
|
+
* overlays put the same place. A constant offset on screen is a
|
|
1150
|
+
* different distance on the ground at every zoom, so it looked like a
|
|
1151
|
+
* marker sliding across the map as it zoomed, and like nothing at all as
|
|
1152
|
+
* it panned, where the blit moves both together. Every test mounted the
|
|
1153
|
+
* map at the window's origin, where the offset is zero.
|
|
1033
1154
|
*/
|
|
1034
1155
|
_composite(ctx, surface, size,
|
|
1035
|
-
/** Where it lands, in
|
|
1036
|
-
dest,
|
|
1156
|
+
/** Where it lands, in the window's logical pixels. */
|
|
1157
|
+
dest, scale,
|
|
1037
1158
|
/** Which sub-square of the surface to take, in `subSpan`ths. */
|
|
1038
1159
|
subX, subY, subSpan) {
|
|
1039
1160
|
if (!ctx.drawImage || size <= 0)
|
|
1040
1161
|
return;
|
|
1041
|
-
const x0 = Math.round(
|
|
1042
|
-
const y0 = Math.round(
|
|
1043
|
-
const x1 = Math.round((
|
|
1044
|
-
const y1 = Math.round((
|
|
1162
|
+
const x0 = Math.round(dest.x * scale);
|
|
1163
|
+
const y0 = Math.round(dest.y * scale);
|
|
1164
|
+
const x1 = Math.round((dest.x + dest.width) * scale);
|
|
1165
|
+
const y1 = Math.round((dest.y + dest.height) * scale);
|
|
1045
1166
|
if (x1 <= x0 || y1 <= y0)
|
|
1046
1167
|
return;
|
|
1047
1168
|
// The clip is the content box — device pixels, like everything core
|
|
@@ -1083,7 +1204,11 @@ export class MapViewNode extends Node {
|
|
|
1083
1204
|
if (!fonts)
|
|
1084
1205
|
return; // headless: nothing to shape with
|
|
1085
1206
|
const text = this.resolvedTextStyle();
|
|
1086
|
-
const
|
|
1207
|
+
const outgoing = this._outgoing;
|
|
1208
|
+
// While a restyle holds the previous style up, its labels are set in
|
|
1209
|
+
// its face.
|
|
1210
|
+
const family = outgoing?.family ??
|
|
1211
|
+
this._prop('mapStyle')?.fontFamily ??
|
|
1087
1212
|
this._defaultStyle?.fontFamily ??
|
|
1088
1213
|
text.family;
|
|
1089
1214
|
if (!this._shaper) {
|
|
@@ -1092,24 +1217,60 @@ export class MapViewNode extends Node {
|
|
|
1092
1217
|
else {
|
|
1093
1218
|
this._shaper.reconfigure(fonts, family, this._scale);
|
|
1094
1219
|
}
|
|
1095
|
-
const
|
|
1220
|
+
const clip = this._frameClip ? { ...this._frameClip } : null;
|
|
1221
|
+
if (outgoing) {
|
|
1222
|
+
// The labels that were on screen when the restyle began, and not the
|
|
1223
|
+
// new style's, which would go up over a map still drawn in the old
|
|
1224
|
+
// one. Re-placed only when they have to be — a placement is for one
|
|
1225
|
+
// zoom, and a source taken off the map takes its names with it — and
|
|
1226
|
+
// from what they were placed from, since the style that produced
|
|
1227
|
+
// that is being replaced, or was edited in place.
|
|
1228
|
+
const zoom = quantizeZoom(transform.zoom);
|
|
1229
|
+
const sources = this._sources();
|
|
1230
|
+
if (zoom !== outgoing.labelZoom ||
|
|
1231
|
+
!sameSources(sources, outgoing.labelSources)) {
|
|
1232
|
+
const candidates = [];
|
|
1233
|
+
for (const source of sources) {
|
|
1234
|
+
for (const candidate of outgoing.candidates.get(source) ?? []) {
|
|
1235
|
+
candidates.push(candidate);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
outgoing.labels = placeLabels(candidates, transform.world, this._shaper);
|
|
1239
|
+
outgoing.labelZoom = zoom;
|
|
1240
|
+
outgoing.labelSources = sources;
|
|
1241
|
+
}
|
|
1242
|
+
stats.labels = drawLabels(ctx, outgoing.labels, transform, pane, this._scale, clip, this._shaper);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
this._shapedFamily = family;
|
|
1246
|
+
const key = `${quantizeZoom(transform.zoom)}|${this._cache.generation}`;
|
|
1096
1247
|
if (key !== this._labelKey) {
|
|
1097
1248
|
this._labelKey = key;
|
|
1098
1249
|
const styleZoom = Math.floor(transform.zoom);
|
|
1099
1250
|
// Labels come from the tile the **data** came from, at the depth that
|
|
1100
1251
|
// source actually cuts — past which many renderings share one tile
|
|
1101
1252
|
// and collecting per rendering would place every label `span²` times.
|
|
1102
|
-
|
|
1253
|
+
//
|
|
1254
|
+
// And only from the sources on the map now. The cache keeps a
|
|
1255
|
+
// provider's tiles after it is switched away, so that switching back
|
|
1256
|
+
// is free; collecting from all of them drew the old provider's place
|
|
1257
|
+
// names over the new one's map until eviction reached them.
|
|
1258
|
+
const sources = this._sources();
|
|
1259
|
+
const wanted = new Map();
|
|
1260
|
+
for (const source of sources) {
|
|
1261
|
+
wanted.set(source, Math.min(styleZoom, pyramid(source).maxZoom));
|
|
1262
|
+
}
|
|
1103
1263
|
const candidates = [];
|
|
1264
|
+
const bySource = new Map();
|
|
1104
1265
|
for (const cached of this._cache.dataEntries()) {
|
|
1105
1266
|
if (cached.status !== 'ready' || !cached.vector)
|
|
1106
1267
|
continue;
|
|
1107
|
-
if (
|
|
1268
|
+
if (wanted.get(cached.source) !== cached.tile.z)
|
|
1108
1269
|
continue;
|
|
1109
1270
|
const at = `${cached.key}|${styleZoom}`;
|
|
1110
1271
|
let found = this._candidates.get(at);
|
|
1111
1272
|
if (!found) {
|
|
1112
|
-
found = collectLabels(cached.vector, cached.tile, style, styleZoom
|
|
1273
|
+
found = collectLabels(cached.vector, cached.tile, style, styleZoom);
|
|
1113
1274
|
// Bounded the way the shaper's cache is: a map panned across a
|
|
1114
1275
|
// continent must not turn this into a leak, and rebuilding a
|
|
1115
1276
|
// tile's candidates is one walk over its symbol layers.
|
|
@@ -1117,12 +1278,22 @@ export class MapViewNode extends Node {
|
|
|
1117
1278
|
this._candidates.clear();
|
|
1118
1279
|
this._candidates.set(at, found);
|
|
1119
1280
|
}
|
|
1120
|
-
|
|
1281
|
+
let own = bySource.get(cached.source);
|
|
1282
|
+
if (!own) {
|
|
1283
|
+
own = [];
|
|
1284
|
+
bySource.set(cached.source, own);
|
|
1285
|
+
}
|
|
1286
|
+
for (const candidate of found) {
|
|
1121
1287
|
candidates.push(candidate);
|
|
1288
|
+
own.push(candidate);
|
|
1289
|
+
}
|
|
1122
1290
|
}
|
|
1123
1291
|
this._labels = placeLabels(candidates, transform.world, this._shaper);
|
|
1292
|
+
this._labelZoom = quantizeZoom(transform.zoom);
|
|
1293
|
+
this._labelSources = sources;
|
|
1294
|
+
this._labelCandidates = bySource;
|
|
1124
1295
|
}
|
|
1125
|
-
stats.labels = drawLabels(ctx, this._labels, transform, pane, this._scale,
|
|
1296
|
+
stats.labels = drawLabels(ctx, this._labels, transform, pane, this._scale, clip, this._shaper);
|
|
1126
1297
|
}
|
|
1127
1298
|
/**
|
|
1128
1299
|
* The attribution.
|
|
@@ -1137,152 +1308,60 @@ export class MapViewNode extends Node {
|
|
|
1137
1308
|
const text = this._attributionText();
|
|
1138
1309
|
if (!text || !this._shaper)
|
|
1139
1310
|
return;
|
|
1140
|
-
const shaped = this._shaper.shape(text,
|
|
1311
|
+
const shaped = this._shaper.shape(text, ATTRIBUTION_SIZE, palette.text);
|
|
1141
1312
|
if (!shaped)
|
|
1142
1313
|
return;
|
|
1143
|
-
const
|
|
1144
|
-
const padding = 4;
|
|
1145
|
-
const width = shaped.width + padding * 2;
|
|
1146
|
-
const height = shaped.height + padding;
|
|
1147
|
-
const x = pane.x + pane.width - width;
|
|
1148
|
-
const y = pane.y + pane.height - height;
|
|
1314
|
+
const at = attributionLayout(shaped, pane, this._scale);
|
|
1149
1315
|
ctx.save();
|
|
1150
1316
|
if (ctx.globalAlpha !== undefined)
|
|
1151
|
-
ctx.globalAlpha =
|
|
1317
|
+
ctx.globalAlpha = ATTRIBUTION_OPACITY;
|
|
1152
1318
|
ctx.fillStyle = palette.background;
|
|
1153
|
-
ctx.fillRect(
|
|
1319
|
+
ctx.fillRect(at.box.x, at.box.y, at.box.width, at.box.height);
|
|
1154
1320
|
if (ctx.globalAlpha !== undefined)
|
|
1155
1321
|
ctx.globalAlpha = 1;
|
|
1156
|
-
shaped.layout.draw(ctx,
|
|
1322
|
+
shaped.layout.draw(ctx, at.x, at.y);
|
|
1157
1323
|
ctx.restore();
|
|
1158
1324
|
}
|
|
1159
1325
|
// --- behaviour -----------------------------------------------------------
|
|
1160
|
-
/**
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
|
|
1326
|
+
/**
|
|
1327
|
+
* An event as the controller reads it. A synthetic event's `x`/`y` are
|
|
1328
|
+
* logical and relative to the window, so only the pane's own origin has
|
|
1329
|
+
* to come off.
|
|
1330
|
+
*/
|
|
1331
|
+
_input(ev) {
|
|
1164
1332
|
const pane = this._pane();
|
|
1165
|
-
return { x: ev.x - pane.x, y: ev.y - pane.y };
|
|
1166
|
-
}
|
|
1167
|
-
_pointerEvent(ev, marker) {
|
|
1168
|
-
const point = this._point(ev);
|
|
1169
1333
|
return {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1334
|
+
x: ev.x - pane.x,
|
|
1335
|
+
y: ev.y - pane.y,
|
|
1336
|
+
button: ev.button,
|
|
1337
|
+
// The click count core puts on a press and on its release, which the
|
|
1338
|
+
// declarations do not name.
|
|
1339
|
+
detail: ev.detail,
|
|
1340
|
+
shiftKey: ev.shiftKey,
|
|
1341
|
+
ctrlKey: ev.ctrlKey,
|
|
1342
|
+
altKey: ev.altKey,
|
|
1343
|
+
metaKey: ev.metaKey,
|
|
1179
1344
|
};
|
|
1180
1345
|
}
|
|
1181
1346
|
defaultMouseDown(ev) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
id: marker.id,
|
|
1188
|
-
startX: point.x,
|
|
1189
|
-
startY: point.y,
|
|
1190
|
-
};
|
|
1191
|
-
ev.capturePointer?.();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (!this._interactive()) {
|
|
1195
|
-
// Not a pan, but still a press: the release is what makes a click,
|
|
1196
|
-
// and an application listening for one on a frozen map should get it.
|
|
1197
|
-
this._gesture = {
|
|
1198
|
-
kind: 'pan',
|
|
1199
|
-
startX: point.x,
|
|
1200
|
-
startY: point.y,
|
|
1201
|
-
lastX: point.x,
|
|
1202
|
-
lastY: point.y,
|
|
1203
|
-
moved: false,
|
|
1204
|
-
};
|
|
1205
|
-
ev.capturePointer?.();
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
this._gesture = {
|
|
1209
|
-
kind: 'pan',
|
|
1210
|
-
startX: point.x,
|
|
1211
|
-
startY: point.y,
|
|
1212
|
-
lastX: point.x,
|
|
1213
|
-
lastY: point.y,
|
|
1214
|
-
moved: false,
|
|
1215
|
-
};
|
|
1216
|
-
this.focus();
|
|
1347
|
+
// A pan takes the focus, so the arrows work next. A press on a marker,
|
|
1348
|
+
// or on a map that does not move, is captured all the same: its release
|
|
1349
|
+
// is a click, and an application listening for one should get it.
|
|
1350
|
+
if (this._controller.pointerDown(this._input(ev)) === 'pan')
|
|
1351
|
+
this.focus();
|
|
1217
1352
|
ev.capturePointer?.();
|
|
1218
1353
|
}
|
|
1219
1354
|
defaultMouseDrag(ev) {
|
|
1220
|
-
|
|
1221
|
-
if (!gesture || gesture.kind !== 'pan')
|
|
1222
|
-
return;
|
|
1223
|
-
if (!this._interactive())
|
|
1224
|
-
return;
|
|
1225
|
-
const point = this._point(ev);
|
|
1226
|
-
if (!gesture.moved &&
|
|
1227
|
-
Math.abs(point.x - gesture.startX) < DRAG_THRESHOLD &&
|
|
1228
|
-
Math.abs(point.y - gesture.startY) < DRAG_THRESHOLD) {
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
gesture.moved = true;
|
|
1232
|
-
// Whole device pixels, because that is what the blit can shift — a
|
|
1233
|
-
// fractional pan would decline it every frame and repaint the pane.
|
|
1234
|
-
const scale = this._scale;
|
|
1235
|
-
const dx = Math.round((point.x - gesture.lastX) * scale) / scale;
|
|
1236
|
-
const dy = Math.round((point.y - gesture.lastY) * scale) / scale;
|
|
1237
|
-
if (dx === 0 && dy === 0)
|
|
1238
|
-
return;
|
|
1239
|
-
gesture.lastX += dx;
|
|
1240
|
-
gesture.lastY += dy;
|
|
1241
|
-
this._touchGesture();
|
|
1242
|
-
this.panBy(-dx, -dy);
|
|
1355
|
+
this._controller.pointerDrag(this._input(ev));
|
|
1243
1356
|
}
|
|
1244
1357
|
defaultMouseUp(ev) {
|
|
1245
|
-
|
|
1246
|
-
this._gesture = null;
|
|
1247
|
-
if (!gesture)
|
|
1248
|
-
return;
|
|
1249
|
-
const point = this._point(ev);
|
|
1250
|
-
if (gesture.kind === 'marker') {
|
|
1251
|
-
const marker = this._markers().find((m) => m.id === gesture.id);
|
|
1252
|
-
if (marker &&
|
|
1253
|
-
Math.abs(point.x - gesture.startX) < DRAG_THRESHOLD &&
|
|
1254
|
-
Math.abs(point.y - gesture.startY) < DRAG_THRESHOLD) {
|
|
1255
|
-
const event = this._pointerEvent(ev, marker);
|
|
1256
|
-
this._prop('onMarkerClick')?.(marker, event);
|
|
1257
|
-
this._prop('onMapClick')?.(event);
|
|
1258
|
-
}
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
if (gesture.moved) {
|
|
1262
|
-
this._touchGesture();
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
this._prop('onMapClick')?.(this._pointerEvent(ev, null));
|
|
1358
|
+
this._controller.pointerUp(this._input(ev));
|
|
1266
1359
|
}
|
|
1267
1360
|
defaultMouseMove(ev) {
|
|
1268
|
-
|
|
1269
|
-
if (!notify)
|
|
1270
|
-
return;
|
|
1271
|
-
const point = this._point(ev);
|
|
1272
|
-
const marker = markerAt(this._markers(), this._transform(), point.x, point.y);
|
|
1273
|
-
const id = marker?.id ?? null;
|
|
1274
|
-
if (id === this._hover)
|
|
1275
|
-
return;
|
|
1276
|
-
this._hover = id;
|
|
1277
|
-
notify(marker, this._pointerEvent(ev, marker));
|
|
1361
|
+
this._controller.pointerMove(this._input(ev));
|
|
1278
1362
|
}
|
|
1279
1363
|
defaultMouseLeave() {
|
|
1280
|
-
|
|
1281
|
-
return;
|
|
1282
|
-
this._hover = null;
|
|
1283
|
-
// No event: the pointer has left the map, so there is no position on it
|
|
1284
|
-
// to report and inventing one would be worse than saying so.
|
|
1285
|
-
this._prop('onMarkerHover')?.(null, null);
|
|
1364
|
+
this._controller.pointerLeave();
|
|
1286
1365
|
}
|
|
1287
1366
|
/**
|
|
1288
1367
|
* The wheel is a zoom, not a scroll, so it is answered here rather than
|
|
@@ -1291,119 +1370,131 @@ export class MapViewNode extends Node {
|
|
|
1291
1370
|
* scroll chain never hands over.
|
|
1292
1371
|
*/
|
|
1293
1372
|
defaultWheel(ev) {
|
|
1294
|
-
if (!this._interactive())
|
|
1295
|
-
return;
|
|
1296
1373
|
const pane = this._pane();
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
|
|
1374
|
+
const point = { x: ev.x - pane.x, y: ev.y - pane.y };
|
|
1375
|
+
// Consumed whether or not the zoom moved: a wheel over a map that moves
|
|
1376
|
+
// is never meant for whatever is behind it.
|
|
1377
|
+
// `ev.smooth` is the device: a wheel that clicks whole notches, or a
|
|
1378
|
+
// touchpad whose valuators measured the scroll. The controller eases
|
|
1379
|
+
// the one and applies the other (`MapController.wheel`).
|
|
1380
|
+
if (this._controller.wheel(point, ev.deltaY ?? 0, ev.smooth === true)) {
|
|
1381
|
+
ev.preventDefault();
|
|
1382
|
+
}
|
|
1302
1383
|
}
|
|
1303
1384
|
defaultKeyDown(ev) {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
// optional call rather than a plain one.
|
|
1307
|
-
if (!this._interactive()) {
|
|
1308
|
-
super.defaultKeyDown?.(ev);
|
|
1385
|
+
if (this._controller.keyDown(ev.keysym ?? 0, ev.shiftKey ?? false)) {
|
|
1386
|
+
ev.preventDefault();
|
|
1309
1387
|
return;
|
|
1310
1388
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
case 0xff54: // XK_Down
|
|
1324
|
-
this.panBy(0, step);
|
|
1325
|
-
break;
|
|
1326
|
-
case 0x002b: // XK_plus
|
|
1327
|
-
case 0x003d: // XK_equal
|
|
1328
|
-
case 0xffab: // XK_KP_Add
|
|
1329
|
-
this._zoomAbout(1, pane.width / 2, pane.height / 2);
|
|
1330
|
-
break;
|
|
1331
|
-
case 0x002d: // XK_minus
|
|
1332
|
-
case 0xffad: // XK_KP_Subtract
|
|
1333
|
-
this._zoomAbout(-1, pane.width / 2, pane.height / 2);
|
|
1334
|
-
break;
|
|
1335
|
-
default:
|
|
1336
|
-
// Everything else goes to the base class, which is what keeps the
|
|
1337
|
-
// selection keys and Space/Enter-as-a-click working.
|
|
1338
|
-
super.defaultKeyDown?.(ev);
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
ev.preventDefault();
|
|
1389
|
+
// Everything else goes to the base class, which is what keeps the
|
|
1390
|
+
// selection keys and Space/Enter-as-a-click working. `Node` declares the
|
|
1391
|
+
// default actions optional — an element that has no behaviour of its
|
|
1392
|
+
// own simply has none — so calling up is an optional call.
|
|
1393
|
+
super.defaultKeyDown?.(ev);
|
|
1394
|
+
}
|
|
1395
|
+
/** What a screen reader meets: the markers in view, as buttons — the
|
|
1396
|
+
* controller's answer, which both renderers give. */
|
|
1397
|
+
a11yScene() {
|
|
1398
|
+
if (!this._visible())
|
|
1399
|
+
return [];
|
|
1400
|
+
return this._controller.markerScene();
|
|
1342
1401
|
}
|
|
1402
|
+
/** Every source this element has been handed, and how many `sources`
|
|
1403
|
+
* changes in a row have made a slot anew — see `_noticeRemade`. */
|
|
1404
|
+
_seenSources = new WeakSet();
|
|
1405
|
+
_remade = 0;
|
|
1406
|
+
_warnedRemade = false;
|
|
1343
1407
|
/**
|
|
1344
|
-
*
|
|
1408
|
+
* Say so, once, when the application makes a source anew on every
|
|
1409
|
+
* render.
|
|
1410
|
+
*
|
|
1411
|
+
* Tiles are cached per source object (see `./tiles.ts`), so a source
|
|
1412
|
+
* rebuilt on each render starts from an empty cache on each render: every
|
|
1413
|
+
* tile in view is fetched again, and the map is its background until they
|
|
1414
|
+
* land. Under a controlled camera that is a render per pan step — a drag
|
|
1415
|
+
* across a blank map that fetches the whole view on every step.
|
|
1345
1416
|
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1417
|
+
* What gives it away is a slot handed a source this element has never
|
|
1418
|
+
* had, looking like the one it replaces — the same id, pyramid and
|
|
1419
|
+
* attribution — on three `sources` changes in a row. A provider switch
|
|
1420
|
+
* does that once, and a switch back to a source already shown does not
|
|
1421
|
+
* count at all. Looking alike is only ever the basis of a warning, never
|
|
1422
|
+
* of a cache key: two providers can look exactly alike.
|
|
1423
|
+
*
|
|
1424
|
+
* `process` and `console` come off `globalThis` because `src/` compiles
|
|
1425
|
+
* with `types: []`, as for the terminal's warning.
|
|
1351
1426
|
*/
|
|
1352
|
-
|
|
1353
|
-
if (
|
|
1354
|
-
return
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1427
|
+
_noticeRemade(before, next) {
|
|
1428
|
+
if (this._warnedRemade)
|
|
1429
|
+
return;
|
|
1430
|
+
let slot = -1;
|
|
1431
|
+
if (before && next) {
|
|
1432
|
+
const length = Math.min(before.length, next.length);
|
|
1433
|
+
for (let i = 0; i < length && slot < 0; i++) {
|
|
1434
|
+
const was = before[i];
|
|
1435
|
+
const now = next[i];
|
|
1436
|
+
if (was &&
|
|
1437
|
+
now &&
|
|
1438
|
+
now !== was &&
|
|
1439
|
+
!this._seenSources.has(now) &&
|
|
1440
|
+
now.id === was.id &&
|
|
1441
|
+
now.tileSize === was.tileSize &&
|
|
1442
|
+
now.minZoom === was.minZoom &&
|
|
1443
|
+
now.maxZoom === was.maxZoom &&
|
|
1444
|
+
now.attribution === was.attribution) {
|
|
1445
|
+
slot = i;
|
|
1446
|
+
}
|
|
1366
1447
|
}
|
|
1367
|
-
items.push({
|
|
1368
|
-
id: `marker:${marker.id}`,
|
|
1369
|
-
// Device pixels in the owning window's coordinates, which is what
|
|
1370
|
-
// an a11y scene rect is — the same space as `abs`.
|
|
1371
|
-
rect: {
|
|
1372
|
-
x: (pane.x + rect.x) * scale,
|
|
1373
|
-
y: (pane.y + rect.y) * scale,
|
|
1374
|
-
width: rect.width * scale,
|
|
1375
|
-
height: rect.height * scale,
|
|
1376
|
-
},
|
|
1377
|
-
role: 'button',
|
|
1378
|
-
name: marker.title ??
|
|
1379
|
-
`${marker.position.lat.toFixed(4)}, ${marker.position.lon.toFixed(4)}`,
|
|
1380
|
-
states: { selected: marker.selected ?? false },
|
|
1381
|
-
});
|
|
1382
1448
|
}
|
|
1383
|
-
|
|
1449
|
+
for (const source of before ?? [])
|
|
1450
|
+
this._seenSources.add(source);
|
|
1451
|
+
for (const source of next ?? [])
|
|
1452
|
+
this._seenSources.add(source);
|
|
1453
|
+
this._remade = slot < 0 ? 0 : this._remade + 1;
|
|
1454
|
+
if (this._remade < 3)
|
|
1455
|
+
return;
|
|
1456
|
+
this._warnedRemade = true;
|
|
1457
|
+
const g = globalThis;
|
|
1458
|
+
if (g.process?.env?.NODE_ENV === 'production')
|
|
1459
|
+
return;
|
|
1460
|
+
g.console?.warn(`@react-x11/components: <Map> was handed a new sources[${slot}] on ` +
|
|
1461
|
+
'three renders in a row. Tiles are cached per source object, so ' +
|
|
1462
|
+
'each one started from an empty cache and fetched every tile in ' +
|
|
1463
|
+
'view again. If it is the same provider each time, make it once — ' +
|
|
1464
|
+
'at module scope, or with useMemo.');
|
|
1384
1465
|
}
|
|
1385
1466
|
applyProps(next, prev) {
|
|
1386
1467
|
const before = prev ?? this.props;
|
|
1387
1468
|
super.applyProps(next, prev);
|
|
1469
|
+
if (this._ownsController) {
|
|
1470
|
+
this._controller.setProps(this.props);
|
|
1471
|
+
}
|
|
1472
|
+
if (next.sources !== before.sources) {
|
|
1473
|
+
this._noticeRemade(before.sources, next.sources);
|
|
1474
|
+
}
|
|
1388
1475
|
// Every one of these is in `selfDamagedProps`, so the commit claimed
|
|
1389
1476
|
// nothing for them and this is the only claim there will be.
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
this._repaint('props');
|
|
1477
|
+
//
|
|
1478
|
+
// A restyle that repainted the pane covers everything else the commit
|
|
1479
|
+
// changed. One that is held claims a single pixel — nothing on screen
|
|
1480
|
+
// changes until the swap — so a camera or the markers moved in the same
|
|
1481
|
+
// commit still have to claim their own damage below.
|
|
1482
|
+
if (next.mapStyle !== before.mapStyle && !this._restyle())
|
|
1397
1483
|
return;
|
|
1398
|
-
}
|
|
1399
1484
|
if (next.sources !== before.sources) {
|
|
1485
|
+
// Nothing to throw away here: tiles are filed under the source
|
|
1486
|
+
// object, so a new provider starts empty, and one switched away from
|
|
1487
|
+
// keeps the tiles it has for when it comes back — its loads still in
|
|
1488
|
+
// flight are cancelled by the next frame's `sweep`, since nothing asks
|
|
1489
|
+
// for them. Only the label placement is stale.
|
|
1400
1490
|
this._labelKey = '';
|
|
1401
1491
|
this._repaint('props');
|
|
1402
1492
|
return;
|
|
1403
1493
|
}
|
|
1404
1494
|
if (next.camera !== before.camera && next.camera !== undefined) {
|
|
1405
1495
|
const camera = next.camera;
|
|
1406
|
-
const previous = before.camera ??
|
|
1496
|
+
const previous = before.camera ??
|
|
1497
|
+
this._controller.ownCamera();
|
|
1407
1498
|
if (camera.zoom !== previous.zoom ||
|
|
1408
1499
|
camera.center.lon !== previous.center.lon ||
|
|
1409
1500
|
camera.center.lat !== previous.center.lat) {
|
|
@@ -1446,13 +1537,14 @@ export class MapViewNode extends Node {
|
|
|
1446
1537
|
claim(after);
|
|
1447
1538
|
}
|
|
1448
1539
|
destroySubtree() {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
this.
|
|
1452
|
-
}
|
|
1540
|
+
this._controller.detach(this._view);
|
|
1541
|
+
if (this._ownsController)
|
|
1542
|
+
this._controller.dispose();
|
|
1453
1543
|
this._cache.destroy();
|
|
1454
1544
|
this._candidates.clear();
|
|
1455
1545
|
this._labels = [];
|
|
1546
|
+
this._labelCandidates = new Map();
|
|
1547
|
+
this._outgoing = null;
|
|
1456
1548
|
super.destroySubtree();
|
|
1457
1549
|
}
|
|
1458
1550
|
}
|