@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.
Files changed (219) hide show
  1. package/README.md +125 -4
  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 +7 -1
  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/reorder/hx.d.ts +18 -0
  140. package/dist/reorder/hx.d.ts.map +1 -0
  141. package/dist/reorder/hx.js +10 -0
  142. package/dist/reorder/hx.js.map +1 -0
  143. package/dist/reorder/index.d.ts +314 -0
  144. package/dist/reorder/index.d.ts.map +1 -0
  145. package/dist/reorder/index.js +1539 -0
  146. package/dist/reorder/index.js.map +1 -0
  147. package/dist/reorder/model.d.ts +107 -0
  148. package/dist/reorder/model.d.ts.map +1 -0
  149. package/dist/reorder/model.js +162 -0
  150. package/dist/reorder/model.js.map +1 -0
  151. package/dist/richtext/node.d.ts +15 -5
  152. package/dist/richtext/node.d.ts.map +1 -1
  153. package/dist/richtext/node.js +40 -11
  154. package/dist/richtext/node.js.map +1 -1
  155. package/dist/richtext/runs.d.ts +35 -13
  156. package/dist/richtext/runs.d.ts.map +1 -1
  157. package/dist/richtext/runs.js +83 -27
  158. package/dist/richtext/runs.js.map +1 -1
  159. package/dist/tabs/index.d.ts +17 -0
  160. package/dist/tabs/index.d.ts.map +1 -1
  161. package/dist/tabs/index.js +849 -186
  162. package/dist/tabs/index.js.map +1 -1
  163. package/dist/terminal/vt/fonts.d.ts +45 -15
  164. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  165. package/dist/terminal/vt/fonts.js +89 -19
  166. package/dist/terminal/vt/fonts.js.map +1 -1
  167. package/dist/terminal/vt/node.d.ts +42 -0
  168. package/dist/terminal/vt/node.d.ts.map +1 -1
  169. package/dist/terminal/vt/node.js +95 -11
  170. package/dist/terminal/vt/node.js.map +1 -1
  171. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  172. package/dist/terminal/vt/renderer.js +13 -3
  173. package/dist/terminal/vt/renderer.js.map +1 -1
  174. package/package.json +13 -3
  175. package/src/charts/components.ts +14 -7
  176. package/src/charts/node.ts +81 -26
  177. package/src/charts/render.ts +16 -6
  178. package/src/code-editor/node.ts +147 -53
  179. package/src/flow/draw.ts +104 -35
  180. package/src/flow/index.ts +33 -13
  181. package/src/flow/model.ts +4 -2
  182. package/src/flow/node.ts +158 -59
  183. package/src/flow/types.ts +46 -21
  184. package/src/formula/index.ts +4 -2
  185. package/src/formula/node.ts +24 -5
  186. package/src/html/controls.ts +4 -1
  187. package/src/html/css/cascade.ts +18 -7
  188. package/src/html/css/parse.ts +46 -21
  189. package/src/html/css/style.ts +6 -1
  190. package/src/html/css/values.ts +25 -9
  191. package/src/html/index.ts +1 -0
  192. package/src/html/layout/boxes.ts +19 -9
  193. package/src/html/layout/inline.ts +23 -1
  194. package/src/html/node.ts +110 -17
  195. package/src/html/paint.ts +17 -2
  196. package/src/index.ts +98 -0
  197. package/src/maps/clip.ts +184 -0
  198. package/src/maps/gzip.ts +66 -0
  199. package/src/maps/index.ts +316 -0
  200. package/src/maps/labels.ts +442 -0
  201. package/src/maps/mvt.ts +857 -0
  202. package/src/maps/node.ts +1819 -0
  203. package/src/maps/overlay.ts +639 -0
  204. package/src/maps/paint.ts +978 -0
  205. package/src/maps/proj.ts +641 -0
  206. package/src/maps/sources.ts +425 -0
  207. package/src/maps/style.ts +309 -0
  208. package/src/maps/styles.ts +1040 -0
  209. package/src/maps/tiles.ts +676 -0
  210. package/src/maps/types.ts +260 -0
  211. package/src/reorder/hx.ts +31 -0
  212. package/src/reorder/index.ts +2223 -0
  213. package/src/reorder/model.ts +232 -0
  214. package/src/richtext/node.ts +49 -14
  215. package/src/richtext/runs.ts +115 -30
  216. package/src/tabs/index.ts +1040 -204
  217. package/src/terminal/vt/fonts.ts +128 -36
  218. package/src/terminal/vt/node.ts +105 -12
  219. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,641 @@
1
+ // Web Mercator, the slippy-map tile grid, and the camera — the arithmetic
2
+ // every other module here stands on, with no display anywhere in it.
3
+ //
4
+ // Three coordinate spaces, and knowing which one you are in is most of the
5
+ // correctness of a map:
6
+ //
7
+ // 1. **Geographic** — `{ lon, lat }` in WGS84 degrees, what an application
8
+ // says and what a marker is placed at.
9
+ // 2. **Mercator** — `{ x, y }` normalized to the unit square, y increasing
10
+ // *south*: the projection every provider's tiles are cut on
11
+ // (EPSG:3857, "Web Mercator"). Kept normalized rather than in metres or
12
+ // in pixels because it is the one space that does not depend on the
13
+ // zoom, so a cached derivation of it stays valid across one.
14
+ // 3. **Screen** — logical pixels in the pane, which is where the camera
15
+ // comes in.
16
+ //
17
+ // The screen conversion is deliberately written as *centre-relative*:
18
+ //
19
+ // screenX = paneCentreX + (mx - centreMx) * worldSize
20
+ //
21
+ // and not as `mx * worldSize - centreMx * worldSize`. The two are the same
22
+ // number in exact arithmetic and not in float64: at zoom 20 `worldSize` is
23
+ // 5.4e8, so `mx * worldSize` for a point in Melbourne is ~4.1e8 and the
24
+ // spacing between representable doubles there is ~6e-8 pixels — fine — but
25
+ // the *difference* of two such products loses the low bits of both, and a
26
+ // building's wall ends up a wobbling half-pixel from where its neighbour's
27
+ // begins. Subtracting first keeps the small quantity small. Everything in
28
+ // this file that produces a pixel does it in that order, and
29
+ // {@link tileTransform} is the same trick again for a tile's own integers.
30
+
31
+ /** A geographic position, in WGS84 degrees. */
32
+ export interface LngLat {
33
+ lon: number;
34
+ lat: number;
35
+ }
36
+
37
+ /** A geographic rectangle. `west` may exceed `east` for a box that crosses
38
+ * the antimeridian. */
39
+ export interface LngLatBounds {
40
+ west: number;
41
+ south: number;
42
+ east: number;
43
+ north: number;
44
+ }
45
+
46
+ /** A position in normalized Web Mercator: both axes in `[0, 1]`, `y`
47
+ * increasing south. */
48
+ export interface MercatorPoint {
49
+ x: number;
50
+ y: number;
51
+ }
52
+
53
+ /** A tile's address in the slippy-map grid. */
54
+ export interface TileId {
55
+ z: number;
56
+ x: number;
57
+ y: number;
58
+ }
59
+
60
+ /** Where the map is looking. `zoom` is fractional — the integer part picks
61
+ * the tile pyramid level, the fraction scales it. */
62
+ export interface MapCamera {
63
+ center: LngLat;
64
+ zoom: number;
65
+ }
66
+
67
+ /** A rectangle in pane-local logical pixels. */
68
+ export interface ScreenRect {
69
+ x: number;
70
+ y: number;
71
+ width: number;
72
+ height: number;
73
+ }
74
+
75
+ /**
76
+ * The latitude Web Mercator can represent, `atan(sinh(pi))` in degrees.
77
+ *
78
+ * Not a rounding of 85: it is the exact latitude at which the projection's
79
+ * `y` reaches 1, and clamping to anything larger produces a `y` outside the
80
+ * unit square, which then addresses a tile row that does not exist. Every
81
+ * projection entry point clamps to it.
82
+ */
83
+ export const MAX_LATITUDE = 85.0511287798066;
84
+
85
+ /** WGS84's semi-major axis, in metres — what a scale bar is measured
86
+ * against. */
87
+ export const EARTH_RADIUS = 6378137;
88
+
89
+ /** The equator, in metres. */
90
+ export const EARTH_CIRCUMFERENCE = 2 * Math.PI * EARTH_RADIUS;
91
+
92
+ /** The tile size providers overwhelmingly serve vector tiles at. 256 is the
93
+ * older raster convention and is still common; both are supported per
94
+ * source, and this is only the default. */
95
+ export const DEFAULT_TILE_SIZE = 512;
96
+
97
+ const DEG = Math.PI / 180;
98
+
99
+ function clamp(value: number, low: number, high: number): number {
100
+ return value < low ? low : value > high ? high : value;
101
+ }
102
+
103
+ /** Longitude folded into `[-180, 180)`. */
104
+ export function wrapLon(lon: number): number {
105
+ if (lon >= -180 && lon < 180) return lon;
106
+ const wrapped = ((((lon + 180) % 360) + 360) % 360) - 180;
107
+ return wrapped;
108
+ }
109
+
110
+ // --- the projection --------------------------------------------------------
111
+
112
+ /** Longitude to normalized mercator `x`. Not wrapped: a caller drawing world
113
+ * copies wants to know it went past the edge. */
114
+ export function mercatorXFromLon(lon: number): number {
115
+ return (180 + lon) / 360;
116
+ }
117
+
118
+ /** Latitude to normalized mercator `y`, clamped to {@link MAX_LATITUDE}. */
119
+ export function mercatorYFromLat(lat: number): number {
120
+ const phi = clamp(lat, -MAX_LATITUDE, MAX_LATITUDE);
121
+ return (
122
+ (180 -
123
+ (180 / Math.PI) * Math.log(Math.tan(Math.PI / 4 + phi * DEG * 0.5))) /
124
+ 360
125
+ );
126
+ }
127
+
128
+ export function lonFromMercatorX(x: number): number {
129
+ return x * 360 - 180;
130
+ }
131
+
132
+ export function latFromMercatorY(y: number): number {
133
+ return (360 / Math.PI) * Math.atan(Math.exp((180 - y * 360) * DEG)) - 90;
134
+ }
135
+
136
+ export function project(position: LngLat): MercatorPoint {
137
+ return {
138
+ x: mercatorXFromLon(position.lon),
139
+ y: mercatorYFromLat(position.lat),
140
+ };
141
+ }
142
+
143
+ export function unproject(point: MercatorPoint): LngLat {
144
+ return { lon: lonFromMercatorX(point.x), lat: latFromMercatorY(point.y) };
145
+ }
146
+
147
+ /**
148
+ * How much Mercator stretches distance at this latitude — `1 / cos(lat)`.
149
+ *
150
+ * The reason a scale bar cannot be a constant: one pixel is 156 km at the
151
+ * equator on zoom 0 and 13 km at 85°N, on the same map.
152
+ */
153
+ export function mercatorScale(lat: number): number {
154
+ return 1 / Math.cos(clamp(lat, -MAX_LATITUDE, MAX_LATITUDE) * DEG);
155
+ }
156
+
157
+ /** Ground metres one logical pixel covers, at this latitude and zoom. */
158
+ export function metresPerPixel(
159
+ lat: number,
160
+ zoom: number,
161
+ tileSize = DEFAULT_TILE_SIZE,
162
+ ): number {
163
+ return (
164
+ (EARTH_CIRCUMFERENCE *
165
+ Math.cos(clamp(lat, -MAX_LATITUDE, MAX_LATITUDE) * DEG)) /
166
+ (tileSize * Math.pow(2, zoom))
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Great-circle distance in metres, by the haversine formula.
172
+ *
173
+ * On the sphere rather than the WGS84 ellipsoid: the error is under 0.5%,
174
+ * which is well inside what a map at any zoom can draw, and the ellipsoidal
175
+ * form (Vincenty, Karney) is an iteration that a hover readout should not
176
+ * be running per frame.
177
+ */
178
+ export function distanceMetres(a: LngLat, b: LngLat): number {
179
+ const dLat = (b.lat - a.lat) * DEG;
180
+ const dLon = (b.lon - a.lon) * DEG;
181
+ const sLat = Math.sin(dLat / 2);
182
+ const sLon = Math.sin(dLon / 2);
183
+ const h =
184
+ sLat * sLat + Math.cos(a.lat * DEG) * Math.cos(b.lat * DEG) * sLon * sLon;
185
+ return 2 * EARTH_RADIUS * Math.asin(Math.min(1, Math.sqrt(h)));
186
+ }
187
+
188
+ // --- the tile grid ---------------------------------------------------------
189
+
190
+ /** Tiles across the world at this zoom: `2^z`. */
191
+ export function tileCountAt(z: number): number {
192
+ return Math.pow(2, z);
193
+ }
194
+
195
+ /** A tile column folded into `[0, 2^z)`. Longitude wraps and latitude does
196
+ * not, so this exists for `x` and has no counterpart for `y`. */
197
+ export function wrapTileX(x: number, z: number): number {
198
+ const n = tileCountAt(z);
199
+ return ((x % n) + n) % n;
200
+ }
201
+
202
+ /** The tile containing a position. */
203
+ export function tileOf(position: LngLat, z: number): TileId {
204
+ const n = tileCountAt(z);
205
+ const x = Math.floor(mercatorXFromLon(position.lon) * n);
206
+ const y = Math.floor(mercatorYFromLat(position.lat) * n);
207
+ return { z, x: wrapTileX(x, z), y: clamp(y, 0, n - 1) };
208
+ }
209
+
210
+ /** A tile's geographic extent. */
211
+ export function tileBounds(tile: TileId): LngLatBounds {
212
+ const n = tileCountAt(tile.z);
213
+ return {
214
+ west: lonFromMercatorX(tile.x / n),
215
+ east: lonFromMercatorX((tile.x + 1) / n),
216
+ north: latFromMercatorY(tile.y / n),
217
+ south: latFromMercatorY((tile.y + 1) / n),
218
+ };
219
+ }
220
+
221
+ /** `z/x/y`, the key every cache here is keyed on. */
222
+ export function tileKey(tile: TileId): string {
223
+ return `${tile.z}/${tile.x}/${tile.y}`;
224
+ }
225
+
226
+ /** A tile's parent, or null at the root — how a missing tile is covered by
227
+ * the coarser one that is already loaded. */
228
+ export function parentTile(tile: TileId): TileId | null {
229
+ if (tile.z <= 0) return null;
230
+ return { z: tile.z - 1, x: tile.x >> 1, y: tile.y >> 1 };
231
+ }
232
+
233
+ /** Whether `outer` contains `inner` in the pyramid — the test behind
234
+ * covering a gap with an ancestor's pixels. */
235
+ export function tileContains(outer: TileId, inner: TileId): boolean {
236
+ if (outer.z > inner.z) return false;
237
+ const shift = inner.z - outer.z;
238
+ return inner.x >> shift === outer.x && inner.y >> shift === outer.y;
239
+ }
240
+
241
+ // --- the camera ------------------------------------------------------------
242
+
243
+ /** Logical pixels across the whole world at this zoom. */
244
+ export function worldSize(zoom: number, tileSize = DEFAULT_TILE_SIZE): number {
245
+ return tileSize * Math.pow(2, zoom);
246
+ }
247
+
248
+ /**
249
+ * The camera resolved against a viewport: everything the projection needs,
250
+ * computed once per frame instead of per point.
251
+ *
252
+ * `centerX`/`centerY` are the *mercator* centre and `paneX`/`paneY` the
253
+ * pane-local pixel the camera looks at, which is what makes
254
+ * {@link projectPoint} centre-relative.
255
+ */
256
+ export interface Transform {
257
+ centerX: number;
258
+ centerY: number;
259
+ zoom: number;
260
+ tileSize: number;
261
+ /** Pane size in logical pixels. */
262
+ width: number;
263
+ height: number;
264
+ /** Where the centre lands — the middle of the pane. */
265
+ paneX: number;
266
+ paneY: number;
267
+ /** `tileSize * 2^zoom`. */
268
+ world: number;
269
+ }
270
+
271
+ export function transformFor(
272
+ camera: MapCamera,
273
+ size: { width: number; height: number },
274
+ tileSize = DEFAULT_TILE_SIZE,
275
+ ): Transform {
276
+ const center = project(camera.center);
277
+ return {
278
+ centerX: center.x,
279
+ centerY: center.y,
280
+ zoom: camera.zoom,
281
+ tileSize,
282
+ width: size.width,
283
+ height: size.height,
284
+ paneX: size.width / 2,
285
+ paneY: size.height / 2,
286
+ world: worldSize(camera.zoom, tileSize),
287
+ };
288
+ }
289
+
290
+ /** A mercator point in pane-local logical pixels. */
291
+ export function projectPoint(
292
+ t: Transform,
293
+ point: MercatorPoint,
294
+ ): { x: number; y: number } {
295
+ return {
296
+ x: t.paneX + (point.x - t.centerX) * t.world,
297
+ y: t.paneY + (point.y - t.centerY) * t.world,
298
+ };
299
+ }
300
+
301
+ /** A geographic position in pane-local logical pixels, taking the nearest
302
+ * copy of the world so a marker just past the antimeridian draws beside the
303
+ * map rather than a world away from it. */
304
+ export function projectLngLat(
305
+ t: Transform,
306
+ position: LngLat,
307
+ ): { x: number; y: number } {
308
+ let mx = mercatorXFromLon(position.lon);
309
+ // The camera is somewhere in `[0, 1)`; a point more than half a world away
310
+ // in mercator is nearer in the other direction.
311
+ const shift = Math.round(mx - t.centerX);
312
+ mx -= shift;
313
+ return projectPoint(t, { x: mx, y: mercatorYFromLat(position.lat) });
314
+ }
315
+
316
+ /** A pane-local pixel back to a geographic position. */
317
+ export function unprojectPoint(t: Transform, x: number, y: number): LngLat {
318
+ const mx = t.centerX + (x - t.paneX) / t.world;
319
+ const my = clamp(t.centerY + (y - t.paneY) / t.world, 0, 1);
320
+ return { lon: wrapLon(lonFromMercatorX(mx)), lat: latFromMercatorY(my) };
321
+ }
322
+
323
+ /** What the pane can see, as geography. `west > east` when the view spans
324
+ * the antimeridian; a view wider than the world reports the whole of it. */
325
+ export function visibleBounds(t: Transform): LngLatBounds {
326
+ const nw = unprojectPoint(t, 0, 0);
327
+ const se = unprojectPoint(t, t.width, t.height);
328
+ if (t.width >= t.world) {
329
+ return { west: -180, east: 180, south: se.lat, north: nw.lat };
330
+ }
331
+ return { west: nw.lon, north: nw.lat, east: se.lon, south: se.lat };
332
+ }
333
+
334
+ // --- the cover -------------------------------------------------------------
335
+
336
+ /** What a source can serve, and at what size. */
337
+ export interface TilePyramid {
338
+ minZoom: number;
339
+ maxZoom: number;
340
+ tileSize: number;
341
+ }
342
+
343
+ /** One tile the pane can see, and where it lands. */
344
+ export interface TileCoverEntry {
345
+ /** The tile to load. `x` is wrapped into `[0, 2^z)`, so this is a cache
346
+ * key and `worldCopy` is what a screen position needs on top of it. */
347
+ tile: TileId;
348
+ /** Which copy of the world this instance is — 0 for the one containing the
349
+ * camera, ±1 for the repeats either side of the antimeridian. */
350
+ worldCopy: number;
351
+ /** Top-left in pane-local logical pixels. */
352
+ x: number;
353
+ y: number;
354
+ /** Edge length on screen, in logical pixels. `tileSize` exactly when the
355
+ * zoom is integral and the pyramid is neither over- nor under-zoomed. */
356
+ size: number;
357
+ /** How far this tile is from the pane's centre, in logical pixels — what
358
+ * the centre-out load and draw order sorts on. */
359
+ distance: number;
360
+ }
361
+
362
+ /** How the raster of a tile is sized, and what compositing it then costs. */
363
+ export interface TileRaster {
364
+ /** Edge length of the tile's own surface, in **device** pixels. */
365
+ size: number;
366
+ /** `size / (tileSize * scale)` — the power of two the raster is above the
367
+ * tile's natural size, for an overzoomed pyramid. */
368
+ factor: number;
369
+ }
370
+
371
+ /**
372
+ * Which pyramid level serves a camera zoom.
373
+ *
374
+ * `floor` rather than `round`, which is the convention every slippy-map
375
+ * client uses: a zoom of 14.9 is served by z14 tiles scaled up, and the
376
+ * moment it crosses 15 the finer level takes over. Rounding instead would
377
+ * load z15 at 14.5 and draw it at 0.7× — more data, downsampled.
378
+ */
379
+ /**
380
+ * How many levels deeper than the view a source is read, because its tiles
381
+ * are smaller than the transform's cell.
382
+ *
383
+ * 0 for the 512-px vector sources this defaults to, 1 for the 256-px raster
384
+ * services, and it falls out of the arithmetic rather than being a special
385
+ * case: a tile is drawn at its natural size when
386
+ * `t.tileSize * 2^(zoom - z) === pyramid.tileSize`.
387
+ */
388
+ export function zoomOffsetFor(
389
+ t: Pick<Transform, 'tileSize'>,
390
+ pyramid: Pick<TilePyramid, 'tileSize'>,
391
+ ): number {
392
+ if (!(pyramid.tileSize > 0) || !(t.tileSize > 0)) return 0;
393
+ return Math.round(Math.log2(t.tileSize / pyramid.tileSize));
394
+ }
395
+
396
+ export function sourceZoomFor(
397
+ zoom: number,
398
+ pyramid: TilePyramid,
399
+ overzoom = 0,
400
+ ): number {
401
+ return clamp(
402
+ Math.floor(zoom),
403
+ pyramid.minZoom,
404
+ pyramid.maxZoom + Math.max(0, overzoom),
405
+ );
406
+ }
407
+
408
+ /**
409
+ * The tile whose **data** serves a tile the cover asked for.
410
+ *
411
+ * Itself, until the cover goes deeper than the pyramid does — past that,
412
+ * its ancestor at the deepest level the source cuts. This is what makes
413
+ * overzoom sharp rather than blurry: instead of drawing one z14 tile onto a
414
+ * surface and stretching it sixty-four times, the cover asks for z20 tiles,
415
+ * two hundred and fifty-six of them share that one z14 tile's data, and
416
+ * each is rasterized at its own natural size. One fetch, one parse, and
417
+ * detail limited by the data rather than by a bitmap.
418
+ */
419
+ export function dataTileFor(tile: TileId, maxZoom: number): TileId {
420
+ if (tile.z <= maxZoom) return tile;
421
+ const down = tile.z - maxZoom;
422
+ return { z: maxZoom, x: tile.x >> down, y: tile.y >> down };
423
+ }
424
+
425
+ /**
426
+ * Where a tile sits inside the data tile that serves it: cell
427
+ * `(x, y)` of a `span × span` grid, and `span` is 1 when the tile *is* its
428
+ * own data.
429
+ */
430
+ export function subTileOf(
431
+ tile: TileId,
432
+ maxZoom: number,
433
+ ): { x: number; y: number; span: number } {
434
+ if (tile.z <= maxZoom) return { x: 0, y: 0, span: 1 };
435
+ const down = tile.z - maxZoom;
436
+ const span = 1 << down;
437
+ return {
438
+ x: tile.x - ((tile.x >> down) << down),
439
+ y: tile.y - ((tile.y >> down) << down),
440
+ span,
441
+ };
442
+ }
443
+
444
+ /**
445
+ * How large to rasterize a tile whose pyramid level is `z` at camera `zoom`.
446
+ *
447
+ * The rule: **rasterize at the size the tile has at integer zoom, and let
448
+ * the composite carry only the fraction.** `2^(zoom - z)` splits into
449
+ * `2^(floor(zoom) - z)`, a power of two that goes into the raster, times
450
+ * `2^frac(zoom)`, which is in `[1, 2)` and is a `drawImage` away. So a
451
+ * pinch never re-rasterizes — the expensive half of the frame is the same
452
+ * pixels at every zoom inside one level — and a tile is never composited at
453
+ * more than 2× upscale unless the cap below is what stopped it.
454
+ *
455
+ * `maxSize` is that cap, and it is about memory rather than sharpness: an
456
+ * argb32 surface is `4 * size²` bytes, so a 1024-pixel tile is 4 MB and a
457
+ * screenful of them is a real number. Past the cap the raster stays where it
458
+ * is and the composite upscales, which is the same trade a raster tile makes
459
+ * when it is overzoomed.
460
+ */
461
+ export function rasterFor(
462
+ zoom: number,
463
+ z: number,
464
+ pyramid: TilePyramid,
465
+ scale: number,
466
+ maxSize: number,
467
+ ): TileRaster {
468
+ const natural = pyramid.tileSize * scale;
469
+ const steps = Math.floor(zoom) - z;
470
+ let factor = Math.pow(2, clamp(steps, -2, 2));
471
+ while (factor > 1 && natural * factor > maxSize) factor /= 2;
472
+ return { size: Math.max(1, Math.round(natural * factor)), factor };
473
+ }
474
+
475
+ /**
476
+ * Every tile the pane can see, centre-out.
477
+ *
478
+ * `padding` grows the rectangle in logical pixels, which is how a ring of
479
+ * tiles is kept warm so a pan has them before it needs them. The order is
480
+ * the load order and the draw order both: the tile under the pointer is the
481
+ * one whose arrival the user is waiting for.
482
+ */
483
+ export function tileCover(
484
+ t: Transform,
485
+ pyramid: TilePyramid,
486
+ padding = 0,
487
+ ): TileCoverEntry[] {
488
+ // A source whose tiles are 256 px covers the transform's 512-px cell with
489
+ // two of them each way, so it is read one level **deeper** for the same
490
+ // view: the same ground, a finer grid, and each image drawn at its own
491
+ // natural size instead of stretched to twice it. This is the convention
492
+ // every web map uses, and getting it wrong is not a blurry map but a
493
+ // wrong one — see below.
494
+ const z = sourceZoomFor(t.zoom + zoomOffsetFor(t, pyramid), pyramid);
495
+ const n = tileCountAt(z);
496
+ // Screen size of one tile of this level, from **the transform's** world
497
+ // and not the pyramid's tile size.
498
+ //
499
+ // The distinction is the whole of react-x11-components#65. Placement below
500
+ // is `paneX + (ix - fx) * size` with `fx = centerX * n`, so the world this
501
+ // grid is laid out on is `n * size`. That has to be `t.world`, because
502
+ // `t.world` is what places every *other* thing in the pane — markers,
503
+ // overlays, labels, `project()`, and the shift a pan blit asks for. Using
504
+ // the pyramid's tile size here made `n * size` the world of a 256-px
505
+ // pyramid while everything else used a 512-px one, so on a raster source
506
+ // the tiles moved half as far as the pointer and the markers slid across
507
+ // them. `t.tileSize * 2^(zoom - z)` keeps `n * size === t.world` by
508
+ // construction, at every level, overzoomed or not.
509
+ const size = t.tileSize * Math.pow(2, t.zoom - z);
510
+ // The pane's edges as fractional tile coordinates. `centerX * n` is where
511
+ // the camera sits in this level's grid.
512
+ const fx = t.centerX * n;
513
+ const fy = t.centerY * n;
514
+ const left = fx - (t.paneX + padding) / size;
515
+ const right = fx + (t.width - t.paneX + padding) / size;
516
+ const top = fy - (t.paneY + padding) / size;
517
+ const bottom = fy + (t.height - t.paneY + padding) / size;
518
+
519
+ const yMin = Math.max(0, Math.floor(top));
520
+ const yMax = Math.min(n - 1, Math.ceil(bottom) - 1);
521
+ const xMin = Math.floor(left);
522
+ const xMax = Math.ceil(right) - 1;
523
+
524
+ const out: TileCoverEntry[] = [];
525
+ // A pane wider than the world would otherwise ask for the same tile
526
+ // hundreds of times over; three copies is what a view at zoom 0 can show.
527
+ const span = Math.min(xMax - xMin, 3 * n - 1);
528
+ for (let ix = xMin; ix <= xMin + span; ix++) {
529
+ for (let iy = yMin; iy <= yMax; iy++) {
530
+ const x = t.paneX + (ix - fx) * size;
531
+ const y = t.paneY + (iy - fy) * size;
532
+ const cx = x + size / 2 - t.paneX;
533
+ const cy = y + size / 2 - t.paneY;
534
+ out.push({
535
+ tile: { z, x: wrapTileX(ix, z), y: iy },
536
+ worldCopy: Math.floor(ix / n),
537
+ x,
538
+ y,
539
+ size,
540
+ distance: Math.sqrt(cx * cx + cy * cy),
541
+ });
542
+ }
543
+ }
544
+ out.sort((a, b) => a.distance - b.distance);
545
+ return out;
546
+ }
547
+
548
+ /**
549
+ * The affine that takes a tile's own integer coordinates to pane pixels:
550
+ * `screenX = ox + local * k`.
551
+ *
552
+ * Two multiplies and an add per vertex, and — the point — the subtraction
553
+ * that could lose precision happened once, here, on the tile's index rather
554
+ * than on every one of its ten thousand vertices.
555
+ */
556
+ export function tileTransform(
557
+ entry: TileCoverEntry,
558
+ extent: number,
559
+ ): { ox: number; oy: number; k: number } {
560
+ return { ox: entry.x, oy: entry.y, k: entry.size / extent };
561
+ }
562
+
563
+ /**
564
+ * The camera that frames `bounds` in a viewport.
565
+ *
566
+ * `padding` is logical pixels kept clear on every side. A degenerate box — a
567
+ * single point, or a set of markers all at one place — has no zoom that
568
+ * frames it, so `maxZoom` is what it lands at rather than infinity.
569
+ */
570
+ export function cameraForBounds(
571
+ bounds: LngLatBounds,
572
+ size: { width: number; height: number },
573
+ options?: {
574
+ padding?: number;
575
+ tileSize?: number;
576
+ minZoom?: number;
577
+ maxZoom?: number;
578
+ },
579
+ ): MapCamera {
580
+ const tileSize = options?.tileSize ?? DEFAULT_TILE_SIZE;
581
+ const padding = options?.padding ?? 0;
582
+ const minZoom = options?.minZoom ?? 0;
583
+ const maxZoom = options?.maxZoom ?? 22;
584
+ const west = mercatorXFromLon(bounds.west);
585
+ let east = mercatorXFromLon(bounds.east);
586
+ // A box written across the antimeridian (west 170, east -170) is 20° wide,
587
+ // not 340°.
588
+ if (east < west) east += 1;
589
+ const north = mercatorYFromLat(bounds.north);
590
+ const south = mercatorYFromLat(bounds.south);
591
+ const dx = Math.max(east - west, 1e-12);
592
+ const dy = Math.max(south - north, 1e-12);
593
+ const usableW = Math.max(1, size.width - padding * 2);
594
+ const usableH = Math.max(1, size.height - padding * 2);
595
+ const zoom = clamp(
596
+ Math.log2(Math.min(usableW / (dx * tileSize), usableH / (dy * tileSize))),
597
+ minZoom,
598
+ maxZoom,
599
+ );
600
+ const mx = (west + east) / 2;
601
+ return {
602
+ center: {
603
+ lon: wrapLon(lonFromMercatorX(mx)),
604
+ lat: latFromMercatorY((north + south) / 2),
605
+ },
606
+ zoom,
607
+ };
608
+ }
609
+
610
+ /** The box that contains every position given, or null for none. Grows east
611
+ * rather than wrapping, so a set spanning the antimeridian comes back with
612
+ * `west > east` and {@link cameraForBounds} reads it the same way. */
613
+ export function boundsOf(positions: readonly LngLat[]): LngLatBounds | null {
614
+ if (positions.length === 0) return null;
615
+ let west = Infinity;
616
+ let east = -Infinity;
617
+ let south = Infinity;
618
+ let north = -Infinity;
619
+ for (const p of positions) {
620
+ const lon = wrapLon(p.lon);
621
+ if (lon < west) west = lon;
622
+ if (lon > east) east = lon;
623
+ if (p.lat < south) south = p.lat;
624
+ if (p.lat > north) north = p.lat;
625
+ }
626
+ // Two points either side of the antimeridian look 340° apart the long way
627
+ // round and 20° apart the short way; the short way is what was meant.
628
+ if (east - west > 180) {
629
+ let w = Infinity;
630
+ let e = -Infinity;
631
+ for (const p of positions) {
632
+ const lon = wrapLon(p.lon);
633
+ const shifted = lon < 0 ? lon + 360 : lon;
634
+ if (shifted < w) w = shifted;
635
+ if (shifted > e) e = shifted;
636
+ }
637
+ if (e - w < east - west)
638
+ return { west: wrapLon(w), east: wrapLon(e), south, north };
639
+ }
640
+ return { west, east, south, north };
641
+ }