@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +676 -0
- package/NOTICE +25 -0
- package/README.md +59 -2
- package/app.plugin.js +19 -0
- package/dist/CameraBinding.web.d.ts +24 -0
- package/dist/CameraBinding.web.js +46 -0
- package/dist/DefaultMapMarker.d.ts +8 -0
- package/dist/DefaultMapMarker.js +30 -0
- package/dist/GeoJSONSource.d.ts +2 -0
- package/dist/GeoJSONSource.js +17 -0
- package/dist/GeoJSONSource.web.d.ts +2 -0
- package/dist/GeoJSONSource.web.js +68 -0
- package/dist/InteractionBinding.web.d.ts +7 -0
- package/dist/InteractionBinding.web.js +35 -0
- package/dist/Layer.d.ts +2 -0
- package/dist/Layer.js +21 -0
- package/dist/Layer.web.d.ts +2 -0
- package/dist/Layer.web.js +46 -0
- package/dist/LayerStyle.web.d.ts +10 -0
- package/dist/LayerStyle.web.js +45 -0
- package/dist/LightBinding.web.d.ts +5 -0
- package/dist/LightBinding.web.js +19 -0
- package/dist/MapAttribution.d.ts +27 -0
- package/dist/MapAttribution.js +149 -0
- package/dist/MapBrand.d.ts +22 -0
- package/dist/MapBrand.js +91 -0
- package/dist/MapErrorState.d.ts +9 -0
- package/dist/MapErrorState.js +58 -0
- package/dist/MapPlugin.d.ts +6 -0
- package/dist/MapPlugin.js +3 -0
- package/dist/MapPlugin.web.d.ts +2 -0
- package/dist/MapPlugin.web.js +43 -0
- package/dist/MapView.d.ts +2 -0
- package/dist/MapView.js +420 -0
- package/dist/MapView.web.d.ts +5 -0
- package/dist/MapView.web.js +413 -0
- package/dist/Marker.d.ts +13 -0
- package/dist/Marker.js +11 -0
- package/dist/Marker.web.d.ts +15 -0
- package/dist/Marker.web.js +51 -0
- package/dist/Markers.d.ts +2 -0
- package/dist/Markers.js +13 -0
- package/dist/Markers.web.d.ts +2 -0
- package/dist/Markers.web.js +13 -0
- package/dist/PluginProps.web.d.ts +7 -0
- package/dist/PluginProps.web.js +19 -0
- package/dist/RasterDEMSource.d.ts +2 -0
- package/dist/RasterDEMSource.js +7 -0
- package/dist/RasterDEMSource.web.d.ts +2 -0
- package/dist/RasterDEMSource.web.js +44 -0
- package/dist/SourceData.web.d.ts +6 -0
- package/dist/SourceData.web.js +14 -0
- package/dist/UserLocation.d.ts +2 -0
- package/dist/UserLocation.js +19 -0
- package/dist/UserLocation.web.d.ts +2 -0
- package/dist/UserLocation.web.js +21 -0
- package/dist/brand/BrandedChrome.d.ts +47 -0
- package/dist/brand/BrandedChrome.js +51 -0
- package/dist/brand/GowayLogo.d.ts +28 -0
- package/dist/brand/GowayLogo.js +50 -0
- package/dist/brand/artwork.d.ts +130 -0
- package/dist/brand/artwork.js +176 -0
- package/dist/cameraReports.web.d.ts +50 -0
- package/dist/cameraReports.web.js +117 -0
- package/dist/cameraTarget.d.ts +14 -0
- package/dist/cameraTarget.js +8 -0
- package/dist/chromeLift.d.ts +15 -0
- package/dist/chromeLift.js +15 -0
- package/dist/clusters.d.ts +57 -0
- package/dist/clusters.js +102 -0
- package/dist/commands.web.d.ts +16 -0
- package/dist/commands.web.js +158 -0
- package/dist/compactCredit.d.ts +52 -0
- package/dist/compactCredit.js +40 -0
- package/dist/creditHeight.d.ts +3 -0
- package/dist/creditHeight.js +12 -0
- package/dist/creditLabels.d.ts +30 -0
- package/dist/creditLabels.js +30 -0
- package/dist/creditRise.d.ts +28 -0
- package/dist/creditRise.js +33 -0
- package/dist/dragAxes.web.d.ts +65 -0
- package/dist/dragAxes.web.js +70 -0
- package/dist/engineContext.web.d.ts +13 -0
- package/dist/engineContext.web.js +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/index.web.d.ts +13 -0
- package/dist/index.web.js +13 -0
- package/dist/labels.d.ts +202 -0
- package/dist/labels.js +451 -0
- package/dist/loadedCamera.d.ts +64 -0
- package/dist/loadedCamera.js +56 -0
- package/dist/locationStore.d.ts +16 -0
- package/dist/locationStore.js +132 -0
- package/dist/mapRef.d.ts +4 -0
- package/dist/mapRef.js +13 -0
- package/dist/moveMarkers.d.ts +27 -0
- package/dist/moveMarkers.js +22 -0
- package/dist/nativeCameraReports.d.ts +44 -0
- package/dist/nativeCameraReports.js +73 -0
- package/dist/plugin.d.ts +63 -0
- package/dist/plugin.js +2 -0
- package/dist/queryLayers.d.ts +19 -0
- package/dist/queryLayers.js +54 -0
- package/dist/shared-exports.d.ts +17 -0
- package/dist/shared-exports.js +13 -0
- package/dist/shared.d.ts +170 -0
- package/dist/shared.js +337 -0
- package/dist/sourceContext.d.ts +2 -0
- package/dist/sourceContext.js +9 -0
- package/dist/styleUrl.d.ts +19 -0
- package/dist/styleUrl.js +22 -0
- package/dist/testing.d.ts +41 -0
- package/dist/testing.js +80 -0
- package/dist/types.d.ts +348 -0
- package/dist/types.js +1 -0
- package/dist/useMarkerSlots.d.ts +6 -0
- package/dist/useMarkerSlots.js +56 -0
- package/dist/useUserLocation.d.ts +44 -0
- package/dist/useUserLocation.js +247 -0
- package/dist/zoom.d.ts +2 -0
- package/dist/zoom.js +10 -0
- package/metro.cjs +70 -0
- package/package.json +115 -4
package/dist/shared.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation helpers shared by the two engine forks.
|
|
3
|
+
*
|
|
4
|
+
* This is the ONLY place GoWay's `{ latitude, longitude }` becomes MapLibre's
|
|
5
|
+
* `[longitude, latitude]` — once, tested by both forks using it, rather than at
|
|
6
|
+
* every call site where a swapped pair renders a plausible map of the wrong
|
|
7
|
+
* place and nothing errors.
|
|
8
|
+
*
|
|
9
|
+
* It is also the gate. `isDrawableCoordinate` / `drawableMarkers` /
|
|
10
|
+
* `isDrawableBounds` / `resolvePadding` / `asFinite` keep a `NaN` from reaching
|
|
11
|
+
* an engine, which is a THROW rather than a bad frame — see the block comment
|
|
12
|
+
* above {@link isDrawableCoordinate}. `isDrawableGeoJSON` covers the one path
|
|
13
|
+
* that fails the OTHER way: raw GeoJSON does not throw, it renders nowhere and
|
|
14
|
+
* says nothing. `optionalScalar` covers the third: a key that is PRESENT and
|
|
15
|
+
* `undefined` is not the same as an absent one, and an engine that defaults the
|
|
16
|
+
* absent one does not default this one.
|
|
17
|
+
*
|
|
18
|
+
* And `runEngineCommand` is the floor under all of it: whatever still gets
|
|
19
|
+
* through, an engine call that throws costs the CALL, not the application.
|
|
20
|
+
*/
|
|
21
|
+
import { unwrappedEast } from '@goway.to/sdk/geo';
|
|
22
|
+
export function toLngLat(coordinate) {
|
|
23
|
+
return [coordinate.longitude, coordinate.latitude];
|
|
24
|
+
}
|
|
25
|
+
export function fromLngLat(position) {
|
|
26
|
+
return { longitude: position[0], latitude: position[1] };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A box in engine order, its east edge carried past 180 when it crosses the
|
|
30
|
+
* antimeridian so both engines frame the strip it describes.
|
|
31
|
+
*/
|
|
32
|
+
export function toBoundsArray(bounds) {
|
|
33
|
+
return [bounds.west, bounds.south, unwrappedEast(bounds), bounds.north];
|
|
34
|
+
}
|
|
35
|
+
/** Every interaction is on unless a caller turns it off. */
|
|
36
|
+
export function resolveInteraction(options) {
|
|
37
|
+
return {
|
|
38
|
+
pan: options?.pan ?? true,
|
|
39
|
+
zoom: options?.zoom ?? true,
|
|
40
|
+
rotate: options?.rotate ?? true,
|
|
41
|
+
pitch: options?.pitch ?? true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Normalise a `moveTo` target: a bare coordinate leaves the camera's zoom alone.
|
|
46
|
+
*
|
|
47
|
+
* `Number.isFinite`, not `typeof === 'number'` — `typeof NaN` IS `'number'`, so
|
|
48
|
+
* the old test accepted `{ latitude, longitude, zoom: NaN }` as a viewport and
|
|
49
|
+
* passed that `NaN` on to the engine. See {@link asFinite} for what that costs.
|
|
50
|
+
* A viewport whose zoom is not a number is treated as the bare coordinate it
|
|
51
|
+
* usefully is.
|
|
52
|
+
*/
|
|
53
|
+
export function isViewport(target) {
|
|
54
|
+
return Number.isFinite(target.zoom);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A camera scalar the engine can use, or `undefined`.
|
|
58
|
+
*
|
|
59
|
+
* Zoom, bearing, pitch, duration and maxZoom are the seam's unchecked numbers,
|
|
60
|
+
* and a `NaN` in any of them is not a bad frame — it is the SAME crash a bad
|
|
61
|
+
* coordinate is, one step later. `transform.setZoom(NaN)` survives MapLibre's
|
|
62
|
+
* own `clamp` (every comparison against `NaN` is false), so `_scale` and
|
|
63
|
+
* `worldSize` become `NaN`; the next unprojection — `map.getBounds()`, which
|
|
64
|
+
* this canvas calls on every `move` event, and which `jumpTo` fires
|
|
65
|
+
* SYNCHRONOUSLY — builds a `LngLat` out of two `NaN`s and throws
|
|
66
|
+
* `Invalid LngLat object: (NaN, NaN)` from inside the caller's own React
|
|
67
|
+
* handler. The camera simply not moving is the better failure.
|
|
68
|
+
*/
|
|
69
|
+
export function asFinite(value) {
|
|
70
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A scalar an engine's options object may carry — or NOTHING AT ALL.
|
|
74
|
+
*
|
|
75
|
+
* ## The defect this exists to remove
|
|
76
|
+
*
|
|
77
|
+
* `{ maxZoom: undefined }` and `{}` look interchangeable in TypeScript, and to
|
|
78
|
+
* MapLibre GL JS they are opposites. `Map._cameraForBoxAndBearing` fills in its
|
|
79
|
+
* own defaults with
|
|
80
|
+
*
|
|
81
|
+
* ```
|
|
82
|
+
* options = extend({ padding, offset: [0, 0], maxZoom: this.transform.maxZoom }, options);
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* and `extend` is `for (const k in src) dest[k] = src[k]` — an OWN key wins
|
|
86
|
+
* even when its value is `undefined`. So passing the key at all replaces the
|
|
87
|
+
* engine's maximum zoom with `undefined`, and the very next line is
|
|
88
|
+
*
|
|
89
|
+
* ```
|
|
90
|
+
* const zoom = Math.min(scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom);
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* `Math.min(anything, undefined)` is `NaN`. That `NaN` divides into
|
|
94
|
+
* `offsetAtFinalZoom`, reaches the centre `cameraForBoxAndBearing` unprojects,
|
|
95
|
+
* and `new LngLat(NaN, NaN)` THROWS — from inside the React effect that asked
|
|
96
|
+
* for the fit, which is the error boundary and the whole app. Omitting the key
|
|
97
|
+
* is the only way to mean "use yours".
|
|
98
|
+
*
|
|
99
|
+
* Every optional scalar this seam forwards therefore goes through here, so
|
|
100
|
+
* "the caller did not say" can never arrive as "the caller said nothing".
|
|
101
|
+
*/
|
|
102
|
+
export function optionalScalar(key, value) {
|
|
103
|
+
const finite = asFinite(value);
|
|
104
|
+
return finite === undefined ? {} : { [key]: finite };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Fit padding, expanded to the four-sided form both engines want — or `null`
|
|
108
|
+
* when it is not a number the engine can use.
|
|
109
|
+
*
|
|
110
|
+
* See {@link isDrawableCoordinate} for why a non-finite number is refused here
|
|
111
|
+
* rather than passed on: MapLibre's `cameraForBoxAndBearing` divides the free
|
|
112
|
+
* viewport by the padding, and `NaN` propagates all the way to the LngLat it
|
|
113
|
+
* constructs for the new centre, which throws. A NEGATIVE padding does not
|
|
114
|
+
* throw, but it is never what a caller meant (it frames content OUTSIDE the
|
|
115
|
+
* viewport), and a negative pair is one of the ways the free space lands on
|
|
116
|
+
* exactly zero, which does throw. So negatives clamp to zero and non-finite
|
|
117
|
+
* values refuse the fit outright.
|
|
118
|
+
*/
|
|
119
|
+
export function resolvePadding(padding, fallback) {
|
|
120
|
+
const raw = padding == null
|
|
121
|
+
? { top: fallback, right: fallback, bottom: fallback, left: fallback }
|
|
122
|
+
: typeof padding === 'number'
|
|
123
|
+
? { top: padding, right: padding, bottom: padding, left: padding }
|
|
124
|
+
: padding;
|
|
125
|
+
if (!Number.isFinite(raw.top) ||
|
|
126
|
+
!Number.isFinite(raw.right) ||
|
|
127
|
+
!Number.isFinite(raw.bottom) ||
|
|
128
|
+
!Number.isFinite(raw.left)) {
|
|
129
|
+
reportMapDefect('padding:non-finite', `Refusing a fit: padding is not finite (${describeNumbers(raw)}).`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
top: Math.max(0, raw.top),
|
|
134
|
+
right: Math.max(0, raw.right),
|
|
135
|
+
bottom: Math.max(0, raw.bottom),
|
|
136
|
+
left: Math.max(0, raw.left),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// The garbage gate
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/**
|
|
143
|
+
* Both MapLibre engines build a `LngLat` out of whatever they are handed, and
|
|
144
|
+
* `LngLat` THROWS on a non-number — `Invalid LngLat object: (NaN, NaN)`. Thrown
|
|
145
|
+
* from a React effect (setting a marker's position, framing a route) that
|
|
146
|
+
* reaches the nearest error boundary, so one bad pin takes the whole screen
|
|
147
|
+
* down and the user sees "Something went wrong" instead of a map.
|
|
148
|
+
*
|
|
149
|
+
* `@goway.to/maps` is the single seam every coordinate crosses on its way to
|
|
150
|
+
* an engine, so it is where that stops being possible. A marker whose
|
|
151
|
+
* coordinate is not drawable is DROPPED (a map missing one pin is a degraded
|
|
152
|
+
* map; a map that throws is no map) and a camera move whose target is not
|
|
153
|
+
* drawable is a no-op.
|
|
154
|
+
*
|
|
155
|
+
* ## Why this is not `isValidCoordinate`
|
|
156
|
+
*
|
|
157
|
+
* `@goway.to/sdk/geo`'s `isValidCoordinate` is the PRODUCT's rule — a real point
|
|
158
|
+
* on the earth, longitude within ±180. This is the ENGINE's rule, which is
|
|
159
|
+
* looser on longitude on purpose: MapLibre reports unwrapped longitudes across
|
|
160
|
+
* the antimeridian (a click at 181°E comes back as 181, not −179), and feeding
|
|
161
|
+
* one of those back in is legal and does the right thing. Rejecting it here
|
|
162
|
+
* would break panning past the date line in the name of safety. Latitude is
|
|
163
|
+
* bounded, because `LngLat` throws outside ±90 too.
|
|
164
|
+
*/
|
|
165
|
+
export function isDrawableCoordinate(value) {
|
|
166
|
+
if (!value || typeof value !== 'object')
|
|
167
|
+
return false;
|
|
168
|
+
const { latitude, longitude } = value;
|
|
169
|
+
return (Number.isFinite(longitude) && Number.isFinite(latitude) && latitude >= -90 && latitude <= 90);
|
|
170
|
+
}
|
|
171
|
+
/** The same rule as {@link isDrawableCoordinate}, applied to a box's corners. */
|
|
172
|
+
export function isDrawableBounds(bounds) {
|
|
173
|
+
if (!bounds)
|
|
174
|
+
return false;
|
|
175
|
+
return (isDrawableCoordinate({ latitude: bounds.south, longitude: bounds.west }) &&
|
|
176
|
+
isDrawableCoordinate({ latitude: bounds.north, longitude: bounds.east }));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The markers an engine may be given, with the undrawable ones removed.
|
|
180
|
+
*
|
|
181
|
+
* Returns the INPUT ARRAY when nothing is wrong, so the common path adds one
|
|
182
|
+
* pass and no allocation, and the forks' effect dependencies do not change
|
|
183
|
+
* identity every render.
|
|
184
|
+
*/
|
|
185
|
+
export function drawableMarkers(markers) {
|
|
186
|
+
if (!markers || markers.length === 0)
|
|
187
|
+
return EMPTY_MARKERS;
|
|
188
|
+
let firstBad = -1;
|
|
189
|
+
for (let index = 0; index < markers.length; index += 1) {
|
|
190
|
+
if (!isDrawableCoordinate(markers[index].coordinate)) {
|
|
191
|
+
firstBad = index;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (firstBad < 0)
|
|
196
|
+
return markers;
|
|
197
|
+
const kept = markers.slice(0, firstBad);
|
|
198
|
+
for (let index = firstBad; index < markers.length; index += 1) {
|
|
199
|
+
const marker = markers[index];
|
|
200
|
+
if (isDrawableCoordinate(marker.coordinate)) {
|
|
201
|
+
kept.push(marker);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
reportMapDefect(`marker:${marker.id}`, `Dropped marker "${marker.id}": coordinate is not drawable (${describeNumbers(marker.coordinate)}).`);
|
|
205
|
+
}
|
|
206
|
+
return kept;
|
|
207
|
+
}
|
|
208
|
+
const EMPTY_MARKERS = [];
|
|
209
|
+
/**
|
|
210
|
+
* Every position in a GeoJSON document is a point the engine can project, and
|
|
211
|
+
* every geometry has enough of them to BE that geometry.
|
|
212
|
+
*
|
|
213
|
+
* The arity check is not pedantry about RFC 7946: a one-point `LineString` and
|
|
214
|
+
* an empty ring are exactly what a mis-sliced route produces, they draw
|
|
215
|
+
* nothing, and they are indistinguishable from "the route did not arrive"
|
|
216
|
+
* unless something says so.
|
|
217
|
+
*
|
|
218
|
+
* A composite geometry needs the same check one level up, for the same reason
|
|
219
|
+
* and one that is easier to miss: `[].every()` is `true`, so an empty
|
|
220
|
+
* `coordinates` array satisfies every per-part rule vacuously. An empty
|
|
221
|
+
* `Polygon` has no ring that can fail `ring()`, and a `MultiPolygon` of `[[]]`
|
|
222
|
+
* has no polygon that can. Both are what a slice that matched nothing
|
|
223
|
+
* produces, both reach the engine, and both draw exactly nothing — so every
|
|
224
|
+
* composite array is required to be non-empty BEFORE its parts are checked.
|
|
225
|
+
*/
|
|
226
|
+
export function isDrawableGeoJSON(data) {
|
|
227
|
+
if (!data || typeof data !== 'object')
|
|
228
|
+
return false;
|
|
229
|
+
switch (data.type) {
|
|
230
|
+
case 'FeatureCollection':
|
|
231
|
+
return Array.isArray(data.features) && data.features.every((f) => isDrawableGeoJSON(f));
|
|
232
|
+
case 'Feature':
|
|
233
|
+
// A null geometry is legal GeoJSON and draws nothing, which for an
|
|
234
|
+
// overlay is the same as not being there.
|
|
235
|
+
return data.geometry != null && isDrawableGeoJSON(data.geometry);
|
|
236
|
+
case 'GeometryCollection':
|
|
237
|
+
return (Array.isArray(data.geometries) &&
|
|
238
|
+
data.geometries.length > 0 &&
|
|
239
|
+
data.geometries.every((g) => isDrawableGeoJSON(g)));
|
|
240
|
+
case 'Point':
|
|
241
|
+
return isDrawablePosition(data.coordinates);
|
|
242
|
+
case 'MultiPoint':
|
|
243
|
+
return ring(data.coordinates, 1);
|
|
244
|
+
case 'LineString':
|
|
245
|
+
return ring(data.coordinates, 2);
|
|
246
|
+
case 'MultiLineString':
|
|
247
|
+
return nonEmpty(data.coordinates) && data.coordinates.every((l) => ring(l, 2));
|
|
248
|
+
case 'Polygon':
|
|
249
|
+
return nonEmpty(data.coordinates) && data.coordinates.every((r) => ring(r, 4));
|
|
250
|
+
case 'MultiPolygon':
|
|
251
|
+
return (nonEmpty(data.coordinates) &&
|
|
252
|
+
data.coordinates.every((p) => nonEmpty(p) && p.every((r) => ring(r, 4))));
|
|
253
|
+
default:
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function ring(positions, minimum) {
|
|
258
|
+
return (Array.isArray(positions) && positions.length >= minimum && positions.every(isDrawablePosition));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* An array with something in it.
|
|
262
|
+
*
|
|
263
|
+
* `[].every()` is `true`, so a composite geometry whose only check is `every`
|
|
264
|
+
* accepts the empty case: `{ type: 'Polygon', coordinates: [] }` has no ring to
|
|
265
|
+
* fail `ring()` and passed every test above. That is the exact silent failure
|
|
266
|
+
* this module exists to catch — a geometry the engine accepts, tiles, and draws
|
|
267
|
+
* nothing for — so the outer array is checked before the inner ones are.
|
|
268
|
+
*/
|
|
269
|
+
function nonEmpty(value) {
|
|
270
|
+
return Array.isArray(value) && value.length > 0;
|
|
271
|
+
}
|
|
272
|
+
/** A GeoJSON position is `[longitude, latitude]`, altitude optional. */
|
|
273
|
+
function isDrawablePosition(position) {
|
|
274
|
+
if (!Array.isArray(position) || position.length < 2)
|
|
275
|
+
return false;
|
|
276
|
+
return isDrawableCoordinate({ longitude: position[0], latitude: position[1] });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Say it ONCE per defect.
|
|
280
|
+
*
|
|
281
|
+
* Markers are re-applied on every camera frame a parent reacts to, so a
|
|
282
|
+
* per-occurrence warning would be thousands of identical lines during a single
|
|
283
|
+
* drag — which is how a real signal becomes noise somebody mutes. The key is
|
|
284
|
+
* the marker id (or the kind of camera defect), so a second, different bad pin
|
|
285
|
+
* is still reported.
|
|
286
|
+
*/
|
|
287
|
+
const reported = new Set();
|
|
288
|
+
export function reportMapDefect(key, message) {
|
|
289
|
+
if (reported.has(key))
|
|
290
|
+
return;
|
|
291
|
+
reported.add(key);
|
|
292
|
+
console.warn(`[goway/maps] ${message}`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Run one engine command, and let a throw cost the COMMAND rather than the app.
|
|
296
|
+
*
|
|
297
|
+
* This is the BACKSTOP, deliberately separate from the checks above. Those
|
|
298
|
+
* refuse the bad values we know about; this one is for the ones we do not. Every
|
|
299
|
+
* guard in this file was written after a crash, which is the evidence that the
|
|
300
|
+
* next non-finite number will arrive from somewhere nobody predicted — a
|
|
301
|
+
* renderer upgrade that starts rejecting an input it used to accept, a new
|
|
302
|
+
* option forwarded through an unexamined path, a device API that changes shape.
|
|
303
|
+
*
|
|
304
|
+
* What makes that catastrophic rather than cosmetic is WHERE these calls run:
|
|
305
|
+
* a camera move issued from a `useEffect` throws inside React's commit phase,
|
|
306
|
+
* so the nearest error boundary unmounts the tree and the user gets "Something
|
|
307
|
+
* went wrong" instead of a map. A camera that did not move is a map. That is
|
|
308
|
+
* the whole trade, and it is not close.
|
|
309
|
+
*
|
|
310
|
+
* It is not a licence to stop validating: a swallowed throw is a `console.warn`
|
|
311
|
+
* nobody reads, so anything we can name is still refused BEFORE it gets here,
|
|
312
|
+
* by a check that says which value was wrong and why.
|
|
313
|
+
*
|
|
314
|
+
* Answers whether the command ran: `false` is a dropped command, which a
|
|
315
|
+
* caller waiting on its outcome (a camera move's settle) must not wait for.
|
|
316
|
+
*/
|
|
317
|
+
export function runEngineCommand(command, run) {
|
|
318
|
+
try {
|
|
319
|
+
run();
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
catch (cause) {
|
|
323
|
+
reportMapDefect(`engine:${command}`, `The map engine threw on "${command}" and the command was dropped: ${String(cause)}`);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `key NaN` — never `JSON.stringify`, which prints `NaN` and `Infinity` as
|
|
329
|
+
* `null` and so erases the one detail the reader needs.
|
|
330
|
+
*/
|
|
331
|
+
export function describeNumbers(value) {
|
|
332
|
+
if (!value || typeof value !== 'object')
|
|
333
|
+
return String(value);
|
|
334
|
+
return Object.entries(value)
|
|
335
|
+
.map(([key, entry]) => `${key} ${String(entry)}`)
|
|
336
|
+
.join(', ');
|
|
337
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The id of the source a `<Layer>` draws: the `<GeoJSONSource>` / `<RasterDEMSource>` around it. */
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
export const SourceIdContext = createContext(null);
|
|
4
|
+
export function useSourceId() {
|
|
5
|
+
const id = useContext(SourceIdContext);
|
|
6
|
+
if (!id)
|
|
7
|
+
throw new Error('A <Layer> must be rendered inside a source (<GeoJSONSource>, <RasterDEMSource>).');
|
|
8
|
+
return id;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which style document a map draws.
|
|
3
|
+
*
|
|
4
|
+
* `goway://styles/<id>` is GoWay's name for a published style, as
|
|
5
|
+
* `mapbox://styles/...` is Mapbox's; `@goway.to/sdk` resolves it to its
|
|
6
|
+
* document on goway.to. A GoWay style that follows the light is drawn in the
|
|
7
|
+
* app's theme — day in light mode, night in dark — unless the screen pins a
|
|
8
|
+
* light. Any other URL is used as it is.
|
|
9
|
+
*/
|
|
10
|
+
import { type MapLightPreset } from '@goway.to/sdk';
|
|
11
|
+
/** The style a `MapView` draws when it is given none: GoWay Standard. */
|
|
12
|
+
export declare const DEFAULT_MAP_STYLE: string;
|
|
13
|
+
/**
|
|
14
|
+
* The document URL for `mapStyle`, in `lightPreset` or the theme's light.
|
|
15
|
+
* Throws `GoWayValidationError` for a `goway://` name that is not a published
|
|
16
|
+
* style — the map reports that as a style failure rather than drawing a
|
|
17
|
+
* different map than the one asked for.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveStyleUrl(mapStyle: string, lightPreset: MapLightPreset | undefined, dark: boolean): string;
|
package/dist/styleUrl.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which style document a map draws.
|
|
3
|
+
*
|
|
4
|
+
* `goway://styles/<id>` is GoWay's name for a published style, as
|
|
5
|
+
* `mapbox://styles/...` is Mapbox's; `@goway.to/sdk` resolves it to its
|
|
6
|
+
* document on goway.to. A GoWay style that follows the light is drawn in the
|
|
7
|
+
* app's theme — day in light mode, night in dark — unless the screen pins a
|
|
8
|
+
* light. Any other URL is used as it is.
|
|
9
|
+
*/
|
|
10
|
+
import { createGoWayClient, gowayStyleUrl } from '@goway.to/sdk';
|
|
11
|
+
/** The style a `MapView` draws when it is given none: GoWay Standard. */
|
|
12
|
+
export const DEFAULT_MAP_STYLE = gowayStyleUrl('standard');
|
|
13
|
+
const links = createGoWayClient().links;
|
|
14
|
+
/**
|
|
15
|
+
* The document URL for `mapStyle`, in `lightPreset` or the theme's light.
|
|
16
|
+
* Throws `GoWayValidationError` for a `goway://` name that is not a published
|
|
17
|
+
* style — the map reports that as a style failure rather than drawing a
|
|
18
|
+
* different map than the one asked for.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveStyleUrl(mapStyle, lightPreset, dark) {
|
|
21
|
+
return links.styleUrl(mapStyle, lightPreset ?? (dark ? 'night' : 'day'));
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MapCameraChange } from './types';
|
|
2
|
+
export { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT } from './cameraReports.web';
|
|
3
|
+
export interface FakeCamera {
|
|
4
|
+
lat: number;
|
|
5
|
+
lng: number;
|
|
6
|
+
zoom: number;
|
|
7
|
+
bearing: number;
|
|
8
|
+
pitch: number;
|
|
9
|
+
/** Half the box's height and width, in degrees: a bigger container, a bigger box. */
|
|
10
|
+
span: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function fakeCameraEngine(start?: Partial<FakeCamera>): {
|
|
13
|
+
camera: FakeCamera;
|
|
14
|
+
fire(type: string, event?: object): void;
|
|
15
|
+
/** A whole camera move, as MapLibre fires it, ending wherever `to` says. */
|
|
16
|
+
move(event: object, to?: Partial<FakeCamera>): void;
|
|
17
|
+
listenerCount: () => number;
|
|
18
|
+
on(type: string, listener: (event: unknown) => void): /*elided*/ any;
|
|
19
|
+
off(type: string, listener: (event: unknown) => void): /*elided*/ any;
|
|
20
|
+
getCenter: () => {
|
|
21
|
+
lat: number;
|
|
22
|
+
lng: number;
|
|
23
|
+
};
|
|
24
|
+
getBounds: () => {
|
|
25
|
+
getWest: () => number;
|
|
26
|
+
getSouth: () => number;
|
|
27
|
+
getEast: () => number;
|
|
28
|
+
getNorth: () => number;
|
|
29
|
+
};
|
|
30
|
+
getZoom: () => number;
|
|
31
|
+
getBearing: () => number;
|
|
32
|
+
getPitch: () => number;
|
|
33
|
+
};
|
|
34
|
+
export type FakeCameraEngine = ReturnType<typeof fakeCameraEngine>;
|
|
35
|
+
/** Attach the real reports to a fake engine; `withListener: false` is the embed. */
|
|
36
|
+
export declare function attachToFake(engine: FakeCameraEngine, withListener?: boolean): {
|
|
37
|
+
changes: MapCameraChange[];
|
|
38
|
+
detach: () => void;
|
|
39
|
+
finals: () => MapCameraChange[];
|
|
40
|
+
advance(ms: number): void;
|
|
41
|
+
};
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@goway.to/maps/testing` — drive the map's REAL camera reports without a GPU.
|
|
3
|
+
*
|
|
4
|
+
* What a screen does with `onCameraChanged` (arm "Search this area", commit a
|
|
5
|
+
* box, pause a tour) is the screen's logic, and the sequences that matter —
|
|
6
|
+
* the opening camera, a programmatic frame, the reader's drag — are the ones a
|
|
7
|
+
* loading map produces. `fakeCameraEngine` is a MapLibre map as far as the
|
|
8
|
+
* reports see it (the events it fires and the camera it reads; the real one
|
|
9
|
+
* needs WebGL), and `attachToFake` runs the SDK's own web reporting on it.
|
|
10
|
+
*
|
|
11
|
+
* const engine = fakeCameraEngine({ lat: 41.39, lng: 2.17, zoom: 11 });
|
|
12
|
+
* const reports = attachToFake(engine);
|
|
13
|
+
* engine.fire('load');
|
|
14
|
+
* engine.move({ originalEvent: {} }, { lng: 2.2 }); // the reader drags
|
|
15
|
+
* engine.move(PROGRAMMATIC, { zoom: 14 }); // the app frames
|
|
16
|
+
*/
|
|
17
|
+
import { attachCameraReports } from './cameraReports.web';
|
|
18
|
+
export { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT } from './cameraReports.web';
|
|
19
|
+
export function fakeCameraEngine(start = {}) {
|
|
20
|
+
const listeners = new Map();
|
|
21
|
+
const camera = {
|
|
22
|
+
lat: 41.4036,
|
|
23
|
+
lng: 2.1744,
|
|
24
|
+
zoom: 16.5,
|
|
25
|
+
bearing: 30,
|
|
26
|
+
pitch: 60,
|
|
27
|
+
span: 0.01,
|
|
28
|
+
...start,
|
|
29
|
+
};
|
|
30
|
+
const engine = {
|
|
31
|
+
camera,
|
|
32
|
+
fire(type, event = {}) {
|
|
33
|
+
for (const listener of listeners.get(type) ?? [])
|
|
34
|
+
listener({ type, ...event });
|
|
35
|
+
},
|
|
36
|
+
/** A whole camera move, as MapLibre fires it, ending wherever `to` says. */
|
|
37
|
+
move(event, to = {}) {
|
|
38
|
+
engine.fire('movestart', event);
|
|
39
|
+
Object.assign(camera, to);
|
|
40
|
+
engine.fire('move', event);
|
|
41
|
+
engine.fire('moveend', event);
|
|
42
|
+
},
|
|
43
|
+
listenerCount: () => [...listeners.values()].reduce((sum, set) => sum + set.size, 0),
|
|
44
|
+
on(type, listener) {
|
|
45
|
+
if (!listeners.has(type))
|
|
46
|
+
listeners.set(type, new Set());
|
|
47
|
+
listeners.get(type)?.add(listener);
|
|
48
|
+
return engine;
|
|
49
|
+
},
|
|
50
|
+
off(type, listener) {
|
|
51
|
+
listeners.get(type)?.delete(listener);
|
|
52
|
+
return engine;
|
|
53
|
+
},
|
|
54
|
+
getCenter: () => ({ lat: camera.lat, lng: camera.lng }),
|
|
55
|
+
getBounds: () => ({
|
|
56
|
+
getWest: () => camera.lng - camera.span,
|
|
57
|
+
getSouth: () => camera.lat - camera.span,
|
|
58
|
+
getEast: () => camera.lng + camera.span,
|
|
59
|
+
getNorth: () => camera.lat + camera.span,
|
|
60
|
+
}),
|
|
61
|
+
getZoom: () => camera.zoom,
|
|
62
|
+
getBearing: () => camera.bearing,
|
|
63
|
+
getPitch: () => camera.pitch,
|
|
64
|
+
};
|
|
65
|
+
return engine;
|
|
66
|
+
}
|
|
67
|
+
/** Attach the real reports to a fake engine; `withListener: false` is the embed. */
|
|
68
|
+
export function attachToFake(engine, withListener = true) {
|
|
69
|
+
const changes = [];
|
|
70
|
+
let clock = 0;
|
|
71
|
+
const detach = attachCameraReports(engine, () => (withListener ? (change) => changes.push(change) : undefined), () => clock);
|
|
72
|
+
return {
|
|
73
|
+
changes,
|
|
74
|
+
detach,
|
|
75
|
+
finals: () => changes.filter((change) => change.isFinal),
|
|
76
|
+
advance(ms) {
|
|
77
|
+
clock += ms;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|