@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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader's location permission and position, as external stores.
|
|
3
|
+
*
|
|
4
|
+
* `<UserLocation>` reads both through `useSyncExternalStore`, so the dot
|
|
5
|
+
* appears the moment a permission is granted — by `useUserLocation().locate()`
|
|
6
|
+
* on a tap, or because it already was — with no effect anywhere.
|
|
7
|
+
*
|
|
8
|
+
* The permission is READ (`getForegroundPermissionsAsync`, which never
|
|
9
|
+
* prompts) when the first `<UserLocation>` subscribes, and settled by
|
|
10
|
+
* `useUserLocation` whenever it asks. Nothing here can show a prompt.
|
|
11
|
+
*
|
|
12
|
+
* The position is watched only while something subscribes AND the permission
|
|
13
|
+
* is granted, and it is kept in memory only: no store, cache or history.
|
|
14
|
+
*/
|
|
15
|
+
import * as Location from 'expo-location';
|
|
16
|
+
import { isDrawableCoordinate, reportMapDefect } from './shared';
|
|
17
|
+
function createPermissionStore() {
|
|
18
|
+
let state = 'unknown';
|
|
19
|
+
let reading = false;
|
|
20
|
+
const listeners = new Set();
|
|
21
|
+
const notify = () => {
|
|
22
|
+
for (const listener of listeners)
|
|
23
|
+
listener();
|
|
24
|
+
};
|
|
25
|
+
const settle = (next) => {
|
|
26
|
+
if (next === state)
|
|
27
|
+
return;
|
|
28
|
+
state = next;
|
|
29
|
+
notify();
|
|
30
|
+
};
|
|
31
|
+
const read = () => {
|
|
32
|
+
if (reading || state !== 'unknown')
|
|
33
|
+
return;
|
|
34
|
+
reading = true;
|
|
35
|
+
Location.getForegroundPermissionsAsync()
|
|
36
|
+
.then((permission) => settle(permission.granted ? 'granted' : 'denied'))
|
|
37
|
+
.catch(() => settle('denied'))
|
|
38
|
+
.finally(() => {
|
|
39
|
+
reading = false;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
subscribe(listener) {
|
|
44
|
+
listeners.add(listener);
|
|
45
|
+
read();
|
|
46
|
+
return () => {
|
|
47
|
+
listeners.delete(listener);
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
getSnapshot() {
|
|
51
|
+
return state;
|
|
52
|
+
},
|
|
53
|
+
/** What `useUserLocation` learned by asking. */
|
|
54
|
+
settle,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The one permission store; the platform's permission is one per app. */
|
|
58
|
+
export const locationPermission = createPermissionStore();
|
|
59
|
+
function createPositionStore() {
|
|
60
|
+
let position = null;
|
|
61
|
+
let watcher = null;
|
|
62
|
+
let starting = false;
|
|
63
|
+
let unsubscribePermission = null;
|
|
64
|
+
const listeners = new Set();
|
|
65
|
+
const notify = () => {
|
|
66
|
+
for (const listener of listeners)
|
|
67
|
+
listener();
|
|
68
|
+
};
|
|
69
|
+
const stop = () => {
|
|
70
|
+
watcher?.remove();
|
|
71
|
+
watcher = null;
|
|
72
|
+
if (position !== null) {
|
|
73
|
+
position = null;
|
|
74
|
+
notify();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const start = () => {
|
|
78
|
+
if (watcher || starting || locationPermission.getSnapshot() !== 'granted')
|
|
79
|
+
return;
|
|
80
|
+
starting = true;
|
|
81
|
+
Location.watchPositionAsync({ accuracy: Location.Accuracy.Balanced, distanceInterval: 10 }, (fix) => {
|
|
82
|
+
const next = { latitude: fix.coords.latitude, longitude: fix.coords.longitude };
|
|
83
|
+
// A fix with no usable position is "no dot", not "no map".
|
|
84
|
+
if (!isDrawableCoordinate(next)) {
|
|
85
|
+
reportMapDefect('userLocation', 'Skipped a location fix with a non-drawable coordinate.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
position = next;
|
|
89
|
+
notify();
|
|
90
|
+
})
|
|
91
|
+
.then((subscription) => {
|
|
92
|
+
if (listeners.size === 0)
|
|
93
|
+
subscription.remove();
|
|
94
|
+
else
|
|
95
|
+
watcher = subscription;
|
|
96
|
+
})
|
|
97
|
+
.catch(() => {
|
|
98
|
+
// No fix available is not a map failure; the dot simply does not appear.
|
|
99
|
+
})
|
|
100
|
+
.finally(() => {
|
|
101
|
+
starting = false;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const onPermission = () => {
|
|
105
|
+
if (locationPermission.getSnapshot() === 'granted')
|
|
106
|
+
start();
|
|
107
|
+
else
|
|
108
|
+
stop();
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
subscribe(listener) {
|
|
112
|
+
listeners.add(listener);
|
|
113
|
+
if (listeners.size === 1) {
|
|
114
|
+
unsubscribePermission = locationPermission.subscribe(onPermission);
|
|
115
|
+
onPermission();
|
|
116
|
+
}
|
|
117
|
+
return () => {
|
|
118
|
+
listeners.delete(listener);
|
|
119
|
+
if (listeners.size === 0) {
|
|
120
|
+
unsubscribePermission?.();
|
|
121
|
+
unsubscribePermission = null;
|
|
122
|
+
stop();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
getSnapshot() {
|
|
127
|
+
return position;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** The reader's position while a `<UserLocation>` is drawn, in memory only. */
|
|
132
|
+
export const userPosition = createPositionStore();
|
package/dist/mapRef.d.ts
ADDED
package/dist/mapRef.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useMap()` — the nearest `MapView`'s handle, for chrome laid over a map
|
|
3
|
+
* (a "locate me" button, a re-frame) to call from its own event handlers.
|
|
4
|
+
*/
|
|
5
|
+
import { createContext, useContext } from 'react';
|
|
6
|
+
export const MapRefContext = createContext(null);
|
|
7
|
+
/** The enclosing `MapView`'s handle. Throws outside one: there is no map to move. */
|
|
8
|
+
export function useMap() {
|
|
9
|
+
const handle = useContext(MapRefContext);
|
|
10
|
+
if (!handle)
|
|
11
|
+
throw new Error('useMap() must be called inside a <MapView>.');
|
|
12
|
+
return handle;
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marks a camera COMMAND carries, so the map reports the camera it
|
|
3
|
+
* produces as the app's move and never as the reader's. Every command
|
|
4
|
+
* `@goway.to/maps` issues is marked; an engine plugin that moves the camera
|
|
5
|
+
* itself passes {@link PROGRAMMATIC} as the command's event data
|
|
6
|
+
* (`map.easeTo(options, PROGRAMMATIC)`).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Marker put on every camera COMMAND the map issues, read back off the event.
|
|
10
|
+
*
|
|
11
|
+
* Marking our own commands (rather than sniffing for a DOM `originalEvent`) is
|
|
12
|
+
* the safer direction of failure: a command somebody forgets to mark reads as a
|
|
13
|
+
* user gesture, so "Search this area" appears once when it need not have. The
|
|
14
|
+
* alternative — treat anything unrecognised as programmatic — hides the button
|
|
15
|
+
* for good and looks like the feature was never built.
|
|
16
|
+
*/
|
|
17
|
+
export declare const PROGRAMMATIC: {
|
|
18
|
+
readonly gowayProgrammatic: true;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* One frame of a caller-driven animation (`MapCameraOptions.transient`): a
|
|
22
|
+
* programmatic move whose `moveend` is not a settled camera.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PROGRAMMATIC_TRANSIENT: {
|
|
25
|
+
readonly gowayProgrammatic: true;
|
|
26
|
+
readonly gowayTransient: true;
|
|
27
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marks a camera COMMAND carries, so the map reports the camera it
|
|
3
|
+
* produces as the app's move and never as the reader's. Every command
|
|
4
|
+
* `@goway.to/maps` issues is marked; an engine plugin that moves the camera
|
|
5
|
+
* itself passes {@link PROGRAMMATIC} as the command's event data
|
|
6
|
+
* (`map.easeTo(options, PROGRAMMATIC)`).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Marker put on every camera COMMAND the map issues, read back off the event.
|
|
10
|
+
*
|
|
11
|
+
* Marking our own commands (rather than sniffing for a DOM `originalEvent`) is
|
|
12
|
+
* the safer direction of failure: a command somebody forgets to mark reads as a
|
|
13
|
+
* user gesture, so "Search this area" appears once when it need not have. The
|
|
14
|
+
* alternative — treat anything unrecognised as programmatic — hides the button
|
|
15
|
+
* for good and looks like the feature was never built.
|
|
16
|
+
*/
|
|
17
|
+
export const PROGRAMMATIC = { gowayProgrammatic: true };
|
|
18
|
+
/**
|
|
19
|
+
* One frame of a caller-driven animation (`MapCameraOptions.transient`): a
|
|
20
|
+
* programmatic move whose `moveend` is not a settled camera.
|
|
21
|
+
*/
|
|
22
|
+
export const PROGRAMMATIC_TRANSIENT = { gowayProgrammatic: true, gowayTransient: true };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native canvas's camera reports: MapLibre Native's region events and its
|
|
3
|
+
* style load turned into `MapViewProps.onCameraChanged`.
|
|
4
|
+
*
|
|
5
|
+
* The counterpart of `cameraReports.web.ts`, kept free of React Native so it
|
|
6
|
+
* runs in a test; `MapCanvas.native.tsx` creates one per engine (a retry
|
|
7
|
+
* remounts the engine and creates another) and forwards the engine's events.
|
|
8
|
+
*
|
|
9
|
+
* - `onRegionIsChanging`: the live camera, never settled.
|
|
10
|
+
* - `onRegionDidChange`: a settled camera, unless the move was a frame of a
|
|
11
|
+
* caller-driven animation (`MapCameraOptions.transient`).
|
|
12
|
+
* - the style load: the camera the map opened at, by the rule both forks
|
|
13
|
+
* share (`loadedCamera.ts`), read with `getViewState()`.
|
|
14
|
+
*
|
|
15
|
+
* `source` is the engine's own answer: MapLibre Native says whether a finger
|
|
16
|
+
* caused the change, so GoWay never infers it from a marker on the command.
|
|
17
|
+
*/
|
|
18
|
+
import { type CameraReading } from './loadedCamera';
|
|
19
|
+
import type { MapCameraChange } from './types';
|
|
20
|
+
/** MapLibre Native's `ViewState`, structurally (`center` and `bounds` in MapLibre order). */
|
|
21
|
+
export interface NativeViewState {
|
|
22
|
+
center: readonly [number, number];
|
|
23
|
+
zoom: number;
|
|
24
|
+
bearing: number;
|
|
25
|
+
pitch: number;
|
|
26
|
+
bounds: readonly [number, number, number, number];
|
|
27
|
+
}
|
|
28
|
+
/** A region event's payload (`ViewStateChangeEvent`). */
|
|
29
|
+
export interface NativeRegionChange extends NativeViewState {
|
|
30
|
+
userInteraction: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** MapLibre Native's view state, in GoWay's terms. */
|
|
33
|
+
export declare function cameraReadingOf(view: NativeViewState): CameraReading;
|
|
34
|
+
export interface NativeCameraReports {
|
|
35
|
+
/** GoWay commanded the camera; `transient` for one frame of a caller-driven animation. */
|
|
36
|
+
commanded(transient: boolean): void;
|
|
37
|
+
regionWillChange(region: NativeRegionChange): void;
|
|
38
|
+
regionIsChanging(region: NativeRegionChange): void;
|
|
39
|
+
/** `true` when the change was a settled camera, `false` for a transient frame. */
|
|
40
|
+
regionDidChange(region: NativeRegionChange): boolean;
|
|
41
|
+
/** The style finished loading; only the engine's first load reports. */
|
|
42
|
+
loaded(getViewState: () => Promise<NativeViewState>): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function createNativeCameraReports(listener: () => ((change: MapCameraChange) => void) | null | undefined): NativeCameraReports;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native canvas's camera reports: MapLibre Native's region events and its
|
|
3
|
+
* style load turned into `MapViewProps.onCameraChanged`.
|
|
4
|
+
*
|
|
5
|
+
* The counterpart of `cameraReports.web.ts`, kept free of React Native so it
|
|
6
|
+
* runs in a test; `MapCanvas.native.tsx` creates one per engine (a retry
|
|
7
|
+
* remounts the engine and creates another) and forwards the engine's events.
|
|
8
|
+
*
|
|
9
|
+
* - `onRegionIsChanging`: the live camera, never settled.
|
|
10
|
+
* - `onRegionDidChange`: a settled camera, unless the move was a frame of a
|
|
11
|
+
* caller-driven animation (`MapCameraOptions.transient`).
|
|
12
|
+
* - the style load: the camera the map opened at, by the rule both forks
|
|
13
|
+
* share (`loadedCamera.ts`), read with `getViewState()`.
|
|
14
|
+
*
|
|
15
|
+
* `source` is the engine's own answer: MapLibre Native says whether a finger
|
|
16
|
+
* caused the change, so GoWay never infers it from a marker on the command.
|
|
17
|
+
*/
|
|
18
|
+
import { createLoadedCameraReporter } from './loadedCamera';
|
|
19
|
+
/** MapLibre Native's view state, in GoWay's terms. */
|
|
20
|
+
export function cameraReadingOf(view) {
|
|
21
|
+
const [west, south, east, north] = view.bounds;
|
|
22
|
+
return {
|
|
23
|
+
viewport: {
|
|
24
|
+
latitude: view.center[1],
|
|
25
|
+
longitude: view.center[0],
|
|
26
|
+
zoom: view.zoom,
|
|
27
|
+
bearing: view.bearing,
|
|
28
|
+
pitch: view.pitch,
|
|
29
|
+
},
|
|
30
|
+
bounds: { west, south, east, north },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function createNativeCameraReports(listener) {
|
|
34
|
+
const loadedReport = createLoadedCameraReporter();
|
|
35
|
+
// A frame of a caller-driven animation is not a settled camera. The flag is
|
|
36
|
+
// the latest COMMAND's: every camera command declares it, and a move the
|
|
37
|
+
// reader starts ends it — a flyover the reader interrupted, or one that
|
|
38
|
+
// stopped without its closing ordinary move, leaves nothing behind, so the
|
|
39
|
+
// reader's settles are final again (on the web the marker is per event).
|
|
40
|
+
// Not cleared at every move start: a flyover's frames cross the bridge as
|
|
41
|
+
// commands ahead of their region events, so the next command's flag can
|
|
42
|
+
// already be set when the previous frame's move starts.
|
|
43
|
+
let transientCommand = false;
|
|
44
|
+
const report = (region, isFinal) => {
|
|
45
|
+
listener()?.({
|
|
46
|
+
...cameraReadingOf(region),
|
|
47
|
+
source: region.userInteraction ? 'user' : 'programmatic',
|
|
48
|
+
isFinal,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
commanded(transient) {
|
|
53
|
+
transientCommand = transient;
|
|
54
|
+
},
|
|
55
|
+
regionWillChange(region) {
|
|
56
|
+
if (region.userInteraction)
|
|
57
|
+
transientCommand = false;
|
|
58
|
+
loadedReport.moveStarted();
|
|
59
|
+
},
|
|
60
|
+
regionIsChanging(region) {
|
|
61
|
+
report(region, false);
|
|
62
|
+
},
|
|
63
|
+
regionDidChange(region) {
|
|
64
|
+
const transient = transientCommand && !region.userInteraction;
|
|
65
|
+
loadedReport.moveEnded(transient);
|
|
66
|
+
report(region, !transient);
|
|
67
|
+
return !transient;
|
|
68
|
+
},
|
|
69
|
+
loaded(getViewState) {
|
|
70
|
+
loadedReport.loaded(async () => cameraReadingOf(await getViewState()), listener);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine plugins: what an app attaches to the web engine beyond sources,
|
|
3
|
+
* layers and markers — a three.js custom layer, terrain, a camera rule.
|
|
4
|
+
*
|
|
5
|
+
* A plugin is an imperative object with a lifecycle, and the `<MapPlugin>`
|
|
6
|
+
* component is the ONE place an app's plugin is bound to the engine: it
|
|
7
|
+
* attaches when the engine is ready, hands the plugin every new `props` through
|
|
8
|
+
* `update`, and detaches it when the component unmounts or the engine is
|
|
9
|
+
* rebuilt. The app passes props; it never runs an effect.
|
|
10
|
+
*
|
|
11
|
+
* Web only. On native `<MapPlugin>` renders nothing: MapLibre Native has no
|
|
12
|
+
* custom-layer API.
|
|
13
|
+
*
|
|
14
|
+
* `@goway.to/maps/plugin` is this module: the protocol, and the guards every
|
|
15
|
+
* coordinate and command crossing into an engine goes through — pure, with no
|
|
16
|
+
* React and no React Native, so an engine module (and its tests) can load it.
|
|
17
|
+
*/
|
|
18
|
+
import type { Map as MapLibreMap } from 'maplibre-gl';
|
|
19
|
+
export { asFinite, isDrawableBounds, isDrawableCoordinate, isDrawableGeoJSON, optionalScalar, reportMapDefect, runEngineCommand, toLngLat, } from './shared';
|
|
20
|
+
export { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT } from './moveMarkers';
|
|
21
|
+
/** A source's failure, as the engine reported it. */
|
|
22
|
+
export interface MapSourceError {
|
|
23
|
+
sourceId: string;
|
|
24
|
+
/** The HTTP status, when the failure was a response. */
|
|
25
|
+
status?: number;
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface MapPluginHost {
|
|
29
|
+
/** The element the engine draws into. */
|
|
30
|
+
container: HTMLElement;
|
|
31
|
+
}
|
|
32
|
+
export interface MapPluginHandle<Props> {
|
|
33
|
+
/** New props for an attached plugin. */
|
|
34
|
+
update?(props: Props): void;
|
|
35
|
+
/** Undo everything `attach` did. */
|
|
36
|
+
detach(): void;
|
|
37
|
+
/**
|
|
38
|
+
* A press at a screen point, after the markers and before the basemap's
|
|
39
|
+
* labels. `true` takes it: no label is resolved and `onPress` does not fire.
|
|
40
|
+
*/
|
|
41
|
+
press?(point: {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}): boolean;
|
|
45
|
+
/** Whether the pointer at `point` is over something this plugin draws (the cursor says so). */
|
|
46
|
+
hover?(point: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
}): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a source's error is the plugin's to handle — a decorative source
|
|
52
|
+
* that may legitimately 404 — rather than the map's degraded state. The
|
|
53
|
+
* plugin reports what it takes, if anything is worth reporting.
|
|
54
|
+
*/
|
|
55
|
+
ownsError?(error: MapSourceError): boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface MapPlugin<Props> {
|
|
58
|
+
attach(map: MapLibreMap, props: Props, host: MapPluginHost): MapPluginHandle<Props>;
|
|
59
|
+
}
|
|
60
|
+
export interface MapPluginProps<Props> {
|
|
61
|
+
plugin: MapPlugin<Props>;
|
|
62
|
+
props: Props;
|
|
63
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface QueryLayers {
|
|
2
|
+
tap: readonly string[];
|
|
3
|
+
labels: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare const NO_QUERY_LAYERS: QueryLayers;
|
|
6
|
+
interface StyleLayerLike {
|
|
7
|
+
id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
'source-layer'?: string;
|
|
10
|
+
}
|
|
11
|
+
/** The tap and label layer ids of a style document. */
|
|
12
|
+
export declare function queryLayersOf(layers: readonly StyleLayerLike[] | undefined): QueryLayers;
|
|
13
|
+
/**
|
|
14
|
+
* The query layers of the style at `url`, fetched once per URL and shared. A
|
|
15
|
+
* style that cannot be read answers with none: presses then resolve to bare
|
|
16
|
+
* map, which is what they did before labels were tappable.
|
|
17
|
+
*/
|
|
18
|
+
export declare function queryLayersForUrl(url: string): Promise<QueryLayers>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which style layers a press and a label read are queried against, derived
|
|
3
|
+
* from the style document itself.
|
|
4
|
+
*
|
|
5
|
+
* Deriving beats naming: layers can be flag-gated, renamed, or absent from a
|
|
6
|
+
* style GoWay did not author, and the OpenMapTiles `source-layer` is the part
|
|
7
|
+
* no style gets to rename. Web reads the loaded style from the engine; native,
|
|
8
|
+
* whose engine does not hand the style back to JS, reads the same document
|
|
9
|
+
* over the network once per URL ({@link queryLayersForUrl}).
|
|
10
|
+
*
|
|
11
|
+
* `tap` is symbols AND circles — the POI dot is what sits at the anchor, and
|
|
12
|
+
* the label is drawn below it, so a tap on the dot has to resolve. `labels` is
|
|
13
|
+
* symbols only: a dot whose label lost its collision is not a NAME on screen.
|
|
14
|
+
*/
|
|
15
|
+
import { isLabelSourceLayer } from './labels';
|
|
16
|
+
export const NO_QUERY_LAYERS = { tap: [], labels: [] };
|
|
17
|
+
/** The tap and label layer ids of a style document. */
|
|
18
|
+
export function queryLayersOf(layers) {
|
|
19
|
+
const tap = [];
|
|
20
|
+
const labels = [];
|
|
21
|
+
for (const layer of layers ?? []) {
|
|
22
|
+
const sourceLayer = layer['source-layer'];
|
|
23
|
+
if (!sourceLayer || !isLabelSourceLayer(sourceLayer))
|
|
24
|
+
continue;
|
|
25
|
+
if (layer.type === 'symbol') {
|
|
26
|
+
tap.push(layer.id);
|
|
27
|
+
labels.push(layer.id);
|
|
28
|
+
}
|
|
29
|
+
else if (layer.type === 'circle') {
|
|
30
|
+
tap.push(layer.id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { tap, labels };
|
|
34
|
+
}
|
|
35
|
+
const byUrl = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* The query layers of the style at `url`, fetched once per URL and shared. A
|
|
38
|
+
* style that cannot be read answers with none: presses then resolve to bare
|
|
39
|
+
* map, which is what they did before labels were tappable.
|
|
40
|
+
*/
|
|
41
|
+
export function queryLayersForUrl(url) {
|
|
42
|
+
let pending = byUrl.get(url);
|
|
43
|
+
if (!pending) {
|
|
44
|
+
pending = fetch(url)
|
|
45
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
46
|
+
.then((style) => queryLayersOf(style?.layers))
|
|
47
|
+
.catch(() => {
|
|
48
|
+
byUrl.delete(url);
|
|
49
|
+
return NO_QUERY_LAYERS;
|
|
50
|
+
});
|
|
51
|
+
byUrl.set(url, pending);
|
|
52
|
+
}
|
|
53
|
+
return pending;
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** What both entries export beyond their platform's components. */
|
|
2
|
+
export { useMap } from './mapRef';
|
|
3
|
+
export { useUserLocation } from './useUserLocation';
|
|
4
|
+
export type { LocationErrorReason, LocationPermissionStatus, UserLocationApi, UserLocationState, } from './useUserLocation';
|
|
5
|
+
export { DefaultMapMarker } from './DefaultMapMarker';
|
|
6
|
+
export { MAP_CREDIT_CLEARANCE, MAP_DATA_CREDITS } from './MapAttribution';
|
|
7
|
+
export { MAP_BRAND_CLEARANCE, MAP_BRAND_HEIGHT, MAP_BRAND_WIDTH } from './MapBrand';
|
|
8
|
+
export { CREDIT_GAP, creditRise } from './creditRise';
|
|
9
|
+
export { sameLabelSet } from './labels';
|
|
10
|
+
export { MapChromeLiftContext, useMapChromeLift } from './chromeLift';
|
|
11
|
+
export type { MapChromeLift } from './chromeLift';
|
|
12
|
+
export { MapCreditHeightContext, useMapCreditHeight } from './creditHeight';
|
|
13
|
+
export { BrandedChromeProvider, useBrandedChrome } from './brand/BrandedChrome';
|
|
14
|
+
export { GowayLogo, gowayLogoHeight } from './brand/GowayLogo';
|
|
15
|
+
export type { GowayLogoProps, GowayLogoVariant } from './brand/GowayLogo';
|
|
16
|
+
export { DEFAULT_MAP_STYLE } from './styleUrl';
|
|
17
|
+
export type { GeoBoundingBox, GeoCoordinate, GeoJSONSourceProps, LayerProps, MapCameraChange, MapCameraOptions, MapCameraTarget, MapCreditDisclosure, MapCreditLink, MapError, MapErrorReason, MapFitOptions, MapFrame, MapInitialViewState, MapInteractionOptions, MapLabelFeature, MapLabelKind, MapLayerType, MapMarkerCluster, MapMarkerData, MapMarkerItem, MapMoveSource, MapPadding, MapPressEvent, MapViewport, MapViewProps, MapViewRef, MarkersProps, RasterDEMSourceProps, ResolvedMapViewport, UserLocationProps, } from './types';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** What both entries export beyond their platform's components. */
|
|
2
|
+
export { useMap } from './mapRef';
|
|
3
|
+
export { useUserLocation } from './useUserLocation';
|
|
4
|
+
export { DefaultMapMarker } from './DefaultMapMarker';
|
|
5
|
+
export { MAP_CREDIT_CLEARANCE, MAP_DATA_CREDITS } from './MapAttribution';
|
|
6
|
+
export { MAP_BRAND_CLEARANCE, MAP_BRAND_HEIGHT, MAP_BRAND_WIDTH } from './MapBrand';
|
|
7
|
+
export { CREDIT_GAP, creditRise } from './creditRise';
|
|
8
|
+
export { sameLabelSet } from './labels';
|
|
9
|
+
export { MapChromeLiftContext, useMapChromeLift } from './chromeLift';
|
|
10
|
+
export { MapCreditHeightContext, useMapCreditHeight } from './creditHeight';
|
|
11
|
+
export { BrandedChromeProvider, useBrandedChrome } from './brand/BrandedChrome';
|
|
12
|
+
export { GowayLogo, gowayLogoHeight } from './brand/GowayLogo';
|
|
13
|
+
export { DEFAULT_MAP_STYLE } from './styleUrl';
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { GeoBoundingBox, GeoCoordinate, MapInteractionOptions, MapMarkerData, MapPadding, MapViewport } from './types';
|
|
2
|
+
/** MapLibre position order, on both engines. */
|
|
3
|
+
export type LngLat = [longitude: number, latitude: number];
|
|
4
|
+
/** MapLibre `[west, south, east, north]`, on both engines. */
|
|
5
|
+
export type LngLatBoundsArray = [west: number, south: number, east: number, north: number];
|
|
6
|
+
export declare function toLngLat(coordinate: GeoCoordinate): LngLat;
|
|
7
|
+
export declare function fromLngLat(position: readonly [number, number]): GeoCoordinate;
|
|
8
|
+
/**
|
|
9
|
+
* A box in engine order, its east edge carried past 180 when it crosses the
|
|
10
|
+
* antimeridian so both engines frame the strip it describes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toBoundsArray(bounds: GeoBoundingBox): LngLatBoundsArray;
|
|
13
|
+
/** Every interaction is on unless a caller turns it off. */
|
|
14
|
+
export declare function resolveInteraction(options?: MapInteractionOptions): Required<MapInteractionOptions>;
|
|
15
|
+
/**
|
|
16
|
+
* Normalise a `moveTo` target: a bare coordinate leaves the camera's zoom alone.
|
|
17
|
+
*
|
|
18
|
+
* `Number.isFinite`, not `typeof === 'number'` — `typeof NaN` IS `'number'`, so
|
|
19
|
+
* the old test accepted `{ latitude, longitude, zoom: NaN }` as a viewport and
|
|
20
|
+
* passed that `NaN` on to the engine. See {@link asFinite} for what that costs.
|
|
21
|
+
* A viewport whose zoom is not a number is treated as the bare coordinate it
|
|
22
|
+
* usefully is.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isViewport(target: GeoCoordinate | MapViewport): target is MapViewport;
|
|
25
|
+
/**
|
|
26
|
+
* A camera scalar the engine can use, or `undefined`.
|
|
27
|
+
*
|
|
28
|
+
* Zoom, bearing, pitch, duration and maxZoom are the seam's unchecked numbers,
|
|
29
|
+
* and a `NaN` in any of them is not a bad frame — it is the SAME crash a bad
|
|
30
|
+
* coordinate is, one step later. `transform.setZoom(NaN)` survives MapLibre's
|
|
31
|
+
* own `clamp` (every comparison against `NaN` is false), so `_scale` and
|
|
32
|
+
* `worldSize` become `NaN`; the next unprojection — `map.getBounds()`, which
|
|
33
|
+
* this canvas calls on every `move` event, and which `jumpTo` fires
|
|
34
|
+
* SYNCHRONOUSLY — builds a `LngLat` out of two `NaN`s and throws
|
|
35
|
+
* `Invalid LngLat object: (NaN, NaN)` from inside the caller's own React
|
|
36
|
+
* handler. The camera simply not moving is the better failure.
|
|
37
|
+
*/
|
|
38
|
+
export declare function asFinite(value: number | null | undefined): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* A scalar an engine's options object may carry — or NOTHING AT ALL.
|
|
41
|
+
*
|
|
42
|
+
* ## The defect this exists to remove
|
|
43
|
+
*
|
|
44
|
+
* `{ maxZoom: undefined }` and `{}` look interchangeable in TypeScript, and to
|
|
45
|
+
* MapLibre GL JS they are opposites. `Map._cameraForBoxAndBearing` fills in its
|
|
46
|
+
* own defaults with
|
|
47
|
+
*
|
|
48
|
+
* ```
|
|
49
|
+
* options = extend({ padding, offset: [0, 0], maxZoom: this.transform.maxZoom }, options);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* and `extend` is `for (const k in src) dest[k] = src[k]` — an OWN key wins
|
|
53
|
+
* even when its value is `undefined`. So passing the key at all replaces the
|
|
54
|
+
* engine's maximum zoom with `undefined`, and the very next line is
|
|
55
|
+
*
|
|
56
|
+
* ```
|
|
57
|
+
* const zoom = Math.min(scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom);
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* `Math.min(anything, undefined)` is `NaN`. That `NaN` divides into
|
|
61
|
+
* `offsetAtFinalZoom`, reaches the centre `cameraForBoxAndBearing` unprojects,
|
|
62
|
+
* and `new LngLat(NaN, NaN)` THROWS — from inside the React effect that asked
|
|
63
|
+
* for the fit, which is the error boundary and the whole app. Omitting the key
|
|
64
|
+
* is the only way to mean "use yours".
|
|
65
|
+
*
|
|
66
|
+
* Every optional scalar this seam forwards therefore goes through here, so
|
|
67
|
+
* "the caller did not say" can never arrive as "the caller said nothing".
|
|
68
|
+
*/
|
|
69
|
+
export declare function optionalScalar<K extends string>(key: K, value: number | null | undefined): Partial<Record<K, number>>;
|
|
70
|
+
/**
|
|
71
|
+
* Fit padding, expanded to the four-sided form both engines want — or `null`
|
|
72
|
+
* when it is not a number the engine can use.
|
|
73
|
+
*
|
|
74
|
+
* See {@link isDrawableCoordinate} for why a non-finite number is refused here
|
|
75
|
+
* rather than passed on: MapLibre's `cameraForBoxAndBearing` divides the free
|
|
76
|
+
* viewport by the padding, and `NaN` propagates all the way to the LngLat it
|
|
77
|
+
* constructs for the new centre, which throws. A NEGATIVE padding does not
|
|
78
|
+
* throw, but it is never what a caller meant (it frames content OUTSIDE the
|
|
79
|
+
* viewport), and a negative pair is one of the ways the free space lands on
|
|
80
|
+
* exactly zero, which does throw. So negatives clamp to zero and non-finite
|
|
81
|
+
* values refuse the fit outright.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolvePadding(padding: MapPadding | undefined, fallback: number): {
|
|
84
|
+
top: number;
|
|
85
|
+
right: number;
|
|
86
|
+
bottom: number;
|
|
87
|
+
left: number;
|
|
88
|
+
} | null;
|
|
89
|
+
/**
|
|
90
|
+
* Both MapLibre engines build a `LngLat` out of whatever they are handed, and
|
|
91
|
+
* `LngLat` THROWS on a non-number — `Invalid LngLat object: (NaN, NaN)`. Thrown
|
|
92
|
+
* from a React effect (setting a marker's position, framing a route) that
|
|
93
|
+
* reaches the nearest error boundary, so one bad pin takes the whole screen
|
|
94
|
+
* down and the user sees "Something went wrong" instead of a map.
|
|
95
|
+
*
|
|
96
|
+
* `@goway.to/maps` is the single seam every coordinate crosses on its way to
|
|
97
|
+
* an engine, so it is where that stops being possible. A marker whose
|
|
98
|
+
* coordinate is not drawable is DROPPED (a map missing one pin is a degraded
|
|
99
|
+
* map; a map that throws is no map) and a camera move whose target is not
|
|
100
|
+
* drawable is a no-op.
|
|
101
|
+
*
|
|
102
|
+
* ## Why this is not `isValidCoordinate`
|
|
103
|
+
*
|
|
104
|
+
* `@goway.to/sdk/geo`'s `isValidCoordinate` is the PRODUCT's rule — a real point
|
|
105
|
+
* on the earth, longitude within ±180. This is the ENGINE's rule, which is
|
|
106
|
+
* looser on longitude on purpose: MapLibre reports unwrapped longitudes across
|
|
107
|
+
* the antimeridian (a click at 181°E comes back as 181, not −179), and feeding
|
|
108
|
+
* one of those back in is legal and does the right thing. Rejecting it here
|
|
109
|
+
* would break panning past the date line in the name of safety. Latitude is
|
|
110
|
+
* bounded, because `LngLat` throws outside ±90 too.
|
|
111
|
+
*/
|
|
112
|
+
export declare function isDrawableCoordinate(value: unknown): value is GeoCoordinate;
|
|
113
|
+
/** The same rule as {@link isDrawableCoordinate}, applied to a box's corners. */
|
|
114
|
+
export declare function isDrawableBounds(bounds: GeoBoundingBox | null | undefined): bounds is GeoBoundingBox;
|
|
115
|
+
/**
|
|
116
|
+
* The markers an engine may be given, with the undrawable ones removed.
|
|
117
|
+
*
|
|
118
|
+
* Returns the INPUT ARRAY when nothing is wrong, so the common path adds one
|
|
119
|
+
* pass and no allocation, and the forks' effect dependencies do not change
|
|
120
|
+
* identity every render.
|
|
121
|
+
*/
|
|
122
|
+
export declare function drawableMarkers(markers: readonly MapMarkerData[] | undefined): readonly MapMarkerData[];
|
|
123
|
+
/**
|
|
124
|
+
* Every position in a GeoJSON document is a point the engine can project, and
|
|
125
|
+
* every geometry has enough of them to BE that geometry.
|
|
126
|
+
*
|
|
127
|
+
* The arity check is not pedantry about RFC 7946: a one-point `LineString` and
|
|
128
|
+
* an empty ring are exactly what a mis-sliced route produces, they draw
|
|
129
|
+
* nothing, and they are indistinguishable from "the route did not arrive"
|
|
130
|
+
* unless something says so.
|
|
131
|
+
*
|
|
132
|
+
* A composite geometry needs the same check one level up, for the same reason
|
|
133
|
+
* and one that is easier to miss: `[].every()` is `true`, so an empty
|
|
134
|
+
* `coordinates` array satisfies every per-part rule vacuously. An empty
|
|
135
|
+
* `Polygon` has no ring that can fail `ring()`, and a `MultiPolygon` of `[[]]`
|
|
136
|
+
* has no polygon that can. Both are what a slice that matched nothing
|
|
137
|
+
* produces, both reach the engine, and both draw exactly nothing — so every
|
|
138
|
+
* composite array is required to be non-empty BEFORE its parts are checked.
|
|
139
|
+
*/
|
|
140
|
+
export declare function isDrawableGeoJSON(data: GeoJSON.GeoJSON | null | undefined): boolean;
|
|
141
|
+
export declare function reportMapDefect(key: string, message: string): void;
|
|
142
|
+
/**
|
|
143
|
+
* Run one engine command, and let a throw cost the COMMAND rather than the app.
|
|
144
|
+
*
|
|
145
|
+
* This is the BACKSTOP, deliberately separate from the checks above. Those
|
|
146
|
+
* refuse the bad values we know about; this one is for the ones we do not. Every
|
|
147
|
+
* guard in this file was written after a crash, which is the evidence that the
|
|
148
|
+
* next non-finite number will arrive from somewhere nobody predicted — a
|
|
149
|
+
* renderer upgrade that starts rejecting an input it used to accept, a new
|
|
150
|
+
* option forwarded through an unexamined path, a device API that changes shape.
|
|
151
|
+
*
|
|
152
|
+
* What makes that catastrophic rather than cosmetic is WHERE these calls run:
|
|
153
|
+
* a camera move issued from a `useEffect` throws inside React's commit phase,
|
|
154
|
+
* so the nearest error boundary unmounts the tree and the user gets "Something
|
|
155
|
+
* went wrong" instead of a map. A camera that did not move is a map. That is
|
|
156
|
+
* the whole trade, and it is not close.
|
|
157
|
+
*
|
|
158
|
+
* It is not a licence to stop validating: a swallowed throw is a `console.warn`
|
|
159
|
+
* nobody reads, so anything we can name is still refused BEFORE it gets here,
|
|
160
|
+
* by a check that says which value was wrong and why.
|
|
161
|
+
*
|
|
162
|
+
* Answers whether the command ran: `false` is a dropped command, which a
|
|
163
|
+
* caller waiting on its outcome (a camera move's settle) must not wait for.
|
|
164
|
+
*/
|
|
165
|
+
export declare function runEngineCommand(command: string, run: () => void): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* `key NaN` — never `JSON.stringify`, which prints `NaN` and `Infinity` as
|
|
168
|
+
* `null` and so erases the one detail the reader needs.
|
|
169
|
+
*/
|
|
170
|
+
export declare function describeNumbers(value: unknown): string;
|