panelui-native 0.22.5 → 0.25.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/README.md +8 -3
- package/lib/module/components/area-chart/index.js +826 -0
- package/lib/module/components/area-chart/index.js.map +1 -0
- package/lib/module/components/avatar/index.js +1 -1
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +853 -0
- package/lib/module/components/bar-chart/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +207 -23
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/breadcrumb/index.js +275 -0
- package/lib/module/components/breadcrumb/index.js.map +1 -0
- package/lib/module/components/chip/index.js +2 -2
- package/lib/module/components/direction/index.js +13 -19
- package/lib/module/components/direction/index.js.map +1 -1
- package/lib/module/components/frame/index.js +17 -0
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +28 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +2 -2
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +4 -146
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/loader/index.js +758 -0
- package/lib/module/components/loader/index.js.map +1 -0
- package/lib/module/components/map/basemap.js +265 -0
- package/lib/module/components/map/basemap.js.map +1 -0
- package/lib/module/components/map/index.js +850 -0
- package/lib/module/components/map/index.js.map +1 -0
- package/lib/module/components/map/maplibre.js +87 -0
- package/lib/module/components/map/maplibre.js.map +1 -0
- package/lib/module/components/message/index.js +8 -7
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/otp-input/index.js +2 -2
- package/lib/module/components/progress/index.js +8 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +423 -0
- package/lib/module/components/ring-chart/index.js.map +1 -0
- package/lib/module/components/section-rail/index.js +2 -2
- package/lib/module/components/separator/index.js +76 -22
- package/lib/module/components/separator/index.js.map +1 -1
- package/lib/module/components/shimmer/index.js +10 -1
- package/lib/module/components/shimmer/index.js.map +1 -1
- package/lib/module/components/slider/index.js +19 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/steps/index.js +1 -1
- package/lib/module/components/switch/index.js +6 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/typography/index.js +5 -0
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/hooks/use-direction.js +43 -0
- package/lib/module/hooks/use-direction.js.map +1 -0
- package/lib/module/icons/index.js +117 -2
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +8 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/text.js +30 -10
- package/lib/module/primitives/text.js.map +1 -1
- package/lib/module/utils/chart.js +279 -0
- package/lib/module/utils/chart.js.map +1 -0
- package/lib/typescript/src/components/area-chart/index.d.ts +197 -0
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bar-chart/index.d.ts +228 -0
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
- package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/direction/index.d.ts +11 -10
- package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +17 -0
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +1 -0
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/loader/index.d.ts +38 -0
- package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/basemap.d.ts +76 -0
- package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
- package/lib/typescript/src/components/map/index.d.ts +367 -0
- package/lib/typescript/src/components/map/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
- package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/separator/index.d.ts +36 -1
- package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
- package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/typography/index.d.ts +6 -0
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
- package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +5 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/text.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +96 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -0
- package/package.json +9 -1
- package/src/components/area-chart/index.tsx +994 -0
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/bar-chart/index.tsx +1048 -0
- package/src/components/bottom-sheet/index.tsx +280 -20
- package/src/components/breadcrumb/index.tsx +365 -0
- package/src/components/chip/index.tsx +2 -2
- package/src/components/direction/index.tsx +20 -21
- package/src/components/frame/index.tsx +17 -0
- package/src/components/heatmap-chart/index.tsx +58 -11
- package/src/components/input-group/index.tsx +2 -2
- package/src/components/line-chart/index.tsx +13 -181
- package/src/components/loader/index.tsx +812 -0
- package/src/components/map/basemap.ts +304 -0
- package/src/components/map/index.tsx +1118 -0
- package/src/components/map/maplibre.ts +201 -0
- package/src/components/message/index.tsx +8 -7
- package/src/components/otp-input/index.tsx +2 -2
- package/src/components/progress/index.tsx +10 -2
- package/src/components/ring-chart/index.tsx +543 -0
- package/src/components/section-rail/index.tsx +2 -2
- package/src/components/separator/index.tsx +77 -19
- package/src/components/shimmer/index.tsx +10 -1
- package/src/components/slider/index.tsx +20 -5
- package/src/components/steps/index.tsx +1 -1
- package/src/components/switch/index.tsx +6 -1
- package/src/components/typography/index.tsx +5 -0
- package/src/hooks/use-direction.ts +45 -0
- package/src/icons/index.tsx +66 -0
- package/src/index.ts +84 -0
- package/src/primitives/text.tsx +25 -7
- package/src/utils/chart.ts +355 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map — a themed vector map.
|
|
5
|
+
*
|
|
6
|
+
* Everything visible is built from theme tokens rather than shipped as a
|
|
7
|
+
* finished style, so the map belongs to whatever theme is active instead of
|
|
8
|
+
* being the one rectangle on the screen that stayed grey. See `basemap.ts` for
|
|
9
|
+
* how the style is assembled; the short version is that the tiles are bought
|
|
10
|
+
* and the colours are ours.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Map center={[-0.12, 51.5]} zoom={11}>
|
|
14
|
+
* <Map.Marker lngLat={[-0.12, 51.5]}>
|
|
15
|
+
* <Map.Popup>
|
|
16
|
+
* <Text weight="medium">Charing Cross</Text>
|
|
17
|
+
* </Map.Popup>
|
|
18
|
+
* </Map.Marker>
|
|
19
|
+
* <Map.Controls locate />
|
|
20
|
+
* </Map>
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The renderer is native and optional, so `Map` has a state no other component
|
|
24
|
+
* here does: it may be unable to draw at all. It says so in place rather than
|
|
25
|
+
* throwing, because the usual way to reach that state is running in a client
|
|
26
|
+
* that cannot load native modules, and a screen explaining the build you need
|
|
27
|
+
* is more use than a stack trace.
|
|
28
|
+
*
|
|
29
|
+
* `Map.Popup` reads its position from the marker it sits inside, the way a
|
|
30
|
+
* frame's title reads its weight from its slot. Given a `lngLat` of its own it
|
|
31
|
+
* anchors to that coordinate instead, so the same name covers both the popup
|
|
32
|
+
* attached to a pin and the one floating over a place with no pin at all.
|
|
33
|
+
*/
|
|
34
|
+
import { Children, createContext, forwardRef, isValidElement, useCallback, useContext, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
35
|
+
import { Pressable, View } from 'react-native';
|
|
36
|
+
import { useCSSVariable } from 'uniwind';
|
|
37
|
+
import { tv } from 'tailwind-variants';
|
|
38
|
+
import { CompassIcon, CrosshairIcon, MinusIcon, PlusIcon } from "../../icons/index.js";
|
|
39
|
+
import { Text } from "../../primitives/text.js";
|
|
40
|
+
import { cn } from "../../utils/cn.js";
|
|
41
|
+
import { buildBasemapStyle, CARTO_SOURCE } from "./basemap.js";
|
|
42
|
+
import { hasMapLibre, MapLibre } from "./maplibre.js";
|
|
43
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
44
|
+
export { hasMapLibre, CARTO_SOURCE };
|
|
45
|
+
const mapVariants = tv({
|
|
46
|
+
slots: {
|
|
47
|
+
root: 'flex-1 overflow-hidden',
|
|
48
|
+
controls: 'absolute gap-2',
|
|
49
|
+
// A control group is one surface split by hairlines rather than separate
|
|
50
|
+
// buttons: at 36pt a gap between them reads as damage, not as spacing.
|
|
51
|
+
group: 'overflow-hidden rounded-xl border border-border bg-card shadow-sm',
|
|
52
|
+
control: 'h-9 w-9 items-center justify-center active:bg-muted',
|
|
53
|
+
popup: 'rounded-xl border border-border bg-popover px-3 py-2 shadow-lg',
|
|
54
|
+
pin: 'h-3.5 w-3.5 rounded-full border-2 border-background bg-primary shadow-md'
|
|
55
|
+
},
|
|
56
|
+
variants: {
|
|
57
|
+
position: {
|
|
58
|
+
'top-left': {
|
|
59
|
+
controls: 'left-3 top-3'
|
|
60
|
+
},
|
|
61
|
+
'top-right': {
|
|
62
|
+
controls: 'right-3 top-3'
|
|
63
|
+
},
|
|
64
|
+
'bottom-left': {
|
|
65
|
+
controls: 'bottom-3 left-3'
|
|
66
|
+
},
|
|
67
|
+
'bottom-right': {
|
|
68
|
+
controls: 'bottom-3 right-3'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
defaultVariants: {
|
|
73
|
+
position: 'bottom-right'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
/** Reads a theme token, falling back when it resolves to something unusable. */
|
|
77
|
+
function useToken(name, fallback) {
|
|
78
|
+
const value = useCSSVariable(name);
|
|
79
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
80
|
+
}
|
|
81
|
+
const MapContext = /*#__PURE__*/createContext(null);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The map's camera and view state, from inside it.
|
|
85
|
+
*
|
|
86
|
+
* For a control, an overlay, or anything that has to move the map in response
|
|
87
|
+
* to something that is not a gesture on it.
|
|
88
|
+
*/
|
|
89
|
+
export function useMap() {
|
|
90
|
+
const context = useContext(MapContext);
|
|
91
|
+
if (!context) throw new Error('useMap must be used inside a <Map>.');
|
|
92
|
+
return context;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Set by `Map.Marker`, so a popup inside one knows where it is. */
|
|
96
|
+
const MarkerContext = /*#__PURE__*/createContext(null);
|
|
97
|
+
/**
|
|
98
|
+
* Shown when the renderer is missing. Deliberately explicit about the cause:
|
|
99
|
+
* "map unavailable" sends people looking at their network, and the actual
|
|
100
|
+
* problem is almost always a build that never included the native module.
|
|
101
|
+
*/
|
|
102
|
+
function MapUnavailable({
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}) {
|
|
106
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
107
|
+
className: cn('flex-1 items-center justify-center gap-1 rounded-2xl border border-dashed border-border bg-muted/25 p-6', className),
|
|
108
|
+
accessibilityRole: "alert",
|
|
109
|
+
...props,
|
|
110
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
111
|
+
size: "sm",
|
|
112
|
+
weight: "medium",
|
|
113
|
+
children: "Map renderer not installed"
|
|
114
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
115
|
+
size: "xs",
|
|
116
|
+
muted: true,
|
|
117
|
+
className: "text-center",
|
|
118
|
+
children: "Map needs a development build. Install @maplibre/maplibre-react-native, add its config plugin, and rebuild \u2014 it cannot run in a client that loads no native modules."
|
|
119
|
+
})]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const MapRoot = /*#__PURE__*/forwardRef(function MapRoot({
|
|
123
|
+
children,
|
|
124
|
+
className,
|
|
125
|
+
center,
|
|
126
|
+
zoom = 2,
|
|
127
|
+
bearing,
|
|
128
|
+
pitch,
|
|
129
|
+
bounds,
|
|
130
|
+
blank = false,
|
|
131
|
+
source = CARTO_SOURCE,
|
|
132
|
+
mapStyle,
|
|
133
|
+
rotatable = false,
|
|
134
|
+
interactive = true,
|
|
135
|
+
onViewStateChange,
|
|
136
|
+
onPress,
|
|
137
|
+
onReady,
|
|
138
|
+
...props
|
|
139
|
+
}, ref) {
|
|
140
|
+
const slots = mapVariants();
|
|
141
|
+
const mapRef = useRef(null);
|
|
142
|
+
const cameraRef = useRef(null);
|
|
143
|
+
const [ready, setReady] = useState(false);
|
|
144
|
+
|
|
145
|
+
// Every colour the style needs, resolved from the active theme. Hooks run
|
|
146
|
+
// unconditionally, so they sit above the renderer check rather than inside it.
|
|
147
|
+
const tokens = {
|
|
148
|
+
background: useToken('--color-background', '#ffffff'),
|
|
149
|
+
water: useToken('--color-muted', 'rgba(0,0,0,0.06)'),
|
|
150
|
+
land: useToken('--color-surface-secondary', 'rgba(0,0,0,0.04)'),
|
|
151
|
+
building: useToken('--color-surface-tertiary', 'rgba(0,0,0,0.08)'),
|
|
152
|
+
line: useToken('--color-border', 'rgba(0,0,0,0.1)'),
|
|
153
|
+
label: useToken('--color-muted-foreground', '#686868'),
|
|
154
|
+
labelHalo: useToken('--color-background', '#ffffff')
|
|
155
|
+
};
|
|
156
|
+
const style = useMemo(() => mapStyle ?? buildBasemapStyle(tokens, {
|
|
157
|
+
blank,
|
|
158
|
+
source
|
|
159
|
+
}),
|
|
160
|
+
// The token object is rebuilt every render, so depend on its values.
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
|
+
[mapStyle, blank, source, tokens.background, tokens.water, tokens.land, tokens.building, tokens.line, tokens.label, tokens.labelHalo]);
|
|
163
|
+
useImperativeHandle(ref, () => ({
|
|
164
|
+
flyTo: ({
|
|
165
|
+
center: to,
|
|
166
|
+
zoom: z,
|
|
167
|
+
duration = 900
|
|
168
|
+
}) => cameraRef.current?.flyTo({
|
|
169
|
+
center: to,
|
|
170
|
+
zoom: z,
|
|
171
|
+
duration
|
|
172
|
+
}),
|
|
173
|
+
fitBounds: (box, padding = 48) => cameraRef.current?.fitBounds(box, {
|
|
174
|
+
padding: {
|
|
175
|
+
top: padding,
|
|
176
|
+
right: padding,
|
|
177
|
+
bottom: padding,
|
|
178
|
+
left: padding
|
|
179
|
+
},
|
|
180
|
+
duration: 900
|
|
181
|
+
}),
|
|
182
|
+
getViewState: async () => (await mapRef.current?.getViewState()) ?? null
|
|
183
|
+
}), []);
|
|
184
|
+
const context = useMemo(() => ({
|
|
185
|
+
mapRef,
|
|
186
|
+
cameraRef,
|
|
187
|
+
ready
|
|
188
|
+
}), [ready]);
|
|
189
|
+
if (!hasMapLibre || !MapLibre) {
|
|
190
|
+
return /*#__PURE__*/_jsx(MapUnavailable, {
|
|
191
|
+
className: className,
|
|
192
|
+
...props
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const {
|
|
196
|
+
Map: MapLibreMap,
|
|
197
|
+
Camera
|
|
198
|
+
} = MapLibre;
|
|
199
|
+
return /*#__PURE__*/_jsx(View, {
|
|
200
|
+
className: slots.root({
|
|
201
|
+
className
|
|
202
|
+
}),
|
|
203
|
+
...props,
|
|
204
|
+
children: /*#__PURE__*/_jsx(MapContext.Provider, {
|
|
205
|
+
value: context,
|
|
206
|
+
children: /*#__PURE__*/_jsxs(MapLibreMap, {
|
|
207
|
+
ref: mapRef,
|
|
208
|
+
mapStyle: style,
|
|
209
|
+
style: {
|
|
210
|
+
flex: 1
|
|
211
|
+
}
|
|
212
|
+
// The library's own ornaments are turned off across the board:
|
|
213
|
+
// they are drawn by the renderer in its own style and cannot be
|
|
214
|
+
// themed, so `Map.Controls` replaces them with views that can.
|
|
215
|
+
,
|
|
216
|
+
logo: false,
|
|
217
|
+
compass: false,
|
|
218
|
+
attribution: false,
|
|
219
|
+
scaleBar: false,
|
|
220
|
+
dragPan: interactive,
|
|
221
|
+
touchZoom: interactive,
|
|
222
|
+
touchRotate: rotatable,
|
|
223
|
+
touchPitch: rotatable,
|
|
224
|
+
onDidFinishLoadingMap: () => {
|
|
225
|
+
setReady(true);
|
|
226
|
+
onReady?.();
|
|
227
|
+
},
|
|
228
|
+
onRegionDidChange: onViewStateChange ? event => onViewStateChange(event.nativeEvent) : undefined,
|
|
229
|
+
onPress: onPress ? event => onPress(event.nativeEvent.coordinates) : undefined,
|
|
230
|
+
children: [/*#__PURE__*/_jsx(Camera, {
|
|
231
|
+
ref: cameraRef
|
|
232
|
+
// The opening position, and only that: this is applied on the
|
|
233
|
+
// first frame and never again, so a re-render cannot pull the map
|
|
234
|
+
// back to its starting point mid-gesture. Everything afterwards
|
|
235
|
+
// goes through the ref.
|
|
236
|
+
,
|
|
237
|
+
initialViewState: bounds ? {
|
|
238
|
+
bounds
|
|
239
|
+
} : {
|
|
240
|
+
center: center ?? [0, 20],
|
|
241
|
+
zoom,
|
|
242
|
+
bearing,
|
|
243
|
+
pitch
|
|
244
|
+
}
|
|
245
|
+
}), children]
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
MapRoot.displayName = 'Map';
|
|
251
|
+
/**
|
|
252
|
+
* A point on the map, drawn as React views rather than as a style layer — so
|
|
253
|
+
* it can be anything the rest of the library can draw. For hundreds of points
|
|
254
|
+
* use `Map.Cluster` instead: a view per marker stops being affordable long
|
|
255
|
+
* before the map stops being readable.
|
|
256
|
+
*/
|
|
257
|
+
function MapMarker({
|
|
258
|
+
lngLat,
|
|
259
|
+
anchor = 'center',
|
|
260
|
+
onPress,
|
|
261
|
+
className,
|
|
262
|
+
children
|
|
263
|
+
}) {
|
|
264
|
+
const slots = mapVariants();
|
|
265
|
+
const [open, setOpen] = useState(false);
|
|
266
|
+
|
|
267
|
+
/*
|
|
268
|
+
* Popups are pulled out of the marker's own children rather than drawn
|
|
269
|
+
* inside it. Left in place a popup would be laid out under the pin, which
|
|
270
|
+
* costs twice: it would be permanently visible instead of opening on press,
|
|
271
|
+
* and — because the marker is anchored by the centre of its box — a card
|
|
272
|
+
* below the pin would drag that centre downward and leave the pin sitting
|
|
273
|
+
* well above the coordinate it is meant to mark.
|
|
274
|
+
*/
|
|
275
|
+
const popups = [];
|
|
276
|
+
const labels = [];
|
|
277
|
+
const content = [];
|
|
278
|
+
Children.forEach(children, child => {
|
|
279
|
+
if (! /*#__PURE__*/isValidElement(child)) {
|
|
280
|
+
content.push(child);
|
|
281
|
+
} else if (child.type === MapPopup) {
|
|
282
|
+
popups.push(child);
|
|
283
|
+
} else if (child.type === MapLabel) {
|
|
284
|
+
labels.push(child);
|
|
285
|
+
} else {
|
|
286
|
+
content.push(child);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
if (!MapLibre) return null;
|
|
290
|
+
const {
|
|
291
|
+
Marker
|
|
292
|
+
} = MapLibre;
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* Labels are counted separately from the rest of the children because they
|
|
296
|
+
* are absolutely positioned, and so contribute nothing to the marker's box.
|
|
297
|
+
* A marker whose only child is a label would otherwise measure 0×0 — and the
|
|
298
|
+
* renderer cannot place a zero-sized annotation, so it parks it in the
|
|
299
|
+
* top-left corner of the map instead of on its coordinate. Falling back to
|
|
300
|
+
* the default pin keeps the box real, which is also the right drawing: a
|
|
301
|
+
* label with nothing to label is a caption floating on its own.
|
|
302
|
+
*/
|
|
303
|
+
const body = /*#__PURE__*/_jsxs(View, {
|
|
304
|
+
className: cn('items-center', className),
|
|
305
|
+
children: [content.length > 0 ? content : /*#__PURE__*/_jsx(View, {
|
|
306
|
+
className: slots.pin()
|
|
307
|
+
}), labels]
|
|
308
|
+
});
|
|
309
|
+
const pressable = onPress || popups.length > 0;
|
|
310
|
+
return /*#__PURE__*/_jsxs(MarkerContext.Provider, {
|
|
311
|
+
value: lngLat,
|
|
312
|
+
children: [/*#__PURE__*/_jsx(Marker, {
|
|
313
|
+
lngLat: lngLat,
|
|
314
|
+
anchor: anchor,
|
|
315
|
+
children: pressable ? /*#__PURE__*/_jsx(Pressable, {
|
|
316
|
+
accessibilityRole: "button",
|
|
317
|
+
onPress: () => {
|
|
318
|
+
if (popups.length > 0) setOpen(was => !was);
|
|
319
|
+
onPress?.();
|
|
320
|
+
},
|
|
321
|
+
children: body
|
|
322
|
+
}) : body
|
|
323
|
+
}), open ? popups : null]
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
MapMarker.displayName = 'Map.Marker';
|
|
327
|
+
/**
|
|
328
|
+
* A caption pinned to a marker. Always visible, unlike a popup — for the
|
|
329
|
+
* handful of places whose names are the point of the map.
|
|
330
|
+
*
|
|
331
|
+
* It is taken out of the marker's layout flow, which matters more than it
|
|
332
|
+
* sounds: a marker sits on its coordinate by the centre of its box, so a label
|
|
333
|
+
* in flow underneath the pin would pull that centre down and lift every pin
|
|
334
|
+
* off the place it marks. Absolute keeps the box the size of the pin.
|
|
335
|
+
*
|
|
336
|
+
* The overhang on each side is what lets a long name stay centred on the pin
|
|
337
|
+
* without widening the marker — a name is usually far wider than the dot it
|
|
338
|
+
* belongs to, and a box sized to the name would be anchored by the name.
|
|
339
|
+
*/
|
|
340
|
+
function MapLabel({
|
|
341
|
+
children,
|
|
342
|
+
className,
|
|
343
|
+
side = 'bottom'
|
|
344
|
+
}) {
|
|
345
|
+
return /*#__PURE__*/_jsx(View, {
|
|
346
|
+
pointerEvents: "none",
|
|
347
|
+
className: cn('absolute items-center', side === 'top' ? 'bottom-full mb-1' : 'top-full mt-1'),
|
|
348
|
+
style: {
|
|
349
|
+
left: -120,
|
|
350
|
+
right: -120
|
|
351
|
+
},
|
|
352
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
353
|
+
className: cn('rounded-md bg-background/85 px-1.5 py-0.5', className),
|
|
354
|
+
children: typeof children === 'string' ? /*#__PURE__*/_jsx(Text, {
|
|
355
|
+
size: "xs",
|
|
356
|
+
weight: "medium",
|
|
357
|
+
numberOfLines: 1,
|
|
358
|
+
children: children
|
|
359
|
+
}) : children
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
MapLabel.displayName = 'Map.Label';
|
|
364
|
+
/**
|
|
365
|
+
* A card anchored to a point.
|
|
366
|
+
*
|
|
367
|
+
* Inside a `Map.Marker` it takes that marker's coordinate and opens when the
|
|
368
|
+
* marker is pressed. Given a `lngLat` it stands alone at that coordinate — the
|
|
369
|
+
* same component either way, because the difference is where it is anchored
|
|
370
|
+
* rather than what it is.
|
|
371
|
+
*
|
|
372
|
+
* Either way it is its own annotation rather than something drawn inside the
|
|
373
|
+
* marker, which is what keeps a card from dragging the pin off its coordinate.
|
|
374
|
+
* Anchored by its bottom edge, so it floats above the point it describes
|
|
375
|
+
* instead of covering it.
|
|
376
|
+
*/
|
|
377
|
+
function MapPopup({
|
|
378
|
+
children,
|
|
379
|
+
className,
|
|
380
|
+
title,
|
|
381
|
+
lngLat
|
|
382
|
+
}) {
|
|
383
|
+
const slots = mapVariants();
|
|
384
|
+
const markerLngLat = useContext(MarkerContext);
|
|
385
|
+
const coordinate = lngLat ?? markerLngLat;
|
|
386
|
+
if (!MapLibre || !coordinate) return null;
|
|
387
|
+
const {
|
|
388
|
+
Marker
|
|
389
|
+
} = MapLibre;
|
|
390
|
+
return /*#__PURE__*/_jsx(Marker, {
|
|
391
|
+
lngLat: coordinate,
|
|
392
|
+
anchor: "bottom",
|
|
393
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
394
|
+
className: cn('mb-2 items-center', slots.popup({
|
|
395
|
+
className
|
|
396
|
+
})),
|
|
397
|
+
children: [title ? /*#__PURE__*/_jsx(Text, {
|
|
398
|
+
size: "sm",
|
|
399
|
+
weight: "medium",
|
|
400
|
+
children: title
|
|
401
|
+
}) : null, children]
|
|
402
|
+
})
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
MapPopup.displayName = 'Map.Popup';
|
|
406
|
+
function ControlButton({
|
|
407
|
+
label,
|
|
408
|
+
onPress,
|
|
409
|
+
children
|
|
410
|
+
}) {
|
|
411
|
+
const slots = mapVariants();
|
|
412
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
413
|
+
accessibilityRole: "button",
|
|
414
|
+
accessibilityLabel: label,
|
|
415
|
+
className: slots.control(),
|
|
416
|
+
onPress: onPress,
|
|
417
|
+
children: children
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The map's own chrome, as themed views.
|
|
423
|
+
*
|
|
424
|
+
* The renderer draws its own zoom and compass ornaments, but in its style
|
|
425
|
+
* rather than the app's, and they cannot be recoloured — so they are turned
|
|
426
|
+
* off in `Map` and replaced here by buttons built from the same tokens as
|
|
427
|
+
* every other control in the library.
|
|
428
|
+
*/
|
|
429
|
+
function MapControls({
|
|
430
|
+
position = 'bottom-right',
|
|
431
|
+
zoom = true,
|
|
432
|
+
locate = false,
|
|
433
|
+
compass = false,
|
|
434
|
+
className,
|
|
435
|
+
onLocate
|
|
436
|
+
}) {
|
|
437
|
+
const slots = mapVariants({
|
|
438
|
+
position
|
|
439
|
+
});
|
|
440
|
+
const {
|
|
441
|
+
mapRef,
|
|
442
|
+
cameraRef
|
|
443
|
+
} = useMap();
|
|
444
|
+
|
|
445
|
+
// Read the live zoom rather than tracking it in state: the map is moved by
|
|
446
|
+
// gestures too, and a counter kept alongside it drifts on the first pinch.
|
|
447
|
+
const nudgeZoom = useCallback(async delta => {
|
|
448
|
+
const map = mapRef.current;
|
|
449
|
+
const camera = cameraRef.current;
|
|
450
|
+
if (!map || !camera) return;
|
|
451
|
+
const [center, current] = await Promise.all([map.getCenter(), map.getZoom()]);
|
|
452
|
+
camera.easeTo({
|
|
453
|
+
center,
|
|
454
|
+
zoom: Math.min(22, Math.max(0, current + delta)),
|
|
455
|
+
duration: 250
|
|
456
|
+
});
|
|
457
|
+
}, [mapRef, cameraRef]);
|
|
458
|
+
const handleLocate = useCallback(async () => {
|
|
459
|
+
let position$ = null;
|
|
460
|
+
try {
|
|
461
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
462
|
+
const location = require('expo-location');
|
|
463
|
+
const granted = await location.requestForegroundPermissionsAsync();
|
|
464
|
+
if (!granted?.granted) return;
|
|
465
|
+
position$ = await location.getCurrentPositionAsync({});
|
|
466
|
+
} catch {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (!position$) return;
|
|
470
|
+
const lngLat = [position$.coords.longitude, position$.coords.latitude];
|
|
471
|
+
cameraRef.current?.flyTo({
|
|
472
|
+
center: lngLat,
|
|
473
|
+
zoom: 14,
|
|
474
|
+
duration: 900
|
|
475
|
+
});
|
|
476
|
+
onLocate?.(lngLat);
|
|
477
|
+
}, [cameraRef, onLocate]);
|
|
478
|
+
const resetNorth = useCallback(async () => {
|
|
479
|
+
const center = await mapRef.current?.getCenter();
|
|
480
|
+
if (!center) return;
|
|
481
|
+
cameraRef.current?.easeTo({
|
|
482
|
+
center,
|
|
483
|
+
bearing: 0,
|
|
484
|
+
pitch: 0,
|
|
485
|
+
duration: 300
|
|
486
|
+
});
|
|
487
|
+
}, [mapRef, cameraRef]);
|
|
488
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
489
|
+
className: slots.controls({
|
|
490
|
+
className
|
|
491
|
+
}),
|
|
492
|
+
children: [zoom ? /*#__PURE__*/_jsxs(View, {
|
|
493
|
+
className: slots.group(),
|
|
494
|
+
children: [/*#__PURE__*/_jsx(ControlButton, {
|
|
495
|
+
label: "Zoom in",
|
|
496
|
+
onPress: () => nudgeZoom(1),
|
|
497
|
+
children: /*#__PURE__*/_jsx(PlusIcon, {
|
|
498
|
+
size: 16
|
|
499
|
+
})
|
|
500
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
501
|
+
className: "h-px bg-border"
|
|
502
|
+
}), /*#__PURE__*/_jsx(ControlButton, {
|
|
503
|
+
label: "Zoom out",
|
|
504
|
+
onPress: () => nudgeZoom(-1),
|
|
505
|
+
children: /*#__PURE__*/_jsx(MinusIcon, {
|
|
506
|
+
size: 16
|
|
507
|
+
})
|
|
508
|
+
})]
|
|
509
|
+
}) : null, compass ? /*#__PURE__*/_jsx(View, {
|
|
510
|
+
className: slots.group(),
|
|
511
|
+
children: /*#__PURE__*/_jsx(ControlButton, {
|
|
512
|
+
label: "Face north",
|
|
513
|
+
onPress: resetNorth,
|
|
514
|
+
children: /*#__PURE__*/_jsx(CompassIcon, {
|
|
515
|
+
size: 16
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
}) : null, locate ? /*#__PURE__*/_jsx(View, {
|
|
519
|
+
className: slots.group(),
|
|
520
|
+
children: /*#__PURE__*/_jsx(ControlButton, {
|
|
521
|
+
label: "Show my location",
|
|
522
|
+
onPress: handleLocate,
|
|
523
|
+
children: /*#__PURE__*/_jsx(CrosshairIcon, {
|
|
524
|
+
size: 16
|
|
525
|
+
})
|
|
526
|
+
})
|
|
527
|
+
}) : null]
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
MapControls.displayName = 'Map.Controls';
|
|
531
|
+
|
|
532
|
+
/** Turns a token into a colour a style layer can use. */
|
|
533
|
+
function useLineColor(color) {
|
|
534
|
+
const fallback = useToken('--color-primary', '#262626');
|
|
535
|
+
return color ?? fallback;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* A path across the map, drawn as a style layer rather than as views — so its
|
|
539
|
+
* cost does not grow with the number of points in it.
|
|
540
|
+
*/
|
|
541
|
+
function MapRoute({
|
|
542
|
+
coordinates,
|
|
543
|
+
color,
|
|
544
|
+
width = 3,
|
|
545
|
+
dashed = false,
|
|
546
|
+
opacity = 1,
|
|
547
|
+
id = 'route'
|
|
548
|
+
}) {
|
|
549
|
+
const stroke = useLineColor(color);
|
|
550
|
+
const data = useMemo(() => ({
|
|
551
|
+
type: 'Feature',
|
|
552
|
+
properties: {},
|
|
553
|
+
geometry: {
|
|
554
|
+
type: 'LineString',
|
|
555
|
+
coordinates
|
|
556
|
+
}
|
|
557
|
+
}), [coordinates]);
|
|
558
|
+
if (!MapLibre) return null;
|
|
559
|
+
const {
|
|
560
|
+
GeoJSONSource,
|
|
561
|
+
Layer
|
|
562
|
+
} = MapLibre;
|
|
563
|
+
return /*#__PURE__*/_jsx(GeoJSONSource, {
|
|
564
|
+
id: `${id}-source`,
|
|
565
|
+
data: data,
|
|
566
|
+
lineMetrics: true,
|
|
567
|
+
children: /*#__PURE__*/_jsx(Layer, {
|
|
568
|
+
id: id,
|
|
569
|
+
type: "line",
|
|
570
|
+
style: {
|
|
571
|
+
lineColor: stroke,
|
|
572
|
+
lineWidth: width,
|
|
573
|
+
lineOpacity: opacity,
|
|
574
|
+
lineCap: 'round',
|
|
575
|
+
lineJoin: 'round',
|
|
576
|
+
...(dashed ? {
|
|
577
|
+
lineDasharray: [2, 1.5]
|
|
578
|
+
} : {})
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
MapRoute.displayName = 'Map.Route';
|
|
584
|
+
/**
|
|
585
|
+
* A curved connection between two points.
|
|
586
|
+
*
|
|
587
|
+
* The curve is not geography — a great circle between two cities is not bowed
|
|
588
|
+
* on a flat projection. It is there so that two arcs sharing an endpoint stay
|
|
589
|
+
* tellable apart, which a bundle of straight lines through one city does not.
|
|
590
|
+
*/
|
|
591
|
+
function MapArc({
|
|
592
|
+
from,
|
|
593
|
+
to,
|
|
594
|
+
curvature = 0.2,
|
|
595
|
+
color,
|
|
596
|
+
width = 2,
|
|
597
|
+
opacity = 0.9,
|
|
598
|
+
id = 'arc'
|
|
599
|
+
}) {
|
|
600
|
+
const stroke = useLineColor(color);
|
|
601
|
+
const data = useMemo(() => {
|
|
602
|
+
// A quadratic Bézier whose control point is pushed off the midpoint
|
|
603
|
+
// perpendicular to the line, so the bow scales with the distance rather
|
|
604
|
+
// than being a fixed lift that vanishes on long hops.
|
|
605
|
+
const [x1, y1] = from;
|
|
606
|
+
const [x2, y2] = to;
|
|
607
|
+
const midX = (x1 + x2) / 2;
|
|
608
|
+
const midY = (y1 + y2) / 2;
|
|
609
|
+
const controlX = midX - (y2 - y1) * curvature;
|
|
610
|
+
const controlY = midY + (x2 - x1) * curvature;
|
|
611
|
+
const SEGMENTS = 64;
|
|
612
|
+
const coordinates = [];
|
|
613
|
+
for (let i = 0; i <= SEGMENTS; i += 1) {
|
|
614
|
+
const t = i / SEGMENTS;
|
|
615
|
+
const inv = 1 - t;
|
|
616
|
+
coordinates.push([inv * inv * x1 + 2 * inv * t * controlX + t * t * x2, inv * inv * y1 + 2 * inv * t * controlY + t * t * y2]);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
type: 'Feature',
|
|
620
|
+
properties: {},
|
|
621
|
+
geometry: {
|
|
622
|
+
type: 'LineString',
|
|
623
|
+
coordinates
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}, [from, to, curvature]);
|
|
627
|
+
if (!MapLibre) return null;
|
|
628
|
+
const {
|
|
629
|
+
GeoJSONSource,
|
|
630
|
+
Layer
|
|
631
|
+
} = MapLibre;
|
|
632
|
+
return /*#__PURE__*/_jsx(GeoJSONSource, {
|
|
633
|
+
id: `${id}-source`,
|
|
634
|
+
data: data,
|
|
635
|
+
lineMetrics: true,
|
|
636
|
+
children: /*#__PURE__*/_jsx(Layer, {
|
|
637
|
+
id: id,
|
|
638
|
+
type: "line",
|
|
639
|
+
style: {
|
|
640
|
+
lineColor: stroke,
|
|
641
|
+
lineWidth: width,
|
|
642
|
+
lineOpacity: opacity,
|
|
643
|
+
lineCap: 'round'
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
MapArc.displayName = 'Map.Arc';
|
|
649
|
+
/**
|
|
650
|
+
* Arbitrary geography as a themed layer — the component behind a choropleth,
|
|
651
|
+
* a coverage area, or any other shape that comes from data rather than from
|
|
652
|
+
* the basemap.
|
|
653
|
+
*
|
|
654
|
+
* `fill` takes a style expression as well as a colour, which is what makes a
|
|
655
|
+
* choropleth one layer instead of one layer per bucket.
|
|
656
|
+
*/
|
|
657
|
+
function MapGeoJSON({
|
|
658
|
+
data,
|
|
659
|
+
fill,
|
|
660
|
+
stroke,
|
|
661
|
+
strokeWidth = 1,
|
|
662
|
+
fillOpacity = 0.7,
|
|
663
|
+
onPress,
|
|
664
|
+
id = 'geojson'
|
|
665
|
+
}) {
|
|
666
|
+
const defaultFill = useToken('--color-primary', '#262626');
|
|
667
|
+
const defaultStroke = useToken('--color-border', 'rgba(0,0,0,0.1)');
|
|
668
|
+
if (!MapLibre) return null;
|
|
669
|
+
const {
|
|
670
|
+
GeoJSONSource,
|
|
671
|
+
Layer
|
|
672
|
+
} = MapLibre;
|
|
673
|
+
return /*#__PURE__*/_jsxs(GeoJSONSource, {
|
|
674
|
+
id: `${id}-source`,
|
|
675
|
+
data: data,
|
|
676
|
+
onPress: onPress ? event => onPress(event.nativeEvent.features?.[0]) : undefined,
|
|
677
|
+
children: [/*#__PURE__*/_jsx(Layer, {
|
|
678
|
+
id: `${id}-fill`,
|
|
679
|
+
type: "fill",
|
|
680
|
+
style: {
|
|
681
|
+
fillColor: fill ?? defaultFill,
|
|
682
|
+
fillOpacity
|
|
683
|
+
}
|
|
684
|
+
}), /*#__PURE__*/_jsx(Layer, {
|
|
685
|
+
id: `${id}-line`,
|
|
686
|
+
type: "line",
|
|
687
|
+
style: {
|
|
688
|
+
lineColor: stroke ?? defaultStroke,
|
|
689
|
+
lineWidth: strokeWidth
|
|
690
|
+
}
|
|
691
|
+
})]
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
MapGeoJSON.displayName = 'Map.GeoJSON';
|
|
695
|
+
/**
|
|
696
|
+
* Dense points, merged as they get too close to tell apart.
|
|
697
|
+
*
|
|
698
|
+
* This is the layer to reach for past a few dozen points: `Map.Marker` mounts
|
|
699
|
+
* a React view each, which a thousand points cannot afford, and a thousand
|
|
700
|
+
* overlapping pins would be unreadable even if it could.
|
|
701
|
+
*/
|
|
702
|
+
function MapCluster({
|
|
703
|
+
data,
|
|
704
|
+
color,
|
|
705
|
+
textColor,
|
|
706
|
+
radius = 50,
|
|
707
|
+
maxZoom = 14,
|
|
708
|
+
onPress,
|
|
709
|
+
id = 'cluster'
|
|
710
|
+
}) {
|
|
711
|
+
const fill = useToken('--color-primary', '#262626');
|
|
712
|
+
const onFill = useToken('--color-primary-foreground', '#fafafa');
|
|
713
|
+
const bubble = color ?? fill;
|
|
714
|
+
const ink = textColor ?? onFill;
|
|
715
|
+
if (!MapLibre) return null;
|
|
716
|
+
const {
|
|
717
|
+
GeoJSONSource,
|
|
718
|
+
Layer
|
|
719
|
+
} = MapLibre;
|
|
720
|
+
return /*#__PURE__*/_jsxs(GeoJSONSource, {
|
|
721
|
+
id: `${id}-source`,
|
|
722
|
+
data: data,
|
|
723
|
+
cluster: true,
|
|
724
|
+
clusterRadius: radius,
|
|
725
|
+
clusterMaxZoom: maxZoom,
|
|
726
|
+
onPress: onPress ? event => onPress(event.nativeEvent.features?.[0]) : undefined,
|
|
727
|
+
children: [/*#__PURE__*/_jsx(Layer, {
|
|
728
|
+
id: `${id}-bubble`,
|
|
729
|
+
type: "circle",
|
|
730
|
+
filter: ['has', 'point_count'],
|
|
731
|
+
style: {
|
|
732
|
+
circleColor: bubble,
|
|
733
|
+
circleRadius: ['step', ['get', 'point_count'], 16, 25, 22, 100, 28],
|
|
734
|
+
circleStrokeWidth: 2,
|
|
735
|
+
circleStrokeColor: ink,
|
|
736
|
+
circleStrokeOpacity: 0.25
|
|
737
|
+
}
|
|
738
|
+
}), /*#__PURE__*/_jsx(Layer, {
|
|
739
|
+
id: `${id}-count`,
|
|
740
|
+
type: "symbol",
|
|
741
|
+
filter: ['has', 'point_count'],
|
|
742
|
+
style: {
|
|
743
|
+
textField: ['get', 'point_count_abbreviated'],
|
|
744
|
+
textSize: 12,
|
|
745
|
+
textColor: ink
|
|
746
|
+
}
|
|
747
|
+
}), /*#__PURE__*/_jsx(Layer, {
|
|
748
|
+
id: `${id}-point`,
|
|
749
|
+
type: "circle",
|
|
750
|
+
filter: ['!', ['has', 'point_count']],
|
|
751
|
+
style: {
|
|
752
|
+
circleColor: bubble,
|
|
753
|
+
circleRadius: 6,
|
|
754
|
+
circleStrokeWidth: 2,
|
|
755
|
+
circleStrokeColor: ink,
|
|
756
|
+
circleStrokeOpacity: 0.4
|
|
757
|
+
}
|
|
758
|
+
})]
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
MapCluster.displayName = 'Map.Cluster';
|
|
762
|
+
/**
|
|
763
|
+
* Point density as a continuous field.
|
|
764
|
+
*
|
|
765
|
+
* The opposite trade to `Map.Cluster`: a cluster keeps every point countable
|
|
766
|
+
* and tells you nothing about the space between them, while a heatmap shows
|
|
767
|
+
* the shape of the distribution and no longer lets you count anything. Reach
|
|
768
|
+
* for it when the question is *where* rather than *how many*.
|
|
769
|
+
*
|
|
770
|
+
* It fades out past `maxZoom` on purpose. Zoomed far enough in, every point is
|
|
771
|
+
* its own island and the blur says less than the points would — so the layer
|
|
772
|
+
* gets out of the way rather than smearing five records across a street.
|
|
773
|
+
*
|
|
774
|
+
* The radius is tied to zoom for the mirror-image reason. Left as a fixed
|
|
775
|
+
* number of screen points it would mean a different distance at every zoom:
|
|
776
|
+
* a blur that reads as a city at street level covers half a continent once
|
|
777
|
+
* the map is pulled out, and a field measured over land ends up sitting in
|
|
778
|
+
* the sea. Scaling it with the projection keeps the claim the same one.
|
|
779
|
+
*/
|
|
780
|
+
function MapHeatmap({
|
|
781
|
+
data,
|
|
782
|
+
weight,
|
|
783
|
+
radius = 24,
|
|
784
|
+
intensity = 1,
|
|
785
|
+
opacity = 0.85,
|
|
786
|
+
maxZoom = 15,
|
|
787
|
+
id = 'heatmap'
|
|
788
|
+
}) {
|
|
789
|
+
const cool = useToken('--color-muted', 'rgba(0,0,0,0.06)');
|
|
790
|
+
const warm = useToken('--color-chart-1', '#262626');
|
|
791
|
+
const hot = useToken('--color-chart-2', '#666666');
|
|
792
|
+
if (!MapLibre) return null;
|
|
793
|
+
const {
|
|
794
|
+
GeoJSONSource,
|
|
795
|
+
Layer
|
|
796
|
+
} = MapLibre;
|
|
797
|
+
return /*#__PURE__*/_jsx(GeoJSONSource, {
|
|
798
|
+
id: `${id}-source`,
|
|
799
|
+
data: data,
|
|
800
|
+
children: /*#__PURE__*/_jsx(Layer, {
|
|
801
|
+
id: id,
|
|
802
|
+
type: "heatmap",
|
|
803
|
+
maxzoom: maxZoom,
|
|
804
|
+
style: {
|
|
805
|
+
heatmapWeight: weight ? ['interpolate', ['linear'], ['get', weight], 0, 0, 1, 1] : 1,
|
|
806
|
+
heatmapIntensity: intensity,
|
|
807
|
+
// `radius` is the spread at street zoom; below that it is scaled
|
|
808
|
+
// down so it keeps covering the same ground rather than the same
|
|
809
|
+
// number of pixels. The stops are the projection: each zoom level
|
|
810
|
+
// halves the ground a pixel covers, so the radius halves with it,
|
|
811
|
+
// floored so the field stays visible at world zoom.
|
|
812
|
+
heatmapRadius: ['interpolate', ['exponential', 2], ['zoom'], 0, Math.max(2, radius / 10), 8, Math.max(6, radius / 1.8), 14, radius],
|
|
813
|
+
// Transparent at zero rather than the coolest colour, so the map
|
|
814
|
+
// shows through everywhere nothing was measured instead of the whole
|
|
815
|
+
// viewport being tinted by the absence of data.
|
|
816
|
+
heatmapColor: ['interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(0,0,0,0)', 0.2, cool, 0.6, warm, 1, hot],
|
|
817
|
+
heatmapOpacity: ['interpolate', ['linear'], ['zoom'], maxZoom - 2, opacity, maxZoom, 0]
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
MapHeatmap.displayName = 'Map.Heatmap';
|
|
823
|
+
/** The device's own position, drawn by the renderer. */
|
|
824
|
+
function MapUserLocation({
|
|
825
|
+
heading = false,
|
|
826
|
+
accuracy = false
|
|
827
|
+
}) {
|
|
828
|
+
if (!MapLibre) return null;
|
|
829
|
+
const {
|
|
830
|
+
UserLocation
|
|
831
|
+
} = MapLibre;
|
|
832
|
+
return /*#__PURE__*/_jsx(UserLocation, {
|
|
833
|
+
heading: heading,
|
|
834
|
+
accuracy: accuracy
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
MapUserLocation.displayName = 'Map.UserLocation';
|
|
838
|
+
export const Map = Object.assign(MapRoot, {
|
|
839
|
+
Marker: MapMarker,
|
|
840
|
+
Label: MapLabel,
|
|
841
|
+
Popup: MapPopup,
|
|
842
|
+
Controls: MapControls,
|
|
843
|
+
Route: MapRoute,
|
|
844
|
+
Arc: MapArc,
|
|
845
|
+
GeoJSON: MapGeoJSON,
|
|
846
|
+
Cluster: MapCluster,
|
|
847
|
+
Heatmap: MapHeatmap,
|
|
848
|
+
UserLocation: MapUserLocation
|
|
849
|
+
});
|
|
850
|
+
//# sourceMappingURL=index.js.map
|