@react-x11/components 0.4.0 → 0.6.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 +143 -8
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/markdown/ast.d.ts +80 -6
- package/dist/markdown/ast.d.ts.map +1 -1
- package/dist/markdown/ast.js +13 -4
- package/dist/markdown/ast.js.map +1 -1
- package/dist/markdown/expressions.d.ts +27 -0
- package/dist/markdown/expressions.d.ts.map +1 -0
- package/dist/markdown/expressions.js +203 -0
- package/dist/markdown/expressions.js.map +1 -0
- package/dist/markdown/index.d.ts +61 -2
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +101 -2
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/parse.d.ts +1 -1
- package/dist/markdown/parse.d.ts.map +1 -1
- package/dist/markdown/parse.js +163 -15
- package/dist/markdown/parse.js.map +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +4 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/markdown/tags.d.ts +35 -0
- package/dist/markdown/tags.d.ts.map +1 -0
- package/dist/markdown/tags.js +213 -0
- package/dist/markdown/tags.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/index.d.ts +17 -0
- package/dist/tabs/index.d.ts.map +1 -1
- package/dist/tabs/index.js +849 -186
- package/dist/tabs/index.js.map +1 -1
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +14 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +102 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/markdown/ast.ts +85 -9
- package/src/markdown/expressions.ts +223 -0
- package/src/markdown/index.ts +190 -5
- package/src/markdown/parse.ts +230 -15
- package/src/markdown/spans.ts +4 -0
- package/src/markdown/tags.ts +240 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/index.ts +1040 -204
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/renderer.ts +13 -3
|
@@ -0,0 +1,1459 @@
|
|
|
1
|
+
// `<mapview>` — the element that draws the map.
|
|
2
|
+
//
|
|
3
|
+
// One element, drawing a whole scene, for the reason `AGENTS.md` gives
|
|
4
|
+
// under "Drawing beats composing when the viewport is a transform": pan and
|
|
5
|
+
// zoom are a transform, this renderer's style vocabulary has no transform,
|
|
6
|
+
// and a composed map would re-render every road through React and re-lay it
|
|
7
|
+
// out through yoga on every pointer step. `<Flow>` reached the same place
|
|
8
|
+
// first; this adds the part a graph does not have, which is that the scene
|
|
9
|
+
// arrives a tile at a time and costs tens of milliseconds a tile to draw.
|
|
10
|
+
//
|
|
11
|
+
// Three caches, and the map's whole performance argument is the way they
|
|
12
|
+
// are layered:
|
|
13
|
+
//
|
|
14
|
+
// 1. **Tile data**, keyed on `source/z/x/y` and valid forever.
|
|
15
|
+
// 2. **A rendered `Surface` per tile**, valid for a zoom *level* and a
|
|
16
|
+
// style — not for a camera position. So a **pan** composites the same
|
|
17
|
+
// surfaces at new offsets (and blits, so most of them are not even
|
|
18
|
+
// composited), and a **fractional zoom** composites them scaled. Neither
|
|
19
|
+
// rasterizes anything. Only crossing an integer zoom does.
|
|
20
|
+
// 3. **A label placement in world pixels**, valid for a zoom and a set of
|
|
21
|
+
// loaded tiles — so a pan translates it rather than recomputing it, and
|
|
22
|
+
// the blit stays correct.
|
|
23
|
+
//
|
|
24
|
+
// And one budget: rasterization is resumable by style run, and a frame
|
|
25
|
+
// spends at most `rasterBudgetMs` on it. A dense city tile is 50-140 ms to
|
|
26
|
+
// draw (see `docs/prd-maps.md` for the measurements), so without this a
|
|
27
|
+
// tile arriving would drop eight frames; with it, the tile fills in over a
|
|
28
|
+
// dozen frames and no frame is late. Gestures set the budget to zero, so
|
|
29
|
+
// nothing is ever rasterized during a drag.
|
|
30
|
+
import { Node } from 'react-x11/node';
|
|
31
|
+
import { Surface } from 'react-x11/ntk';
|
|
32
|
+
import { GeometryBuffer } from './mvt.js';
|
|
33
|
+
import { BATCH_VERTICES, DrawScratch, drawTileRun, isMapCanvas, now, prepareStyle, } from './paint.js';
|
|
34
|
+
import { DEFAULT_TILE_SIZE, cameraForBounds, boundsOf, dataTileFor, subTileOf, projectLngLat, rasterFor, tileCover, transformFor, unprojectPoint, visibleBounds, } from './proj.js';
|
|
35
|
+
import { TileCache, pyramid } from './tiles.js';
|
|
36
|
+
import { shortbreadStyle } from './styles.js';
|
|
37
|
+
import { LabelShaper, collectLabels, drawLabels, placeLabels, } from './labels.js';
|
|
38
|
+
import { drawMarkers, drawOverlays, markerAt, markerRect } from './overlay.js';
|
|
39
|
+
/** Registration key, `kind` and JSX tag, one string — react-x11 rejects a
|
|
40
|
+
* node whose `kind` is not the name it was registered under, because
|
|
41
|
+
* `kind` is what paint order, the test queries and the DEV style assertion
|
|
42
|
+
* all match on. */
|
|
43
|
+
export const ELEMENT = 'mapview';
|
|
44
|
+
/**
|
|
45
|
+
* The props whose change means different pixels but whose damage this
|
|
46
|
+
* element claims for itself.
|
|
47
|
+
*
|
|
48
|
+
* Without this, a controlled map committing a new `markers` array per
|
|
49
|
+
* pointer step would claim the whole pane on every one of them, and the
|
|
50
|
+
* scoped claim the gesture made would be swallowed by it — the same trap
|
|
51
|
+
* `<Flow>` documents (react-x11#301).
|
|
52
|
+
*/
|
|
53
|
+
export const SELF_DAMAGED_PROPS = [
|
|
54
|
+
'camera',
|
|
55
|
+
'markers',
|
|
56
|
+
'overlays',
|
|
57
|
+
'sources',
|
|
58
|
+
'mapStyle',
|
|
59
|
+
];
|
|
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
|
+
/** The largest tile surface, per edge, in device pixels. An argb32 surface
|
|
75
|
+
* is `4 × size²` bytes, so 2048 is 16 MB and is already more than any
|
|
76
|
+
* pyramid justifies. */
|
|
77
|
+
const MAX_RASTER = 2048;
|
|
78
|
+
/**
|
|
79
|
+
* How many levels past a source's own depth the cover may go.
|
|
80
|
+
*
|
|
81
|
+
* Each level is a factor of two in linear detail and four in the number of
|
|
82
|
+
* tiles sharing one fetch, so six is 64× sharper than the stretched bitmap
|
|
83
|
+
* it replaces and 4,096 renderings per source tile at the very bottom —
|
|
84
|
+
* which is fine, because only the handful on screen are ever built. Beyond
|
|
85
|
+
* this the data itself is the limit: at zoom 20 one unit of a zoom-14
|
|
86
|
+
* tile's 4,096-unit grid is already 16 device pixels across, so there is no
|
|
87
|
+
* more shape to draw.
|
|
88
|
+
*/
|
|
89
|
+
const MAX_OVERZOOM = 6;
|
|
90
|
+
/** How far outside the pane tiles are kept warm, in logical pixels. Half a
|
|
91
|
+
* tile: enough that an ordinary flick has its tiles, not so much that a
|
|
92
|
+
* window covers four times the tiles it shows. */
|
|
93
|
+
const COVER_PADDING = 256;
|
|
94
|
+
/** The overlap of two rects, or null when they do not meet. */
|
|
95
|
+
function intersectRects(a, b) {
|
|
96
|
+
const x = Math.max(a.x, b.x);
|
|
97
|
+
const y = Math.max(a.y, b.y);
|
|
98
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
99
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
100
|
+
if (right <= x || bottom <= y)
|
|
101
|
+
return null;
|
|
102
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
103
|
+
}
|
|
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
|
+
/** A logical value put on the device grid — the same helper `src/flow/`
|
|
125
|
+
* keeps, and for the same reason: ntk's fast paths for a blit and a
|
|
126
|
+
* rounded box are gated on integral geometry, and `x * 1.5` is not always
|
|
127
|
+
* the integer it should be in floating point. */
|
|
128
|
+
function toDevice(value, scale) {
|
|
129
|
+
const out = value * scale;
|
|
130
|
+
const whole = Math.round(out);
|
|
131
|
+
return Math.abs(out - whole) < 1e-6 ? whole : out;
|
|
132
|
+
}
|
|
133
|
+
function rectsOverlap(a, b) {
|
|
134
|
+
return (a.x < b.x + b.width &&
|
|
135
|
+
a.x + a.width > b.x &&
|
|
136
|
+
a.y < b.y + b.height &&
|
|
137
|
+
a.y + a.height > b.y);
|
|
138
|
+
}
|
|
139
|
+
export class MapViewNode extends Node {
|
|
140
|
+
_cache;
|
|
141
|
+
_scratch = new DrawScratch();
|
|
142
|
+
_geometry = new GeometryBuffer();
|
|
143
|
+
_shaper = null;
|
|
144
|
+
/**
|
|
145
|
+
* The camera this element owns, used whenever `props.camera` is absent.
|
|
146
|
+
*
|
|
147
|
+
* The element keeping it — rather than the component above holding it in
|
|
148
|
+
* `useState` — is what makes a pan cost nothing but a blit: a drag step
|
|
149
|
+
* moves this number and claims a strip, and React is not involved at all.
|
|
150
|
+
* Routed through state instead, every pointer step would be a render, a
|
|
151
|
+
* commit and a full-pane claim, which is the shape `<Flow>` documents as
|
|
152
|
+
* "the content lags and catches up".
|
|
153
|
+
*/
|
|
154
|
+
_camera = { center: { lon: 0, lat: 20 }, zoom: 2 };
|
|
155
|
+
_prepared = null;
|
|
156
|
+
_preparedFrom = null;
|
|
157
|
+
_defaultStyle = null;
|
|
158
|
+
/** The placement, and what it was computed for. */
|
|
159
|
+
_labels = [];
|
|
160
|
+
_labelKey = '';
|
|
161
|
+
/** Candidates per tile, so a pan that brings a tile back does not redo
|
|
162
|
+
* the walk over its symbol layers. */
|
|
163
|
+
_candidates = new Map();
|
|
164
|
+
_gesture = null;
|
|
165
|
+
_hover = null;
|
|
166
|
+
/** Set while a gesture is in flight and for `SETTLE_MS` after it, which
|
|
167
|
+
* is when rasterization is suspended. */
|
|
168
|
+
_settleAt = 0;
|
|
169
|
+
_settleTimer = null;
|
|
170
|
+
_painting = false;
|
|
171
|
+
/** Whether any tile has been rasterized in the frame being painted — the
|
|
172
|
+
* forward-progress guarantee below. */
|
|
173
|
+
_rastered = false;
|
|
174
|
+
_frameClip = null;
|
|
175
|
+
_stats = null;
|
|
176
|
+
_sceneAnnounced = false;
|
|
177
|
+
constructor(props, app) {
|
|
178
|
+
super(ELEMENT, props, app);
|
|
179
|
+
// Seeded once. `defaultCamera` is read here and never again, which is
|
|
180
|
+
// what makes it a *default* rather than a second controlled prop.
|
|
181
|
+
const seed = (props.camera ?? props.defaultCamera);
|
|
182
|
+
if (seed)
|
|
183
|
+
this._camera = { center: { ...seed.center }, zoom: seed.zoom };
|
|
184
|
+
// A map is a thing you drive with the keyboard as well as the mouse:
|
|
185
|
+
// arrows pan, +/- zoom. Without this it is never focused and no key
|
|
186
|
+
// arrives.
|
|
187
|
+
this.focusableByDefault = true;
|
|
188
|
+
this.defaultCursor = 'grab';
|
|
189
|
+
this._cache = new TileCache({
|
|
190
|
+
surfaceBudget: this._prop('surfaceBudget'),
|
|
191
|
+
onError: (entry) => {
|
|
192
|
+
this._prop('onTileError')?.(entry.error, {
|
|
193
|
+
...entry.tile,
|
|
194
|
+
// The data entry is keyed per source, so the id is recoverable
|
|
195
|
+
// from the key it was built with.
|
|
196
|
+
sourceId: entry.key.slice(0, entry.key.lastIndexOf(':')),
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
onChange: () => {
|
|
200
|
+
// A tile landed. Its own box is the honest claim, but the tile is
|
|
201
|
+
// not yet rasterized and the label placement may change, so the
|
|
202
|
+
// frame is a full one — which is what a tile arriving looks like
|
|
203
|
+
// anyway, and it happens once per tile rather than per frame.
|
|
204
|
+
this._labelKey = '';
|
|
205
|
+
this._repaint('content');
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// --- props ---------------------------------------------------------------
|
|
210
|
+
_prop(name) {
|
|
211
|
+
return this.props[name];
|
|
212
|
+
}
|
|
213
|
+
get _scale() {
|
|
214
|
+
return this.scale > 0 ? this.scale : 1;
|
|
215
|
+
}
|
|
216
|
+
/** The style, compiled. Recompiled only when the style object changes
|
|
217
|
+
* identity, so an application holding one in a module constant pays
|
|
218
|
+
* once for the life of the process. */
|
|
219
|
+
_style() {
|
|
220
|
+
const given = this._prop('mapStyle');
|
|
221
|
+
if (given) {
|
|
222
|
+
if (this._prepared && this._preparedFrom === given)
|
|
223
|
+
return this._prepared;
|
|
224
|
+
this._preparedFrom = given;
|
|
225
|
+
this._prepared = prepareStyle(given);
|
|
226
|
+
return this._prepared;
|
|
227
|
+
}
|
|
228
|
+
if (!this._defaultStyle) {
|
|
229
|
+
// Built once, and from the theme's own light/dark decision rather
|
|
230
|
+
// than from a prop: a map inside a dark application that stays light
|
|
231
|
+
// is the thing everyone notices first.
|
|
232
|
+
this._defaultStyle = shortbreadStyle({ dark: this._isDark() });
|
|
233
|
+
}
|
|
234
|
+
if (this._prepared && this._preparedFrom === this._defaultStyle) {
|
|
235
|
+
return this._prepared;
|
|
236
|
+
}
|
|
237
|
+
this._preparedFrom = this._defaultStyle;
|
|
238
|
+
this._prepared = prepareStyle(this._defaultStyle);
|
|
239
|
+
return this._prepared;
|
|
240
|
+
}
|
|
241
|
+
_isDark() {
|
|
242
|
+
const theme = this.theme;
|
|
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;
|
|
255
|
+
}
|
|
256
|
+
_palette() {
|
|
257
|
+
const theme = this.theme;
|
|
258
|
+
return {
|
|
259
|
+
accent: theme?.accent ?? '#2d6cdf',
|
|
260
|
+
background: theme?.background ?? '#ffffff',
|
|
261
|
+
text: theme?.text ?? '#111111',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
_sources() {
|
|
265
|
+
const given = this._prop('sources');
|
|
266
|
+
return given ? [...given] : [];
|
|
267
|
+
}
|
|
268
|
+
_sourceId(source, index) {
|
|
269
|
+
return source.id ?? `source-${index}`;
|
|
270
|
+
}
|
|
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
|
+
_markers() {
|
|
281
|
+
return this._prop('markers') ?? [];
|
|
282
|
+
}
|
|
283
|
+
_overlays() {
|
|
284
|
+
return this._prop('overlays') ?? [];
|
|
285
|
+
}
|
|
286
|
+
// --- geometry ------------------------------------------------------------
|
|
287
|
+
/** The pane in logical pixels — the unit everything public here speaks.
|
|
288
|
+
* `contentBox()` is device, like everything core hands an element. */
|
|
289
|
+
_pane() {
|
|
290
|
+
const box = this.contentBox();
|
|
291
|
+
const s = this._scale;
|
|
292
|
+
return {
|
|
293
|
+
x: box.x / s,
|
|
294
|
+
y: box.y / s,
|
|
295
|
+
width: box.width / s,
|
|
296
|
+
height: box.height / s,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
_visible() {
|
|
300
|
+
const pane = this._pane();
|
|
301
|
+
return pane.width > 0 && pane.height > 0;
|
|
302
|
+
}
|
|
303
|
+
camera() {
|
|
304
|
+
const given = this._prop('camera');
|
|
305
|
+
return given ?? this._camera;
|
|
306
|
+
}
|
|
307
|
+
/** The camera resolved against the pane. */
|
|
308
|
+
_transform(camera = this.camera()) {
|
|
309
|
+
const pane = this._pane();
|
|
310
|
+
return transformFor(camera, { width: pane.width, height: pane.height }, DEFAULT_TILE_SIZE);
|
|
311
|
+
}
|
|
312
|
+
/** Claim a logical rect as damage. */
|
|
313
|
+
_claim(rect, reason) {
|
|
314
|
+
const s = this._scale;
|
|
315
|
+
const x = Math.floor(toDevice(rect.x, s));
|
|
316
|
+
const y = Math.floor(toDevice(rect.y, s));
|
|
317
|
+
this.invalidate(false, {
|
|
318
|
+
x,
|
|
319
|
+
y,
|
|
320
|
+
width: Math.ceil(toDevice(rect.x + rect.width, s)) - x,
|
|
321
|
+
height: Math.ceil(toDevice(rect.y + rect.height, s)) - y,
|
|
322
|
+
}, reason);
|
|
323
|
+
}
|
|
324
|
+
_repaint(reason = 'content') {
|
|
325
|
+
this.invalidate(false, this.abs, reason);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Ask for another frame without asking for a repaint.
|
|
329
|
+
*
|
|
330
|
+
* There is no "call me next frame" on the element seam — damage is what
|
|
331
|
+
* schedules a paint — so this claims a single pixel. That is the honest
|
|
332
|
+
* claim for a frame whose only job is to continue a rasterization: the
|
|
333
|
+
* tile being drawn is a second surface nobody is looking at, so *nothing
|
|
334
|
+
* on screen changes* until it lands, and the one thing that does change
|
|
335
|
+
* pixels claims its own box when it does.
|
|
336
|
+
*
|
|
337
|
+
* Claiming the pane instead repaints the whole map at the refresh rate
|
|
338
|
+
* for the several frames a redraw takes. On X11 that is wasted work; on
|
|
339
|
+
* the Cocoa backend, which paints many more frames a second, it is a
|
|
340
|
+
* visible burst of repaints at the end of every zoom.
|
|
341
|
+
*/
|
|
342
|
+
_wake(reason = 'content') {
|
|
343
|
+
const box = this.contentBox();
|
|
344
|
+
this.invalidate(false, { x: box.x, y: box.y, width: 1, height: 1 }, reason);
|
|
345
|
+
}
|
|
346
|
+
// --- 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
|
+
/**
|
|
388
|
+
* A pan is a scroll in every way but the bookkeeping, and react-x11#303
|
|
389
|
+
* made the bookkeeping public: `scrollContents` claims the pane, arms the
|
|
390
|
+
* frame to blit the band that survives, and narrows the claim to the
|
|
391
|
+
* strip the shift exposed — which `paintDamage()` then hands to `paint`,
|
|
392
|
+
* so the existing culling draws the sliver and nothing else.
|
|
393
|
+
*
|
|
394
|
+
* The attribution strip is pinned to the pane, so its pixels must not
|
|
395
|
+
* ride the blit: its band is carved out of the region that shifts and
|
|
396
|
+
* claimed the ordinary way. The blit gate tests foreign claims against
|
|
397
|
+
* the *rect* (react-x11#309/#310), so a claim sitting edge to edge with
|
|
398
|
+
* it leaves the frame a blit.
|
|
399
|
+
*
|
|
400
|
+
* Still a full repaint when the zoom moved (scaling is not a blit) or the
|
|
401
|
+
* shift is fractional on the device grid — every real pan gesture is
|
|
402
|
+
* whole device pixels, because that is how it came off the wire.
|
|
403
|
+
*/
|
|
404
|
+
_blitPan(previous, next) {
|
|
405
|
+
if (next.zoom !== previous.zoom)
|
|
406
|
+
return false;
|
|
407
|
+
const pane = this._pane();
|
|
408
|
+
if (pane.width <= 0 || pane.height <= 0)
|
|
409
|
+
return false;
|
|
410
|
+
const before = this._transform(previous);
|
|
411
|
+
const after = this._transform(next);
|
|
412
|
+
if (before.world !== after.world)
|
|
413
|
+
return false;
|
|
414
|
+
const s = this._scale;
|
|
415
|
+
// How far the *pixels* moved — the sense `Surface.copyWithin` uses,
|
|
416
|
+
// which is the opposite of the camera's motion.
|
|
417
|
+
const shiftX = toDevice((before.centerX - after.centerX) * before.world, s);
|
|
418
|
+
const shiftY = toDevice((before.centerY - after.centerY) * before.world, s);
|
|
419
|
+
const dx = Math.round(shiftX);
|
|
420
|
+
const dy = Math.round(shiftY);
|
|
421
|
+
if (dx === 0 && dy === 0)
|
|
422
|
+
return true; // sub-pixel: nothing to show yet
|
|
423
|
+
if (shiftX !== dx || shiftY !== dy)
|
|
424
|
+
return false;
|
|
425
|
+
const box = this.contentBox();
|
|
426
|
+
const strip = Math.ceil(toDevice(this._attributionHeight(), s));
|
|
427
|
+
const blit = {
|
|
428
|
+
x: box.x,
|
|
429
|
+
y: box.y,
|
|
430
|
+
width: box.width,
|
|
431
|
+
height: box.height - strip,
|
|
432
|
+
};
|
|
433
|
+
if (blit.width < 64 * s || blit.height < 64 * s)
|
|
434
|
+
return false;
|
|
435
|
+
if (Math.abs(dx) >= blit.width || Math.abs(dy) >= blit.height)
|
|
436
|
+
return false;
|
|
437
|
+
this.scrollContents(blit, dx, dy);
|
|
438
|
+
if (strip > 0) {
|
|
439
|
+
this.invalidate(false, { x: box.x, y: blit.y + blit.height, width: box.width, height: strip }, 'scroll');
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
/** Zoom about a point that must not move — the pointer under a wheel,
|
|
444
|
+
* the pane's centre for a key. */
|
|
445
|
+
_zoomAbout(delta, screenX, screenY) {
|
|
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
|
+
}
|
|
490
|
+
get _gesturing() {
|
|
491
|
+
return this._gesture !== null || Date.now() < this._settleAt;
|
|
492
|
+
}
|
|
493
|
+
// --- the imperative surface ----------------------------------------------
|
|
494
|
+
getCamera() {
|
|
495
|
+
const camera = this.camera();
|
|
496
|
+
return { center: { ...camera.center }, zoom: camera.zoom };
|
|
497
|
+
}
|
|
498
|
+
setCamera(camera) {
|
|
499
|
+
const current = this.camera();
|
|
500
|
+
this._applyCamera({
|
|
501
|
+
center: camera.center ?? current.center,
|
|
502
|
+
zoom: camera.zoom ?? current.zoom,
|
|
503
|
+
}, false);
|
|
504
|
+
}
|
|
505
|
+
/** Move by a distance in pane-local logical pixels. */
|
|
506
|
+
panBy(dx, dy) {
|
|
507
|
+
const transform = this._transform();
|
|
508
|
+
this._applyCamera({
|
|
509
|
+
zoom: transform.zoom,
|
|
510
|
+
center: unprojectPoint(transform, transform.paneX + dx, transform.paneY + dy),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
zoomIn(step = 1) {
|
|
514
|
+
const pane = this._pane();
|
|
515
|
+
this._zoomAbout(step, pane.width / 2, pane.height / 2);
|
|
516
|
+
}
|
|
517
|
+
zoomOut(step = 1) {
|
|
518
|
+
this.zoomIn(-step);
|
|
519
|
+
}
|
|
520
|
+
zoomTo(zoom) {
|
|
521
|
+
this.setCamera({ zoom });
|
|
522
|
+
}
|
|
523
|
+
fitBounds(bounds, options) {
|
|
524
|
+
const pane = this._pane();
|
|
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);
|
|
537
|
+
}
|
|
538
|
+
fitMarkers(ids, options) {
|
|
539
|
+
const wanted = ids ? new Set(ids) : null;
|
|
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);
|
|
549
|
+
}
|
|
550
|
+
getBounds() {
|
|
551
|
+
return visibleBounds(this._transform());
|
|
552
|
+
}
|
|
553
|
+
project(position) {
|
|
554
|
+
return projectLngLat(this._transform(), position);
|
|
555
|
+
}
|
|
556
|
+
unproject(x, y) {
|
|
557
|
+
return unprojectPoint(this._transform(), x, y);
|
|
558
|
+
}
|
|
559
|
+
markerAt(x, y) {
|
|
560
|
+
return markerAt(this._markers(), this._transform(), x, y);
|
|
561
|
+
}
|
|
562
|
+
refresh() {
|
|
563
|
+
this._cache.invalidateStyle();
|
|
564
|
+
this._labelKey = '';
|
|
565
|
+
this._repaint('content');
|
|
566
|
+
}
|
|
567
|
+
stats() {
|
|
568
|
+
return this._stats;
|
|
569
|
+
}
|
|
570
|
+
_pendingFit = null;
|
|
571
|
+
// --- painting ------------------------------------------------------------
|
|
572
|
+
/** How tall the attribution strip is, in logical pixels — 0 when there is
|
|
573
|
+
* nothing to say. Read by the blit as well as the paint, so the band it
|
|
574
|
+
* carves out and the band that is drawn are one number. */
|
|
575
|
+
_attributionHeight() {
|
|
576
|
+
return this._attributionText() ? 16 : 0;
|
|
577
|
+
}
|
|
578
|
+
_attributionText() {
|
|
579
|
+
const given = this._prop('attribution');
|
|
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(' · ');
|
|
589
|
+
}
|
|
590
|
+
/** How large a tile is rasterized, and how many surface pixels one
|
|
591
|
+
* logical pixel is — the pair the rasterizer needs, and the pair that
|
|
592
|
+
* keeps a road two logical pixels wide at every fractional zoom. */
|
|
593
|
+
_rasterPlan(entry, pyramid, zoom) {
|
|
594
|
+
const scale = this._prop('rasterScale') ?? this._scale;
|
|
595
|
+
// The cover level, not the source's: past the source's own depth the
|
|
596
|
+
// cover synthesizes tiles, and each is rasterized at its own natural
|
|
597
|
+
// size rather than as a slice of a stretched one.
|
|
598
|
+
const raster = rasterFor(zoom, entry.tile.z, { ...pyramid, maxZoom: pyramid.maxZoom + MAX_OVERZOOM }, scale, MAX_RASTER);
|
|
599
|
+
return { size: raster.size, pixelsPerLogical: raster.size / entry.size };
|
|
600
|
+
}
|
|
601
|
+
_makeSurface(size) {
|
|
602
|
+
try {
|
|
603
|
+
return new Surface(this.app, {
|
|
604
|
+
width: size,
|
|
605
|
+
height: size,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
// A backend with no offscreen surface — the headless mock. The map
|
|
610
|
+
// then draws its background, its overlays and its markers and no
|
|
611
|
+
// basemap, which is the same posture `src/terminal/vt/` takes when
|
|
612
|
+
// there is no pixel API: degrade, never throw.
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
paint(ctx) {
|
|
617
|
+
const damage = this.paintDamage();
|
|
618
|
+
super.paint(ctx);
|
|
619
|
+
if (!this._visible() || !isMapCanvas(ctx))
|
|
620
|
+
return;
|
|
621
|
+
const started = now();
|
|
622
|
+
this._painting = true;
|
|
623
|
+
const scale = this._scale;
|
|
624
|
+
this._frameClip = damage
|
|
625
|
+
? {
|
|
626
|
+
x: damage.x / scale,
|
|
627
|
+
y: damage.y / scale,
|
|
628
|
+
width: damage.width / scale,
|
|
629
|
+
height: damage.height / scale,
|
|
630
|
+
}
|
|
631
|
+
: null;
|
|
632
|
+
if (this._pendingFit) {
|
|
633
|
+
const { bounds, options } = this._pendingFit;
|
|
634
|
+
this._pendingFit = null;
|
|
635
|
+
this._painting = false;
|
|
636
|
+
this.fitBounds(bounds, options);
|
|
637
|
+
this._painting = true;
|
|
638
|
+
}
|
|
639
|
+
const pane = this._pane();
|
|
640
|
+
const camera = this.camera();
|
|
641
|
+
const transform = this._transform(camera);
|
|
642
|
+
const style = this._style();
|
|
643
|
+
const frame = this._cache.beginFrame();
|
|
644
|
+
const stats = {
|
|
645
|
+
rasterMs: 0,
|
|
646
|
+
drawMs: 0,
|
|
647
|
+
tiles: 0,
|
|
648
|
+
ready: 0,
|
|
649
|
+
fromAncestor: 0,
|
|
650
|
+
fromDescendant: 0,
|
|
651
|
+
pending: 0,
|
|
652
|
+
labels: 0,
|
|
653
|
+
errors: 0,
|
|
654
|
+
surfaceBytes: 0,
|
|
655
|
+
damage: damage ? { ...damage } : null,
|
|
656
|
+
draw: { features: 0, vertices: 0, decimated: 0, culled: 0, batches: 0 },
|
|
657
|
+
};
|
|
658
|
+
ctx.save();
|
|
659
|
+
ctx.beginPath();
|
|
660
|
+
const box = this.contentBox();
|
|
661
|
+
// **The damage rect, not just the pane.**
|
|
662
|
+
//
|
|
663
|
+
// Everything below draws in pane coordinates — a tile at its own box,
|
|
664
|
+
// the whole label layer, every overlay and marker, the attribution —
|
|
665
|
+
// and a partial frame must not put any of it outside the rect it
|
|
666
|
+
// claimed. Core presents the claimed region; pixels drawn beyond it
|
|
667
|
+
// reach the backing store without reaching the screen, and the two
|
|
668
|
+
// then disagree until something repaints the lot. That is what a
|
|
669
|
+
// stale strip of the *previous style* surviving a theme switch is,
|
|
670
|
+
// and why an app switch or a window drag clears it: those force a
|
|
671
|
+
// full expose, which presents everything.
|
|
672
|
+
//
|
|
673
|
+
// It is also most of the cost of a wake frame. A frame that only
|
|
674
|
+
// continues a rasterization claims one pixel and used to redraw every
|
|
675
|
+
// label and marker on the map into it.
|
|
676
|
+
const clip = this._frameClip
|
|
677
|
+
? intersectRects(box, this._deviceRect(this._frameClip))
|
|
678
|
+
: box;
|
|
679
|
+
if (!clip) {
|
|
680
|
+
this._painting = false;
|
|
681
|
+
this._frameClip = null;
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
ctx.rect(clip.x, clip.y, clip.width, clip.height);
|
|
685
|
+
ctx.clip();
|
|
686
|
+
// The style's background under everything: it is what the parts of the
|
|
687
|
+
// world with no tile yet look like, so it is most of what a map looks
|
|
688
|
+
// like while it loads.
|
|
689
|
+
const background = this.style.backgroundColor ??
|
|
690
|
+
this._preparedBackground();
|
|
691
|
+
if (background) {
|
|
692
|
+
ctx.fillStyle = background;
|
|
693
|
+
const region = this._frameClip
|
|
694
|
+
? this._deviceRect(this._frameClip)
|
|
695
|
+
: { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
696
|
+
ctx.fillRect(region.x, region.y, region.width, region.height);
|
|
697
|
+
}
|
|
698
|
+
// Rasterization is suspended for the length of a gesture, so a drag or
|
|
699
|
+
// a wheel is composites only. `rasterBudgetMs` bounds the rest.
|
|
700
|
+
const budget = this._gesturing
|
|
701
|
+
? 0
|
|
702
|
+
: (this._prop('rasterBudgetMs') ?? 8);
|
|
703
|
+
const deadline = started + budget;
|
|
704
|
+
this._rastered = false;
|
|
705
|
+
const sources = this._sources();
|
|
706
|
+
for (let i = 0; i < sources.length; i++) {
|
|
707
|
+
this._paintSource(ctx, sources[i], this._sourceId(sources[i], i), transform, pane, style, stats, budget > 0, deadline);
|
|
708
|
+
}
|
|
709
|
+
this._paintLabels(ctx, transform, pane, style, stats);
|
|
710
|
+
const palette = this._palette();
|
|
711
|
+
drawOverlays(ctx, this._overlays(), transform, pane, scale, palette);
|
|
712
|
+
drawMarkers(ctx, this._markers(), transform, pane, scale, palette);
|
|
713
|
+
this._paintAttribution(ctx, pane, palette);
|
|
714
|
+
ctx.restore();
|
|
715
|
+
this._cache.sweep();
|
|
716
|
+
stats.surfaceBytes = this._cache.surfaceBytes;
|
|
717
|
+
stats.drawMs = now() - started - stats.rasterMs;
|
|
718
|
+
this._stats = stats;
|
|
719
|
+
this._painting = false;
|
|
720
|
+
this._frameClip = null;
|
|
721
|
+
void frame;
|
|
722
|
+
this._prop('onFrame')?.(stats);
|
|
723
|
+
// Tiles left to rasterize: come back next frame and spend another
|
|
724
|
+
// budget on them. This is the whole of the progressive fill-in.
|
|
725
|
+
//
|
|
726
|
+
// Only when a next frame could make progress. With no budget — during a
|
|
727
|
+
// gesture, or because an application pinned `rasterBudgetMs` to 0 — the
|
|
728
|
+
// next frame would draw exactly this one again, and asking for it is a
|
|
729
|
+
// spin. The gesture's own settle timer is what brings the map back.
|
|
730
|
+
//
|
|
731
|
+
// And the claim is **one pixel**, not the pane. A frame that only
|
|
732
|
+
// continues a rasterization changes nothing on screen — the tile being
|
|
733
|
+
// drawn is a second surface nobody is looking at — so claiming the pane
|
|
734
|
+
// asks the renderer to repaint the whole map, at the refresh rate, for
|
|
735
|
+
// the several frames a redraw takes. On X11 that is wasted work; on the
|
|
736
|
+
// Cocoa backend, which paints many more frames a second, it is a
|
|
737
|
+
// visible burst of repaints at the end of every zoom. The one thing
|
|
738
|
+
// that *does* change pixels is a tile finishing, and that claims its
|
|
739
|
+
// own box above.
|
|
740
|
+
if (stats.pending > 0 && budget > 0)
|
|
741
|
+
this._wake('content');
|
|
742
|
+
if (!this._sceneAnnounced) {
|
|
743
|
+
this._sceneAnnounced = true;
|
|
744
|
+
this.notifyA11ySceneChanged();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
_preparedBackground() {
|
|
748
|
+
const given = this._prop('mapStyle');
|
|
749
|
+
if (given)
|
|
750
|
+
return given.background;
|
|
751
|
+
this._style(); // builds `_defaultStyle` on the first paint
|
|
752
|
+
return this._defaultStyle?.background;
|
|
753
|
+
}
|
|
754
|
+
_deviceRect(rect) {
|
|
755
|
+
const s = this._scale;
|
|
756
|
+
const x = Math.floor(rect.x * s);
|
|
757
|
+
const y = Math.floor(rect.y * s);
|
|
758
|
+
return {
|
|
759
|
+
x,
|
|
760
|
+
y,
|
|
761
|
+
width: Math.ceil((rect.x + rect.width) * s) - x,
|
|
762
|
+
height: Math.ceil((rect.y + rect.height) * s) - y,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
_paintSource(ctx, source, sourceId, transform, pane, style, stats,
|
|
766
|
+
/** False for the length of a gesture, when nothing is rasterized. */
|
|
767
|
+
mayRaster, deadline) {
|
|
768
|
+
const scale = this._scale;
|
|
769
|
+
const p = pyramid(source);
|
|
770
|
+
// The cover goes **deeper than the source cuts**, up to
|
|
771
|
+
// `MAX_OVERZOOM` levels past it, and the data for those tiles comes
|
|
772
|
+
// from their ancestor at the deepest cut level. That is what makes an
|
|
773
|
+
// overzoomed map sharp: instead of one tile rasterized onto a surface
|
|
774
|
+
// and stretched sixty-four times, there are two hundred and fifty-six
|
|
775
|
+
// tiles sharing one fetch, each drawn at its own natural size, with
|
|
776
|
+
// detail limited by the data rather than by a bitmap.
|
|
777
|
+
const cover = tileCover({ ...transform, zoom: transform.zoom }, { ...p, maxZoom: p.maxZoom + MAX_OVERZOOM }, COVER_PADDING);
|
|
778
|
+
const zoom = transform.zoom;
|
|
779
|
+
const styleZoom = Math.floor(zoom);
|
|
780
|
+
const progressive = this._prop('progressive') === true;
|
|
781
|
+
for (const entry of cover) {
|
|
782
|
+
const box = {
|
|
783
|
+
x: pane.x + entry.x,
|
|
784
|
+
y: pane.y + entry.y,
|
|
785
|
+
width: entry.size,
|
|
786
|
+
height: entry.size,
|
|
787
|
+
};
|
|
788
|
+
// Two different questions, and conflating them was a bug worth
|
|
789
|
+
// spelling out. **Whether to work on a tile** is about the pane: the
|
|
790
|
+
// cover is padded, so some of it is off screen and those tiles are
|
|
791
|
+
// wanted (so they load) but never drawn. **Whether to composite it**
|
|
792
|
+
// is about this pass's damage rect, which may be far smaller —
|
|
793
|
+
// including the deliberately tiny claim a rasterization continuation
|
|
794
|
+
// makes, which must still let the rasterizer run.
|
|
795
|
+
const onScreen = rectsOverlap(box, pane);
|
|
796
|
+
const inPass = this._frameClip === null || rectsOverlap(box, this._frameClip);
|
|
797
|
+
const cached = this._cache.want(source, sourceId, entry.tile, dataTileFor(entry.tile, p.maxZoom), subTileOf(entry.tile, p.maxZoom));
|
|
798
|
+
if (!onScreen)
|
|
799
|
+
continue;
|
|
800
|
+
stats.tiles++;
|
|
801
|
+
if (cached.status === 'error')
|
|
802
|
+
stats.errors++;
|
|
803
|
+
if (cached.status === 'ready') {
|
|
804
|
+
const plan = this._rasterPlan(entry, p, zoom);
|
|
805
|
+
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));
|
|
807
|
+
if (drawing && drawing.progress !== -1) {
|
|
808
|
+
if (cached.raster) {
|
|
809
|
+
this._uploadRaster(cached, drawing);
|
|
810
|
+
}
|
|
811
|
+
else if (mayRaster && (!this._rastered || now() < deadline)) {
|
|
812
|
+
// **At least one tile per frame, whatever the budget.** A
|
|
813
|
+
// budget smaller than one unit of work is not "do less", it is
|
|
814
|
+
// "do nothing" — and since the frame then still has tiles
|
|
815
|
+
// pending it asks for another one, forever, at the refresh
|
|
816
|
+
// rate. So the first tile of a frame ignores the deadline and
|
|
817
|
+
// every tile after it respects it, which bounds a frame at one
|
|
818
|
+
// tile's overrun and guarantees the map finishes.
|
|
819
|
+
this._rasterize(cached, drawing, style, entry, plan, styleZoom, stats, deadline);
|
|
820
|
+
}
|
|
821
|
+
// Finished this frame: the new picture replaces the old one, and
|
|
822
|
+
// the swap is what the whole pair exists for — the tile never
|
|
823
|
+
// goes blank between them. Claim the box it occupies, because
|
|
824
|
+
// *that* is the pixel change this whole sequence of frames was
|
|
825
|
+
// for; the frames before it claimed almost nothing.
|
|
826
|
+
if (this._cache.promote(cached))
|
|
827
|
+
this._claim(box, 'content');
|
|
828
|
+
}
|
|
829
|
+
// "Pending" means *there is work left that this map could still
|
|
830
|
+
// do*, and nothing weaker — because `paint` asks for another frame
|
|
831
|
+
// while it is non-zero. A tile whose surface could not be made (a
|
|
832
|
+
// backend that has none) never becomes drawable, and counting it
|
|
833
|
+
// would spin the frame clock at the refresh rate forever,
|
|
834
|
+
// repainting a map that cannot change.
|
|
835
|
+
if (cached.drawing)
|
|
836
|
+
stats.pending++;
|
|
837
|
+
}
|
|
838
|
+
// What is composited is `shown`, which is **finished by
|
|
839
|
+
// construction** — a rendering only becomes `shown` when its last
|
|
840
|
+
// style run is done. So a tile appears whole rather than as water,
|
|
841
|
+
// then landuse, then road casings, then roads over a dozen frames,
|
|
842
|
+
// and a *re*-rasterization does not blank it either: the previous
|
|
843
|
+
// picture stays up until the new one is ready to replace it.
|
|
844
|
+
//
|
|
845
|
+
// `progressive` composites the draft instead, which is the old
|
|
846
|
+
// behaviour and is honest about what the renderer is doing.
|
|
847
|
+
const showing = progressive && cached.drawing ? cached.drawing : cached.shown;
|
|
848
|
+
if (showing) {
|
|
849
|
+
if (!inPass)
|
|
850
|
+
continue;
|
|
851
|
+
this._composite(ctx, showing.surface, showing.size, box, pane, scale, 0, 0, 1);
|
|
852
|
+
stats.ready++;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
// Nothing of this tile yet — a first load, which no buffering can
|
|
856
|
+
// help. Borrow the ancestor that is already drawn, scaled up: that is
|
|
857
|
+
// the difference between a map that fills in and one that flashes
|
|
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.
|
|
864
|
+
//
|
|
865
|
+
// **Zooming in**, the tile already in hand is this one's *ancestor*:
|
|
866
|
+
// one composite, scaled up, blurry but complete. **Zooming out**, the
|
|
867
|
+
// tiles in hand are its *descendants*: several composites, scaled
|
|
868
|
+
// down, sharp but only as complete as the pieces that are cached.
|
|
869
|
+
// Only the first of those existed at first, so a zoom out showed the
|
|
870
|
+
// background — with the labels and the markers still drawn over it —
|
|
871
|
+
// until the coarser tile had been fetched, rasterized and composited.
|
|
872
|
+
//
|
|
873
|
+
// Descendants win when they cover the whole square, because they are
|
|
874
|
+
// sharper and they are the level the user is coming *from*; the
|
|
875
|
+
// ancestor wins when they do not, because a complete blurry picture
|
|
876
|
+
// beats a sharp one with holes in it.
|
|
877
|
+
const kids = this._cache.descendantsWithSurface(sourceId, entry.tile);
|
|
878
|
+
const covered = kids.length > 0 && kids.length === kids[0].span * kids[0].span;
|
|
879
|
+
const ancestor = covered
|
|
880
|
+
? null
|
|
881
|
+
: this._cache.ancestorWithSurface(sourceId, entry.tile);
|
|
882
|
+
if (ancestor?.shown) {
|
|
883
|
+
const up = entry.tile.z - ancestor.tile.z;
|
|
884
|
+
const span = 1 << up;
|
|
885
|
+
const fx = entry.tile.x - (ancestor.tile.x << up);
|
|
886
|
+
const fy = entry.tile.y - (ancestor.tile.y << up);
|
|
887
|
+
this._composite(ctx, ancestor.shown.surface, ancestor.shown.size, box, pane, scale, fx, fy, span);
|
|
888
|
+
stats.fromAncestor++;
|
|
889
|
+
}
|
|
890
|
+
else if (kids.length > 0) {
|
|
891
|
+
for (const kid of kids) {
|
|
892
|
+
const piece = entry.size / kid.span;
|
|
893
|
+
this._composite(ctx, kid.entry.shown.surface, kid.entry.shown.size, {
|
|
894
|
+
x: box.x + kid.x * piece,
|
|
895
|
+
y: box.y + kid.y * piece,
|
|
896
|
+
width: piece,
|
|
897
|
+
height: piece,
|
|
898
|
+
}, pane, scale, 0, 0, 1);
|
|
899
|
+
}
|
|
900
|
+
stats.fromDescendant++;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Rasterize as much of a tile as the budget allows, run by run.
|
|
906
|
+
*
|
|
907
|
+
* `progress` is where it stopped, so the next frame carries on. Layers
|
|
908
|
+
* are painted bottom-up, so a tile stopped part-way looks like a map
|
|
909
|
+
* whose upper layers have not arrived rather than like a hole.
|
|
910
|
+
*/
|
|
911
|
+
_rasterize(cached, render, style, entry, plan, styleZoom, stats, deadline) {
|
|
912
|
+
const vector = cached.vector;
|
|
913
|
+
if (!vector)
|
|
914
|
+
return;
|
|
915
|
+
const context = render.context;
|
|
916
|
+
if (!isMapCanvas(context)) {
|
|
917
|
+
// No path API on this surface: call it finished and empty rather than
|
|
918
|
+
// asking again every frame.
|
|
919
|
+
render.progress = -1;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const started = now();
|
|
923
|
+
this._rastered = true;
|
|
924
|
+
const pixels = plan.pixelsPerLogical;
|
|
925
|
+
this._scratch.resetStats();
|
|
926
|
+
// Where the **data** tile's square lands on this surface. When the
|
|
927
|
+
// cover has gone deeper than the source cuts, this tile is one cell of
|
|
928
|
+
// a `span × span` grid over that square, so the square is `span` times
|
|
929
|
+
// the surface and starts `sub.x` surfaces to the left of it. Everything
|
|
930
|
+
// outside the surface is clipped by the surface itself, and the cull
|
|
931
|
+
// below stops it being drawn at all.
|
|
932
|
+
const sub = cached.sub;
|
|
933
|
+
const span = render.size * sub.span;
|
|
934
|
+
const draw = {
|
|
935
|
+
ox: -sub.x * render.size,
|
|
936
|
+
oy: -sub.y * render.size,
|
|
937
|
+
span,
|
|
938
|
+
pixelsPerLogical: pixels,
|
|
939
|
+
zoom: styleZoom,
|
|
940
|
+
// A vertex closer than two-thirds of a pixel to the last one kept
|
|
941
|
+
// says nothing; a feature under a pixel and a half is not worth a
|
|
942
|
+
// path. Both are in surface pixels, which is why they are scaled.
|
|
943
|
+
tolerance: 0.65 * pixels,
|
|
944
|
+
minFeature: 1.5 * pixels,
|
|
945
|
+
batchVertices: this._batchVertices(),
|
|
946
|
+
// The surface, in its own coordinates. Only meaningful when this is
|
|
947
|
+
// one cell of a larger square — and then it is what stops each of the
|
|
948
|
+
// cells re-drawing the whole tile's features, which would make an
|
|
949
|
+
// overzoomed frame cost `span²` times what it should.
|
|
950
|
+
clip: sub.span > 1
|
|
951
|
+
? { x: 0, y: 0, width: render.size, height: render.size }
|
|
952
|
+
: null,
|
|
953
|
+
};
|
|
954
|
+
let run = render.progress;
|
|
955
|
+
let layer = render.progressLayer;
|
|
956
|
+
while (run < style.runs.length) {
|
|
957
|
+
const stoppedAt = drawTileRun(context, vector, style, run, draw, this._scratch, {
|
|
958
|
+
fromLayer: layer,
|
|
959
|
+
deadline,
|
|
960
|
+
});
|
|
961
|
+
if (stoppedAt >= 0) {
|
|
962
|
+
// The budget ran out inside the run; come back to the same run at
|
|
963
|
+
// the layer after the one that crossed it.
|
|
964
|
+
layer = stoppedAt;
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
run++;
|
|
968
|
+
layer = 0;
|
|
969
|
+
if (now() >= deadline)
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
this._cache.advance(render, run, layer, style);
|
|
973
|
+
const drawn = this._scratch.stats;
|
|
974
|
+
stats.draw.features += drawn.features;
|
|
975
|
+
stats.draw.vertices += drawn.vertices;
|
|
976
|
+
stats.draw.decimated += drawn.decimated;
|
|
977
|
+
stats.draw.culled += drawn.culled;
|
|
978
|
+
stats.draw.batches += drawn.batches;
|
|
979
|
+
stats.rasterMs += now() - started;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* How large a path to accumulate before flushing it.
|
|
983
|
+
*
|
|
984
|
+
* One number for both backends, which it was not until react-x11 2.6.1.
|
|
985
|
+
* Before it, the two rasterizers wanted opposite things — X11 turns a
|
|
986
|
+
* fill into one a8 coverage mask upload, so a bigger path is fewer
|
|
987
|
+
* uploads over the same pixels, while `CGContextStrokePath` was quadratic
|
|
988
|
+
* in the number of subpaths — and this element probed the backend and
|
|
989
|
+
* picked 512 or 12,000. Core chunks a Cocoa stroke itself now
|
|
990
|
+
* (react-x11#457), at a size it can choose and a caller cannot, so
|
|
991
|
+
* batching small on that backend only defeats it: on the profiling corpus
|
|
992
|
+
* 12,000 measures 114 ms against 512's 142 ms at zoom 8, and 96 against
|
|
993
|
+
* 101 at zoom 12.
|
|
994
|
+
*
|
|
995
|
+
* The prop stays, because the number is still a real X11 trade.
|
|
996
|
+
*/
|
|
997
|
+
_batchVertices() {
|
|
998
|
+
return this._prop('batchVertices') ?? BATCH_VERTICES;
|
|
999
|
+
}
|
|
1000
|
+
_uploadRaster(cached, render) {
|
|
1001
|
+
const raster = cached.raster;
|
|
1002
|
+
if (!raster)
|
|
1003
|
+
return;
|
|
1004
|
+
const context = render.context;
|
|
1005
|
+
if (isMapCanvas(context) && context.putImageData) {
|
|
1006
|
+
context.putImageData(raster, 0, 0);
|
|
1007
|
+
}
|
|
1008
|
+
render.progress = -1;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Composite one tile's surface, clipped to the pane.
|
|
1012
|
+
*
|
|
1013
|
+
* Two things this has to get right, and they pull in different
|
|
1014
|
+
* directions.
|
|
1015
|
+
*
|
|
1016
|
+
* **The destination edges are rounded independently**, so two tiles that
|
|
1017
|
+
* share an edge round it to the same device pixel and abut exactly. Round
|
|
1018
|
+
* the origin and the size instead and adjacent tiles differ by a pixel
|
|
1019
|
+
* wherever the fractional zoom lands, which draws a grid of hairlines
|
|
1020
|
+
* across the map — the classic tiled-renderer seam.
|
|
1021
|
+
*
|
|
1022
|
+
* **And the destination is clipped before it is handed over**, because
|
|
1023
|
+
* XRender takes composite coordinates as **int16** and an overzoomed tile
|
|
1024
|
+
* is far larger than the pane: at zoom 22 against a pyramid that stops at
|
|
1025
|
+
* 14, one tile is `512 · 2^8` = 131,072 logical pixels across, so a tile
|
|
1026
|
+
* that overlaps the pane can start 73,000 pixels outside it. Unclipped
|
|
1027
|
+
* that is a `RangeError` from `x11/lib/ext/render.js` thrown inside
|
|
1028
|
+
* `paint`, which is the same shape of bug as the unclipped overlay and a
|
|
1029
|
+
* different limit — 32,767 rather than the stroke path's 16.16 fixed
|
|
1030
|
+
* point. Clipping the destination and moving the source rectangle to
|
|
1031
|
+
* match keeps the scale factor `sw/dw` exactly what it was, so nothing
|
|
1032
|
+
* about the picture changes.
|
|
1033
|
+
*/
|
|
1034
|
+
_composite(ctx, surface, size,
|
|
1035
|
+
/** Where it lands, in pane-local logical pixels. */
|
|
1036
|
+
dest, pane, scale,
|
|
1037
|
+
/** Which sub-square of the surface to take, in `subSpan`ths. */
|
|
1038
|
+
subX, subY, subSpan) {
|
|
1039
|
+
if (!ctx.drawImage || size <= 0)
|
|
1040
|
+
return;
|
|
1041
|
+
const x0 = Math.round((pane.x + dest.x) * scale);
|
|
1042
|
+
const y0 = Math.round((pane.y + dest.y) * scale);
|
|
1043
|
+
const x1 = Math.round((pane.x + dest.x + dest.width) * scale);
|
|
1044
|
+
const y1 = Math.round((pane.y + dest.y + dest.height) * scale);
|
|
1045
|
+
if (x1 <= x0 || y1 <= y0)
|
|
1046
|
+
return;
|
|
1047
|
+
// The clip is the content box — device pixels, like everything core
|
|
1048
|
+
// hands an element — so what survives is bounded by the window.
|
|
1049
|
+
const box = this.contentBox();
|
|
1050
|
+
const cx0 = Math.max(x0, Math.floor(box.x));
|
|
1051
|
+
const cy0 = Math.max(y0, Math.floor(box.y));
|
|
1052
|
+
const cx1 = Math.min(x1, Math.ceil(box.x + box.width));
|
|
1053
|
+
const cy1 = Math.min(y1, Math.ceil(box.y + box.height));
|
|
1054
|
+
if (cx1 <= cx0 || cy1 <= cy0)
|
|
1055
|
+
return;
|
|
1056
|
+
const span = size / subSpan;
|
|
1057
|
+
// Source pixels per destination pixel. Preserved exactly by the
|
|
1058
|
+
// clipping below, which is what keeps the composite's scale right.
|
|
1059
|
+
const kx = span / (x1 - x0);
|
|
1060
|
+
const ky = span / (y1 - y0);
|
|
1061
|
+
const sx = subX * span + (cx0 - x0) * kx;
|
|
1062
|
+
const sy = subY * span + (cy0 - y0) * ky;
|
|
1063
|
+
// Clamped to the surface: a rounding of the destination edges must not
|
|
1064
|
+
// sample a pixel that is not there.
|
|
1065
|
+
const sw = Math.min((cx1 - cx0) * kx, size - sx);
|
|
1066
|
+
const sh = Math.min((cy1 - cy0) * ky, size - sy);
|
|
1067
|
+
if (!(sw > 0) || !(sh > 0))
|
|
1068
|
+
return;
|
|
1069
|
+
ctx.drawImage(surface, sx, sy, sw, sh, cx0, cy0, cx1 - cx0, cy1 - cy0);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Place the labels if the placement is stale, then draw the ones on
|
|
1073
|
+
* screen.
|
|
1074
|
+
*
|
|
1075
|
+
* The placement is keyed on the zoom and the cache generation, and *not*
|
|
1076
|
+
* on the camera position — which is the whole reason it is computed in
|
|
1077
|
+
* world pixels. A pan reuses it and blits; a zoom recomputes it, and a
|
|
1078
|
+
* zoom is already a full repaint. A tile arriving clears the key from
|
|
1079
|
+
* `onChange`, which is the only other thing that can change who wins.
|
|
1080
|
+
*/
|
|
1081
|
+
_paintLabels(ctx, transform, pane, style, stats) {
|
|
1082
|
+
const fonts = this.app?.fonts;
|
|
1083
|
+
if (!fonts)
|
|
1084
|
+
return; // headless: nothing to shape with
|
|
1085
|
+
const text = this.resolvedTextStyle();
|
|
1086
|
+
const family = this._prop('mapStyle')?.fontFamily ??
|
|
1087
|
+
this._defaultStyle?.fontFamily ??
|
|
1088
|
+
text.family;
|
|
1089
|
+
if (!this._shaper) {
|
|
1090
|
+
this._shaper = new LabelShaper(fonts, family, this._scale);
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
this._shaper.reconfigure(fonts, family, this._scale);
|
|
1094
|
+
}
|
|
1095
|
+
const key = `${quantize(transform.zoom)}|${this._cache.generation}`;
|
|
1096
|
+
if (key !== this._labelKey) {
|
|
1097
|
+
this._labelKey = key;
|
|
1098
|
+
const styleZoom = Math.floor(transform.zoom);
|
|
1099
|
+
// Labels come from the tile the **data** came from, at the depth that
|
|
1100
|
+
// source actually cuts — past which many renderings share one tile
|
|
1101
|
+
// and collecting per rendering would place every label `span²` times.
|
|
1102
|
+
const wanted = new Set(this._sources().map((source) => Math.min(styleZoom, pyramid(source).maxZoom)));
|
|
1103
|
+
const candidates = [];
|
|
1104
|
+
for (const cached of this._cache.dataEntries()) {
|
|
1105
|
+
if (cached.status !== 'ready' || !cached.vector)
|
|
1106
|
+
continue;
|
|
1107
|
+
if (!wanted.has(cached.tile.z))
|
|
1108
|
+
continue;
|
|
1109
|
+
const at = `${cached.key}|${styleZoom}`;
|
|
1110
|
+
let found = this._candidates.get(at);
|
|
1111
|
+
if (!found) {
|
|
1112
|
+
found = collectLabels(cached.vector, cached.tile, style, styleZoom, this._geometry);
|
|
1113
|
+
// Bounded the way the shaper's cache is: a map panned across a
|
|
1114
|
+
// continent must not turn this into a leak, and rebuilding a
|
|
1115
|
+
// tile's candidates is one walk over its symbol layers.
|
|
1116
|
+
if (this._candidates.size > 512)
|
|
1117
|
+
this._candidates.clear();
|
|
1118
|
+
this._candidates.set(at, found);
|
|
1119
|
+
}
|
|
1120
|
+
for (const candidate of found)
|
|
1121
|
+
candidates.push(candidate);
|
|
1122
|
+
}
|
|
1123
|
+
this._labels = placeLabels(candidates, transform.world, this._shaper);
|
|
1124
|
+
}
|
|
1125
|
+
stats.labels = drawLabels(ctx, this._labels, transform, pane, this._scale, this._frameClip ? { ...this._frameClip } : null, this._shaper);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* The attribution.
|
|
1129
|
+
*
|
|
1130
|
+
* Drawn by the map rather than left to the application because for open
|
|
1131
|
+
* data it is a licence condition rather than a nicety, and a component
|
|
1132
|
+
* whose default quietly omitted it would put every application that used
|
|
1133
|
+
* it in breach. `attribution=""` is the way to say it has been put
|
|
1134
|
+
* somewhere else.
|
|
1135
|
+
*/
|
|
1136
|
+
_paintAttribution(ctx, pane, palette) {
|
|
1137
|
+
const text = this._attributionText();
|
|
1138
|
+
if (!text || !this._shaper)
|
|
1139
|
+
return;
|
|
1140
|
+
const shaped = this._shaper.shape(text, 9, palette.text);
|
|
1141
|
+
if (!shaped)
|
|
1142
|
+
return;
|
|
1143
|
+
const scale = this._scale;
|
|
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;
|
|
1149
|
+
ctx.save();
|
|
1150
|
+
if (ctx.globalAlpha !== undefined)
|
|
1151
|
+
ctx.globalAlpha = 0.72;
|
|
1152
|
+
ctx.fillStyle = palette.background;
|
|
1153
|
+
ctx.fillRect(Math.round(x * scale), Math.round(y * scale), Math.ceil(width * scale), Math.ceil(height * scale));
|
|
1154
|
+
if (ctx.globalAlpha !== undefined)
|
|
1155
|
+
ctx.globalAlpha = 1;
|
|
1156
|
+
shaped.layout.draw(ctx, Math.round((x + padding) * scale), Math.round((y + padding / 2) * scale));
|
|
1157
|
+
ctx.restore();
|
|
1158
|
+
}
|
|
1159
|
+
// --- behaviour -----------------------------------------------------------
|
|
1160
|
+
/** An event's position in the pane's own logical pixels. A synthetic
|
|
1161
|
+
* event's `x`/`y` are logical and relative to the window, so only the
|
|
1162
|
+
* pane's own origin has to come off. */
|
|
1163
|
+
_point(ev) {
|
|
1164
|
+
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
|
+
return {
|
|
1170
|
+
lngLat: unprojectPoint(this._transform(), point.x, point.y),
|
|
1171
|
+
x: point.x,
|
|
1172
|
+
y: point.y,
|
|
1173
|
+
marker,
|
|
1174
|
+
shiftKey: ev.shiftKey ?? false,
|
|
1175
|
+
ctrlKey: ev.ctrlKey ?? false,
|
|
1176
|
+
altKey: ev.altKey ?? false,
|
|
1177
|
+
metaKey: ev.metaKey ?? false,
|
|
1178
|
+
button: ev.button ?? 1,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
defaultMouseDown(ev) {
|
|
1182
|
+
const point = this._point(ev);
|
|
1183
|
+
const marker = markerAt(this._markers(), this._transform(), point.x, point.y);
|
|
1184
|
+
if (marker) {
|
|
1185
|
+
this._gesture = {
|
|
1186
|
+
kind: 'marker',
|
|
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();
|
|
1217
|
+
ev.capturePointer?.();
|
|
1218
|
+
}
|
|
1219
|
+
defaultMouseDrag(ev) {
|
|
1220
|
+
const gesture = this._gesture;
|
|
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);
|
|
1243
|
+
}
|
|
1244
|
+
defaultMouseUp(ev) {
|
|
1245
|
+
const gesture = this._gesture;
|
|
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));
|
|
1266
|
+
}
|
|
1267
|
+
defaultMouseMove(ev) {
|
|
1268
|
+
const notify = this._prop('onMarkerHover');
|
|
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));
|
|
1278
|
+
}
|
|
1279
|
+
defaultMouseLeave() {
|
|
1280
|
+
if (this._hover === null)
|
|
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);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* The wheel is a zoom, not a scroll, so it is answered here rather than
|
|
1289
|
+
* through `canScroll`/`scrollBy` — the case react-x11's docs/extending.md
|
|
1290
|
+
* carves out. A zoom needs the point that must *not* move, which the
|
|
1291
|
+
* scroll chain never hands over.
|
|
1292
|
+
*/
|
|
1293
|
+
defaultWheel(ev) {
|
|
1294
|
+
if (!this._interactive())
|
|
1295
|
+
return;
|
|
1296
|
+
const pane = this._pane();
|
|
1297
|
+
this._touchGesture();
|
|
1298
|
+
this._zoomAbout(-(ev.deltaY ?? 0) * WHEEL_ZOOM * 0.02, ev.x - pane.x, ev.y - pane.y);
|
|
1299
|
+
// Consumed whether or not the zoom moved: a wheel over a map is never
|
|
1300
|
+
// meant for whatever is behind it.
|
|
1301
|
+
ev.preventDefault();
|
|
1302
|
+
}
|
|
1303
|
+
defaultKeyDown(ev) {
|
|
1304
|
+
// `Node` declares the default actions optional — an element that has
|
|
1305
|
+
// no behaviour of its own simply has none — so calling up is an
|
|
1306
|
+
// optional call rather than a plain one.
|
|
1307
|
+
if (!this._interactive()) {
|
|
1308
|
+
super.defaultKeyDown?.(ev);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const pane = this._pane();
|
|
1312
|
+
const step = ev.shiftKey ? 200 : 60;
|
|
1313
|
+
switch (ev.keysym) {
|
|
1314
|
+
case 0xff51: // XK_Left
|
|
1315
|
+
this.panBy(-step, 0);
|
|
1316
|
+
break;
|
|
1317
|
+
case 0xff53: // XK_Right
|
|
1318
|
+
this.panBy(step, 0);
|
|
1319
|
+
break;
|
|
1320
|
+
case 0xff52: // XK_Up
|
|
1321
|
+
this.panBy(0, -step);
|
|
1322
|
+
break;
|
|
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();
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* What a screen reader meets.
|
|
1345
|
+
*
|
|
1346
|
+
* A map is one painted rectangle to an assistive technology, and its
|
|
1347
|
+
* markers are the only things in it that are *objects* rather than
|
|
1348
|
+
* cartography — so those are the scene, and the map itself carries the
|
|
1349
|
+
* camera in its description. Announcing every road would be worse than
|
|
1350
|
+
* announcing none.
|
|
1351
|
+
*/
|
|
1352
|
+
a11yScene() {
|
|
1353
|
+
if (!this._visible())
|
|
1354
|
+
return [];
|
|
1355
|
+
const transform = this._transform();
|
|
1356
|
+
const pane = this._pane();
|
|
1357
|
+
const scale = this._scale;
|
|
1358
|
+
const items = [];
|
|
1359
|
+
for (const marker of this._markers()) {
|
|
1360
|
+
const rect = markerRect(marker, transform);
|
|
1361
|
+
if (rect.x + rect.width < 0 ||
|
|
1362
|
+
rect.y + rect.height < 0 ||
|
|
1363
|
+
rect.x > pane.width ||
|
|
1364
|
+
rect.y > pane.height) {
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
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
|
+
}
|
|
1383
|
+
return items;
|
|
1384
|
+
}
|
|
1385
|
+
applyProps(next, prev) {
|
|
1386
|
+
const before = prev ?? this.props;
|
|
1387
|
+
super.applyProps(next, prev);
|
|
1388
|
+
// Every one of these is in `selfDamagedProps`, so the commit claimed
|
|
1389
|
+
// nothing for them and this is the only claim there will be.
|
|
1390
|
+
if (next.mapStyle !== before.mapStyle) {
|
|
1391
|
+
this._prepared = null;
|
|
1392
|
+
this._preparedFrom = null;
|
|
1393
|
+
this._candidates.clear();
|
|
1394
|
+
this._cache.invalidateStyle();
|
|
1395
|
+
this._labelKey = '';
|
|
1396
|
+
this._repaint('props');
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (next.sources !== before.sources) {
|
|
1400
|
+
this._labelKey = '';
|
|
1401
|
+
this._repaint('props');
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (next.camera !== before.camera && next.camera !== undefined) {
|
|
1405
|
+
const camera = next.camera;
|
|
1406
|
+
const previous = before.camera ?? this._camera;
|
|
1407
|
+
if (camera.zoom !== previous.zoom ||
|
|
1408
|
+
camera.center.lon !== previous.center.lon ||
|
|
1409
|
+
camera.center.lat !== previous.center.lat) {
|
|
1410
|
+
if (camera.zoom !== previous.zoom)
|
|
1411
|
+
this._labelKey = '';
|
|
1412
|
+
if (!this._blitPan(previous, camera))
|
|
1413
|
+
this._repaint('props');
|
|
1414
|
+
}
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (next.markers !== before.markers || next.overlays !== before.overlays) {
|
|
1418
|
+
// The union of where they were and where they are. A vehicle moving
|
|
1419
|
+
// across a city claims two marker-sized boxes rather than the pane.
|
|
1420
|
+
this._claimOverlayDamage(before.markers ?? [], next.markers ?? [], next.overlays !== before.overlays);
|
|
1421
|
+
this.notifyA11ySceneChanged();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
_claimOverlayDamage(before, after, overlaysChanged) {
|
|
1425
|
+
if (overlaysChanged || !this._visible()) {
|
|
1426
|
+
// An overlay is an arbitrary polyline; its damage is not worth
|
|
1427
|
+
// deriving, and a route changing is not a per-frame event.
|
|
1428
|
+
this._repaint('props');
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const transform = this._transform();
|
|
1432
|
+
const pane = this._pane();
|
|
1433
|
+
const claim = (markers) => {
|
|
1434
|
+
for (const marker of markers) {
|
|
1435
|
+
const rect = markerRect(marker, transform);
|
|
1436
|
+
this._claim({
|
|
1437
|
+
x: pane.x + rect.x - 3,
|
|
1438
|
+
y: pane.y + rect.y - 3,
|
|
1439
|
+
width: rect.width + 6,
|
|
1440
|
+
height: rect.height + 6,
|
|
1441
|
+
}, 'props');
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
// Both sets, because a marker that was removed has to be painted over.
|
|
1445
|
+
claim(before);
|
|
1446
|
+
claim(after);
|
|
1447
|
+
}
|
|
1448
|
+
destroySubtree() {
|
|
1449
|
+
if (this._settleTimer !== null) {
|
|
1450
|
+
timers.clearTimeout?.(this._settleTimer);
|
|
1451
|
+
this._settleTimer = null;
|
|
1452
|
+
}
|
|
1453
|
+
this._cache.destroy();
|
|
1454
|
+
this._candidates.clear();
|
|
1455
|
+
this._labels = [];
|
|
1456
|
+
super.destroySubtree();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
//# sourceMappingURL=node.js.map
|