@goway.to/maps 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +676 -0
  3. package/NOTICE +25 -0
  4. package/README.md +59 -2
  5. package/app.plugin.js +19 -0
  6. package/dist/CameraBinding.web.d.ts +24 -0
  7. package/dist/CameraBinding.web.js +46 -0
  8. package/dist/DefaultMapMarker.d.ts +8 -0
  9. package/dist/DefaultMapMarker.js +30 -0
  10. package/dist/GeoJSONSource.d.ts +2 -0
  11. package/dist/GeoJSONSource.js +17 -0
  12. package/dist/GeoJSONSource.web.d.ts +2 -0
  13. package/dist/GeoJSONSource.web.js +68 -0
  14. package/dist/InteractionBinding.web.d.ts +7 -0
  15. package/dist/InteractionBinding.web.js +35 -0
  16. package/dist/Layer.d.ts +2 -0
  17. package/dist/Layer.js +21 -0
  18. package/dist/Layer.web.d.ts +2 -0
  19. package/dist/Layer.web.js +46 -0
  20. package/dist/LayerStyle.web.d.ts +10 -0
  21. package/dist/LayerStyle.web.js +45 -0
  22. package/dist/LightBinding.web.d.ts +5 -0
  23. package/dist/LightBinding.web.js +19 -0
  24. package/dist/MapAttribution.d.ts +27 -0
  25. package/dist/MapAttribution.js +149 -0
  26. package/dist/MapBrand.d.ts +22 -0
  27. package/dist/MapBrand.js +91 -0
  28. package/dist/MapErrorState.d.ts +9 -0
  29. package/dist/MapErrorState.js +58 -0
  30. package/dist/MapPlugin.d.ts +6 -0
  31. package/dist/MapPlugin.js +3 -0
  32. package/dist/MapPlugin.web.d.ts +2 -0
  33. package/dist/MapPlugin.web.js +43 -0
  34. package/dist/MapView.d.ts +2 -0
  35. package/dist/MapView.js +420 -0
  36. package/dist/MapView.web.d.ts +5 -0
  37. package/dist/MapView.web.js +413 -0
  38. package/dist/Marker.d.ts +13 -0
  39. package/dist/Marker.js +11 -0
  40. package/dist/Marker.web.d.ts +15 -0
  41. package/dist/Marker.web.js +51 -0
  42. package/dist/Markers.d.ts +2 -0
  43. package/dist/Markers.js +13 -0
  44. package/dist/Markers.web.d.ts +2 -0
  45. package/dist/Markers.web.js +13 -0
  46. package/dist/PluginProps.web.d.ts +7 -0
  47. package/dist/PluginProps.web.js +19 -0
  48. package/dist/RasterDEMSource.d.ts +2 -0
  49. package/dist/RasterDEMSource.js +7 -0
  50. package/dist/RasterDEMSource.web.d.ts +2 -0
  51. package/dist/RasterDEMSource.web.js +44 -0
  52. package/dist/SourceData.web.d.ts +6 -0
  53. package/dist/SourceData.web.js +14 -0
  54. package/dist/UserLocation.d.ts +2 -0
  55. package/dist/UserLocation.js +19 -0
  56. package/dist/UserLocation.web.d.ts +2 -0
  57. package/dist/UserLocation.web.js +21 -0
  58. package/dist/brand/BrandedChrome.d.ts +47 -0
  59. package/dist/brand/BrandedChrome.js +51 -0
  60. package/dist/brand/GowayLogo.d.ts +28 -0
  61. package/dist/brand/GowayLogo.js +50 -0
  62. package/dist/brand/artwork.d.ts +130 -0
  63. package/dist/brand/artwork.js +176 -0
  64. package/dist/cameraReports.web.d.ts +50 -0
  65. package/dist/cameraReports.web.js +117 -0
  66. package/dist/cameraTarget.d.ts +14 -0
  67. package/dist/cameraTarget.js +8 -0
  68. package/dist/chromeLift.d.ts +15 -0
  69. package/dist/chromeLift.js +15 -0
  70. package/dist/clusters.d.ts +57 -0
  71. package/dist/clusters.js +102 -0
  72. package/dist/commands.web.d.ts +16 -0
  73. package/dist/commands.web.js +158 -0
  74. package/dist/compactCredit.d.ts +52 -0
  75. package/dist/compactCredit.js +40 -0
  76. package/dist/creditHeight.d.ts +3 -0
  77. package/dist/creditHeight.js +12 -0
  78. package/dist/creditLabels.d.ts +30 -0
  79. package/dist/creditLabels.js +30 -0
  80. package/dist/creditRise.d.ts +28 -0
  81. package/dist/creditRise.js +33 -0
  82. package/dist/dragAxes.web.d.ts +65 -0
  83. package/dist/dragAxes.web.js +70 -0
  84. package/dist/engineContext.web.d.ts +13 -0
  85. package/dist/engineContext.web.js +12 -0
  86. package/dist/index.d.ts +12 -0
  87. package/dist/index.js +12 -0
  88. package/dist/index.web.d.ts +13 -0
  89. package/dist/index.web.js +13 -0
  90. package/dist/labels.d.ts +202 -0
  91. package/dist/labels.js +451 -0
  92. package/dist/loadedCamera.d.ts +64 -0
  93. package/dist/loadedCamera.js +56 -0
  94. package/dist/locationStore.d.ts +16 -0
  95. package/dist/locationStore.js +132 -0
  96. package/dist/mapRef.d.ts +4 -0
  97. package/dist/mapRef.js +13 -0
  98. package/dist/moveMarkers.d.ts +27 -0
  99. package/dist/moveMarkers.js +22 -0
  100. package/dist/nativeCameraReports.d.ts +44 -0
  101. package/dist/nativeCameraReports.js +73 -0
  102. package/dist/plugin.d.ts +63 -0
  103. package/dist/plugin.js +2 -0
  104. package/dist/queryLayers.d.ts +19 -0
  105. package/dist/queryLayers.js +54 -0
  106. package/dist/shared-exports.d.ts +17 -0
  107. package/dist/shared-exports.js +13 -0
  108. package/dist/shared.d.ts +170 -0
  109. package/dist/shared.js +337 -0
  110. package/dist/sourceContext.d.ts +2 -0
  111. package/dist/sourceContext.js +9 -0
  112. package/dist/styleUrl.d.ts +19 -0
  113. package/dist/styleUrl.js +22 -0
  114. package/dist/testing.d.ts +41 -0
  115. package/dist/testing.js +80 -0
  116. package/dist/types.d.ts +348 -0
  117. package/dist/types.js +1 -0
  118. package/dist/useMarkerSlots.d.ts +6 -0
  119. package/dist/useMarkerSlots.js +56 -0
  120. package/dist/useUserLocation.d.ts +44 -0
  121. package/dist/useUserLocation.js +247 -0
  122. package/dist/zoom.d.ts +2 -0
  123. package/dist/zoom.js +10 -0
  124. package/metro.cjs +70 -0
  125. package/package.json +115 -4
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The web canvas's camera reports: MapLibre's `load`, `movestart`, `move` and
3
+ * `moveend` turned into `MapViewProps.onCameraChanged`.
4
+ *
5
+ * Its own module so it runs against a fake engine in a test (the real one
6
+ * needs WebGL); `MapCanvas.web.tsx` attaches it to every map it builds.
7
+ *
8
+ * - `load`: the camera the map opened at, by the rule both forks share
9
+ * (`loadedCamera.ts`).
10
+ * - `move`: the live camera, throttled, never settled.
11
+ * - `moveend`: a settled camera, unless it ends a frame of a caller-driven
12
+ * animation (`MapCameraOptions.transient`), which `move` already reported.
13
+ *
14
+ * `source` comes from the marker GoWay puts on its own camera commands: a
15
+ * marked move is `programmatic` and never arms "Search this area"; so is a
16
+ * container resize (`moveSourceOf`).
17
+ */
18
+ import type { Map as MapLibreMap } from 'maplibre-gl';
19
+ import { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT } from './moveMarkers';
20
+ export { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT };
21
+ import type { MapMoveSource, MapCameraChange } from './types';
22
+ /** Streaming viewport updates while a drag is in progress. */
23
+ export declare const MOVE_THROTTLE_MS = 100;
24
+ /**
25
+ * Who moved the camera: GoWay (a marked command), the engine itself because
26
+ * its container changed size, or — anything else — the reader.
27
+ *
28
+ * A container resize keeps the centre and zoom and changes only how much is
29
+ * on screen; nobody chose it, so it must neither arm "Search this area" nor
30
+ * end what a reader's move ends (a landmark tour).
31
+ */
32
+ export declare function moveSourceOf(event: unknown): MapMoveSource;
33
+ /**
34
+ * A move MapLibre made because its container was resized. Its ResizeObserver
35
+ * hands the observer's entries to `resize()` as the event data, so the move's
36
+ * events carry them — the test MapLibre's own GeolocateControl uses
37
+ * (`event[0] instanceof ResizeObserverEntry`).
38
+ */
39
+ export declare function isContainerResize(event: unknown): boolean;
40
+ /** A camera move a pointer, wheel, key or touch caused (MapLibre attaches it). */
41
+ export declare function isGestureMove(event: unknown): boolean;
42
+ export declare function isTransientMove(event: unknown): boolean;
43
+ /** The part of a MapLibre map the reports read and listen to. */
44
+ export type CameraEngine = Pick<MapLibreMap, 'on' | 'off' | 'getCenter' | 'getBounds' | 'getZoom' | 'getBearing' | 'getPitch'>;
45
+ /**
46
+ * Report `map`'s camera to whatever `listener()` answers at the time — `null`
47
+ * for a caller with no `onCameraChanged` (the embed), which costs nothing.
48
+ * Returns the detach.
49
+ */
50
+ export declare function attachCameraReports(map: CameraEngine, listener: () => ((change: MapCameraChange) => void) | null | undefined, now?: () => number): () => void;
@@ -0,0 +1,117 @@
1
+ import { createLoadedCameraReporter } from './loadedCamera';
2
+ import { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT } from './moveMarkers';
3
+ import { runEngineCommand } from './shared';
4
+ export { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT };
5
+ /** Streaming viewport updates while a drag is in progress. */
6
+ export const MOVE_THROTTLE_MS = 100;
7
+ /**
8
+ * Who moved the camera: GoWay (a marked command), the engine itself because
9
+ * its container changed size, or — anything else — the reader.
10
+ *
11
+ * A container resize keeps the centre and zoom and changes only how much is
12
+ * on screen; nobody chose it, so it must neither arm "Search this area" nor
13
+ * end what a reader's move ends (a landmark tour).
14
+ */
15
+ export function moveSourceOf(event) {
16
+ const marked = event;
17
+ if (marked?.gowayProgrammatic === true)
18
+ return 'programmatic';
19
+ return isContainerResize(event) ? 'programmatic' : 'user';
20
+ }
21
+ /**
22
+ * A move MapLibre made because its container was resized. Its ResizeObserver
23
+ * hands the observer's entries to `resize()` as the event data, so the move's
24
+ * events carry them — the test MapLibre's own GeolocateControl uses
25
+ * (`event[0] instanceof ResizeObserverEntry`).
26
+ */
27
+ export function isContainerResize(event) {
28
+ if (typeof ResizeObserverEntry === 'undefined')
29
+ return false;
30
+ return event?.[0] instanceof ResizeObserverEntry;
31
+ }
32
+ /** A camera move a pointer, wheel, key or touch caused (MapLibre attaches it). */
33
+ export function isGestureMove(event) {
34
+ return event?.originalEvent != null;
35
+ }
36
+ export function isTransientMove(event) {
37
+ return event?.gowayTransient === true;
38
+ }
39
+ /**
40
+ * Report `map`'s camera to whatever `listener()` answers at the time — `null`
41
+ * for a caller with no `onCameraChanged` (the embed), which costs nothing.
42
+ * Returns the detach.
43
+ */
44
+ export function attachCameraReports(map, listener, now = Date.now) {
45
+ let lastMoveReport = 0;
46
+ // This engine's one loaded-camera report (`loadedCamera.ts`). A command
47
+ // issued before `load` starts and ends its move before `load` too (`jumpTo`
48
+ // fires both synchronously; `easeTo` is still moving), so the reporter knows
49
+ // at `load` whether the camera is settled.
50
+ const loaded = createLoadedCameraReporter();
51
+ // `getCenter`/`getBounds` BUILD `LngLat`s out of the live transform, so a
52
+ // camera that has gone non-finite throws HERE — and this runs from the
53
+ // engine's own `move` event, which `jumpTo` fires synchronously inside
54
+ // whatever effect commanded it. Not reporting a viewport is a stale
55
+ // "Search this area"; throwing is the error boundary.
56
+ const read = () => {
57
+ let reading = null;
58
+ runEngineCommand('getViewport', () => {
59
+ const center = map.getCenter();
60
+ const bounds = map.getBounds();
61
+ reading = {
62
+ viewport: {
63
+ latitude: center.lat,
64
+ longitude: center.lng,
65
+ zoom: map.getZoom(),
66
+ bearing: map.getBearing(),
67
+ pitch: map.getPitch(),
68
+ },
69
+ bounds: {
70
+ west: bounds.getWest(),
71
+ south: bounds.getSouth(),
72
+ east: bounds.getEast(),
73
+ north: bounds.getNorth(),
74
+ },
75
+ };
76
+ });
77
+ return reading;
78
+ };
79
+ const report = (changeOf) => {
80
+ const change = listener();
81
+ if (!change)
82
+ return;
83
+ const reading = read();
84
+ if (!reading)
85
+ return;
86
+ const next = changeOf(reading);
87
+ if (next)
88
+ change(next);
89
+ };
90
+ const handleLoad = () => loaded.loaded(read, listener);
91
+ const handleMoveStart = () => loaded.moveStarted();
92
+ const handleMove = (event) => {
93
+ const at = now();
94
+ if (at - lastMoveReport < MOVE_THROTTLE_MS)
95
+ return;
96
+ lastMoveReport = at;
97
+ report((reading) => ({ ...reading, source: moveSourceOf(event), isFinal: false }));
98
+ };
99
+ const handleMoveEnd = (event) => {
100
+ const transient = isTransientMove(event);
101
+ loaded.moveEnded(transient);
102
+ if (transient)
103
+ return;
104
+ lastMoveReport = 0;
105
+ report((reading) => ({ ...reading, source: moveSourceOf(event), isFinal: true }));
106
+ };
107
+ map.on('load', handleLoad);
108
+ map.on('movestart', handleMoveStart);
109
+ map.on('move', handleMove);
110
+ map.on('moveend', handleMoveEnd);
111
+ return () => {
112
+ map.off('load', handleLoad);
113
+ map.off('movestart', handleMoveStart);
114
+ map.off('move', handleMove);
115
+ map.off('moveend', handleMoveEnd);
116
+ };
117
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The VALUE a `<Camera>` target stands for, as a string a binding can key on.
3
+ *
4
+ * A box or a point arrives as a fresh object on every render of a screen that
5
+ * derives it; keying on the object would re-frame the camera on every render,
6
+ * under a reader who has panned away. Keying on its numbers re-frames it only
7
+ * when the target really changed. `null` is "no target".
8
+ */
9
+ import type { GeoBoundingBox, GeoCoordinate } from './types';
10
+ export declare function cameraTargetKey(target: {
11
+ bounds?: GeoBoundingBox | null;
12
+ center?: GeoCoordinate | null;
13
+ zoom?: number;
14
+ }): string | null;
@@ -0,0 +1,8 @@
1
+ export function cameraTargetKey(target) {
2
+ const { bounds, center, zoom } = target;
3
+ if (bounds)
4
+ return `b:${bounds.west},${bounds.south},${bounds.east},${bounds.north}`;
5
+ if (center)
6
+ return `c:${center.latitude},${center.longitude},${zoom ?? ''}`;
7
+ return null;
8
+ }
@@ -0,0 +1,15 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ export interface MapChromeLift {
3
+ /** px to translate up, on the UI thread. */
4
+ lift: SharedValue<number>;
5
+ /** 0 … 1, on the UI thread. */
6
+ opacity: SharedValue<number>;
7
+ /**
8
+ * px the screen keeps for its own controls at the bottom-right ("My
9
+ * location"), which a full-width piece of map chrome — the time-of-day
10
+ * scrubber — leaves free so the two never overlap.
11
+ */
12
+ rightClearance: number;
13
+ }
14
+ export declare const MapChromeLiftContext: import("react").Context<MapChromeLift | null>;
15
+ export declare function useMapChromeLift(): MapChromeLift | null;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The live lift a screen with a bottom sheet hands the map's own bottom chrome
3
+ * — the data credit (`MapAttribution`) — so it stays above the sheet's CURRENT
4
+ * top edge rather than only above its resting claim.
5
+ *
6
+ * Optional by design: a map with no provider lays the credit out against
7
+ * Bloom's bottom edge
8
+ * exactly as before. A provider can move the credit; nothing can hide it but
9
+ * the sheet itself covering the map.
10
+ */
11
+ import { createContext, useContext } from 'react';
12
+ export const MapChromeLiftContext = createContext(null);
13
+ export function useMapChromeLift() {
14
+ return useContext(MapChromeLiftContext);
15
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What `<Markers>` draws at a zoom: each marker, or the cluster that holds it.
3
+ *
4
+ * Mapbox clusters by SOURCE OPTIONS — `cluster`, `clusterRadius` (50),
5
+ * `clusterMaxZoom` (14) — and draws the result as GL layers. `<Markers>` takes
6
+ * the same options with the same defaults, but its markers are Bloom
7
+ * components (buttons with a name and a pressed state), which neither engine
8
+ * can draw: the GL layer's clustered features never come back to JS in time to
9
+ * become React views. So the clustering runs here, with Supercluster —
10
+ * Mapbox's own clustering library, the one Mapbox GL's `cluster: true` uses —
11
+ * over the same 512 px tile grid MapLibre's zoom levels are defined against,
12
+ * which is what makes `clusterRadius` mean the same pixels it means there.
13
+ *
14
+ * Pure, and derived in render from the data and the integer zoom: a cluster is
15
+ * a function of what is shown and how far in, never a piece of state.
16
+ *
17
+ * ## A pin sits where its data put it
18
+ *
19
+ * A point is drawn at the coordinate it arrived with, looked up by id — never
20
+ * at a position recomputed from an index — so a published coordinate is drawn
21
+ * at exactly the precision it was published with. A cluster has no position of
22
+ * its own, so its centroid is the only place it can be drawn.
23
+ */
24
+ import Supercluster from 'supercluster';
25
+ import type { GeoCoordinate, MapMarkerData, MapMarkerItem } from './types';
26
+ export interface ClusterOptions {
27
+ radius: number;
28
+ maxZoom: number;
29
+ minPoints: number;
30
+ }
31
+ /** Mapbox's defaults (`clusterRadius` 50, `clusterMaxZoom` 14), and two points make a cluster. */
32
+ export declare const DEFAULT_CLUSTER_OPTIONS: ClusterOptions;
33
+ /** One thing to draw, keyed stably for React and for the engine's marker. */
34
+ export interface MarkerSlot {
35
+ key: string;
36
+ coordinate: GeoCoordinate;
37
+ item: MapMarkerItem;
38
+ /** For a cluster: the zoom at which it splits. */
39
+ expansionZoom?: number;
40
+ }
41
+ type PointProperties = {
42
+ id: string;
43
+ };
44
+ export interface MarkerIndex {
45
+ /** Every marker by id. */
46
+ byId: ReadonlyMap<string, MapMarkerData>;
47
+ /** `null` when clustering is off. */
48
+ clusters: Supercluster<PointProperties, Supercluster.AnyProps> | null;
49
+ }
50
+ /** Build the index once per data set and option set. */
51
+ export declare function indexMarkers(data: readonly MapMarkerData[], options: ClusterOptions | null): MarkerIndex;
52
+ /**
53
+ * What to draw at `zoom`, with Bloom's states: the selected marker (and the
54
+ * cluster holding it) `active`, markers pressed before `visited`.
55
+ */
56
+ export declare function markerSlotsAt(index: MarkerIndex, zoom: number, selectedId: string | null | undefined, visited: ReadonlySet<string>): MarkerSlot[];
57
+ export {};
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What `<Markers>` draws at a zoom: each marker, or the cluster that holds it.
3
+ *
4
+ * Mapbox clusters by SOURCE OPTIONS — `cluster`, `clusterRadius` (50),
5
+ * `clusterMaxZoom` (14) — and draws the result as GL layers. `<Markers>` takes
6
+ * the same options with the same defaults, but its markers are Bloom
7
+ * components (buttons with a name and a pressed state), which neither engine
8
+ * can draw: the GL layer's clustered features never come back to JS in time to
9
+ * become React views. So the clustering runs here, with Supercluster —
10
+ * Mapbox's own clustering library, the one Mapbox GL's `cluster: true` uses —
11
+ * over the same 512 px tile grid MapLibre's zoom levels are defined against,
12
+ * which is what makes `clusterRadius` mean the same pixels it means there.
13
+ *
14
+ * Pure, and derived in render from the data and the integer zoom: a cluster is
15
+ * a function of what is shown and how far in, never a piece of state.
16
+ *
17
+ * ## A pin sits where its data put it
18
+ *
19
+ * A point is drawn at the coordinate it arrived with, looked up by id — never
20
+ * at a position recomputed from an index — so a published coordinate is drawn
21
+ * at exactly the precision it was published with. A cluster has no position of
22
+ * its own, so its centroid is the only place it can be drawn.
23
+ */
24
+ import Supercluster from 'supercluster';
25
+ /** Mapbox's defaults (`clusterRadius` 50, `clusterMaxZoom` 14), and two points make a cluster. */
26
+ export const DEFAULT_CLUSTER_OPTIONS = { radius: 50, maxZoom: 14, minPoints: 2 };
27
+ /** Build the index once per data set and option set. */
28
+ export function indexMarkers(data, options) {
29
+ const byId = new Map();
30
+ for (const marker of data)
31
+ byId.set(marker.id, marker);
32
+ if (!options)
33
+ return { byId, clusters: null };
34
+ const clusters = new Supercluster({
35
+ radius: options.radius,
36
+ maxZoom: options.maxZoom,
37
+ minPoints: options.minPoints,
38
+ // MapLibre's zoom is defined on 512 px tiles: a radius in this extent is
39
+ // the same number of pixels on screen.
40
+ extent: 512,
41
+ });
42
+ clusters.load(data.map((marker) => ({
43
+ type: 'Feature',
44
+ geometry: {
45
+ type: 'Point',
46
+ coordinates: [marker.coordinate.longitude, marker.coordinate.latitude],
47
+ },
48
+ properties: { id: marker.id },
49
+ })));
50
+ return { byId, clusters };
51
+ }
52
+ const WORLD = [-180, -85.051129, 180, 85.051129];
53
+ /**
54
+ * What to draw at `zoom`, with Bloom's states: the selected marker (and the
55
+ * cluster holding it) `active`, markers pressed before `visited`.
56
+ */
57
+ export function markerSlotsAt(index, zoom, selectedId, visited) {
58
+ const pointState = (id) => id === selectedId ? 'active' : visited.has(id) ? 'visited' : 'default';
59
+ if (!index.clusters) {
60
+ return [...index.byId.values()].map((marker) => ({
61
+ key: `point:${marker.id}`,
62
+ coordinate: marker.coordinate,
63
+ item: { kind: 'point', marker, state: pointState(marker.id) },
64
+ }));
65
+ }
66
+ const slots = [];
67
+ for (const feature of index.clusters.getClusters(WORLD, Math.floor(zoom))) {
68
+ const properties = feature.properties;
69
+ if (properties.cluster && typeof properties.cluster_id === 'number') {
70
+ const clusterId = properties.cluster_id;
71
+ const leaves = index.clusters.getLeaves(clusterId, Number.POSITIVE_INFINITY);
72
+ const markerIds = leaves.map((leaf) => String(leaf.properties.id));
73
+ const [longitude, latitude] = feature.geometry.coordinates;
74
+ const cluster = {
75
+ id: `cluster:${clusterId}`,
76
+ coordinate: { latitude, longitude },
77
+ count: properties.point_count ?? markerIds.length,
78
+ markerIds,
79
+ };
80
+ slots.push({
81
+ key: cluster.id,
82
+ coordinate: cluster.coordinate,
83
+ item: {
84
+ kind: 'cluster',
85
+ cluster,
86
+ state: selectedId != null && markerIds.includes(selectedId) ? 'active' : 'default',
87
+ },
88
+ expansionZoom: index.clusters.getClusterExpansionZoom(clusterId),
89
+ });
90
+ continue;
91
+ }
92
+ const marker = properties.id !== undefined ? index.byId.get(properties.id) : undefined;
93
+ if (!marker)
94
+ continue;
95
+ slots.push({
96
+ key: `point:${marker.id}`,
97
+ coordinate: marker.coordinate,
98
+ item: { kind: 'point', marker, state: pointState(marker.id) },
99
+ });
100
+ }
101
+ return slots;
102
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The web map's camera commands — the `MapViewRef` and `<Camera>` both move
3
+ * the camera through these, so the guards below are the only route a camera
4
+ * change takes into maplibre-gl.
5
+ *
6
+ * Every command is marked PROGRAMMATIC (`cameraReports.web.ts`), so the camera
7
+ * it produces is reported as the app's move and never as the reader's.
8
+ */
9
+ import type { Map as MapLibreMap } from 'maplibre-gl';
10
+ import type { MapViewRef } from './types';
11
+ /** Zoom used when a fit has nothing with area to fit — see `isDegenerateBounds`. */
12
+ export declare const DEGENERATE_FIT_ZOOM = 15;
13
+ /** Breathing room, in px, around a fitted box. */
14
+ export declare const DEFAULT_FIT_PADDING = 48;
15
+ export declare const DEFAULT_CAMERA_DURATION_MS = 500;
16
+ export declare function createWebMapApi(getMap: () => MapLibreMap | null): MapViewRef;
@@ -0,0 +1,158 @@
1
+ import { boundsCenter, boundsOf, isDegenerateBounds } from '@goway.to/sdk/geo';
2
+ import { PROGRAMMATIC, PROGRAMMATIC_TRANSIENT } from './cameraReports.web';
3
+ import { asFinite, describeNumbers, isDrawableBounds, isDrawableCoordinate, isViewport, optionalScalar, reportMapDefect, resolvePadding, runEngineCommand, toBoundsArray, toLngLat, } from './shared';
4
+ /** Zoom used when a fit has nothing with area to fit — see `isDegenerateBounds`. */
5
+ export const DEGENERATE_FIT_ZOOM = 15;
6
+ /** Breathing room, in px, around a fitted box. */
7
+ export const DEFAULT_FIT_PADDING = 48;
8
+ export const DEFAULT_CAMERA_DURATION_MS = 500;
9
+ export function createWebMapApi(getMap) {
10
+ const api = {
11
+ moveTo(target, options) {
12
+ const map = getMap();
13
+ if (!map)
14
+ return false;
15
+ const coordinate = isViewport(target)
16
+ ? { latitude: target.latitude, longitude: target.longitude }
17
+ : target;
18
+ // Leaving the camera where it is beats throwing out of the handler that
19
+ // asked for the move.
20
+ if (!isDrawableCoordinate(coordinate)) {
21
+ reportMapDefect('moveTo', `Ignored moveTo: target is not drawable (${describeNumbers(coordinate)}).`);
22
+ return false;
23
+ }
24
+ const viewport = isViewport(target) ? target : undefined;
25
+ // `asFinite` on every scalar: a NaN zoom makes `transform.worldSize` NaN,
26
+ // and the next `getBounds()` throws `Invalid LngLat object: (NaN, NaN)`.
27
+ const duration = asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS;
28
+ const marker = options?.transient ? PROGRAMMATIC_TRANSIENT : PROGRAMMATIC;
29
+ const camera = {
30
+ center: toLngLat(coordinate),
31
+ zoom: asFinite(options?.zoom) ?? asFinite(viewport?.zoom) ?? map.getZoom(),
32
+ bearing: asFinite(options?.bearing) ?? asFinite(viewport?.bearing) ?? map.getBearing(),
33
+ pitch: asFinite(options?.pitch) ?? asFinite(viewport?.pitch) ?? map.getPitch(),
34
+ };
35
+ // A narrower lens (and back); range-checked so a bad value leaves the
36
+ // lens alone rather than inverting the camera.
37
+ const fov = asFinite(options?.fieldOfView);
38
+ if (fov !== undefined && fov >= 10 && fov <= 60 && fov !== map.getVerticalFieldOfView()) {
39
+ runEngineCommand('setVerticalFieldOfView', () => map.setVerticalFieldOfView(fov, marker));
40
+ }
41
+ if (duration <= 0) {
42
+ return runEngineCommand('jumpTo', () => map.jumpTo(camera, marker));
43
+ }
44
+ return runEngineCommand('easeTo', () => map.easeTo({ ...camera, duration }, marker));
45
+ },
46
+ fitBounds(bounds, options) {
47
+ const map = getMap();
48
+ if (!map)
49
+ return;
50
+ // `isDegenerateBounds` answers `false` for a NaN box — every comparison
51
+ // against NaN is false — so this check comes FIRST.
52
+ if (!isDrawableBounds(bounds)) {
53
+ reportMapDefect('fitBounds', `Ignored fitBounds: box is not drawable (${describeNumbers(bounds)}).`);
54
+ return;
55
+ }
56
+ const padding = resolvePadding(options?.padding, DEFAULT_FIT_PADDING);
57
+ if (!padding)
58
+ return;
59
+ // A canvas with no size is a real state (the first frame before layout,
60
+ // a hidden tab): `cameraForBoxAndBearing` divides the free viewport by
61
+ // the box, so zero pixels is `0 / 0` and a NaN centre that throws.
62
+ const container = map.getContainer();
63
+ if (!container.clientWidth || !container.clientHeight) {
64
+ reportMapDefect('fitBounds:noCanvas', 'Ignored fitBounds: the canvas has no size yet.');
65
+ return;
66
+ }
67
+ // `maxZoom` goes straight into `Math.min(computedZoom, maxZoom)`, and
68
+ // `Math.min(x, undefined)` is NaN — so an absent cap must be an ABSENT
69
+ // key, never a present-and-undefined one (`optionalScalar`).
70
+ const maxZoom = asFinite(options?.maxZoom);
71
+ const duration = asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS;
72
+ if (isDegenerateBounds(bounds)) {
73
+ const centre = boundsCenter(bounds);
74
+ runEngineCommand('easeTo', () => map.easeTo({
75
+ center: toLngLat(centre),
76
+ zoom: Math.min(maxZoom ?? DEGENERATE_FIT_ZOOM, DEGENERATE_FIT_ZOOM),
77
+ duration,
78
+ }, PROGRAMMATIC));
79
+ return;
80
+ }
81
+ const [west, south, east, north] = toBoundsArray(bounds);
82
+ runEngineCommand('fitBounds', () => map.fitBounds([
83
+ [west, south],
84
+ [east, north],
85
+ ], { padding, duration, ...optionalScalar('maxZoom', maxZoom) }, PROGRAMMATIC));
86
+ },
87
+ fitCoordinates(coordinates, options) {
88
+ const bounds = boundsOf(coordinates);
89
+ if (!bounds)
90
+ return;
91
+ api.fitBounds(bounds, options);
92
+ },
93
+ setBearing(bearing, options) {
94
+ const heading = asFinite(bearing);
95
+ if (heading === undefined) {
96
+ reportMapDefect('setBearing', `Ignored setBearing: ${String(bearing)} is not a bearing.`);
97
+ return;
98
+ }
99
+ const map = getMap();
100
+ if (!map)
101
+ return;
102
+ runEngineCommand('easeTo', () => map.easeTo({
103
+ bearing: heading,
104
+ duration: asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS,
105
+ }, PROGRAMMATIC));
106
+ },
107
+ resetNorth(options) {
108
+ const map = getMap();
109
+ if (!map)
110
+ return;
111
+ runEngineCommand('easeTo', () => map.easeTo({
112
+ bearing: 0,
113
+ pitch: 0,
114
+ duration: asFinite(options?.duration) ?? DEFAULT_CAMERA_DURATION_MS,
115
+ }, PROGRAMMATIC));
116
+ },
117
+ async getViewport() {
118
+ const map = getMap();
119
+ if (!map)
120
+ return null;
121
+ const center = map.getCenter();
122
+ return {
123
+ latitude: center.lat,
124
+ longitude: center.lng,
125
+ zoom: map.getZoom(),
126
+ bearing: map.getBearing(),
127
+ pitch: map.getPitch(),
128
+ };
129
+ },
130
+ async getBounds() {
131
+ const map = getMap();
132
+ if (!map)
133
+ return null;
134
+ const bounds = map.getBounds();
135
+ return {
136
+ west: bounds.getWest(),
137
+ south: bounds.getSouth(),
138
+ east: bounds.getEast(),
139
+ north: bounds.getNorth(),
140
+ };
141
+ },
142
+ captureFrames(draw) {
143
+ const map = getMap();
144
+ if (!map)
145
+ return () => { };
146
+ // `render` fires right after MapLibre drew the frame, in the same task —
147
+ // the one moment a WebGL canvas without a preserved drawing buffer can
148
+ // still be read.
149
+ const onRender = () => draw({ canvas: map.getCanvas() });
150
+ map.on('render', onRender);
151
+ map.triggerRepaint();
152
+ return () => {
153
+ map.off('render', onRender);
154
+ };
155
+ },
156
+ };
157
+ return api;
158
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * When the map's data credit is spelled out and when it is folded into an ⓘ
3
+ * button — the OSMF Attribution Guidelines' compact form
4
+ * (https://osmfoundation.org/wiki/Licence/Attribution_Guidelines): the credit
5
+ * is VISIBLE when the map loads, and may collapse once the reader has had it —
6
+ * after {@link CREDIT_AUTO_COLLAPSE_MS}, or at their first interaction with
7
+ * the map (pan, zoom, rotate, tap) — into a control in the same corner that
8
+ * shows it again.
9
+ *
10
+ * Pure, so "visible at load, collapsed only by those triggers" is a test
11
+ * (`test/compactCredit.test.ts`). `MapView` (both forks) runs it and renders
12
+ * `MapAttribution` from it, with no prop to turn the credit off.
13
+ */
14
+ /** How long the credit stays spelled out after the map has loaded. */
15
+ export declare const CREDIT_AUTO_COLLAPSE_MS = 5000;
16
+ export type CreditEvent =
17
+ /** {@link CREDIT_AUTO_COLLAPSE_MS} have passed since the map loaded. */
18
+ {
19
+ type: 'elapsed';
20
+ }
21
+ /** The reader moved or tapped the map (never a move the app made). */
22
+ | {
23
+ type: 'mapInteraction';
24
+ }
25
+ /** The reader pressed the ⓘ (or the credit's own collapse control). */
26
+ | {
27
+ type: 'toggle';
28
+ };
29
+ export interface CreditState {
30
+ expanded: boolean;
31
+ /**
32
+ * Still in the opening period, where the timer may fold it. Once the reader
33
+ * has pressed the control or touched the map, only they decide.
34
+ */
35
+ auto: boolean;
36
+ }
37
+ export declare const INITIAL_CREDIT_STATE: CreditState;
38
+ export declare function creditReducer(state: CreditState, event: CreditEvent): CreditState;
39
+ /**
40
+ * What the credit shows of a disclosure it carries (a label for simulated
41
+ * content, say): spelled out, the
42
+ * whole label as its first line; folded, its one word beside the ⓘ. In
43
+ * neither state is it hidden, so whatever folds the credit can never hide the
44
+ * label; without a disclosure, nothing.
45
+ */
46
+ export declare function creditDisclosure(expanded: boolean, disclosure: {
47
+ text: string;
48
+ short: string;
49
+ } | null): {
50
+ line: string | null;
51
+ tag: string | null;
52
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * When the map's data credit is spelled out and when it is folded into an ⓘ
3
+ * button — the OSMF Attribution Guidelines' compact form
4
+ * (https://osmfoundation.org/wiki/Licence/Attribution_Guidelines): the credit
5
+ * is VISIBLE when the map loads, and may collapse once the reader has had it —
6
+ * after {@link CREDIT_AUTO_COLLAPSE_MS}, or at their first interaction with
7
+ * the map (pan, zoom, rotate, tap) — into a control in the same corner that
8
+ * shows it again.
9
+ *
10
+ * Pure, so "visible at load, collapsed only by those triggers" is a test
11
+ * (`test/compactCredit.test.ts`). `MapView` (both forks) runs it and renders
12
+ * `MapAttribution` from it, with no prop to turn the credit off.
13
+ */
14
+ /** How long the credit stays spelled out after the map has loaded. */
15
+ export const CREDIT_AUTO_COLLAPSE_MS = 5000;
16
+ export const INITIAL_CREDIT_STATE = { expanded: true, auto: true };
17
+ export function creditReducer(state, event) {
18
+ switch (event.type) {
19
+ case 'elapsed':
20
+ // A credit the reader opened themselves is not folded under them.
21
+ return state.auto ? { expanded: false, auto: false } : state;
22
+ case 'mapInteraction':
23
+ // Back to the map: the credit gets out of the way (and stays reachable).
24
+ return state.expanded || state.auto ? { expanded: false, auto: false } : state;
25
+ case 'toggle':
26
+ return { expanded: !state.expanded, auto: false };
27
+ }
28
+ }
29
+ /**
30
+ * What the credit shows of a disclosure it carries (a label for simulated
31
+ * content, say): spelled out, the
32
+ * whole label as its first line; folded, its one word beside the ⓘ. In
33
+ * neither state is it hidden, so whatever folds the credit can never hide the
34
+ * label; without a disclosure, nothing.
35
+ */
36
+ export function creditDisclosure(expanded, disclosure) {
37
+ if (!disclosure)
38
+ return { line: null, tag: null };
39
+ return expanded ? { line: disclosure.text, tag: null } : { line: null, tag: disclosure.short };
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ export declare const MapCreditHeightContext: import("react").Context<SharedValue<number> | null>;
3
+ export declare function useMapCreditHeight(): SharedValue<number> | null;