@goway.to/maps 0.0.0-stage → 0.1.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 (125) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +676 -0
  3. package/NOTICE +25 -0
  4. package/README.md +59 -2
  5. package/app.plugin.js +19 -0
  6. package/dist/CameraBinding.web.d.ts +24 -0
  7. package/dist/CameraBinding.web.js +46 -0
  8. package/dist/DefaultMapMarker.d.ts +8 -0
  9. package/dist/DefaultMapMarker.js +30 -0
  10. package/dist/GeoJSONSource.d.ts +2 -0
  11. package/dist/GeoJSONSource.js +17 -0
  12. package/dist/GeoJSONSource.web.d.ts +2 -0
  13. package/dist/GeoJSONSource.web.js +68 -0
  14. package/dist/InteractionBinding.web.d.ts +7 -0
  15. package/dist/InteractionBinding.web.js +35 -0
  16. package/dist/Layer.d.ts +2 -0
  17. package/dist/Layer.js +21 -0
  18. package/dist/Layer.web.d.ts +2 -0
  19. package/dist/Layer.web.js +46 -0
  20. package/dist/LayerStyle.web.d.ts +10 -0
  21. package/dist/LayerStyle.web.js +45 -0
  22. package/dist/LightBinding.web.d.ts +5 -0
  23. package/dist/LightBinding.web.js +19 -0
  24. package/dist/MapAttribution.d.ts +27 -0
  25. package/dist/MapAttribution.js +149 -0
  26. package/dist/MapBrand.d.ts +22 -0
  27. package/dist/MapBrand.js +91 -0
  28. package/dist/MapErrorState.d.ts +9 -0
  29. package/dist/MapErrorState.js +58 -0
  30. package/dist/MapPlugin.d.ts +6 -0
  31. package/dist/MapPlugin.js +3 -0
  32. package/dist/MapPlugin.web.d.ts +2 -0
  33. package/dist/MapPlugin.web.js +43 -0
  34. package/dist/MapView.d.ts +2 -0
  35. package/dist/MapView.js +420 -0
  36. package/dist/MapView.web.d.ts +5 -0
  37. package/dist/MapView.web.js +413 -0
  38. package/dist/Marker.d.ts +13 -0
  39. package/dist/Marker.js +11 -0
  40. package/dist/Marker.web.d.ts +15 -0
  41. package/dist/Marker.web.js +51 -0
  42. package/dist/Markers.d.ts +2 -0
  43. package/dist/Markers.js +13 -0
  44. package/dist/Markers.web.d.ts +2 -0
  45. package/dist/Markers.web.js +13 -0
  46. package/dist/PluginProps.web.d.ts +7 -0
  47. package/dist/PluginProps.web.js +19 -0
  48. package/dist/RasterDEMSource.d.ts +2 -0
  49. package/dist/RasterDEMSource.js +7 -0
  50. package/dist/RasterDEMSource.web.d.ts +2 -0
  51. package/dist/RasterDEMSource.web.js +44 -0
  52. package/dist/SourceData.web.d.ts +6 -0
  53. package/dist/SourceData.web.js +14 -0
  54. package/dist/UserLocation.d.ts +2 -0
  55. package/dist/UserLocation.js +19 -0
  56. package/dist/UserLocation.web.d.ts +2 -0
  57. package/dist/UserLocation.web.js +21 -0
  58. package/dist/brand/BrandedChrome.d.ts +47 -0
  59. package/dist/brand/BrandedChrome.js +51 -0
  60. package/dist/brand/GowayLogo.d.ts +28 -0
  61. package/dist/brand/GowayLogo.js +50 -0
  62. package/dist/brand/artwork.d.ts +130 -0
  63. package/dist/brand/artwork.js +176 -0
  64. package/dist/cameraReports.web.d.ts +50 -0
  65. package/dist/cameraReports.web.js +117 -0
  66. package/dist/cameraTarget.d.ts +14 -0
  67. package/dist/cameraTarget.js +8 -0
  68. package/dist/chromeLift.d.ts +15 -0
  69. package/dist/chromeLift.js +15 -0
  70. package/dist/clusters.d.ts +57 -0
  71. package/dist/clusters.js +102 -0
  72. package/dist/commands.web.d.ts +16 -0
  73. package/dist/commands.web.js +158 -0
  74. package/dist/compactCredit.d.ts +52 -0
  75. package/dist/compactCredit.js +40 -0
  76. package/dist/creditHeight.d.ts +3 -0
  77. package/dist/creditHeight.js +12 -0
  78. package/dist/creditLabels.d.ts +30 -0
  79. package/dist/creditLabels.js +30 -0
  80. package/dist/creditRise.d.ts +28 -0
  81. package/dist/creditRise.js +33 -0
  82. package/dist/dragAxes.web.d.ts +65 -0
  83. package/dist/dragAxes.web.js +70 -0
  84. package/dist/engineContext.web.d.ts +13 -0
  85. package/dist/engineContext.web.js +12 -0
  86. package/dist/index.d.ts +12 -0
  87. package/dist/index.js +12 -0
  88. package/dist/index.web.d.ts +13 -0
  89. package/dist/index.web.js +13 -0
  90. package/dist/labels.d.ts +202 -0
  91. package/dist/labels.js +451 -0
  92. package/dist/loadedCamera.d.ts +64 -0
  93. package/dist/loadedCamera.js +56 -0
  94. package/dist/locationStore.d.ts +16 -0
  95. package/dist/locationStore.js +132 -0
  96. package/dist/mapRef.d.ts +4 -0
  97. package/dist/mapRef.js +13 -0
  98. package/dist/moveMarkers.d.ts +27 -0
  99. package/dist/moveMarkers.js +22 -0
  100. package/dist/nativeCameraReports.d.ts +44 -0
  101. package/dist/nativeCameraReports.js +73 -0
  102. package/dist/plugin.d.ts +63 -0
  103. package/dist/plugin.js +2 -0
  104. package/dist/queryLayers.d.ts +19 -0
  105. package/dist/queryLayers.js +54 -0
  106. package/dist/shared-exports.d.ts +17 -0
  107. package/dist/shared-exports.js +13 -0
  108. package/dist/shared.d.ts +170 -0
  109. package/dist/shared.js +337 -0
  110. package/dist/sourceContext.d.ts +2 -0
  111. package/dist/sourceContext.js +9 -0
  112. package/dist/styleUrl.d.ts +19 -0
  113. package/dist/styleUrl.js +22 -0
  114. package/dist/testing.d.ts +41 -0
  115. package/dist/testing.js +80 -0
  116. package/dist/types.d.ts +348 -0
  117. package/dist/types.js +1 -0
  118. package/dist/useMarkerSlots.d.ts +6 -0
  119. package/dist/useMarkerSlots.js +56 -0
  120. package/dist/useUserLocation.d.ts +44 -0
  121. package/dist/useUserLocation.js +247 -0
  122. package/dist/zoom.d.ts +2 -0
  123. package/dist/zoom.js +10 -0
  124. package/metro.cjs +70 -0
  125. package/package.json +115 -4
package/dist/labels.js ADDED
@@ -0,0 +1,451 @@
1
+ /**
2
+ * The basemap's own labels, as GoWay-shaped things.
3
+ *
4
+ * The vector tiles have always carried the names — POIs, streets, rivers,
5
+ * districts, cities — and GoWay's style has always drawn them. Until this
6
+ * module they were PAINT: `map.on('click')` reported a coordinate and nothing
7
+ * ever asked the engine what was under the finger. Every name on the map was
8
+ * dead pixels, while GoWay drew its own chips on top of some of them.
9
+ *
10
+ * This file is where a tapped tile feature stops being the engine's and becomes
11
+ * GoWay's. Both engine forks query their own engine (`queryRenderedFeatures`
12
+ * on web, `MapRef.queryRenderedFeatures` on native), flatten each hit into a
13
+ * {@link QueriedLabel}, and hand the list here; what comes back is a
14
+ * {@link MapLabelFeature}, the only shape an app ever sees. The flattening
15
+ * happens in the forks and the RANKING happens here: the rule that decides
16
+ * which of five overlapping labels a tap meant must be identical on iOS and on
17
+ * the web, and it is testable only if it is a pure function.
18
+ *
19
+ * ## Every text on the map, not just the shop pins
20
+ *
21
+ * {@link LABEL_SOURCE_LAYERS} covers all six source-layers GoWay's style draws
22
+ * a name from: POIs, places (city → neighbourhood), parks, aerodromes, peaks,
23
+ * street names, water bodies and waterways. A tappable shop beside a dead
24
+ * street name is the inconsistency a user notices first.
25
+ *
26
+ * ## What was measured, rather than assumed
27
+ *
28
+ * Driving headless Chromium against the real GoWay style and real OpenFreeMap
29
+ * tiles over Barcelona:
30
+ *
31
+ * - A query at the EXACT pixel of a POI's anchor returns the dot and nothing
32
+ * else; six pixels away it returns nothing at all. A label glyph is a few
33
+ * pixels tall and `text-anchor: top` puts the text BELOW the anchor, so the
34
+ * thing a user aims at and the point the engine indexes are 12–20 px apart.
35
+ * That is why {@link LABEL_HIT_PAD_PX} exists and why it is not 2.
36
+ * - A padded box of 10 px catches both the dot and its label across the whole
37
+ * band; a tap on empty ground still returns `[]`, so a generous pad does not
38
+ * manufacture hits.
39
+ * - `queryRenderedFeatures` on the symbol layers returned 16 labels where the
40
+ * loaded source held 3185 POI features in the same view. It answers with
41
+ * what the collision system actually PLACED, not with what the tiles
42
+ * contain.
43
+ */
44
+ import { OPENMAPTILES_SOURCE_LAYERS as SL } from '@goway.to/sdk';
45
+ import { isDrawableCoordinate } from './shared';
46
+ /**
47
+ * How far around the tap the engine is asked to look, in px (web) / dp (native).
48
+ *
49
+ * The measurement above says 10 px is the minimum that reaches both a POI's dot
50
+ * and its label. 18 is that plus finger slop, and a generous pad costs nothing
51
+ * because {@link pickLabelFeature} resolves a crowded box by distance — the
52
+ * nearest label still wins, so widening the net changes what a sloppy tap hits
53
+ * and never what an accurate one hits.
54
+ *
55
+ * One number for mouse and touch on purpose. Sniffing the pointer type is
56
+ * unreliable (a browser-synthesised click from a touch is an ordinary
57
+ * `MouseEvent`), and a click 14 px from a label is a click that meant the
58
+ * label: a tap on genuinely empty ground was measured to return nothing at any
59
+ * pad, so the failure this could cause — "I clicked nothing and got something"
60
+ * — has nothing to be caused by.
61
+ */
62
+ export const LABEL_HIT_PAD_PX = 18;
63
+ /**
64
+ * Source-layers a tap may resolve against, in the OpenMapTiles v3 vocabulary.
65
+ *
66
+ * Read from GoWay's tile schema (`@goway.to/sdk`) rather than spelled here: a
67
+ * wrong source-layer name is the failure mode that logs nothing and throws
68
+ * nothing, and the schema is the one place those names are recorded.
69
+ *
70
+ * This is every layer GoWay's style puts a NAME on. `transportation` is absent
71
+ * although the style draws a symbol from it — that symbol is `road-oneway`, an
72
+ * arrow, and an arrow is not a label. `building` and `housenumber` are absent
73
+ * because a house number is an address fragment, not a thing with a name.
74
+ */
75
+ export const LABEL_SOURCE_LAYERS = [
76
+ SL.poi,
77
+ SL.place,
78
+ SL.park,
79
+ SL.aerodromeLabel,
80
+ SL.mountainPeak,
81
+ SL.transportationName,
82
+ SL.waterName,
83
+ SL.waterway,
84
+ ];
85
+ const LABEL_SOURCE_LAYER_SET = new Set(LABEL_SOURCE_LAYERS);
86
+ /**
87
+ * The name the basemap is DRAWING, or `null`.
88
+ *
89
+ * Exactly `coalesce(name:latin, name)` — the `text-field` expression of
90
+ * GoWay's styles. Not `name_en`, not `name_int`: a label that says
91
+ * one thing on screen and another in the sheet is a worse bug than no sheet,
92
+ * and the user tapped what they could read.
93
+ */
94
+ export function labelNameOf(properties) {
95
+ const latin = properties['name:latin'];
96
+ if (typeof latin === 'string' && latin.trim())
97
+ return latin.trim();
98
+ const name = properties.name;
99
+ if (typeof name === 'string' && name.trim())
100
+ return name.trim();
101
+ return null;
102
+ }
103
+ /**
104
+ * What kind of named thing this is, decided by SOURCE-LAYER rather than by
105
+ * guesswork over the name.
106
+ *
107
+ * Four kinds, because four is what the product acts on differently — see
108
+ * `MapLabelKind` in `types.ts` for what each one means and what a tap on it
109
+ * does.
110
+ */
111
+ export function labelKindOf(sourceLayer) {
112
+ switch (sourceLayer) {
113
+ case SL.poi:
114
+ case SL.aerodromeLabel:
115
+ return 'poi';
116
+ case SL.place:
117
+ case SL.park:
118
+ case SL.mountainPeak:
119
+ return 'area';
120
+ case SL.transportationName:
121
+ return 'road';
122
+ case SL.waterName:
123
+ case SL.waterway:
124
+ return 'water';
125
+ default:
126
+ return null;
127
+ }
128
+ }
129
+ /** Whether a source-layer can answer a tap at all. */
130
+ export function isLabelSourceLayer(sourceLayer) {
131
+ return LABEL_SOURCE_LAYER_SET.has(sourceLayer);
132
+ }
133
+ /**
134
+ * One queried hit as a GoWay label, or `null` when it is not one.
135
+ *
136
+ * `fallback` is where the user tapped. It is used when the feature has no
137
+ * anchor of its own — a label placed over a POLYGON, such as a park, whose name
138
+ * sits at the pole of inaccessibility that exists only inside MapLibre's
139
+ * placement and not in the tile. The tap is inside the polygon by construction,
140
+ * so it is a truthful point for that thing.
141
+ *
142
+ * Anything derived from a queried geometry goes through `isDrawableCoordinate`
143
+ * for the reason the whole seam exists — a `NaN` reaching MapLibre is a THROW
144
+ * out of the effect that positions things, not a misplaced pin (see
145
+ * `shared.ts`).
146
+ */
147
+ export function toLabelFeature(candidate, fallback, anchor) {
148
+ const kind = labelKindOf(candidate.sourceLayer);
149
+ if (!kind)
150
+ return null;
151
+ const name = labelNameOf(candidate.properties);
152
+ if (!name)
153
+ return null;
154
+ const own = anchor ?? candidate.coordinate;
155
+ const anchored = isDrawableCoordinate(own);
156
+ const coordinate = anchored ? own : fallback;
157
+ if (!isDrawableCoordinate(coordinate))
158
+ return null;
159
+ const category = stringProp(candidate.properties, 'class');
160
+ const subcategory = stringProp(candidate.properties, 'subclass');
161
+ const rank = numberProp(candidate.properties, 'rank');
162
+ return {
163
+ // The tiles' feature id where there is one, and a coordinate-derived id
164
+ // where there is not, so a label is always keyable. It is NOT a GoWay Place
165
+ // ID and cannot be joined to one — see the note in `types.ts`.
166
+ id: candidate.featureId == null
167
+ ? `${candidate.sourceLayer}:${coordinate.longitude.toFixed(6)},${coordinate.latitude.toFixed(6)}`
168
+ : `${candidate.sourceLayer}:${String(candidate.featureId)}`,
169
+ name,
170
+ kind,
171
+ coordinate,
172
+ anchored,
173
+ ...(category ? { category } : {}),
174
+ ...(subcategory ? { subcategory } : {}),
175
+ ...(rank === null ? {} : { rank }),
176
+ };
177
+ }
178
+ /**
179
+ * Every hit in the box, as GoWay labels, each anchored where it really is.
180
+ *
181
+ * Split from the ranking below because the two halves need different things and
182
+ * one platform cannot do them together. Anchoring is PURE GEOMETRY — no
183
+ * projection — which is what lets the native fork do it synchronously for every
184
+ * candidate and then pay for exactly one projection each, instead of one bridge
185
+ * call per vertex of every street in the box.
186
+ */
187
+ export function labelCandidatesOf(candidates, tapCoordinate) {
188
+ const out = [];
189
+ for (const candidate of candidates) {
190
+ const label = toLabelFeature(candidate, tapCoordinate, anchorFor(candidate, tapCoordinate));
191
+ if (label)
192
+ out.push(label);
193
+ }
194
+ return out;
195
+ }
196
+ /**
197
+ * The label a tap meant, out of everything inside the padded box.
198
+ *
199
+ * ## The rule, written down rather than left to the renderer's return order
200
+ *
201
+ * 1. **Nearest wins.** Distance in SCREEN PIXELS from the tap to the label's
202
+ * own anchor — a point for a POI or a city, and for a street or a river the
203
+ * nearest point on the line, which is a real place on that way rather than a
204
+ * vertex that may be half a tile away. Pixels, not degrees: a degree is a
205
+ * different distance at every latitude, and the user is pointing at pixels.
206
+ * 2. **A label with no anchor of its own is ranked as if it were at the edge of
207
+ * the pad.** A park's name is placed by MapLibre at a pole of inaccessibility
208
+ * that does not exist in the tile, so its only honest position is "somewhere
209
+ * in this box". Scoring it at zero would make it beat everything; scoring it
210
+ * at {@link LABEL_HIT_PAD_PX} makes it lose to anything genuinely nearer and
211
+ * win over nothing.
212
+ * 3. **Then the kind**, most specific first: a POI you can walk into, then the
213
+ * street it is on, then water, then the district or city containing all of
214
+ * them. A city label spans the screen and is never what a tap meant while
215
+ * something smaller is under the finger.
216
+ * 4. **Then the tiles' own `rank`**, OpenMapTiles' importance ordering.
217
+ * 5. **Then the id**, so the answer is deterministic rather than dependent on
218
+ * the order a renderer happened to return its layers in.
219
+ */
220
+ export function pickLabelFeature(labels, point, project) {
221
+ let best = null;
222
+ let bestScore = null;
223
+ for (const label of labels) {
224
+ const projected = label.anchored ? project(label.coordinate) : null;
225
+ const distance = projected && Number.isFinite(projected.x) && Number.isFinite(projected.y)
226
+ ? Math.hypot(projected.x - point.x, projected.y - point.y)
227
+ : // Rule 2: no anchor of its own, so "somewhere in the box".
228
+ LABEL_HIT_PAD_PX;
229
+ const score = [
230
+ distance,
231
+ KIND_ORDER[label.kind],
232
+ label.rank ?? Number.MAX_SAFE_INTEGER,
233
+ label.id,
234
+ ];
235
+ if (bestScore === null || compareScores(score, bestScore) < 0) {
236
+ best = label;
237
+ bestScore = score;
238
+ }
239
+ }
240
+ return best;
241
+ }
242
+ /** Most specific first. See rule 3. */
243
+ const KIND_ORDER = { poi: 0, road: 1, water: 2, area: 3 };
244
+ function compareScores(a, b) {
245
+ if (a[0] !== b[0])
246
+ return a[0] - b[0];
247
+ if (a[1] !== b[1])
248
+ return a[1] - b[1];
249
+ if (a[2] !== b[2])
250
+ return a[2] - b[2];
251
+ return a[3] < b[3] ? -1 : a[3] > b[3] ? 1 : 0;
252
+ }
253
+ /**
254
+ * Where a line-drawn label should be considered to BE: the point on the way
255
+ * nearest the tap.
256
+ *
257
+ * Not the nearest VERTEX, which on a long straight avenue can be far from a tap
258
+ * that landed squarely on the street. The result is a real point on the way,
259
+ * which is what a pin should sit on and what "directions to this street" needs.
260
+ *
261
+ * ## Why this is flat geometry rather than a projection
262
+ *
263
+ * The search runs in a local planar frame — longitude scaled by `cos(latitude)`
264
+ * — instead of in screen pixels. Over one tile at label zooms the difference is
265
+ * immaterial, and it buys the thing that matters: no projection, so this is
266
+ * pure, testable, and cheap enough for the NATIVE fork, where every projection
267
+ * is a bridge call and projecting every vertex of every street in the box would
268
+ * not be affordable. Both forks therefore anchor identically.
269
+ *
270
+ * Returns `null` for anything that is not a usable line, so the caller falls
271
+ * back to the feature's own point and then to the tap.
272
+ */
273
+ export function anchorFor(candidate, tap) {
274
+ const path = candidate.path;
275
+ if (!path || path.length === 0)
276
+ return null;
277
+ // One scale for the whole search, taken at the tap: using each vertex's own
278
+ // latitude would make the metric inconsistent between segments.
279
+ const scale = Math.cos((tap.latitude * Math.PI) / 180) || 1;
280
+ const flat = (at) => ({ x: at.longitude * scale, y: at.latitude });
281
+ const here = flat(tap);
282
+ let best = null;
283
+ let bestDistance = Number.POSITIVE_INFINITY;
284
+ let previous = null;
285
+ for (const vertex of path) {
286
+ if (!isDrawableCoordinate(vertex)) {
287
+ previous = null;
288
+ continue;
289
+ }
290
+ if (previous) {
291
+ const hit = nearestOnSegment(previous, vertex, here, flat);
292
+ if (hit.distance < bestDistance) {
293
+ bestDistance = hit.distance;
294
+ best = hit.at;
295
+ }
296
+ }
297
+ else {
298
+ const at = flat(vertex);
299
+ const distance = Math.hypot(at.x - here.x, at.y - here.y);
300
+ if (distance < bestDistance) {
301
+ bestDistance = distance;
302
+ best = vertex;
303
+ }
304
+ }
305
+ previous = vertex;
306
+ }
307
+ return best;
308
+ }
309
+ /** The point on one segment nearest the tap, in the local planar frame. */
310
+ function nearestOnSegment(a, b, here, flat) {
311
+ const pa = flat(a);
312
+ const pb = flat(b);
313
+ const dx = pb.x - pa.x;
314
+ const dy = pb.y - pa.y;
315
+ const lengthSquared = dx * dx + dy * dy;
316
+ if (lengthSquared === 0) {
317
+ return { at: a, distance: Math.hypot(pa.x - here.x, pa.y - here.y) };
318
+ }
319
+ const raw = ((here.x - pa.x) * dx + (here.y - pa.y) * dy) / lengthSquared;
320
+ const t = raw < 0 ? 0 : raw > 1 ? 1 : raw;
321
+ return {
322
+ at: {
323
+ latitude: a.latitude + t * (b.latitude - a.latitude),
324
+ longitude: a.longitude + t * (b.longitude - a.longitude),
325
+ },
326
+ distance: Math.hypot(pa.x + t * dx - here.x, pa.y + t * dy - here.y),
327
+ };
328
+ }
329
+ /**
330
+ * The labels currently on screen, deduplicated and capped.
331
+ *
332
+ * Both forks report their viewport's labels so the overlay can avoid stacking
333
+ * its own text on top of one. Two things make
334
+ * that safe to do on every settled frame:
335
+ *
336
+ * - **Dedup by id.** One POI is drawn by a dot layer and a label layer, and a
337
+ * feature that straddles a tile boundary is returned once per tile.
338
+ * - **A cap.** A wide, dense viewport is bounded by the collision system to
339
+ * tens of labels in practice (44 over central Barcelona at z16, measured),
340
+ * but "in practice" is not a bound, and this list crosses into React state.
341
+ */
342
+ export function collectLabelFeatures(candidates, fallback, limit = 400) {
343
+ const byId = new Map();
344
+ for (const candidate of candidates) {
345
+ if (byId.size >= limit)
346
+ break;
347
+ const label = toLabelFeature(candidate, fallback);
348
+ if (!label || byId.has(label.id))
349
+ continue;
350
+ byId.set(label.id, label);
351
+ }
352
+ return [...byId.values()];
353
+ }
354
+ /**
355
+ * Whether two reported label sets are the same set.
356
+ *
357
+ * The canvas re-reports on every settled frame, and a fresh array of equal
358
+ * labels would rebuild every marker on the map for no reason. Ids only: a
359
+ * label's name and class come from the same tile feature, so an id that has
360
+ * not changed describes a label that has not changed.
361
+ */
362
+ export function sameLabelSet(a, b) {
363
+ if (a === b)
364
+ return true;
365
+ if (a.length !== b.length)
366
+ return false;
367
+ for (let index = 0; index < a.length; index += 1) {
368
+ if (a[index].id !== b[index].id)
369
+ return false;
370
+ }
371
+ return true;
372
+ }
373
+ function stringProp(properties, key) {
374
+ const value = properties[key];
375
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
376
+ }
377
+ function numberProp(properties, key) {
378
+ const value = properties[key];
379
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
380
+ }
381
+ /**
382
+ * The engine's features, with the engine removed. A feature this cannot
383
+ * classify is dropped rather than guessed at: the source-layer decides whether
384
+ * a hit is a POI or a district, and being wrong opens the wrong sheet.
385
+ */
386
+ export function queriedLabelsOf(features) {
387
+ const out = [];
388
+ for (const feature of features) {
389
+ const properties = feature.properties ?? {};
390
+ const sourceLayer = sourceLayerOf(feature, properties);
391
+ if (!sourceLayer || !isLabelSourceLayer(sourceLayer))
392
+ continue;
393
+ const geometry = feature.geometry;
394
+ // A Point becomes a coordinate; a line becomes a PATH, so a street or a
395
+ // river is anchored at the point on the way the reader pointed at. A
396
+ // polygon (a park) gets neither: its label is placed at a point only the
397
+ // engine knows, and the tap stands in for it — the tap is inside it anyway.
398
+ const coordinate = geometry?.type === 'Point'
399
+ ? {
400
+ longitude: geometry.coordinates[0] ?? Number.NaN,
401
+ latitude: geometry.coordinates[1] ?? Number.NaN,
402
+ }
403
+ : null;
404
+ out.push({
405
+ featureId: feature.id ?? null,
406
+ sourceLayer,
407
+ layerId: feature.layer?.id,
408
+ properties,
409
+ coordinate,
410
+ path: pathOf(geometry),
411
+ });
412
+ }
413
+ return out;
414
+ }
415
+ function sourceLayerOf(feature, properties) {
416
+ if (typeof feature.sourceLayer === 'string' && feature.sourceLayer)
417
+ return feature.sourceLayer;
418
+ for (const key of ['source-layer', 'sourceLayer', 'layer']) {
419
+ const value = properties[key];
420
+ if (typeof value === 'string' && value)
421
+ return value;
422
+ }
423
+ return null;
424
+ }
425
+ /**
426
+ * A line feature's vertices, capped: a motorway clipped to a tile can carry a
427
+ * great many points and this runs inside a press handler, while the query box
428
+ * is a few dozen pixels across, so the part of the line the reader could have
429
+ * meant is a handful of segments.
430
+ */
431
+ const MAX_PATH_VERTICES = 512;
432
+ function pathOf(geometry) {
433
+ if (!geometry)
434
+ return null;
435
+ const lines = geometry.type === 'LineString'
436
+ ? [geometry.coordinates]
437
+ : geometry.type === 'MultiLineString'
438
+ ? geometry.coordinates
439
+ : null;
440
+ if (!lines)
441
+ return null;
442
+ const out = [];
443
+ for (const line of lines) {
444
+ for (const position of line) {
445
+ if (out.length >= MAX_PATH_VERTICES)
446
+ return out;
447
+ out.push({ longitude: position[0] ?? Number.NaN, latitude: position[1] ?? Number.NaN });
448
+ }
449
+ }
450
+ return out.length > 0 ? out : null;
451
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A map that has loaded has a settled camera, and the canvas reports it.
3
+ *
4
+ * Both forks report the camera through `MapViewProps.onCameraChanged` when
5
+ * a move SETTLES: MapLibre's `moveend` on the web, `onRegionDidChange` on
6
+ * native. The camera a map OPENS at is not a move: the web engine sets it with
7
+ * the constructor's own `jumpTo`, which fires `moveend` inside `new Map()`,
8
+ * before any listener exists, and MapLibre Native gives no promise either way.
9
+ * So for a long time nothing reported it, and every caller that needed it got
10
+ * it by accident or wrote a workaround. The explore list got it by accident:
11
+ * its first query waits for the first settled camera (`useExplore`), and that
12
+ * arrived only when something RESIZED the map container (MapLibre's `resize()`
13
+ * fires a `moveend`). A window that never resized (headless Chromium, and any
14
+ * browser whose layout settled before the map was built) showed the list's
15
+ * skeletons until the reader panned.
16
+ *
17
+ * The rule, the same for both forks: ONCE PER ENGINE, when it has loaded, its
18
+ * camera is reported as a settled (`isFinal`) change, `programmatic` because
19
+ * the app opened the map there and the reader did nothing — so it can never
20
+ * arm "Search this area". Unless the camera is not settled:
21
+ *
22
+ * - a move is under way (an animation, a gesture): that move's own settle
23
+ * reports the camera when it gets there;
24
+ * - the camera stands on a frame of a caller-driven animation
25
+ * (`MapCameraOptions.transient`): a flyover frame is not a settled camera,
26
+ * and the animation's closing, ordinary move reports one;
27
+ * - a move started or ended while the camera was being read (native reads it
28
+ * asynchronously): the reading may be stale, and that move reports anyway.
29
+ *
30
+ * "Once per engine" is what makes the forks agree. The web builds a new engine
31
+ * for a new style (at the opening camera), so a new style is a new report;
32
+ * MapLibre Native swaps the style under the same engine and camera, so its
33
+ * `onDidFinishLoadingStyle` for a new style is not. A retry on native remounts
34
+ * the engine and starts a new reporter.
35
+ */
36
+ import type { GeoBoundingBox, MapCameraChange, ResolvedMapViewport } from './types';
37
+ /** What a fork reads off its engine for a camera report. */
38
+ export interface CameraReading {
39
+ viewport: ResolvedMapViewport;
40
+ bounds: GeoBoundingBox;
41
+ }
42
+ /** Whether the camera is somewhere it will not stay. */
43
+ export interface CameraMotion {
44
+ /** A move (animation or gesture) is under way. */
45
+ moving: boolean;
46
+ /** The last move to end was a frame of a caller-driven animation. */
47
+ transient: boolean;
48
+ }
49
+ /** The report a just-loaded map owes its caller, or `null` while the camera is not settled. */
50
+ export declare function loadedCameraChange(reading: CameraReading, motion: CameraMotion): MapCameraChange | null;
51
+ /** One engine's loaded-camera report: fed its moves, asked once it has loaded. */
52
+ export interface LoadedCameraReporter {
53
+ /** A move began (web `movestart`, native `onRegionWillChange`). */
54
+ moveStarted(): void;
55
+ /** A move ended; `transient` when it was a frame of a caller-driven animation. */
56
+ moveEnded(transient: boolean): void;
57
+ /**
58
+ * The engine has loaded. `read` answers its camera, now or later; the
59
+ * report goes to whatever `listener()` answers then (`null`: no caller).
60
+ * Only the first call per reporter does anything.
61
+ */
62
+ loaded(read: () => CameraReading | null | Promise<CameraReading | null>, listener: () => ((change: MapCameraChange) => void) | null | undefined): void;
63
+ }
64
+ export declare function createLoadedCameraReporter(): LoadedCameraReporter;
@@ -0,0 +1,56 @@
1
+ /** The report a just-loaded map owes its caller, or `null` while the camera is not settled. */
2
+ export function loadedCameraChange(reading, motion) {
3
+ if (motion.moving || motion.transient)
4
+ return null;
5
+ return {
6
+ viewport: reading.viewport,
7
+ bounds: reading.bounds,
8
+ source: 'programmatic',
9
+ isFinal: true,
10
+ };
11
+ }
12
+ export function createLoadedCameraReporter() {
13
+ let moving = false;
14
+ let transient = false;
15
+ // Bumped by every move boundary, so an asynchronous read can tell that the
16
+ // camera it is holding may no longer be the one on screen.
17
+ let generation = 0;
18
+ let loaded = false;
19
+ return {
20
+ moveStarted() {
21
+ moving = true;
22
+ generation += 1;
23
+ },
24
+ moveEnded(wasTransient) {
25
+ moving = false;
26
+ transient = wasTransient;
27
+ generation += 1;
28
+ },
29
+ loaded(read, listener) {
30
+ if (loaded)
31
+ return;
32
+ loaded = true;
33
+ if (!listener())
34
+ return;
35
+ const readAt = generation;
36
+ const finish = (reading) => {
37
+ if (!reading || generation !== readAt)
38
+ return;
39
+ const change = loadedCameraChange(reading, { moving, transient });
40
+ const report = listener();
41
+ if (change && report)
42
+ report(change);
43
+ };
44
+ const reading = read();
45
+ if (reading instanceof Promise) {
46
+ reading.then(finish, () => {
47
+ // An engine that cannot answer has no camera to report; the next
48
+ // settled move will.
49
+ });
50
+ }
51
+ else {
52
+ finish(reading);
53
+ }
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,16 @@
1
+ import type { GeoCoordinate } from './types';
2
+ export type PermissionState = 'unknown' | 'granted' | 'denied';
3
+ type Listener = () => void;
4
+ /** The one permission store; the platform's permission is one per app. */
5
+ export declare const locationPermission: {
6
+ subscribe(listener: Listener): () => void;
7
+ getSnapshot(): PermissionState;
8
+ /** What `useUserLocation` learned by asking. */
9
+ settle: (next: PermissionState) => void;
10
+ };
11
+ /** The reader's position while a `<UserLocation>` is drawn, in memory only. */
12
+ export declare const userPosition: {
13
+ subscribe(listener: Listener): () => void;
14
+ getSnapshot(): GeoCoordinate | null;
15
+ };
16
+ export {};