@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.
Files changed (248) hide show
  1. package/README.md +143 -8
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +8 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +21 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/markdown/ast.d.ts +80 -6
  140. package/dist/markdown/ast.d.ts.map +1 -1
  141. package/dist/markdown/ast.js +13 -4
  142. package/dist/markdown/ast.js.map +1 -1
  143. package/dist/markdown/expressions.d.ts +27 -0
  144. package/dist/markdown/expressions.d.ts.map +1 -0
  145. package/dist/markdown/expressions.js +203 -0
  146. package/dist/markdown/expressions.js.map +1 -0
  147. package/dist/markdown/index.d.ts +61 -2
  148. package/dist/markdown/index.d.ts.map +1 -1
  149. package/dist/markdown/index.js +101 -2
  150. package/dist/markdown/index.js.map +1 -1
  151. package/dist/markdown/parse.d.ts +1 -1
  152. package/dist/markdown/parse.d.ts.map +1 -1
  153. package/dist/markdown/parse.js +163 -15
  154. package/dist/markdown/parse.js.map +1 -1
  155. package/dist/markdown/spans.d.ts.map +1 -1
  156. package/dist/markdown/spans.js +4 -0
  157. package/dist/markdown/spans.js.map +1 -1
  158. package/dist/markdown/tags.d.ts +35 -0
  159. package/dist/markdown/tags.d.ts.map +1 -0
  160. package/dist/markdown/tags.js +213 -0
  161. package/dist/markdown/tags.js.map +1 -0
  162. package/dist/reorder/hx.d.ts +18 -0
  163. package/dist/reorder/hx.d.ts.map +1 -0
  164. package/dist/reorder/hx.js +10 -0
  165. package/dist/reorder/hx.js.map +1 -0
  166. package/dist/reorder/index.d.ts +314 -0
  167. package/dist/reorder/index.d.ts.map +1 -0
  168. package/dist/reorder/index.js +1539 -0
  169. package/dist/reorder/index.js.map +1 -0
  170. package/dist/reorder/model.d.ts +107 -0
  171. package/dist/reorder/model.d.ts.map +1 -0
  172. package/dist/reorder/model.js +162 -0
  173. package/dist/reorder/model.js.map +1 -0
  174. package/dist/richtext/node.d.ts +15 -5
  175. package/dist/richtext/node.d.ts.map +1 -1
  176. package/dist/richtext/node.js +40 -11
  177. package/dist/richtext/node.js.map +1 -1
  178. package/dist/richtext/runs.d.ts +35 -13
  179. package/dist/richtext/runs.d.ts.map +1 -1
  180. package/dist/richtext/runs.js +83 -27
  181. package/dist/richtext/runs.js.map +1 -1
  182. package/dist/tabs/index.d.ts +17 -0
  183. package/dist/tabs/index.d.ts.map +1 -1
  184. package/dist/tabs/index.js +849 -186
  185. package/dist/tabs/index.js.map +1 -1
  186. package/dist/terminal/vt/fonts.d.ts +45 -15
  187. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  188. package/dist/terminal/vt/fonts.js +89 -19
  189. package/dist/terminal/vt/fonts.js.map +1 -1
  190. package/dist/terminal/vt/node.d.ts +42 -0
  191. package/dist/terminal/vt/node.d.ts.map +1 -1
  192. package/dist/terminal/vt/node.js +95 -11
  193. package/dist/terminal/vt/node.js.map +1 -1
  194. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  195. package/dist/terminal/vt/renderer.js +13 -3
  196. package/dist/terminal/vt/renderer.js.map +1 -1
  197. package/package.json +14 -3
  198. package/src/charts/components.ts +14 -7
  199. package/src/charts/node.ts +81 -26
  200. package/src/charts/render.ts +16 -6
  201. package/src/code-editor/node.ts +147 -53
  202. package/src/flow/draw.ts +104 -35
  203. package/src/flow/index.ts +33 -13
  204. package/src/flow/model.ts +4 -2
  205. package/src/flow/node.ts +158 -59
  206. package/src/flow/types.ts +46 -21
  207. package/src/formula/index.ts +4 -2
  208. package/src/formula/node.ts +24 -5
  209. package/src/html/controls.ts +4 -1
  210. package/src/html/css/cascade.ts +18 -7
  211. package/src/html/css/parse.ts +46 -21
  212. package/src/html/css/style.ts +6 -1
  213. package/src/html/css/values.ts +25 -9
  214. package/src/html/index.ts +1 -0
  215. package/src/html/layout/boxes.ts +19 -9
  216. package/src/html/layout/inline.ts +23 -1
  217. package/src/html/node.ts +110 -17
  218. package/src/html/paint.ts +17 -2
  219. package/src/index.ts +102 -0
  220. package/src/maps/clip.ts +184 -0
  221. package/src/maps/gzip.ts +66 -0
  222. package/src/maps/index.ts +316 -0
  223. package/src/maps/labels.ts +442 -0
  224. package/src/maps/mvt.ts +857 -0
  225. package/src/maps/node.ts +1819 -0
  226. package/src/maps/overlay.ts +639 -0
  227. package/src/maps/paint.ts +978 -0
  228. package/src/maps/proj.ts +641 -0
  229. package/src/maps/sources.ts +425 -0
  230. package/src/maps/style.ts +309 -0
  231. package/src/maps/styles.ts +1040 -0
  232. package/src/maps/tiles.ts +676 -0
  233. package/src/maps/types.ts +260 -0
  234. package/src/markdown/ast.ts +85 -9
  235. package/src/markdown/expressions.ts +223 -0
  236. package/src/markdown/index.ts +190 -5
  237. package/src/markdown/parse.ts +230 -15
  238. package/src/markdown/spans.ts +4 -0
  239. package/src/markdown/tags.ts +240 -0
  240. package/src/reorder/hx.ts +31 -0
  241. package/src/reorder/index.ts +2223 -0
  242. package/src/reorder/model.ts +232 -0
  243. package/src/richtext/node.ts +49 -14
  244. package/src/richtext/runs.ts +115 -30
  245. package/src/tabs/index.ts +1040 -204
  246. package/src/terminal/vt/fonts.ts +128 -36
  247. package/src/terminal/vt/node.ts +105 -12
  248. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,639 @@
1
+ // What the application puts on the map: markers, and the lines, areas and
2
+ // circles that carry a route, a traffic segment, a transit shape or a
3
+ // GeoJSON layer.
4
+ //
5
+ // All of it is drawn **into the frame**, not into a tile surface, for the
6
+ // reason the labels are: it is application state that changes on its own
7
+ // clock, and baking it into a tile would mean re-rasterizing a tile every
8
+ // time a vehicle moved. It rides the pan blit like everything else on the
9
+ // map, because it is anchored to geography.
10
+ //
11
+ // The vocabulary is deliberately small and deliberately geometric — a line
12
+ // is a line whether it came from a routing engine, a GTFS shape or a
13
+ // traffic feed — with the format adapters ({@link decodePolyline},
14
+ // {@link geoJsonOverlays}) kept as pure functions beside it rather than as
15
+ // props of the map. `docs/prd-maps.md` surveys what the real feeds look
16
+ // like and why this is the shape they all reduce to.
17
+ import { clipOf, clipRing, clipSegment } from './clip.js';
18
+ import type { ClipRect } from './clip.js';
19
+ import type { MapCanvas } from './paint.js';
20
+ import { projectLngLat } from './proj.js';
21
+ import type { LngLat, Transform } from './proj.js';
22
+
23
+ /** A point on the map the user can click. */
24
+ export interface MapMarker {
25
+ /** Stable across renders — what an event names and what a hit test
26
+ * returns. */
27
+ id: string;
28
+ position: LngLat;
29
+ /** `'pin'` (the default) stands on its position; `'circle'` is centred on
30
+ * it. A pin is what a place wants and a circle is what a vehicle or a
31
+ * measurement wants. */
32
+ shape?: 'pin' | 'circle';
33
+ /** Logical pixels: a pin's width, a circle's diameter. 14 by default. */
34
+ size?: number;
35
+ /** Fill. The theme's accent by default. */
36
+ color?: string;
37
+ /** The ring around it, so a marker stays visible on any basemap. The
38
+ * theme's background by default. */
39
+ outline?: string;
40
+ /** Drawn above unselected markers, and given the selected ring. */
41
+ selected?: boolean;
42
+ /** Higher draws later. Ties break on array order. */
43
+ zIndex?: number;
44
+ /** Skipped by hit testing — for a marker that is decoration. */
45
+ interactive?: boolean;
46
+ /** Announced by a screen reader, and shown by an application's own
47
+ * tooltip. */
48
+ title?: string;
49
+ /** Handed back on an event. Never read here. */
50
+ data?: unknown;
51
+ }
52
+
53
+ /** Anything else drawn over the basemap. */
54
+ export type MapOverlay =
55
+ | {
56
+ kind: 'line';
57
+ id: string;
58
+ path: readonly LngLat[];
59
+ color?: string;
60
+ /** Logical pixels. 3 by default. */
61
+ width?: number;
62
+ opacity?: number;
63
+ dash?: readonly number[];
64
+ cap?: 'butt' | 'round' | 'square';
65
+ join?: 'miter' | 'round' | 'bevel';
66
+ /** A second, wider stroke under the first — what makes a route
67
+ * readable over a road of the same colour. */
68
+ casing?: string;
69
+ casingWidth?: number;
70
+ zIndex?: number;
71
+ data?: unknown;
72
+ }
73
+ | {
74
+ kind: 'polygon';
75
+ id: string;
76
+ /** Exterior ring first; every ring after it is a hole. */
77
+ rings: readonly (readonly LngLat[])[];
78
+ fill?: string;
79
+ opacity?: number;
80
+ outline?: string;
81
+ outlineWidth?: number;
82
+ zIndex?: number;
83
+ data?: unknown;
84
+ }
85
+ | {
86
+ kind: 'circle';
87
+ id: string;
88
+ center: LngLat;
89
+ /** Ground metres, so the circle grows with the zoom the way a real
90
+ * radius does — an accuracy ring, a catchment, a geofence. */
91
+ radiusMetres: number;
92
+ fill?: string;
93
+ opacity?: number;
94
+ outline?: string;
95
+ outlineWidth?: number;
96
+ zIndex?: number;
97
+ data?: unknown;
98
+ };
99
+
100
+ /** Colours an overlay falls back to, from the theme. */
101
+ export interface OverlayPalette {
102
+ accent: string;
103
+ background: string;
104
+ text: string;
105
+ }
106
+
107
+ function byZ<T extends { zIndex?: number }>(items: readonly T[]): T[] {
108
+ // A stable sort — the language guarantees it — so items with no `zIndex`
109
+ // keep the order the application listed them in.
110
+ return [...items].sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
111
+ }
112
+
113
+ /**
114
+ * Draw the overlays.
115
+ *
116
+ * Under the markers, because a marker is a thing the user aims at and an
117
+ * overlay is context for it.
118
+ */
119
+ export function drawOverlays(
120
+ ctx: MapCanvas,
121
+ overlays: readonly MapOverlay[],
122
+ transform: Transform,
123
+ pane: { x: number; y: number; width: number; height: number },
124
+ scale: number,
125
+ palette: OverlayPalette,
126
+ ): void {
127
+ const clip = clipOf(pane, scale);
128
+ for (const overlay of byZ(overlays)) {
129
+ const opacity = overlay.opacity ?? 1;
130
+ if (opacity <= 0) continue;
131
+ ctx.save();
132
+ if (ctx.globalAlpha !== undefined && opacity < 1) ctx.globalAlpha = opacity;
133
+ if (overlay.kind === 'line') {
134
+ const points = projectPath(overlay.path, transform, pane, scale);
135
+ const width = (overlay.width ?? 3) * scale;
136
+ if (overlay.casing !== undefined) {
137
+ ctx.strokeStyle = overlay.casing;
138
+ ctx.lineWidth = Math.max(
139
+ width + 2 * scale,
140
+ (overlay.casingWidth ?? 0) * scale,
141
+ );
142
+ ctx.lineCap = overlay.cap ?? 'round';
143
+ ctx.lineJoin = overlay.join ?? 'round';
144
+ strokeClipped(ctx, points, clip);
145
+ }
146
+ ctx.strokeStyle = overlay.color ?? palette.accent;
147
+ ctx.lineWidth = width;
148
+ ctx.lineCap = overlay.cap ?? 'round';
149
+ ctx.lineJoin = overlay.join ?? 'round';
150
+ if (overlay.dash && ctx.setLineDash) {
151
+ ctx.setLineDash(overlay.dash.map((d) => d * scale));
152
+ }
153
+ strokeClipped(ctx, points, clip);
154
+ if (overlay.dash && ctx.setLineDash) ctx.setLineDash([]);
155
+ } else if (overlay.kind === 'polygon') {
156
+ ctx.beginPath();
157
+ let any = false;
158
+ for (const ring of overlay.rings) {
159
+ const clipped = clipRing(
160
+ projectPath(ring, transform, pane, scale),
161
+ clip,
162
+ );
163
+ if (appendRing(ctx, clipped)) any = true;
164
+ }
165
+ if (any) {
166
+ ctx.fillStyle = overlay.fill ?? palette.accent;
167
+ ctx.fill();
168
+ if (overlay.outline !== undefined) {
169
+ ctx.strokeStyle = overlay.outline;
170
+ ctx.lineWidth = (overlay.outlineWidth ?? 1) * scale;
171
+ ctx.stroke();
172
+ }
173
+ }
174
+ } else {
175
+ // A circle in ground metres is an ellipse on a Mercator map, and at
176
+ // the sizes an application draws one it is close enough to a circle
177
+ // that the difference is under a pixel — except near the poles, where
178
+ // it is not. So the radius is computed from the *projection* rather
179
+ // than from a metres-per-pixel constant: one degree of latitude
180
+ // either side of the centre, projected, is exactly the scale factor
181
+ // this circle should be drawn at.
182
+ const centre = projectLngLat(transform, overlay.center);
183
+ const north = projectLngLat(transform, {
184
+ lon: overlay.center.lon,
185
+ lat: Math.min(85, overlay.center.lat + 0.01),
186
+ });
187
+ const metresPerDegree = 111_319.9;
188
+ const pixelsPerMetre =
189
+ Math.abs(centre.y - north.y) / (0.01 * metresPerDegree);
190
+ const radius = overlay.radiusMetres * pixelsPerMetre * scale;
191
+ const cx = (pane.x + centre.x) * scale;
192
+ const cy = (pane.y + centre.y) * scale;
193
+ const outside =
194
+ cx + radius < clip.minX ||
195
+ cx - radius > clip.maxX ||
196
+ cy + radius < clip.minY ||
197
+ cy - radius > clip.maxY;
198
+ if (radius > 0.5 && !outside) {
199
+ ctx.fillStyle = overlay.fill ?? palette.accent;
200
+ if (radius <= MAX_ARC_RADIUS) {
201
+ ctx.beginPath();
202
+ ctx.moveTo(cx + radius, cy);
203
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
204
+ ctx.fill();
205
+ if (overlay.outline !== undefined) {
206
+ ctx.strokeStyle = overlay.outline;
207
+ ctx.lineWidth = (overlay.outlineWidth ?? 1) * scale;
208
+ ctx.stroke();
209
+ }
210
+ } else {
211
+ // Past the arc's safe range the circle becomes a ring and is
212
+ // clipped like any other polygon. The segment count keeps the
213
+ // sagitta under half a pixel, and for a circle this large the
214
+ // visible arc is very nearly straight anyway.
215
+ ctx.beginPath();
216
+ if (appendRing(ctx, clipRing(circleRing(cx, cy, radius), clip))) {
217
+ ctx.fill();
218
+ if (overlay.outline !== undefined) {
219
+ ctx.strokeStyle = overlay.outline;
220
+ ctx.lineWidth = (overlay.outlineWidth ?? 1) * scale;
221
+ ctx.stroke();
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ ctx.restore();
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Everything below this line exists because **an overlay's coordinates are
233
+ * unbounded and the renderer's are not.**
234
+ *
235
+ * A route is geography, so its far end stays where it is when the camera
236
+ * zooms in on one corner of it. World pixels are `512 · 2^zoom`, which at
237
+ * zoom 20 is 134 million, so a vertex a fraction of a degree outside the
238
+ * pane is already tens of thousands of pixels away — and ntk hands a
239
+ * stroke's geometry to XRender as 16.16 fixed point, which overflows a
240
+ * signed 32-bit word at 32,768. The symptom is a `RangeError` out of
241
+ * `x11/lib/ext/render.js` a few zoom steps in, from inside `paint`, which
242
+ * is not a place an application can catch it.
243
+ *
244
+ * So geometry is clipped to the viewport before it reaches the context.
245
+ * That is the fix; the fact that it also stops the renderer rasterizing
246
+ * megametres of off-screen line is a bonus rather than the reason.
247
+ */
248
+
249
+ /** A path projected into target pixels, as a flat `[x0, y0, x1, y1, …]`. */
250
+ function projectPath(
251
+ path: readonly LngLat[],
252
+ transform: Transform,
253
+ pane: { x: number; y: number },
254
+ scale: number,
255
+ ): number[] {
256
+ const out: number[] = [];
257
+ for (const position of path) {
258
+ const point = projectLngLat(transform, position);
259
+ out.push((pane.x + point.x) * scale, (pane.y + point.y) * scale);
260
+ }
261
+ return out;
262
+ }
263
+
264
+ /**
265
+ * Stroke a projected path, clipped.
266
+ *
267
+ * A polyline that leaves and re-enters the window becomes several subpaths,
268
+ * which is why this cannot be a `ctx.clip()` and a single path: the clip
269
+ * would keep the coordinates, and the coordinates are the problem.
270
+ */
271
+ function strokeClipped(
272
+ ctx: MapCanvas,
273
+ points: readonly number[],
274
+ clip: ClipRect,
275
+ ): void {
276
+ if (points.length < 4) return;
277
+ ctx.beginPath();
278
+ let open = false;
279
+ let drew = false;
280
+ for (let i = 0; i + 3 < points.length; i += 2) {
281
+ const piece = clipSegment(
282
+ points[i],
283
+ points[i + 1],
284
+ points[i + 2],
285
+ points[i + 3],
286
+ clip,
287
+ );
288
+ if (!piece) {
289
+ open = false;
290
+ continue;
291
+ }
292
+ const [x0, y0, x1, y1] = piece;
293
+ // A new subpath unless this segment continues exactly where the last
294
+ // one ended — which is the whole-segment-visible case, and the one that
295
+ // has to keep its joins.
296
+ if (!open) {
297
+ ctx.moveTo(x0, y0);
298
+ open = true;
299
+ }
300
+ ctx.lineTo(x1, y1);
301
+ drew = true;
302
+ // The segment was cut short at the far end, so the next one does not
303
+ // continue from here.
304
+ if (x1 !== points[i + 2] || y1 !== points[i + 3]) open = false;
305
+ }
306
+ if (drew) ctx.stroke();
307
+ }
308
+
309
+ function appendRing(ctx: MapCanvas, ring: readonly number[]): boolean {
310
+ if (ring.length < 6) return false;
311
+ ctx.moveTo(ring[0], ring[1]);
312
+ for (let i = 2; i < ring.length; i += 2) ctx.lineTo(ring[i], ring[i + 1]);
313
+ ctx.closePath();
314
+ return true;
315
+ }
316
+
317
+ /** Where a marker's ink lands, in pane-local logical pixels. Shared by the
318
+ * drawing and the hit test, so the two cannot disagree — the bug that
319
+ * makes a marker unclickable a few pixels from where it looks. */
320
+ export function markerRect(
321
+ marker: MapMarker,
322
+ transform: Transform,
323
+ ): {
324
+ x: number;
325
+ y: number;
326
+ width: number;
327
+ height: number;
328
+ tipX: number;
329
+ tipY: number;
330
+ } {
331
+ const point = projectLngLat(transform, marker.position);
332
+ const size = marker.size ?? 14;
333
+ if ((marker.shape ?? 'pin') === 'circle') {
334
+ return {
335
+ x: point.x - size / 2,
336
+ y: point.y - size / 2,
337
+ width: size,
338
+ height: size,
339
+ tipX: point.x,
340
+ tipY: point.y,
341
+ };
342
+ }
343
+ // A pin *stands on* its position: the point is the tip, and the head is
344
+ // above it. Getting this the other way round puts every marker half its
345
+ // own height north of where it belongs, which on a city map is a street.
346
+ const height = size * 1.4;
347
+ return {
348
+ x: point.x - size / 2,
349
+ y: point.y - height,
350
+ width: size,
351
+ height,
352
+ tipX: point.x,
353
+ tipY: point.y,
354
+ };
355
+ }
356
+
357
+ /** Draw the markers, lowest `zIndex` first. */
358
+ export function drawMarkers(
359
+ ctx: MapCanvas,
360
+ markers: readonly MapMarker[],
361
+ transform: Transform,
362
+ pane: { x: number; y: number; width: number; height: number },
363
+ scale: number,
364
+ palette: OverlayPalette,
365
+ ): number {
366
+ let drawn = 0;
367
+ for (const marker of byZ(markers)) {
368
+ const rect = markerRect(marker, transform);
369
+ if (
370
+ rect.x + rect.width < 0 ||
371
+ rect.y + rect.height < 0 ||
372
+ rect.x > pane.width ||
373
+ rect.y > pane.height
374
+ ) {
375
+ continue;
376
+ }
377
+ const size = marker.size ?? 14;
378
+ const fill = marker.color ?? palette.accent;
379
+ const ring = marker.outline ?? palette.background;
380
+ const x = (pane.x + rect.tipX) * scale;
381
+ const y = (pane.y + rect.tipY) * scale;
382
+ const r = (size / 2) * scale;
383
+ ctx.save();
384
+ ctx.lineWidth = Math.max(1, (marker.selected ? 2.5 : 1.5) * scale);
385
+ ctx.strokeStyle = marker.selected ? palette.text : ring;
386
+ ctx.fillStyle = fill;
387
+ if ((marker.shape ?? 'pin') === 'circle') {
388
+ ctx.beginPath();
389
+ ctx.moveTo(x + r, y);
390
+ ctx.arc(x, y, r, 0, Math.PI * 2);
391
+ ctx.fill();
392
+ ctx.stroke();
393
+ } else {
394
+ // A teardrop: a circle for the head and two lines down to the tip.
395
+ // The tangent angle is where the straight sides meet the circle, so
396
+ // the outline is smooth rather than two lines crossing an arc.
397
+ const cy = y - size * 1.4 * scale + r;
398
+ const d = y - cy;
399
+ const angle = Math.asin(Math.min(1, r / d));
400
+ ctx.beginPath();
401
+ ctx.arc(x, cy, r, Math.PI / 2 + angle, Math.PI / 2 - angle);
402
+ ctx.lineTo(x, y);
403
+ ctx.closePath();
404
+ ctx.fill();
405
+ ctx.stroke();
406
+ }
407
+ ctx.restore();
408
+ drawn++;
409
+ }
410
+ return drawn;
411
+ }
412
+
413
+ /** The marker under a pane-local logical point, topmost first, or null. */
414
+ export function markerAt(
415
+ markers: readonly MapMarker[],
416
+ transform: Transform,
417
+ x: number,
418
+ y: number,
419
+ slop = 2,
420
+ ): MapMarker | null {
421
+ const ordered = byZ(markers);
422
+ for (let i = ordered.length - 1; i >= 0; i--) {
423
+ const marker = ordered[i];
424
+ if (marker.interactive === false) continue;
425
+ const rect = markerRect(marker, transform);
426
+ if (
427
+ x >= rect.x - slop &&
428
+ x <= rect.x + rect.width + slop &&
429
+ y >= rect.y - slop &&
430
+ y <= rect.y + rect.height + slop
431
+ ) {
432
+ return marker;
433
+ }
434
+ }
435
+ return null;
436
+ }
437
+
438
+ /**
439
+ * Decode Google's Encoded Polyline Algorithm Format.
440
+ *
441
+ * Here because it is what every routing engine on the open web answers
442
+ * with — Google Directions, OSRM, Valhalla, GraphHopper, Mapbox Directions
443
+ * — so a route arrives as one of these strings far more often than as
444
+ * GeoJSON. `precision` is 5 for Google and OSRM's default, 6 for Valhalla
445
+ * and OSRM's `polyline6`; passing the wrong one puts the route in the
446
+ * Atlantic, which is the standard way to discover this.
447
+ */
448
+ export function decodePolyline(encoded: string, precision = 5): LngLat[] {
449
+ const factor = Math.pow(10, precision);
450
+ const out: LngLat[] = [];
451
+ let index = 0;
452
+ let lat = 0;
453
+ let lon = 0;
454
+ while (index < encoded.length) {
455
+ let result = 0;
456
+ let shift = 0;
457
+ let byte: number;
458
+ do {
459
+ byte = encoded.charCodeAt(index++) - 63;
460
+ result |= (byte & 0x1f) << shift;
461
+ shift += 5;
462
+ } while (byte >= 0x20 && index < encoded.length);
463
+ lat += result & 1 ? ~(result >> 1) : result >> 1;
464
+ result = 0;
465
+ shift = 0;
466
+ do {
467
+ byte = encoded.charCodeAt(index++) - 63;
468
+ result |= (byte & 0x1f) << shift;
469
+ shift += 5;
470
+ } while (byte >= 0x20 && index < encoded.length);
471
+ lon += result & 1 ? ~(result >> 1) : result >> 1;
472
+ out.push({ lat: lat / factor, lon: lon / factor });
473
+ }
474
+ return out;
475
+ }
476
+
477
+ /** The slice of GeoJSON {@link geoJsonOverlays} reads. Written out
478
+ * structurally rather than taken from `@types/geojson`, so this package
479
+ * does not put a type dependency in an application's graph for a shape
480
+ * that is four lines. */
481
+ export interface GeoJsonLike {
482
+ type: string;
483
+ features?: readonly GeoJsonLike[];
484
+ geometry?: GeoJsonLike | null;
485
+ geometries?: readonly GeoJsonLike[];
486
+ coordinates?: unknown;
487
+ properties?: Record<string, unknown> | null;
488
+ id?: string | number;
489
+ }
490
+
491
+ /**
492
+ * GeoJSON to overlays.
493
+ *
494
+ * Points become markers and everything else becomes an overlay, which is
495
+ * the split the drawing makes. `style` is asked once per feature so an
496
+ * application can colour by a property — a traffic feed's congestion, a
497
+ * transit feed's route colour — without this function growing an
498
+ * expression language.
499
+ *
500
+ * Coordinates are `[lon, lat]`, which is GeoJSON's order and the opposite
501
+ * of how most people say it.
502
+ */
503
+ export function geoJsonOverlays(
504
+ geojson: GeoJsonLike,
505
+ style?: (
506
+ feature: GeoJsonLike,
507
+ ) => Partial<Extract<MapOverlay, { kind: 'line' }>> &
508
+ Partial<Extract<MapOverlay, { kind: 'polygon' }>>,
509
+ ): { overlays: MapOverlay[]; markers: MapMarker[] } {
510
+ const overlays: MapOverlay[] = [];
511
+ const markers: MapMarker[] = [];
512
+ let counter = 0;
513
+ const positions = (value: unknown): LngLat[] =>
514
+ Array.isArray(value)
515
+ ? value
516
+ .filter(
517
+ (pair): pair is [number, number] =>
518
+ Array.isArray(pair) &&
519
+ typeof pair[0] === 'number' &&
520
+ typeof pair[1] === 'number',
521
+ )
522
+ .map(([lon, lat]) => ({ lon, lat }))
523
+ : [];
524
+
525
+ const walk = (node: GeoJsonLike, owner: GeoJsonLike): void => {
526
+ const id = String(owner.id ?? `geojson-${counter++}`);
527
+ const extra = style?.(owner) ?? {};
528
+ switch (node.type) {
529
+ case 'FeatureCollection':
530
+ for (const feature of node.features ?? []) walk(feature, feature);
531
+ return;
532
+ case 'Feature':
533
+ if (node.geometry) walk(node.geometry, node);
534
+ return;
535
+ case 'GeometryCollection':
536
+ for (const geometry of node.geometries ?? []) walk(geometry, owner);
537
+ return;
538
+ case 'Point': {
539
+ const [point] = positions([node.coordinates]);
540
+ if (point)
541
+ markers.push({ id, position: point, data: owner.properties });
542
+ return;
543
+ }
544
+ case 'MultiPoint':
545
+ for (const point of positions(node.coordinates)) {
546
+ markers.push({
547
+ id: `${id}-${counter++}`,
548
+ position: point,
549
+ data: owner.properties,
550
+ });
551
+ }
552
+ return;
553
+ case 'LineString': {
554
+ const path = positions(node.coordinates);
555
+ if (path.length >= 2) {
556
+ overlays.push({
557
+ kind: 'line',
558
+ id,
559
+ path,
560
+ data: owner.properties,
561
+ ...extra,
562
+ });
563
+ }
564
+ return;
565
+ }
566
+ case 'MultiLineString':
567
+ for (const part of (node.coordinates as unknown[]) ?? []) {
568
+ const path = positions(part);
569
+ if (path.length >= 2) {
570
+ overlays.push({
571
+ kind: 'line',
572
+ id: `${id}-${counter++}`,
573
+ path,
574
+ data: owner.properties,
575
+ ...extra,
576
+ });
577
+ }
578
+ }
579
+ return;
580
+ case 'Polygon': {
581
+ const rings = ((node.coordinates as unknown[]) ?? []).map(positions);
582
+ if (rings.length > 0) {
583
+ overlays.push({
584
+ kind: 'polygon',
585
+ id,
586
+ rings,
587
+ data: owner.properties,
588
+ ...extra,
589
+ });
590
+ }
591
+ return;
592
+ }
593
+ case 'MultiPolygon':
594
+ for (const polygon of (node.coordinates as unknown[]) ?? []) {
595
+ const rings = ((polygon as unknown[]) ?? []).map(positions);
596
+ if (rings.length > 0) {
597
+ overlays.push({
598
+ kind: 'polygon',
599
+ id: `${id}-${counter++}`,
600
+ rings,
601
+ data: owner.properties,
602
+ ...extra,
603
+ });
604
+ }
605
+ }
606
+ return;
607
+ default:
608
+ return;
609
+ }
610
+ };
611
+ walk(geojson, geojson);
612
+ return { overlays, markers };
613
+ }
614
+
615
+ /**
616
+ * Beyond this radius in target pixels a circle is drawn as a clipped ring
617
+ * rather than an arc.
618
+ *
619
+ * The bound is the renderer's rather than the geometry's: ntk hands a
620
+ * stroke's geometry to XRender in 16.16 fixed point, which overflows a
621
+ * signed 32-bit word at 32,768, so an arc whose control geometry reaches
622
+ * that far is a `RangeError` from inside `paint`. Comfortably under it.
623
+ */
624
+ const MAX_ARC_RADIUS = 8192;
625
+
626
+ /** A circle as a ring, with the sagitta held under half a pixel. */
627
+ function circleRing(cx: number, cy: number, radius: number): number[] {
628
+ const step = 2 * Math.acos(Math.max(-1, 1 - 0.5 / radius));
629
+ const segments = Math.min(
630
+ 4096,
631
+ Math.max(24, Math.ceil((Math.PI * 2) / step)),
632
+ );
633
+ const out: number[] = [];
634
+ for (let i = 0; i < segments; i++) {
635
+ const angle = (i / segments) * Math.PI * 2;
636
+ out.push(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius);
637
+ }
638
+ return out;
639
+ }