@react-x11/components 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -4
- 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 +7 -1
- 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/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 +13 -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 +98 -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/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,978 @@
|
|
|
1
|
+
// The rasterizer: a tile's features, drawn.
|
|
2
|
+
//
|
|
3
|
+
// One function does the whole of it ({@link drawTileLayers}), and it is
|
|
4
|
+
// written for one situation — twelve thousand features and a hundred and
|
|
5
|
+
// eighty thousand vertices, sixty times a second — so every decision in it
|
|
6
|
+
// is about the per-vertex and per-feature paths. In rough order of how much
|
|
7
|
+
// each is worth on the corpus in `scripts/bench/`:
|
|
8
|
+
//
|
|
9
|
+
// 1. **One path per style layer, not per feature.** ntk batches a fill
|
|
10
|
+
// into server-side trapezoids and a stroke into server-side spans, both
|
|
11
|
+
// gated on the whole path arriving before `fill()`. Drawing 2,700
|
|
12
|
+
// streets as 2,700 `beginPath`/`stroke` pairs is 2,700 round-trippable
|
|
13
|
+
// request groups; drawing them as one path is a handful. The path is
|
|
14
|
+
// flushed every {@link BATCH_VERTICES} vertices so a pathological layer
|
|
15
|
+
// cannot make one unbounded request.
|
|
16
|
+
// 2. **Vertex decimation against the target grid.** A vertex less than
|
|
17
|
+
// `tolerance` from the one before it lands on the same pixel, so it is
|
|
18
|
+
// dropped. Sounds marginal; is not — Shortbread's low-zoom `land`
|
|
19
|
+
// layer is a single 130,000-vertex forest multipolygon whose vertices
|
|
20
|
+
// are, at zoom 8, roughly a fifth of a pixel apart.
|
|
21
|
+
// 3. **A sub-pixel feature is skipped whole.** The bounding box came free
|
|
22
|
+
// from the geometry read, so a building a third of a pixel across is
|
|
23
|
+
// one comparison rather than a path.
|
|
24
|
+
// 4. **Paint resolved per layer.** A zoom ramp, a filter compile and a
|
|
25
|
+
// colour string are per layer, never per feature. The filter arrives
|
|
26
|
+
// already compiled ({@link compileFilter}).
|
|
27
|
+
// 5. **Nothing allocates per feature.** One {@link GeometryBuffer} and one
|
|
28
|
+
// {@link FeatureCursor} for the whole call.
|
|
29
|
+
//
|
|
30
|
+
// The unit convention is the other half of the file, and it is the thing to
|
|
31
|
+
// get right before changing anything: **everything here is in *target*
|
|
32
|
+
// pixels**, where the target is either a tile's own surface (the retained
|
|
33
|
+
// path, where the tile occupies the whole surface) or the window's paint
|
|
34
|
+
// context (the direct path). {@link TileDraw} carries the two numbers that
|
|
35
|
+
// make one out of the other, so the same code serves both and neither has a
|
|
36
|
+
// branch in the vertex loop.
|
|
37
|
+
import { clipRing, clipSegment } from './clip.js';
|
|
38
|
+
import type { ClipRect } from './clip.js';
|
|
39
|
+
import type { CompiledFilter, MapStyleLayer, Zoomed } from './style.js';
|
|
40
|
+
import { compileFilter, resolveZoomed } from './style.js';
|
|
41
|
+
import { GeomType, GeometryBuffer } from './mvt.js';
|
|
42
|
+
import type { VectorTile, VectorTileLayer } from './mvt.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The slice of ntk's 2d context this draws through.
|
|
46
|
+
*
|
|
47
|
+
* Structural because react-x11 types `Context2D` as `unknown` on purpose —
|
|
48
|
+
* it is ntk's API rather than react-x11's — so something has to name the
|
|
49
|
+
* operations, and naming them once beats naming them at sixty call sites
|
|
50
|
+
* (the same argument `src/flow/draw.ts` makes).
|
|
51
|
+
*/
|
|
52
|
+
export interface MapCanvas {
|
|
53
|
+
fillStyle: unknown;
|
|
54
|
+
strokeStyle: unknown;
|
|
55
|
+
lineWidth: number;
|
|
56
|
+
lineJoin?: unknown;
|
|
57
|
+
lineCap?: unknown;
|
|
58
|
+
globalAlpha?: number;
|
|
59
|
+
save(): void;
|
|
60
|
+
restore(): void;
|
|
61
|
+
beginPath(): void;
|
|
62
|
+
closePath(): void;
|
|
63
|
+
moveTo(x: number, y: number): void;
|
|
64
|
+
lineTo(x: number, y: number): void;
|
|
65
|
+
rect(x: number, y: number, w: number, h: number): void;
|
|
66
|
+
arc(x: number, y: number, r: number, from: number, to: number): void;
|
|
67
|
+
fill(): void;
|
|
68
|
+
stroke(): void;
|
|
69
|
+
clip(): void;
|
|
70
|
+
fillRect(x: number, y: number, w: number, h: number): void;
|
|
71
|
+
/** One request for many rectangles — what a marker cluster and a
|
|
72
|
+
* scale bar are drawn with. */
|
|
73
|
+
fillRects?(rects: readonly number[]): void;
|
|
74
|
+
setLineDash?(segments: readonly number[]): void;
|
|
75
|
+
roundRect?(x: number, y: number, w: number, h: number, radii: number): void;
|
|
76
|
+
drawImage?(image: unknown, ...args: number[]): void;
|
|
77
|
+
/** Straight RGBA in, for a raster tile the application decoded. */
|
|
78
|
+
putImageData?(
|
|
79
|
+
data: { width: number; height: number; data: Uint8Array },
|
|
80
|
+
x: number,
|
|
81
|
+
y: number,
|
|
82
|
+
): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether a context can draw at all.
|
|
87
|
+
*
|
|
88
|
+
* The mock backend the headless suite runs on has no path API, which is the
|
|
89
|
+
* repo convention for keeping a drawn element testable: the element's paint
|
|
90
|
+
* becomes a no-op there rather than a throw, and everything worth asserting
|
|
91
|
+
* about it — the cover, the projection, the cache, the hit test — is
|
|
92
|
+
* asserted without pixels.
|
|
93
|
+
*/
|
|
94
|
+
export function isMapCanvas(ctx: unknown): ctx is MapCanvas {
|
|
95
|
+
const c = ctx as Partial<MapCanvas> | null | undefined;
|
|
96
|
+
return typeof c?.beginPath === 'function' && typeof c?.fill === 'function';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const globals = globalThis as { performance?: { now(): number } };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The clock a rasterization budget is measured on.
|
|
103
|
+
*
|
|
104
|
+
* `performance.now()` rather than `Date.now()`, because a budget is a
|
|
105
|
+
* sub-frame quantity and `Date.now()` counts whole milliseconds: an 8 ms
|
|
106
|
+
* budget measured on it carries 12% of error, and anything under 1 ms
|
|
107
|
+
* rounds to a deadline that has already passed — so `rasterBudgetMs: 0.5`
|
|
108
|
+
* would silently mean "never rasterize", which is not what it says.
|
|
109
|
+
* Reached through `globalThis` because `src/` compiles with `types: []`.
|
|
110
|
+
*/
|
|
111
|
+
export function now(): number {
|
|
112
|
+
return globals.performance?.now() ?? Date.now();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Vertices after which the accumulated path is flushed. Large enough that
|
|
116
|
+
* a whole ordinary layer is one path, small enough that the 130,000-vertex
|
|
117
|
+
* outlier is a few rather than one enormous request. */
|
|
118
|
+
export const BATCH_VERTICES = 12_000;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* How a tile's own integer coordinates reach the target.
|
|
122
|
+
*
|
|
123
|
+
* `ox`/`oy`/`span` place the tile: its local `0` lands at `ox`, and its
|
|
124
|
+
* whole square is `span` target pixels across — so a layer's scale is
|
|
125
|
+
* `span / layer.extent`, computed **per layer**, because extent is a
|
|
126
|
+
* per-layer field and real tiles use that (OSM's Shortbread cuts `streets`,
|
|
127
|
+
* `land`, `ocean` and `water_polygons` at 2048 and its other twenty layers
|
|
128
|
+
* at 4096; a renderer that reads it once per tile draws half of them at
|
|
129
|
+
* twice their size).
|
|
130
|
+
*
|
|
131
|
+
* `pixelsPerLogical` is what a style's lengths are multiplied by. On the
|
|
132
|
+
* direct path it is simply the display scale. On the retained path it is
|
|
133
|
+
* `surfaceSize / tileScreenSize`, which is the display scale divided by
|
|
134
|
+
* whatever the composite is about to multiply by — so a two-logical-pixel
|
|
135
|
+
* road is two logical pixels on screen at every fractional zoom, without
|
|
136
|
+
* the raster being redrawn for any of them.
|
|
137
|
+
*/
|
|
138
|
+
export interface TileDraw {
|
|
139
|
+
ox: number;
|
|
140
|
+
oy: number;
|
|
141
|
+
span: number;
|
|
142
|
+
pixelsPerLogical: number;
|
|
143
|
+
/** The zoom the style is resolved at. */
|
|
144
|
+
zoom: number;
|
|
145
|
+
/** Target pixels below which a vertex is dropped as a duplicate of the
|
|
146
|
+
* one before it. `0` disables decimation. */
|
|
147
|
+
tolerance?: number;
|
|
148
|
+
/** Target pixels below which a whole feature is skipped. */
|
|
149
|
+
minFeature?: number;
|
|
150
|
+
/** Clip rectangle in target coordinates — the direct path's viewport.
|
|
151
|
+
* Null on the retained path, where the surface is the clip. */
|
|
152
|
+
clip?: { x: number; y: number; width: number; height: number } | null;
|
|
153
|
+
/**
|
|
154
|
+
* Vertices after which the accumulated path is flushed, if not
|
|
155
|
+
* {@link BATCH_VERTICES}.
|
|
156
|
+
*
|
|
157
|
+
* One number for both backends since react-x11 2.6.1, and it was not
|
|
158
|
+
* before that. On **X11** a fill or stroke becomes an a8 coverage mask
|
|
159
|
+
* over the path's bounding box, uploaded with one `PutImage`, so a bigger
|
|
160
|
+
* path is fewer uploads over the same pixels — the number wants to be
|
|
161
|
+
* large. On the **Cocoa** backend `CGContextStrokePath` used to be
|
|
162
|
+
* quadratic in the number of subpaths, so it wanted the opposite, and
|
|
163
|
+
* `<Map>` probed the backend to pick. Core chunks that stroke itself now
|
|
164
|
+
* (react-x11#457), at a size it can choose and a caller cannot, so
|
|
165
|
+
* batching small there only defeats it.
|
|
166
|
+
*/
|
|
167
|
+
batchVertices?: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What a rasterization cost, for the bench and for `onFrame`. */
|
|
171
|
+
export interface DrawStats {
|
|
172
|
+
layers: number;
|
|
173
|
+
features: number;
|
|
174
|
+
/** Vertices that survived decimation and were sent. */
|
|
175
|
+
vertices: number;
|
|
176
|
+
/** Vertices dropped as sub-pixel duplicates. */
|
|
177
|
+
decimated: number;
|
|
178
|
+
/** Features skipped because their whole box was sub-pixel. */
|
|
179
|
+
culled: number;
|
|
180
|
+
/** `fill()` and `stroke()` calls — the request count, near enough. */
|
|
181
|
+
batches: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function emptyStats(): DrawStats {
|
|
185
|
+
return {
|
|
186
|
+
layers: 0,
|
|
187
|
+
features: 0,
|
|
188
|
+
vertices: 0,
|
|
189
|
+
decimated: 0,
|
|
190
|
+
culled: 0,
|
|
191
|
+
batches: 0,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A style layer with its filter already compiled — what a cache holds so
|
|
196
|
+
* the compile happens once per style rather than once per tile. */
|
|
197
|
+
export interface PreparedLayer {
|
|
198
|
+
layer: MapStyleLayer;
|
|
199
|
+
filter: CompiledFilter;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A maximal run of consecutive style layers over one source layer. */
|
|
203
|
+
export interface StyleRun {
|
|
204
|
+
sourceLayer: string;
|
|
205
|
+
/** `[from, to)` into {@link PreparedStyle.layers}. */
|
|
206
|
+
from: number;
|
|
207
|
+
to: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A style with its filters compiled and its runs found — computed once per
|
|
212
|
+
* style, not once per tile.
|
|
213
|
+
*
|
|
214
|
+
* The runs are also the unit of **resumable** rasterization: a tile's own
|
|
215
|
+
* layers are drawn run by run, with a time budget between them, so a
|
|
216
|
+
* hundred-millisecond tile becomes a dozen frames that each stay inside
|
|
217
|
+
* one. See {@link drawTileRun}.
|
|
218
|
+
*/
|
|
219
|
+
export interface PreparedStyle {
|
|
220
|
+
layers: PreparedLayer[];
|
|
221
|
+
runs: StyleRun[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Compile a style once.
|
|
226
|
+
*
|
|
227
|
+
* Runs rather than a global grouping by source layer, because the order a
|
|
228
|
+
* style lists its layers in *is* the cartography — water over landuse,
|
|
229
|
+
* buildings over both — and a grouping would reorder a style that
|
|
230
|
+
* interleaves two sources. A run preserves order exactly and captures the
|
|
231
|
+
* win whenever a style is written the way the casing-then-fill split forces
|
|
232
|
+
* anyway: a road network is fourteen consecutive layers over `streets`.
|
|
233
|
+
*/
|
|
234
|
+
export function prepareStyle(style: {
|
|
235
|
+
layers: readonly MapStyleLayer[];
|
|
236
|
+
}): PreparedStyle {
|
|
237
|
+
const layers = style.layers.map((layer) => ({
|
|
238
|
+
layer,
|
|
239
|
+
filter: compileFilter(layer.filter),
|
|
240
|
+
}));
|
|
241
|
+
const runs: StyleRun[] = [];
|
|
242
|
+
let i = 0;
|
|
243
|
+
while (i < layers.length) {
|
|
244
|
+
const sourceLayer = layers[i].layer.sourceLayer;
|
|
245
|
+
let end = i + 1;
|
|
246
|
+
while (
|
|
247
|
+
end < layers.length &&
|
|
248
|
+
layers[end].layer.sourceLayer === sourceLayer
|
|
249
|
+
) {
|
|
250
|
+
end++;
|
|
251
|
+
}
|
|
252
|
+
runs.push({ sourceLayer, from: i, to: end });
|
|
253
|
+
i = end;
|
|
254
|
+
}
|
|
255
|
+
return { layers, runs };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function num(
|
|
259
|
+
value: Zoomed<number> | undefined,
|
|
260
|
+
zoom: number,
|
|
261
|
+
fallback: number,
|
|
262
|
+
): number {
|
|
263
|
+
return value === undefined ? fallback : resolveZoomed(value, zoom);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function str(
|
|
267
|
+
value: Zoomed<string> | undefined,
|
|
268
|
+
zoom: number,
|
|
269
|
+
): string | undefined {
|
|
270
|
+
return value === undefined ? undefined : resolveZoomed(value, zoom);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Scratch a caller keeps between tiles, so nothing here allocates per
|
|
275
|
+
* feature or per layer.
|
|
276
|
+
*/
|
|
277
|
+
export class DrawScratch {
|
|
278
|
+
buffer = new GeometryBuffer();
|
|
279
|
+
/** One clipped ring at a time, reused, so the overzoom path allocates
|
|
280
|
+
* nothing per part either. */
|
|
281
|
+
ring: number[] = [];
|
|
282
|
+
stats: DrawStats = emptyStats();
|
|
283
|
+
private _buckets: number[][] = [];
|
|
284
|
+
|
|
285
|
+
/** `n` index lists, emptied. Grown to the widest run ever seen and then
|
|
286
|
+
* reused for the life of the element. */
|
|
287
|
+
buckets(n: number): number[][] {
|
|
288
|
+
while (this._buckets.length < n) this._buckets.push([]);
|
|
289
|
+
for (let i = 0; i < n; i++) this._buckets[i].length = 0;
|
|
290
|
+
return this._buckets;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resetStats(): DrawStats {
|
|
294
|
+
this.stats = emptyStats();
|
|
295
|
+
return this.stats;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Whether a style layer draws at this zoom at all. */
|
|
300
|
+
function activeAt(layer: MapStyleLayer, zoom: number): boolean {
|
|
301
|
+
if (layer.visible === false) return false;
|
|
302
|
+
if (layer.type === 'symbol') return false; // placed and drawn per frame
|
|
303
|
+
if (layer.minZoom !== undefined && zoom < layer.minZoom) return false;
|
|
304
|
+
if (layer.maxZoom !== undefined && zoom >= layer.maxZoom) return false;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Which geometry types a style layer draws.
|
|
310
|
+
*
|
|
311
|
+
* A `line` layer takes polygons too — stroking a ring is what a style
|
|
312
|
+
* asking for a pier's outline means — and a `fill` layer takes only
|
|
313
|
+
* polygons, since there is no area to fill in a line.
|
|
314
|
+
*/
|
|
315
|
+
function typeAllowed(type: MapStyleLayer['type'], geometry: GeomType): boolean {
|
|
316
|
+
if (type === 'fill') return geometry === GeomType.Polygon;
|
|
317
|
+
if (type === 'circle') return geometry === GeomType.Point;
|
|
318
|
+
if (type === 'line') return geometry !== GeomType.Point;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Draw every layer of the style this tile has data for.
|
|
324
|
+
*
|
|
325
|
+
* The style is walked in **runs of consecutive layers over one source
|
|
326
|
+
* layer**, and each run's features are seeked once for the whole run. That
|
|
327
|
+
* is worth stating plainly because it is the difference between a fast
|
|
328
|
+
* zoom-12 tile and a slow one: a road network is fourteen style layers over
|
|
329
|
+
* `streets` (seven classes, and a casing pass before the fill pass so that
|
|
330
|
+
* junctions knit), so a layer-at-a-time walk parses each of eight thousand
|
|
331
|
+
* features' headers and tags fourteen times over. A run parses them once
|
|
332
|
+
* and asks fourteen already-compiled filters about them.
|
|
333
|
+
*
|
|
334
|
+
* Runs rather than a global grouping by source layer, because the order a
|
|
335
|
+
* style lists its layers in *is* the cartography — water over landuse,
|
|
336
|
+
* buildings over both — and a grouping would reorder a style that
|
|
337
|
+
* interleaves. A run preserves order exactly and captures the win whenever
|
|
338
|
+
* a style is written the way the casing/fill split forces anyway.
|
|
339
|
+
*/
|
|
340
|
+
export function drawTileLayers(
|
|
341
|
+
ctx: MapCanvas,
|
|
342
|
+
tile: VectorTile,
|
|
343
|
+
prepared: PreparedStyle,
|
|
344
|
+
draw: TileDraw,
|
|
345
|
+
scratch: DrawScratch,
|
|
346
|
+
): DrawStats {
|
|
347
|
+
for (let run = 0; run < prepared.runs.length; run++) {
|
|
348
|
+
drawTileRun(ctx, tile, prepared, run, draw, scratch);
|
|
349
|
+
}
|
|
350
|
+
return scratch.stats;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* One run of the style, drawn.
|
|
355
|
+
*
|
|
356
|
+
* The unit a caller budgets in: the node draws runs until its per-frame
|
|
357
|
+
* budget is spent, remembers where it stopped, and comes back next frame.
|
|
358
|
+
* Layers are painted bottom-up, so a tile stopped part-way looks like a map
|
|
359
|
+
* whose upper layers have not arrived — which is what a partly-drawn map
|
|
360
|
+
* should look like — rather than like a hole.
|
|
361
|
+
*/
|
|
362
|
+
export function drawTileRun(
|
|
363
|
+
ctx: MapCanvas,
|
|
364
|
+
tile: VectorTile,
|
|
365
|
+
prepared: PreparedStyle,
|
|
366
|
+
runIndex: number,
|
|
367
|
+
draw: TileDraw,
|
|
368
|
+
scratch: DrawScratch,
|
|
369
|
+
resume?: {
|
|
370
|
+
/** Skip this many of the run's active layers — where a previous frame
|
|
371
|
+
* stopped. */
|
|
372
|
+
fromLayer?: number;
|
|
373
|
+
/** Stop after the layer that crosses this, on {@link now}'s clock. */
|
|
374
|
+
deadline?: number;
|
|
375
|
+
},
|
|
376
|
+
): number {
|
|
377
|
+
const stats = scratch.stats;
|
|
378
|
+
const run = prepared.runs[runIndex];
|
|
379
|
+
if (!run) return -1;
|
|
380
|
+
const source = tile.layers.get(run.sourceLayer);
|
|
381
|
+
if (!source || source.length === 0) return -1;
|
|
382
|
+
|
|
383
|
+
// The zoom gate before anything is touched. A style with forty layers
|
|
384
|
+
// draws eight at a given zoom, and the other thirty-two must not cost a
|
|
385
|
+
// feature walk.
|
|
386
|
+
const active: PreparedLayer[] = [];
|
|
387
|
+
for (let j = run.from; j < run.to; j++) {
|
|
388
|
+
if (activeAt(prepared.layers[j].layer, draw.zoom)) {
|
|
389
|
+
active.push(prepared.layers[j]);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (active.length === 0) return -1;
|
|
393
|
+
const from = resume?.fromLayer ?? 0;
|
|
394
|
+
if (from >= active.length) return -1;
|
|
395
|
+
stats.layers += active.length - from;
|
|
396
|
+
const deadline = resume?.deadline;
|
|
397
|
+
|
|
398
|
+
if (active.length === 1) {
|
|
399
|
+
// Everything but roads: one layer over the source, so there is nothing
|
|
400
|
+
// to share and no index list to build.
|
|
401
|
+
drawLayer(ctx, source, active[0], draw, scratch, null);
|
|
402
|
+
return -1;
|
|
403
|
+
}
|
|
404
|
+
// Several layers over one source: seek each feature once and ask every
|
|
405
|
+
// filter about it, rather than walking the layer once per style layer.
|
|
406
|
+
// On a zoom-12 tile that is one pass over 8,488 streets instead of
|
|
407
|
+
// fourteen.
|
|
408
|
+
const buckets = scratch.buckets(active.length);
|
|
409
|
+
const cursor = source.feature(0);
|
|
410
|
+
for (let f = 0; f < source.length; f++) {
|
|
411
|
+
source.seek(f, cursor);
|
|
412
|
+
for (let j = from; j < active.length; j++) {
|
|
413
|
+
const { layer, filter } = active[j];
|
|
414
|
+
if (!typeAllowed(layer.type, cursor.type)) continue;
|
|
415
|
+
if (!filter(cursor)) continue;
|
|
416
|
+
buckets[j].push(f);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Stopping between layers rather than only between runs, because a run is
|
|
420
|
+
// not a small unit: a road network is one run and fourteen layers, and
|
|
421
|
+
// one of those layers alone measured 90 ms on the profiling corpus. The
|
|
422
|
+
// budget has to be able to interrupt *inside* it or it is not a budget.
|
|
423
|
+
// Resuming re-does the bucketing pass above — one seek over the source
|
|
424
|
+
// layer, a few milliseconds — which is the price of not holding a frame's
|
|
425
|
+
// scratch across frames.
|
|
426
|
+
for (let j = from; j < active.length; j++) {
|
|
427
|
+
drawLayer(ctx, source, active[j], draw, scratch, buckets[j]);
|
|
428
|
+
if (deadline !== undefined && j + 1 < active.length && now() >= deadline) {
|
|
429
|
+
return j + 1;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return -1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function drawLayer(
|
|
436
|
+
ctx: MapCanvas,
|
|
437
|
+
source: VectorTileLayer,
|
|
438
|
+
prepared: PreparedLayer,
|
|
439
|
+
draw: TileDraw,
|
|
440
|
+
scratch: DrawScratch,
|
|
441
|
+
indices: readonly number[] | null,
|
|
442
|
+
): void {
|
|
443
|
+
const { layer, filter } = prepared;
|
|
444
|
+
switch (layer.type) {
|
|
445
|
+
case 'fill':
|
|
446
|
+
drawFill(ctx, source, layer, filter, draw, scratch, indices);
|
|
447
|
+
break;
|
|
448
|
+
case 'line':
|
|
449
|
+
drawLine(ctx, source, layer, filter, draw, scratch, indices);
|
|
450
|
+
break;
|
|
451
|
+
case 'circle':
|
|
452
|
+
drawCircle(ctx, source, layer, filter, draw, scratch, indices);
|
|
453
|
+
break;
|
|
454
|
+
case 'symbol':
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Append one part of a feature to the path, simplified against the target
|
|
461
|
+
* grid, and answer how many vertices were emitted.
|
|
462
|
+
*
|
|
463
|
+
* Two tests, in the order that makes the cheap one do most of the work:
|
|
464
|
+
*
|
|
465
|
+
* - **radial** — a vertex within `tolerance` of the one last kept lands on
|
|
466
|
+
* the same pixel, so it says nothing;
|
|
467
|
+
* - **perpendicular** — a vertex within `tolerance` of the straight line
|
|
468
|
+
* from the one last kept to the one after it is on that line, so it says
|
|
469
|
+
* nothing either. This is the one that matters on real data: a
|
|
470
|
+
* generalized coastline's vertices are about a pixel apart, which the
|
|
471
|
+
* radial test keeps, and nearly collinear, which this drops.
|
|
472
|
+
*
|
|
473
|
+
* The first and last vertices are always kept: a ring simplified down to
|
|
474
|
+
* its first point is not a ring, and a road whose end moved is a road that
|
|
475
|
+
* no longer meets the next one at the tile seam.
|
|
476
|
+
*/
|
|
477
|
+
function emitPart(
|
|
478
|
+
ctx: MapCanvas,
|
|
479
|
+
coords: Int32Array,
|
|
480
|
+
from: number,
|
|
481
|
+
to: number,
|
|
482
|
+
ox: number,
|
|
483
|
+
oy: number,
|
|
484
|
+
k: number,
|
|
485
|
+
/** Squared, so the vertex loop needs no square root. */
|
|
486
|
+
toleranceSq: number,
|
|
487
|
+
closed: boolean,
|
|
488
|
+
stats: DrawStats,
|
|
489
|
+
/**
|
|
490
|
+
* Clip the geometry to this box, or null to let the target clip it.
|
|
491
|
+
*
|
|
492
|
+
* Null on the ordinary path, where a tile is drawn at its own size and a
|
|
493
|
+
* vertex is a few thousand pixels at worst. Set when a tile is drawn past
|
|
494
|
+
* its own depth — one source tile rasterized into each cell of a grid
|
|
495
|
+
* over it — where a tile-wide polygon is `span` tiles wide in the
|
|
496
|
+
* surface's pixels: at 64 cells that is a hundred thousand, and ntk hands
|
|
497
|
+
* a stroke to XRender as 16.16 fixed point, which overflows a signed
|
|
498
|
+
* 32-bit word at 32,768. The surface would clip the *drawing* correctly;
|
|
499
|
+
* it is the numbers on the way there that do not fit.
|
|
500
|
+
*/
|
|
501
|
+
clip: ClipRect | null,
|
|
502
|
+
/** Scratch for the clipped ring, so a fill allocates nothing per part. */
|
|
503
|
+
scratch: number[],
|
|
504
|
+
): number {
|
|
505
|
+
if (clip) {
|
|
506
|
+
return emitClipped(
|
|
507
|
+
ctx,
|
|
508
|
+
coords,
|
|
509
|
+
from,
|
|
510
|
+
to,
|
|
511
|
+
ox,
|
|
512
|
+
oy,
|
|
513
|
+
k,
|
|
514
|
+
closed,
|
|
515
|
+
clip,
|
|
516
|
+
scratch,
|
|
517
|
+
stats,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const count = to - from;
|
|
521
|
+
if (count < 2) return 0;
|
|
522
|
+
let x = ox + coords[from * 2] * k;
|
|
523
|
+
let y = oy + coords[from * 2 + 1] * k;
|
|
524
|
+
ctx.moveTo(x, y);
|
|
525
|
+
let emitted = 1;
|
|
526
|
+
let keptX = x;
|
|
527
|
+
let keptY = y;
|
|
528
|
+
const last = to - 1;
|
|
529
|
+
for (let i = from + 1; i <= last; i++) {
|
|
530
|
+
x = ox + coords[i * 2] * k;
|
|
531
|
+
y = oy + coords[i * 2 + 1] * k;
|
|
532
|
+
if (toleranceSq > 0 && i !== last) {
|
|
533
|
+
const dx = x - keptX;
|
|
534
|
+
const dy = y - keptY;
|
|
535
|
+
if (dx * dx + dy * dy < toleranceSq) {
|
|
536
|
+
stats.decimated++;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const nx = ox + coords[(i + 1) * 2] * k;
|
|
540
|
+
const ny = oy + coords[(i + 1) * 2 + 1] * k;
|
|
541
|
+
const ax = nx - keptX;
|
|
542
|
+
const ay = ny - keptY;
|
|
543
|
+
const lenSq = ax * ax + ay * ay;
|
|
544
|
+
if (lenSq > 0) {
|
|
545
|
+
// The cross product is the parallelogram's area, so its square over
|
|
546
|
+
// `lenSq` is the squared perpendicular distance — compared as a
|
|
547
|
+
// product to keep the division out of the loop.
|
|
548
|
+
const cross = ax * dy - ay * dx;
|
|
549
|
+
if (cross * cross < toleranceSq * lenSq) {
|
|
550
|
+
stats.decimated++;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
ctx.lineTo(x, y);
|
|
556
|
+
emitted++;
|
|
557
|
+
keptX = x;
|
|
558
|
+
keptY = y;
|
|
559
|
+
}
|
|
560
|
+
if (closed) ctx.closePath();
|
|
561
|
+
return emitted;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* One part, clipped to the target before it is emitted.
|
|
566
|
+
*
|
|
567
|
+
* A ring goes through Sutherland-Hodgman, because a fill needs a closed
|
|
568
|
+
* boundary — the part that crosses the window has to come back along the
|
|
569
|
+
* window's edge. A line goes through Cohen-Sutherland segment by segment,
|
|
570
|
+
* which turns one polyline into as many subpaths as it has visible
|
|
571
|
+
* stretches; joins at the boundary are lost, and they are off-screen.
|
|
572
|
+
*
|
|
573
|
+
* Simplification is deliberately skipped here: this path only runs for the
|
|
574
|
+
* handful of features that survive the box cull in an overzoomed cell, and
|
|
575
|
+
* at that magnification there is nothing to simplify away.
|
|
576
|
+
*/
|
|
577
|
+
function emitClipped(
|
|
578
|
+
ctx: MapCanvas,
|
|
579
|
+
coords: Int32Array,
|
|
580
|
+
from: number,
|
|
581
|
+
to: number,
|
|
582
|
+
ox: number,
|
|
583
|
+
oy: number,
|
|
584
|
+
k: number,
|
|
585
|
+
closed: boolean,
|
|
586
|
+
clip: ClipRect,
|
|
587
|
+
scratch: number[],
|
|
588
|
+
stats: DrawStats,
|
|
589
|
+
): number {
|
|
590
|
+
const count = to - from;
|
|
591
|
+
if (count < 2) return 0;
|
|
592
|
+
scratch.length = 0;
|
|
593
|
+
for (let i = from; i < to; i++) {
|
|
594
|
+
scratch.push(ox + coords[i * 2] * k, oy + coords[i * 2 + 1] * k);
|
|
595
|
+
}
|
|
596
|
+
if (closed) {
|
|
597
|
+
const ring = clipRing(scratch, clip);
|
|
598
|
+
if (ring.length < 6) return 0;
|
|
599
|
+
ctx.moveTo(ring[0], ring[1]);
|
|
600
|
+
for (let i = 2; i < ring.length; i += 2) ctx.lineTo(ring[i], ring[i + 1]);
|
|
601
|
+
ctx.closePath();
|
|
602
|
+
return ring.length / 2;
|
|
603
|
+
}
|
|
604
|
+
let emitted = 0;
|
|
605
|
+
let open = false;
|
|
606
|
+
for (let i = 0; i + 3 < scratch.length; i += 2) {
|
|
607
|
+
const piece = clipSegment(
|
|
608
|
+
scratch[i],
|
|
609
|
+
scratch[i + 1],
|
|
610
|
+
scratch[i + 2],
|
|
611
|
+
scratch[i + 3],
|
|
612
|
+
clip,
|
|
613
|
+
);
|
|
614
|
+
if (!piece) {
|
|
615
|
+
open = false;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const [x0, y0, x1, y1] = piece;
|
|
619
|
+
if (!open) {
|
|
620
|
+
ctx.moveTo(x0, y0);
|
|
621
|
+
open = true;
|
|
622
|
+
emitted++;
|
|
623
|
+
}
|
|
624
|
+
ctx.lineTo(x1, y1);
|
|
625
|
+
emitted++;
|
|
626
|
+
if (x1 !== scratch[i + 2] || y1 !== scratch[i + 3]) open = false;
|
|
627
|
+
}
|
|
628
|
+
void stats;
|
|
629
|
+
return emitted;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** A draw's clip rectangle in the form the clippers take, inflated so a
|
|
633
|
+
* stroke's own width and its joins land outside the surface rather than
|
|
634
|
+
* against its edge. */
|
|
635
|
+
function clipRectOf(
|
|
636
|
+
clip: { x: number; y: number; width: number; height: number } | null,
|
|
637
|
+
): ClipRect | null {
|
|
638
|
+
if (!clip) return null;
|
|
639
|
+
const margin = 64;
|
|
640
|
+
return {
|
|
641
|
+
minX: clip.x - margin,
|
|
642
|
+
minY: clip.y - margin,
|
|
643
|
+
maxX: clip.x + clip.width + margin,
|
|
644
|
+
maxY: clip.y + clip.height + margin,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Whether a part's box is too small to be worth a path. */
|
|
649
|
+
function partTooSmall(
|
|
650
|
+
bounds: Float64Array,
|
|
651
|
+
part: number,
|
|
652
|
+
k: number,
|
|
653
|
+
minFeature: number,
|
|
654
|
+
): boolean {
|
|
655
|
+
if (minFeature <= 0) return false;
|
|
656
|
+
const at = part * 4;
|
|
657
|
+
return (
|
|
658
|
+
(bounds[at + 2] - bounds[at]) * k < minFeature &&
|
|
659
|
+
(bounds[at + 3] - bounds[at + 1]) * k < minFeature
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Whether a whole feature lands outside the target at all.
|
|
665
|
+
*
|
|
666
|
+
* Only ever true when the target is a *cell* of a larger tile — the
|
|
667
|
+
* overzoom case, where one tile's data is drawn into each of `span²`
|
|
668
|
+
* surfaces. Without this each cell would build a path for every feature in
|
|
669
|
+
* the tile and let the surface clip it, so an overzoomed frame would cost
|
|
670
|
+
* `span²` times what it should; the box came free from the geometry read,
|
|
671
|
+
* so the test is four comparisons.
|
|
672
|
+
*/
|
|
673
|
+
function outsideClip(
|
|
674
|
+
buffer: GeometryBuffer,
|
|
675
|
+
ox: number,
|
|
676
|
+
oy: number,
|
|
677
|
+
k: number,
|
|
678
|
+
clip: { x: number; y: number; width: number; height: number },
|
|
679
|
+
/** Slack for a stroke's own width, in target pixels. */
|
|
680
|
+
slack: number,
|
|
681
|
+
): boolean {
|
|
682
|
+
return (
|
|
683
|
+
ox + buffer.maxX * k < clip.x - slack ||
|
|
684
|
+
ox + buffer.minX * k > clip.x + clip.width + slack ||
|
|
685
|
+
oy + buffer.maxY * k < clip.y - slack ||
|
|
686
|
+
oy + buffer.minY * k > clip.y + clip.height + slack
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Iterate a bucket, or the whole layer applying the filter. Written as a
|
|
691
|
+
* pair of loops in each drawer rather than a shared generator: a generator
|
|
692
|
+
* per feature is exactly the allocation this file exists to avoid. */
|
|
693
|
+
function featureCount(
|
|
694
|
+
source: VectorTileLayer,
|
|
695
|
+
indices: readonly number[] | null,
|
|
696
|
+
): number {
|
|
697
|
+
return indices ? indices.length : source.length;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function drawFill(
|
|
701
|
+
ctx: MapCanvas,
|
|
702
|
+
source: VectorTileLayer,
|
|
703
|
+
layer: Extract<MapStyleLayer, { type: 'fill' }>,
|
|
704
|
+
filter: CompiledFilter,
|
|
705
|
+
draw: TileDraw,
|
|
706
|
+
scratch: DrawScratch,
|
|
707
|
+
indices: readonly number[] | null,
|
|
708
|
+
): void {
|
|
709
|
+
const stats = scratch.stats;
|
|
710
|
+
const buffer = scratch.buffer;
|
|
711
|
+
const k = draw.span / source.extent;
|
|
712
|
+
const opacity = num(layer.opacity, draw.zoom, 1);
|
|
713
|
+
if (opacity <= 0) return;
|
|
714
|
+
const outline = str(layer.outlineColor, draw.zoom);
|
|
715
|
+
const tol = (draw.tolerance ?? 0) * (draw.tolerance ?? 0);
|
|
716
|
+
const minFeature = draw.minFeature ?? 0;
|
|
717
|
+
const clip = draw.clip ?? null;
|
|
718
|
+
const clipBox = clipRectOf(clip);
|
|
719
|
+
const cursor = source.feature(0);
|
|
720
|
+
const total = featureCount(source, indices);
|
|
721
|
+
const batch = draw.batchVertices ?? BATCH_VERTICES;
|
|
722
|
+
|
|
723
|
+
ctx.save();
|
|
724
|
+
if (ctx.globalAlpha !== undefined && opacity < 1) ctx.globalAlpha = opacity;
|
|
725
|
+
ctx.fillStyle = resolveZoomed(layer.color, draw.zoom);
|
|
726
|
+
if (outline !== undefined) {
|
|
727
|
+
ctx.strokeStyle = outline;
|
|
728
|
+
ctx.lineWidth = Math.max(1, draw.pixelsPerLogical);
|
|
729
|
+
}
|
|
730
|
+
let pending = 0;
|
|
731
|
+
ctx.beginPath();
|
|
732
|
+
for (let n = 0; n < total; n++) {
|
|
733
|
+
source.seek(indices ? indices[n] : n, cursor);
|
|
734
|
+
if (!indices) {
|
|
735
|
+
if (cursor.type !== GeomType.Polygon) continue;
|
|
736
|
+
if (!filter(cursor)) continue;
|
|
737
|
+
}
|
|
738
|
+
cursor.readGeometry(buffer);
|
|
739
|
+
if (buffer.parts === 0) continue;
|
|
740
|
+
if (clip && outsideClip(buffer, draw.ox, draw.oy, k, clip, 2)) {
|
|
741
|
+
stats.culled++;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
stats.features++;
|
|
745
|
+
for (let part = 0; part < buffer.parts; part++) {
|
|
746
|
+
// Rings go into one path and are filled with the non-zero rule, which
|
|
747
|
+
// is what makes a hole a hole: MVT winds an exterior ring one way and
|
|
748
|
+
// an interior ring the other, so the rule does the work and nothing
|
|
749
|
+
// here pairs them up.
|
|
750
|
+
//
|
|
751
|
+
// Which is exactly why the batch is flushed **only before an exterior
|
|
752
|
+
// ring**. A positive area is a new polygon (the specification's own
|
|
753
|
+
// definition), so that is the one point at which the path so far is a
|
|
754
|
+
// whole number of polygons; flushing between an exterior ring and its
|
|
755
|
+
// interior ones would fill the hole in. This is what `areas` is for.
|
|
756
|
+
const exterior = buffer.areas[part] > 0;
|
|
757
|
+
if (exterior && pending >= batch) {
|
|
758
|
+
ctx.fill();
|
|
759
|
+
if (outline !== undefined) ctx.stroke();
|
|
760
|
+
stats.batches += outline === undefined ? 1 : 2;
|
|
761
|
+
stats.vertices += pending;
|
|
762
|
+
pending = 0;
|
|
763
|
+
ctx.beginPath();
|
|
764
|
+
}
|
|
765
|
+
if (partTooSmall(buffer.partBounds, part, k, minFeature)) {
|
|
766
|
+
stats.culled++;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
pending += emitPart(
|
|
770
|
+
ctx,
|
|
771
|
+
buffer.coords,
|
|
772
|
+
buffer.starts[part],
|
|
773
|
+
buffer.starts[part + 1],
|
|
774
|
+
draw.ox,
|
|
775
|
+
draw.oy,
|
|
776
|
+
k,
|
|
777
|
+
tol,
|
|
778
|
+
true,
|
|
779
|
+
stats,
|
|
780
|
+
clipBox,
|
|
781
|
+
scratch.ring,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (pending > 0) {
|
|
786
|
+
ctx.fill();
|
|
787
|
+
if (outline !== undefined) ctx.stroke();
|
|
788
|
+
stats.batches += outline === undefined ? 1 : 2;
|
|
789
|
+
stats.vertices += pending;
|
|
790
|
+
}
|
|
791
|
+
ctx.restore();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function drawLine(
|
|
795
|
+
ctx: MapCanvas,
|
|
796
|
+
source: VectorTileLayer,
|
|
797
|
+
layer: Extract<MapStyleLayer, { type: 'line' }>,
|
|
798
|
+
filter: CompiledFilter,
|
|
799
|
+
draw: TileDraw,
|
|
800
|
+
scratch: DrawScratch,
|
|
801
|
+
indices: readonly number[] | null,
|
|
802
|
+
): void {
|
|
803
|
+
const stats = scratch.stats;
|
|
804
|
+
const buffer = scratch.buffer;
|
|
805
|
+
const k = draw.span / source.extent;
|
|
806
|
+
const logical = num(layer.width, draw.zoom, 1);
|
|
807
|
+
if (logical <= 0) return;
|
|
808
|
+
const opacity = num(layer.opacity, draw.zoom, 1);
|
|
809
|
+
if (opacity <= 0) return;
|
|
810
|
+
// A road narrower than a pixel is drawn *at* a pixel rather than dropped:
|
|
811
|
+
// a motorway network that vanishes below zoom 6 is worse than one drawn a
|
|
812
|
+
// little heavy, and every map makes this same choice.
|
|
813
|
+
const width = Math.max(1, logical * draw.pixelsPerLogical);
|
|
814
|
+
const tol = (draw.tolerance ?? 0) * (draw.tolerance ?? 0);
|
|
815
|
+
const minFeature = draw.minFeature ?? 0;
|
|
816
|
+
const clip = draw.clip ?? null;
|
|
817
|
+
const clipBox = clipRectOf(clip);
|
|
818
|
+
const cursor = source.feature(0);
|
|
819
|
+
const total = featureCount(source, indices);
|
|
820
|
+
const batch = draw.batchVertices ?? BATCH_VERTICES;
|
|
821
|
+
|
|
822
|
+
ctx.save();
|
|
823
|
+
if (ctx.globalAlpha !== undefined && opacity < 1) ctx.globalAlpha = opacity;
|
|
824
|
+
ctx.strokeStyle = resolveZoomed(layer.color, draw.zoom);
|
|
825
|
+
ctx.lineWidth = width;
|
|
826
|
+
if (layer.cap !== undefined) ctx.lineCap = layer.cap;
|
|
827
|
+
if (layer.join !== undefined) ctx.lineJoin = layer.join;
|
|
828
|
+
if (layer.dash && ctx.setLineDash) {
|
|
829
|
+
ctx.setLineDash(layer.dash.map((d) => d * draw.pixelsPerLogical));
|
|
830
|
+
}
|
|
831
|
+
let pending = 0;
|
|
832
|
+
ctx.beginPath();
|
|
833
|
+
for (let n = 0; n < total; n++) {
|
|
834
|
+
source.seek(indices ? indices[n] : n, cursor);
|
|
835
|
+
if (!indices) {
|
|
836
|
+
if (cursor.type === GeomType.Point) continue;
|
|
837
|
+
if (!filter(cursor)) continue;
|
|
838
|
+
}
|
|
839
|
+
cursor.readGeometry(buffer);
|
|
840
|
+
if (buffer.parts === 0) continue;
|
|
841
|
+
if (clip && outsideClip(buffer, draw.ox, draw.oy, k, clip, width)) {
|
|
842
|
+
stats.culled++;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
stats.features++;
|
|
846
|
+
const closed = cursor.type === GeomType.Polygon;
|
|
847
|
+
for (let part = 0; part < buffer.parts; part++) {
|
|
848
|
+
// Any part boundary is a safe flush point for a stroke: a subpath is
|
|
849
|
+
// stroked independently of every other, so where the path is cut
|
|
850
|
+
// changes nothing about the result. Inside the loop rather than after
|
|
851
|
+
// the feature because the feature that needs it most is a single one
|
|
852
|
+
// — a `land` multipolygon at zoom 8, a `buildings` multipolygon at
|
|
853
|
+
// zoom 14 — with tens of thousands of vertices in it.
|
|
854
|
+
if (pending >= batch) {
|
|
855
|
+
ctx.stroke();
|
|
856
|
+
stats.batches++;
|
|
857
|
+
stats.vertices += pending;
|
|
858
|
+
pending = 0;
|
|
859
|
+
ctx.beginPath();
|
|
860
|
+
}
|
|
861
|
+
if (partTooSmall(buffer.partBounds, part, k, minFeature)) {
|
|
862
|
+
stats.culled++;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
pending += emitPart(
|
|
866
|
+
ctx,
|
|
867
|
+
buffer.coords,
|
|
868
|
+
buffer.starts[part],
|
|
869
|
+
buffer.starts[part + 1],
|
|
870
|
+
draw.ox,
|
|
871
|
+
draw.oy,
|
|
872
|
+
k,
|
|
873
|
+
tol,
|
|
874
|
+
closed,
|
|
875
|
+
stats,
|
|
876
|
+
clipBox,
|
|
877
|
+
scratch.ring,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (pending > 0) {
|
|
882
|
+
ctx.stroke();
|
|
883
|
+
stats.batches++;
|
|
884
|
+
stats.vertices += pending;
|
|
885
|
+
}
|
|
886
|
+
if (layer.dash && ctx.setLineDash) ctx.setLineDash([]);
|
|
887
|
+
ctx.restore();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function drawCircle(
|
|
891
|
+
ctx: MapCanvas,
|
|
892
|
+
source: VectorTileLayer,
|
|
893
|
+
layer: Extract<MapStyleLayer, { type: 'circle' }>,
|
|
894
|
+
filter: CompiledFilter,
|
|
895
|
+
draw: TileDraw,
|
|
896
|
+
scratch: DrawScratch,
|
|
897
|
+
indices: readonly number[] | null,
|
|
898
|
+
): void {
|
|
899
|
+
const stats = scratch.stats;
|
|
900
|
+
const buffer = scratch.buffer;
|
|
901
|
+
const k = draw.span / source.extent;
|
|
902
|
+
const radius = num(layer.radius, draw.zoom, 3) * draw.pixelsPerLogical;
|
|
903
|
+
if (radius <= 0) return;
|
|
904
|
+
const opacity = num(layer.opacity, draw.zoom, 1);
|
|
905
|
+
if (opacity <= 0) return;
|
|
906
|
+
const stroke = str(layer.strokeColor, draw.zoom);
|
|
907
|
+
const strokeWidth =
|
|
908
|
+
num(layer.strokeWidth, draw.zoom, 1) * draw.pixelsPerLogical;
|
|
909
|
+
const clip = draw.clip ?? null;
|
|
910
|
+
const cursor = source.feature(0);
|
|
911
|
+
const total = featureCount(source, indices);
|
|
912
|
+
const discBatch = Math.min(1024, draw.batchVertices ?? BATCH_VERTICES);
|
|
913
|
+
|
|
914
|
+
ctx.save();
|
|
915
|
+
if (ctx.globalAlpha !== undefined && opacity < 1) ctx.globalAlpha = opacity;
|
|
916
|
+
ctx.fillStyle = resolveZoomed(layer.color, draw.zoom);
|
|
917
|
+
if (stroke !== undefined) {
|
|
918
|
+
ctx.strokeStyle = stroke;
|
|
919
|
+
ctx.lineWidth = strokeWidth;
|
|
920
|
+
}
|
|
921
|
+
let pending = 0;
|
|
922
|
+
ctx.beginPath();
|
|
923
|
+
for (let n = 0; n < total; n++) {
|
|
924
|
+
source.seek(indices ? indices[n] : n, cursor);
|
|
925
|
+
if (!indices) {
|
|
926
|
+
if (cursor.type !== GeomType.Point) continue;
|
|
927
|
+
if (!filter(cursor)) continue;
|
|
928
|
+
}
|
|
929
|
+
cursor.readGeometry(buffer);
|
|
930
|
+
if (
|
|
931
|
+
buffer.points > 0 &&
|
|
932
|
+
clip &&
|
|
933
|
+
outsideClip(buffer, draw.ox, draw.oy, k, clip, radius)
|
|
934
|
+
) {
|
|
935
|
+
stats.culled++;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
stats.features++;
|
|
939
|
+
for (let p = 0; p < buffer.points; p++) {
|
|
940
|
+
const x = draw.ox + buffer.coords[p * 2] * k;
|
|
941
|
+
const y = draw.oy + buffer.coords[p * 2 + 1] * k;
|
|
942
|
+
// A disc is a point, so there is nothing to clip — but a multipoint
|
|
943
|
+
// feature can straddle the target, and only the whole feature was
|
|
944
|
+
// culled above. Skip the ones that land outside it.
|
|
945
|
+
if (
|
|
946
|
+
clip &&
|
|
947
|
+
(x + radius < clip.x ||
|
|
948
|
+
x - radius > clip.x + clip.width ||
|
|
949
|
+
y + radius < clip.y ||
|
|
950
|
+
y - radius > clip.y + clip.height)
|
|
951
|
+
) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
// A fresh subpath per disc: an `arc` continuing from the previous
|
|
955
|
+
// point would join the two with a line.
|
|
956
|
+
ctx.moveTo(x + radius, y);
|
|
957
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
958
|
+
pending++;
|
|
959
|
+
}
|
|
960
|
+
// Discs, not vertices — each is `moveTo` plus an `arc` — so the cap is
|
|
961
|
+
// its own, bounded by the backend's batch preference.
|
|
962
|
+
if (pending >= discBatch) {
|
|
963
|
+
ctx.fill();
|
|
964
|
+
if (stroke !== undefined) ctx.stroke();
|
|
965
|
+
stats.batches += stroke === undefined ? 1 : 2;
|
|
966
|
+
stats.vertices += pending;
|
|
967
|
+
pending = 0;
|
|
968
|
+
ctx.beginPath();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (pending > 0) {
|
|
972
|
+
ctx.fill();
|
|
973
|
+
if (stroke !== undefined) ctx.stroke();
|
|
974
|
+
stats.batches += stroke === undefined ? 1 : 2;
|
|
975
|
+
stats.vertices += pending;
|
|
976
|
+
}
|
|
977
|
+
ctx.restore();
|
|
978
|
+
}
|