@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/MapView.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `MapView`, iOS + Android — `@maplibre/maplibre-react-native` v11.
|
|
4
|
+
*
|
|
5
|
+
* MapLibre Native is declarative where it matters: the style, the light, the
|
|
6
|
+
* gestures, the camera's target and every source, layer and marker are PROPS
|
|
7
|
+
* of native views, diffed by Fabric, so nothing here binds anything by hand. A
|
|
8
|
+
* `camera` target with the same numbers is the same prop and is not re-sent.
|
|
9
|
+
*
|
|
10
|
+
* **It needs a development build.** MapLibre Native ships real iOS and Android
|
|
11
|
+
* SDKs, so the module is absent from Expo Go; `@goway.to/maps/app.plugin.js`
|
|
12
|
+
* adds it to a build.
|
|
13
|
+
*
|
|
14
|
+
* Markers are real React views at a coordinate (`<Marker>`), so they are the
|
|
15
|
+
* same Bloom components as on the web.
|
|
16
|
+
*/
|
|
17
|
+
import { forwardRef, useCallback, useImperativeHandle, useMemo, useReducer, useRef, useState, } from 'react';
|
|
18
|
+
import { StyleSheet, View } from 'react-native';
|
|
19
|
+
import { Camera as NativeCamera, Map as NativeMap, } from '@maplibre/maplibre-react-native';
|
|
20
|
+
import { boundsCenter, boundsOf, isDegenerateBounds } from '@goway.to/sdk/geo';
|
|
21
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
22
|
+
import { cameraTargetKey } from './cameraTarget';
|
|
23
|
+
import { CREDIT_AUTO_COLLAPSE_MS, creditReducer, INITIAL_CREDIT_STATE } from './compactCredit';
|
|
24
|
+
import { LABEL_HIT_PAD_PX, labelCandidatesOf, collectLabelFeatures, pickLabelFeature, queriedLabelsOf, } from './labels';
|
|
25
|
+
import { MapAttribution } from './MapAttribution';
|
|
26
|
+
import { MapBrand } from './MapBrand';
|
|
27
|
+
import { MapErrorState } from './MapErrorState';
|
|
28
|
+
import { MapRefContext } from './mapRef';
|
|
29
|
+
import { createNativeCameraReports } from './nativeCameraReports';
|
|
30
|
+
import { queryLayersForUrl } from './queryLayers';
|
|
31
|
+
import { asFinite, describeNumbers, fromLngLat, isDrawableBounds, isDrawableCoordinate, isViewport, optionalScalar, reportMapDefect, resolveInteraction, resolvePadding, runEngineCommand, toBoundsArray, toLngLat, } from './shared';
|
|
32
|
+
import { DEFAULT_MAP_STYLE, resolveStyleUrl } from './styleUrl';
|
|
33
|
+
import { ZoomLevelContext } from './zoom';
|
|
34
|
+
/** Zoom used when a fit has nothing with area to fit — see `isDegenerateBounds`. */
|
|
35
|
+
const DEGENERATE_FIT_ZOOM = 15;
|
|
36
|
+
const DEFAULT_FIT_PADDING = 48;
|
|
37
|
+
const DEFAULT_CAMERA_DURATION_MS = 500;
|
|
38
|
+
const WORLD_VIEW = { latitude: 20, longitude: 0, zoom: 1.5 };
|
|
39
|
+
export const MapView = forwardRef(function MapView(props, ref) {
|
|
40
|
+
const { mapStyle = DEFAULT_MAP_STYLE, lightPreset, initialViewState, camera, light, interaction, credits, creditDisclosure, onPress, onCameraChanged, onLabelsChange, onReady, onError, style, testID, children, } = props;
|
|
41
|
+
const theme = useTheme();
|
|
42
|
+
let styleUrl = null;
|
|
43
|
+
let styleError = null;
|
|
44
|
+
try {
|
|
45
|
+
styleUrl = resolveStyleUrl(mapStyle, lightPreset, theme.isDark);
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
styleError = { reason: 'style', message: String(cause) };
|
|
49
|
+
}
|
|
50
|
+
const mapRef = useRef(null);
|
|
51
|
+
const cameraRef = useRef(null);
|
|
52
|
+
const [error, setError] = useState(null);
|
|
53
|
+
// Remounting the engine is the only reliable retry MapLibre Native offers:
|
|
54
|
+
// a style that failed to load is not re-fetched by setting the same URL.
|
|
55
|
+
const [retry, setRetry] = useState(0);
|
|
56
|
+
const [credit, dispatchCredit] = useReducer(creditReducer, INITIAL_CREDIT_STATE);
|
|
57
|
+
const [zoomLevel, setZoomLevel] = useState(() => Math.floor(openingZoom(initialViewState)));
|
|
58
|
+
// The camera reports (`nativeCameraReports.ts`), one per ENGINE: a retry
|
|
59
|
+
// gives the new engine its own, so a move the old one never finished cannot
|
|
60
|
+
// stop the new one reporting the camera it opens at.
|
|
61
|
+
const latestCameraListener = useRef(onCameraChanged);
|
|
62
|
+
latestCameraListener.current = onCameraChanged;
|
|
63
|
+
const [reports, setReports] = useState(() => createNativeCameraReports(() => latestCameraListener.current));
|
|
64
|
+
const api = useMemo(() => createNativeMapApi(mapRef, cameraRef, () => reports), [reports]);
|
|
65
|
+
useImperativeHandle(ref, () => api, [api]);
|
|
66
|
+
const report = (next) => {
|
|
67
|
+
setError(next);
|
|
68
|
+
onError?.(next);
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A press, resolved against the basemap's own labels. Asynchronous where the
|
|
72
|
+
* web is synchronous: the query runs inside the platform SDK. The CONTRACT
|
|
73
|
+
* matches — the same padded box, the same ranking, the same event.
|
|
74
|
+
*
|
|
75
|
+
* A press on a `<Marker>` never gets here: a marker is a real native view and
|
|
76
|
+
* its `Pressable` takes the touch.
|
|
77
|
+
*/
|
|
78
|
+
const handlePress = (event) => {
|
|
79
|
+
dispatchCredit({ type: 'mapInteraction' });
|
|
80
|
+
if (!onPress)
|
|
81
|
+
return;
|
|
82
|
+
const coordinate = fromLngLat(event.nativeEvent.lngLat);
|
|
83
|
+
if (!isDrawableCoordinate(coordinate)) {
|
|
84
|
+
reportMapDefect('press:coordinate', 'Ignored a map press: the engine reported a non-drawable coordinate.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const map = mapRef.current;
|
|
88
|
+
const point = event.nativeEvent.point;
|
|
89
|
+
if (!map || !styleUrl || !Array.isArray(point)) {
|
|
90
|
+
onPress({ coordinate });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const [x, y] = point;
|
|
94
|
+
void queryLayersForUrl(styleUrl)
|
|
95
|
+
.then(async ({ tap }) => {
|
|
96
|
+
if (tap.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
const features = await map.queryRenderedFeatures([
|
|
99
|
+
[x - LABEL_HIT_PAD_PX, y - LABEL_HIT_PAD_PX],
|
|
100
|
+
[x + LABEL_HIT_PAD_PX, y + LABEL_HIT_PAD_PX],
|
|
101
|
+
], { layers: [...tap] });
|
|
102
|
+
const labels = labelCandidatesOf(queriedLabelsOf(features), coordinate);
|
|
103
|
+
if (labels.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
// One projection per anchored candidate, gathered up front, so the
|
|
106
|
+
// ranking runs against a plain lookup — the web's arithmetic.
|
|
107
|
+
const projected = new Map();
|
|
108
|
+
await Promise.all(labels.map(async (candidate) => {
|
|
109
|
+
if (!candidate.anchored)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
const [px, py] = await map.project(toLngLat(candidate.coordinate));
|
|
113
|
+
projected.set(pointKey(candidate.coordinate), { x: px, y: py });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Unprojectable is ranked as unanchored, not dropped.
|
|
117
|
+
}
|
|
118
|
+
}));
|
|
119
|
+
return pickLabelFeature(labels, { x, y }, (target) => projected.get(pointKey(target)) ?? null);
|
|
120
|
+
})
|
|
121
|
+
.then((label) => onPress({ coordinate, ...(label ? { label } : {}) }))
|
|
122
|
+
// A query the platform refused is a press on bare map, not a dead press.
|
|
123
|
+
.catch(() => onPress({ coordinate }));
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The basemap's labels on the settled frame — `onRegionDidChange` is the
|
|
127
|
+
* closest native has to the web's `idle`, so a label that appears as the
|
|
128
|
+
* last tiles decode is reported on the NEXT settled frame.
|
|
129
|
+
*/
|
|
130
|
+
const reportLabels = (centre) => {
|
|
131
|
+
const map = mapRef.current;
|
|
132
|
+
if (!onLabelsChange || !map || !styleUrl || !isDrawableCoordinate(centre))
|
|
133
|
+
return;
|
|
134
|
+
void queryLayersForUrl(styleUrl)
|
|
135
|
+
.then(({ labels }) => labels.length === 0 ? [] : map.queryRenderedFeatures({ layers: [...labels] }))
|
|
136
|
+
.then((features) => onLabelsChange(collectLabelFeatures(queriedLabelsOf(features), centre)))
|
|
137
|
+
.catch(() => {
|
|
138
|
+
// Leaving the previous set alone keeps the app's view stable across a hiccup.
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const handleStyleLoaded = () => {
|
|
142
|
+
setError((current) => {
|
|
143
|
+
if (current)
|
|
144
|
+
onError?.(null);
|
|
145
|
+
return null;
|
|
146
|
+
});
|
|
147
|
+
// The credit is spelled out at load and folds after a few seconds, or at
|
|
148
|
+
// the reader's first interaction (`compactCredit.ts`).
|
|
149
|
+
setTimeout(() => dispatchCredit({ type: 'elapsed' }), CREDIT_AUTO_COLLAPSE_MS);
|
|
150
|
+
// The camera the map opened at, reported as settled — once per engine.
|
|
151
|
+
reports.loaded(() => {
|
|
152
|
+
const map = mapRef.current;
|
|
153
|
+
if (!map)
|
|
154
|
+
throw new Error('no engine');
|
|
155
|
+
return map.getViewState();
|
|
156
|
+
});
|
|
157
|
+
onReady?.();
|
|
158
|
+
};
|
|
159
|
+
const handleStyleFailed = () => {
|
|
160
|
+
// MapLibre Native reports a style or source failure as one event and
|
|
161
|
+
// surfaces no per-tile errors to JS.
|
|
162
|
+
report({ reason: 'style', message: 'Failed to load the map style.' });
|
|
163
|
+
};
|
|
164
|
+
const handleRetry = useCallback(() => {
|
|
165
|
+
setError(null);
|
|
166
|
+
onError?.(null);
|
|
167
|
+
setReports(createNativeCameraReports(() => latestCameraListener.current));
|
|
168
|
+
setRetry((count) => count + 1);
|
|
169
|
+
}, [onError]);
|
|
170
|
+
const noteZoom = (zoom) => {
|
|
171
|
+
const level = Math.floor(zoom);
|
|
172
|
+
if (Number.isFinite(level))
|
|
173
|
+
setZoomLevel(level);
|
|
174
|
+
};
|
|
175
|
+
// The reader moving the map folds the credit; the app moving it does not.
|
|
176
|
+
const foldOnReaderMove = (event) => {
|
|
177
|
+
if (event.nativeEvent.userInteraction)
|
|
178
|
+
dispatchCredit({ type: 'mapInteraction' });
|
|
179
|
+
};
|
|
180
|
+
const gestures = resolveInteraction(interaction);
|
|
181
|
+
const opening = openingViewState(initialViewState ?? targetView(camera));
|
|
182
|
+
const stop = cameraStop(camera);
|
|
183
|
+
const toggleCredit = useCallback(() => dispatchCredit({ type: 'toggle' }), []);
|
|
184
|
+
const shownError = styleError ?? error;
|
|
185
|
+
return (_jsxs(View, { style: [styles.root, style], testID: testID, children: [styleUrl ? (_jsxs(NativeMap, { ref: mapRef, style: styles.fill, mapStyle: styleUrl, dragPan: gestures.pan, touchZoom: gestures.zoom, touchRotate: gestures.rotate, touchPitch: gestures.pitch,
|
|
186
|
+
// The credit is `MapAttribution`, drawn below on every map.
|
|
187
|
+
attribution: false, logo: false, light: light ? light : undefined, compass: gestures.rotate, compassPosition: { top: 8, right: 8 }, onPress: handlePress, onRegionWillChange: (event) => reports.regionWillChange(event.nativeEvent), onRegionIsChanging: (event) => {
|
|
188
|
+
foldOnReaderMove(event);
|
|
189
|
+
noteZoom(event.nativeEvent.zoom);
|
|
190
|
+
reports.regionIsChanging(event.nativeEvent);
|
|
191
|
+
}, onRegionDidChange: (event) => {
|
|
192
|
+
foldOnReaderMove(event);
|
|
193
|
+
noteZoom(event.nativeEvent.zoom);
|
|
194
|
+
// `false`: one frame of an animation the app drives — not settled.
|
|
195
|
+
if (!reports.regionDidChange(event.nativeEvent))
|
|
196
|
+
return;
|
|
197
|
+
const [longitude, latitude] = event.nativeEvent.center;
|
|
198
|
+
reportLabels({ latitude, longitude });
|
|
199
|
+
}, onDidFinishLoadingStyle: handleStyleLoaded, onDidFailLoadingMap: handleStyleFailed, children: [_jsx(NativeCamera, { ref: cameraRef, initialViewState: opening, ...stop }), _jsx(MapRefContext.Provider, { value: api, children: _jsx(ZoomLevelContext.Provider, { value: zoomLevel, children: children }) })] }, retry)) : null, _jsx(MapBrand, {}), _jsx(MapAttribution, { extra: credits, disclosure: creditDisclosure ?? null, expanded: credit.expanded, onToggle: toggleCredit }), shownError ? _jsx(MapErrorState, { error: shownError, onRetry: handleRetry }) : null] }));
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* The camera's target as MapLibre Native's camera stop props. A box with no
|
|
203
|
+
* area is its centre at street zoom; an undrawable target is no target.
|
|
204
|
+
*/
|
|
205
|
+
function cameraStop(target) {
|
|
206
|
+
if (!target || cameraTargetKey(target) === null)
|
|
207
|
+
return {};
|
|
208
|
+
const duration = asFinite(target.duration) ?? DEFAULT_CAMERA_DURATION_MS;
|
|
209
|
+
if (target.bounds) {
|
|
210
|
+
if (!isDrawableBounds(target.bounds)) {
|
|
211
|
+
reportMapDefect('camera:bounds', `Ignored camera.bounds: ${describeNumbers(target.bounds)}.`);
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
if (isDegenerateBounds(target.bounds)) {
|
|
215
|
+
return {
|
|
216
|
+
center: toLngLat(boundsCenter(target.bounds)),
|
|
217
|
+
zoom: Math.min(asFinite(target.maxZoom) ?? DEGENERATE_FIT_ZOOM, DEGENERATE_FIT_ZOOM),
|
|
218
|
+
duration,
|
|
219
|
+
easing: 'ease',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const padding = resolvePadding(target.padding, DEFAULT_FIT_PADDING);
|
|
223
|
+
return {
|
|
224
|
+
bounds: toBoundsArray(target.bounds),
|
|
225
|
+
...(padding ? { padding } : {}),
|
|
226
|
+
duration,
|
|
227
|
+
easing: 'ease',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (target.center && isDrawableCoordinate(target.center)) {
|
|
231
|
+
return {
|
|
232
|
+
center: toLngLat(target.center),
|
|
233
|
+
...optionalScalar('zoom', target.zoom),
|
|
234
|
+
duration,
|
|
235
|
+
easing: 'ease',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
/** A camera target as an opening view, for a map given no `initialViewState`. */
|
|
241
|
+
function targetView(target) {
|
|
242
|
+
if (target?.bounds) {
|
|
243
|
+
return {
|
|
244
|
+
bounds: target.bounds,
|
|
245
|
+
...(target.padding !== undefined ? { padding: target.padding } : {}),
|
|
246
|
+
...(target.maxZoom !== undefined ? { maxZoom: target.maxZoom } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (target?.center)
|
|
250
|
+
return { ...target.center, zoom: target.zoom ?? WORLD_VIEW.zoom };
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
/** The opening view as MapLibre Native's `initialViewState`. */
|
|
254
|
+
function openingViewState(view) {
|
|
255
|
+
if (view && 'bounds' in view) {
|
|
256
|
+
if (!isDrawableBounds(view.bounds))
|
|
257
|
+
return worldView();
|
|
258
|
+
if (isDegenerateBounds(view.bounds)) {
|
|
259
|
+
return { center: toLngLat(boundsCenter(view.bounds)), zoom: DEGENERATE_FIT_ZOOM };
|
|
260
|
+
}
|
|
261
|
+
const padding = resolvePadding(view.padding, DEFAULT_FIT_PADDING);
|
|
262
|
+
return { bounds: toBoundsArray(view.bounds), ...(padding ? { padding } : {}) };
|
|
263
|
+
}
|
|
264
|
+
if (view && isDrawableCoordinate(view)) {
|
|
265
|
+
return {
|
|
266
|
+
center: toLngLat(view),
|
|
267
|
+
zoom: asFinite(view.zoom) ?? WORLD_VIEW.zoom,
|
|
268
|
+
...optionalScalar('bearing', view.bearing),
|
|
269
|
+
...optionalScalar('pitch', view.pitch),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (view)
|
|
273
|
+
reportMapDefect('initialViewState', 'initialViewState is not drawable; opened on the world.');
|
|
274
|
+
return worldView();
|
|
275
|
+
}
|
|
276
|
+
function worldView() {
|
|
277
|
+
return { center: toLngLat(WORLD_VIEW), zoom: WORLD_VIEW.zoom };
|
|
278
|
+
}
|
|
279
|
+
function openingZoom(view) {
|
|
280
|
+
if (!view)
|
|
281
|
+
return WORLD_VIEW.zoom;
|
|
282
|
+
if ('bounds' in view)
|
|
283
|
+
return asFinite(view.maxZoom) ?? 10;
|
|
284
|
+
return asFinite(view.zoom) ?? WORLD_VIEW.zoom;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Key for the projected-position lookup: native projects one coordinate per
|
|
288
|
+
* bridge call, so the ranking cannot ask for a position mid-comparison. Six
|
|
289
|
+
* decimals is ~11 cm — finer than any two distinct POIs.
|
|
290
|
+
*/
|
|
291
|
+
function pointKey(coordinate) {
|
|
292
|
+
return `${coordinate.longitude.toFixed(6)},${coordinate.latitude.toFixed(6)}`;
|
|
293
|
+
}
|
|
294
|
+
/** The native map's camera commands — the `MapViewRef` on iOS and Android. */
|
|
295
|
+
function createNativeMapApi(mapRef, cameraRef, reports) {
|
|
296
|
+
const api = {
|
|
297
|
+
moveTo(target, options) {
|
|
298
|
+
const camera = cameraRef.current;
|
|
299
|
+
if (!camera)
|
|
300
|
+
return false;
|
|
301
|
+
const coordinate = isViewport(target)
|
|
302
|
+
? { latitude: target.latitude, longitude: target.longitude }
|
|
303
|
+
: target;
|
|
304
|
+
if (!isDrawableCoordinate(coordinate)) {
|
|
305
|
+
reportMapDefect('moveTo', `Ignored moveTo: target is not drawable (${describeNumbers(coordinate)}).`);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const viewport = isViewport(target) ? target : undefined;
|
|
309
|
+
const duration = asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS;
|
|
310
|
+
reports().commanded(options?.transient === true);
|
|
311
|
+
const stop = {
|
|
312
|
+
center: toLngLat(coordinate),
|
|
313
|
+
...optionalScalar('zoom', asFinite(options?.zoom) ?? asFinite(viewport?.zoom)),
|
|
314
|
+
...optionalScalar('bearing', asFinite(options?.bearing) ?? asFinite(viewport?.bearing)),
|
|
315
|
+
...optionalScalar('pitch', asFinite(options?.pitch) ?? asFinite(viewport?.pitch)),
|
|
316
|
+
};
|
|
317
|
+
if (duration <= 0)
|
|
318
|
+
return runEngineCommand('jumpTo', () => camera.jumpTo(stop));
|
|
319
|
+
return runEngineCommand('easeTo', () => camera.easeTo({ ...stop, duration, easing: 'ease' }));
|
|
320
|
+
},
|
|
321
|
+
fitBounds(bounds, options) {
|
|
322
|
+
const camera = cameraRef.current;
|
|
323
|
+
if (!camera)
|
|
324
|
+
return;
|
|
325
|
+
if (!isDrawableBounds(bounds)) {
|
|
326
|
+
reportMapDefect('fitBounds', `Ignored fitBounds: box is not drawable (${describeNumbers(bounds)}).`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const padding = resolvePadding(options?.padding, DEFAULT_FIT_PADDING);
|
|
330
|
+
if (!padding)
|
|
331
|
+
return;
|
|
332
|
+
const maxZoom = asFinite(options?.maxZoom);
|
|
333
|
+
const duration = asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS;
|
|
334
|
+
reports().commanded(false);
|
|
335
|
+
if (isDegenerateBounds(bounds)) {
|
|
336
|
+
runEngineCommand('easeTo', () => camera.easeTo({
|
|
337
|
+
center: toLngLat(boundsCenter(bounds)),
|
|
338
|
+
zoom: Math.min(maxZoom ?? DEGENERATE_FIT_ZOOM, DEGENERATE_FIT_ZOOM),
|
|
339
|
+
duration,
|
|
340
|
+
}));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// The cap is `zoom` here and `maxZoom` on web, and ABSENT rather than
|
|
344
|
+
// `undefined` on both (`optionalScalar`).
|
|
345
|
+
runEngineCommand('fitBounds', () => camera.fitBounds(toBoundsArray(bounds), {
|
|
346
|
+
padding,
|
|
347
|
+
duration,
|
|
348
|
+
...optionalScalar('zoom', maxZoom),
|
|
349
|
+
}));
|
|
350
|
+
},
|
|
351
|
+
fitCoordinates(coordinates, options) {
|
|
352
|
+
const bounds = boundsOf(coordinates);
|
|
353
|
+
if (bounds)
|
|
354
|
+
api.fitBounds(bounds, options);
|
|
355
|
+
},
|
|
356
|
+
setBearing(bearing, options) {
|
|
357
|
+
const heading = asFinite(bearing);
|
|
358
|
+
if (heading === undefined) {
|
|
359
|
+
reportMapDefect('setBearing', `Ignored setBearing: ${String(bearing)} is not a bearing.`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const camera = cameraRef.current;
|
|
363
|
+
if (!camera)
|
|
364
|
+
return;
|
|
365
|
+
reports().commanded(false);
|
|
366
|
+
runEngineCommand('setStop', () => camera.setStop({
|
|
367
|
+
bearing: heading,
|
|
368
|
+
duration: asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS,
|
|
369
|
+
easing: 'ease',
|
|
370
|
+
}));
|
|
371
|
+
},
|
|
372
|
+
resetNorth(options) {
|
|
373
|
+
const camera = cameraRef.current;
|
|
374
|
+
if (!camera)
|
|
375
|
+
return;
|
|
376
|
+
reports().commanded(false);
|
|
377
|
+
runEngineCommand('setStop', () => camera.setStop({
|
|
378
|
+
bearing: 0,
|
|
379
|
+
pitch: 0,
|
|
380
|
+
duration: asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS,
|
|
381
|
+
easing: 'ease',
|
|
382
|
+
}));
|
|
383
|
+
},
|
|
384
|
+
async getViewport() {
|
|
385
|
+
const map = mapRef.current;
|
|
386
|
+
if (!map)
|
|
387
|
+
return null;
|
|
388
|
+
try {
|
|
389
|
+
const view = await map.getViewState();
|
|
390
|
+
return {
|
|
391
|
+
latitude: view.center[1],
|
|
392
|
+
longitude: view.center[0],
|
|
393
|
+
zoom: view.zoom,
|
|
394
|
+
bearing: view.bearing,
|
|
395
|
+
pitch: view.pitch,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
async getBounds() {
|
|
403
|
+
const map = mapRef.current;
|
|
404
|
+
if (!map)
|
|
405
|
+
return null;
|
|
406
|
+
try {
|
|
407
|
+
const [west, south, east, north] = await map.getBounds();
|
|
408
|
+
return { west, south, east, north };
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
return api;
|
|
416
|
+
}
|
|
417
|
+
const styles = StyleSheet.create({
|
|
418
|
+
root: { flex: 1, position: 'relative', overflow: 'hidden' },
|
|
419
|
+
fill: { flex: 1 },
|
|
420
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
2
|
+
import type { MapViewProps, MapViewRef } from './types';
|
|
3
|
+
/** Where `@goway.to/maps/metro` puts MapLibre's worker modules, on this origin. */
|
|
4
|
+
export declare const WORKER_PATH: string;
|
|
5
|
+
export declare const MapView: import("react").ForwardRefExoticComponent<MapViewProps & import("react").RefAttributes<MapViewRef>>;
|