@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
|
@@ -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
|
+
});
|
package/dist/Marker.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/Markers.js
ADDED
|
@@ -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,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,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,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,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,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
|
+
}
|