@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
@@ -0,0 +1,413 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * `MapView`, web — `maplibre-gl` against a real `<div>`.
4
+ *
5
+ * The engine is built in the host element's ref callback and torn down in its
6
+ * cleanup (React 19). The host is KEYED by what the engine is built from — the
7
+ * resolved style document, the projection, a retry — so a new style is a new
8
+ * engine at the camera the old one was at, and everything drawn on it (sources,
9
+ * layers, markers, plugins) binds again to the new one. No effect decides when
10
+ * an engine exists.
11
+ *
12
+ * Children are rendered once the style has loaded, inside the engine's
13
+ * context; each binds itself to the engine (`Camera.web.tsx`,
14
+ * `GeoJSONSource.web.tsx`, …), one named binding per component file.
15
+ *
16
+ * maplibre-gl 6 is ESM-only and starts its tile worker BY URL. Inside a Metro
17
+ * bundle `import.meta.url` is not the package directory, so the worker is
18
+ * served from this origin instead — `@goway.to/maps/metro` copies it to
19
+ * {@link WORKER_PATH}, keyed on the installed version so the worker and the
20
+ * main thread can never be different releases.
21
+ */
22
+ import { forwardRef, useCallback, useImperativeHandle, useMemo, useReducer, useRef, useState, } from 'react';
23
+ import { StyleSheet, View } from 'react-native';
24
+ import * as maplibregl from 'maplibre-gl';
25
+ import 'maplibre-gl/dist/maplibre-gl.css';
26
+ import { boundsCenter, isDegenerateBounds } from '@goway.to/sdk/geo';
27
+ import { useTheme } from '@oxy.so/bloom/theme';
28
+ import { CameraBinding } from './CameraBinding.web';
29
+ import { attachCameraReports, isGestureMove } from './cameraReports.web';
30
+ import { createWebMapApi, DEFAULT_FIT_PADDING, DEGENERATE_FIT_ZOOM } from './commands.web';
31
+ import { CREDIT_AUTO_COLLAPSE_MS, creditReducer, INITIAL_CREDIT_STATE } from './compactCredit';
32
+ import { WebEngineContext } from './engineContext.web';
33
+ import { InteractionBinding } from './InteractionBinding.web';
34
+ import { collectLabelFeatures, LABEL_HIT_PAD_PX, labelCandidatesOf, pickLabelFeature, queriedLabelsOf, } from './labels';
35
+ import { LightBinding } from './LightBinding.web';
36
+ import { MapAttribution } from './MapAttribution';
37
+ import { MapBrand } from './MapBrand';
38
+ import { MapErrorState } from './MapErrorState';
39
+ import { MapRefContext } from './mapRef';
40
+ import { NO_QUERY_LAYERS, queryLayersOf } from './queryLayers';
41
+ import { asFinite, isDrawableBounds, isDrawableCoordinate, reportMapDefect, resolvePadding, runEngineCommand, toBoundsArray, toLngLat, } from './shared';
42
+ import { DEFAULT_MAP_STYLE, resolveStyleUrl } from './styleUrl';
43
+ import { ZoomLevelContext } from './zoom';
44
+ /** Where `@goway.to/maps/metro` puts MapLibre's worker modules, on this origin. */
45
+ export const WORKER_PATH = `/vendor/maplibre-gl/${maplibregl.getVersion()}/maplibre-gl-worker.mjs`;
46
+ if (typeof window !== 'undefined') {
47
+ maplibregl.setWorkerUrl(new URL(WORKER_PATH, window.location.origin).href);
48
+ }
49
+ /** The whole inhabited world, before anything is known. */
50
+ const WORLD_VIEW = { latitude: 20, longitude: 0, zoom: 1.5 };
51
+ const DEFAULT_MAX_PITCH = 60;
52
+ const EMPTY_LABELS = [];
53
+ export const MapView = forwardRef(function MapView(props, ref) {
54
+ const { mapStyle = DEFAULT_MAP_STYLE, lightPreset, projection = 'mercator', camera, light, interaction, maxPitch, credits, creditDisclosure, style, testID, children, } = props;
55
+ const theme = useTheme();
56
+ // The callbacks and construction options of THIS render, for listeners
57
+ // bound once per engine: a new inline `onPress` must not rebuild the map.
58
+ const latest = useRef(props);
59
+ latest.current = props;
60
+ let styleUrl = null;
61
+ let styleError = null;
62
+ try {
63
+ styleUrl = resolveStyleUrl(mapStyle, lightPreset, theme.isDark);
64
+ }
65
+ catch (cause) {
66
+ styleError = { reason: 'style', message: String(cause) };
67
+ }
68
+ const [retry, setRetry] = useState(0);
69
+ const [engine, setEngine] = useState(null);
70
+ const [zoomLevel, setZoomLevel] = useState(() => Math.floor(openingZoom(props.initialViewState)));
71
+ const [error, setError] = useState(null);
72
+ const [credit, dispatchCredit] = useReducer(creditReducer, INITIAL_CREDIT_STATE);
73
+ const mapRef = useRef(null);
74
+ // Where the last engine's camera was when it went: a new style or a new
75
+ // projection never moves the map; only a retry opens at the start again.
76
+ const lastCamera = useRef(null);
77
+ // The `camera` target last applied, across engines (`CameraBinding.web.tsx`).
78
+ const appliedCamera = useRef(null);
79
+ const api = useMemo(() => createWebMapApi(() => mapRef.current), []);
80
+ useImperativeHandle(ref, () => api, [api]);
81
+ const report = useCallback((next) => {
82
+ setError(next);
83
+ latest.current.onError?.(next);
84
+ }, []);
85
+ const engineKey = `${styleUrl}|${projection}|${retry}`;
86
+ const attachEngine = useCallback((node) => {
87
+ if (!node || !styleUrl)
88
+ return;
89
+ // react-native-web renders a `<View>` as a `<div>` and its ref IS that
90
+ // node, so MapLibre gets a real container without a portal of its own.
91
+ const container = node;
92
+ const options = latest.current;
93
+ const opening = openingCamera(lastCamera.current ?? options.initialViewState ?? targetView(options.camera));
94
+ let map;
95
+ try {
96
+ map = new maplibregl.Map({
97
+ container,
98
+ // With the globe, the style goes in through `setStyle` with the
99
+ // projection folded into the document, so the first frame is
100
+ // already a globe rather than a flat map that turns into one.
101
+ ...(projection === 'globe' ? {} : { style: styleUrl }),
102
+ ...opening,
103
+ maxPitch: options.maxPitch ?? DEFAULT_MAX_PITCH,
104
+ ...(asFinite(options.pixelRatio) !== undefined ? { pixelRatio: options.pixelRatio } : {}),
105
+ canvasContextAttributes: { antialias: options.antialias === true },
106
+ // The credit is `MapAttribution`, drawn by this component on every
107
+ // map; the engine's own ornament is off.
108
+ attributionControl: false,
109
+ });
110
+ if (projection === 'globe') {
111
+ map.setStyle(styleUrl, {
112
+ transformStyle: (_previous, next) => ({ ...next, projection: { type: 'globe' } }),
113
+ });
114
+ }
115
+ }
116
+ catch (cause) {
117
+ report({ reason: 'runtime', message: String(cause) });
118
+ return;
119
+ }
120
+ mapRef.current = map;
121
+ const markerElements = new Set();
122
+ const plugins = new Set();
123
+ let queryLayers = NO_QUERY_LAYERS;
124
+ let collapse = null;
125
+ // Before `load`, so the camera the map opened at is reported before
126
+ // `onReady` says the map is there (`loadedCamera.ts`).
127
+ const detachReports = attachCameraReports(map, () => latest.current.onCameraChanged);
128
+ const handleLoad = () => {
129
+ queryLayers = queryLayersOf(map.getStyle()?.layers);
130
+ setEngine({ map, markerElements, plugins, container });
131
+ setError((current) => {
132
+ if (current)
133
+ latest.current.onError?.(null);
134
+ return null;
135
+ });
136
+ // The credit is spelled out at load and folds after a few seconds, or
137
+ // at the reader's first interaction (`compactCredit.ts`).
138
+ collapse = setTimeout(() => dispatchCredit({ type: 'elapsed' }), CREDIT_AUTO_COLLAPSE_MS);
139
+ latest.current.onReady?.();
140
+ };
141
+ const handleMove = (event) => {
142
+ // Only a move carrying the input event that caused it is the reader's:
143
+ // MapLibre also moves the camera itself (padding, resize), unmarked.
144
+ if (isGestureMove(event))
145
+ dispatchCredit({ type: 'mapInteraction' });
146
+ };
147
+ const handleZoom = () => {
148
+ const level = Math.floor(map.getZoom());
149
+ if (Number.isFinite(level))
150
+ setZoomLevel(level);
151
+ };
152
+ /**
153
+ * Was this click aimed at something drawn over the map? A
154
+ * `maplibregl.Marker`'s element is a child of the canvas container, so a
155
+ * click on one bubbles and MapLibre fires a map `click` for it too —
156
+ * which would set a stop under a pin the reader aimed at.
157
+ */
158
+ const pressedOwnMarker = (target) => {
159
+ if (!(target instanceof Node))
160
+ return false;
161
+ for (const element of markerElements) {
162
+ if (element === target || element.contains(target))
163
+ return true;
164
+ }
165
+ return false;
166
+ };
167
+ const handleClick = (event) => {
168
+ dispatchCredit({ type: 'mapInteraction' });
169
+ // Hit priority, step 1: a marker wins outright; its own press ran.
170
+ if (pressedOwnMarker(event.originalEvent?.target ?? null))
171
+ return;
172
+ // Step 2: a plugin's own drawing (a 3D model, a live vehicle).
173
+ for (const plugin of plugins) {
174
+ if (plugin.press?.(event.point))
175
+ return;
176
+ }
177
+ const press = latest.current.onPress;
178
+ if (!press)
179
+ return;
180
+ const coordinate = { latitude: event.lngLat.lat, longitude: event.lngLat.lng };
181
+ if (!isDrawableCoordinate(coordinate)) {
182
+ reportMapDefect('press:coordinate', 'Ignored a map press: the engine reported a non-drawable coordinate.');
183
+ return;
184
+ }
185
+ // Step 3: the basemap's own label inside the hit pad; step 4: bare map.
186
+ const label = labelAt(map, event.point, coordinate, queryLayers.tap);
187
+ press({ coordinate, ...(label ? { label } : {}) });
188
+ };
189
+ /**
190
+ * The pointer says whether there is something under it to press — the
191
+ * same `labelAt` the click uses, so the cursor cannot promise a hit the
192
+ * press then misses. Skipped while the map moves.
193
+ */
194
+ const handleMouseMove = (event) => {
195
+ if (map.isMoving())
196
+ return;
197
+ const canvas = map.getCanvas();
198
+ const coordinate = { latitude: event.lngLat.lat, longitude: event.lngLat.lng };
199
+ let over = false;
200
+ for (const plugin of plugins) {
201
+ if (plugin.hover?.(event.point)) {
202
+ over = true;
203
+ break;
204
+ }
205
+ }
206
+ if (!over && latest.current.onPress && isDrawableCoordinate(coordinate)) {
207
+ over = labelAt(map, event.point, coordinate, queryLayers.tap) !== null;
208
+ }
209
+ const wanted = over ? 'pointer' : '';
210
+ if (canvas.style.cursor !== wanted)
211
+ canvas.style.cursor = wanted;
212
+ };
213
+ const releaseCursor = () => {
214
+ const canvas = map.getCanvas();
215
+ if (canvas.style.cursor !== '')
216
+ canvas.style.cursor = '';
217
+ };
218
+ /**
219
+ * `idle`: every tile has arrived and the collision system has placed the
220
+ * labels — the only frame at which the labels on screen are the ones the
221
+ * reader sees, and the only honest moment to retract a tile failure.
222
+ */
223
+ const handleIdle = () => {
224
+ if (map.areTilesLoaded()) {
225
+ setError((current) => {
226
+ if (current?.reason !== 'tiles')
227
+ return current;
228
+ latest.current.onError?.(null);
229
+ return null;
230
+ });
231
+ }
232
+ const reportLabels = latest.current.onLabelsChange;
233
+ if (!reportLabels)
234
+ return;
235
+ if (queryLayers.labels.length === 0) {
236
+ reportLabels(EMPTY_LABELS);
237
+ return;
238
+ }
239
+ const centre = map.getCenter();
240
+ const fallback = { latitude: centre.lat, longitude: centre.lng };
241
+ if (!isDrawableCoordinate(fallback))
242
+ return;
243
+ let hits;
244
+ try {
245
+ hits = map.queryRenderedFeatures({ layers: [...queryLayers.labels] });
246
+ }
247
+ catch {
248
+ // A style swapped out from under the query; the next idle answers.
249
+ return;
250
+ }
251
+ reportLabels(collectLabelFeatures(queriedLabelsOf(hits), fallback));
252
+ };
253
+ const handleError = (event) => {
254
+ const sourceId = event.sourceId;
255
+ if (sourceId) {
256
+ const failure = {
257
+ sourceId,
258
+ status: event.error?.status,
259
+ message: event.error?.message,
260
+ };
261
+ for (const plugin of plugins) {
262
+ if (plugin.ownsError?.(failure))
263
+ return;
264
+ }
265
+ }
266
+ // A source id means a tile/source request failed — recoverable, the
267
+ // rest of the viewport still draws, and `idle` retracts it. Without one
268
+ // the style document itself did not load: nothing to draw.
269
+ report({ reason: sourceId ? 'tiles' : 'style', message: event.error?.message });
270
+ };
271
+ map.on('load', handleLoad);
272
+ map.on('move', handleMove);
273
+ map.on('zoom', handleZoom);
274
+ map.on('idle', handleIdle);
275
+ map.on('click', handleClick);
276
+ map.on('mousemove', handleMouseMove);
277
+ map.on('mouseout', releaseCursor);
278
+ map.on('dragstart', releaseCursor);
279
+ map.on('error', handleError);
280
+ return () => {
281
+ if (collapse)
282
+ clearTimeout(collapse);
283
+ detachReports();
284
+ releaseCursor();
285
+ runEngineCommand('getCenter', () => {
286
+ const centre = map.getCenter();
287
+ lastCamera.current = {
288
+ latitude: centre.lat,
289
+ longitude: centre.lng,
290
+ zoom: map.getZoom(),
291
+ bearing: map.getBearing(),
292
+ pitch: map.getPitch(),
293
+ };
294
+ });
295
+ setEngine(null);
296
+ if (mapRef.current === map)
297
+ mapRef.current = null;
298
+ map.remove();
299
+ };
300
+ }, [styleUrl, projection, report]);
301
+ const handleRetry = useCallback(() => {
302
+ setError(null);
303
+ latest.current.onError?.(null);
304
+ lastCamera.current = null;
305
+ appliedCamera.current = null;
306
+ setRetry((count) => count + 1);
307
+ }, []);
308
+ const toggleCredit = useCallback(() => dispatchCredit({ type: 'toggle' }), []);
309
+ const shownError = styleError ?? error;
310
+ return (_jsxs(View, { style: [styles.root, style], testID: testID, children: [_jsx(View, { ref: attachEngine, style: styles.engine }, engineKey), _jsx(MapRefContext.Provider, { value: api, children: _jsx(EngineChildren, { engine: engine, zoomLevel: zoomLevel, camera: camera, appliedCamera: appliedCamera, light: light, interaction: interaction, maxPitch: maxPitch, children: children }) }), _jsx(MapBrand, {}), _jsx(MapAttribution, { extra: credits, disclosure: creditDisclosure ?? null, expanded: credit.expanded, onToggle: toggleCredit }), shownError ? _jsx(MapErrorState, { error: shownError, onRetry: handleRetry }) : null] }));
311
+ });
312
+ /**
313
+ * Everything drawn ON the engine, with its context. Before the style loads
314
+ * there is nothing to bind to, and the map components render nothing.
315
+ */
316
+ function EngineChildren({ engine, zoomLevel, camera, appliedCamera, light, interaction, maxPitch, children, }) {
317
+ return (_jsx(WebEngineContext.Provider, { value: engine, children: _jsxs(ZoomLevelContext.Provider, { value: zoomLevel, children: [engine ? (_jsxs(_Fragment, { children: [camera ? (_jsx(CameraBinding, { map: engine.map, target: camera, applied: appliedCamera })) : null, _jsx(LightBinding, { map: engine.map, light: light ?? null }), _jsx(InteractionBinding, { map: engine.map, interaction: interaction, maxPitch: maxPitch ?? DEFAULT_MAX_PITCH })] })) : null, children] }) }));
318
+ }
319
+ /** A camera target as an opening view, for a map given no `initialViewState`. */
320
+ function targetView(target) {
321
+ if (target?.bounds) {
322
+ return {
323
+ bounds: target.bounds,
324
+ ...(target.padding !== undefined ? { padding: target.padding } : {}),
325
+ ...(target.maxZoom !== undefined ? { maxZoom: target.maxZoom } : {}),
326
+ };
327
+ }
328
+ if (target?.center) {
329
+ return { ...target.center, zoom: target.zoom ?? WORLD_VIEW.zoom };
330
+ }
331
+ return undefined;
332
+ }
333
+ /** The zoom a map opens at, for the first clustering pass. */
334
+ function openingZoom(view) {
335
+ if (!view)
336
+ return WORLD_VIEW.zoom;
337
+ if ('bounds' in view)
338
+ return asFinite(view.maxZoom) ?? 10;
339
+ return asFinite(view.zoom) ?? WORLD_VIEW.zoom;
340
+ }
341
+ /**
342
+ * The constructor's camera options for an opening view: a point and zoom, or
343
+ * a box to frame (a box with no area opens at its centre at street zoom). A
344
+ * view the engine cannot build opens on the whole world, named in the console
345
+ * rather than thrown.
346
+ */
347
+ function openingCamera(view) {
348
+ if (view && 'bounds' in view) {
349
+ if (!isDrawableBounds(view.bounds)) {
350
+ reportMapDefect('initialViewState', 'initialViewState.bounds is not drawable; opened on the world.');
351
+ return worldCamera();
352
+ }
353
+ if (isDegenerateBounds(view.bounds)) {
354
+ return { center: toLngLat(boundsCenter(view.bounds)), zoom: DEGENERATE_FIT_ZOOM };
355
+ }
356
+ const padding = resolvePadding(view.padding, DEFAULT_FIT_PADDING) ?? undefined;
357
+ const [west, south, east, north] = toBoundsArray(view.bounds);
358
+ const maxZoom = asFinite(view.maxZoom);
359
+ return {
360
+ bounds: [
361
+ [west, south],
362
+ [east, north],
363
+ ],
364
+ fitBoundsOptions: {
365
+ ...(padding ? { padding } : {}),
366
+ ...(maxZoom !== undefined ? { maxZoom } : {}),
367
+ },
368
+ };
369
+ }
370
+ if (view && isDrawableCoordinate(view)) {
371
+ return {
372
+ center: toLngLat(view),
373
+ zoom: asFinite(view.zoom) ?? WORLD_VIEW.zoom,
374
+ bearing: asFinite(view.bearing) ?? 0,
375
+ pitch: asFinite(view.pitch) ?? 0,
376
+ };
377
+ }
378
+ if (view)
379
+ reportMapDefect('initialViewState', 'initialViewState is not drawable; opened on the world.');
380
+ return worldCamera();
381
+ }
382
+ function worldCamera() {
383
+ return { center: toLngLat(WORLD_VIEW), zoom: WORLD_VIEW.zoom };
384
+ }
385
+ /**
386
+ * The basemap label a press meant, or `null` — queried in a BOX around the
387
+ * press, because a label's glyphs sit 12–20 px from the anchor the engine
388
+ * indexes (`labels.ts` → {@link LABEL_HIT_PAD_PX}).
389
+ */
390
+ function labelAt(map, point, coordinate, layers) {
391
+ if (layers.length === 0)
392
+ return null;
393
+ let hits;
394
+ try {
395
+ hits = map.queryRenderedFeatures([
396
+ [point.x - LABEL_HIT_PAD_PX, point.y - LABEL_HIT_PAD_PX],
397
+ [point.x + LABEL_HIT_PAD_PX, point.y + LABEL_HIT_PAD_PX],
398
+ ], { layers: [...layers] });
399
+ }
400
+ catch {
401
+ // A layer that vanished between the style load and this press.
402
+ return null;
403
+ }
404
+ const labels = labelCandidatesOf(queriedLabelsOf(hits), coordinate);
405
+ return pickLabelFeature(labels, point, (target) => {
406
+ const projected = map.project([target.longitude, target.latitude]);
407
+ return { x: projected.x, y: projected.y };
408
+ });
409
+ }
410
+ const styles = StyleSheet.create({
411
+ root: { flex: 1, position: 'relative', overflow: 'hidden' },
412
+ engine: { flex: 1 },
413
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One view drawn at a coordinate, native — MapLibre Native's `<Marker>`, a
3
+ * real React Native view, so a marker on native is the same Bloom component as
4
+ * on the web. Centred on its coordinate, as Bloom's map markers are designed
5
+ * to be.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ import type { GeoCoordinate } from './types';
9
+ export interface MarkerProps {
10
+ coordinate: GeoCoordinate;
11
+ children: ReactNode;
12
+ }
13
+ export declare function Marker({ coordinate, children }: MarkerProps): import("react").JSX.Element | null;
package/dist/Marker.js ADDED
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { View } from 'react-native';
3
+ import { Marker as NativeMarker } from '@maplibre/maplibre-react-native';
4
+ import { isDrawableCoordinate, reportMapDefect, toLngLat } from './shared';
5
+ export function Marker({ coordinate, children }) {
6
+ if (!isDrawableCoordinate(coordinate)) {
7
+ reportMapDefect('marker:coordinate', 'Dropped a marker: its coordinate is not drawable.');
8
+ return null;
9
+ }
10
+ return (_jsx(NativeMarker, { lngLat: toLngLat(coordinate), anchor: "center", children: _jsx(View, { children: children }) }));
11
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One view drawn at a coordinate, web — a `maplibregl.Marker` whose DOM element
3
+ * React portals the children into, so a marker on the web is the same
4
+ * component as a marker on native.
5
+ *
6
+ * Centred on its coordinate, as Bloom's map markers are designed to be
7
+ * (`@oxy.so/bloom/map-marker`: "centre a marker on its coordinate").
8
+ */
9
+ import { type ReactNode } from 'react';
10
+ import type { GeoCoordinate } from './types';
11
+ export interface MarkerProps {
12
+ coordinate: GeoCoordinate;
13
+ children: ReactNode;
14
+ }
15
+ export declare function Marker({ coordinate, children }: MarkerProps): import("react").JSX.Element | null;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * One view drawn at a coordinate, web — a `maplibregl.Marker` whose DOM element
4
+ * React portals the children into, so a marker on the web is the same
5
+ * component as a marker on native.
6
+ *
7
+ * Centred on its coordinate, as Bloom's map markers are designed to be
8
+ * (`@oxy.so/bloom/map-marker`: "centre a marker on its coordinate").
9
+ */
10
+ import { useContext, useLayoutEffect, useState } from 'react';
11
+ import { createPortal } from 'react-dom';
12
+ import * as maplibregl from 'maplibre-gl';
13
+ import { WebEngineContext } from './engineContext.web';
14
+ import { isDrawableCoordinate, reportMapDefect, runEngineCommand } from './shared';
15
+ export function Marker({ coordinate, children }) {
16
+ const engine = useContext(WebEngineContext);
17
+ if (!engine)
18
+ return null;
19
+ // Undrawable pins never reach `setLngLat`, which throws on one.
20
+ if (!isDrawableCoordinate(coordinate)) {
21
+ reportMapDefect('marker:coordinate', 'Dropped a marker: its coordinate is not drawable.');
22
+ return null;
23
+ }
24
+ return (_jsx(MarkerBinding, { engine: engine, coordinate: coordinate, children: children }));
25
+ }
26
+ function MarkerBinding({ engine, coordinate, children, }) {
27
+ // The element the engine positions; the children are portalled into it.
28
+ const [element] = useState(() => {
29
+ const div = document.createElement('div');
30
+ // The pill sizes itself; a fixed-size element would clip it.
31
+ div.style.willChange = 'transform';
32
+ return div;
33
+ });
34
+ const { latitude, longitude } = coordinate;
35
+ // Binding: a `maplibregl.Marker` holds `element` at `coordinate` on the engine.
36
+ useLayoutEffect(() => {
37
+ const { map, markerElements } = engine;
38
+ let marker = null;
39
+ runEngineCommand('addMarker', () => {
40
+ marker = new maplibregl.Marker({ element, anchor: 'center' })
41
+ .setLngLat([longitude, latitude])
42
+ .addTo(map);
43
+ });
44
+ markerElements.add(element);
45
+ return () => {
46
+ markerElements.delete(element);
47
+ marker?.remove();
48
+ };
49
+ }, [engine, element, latitude, longitude]);
50
+ return createPortal(children, element);
51
+ }
@@ -0,0 +1,2 @@
1
+ import type { MarkersProps } from './types';
2
+ export declare function Markers(props: MarkersProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * `<Markers>`, native — Bloom markers at their coordinates, clustered by the
4
+ * same options Mapbox's sources take (`clusters.ts`).
5
+ */
6
+ import { DefaultMapMarker } from './DefaultMapMarker';
7
+ import { Marker } from './Marker';
8
+ import { useMarkerSlots } from './useMarkerSlots';
9
+ export function Markers(props) {
10
+ const { slots, press } = useMarkerSlots(props);
11
+ const { renderMarker } = props;
12
+ return (_jsx(_Fragment, { children: slots.map((slot) => (_jsx(Marker, { coordinate: slot.coordinate, children: renderMarker ? (renderMarker(slot.item, () => press(slot))) : (_jsx(DefaultMapMarker, { item: slot.item, onPress: () => press(slot) })) }, slot.key))) }));
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { MarkersProps } from './types';
2
+ export declare function Markers(props: MarkersProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * `<Markers>`, web — Bloom markers at their coordinates, clustered by the
4
+ * same options Mapbox's sources take (`clusters.ts`).
5
+ */
6
+ import { DefaultMapMarker } from './DefaultMapMarker';
7
+ import { Marker } from './Marker.web';
8
+ import { useMarkerSlots } from './useMarkerSlots';
9
+ export function Markers(props) {
10
+ const { slots, press } = useMarkerSlots(props);
11
+ const { renderMarker } = props;
12
+ return (_jsx(_Fragment, { children: slots.map((slot) => (_jsx(Marker, { coordinate: slot.coordinate, children: renderMarker ? (renderMarker(slot.item, () => press(slot))) : (_jsx(DefaultMapMarker, { item: slot.item, onPress: () => press(slot) })) }, slot.key))) }));
13
+ }
@@ -0,0 +1,7 @@
1
+ import type { MapPluginHandle } from './plugin';
2
+ export declare function PluginProps<Props>({ handle, attachedWith, props, }: {
3
+ handle: MapPluginHandle<Props>;
4
+ /** The props `attach` received. */
5
+ attachedWith: Props;
6
+ props: Props;
7
+ }): null;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * An attached plugin's props, bound with its `update` (see `MapPlugin.web.tsx`).
3
+ */
4
+ import { useLayoutEffect, useRef } from 'react';
5
+ export function PluginProps({ handle, attachedWith, props, }) {
6
+ // The props the handle has: those it was attached with, then each update.
7
+ const delivered = useRef({
8
+ handle,
9
+ props: attachedWith,
10
+ });
11
+ // Binding: an attached plugin receives every new `props`.
12
+ useLayoutEffect(() => {
13
+ const last = delivered.current.handle === handle ? delivered.current : { handle, props: attachedWith };
14
+ if (last.props !== props)
15
+ handle.update?.(props);
16
+ delivered.current = { handle, props };
17
+ }, [handle, attachedWith, props]);
18
+ return null;
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { RasterDEMSourceProps } from './types';
2
+ export declare function RasterDEMSource({ id, tiles, encoding, tileSize, minzoom, maxzoom, children, }: RasterDEMSourceProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** `<RasterDEMSource>`, native — MapLibre Native's elevation source (for a `hillshade` layer). */
3
+ import { RasterDEMSource as NativeRasterDEMSource } from '@maplibre/maplibre-react-native';
4
+ import { SourceIdContext } from './sourceContext';
5
+ export function RasterDEMSource({ id, tiles, encoding, tileSize, minzoom, maxzoom, children, }) {
6
+ return (_jsx(NativeRasterDEMSource, { id: id, tiles: [...tiles], ...(encoding ? { encoding } : {}), ...(tileSize !== undefined ? { tileSize } : {}), ...(minzoom !== undefined ? { minzoom } : {}), ...(maxzoom !== undefined ? { maxzoom } : {}), children: _jsx(SourceIdContext.Provider, { value: id, children: children }) }));
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { RasterDEMSourceProps } from './types';
2
+ export declare function RasterDEMSource(props: RasterDEMSourceProps): import("react").JSX.Element | null;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * `<RasterDEMSource>`, web — an elevation source (for a `hillshade` layer),
4
+ * added once per engine with its tiles.
5
+ */
6
+ import { useContext, useLayoutEffect, useState } from 'react';
7
+ import { WebEngineContext } from './engineContext.web';
8
+ import { runEngineCommand } from './shared';
9
+ import { SourceIdContext } from './sourceContext';
10
+ export function RasterDEMSource(props) {
11
+ const engine = useContext(WebEngineContext);
12
+ return engine ? _jsx(RasterDEMSourceBinding, { map: engine.map, ...props }) : null;
13
+ }
14
+ function RasterDEMSourceBinding({ map, id, tiles, encoding, tileSize, minzoom, maxzoom, children, }) {
15
+ const [added, setAdded] = useState(null);
16
+ const tileKey = tiles.join('\n');
17
+ // Binding: the source `id` exists on the engine with these tiles.
18
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `tileKey` is the value `tiles` stands for
19
+ useLayoutEffect(() => {
20
+ const spec = {
21
+ type: 'raster-dem',
22
+ tiles: [...tiles],
23
+ ...(encoding ? { encoding } : {}),
24
+ ...(tileSize !== undefined ? { tileSize } : {}),
25
+ ...(minzoom !== undefined ? { minzoom } : {}),
26
+ ...(maxzoom !== undefined ? { maxzoom } : {}),
27
+ };
28
+ if (!runEngineCommand('addSource', () => map.addSource(id, spec)))
29
+ return;
30
+ setAdded(map);
31
+ return () => {
32
+ setAdded(null);
33
+ runEngineCommand('removeSource', () => {
34
+ for (const layer of map.getStyle()?.layers ?? []) {
35
+ if ('source' in layer && layer.source === id)
36
+ map.removeLayer(layer.id);
37
+ }
38
+ if (map.getSource(id))
39
+ map.removeSource(id);
40
+ });
41
+ };
42
+ }, [map, id, tileKey, encoding, tileSize, minzoom, maxzoom]);
43
+ return added === map ? (_jsx(SourceIdContext.Provider, { value: id, children: children })) : null;
44
+ }
@@ -0,0 +1,6 @@
1
+ import type { Map as MapLibreMap } from 'maplibre-gl';
2
+ export declare function SourceData({ map, id, data, }: {
3
+ map: MapLibreMap;
4
+ id: string;
5
+ data: GeoJSON.GeoJSON;
6
+ }): null;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A GeoJSON source's data, bound to its `data` prop with maplibre-gl's
3
+ * imperative `setData` (see `GeoJSONSource.web.tsx` for why it is its own
4
+ * binding).
5
+ */
6
+ import { useLayoutEffect } from 'react';
7
+ import { runEngineCommand } from './shared';
8
+ export function SourceData({ map, id, data, }) {
9
+ // Binding: the source's data follows `data`.
10
+ useLayoutEffect(() => {
11
+ runEngineCommand('setData', () => map.getSource(id)?.setData(data));
12
+ }, [map, id, data]);
13
+ return null;
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { UserLocationProps } from './types';
2
+ export declare function UserLocation({ visible }: UserLocationProps): import("react").JSX.Element | null;