@lugg/maps 0.2.0-alpha.1 → 0.2.0-alpha.10
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 +17 -4
- package/android/src/main/java/com/luggmaps/LuggGoogleMapView.kt +30 -8
- package/android/src/main/java/com/luggmaps/LuggGoogleMapViewManager.kt +10 -2
- package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +7 -1
- package/android/src/main/java/com/luggmaps/LuggMarkerViewManager.kt +6 -0
- package/android/src/main/java/com/luggmaps/LuggPolylineView.kt +7 -0
- package/android/src/main/java/com/luggmaps/LuggPolylineViewManager.kt +6 -0
- package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +89 -43
- package/ios/LuggAppleMapView.mm +45 -5
- package/ios/LuggGoogleMapView.mm +18 -6
- package/ios/LuggMarkerView.h +2 -0
- package/ios/LuggMarkerView.mm +13 -0
- package/ios/LuggPolylineView.h +1 -0
- package/ios/LuggPolylineView.mm +6 -1
- package/ios/core/GMSPolylineAnimator.m +85 -36
- package/ios/core/MKPolylineAnimator.m +63 -32
- package/lib/module/MapProvider.js +13 -0
- package/lib/module/MapProvider.js.map +1 -0
- package/lib/module/MapProvider.types.js +4 -0
- package/lib/module/MapProvider.types.js.map +1 -0
- package/lib/module/MapProvider.web.js +14 -0
- package/lib/module/MapProvider.web.js.map +1 -0
- package/lib/module/MapView.js +8 -2
- package/lib/module/MapView.js.map +1 -1
- package/lib/module/MapView.web.js +266 -0
- package/lib/module/MapView.web.js.map +1 -0
- package/lib/module/components/Marker.js +4 -1
- package/lib/module/components/Marker.js.map +1 -1
- package/lib/module/components/Marker.web.js +34 -0
- package/lib/module/components/Marker.web.js.map +1 -0
- package/lib/module/components/Polyline.js +5 -2
- package/lib/module/components/Polyline.js.map +1 -1
- package/lib/module/components/Polyline.web.js +177 -0
- package/lib/module/components/Polyline.web.js.map +1 -0
- package/lib/module/components/index.js +2 -2
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/index.web.js +5 -0
- package/lib/module/components/index.web.js.map +1 -0
- package/lib/module/fabric/LuggAppleMapViewNativeComponent.ts +4 -1
- package/lib/module/fabric/LuggGoogleMapViewNativeComponent.ts +4 -1
- package/lib/module/index.js +3 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +6 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/typescript/src/MapProvider.d.ts +8 -0
- package/lib/typescript/src/MapProvider.d.ts.map +1 -0
- package/lib/typescript/src/MapProvider.types.d.ts +16 -0
- package/lib/typescript/src/MapProvider.types.d.ts.map +1 -0
- package/lib/typescript/src/MapProvider.web.d.ts +3 -0
- package/lib/typescript/src/MapProvider.web.d.ts.map +1 -0
- package/lib/typescript/src/MapView.d.ts.map +1 -1
- package/lib/typescript/src/MapView.types.d.ts +1 -2
- package/lib/typescript/src/MapView.types.d.ts.map +1 -1
- package/lib/typescript/src/MapView.web.d.ts +12 -0
- package/lib/typescript/src/MapView.web.d.ts.map +1 -0
- package/lib/typescript/src/components/Marker.d.ts +4 -0
- package/lib/typescript/src/components/Marker.d.ts.map +1 -1
- package/lib/typescript/src/components/Marker.web.d.ts +6 -0
- package/lib/typescript/src/components/Marker.web.d.ts.map +1 -0
- package/lib/typescript/src/components/Polyline.d.ts +4 -0
- package/lib/typescript/src/components/Polyline.d.ts.map +1 -1
- package/lib/typescript/src/components/Polyline.web.d.ts +6 -0
- package/lib/typescript/src/components/Polyline.web.d.ts.map +1 -0
- package/lib/typescript/src/components/index.web.d.ts +5 -0
- package/lib/typescript/src/components/index.web.d.ts.map +1 -0
- package/lib/typescript/src/fabric/LuggAppleMapViewNativeComponent.d.ts +1 -1
- package/lib/typescript/src/fabric/LuggAppleMapViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/LuggGoogleMapViewNativeComponent.d.ts +1 -1
- package/lib/typescript/src/fabric/LuggGoogleMapViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +7 -0
- package/lib/typescript/src/index.web.d.ts.map +1 -0
- package/package.json +15 -2
- package/src/MapProvider.tsx +10 -0
- package/src/MapProvider.types.ts +16 -0
- package/src/MapProvider.web.tsx +6 -0
- package/src/MapView.tsx +11 -2
- package/src/MapView.types.ts +1 -2
- package/src/MapView.web.tsx +319 -0
- package/src/components/Marker.tsx +6 -2
- package/src/components/Marker.web.tsx +32 -0
- package/src/components/Polyline.tsx +6 -1
- package/src/components/Polyline.web.tsx +222 -0
- package/src/components/index.web.ts +4 -0
- package/src/fabric/LuggAppleMapViewNativeComponent.ts +4 -1
- package/src/fabric/LuggGoogleMapViewNativeComponent.ts +4 -1
- package/src/index.ts +8 -1
- package/src/index.web.ts +17 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
Component,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type ReactElement,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import type { NativeSyntheticEvent, ViewStyle } from 'react-native';
|
|
13
|
+
import { View } from 'react-native';
|
|
14
|
+
import { Map, useMap } from '@vis.gl/react-google-maps';
|
|
15
|
+
import { Marker } from './components/Marker.web';
|
|
16
|
+
import { Polyline } from './components/Polyline.web';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
MapViewProps,
|
|
20
|
+
MapViewRef,
|
|
21
|
+
MoveCameraOptions,
|
|
22
|
+
FitCoordinatesOptions,
|
|
23
|
+
CameraEventPayload,
|
|
24
|
+
} from './MapView.types';
|
|
25
|
+
import type { Coordinate } from './types';
|
|
26
|
+
|
|
27
|
+
// Map-specific component types that render inside the Google Map
|
|
28
|
+
const MAP_COMPONENT_TYPES = new Set([Marker, Polyline]);
|
|
29
|
+
|
|
30
|
+
const isMapComponent = (child: ReactElement): boolean =>
|
|
31
|
+
MAP_COMPONENT_TYPES.has(child.type as typeof Marker | typeof Polyline);
|
|
32
|
+
|
|
33
|
+
const createSyntheticEvent = <T,>(nativeEvent: T): NativeSyntheticEvent<T> =>
|
|
34
|
+
({
|
|
35
|
+
nativeEvent,
|
|
36
|
+
currentTarget: null,
|
|
37
|
+
target: null,
|
|
38
|
+
bubbles: false,
|
|
39
|
+
cancelable: false,
|
|
40
|
+
defaultPrevented: false,
|
|
41
|
+
eventPhase: 0,
|
|
42
|
+
isTrusted: true,
|
|
43
|
+
preventDefault: () => {},
|
|
44
|
+
stopPropagation: () => {},
|
|
45
|
+
isDefaultPrevented: () => false,
|
|
46
|
+
isPropagationStopped: () => false,
|
|
47
|
+
persist: () => {},
|
|
48
|
+
timeStamp: Date.now(),
|
|
49
|
+
type: '',
|
|
50
|
+
} as unknown as NativeSyntheticEvent<T>);
|
|
51
|
+
|
|
52
|
+
interface MapControllerProps {
|
|
53
|
+
onMapReady: (map: google.maps.Map) => void;
|
|
54
|
+
onCameraMove?: (event: NativeSyntheticEvent<CameraEventPayload>) => void;
|
|
55
|
+
onCameraIdle?: (event: NativeSyntheticEvent<CameraEventPayload>) => void;
|
|
56
|
+
onReady?: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface UserLocationMarkerProps {
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const userLocationDotStyle: CSSProperties = {
|
|
64
|
+
width: 16,
|
|
65
|
+
height: 16,
|
|
66
|
+
backgroundColor: '#4285F4',
|
|
67
|
+
border: '2px solid white',
|
|
68
|
+
borderRadius: '50%',
|
|
69
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.3)',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function UserLocationMarker({ enabled }: UserLocationMarkerProps) {
|
|
73
|
+
const [coordinate, setCoordinate] = useState<Coordinate | null>(null);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!enabled) {
|
|
77
|
+
setCoordinate(null);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let watchId: number | null = null;
|
|
82
|
+
|
|
83
|
+
const updateLocation = (position: GeolocationPosition) => {
|
|
84
|
+
setCoordinate({
|
|
85
|
+
latitude: position.coords.latitude,
|
|
86
|
+
longitude: position.coords.longitude,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
navigator.geolocation.getCurrentPosition(updateLocation, () => {});
|
|
91
|
+
watchId = navigator.geolocation.watchPosition(updateLocation, () => {});
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
if (watchId !== null) {
|
|
95
|
+
navigator.geolocation.clearWatch(watchId);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}, [enabled]);
|
|
99
|
+
|
|
100
|
+
if (!coordinate) return null;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Marker coordinate={coordinate} anchor={{ x: 0.5, y: 0.5 }}>
|
|
104
|
+
<div style={userLocationDotStyle} />
|
|
105
|
+
</Marker>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function MapController({
|
|
110
|
+
onMapReady,
|
|
111
|
+
onCameraMove,
|
|
112
|
+
onCameraIdle,
|
|
113
|
+
onReady,
|
|
114
|
+
}: MapControllerProps) {
|
|
115
|
+
const map = useMap();
|
|
116
|
+
const readyFired = useRef(false);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!map) return;
|
|
120
|
+
onMapReady(map);
|
|
121
|
+
|
|
122
|
+
if (!readyFired.current) {
|
|
123
|
+
readyFired.current = true;
|
|
124
|
+
onReady?.();
|
|
125
|
+
}
|
|
126
|
+
}, [map, onMapReady, onReady]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!map) return;
|
|
130
|
+
|
|
131
|
+
const createPayload = (gesture: boolean): CameraEventPayload => {
|
|
132
|
+
const center = map.getCenter();
|
|
133
|
+
return {
|
|
134
|
+
coordinate: {
|
|
135
|
+
latitude: center?.lat() ?? 0,
|
|
136
|
+
longitude: center?.lng() ?? 0,
|
|
137
|
+
},
|
|
138
|
+
zoom: map.getZoom() ?? 0,
|
|
139
|
+
gesture,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
let isDragging = false;
|
|
144
|
+
|
|
145
|
+
const dragStartListener = map.addListener('dragstart', () => {
|
|
146
|
+
isDragging = true;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const dragEndListener = map.addListener('dragend', () => {
|
|
150
|
+
isDragging = false;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const centerListener = map.addListener('center_changed', () => {
|
|
154
|
+
onCameraMove?.(createSyntheticEvent(createPayload(isDragging)));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const idleListener = map.addListener('idle', () => {
|
|
158
|
+
onCameraIdle?.(createSyntheticEvent(createPayload(false)));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
google.maps.event.removeListener(dragStartListener);
|
|
163
|
+
google.maps.event.removeListener(dragEndListener);
|
|
164
|
+
google.maps.event.removeListener(centerListener);
|
|
165
|
+
google.maps.event.removeListener(idleListener);
|
|
166
|
+
};
|
|
167
|
+
}, [map, onCameraMove, onCameraIdle]);
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export class MapView extends Component<MapViewProps> implements MapViewRef {
|
|
173
|
+
static defaultProps: Partial<MapViewProps> = {
|
|
174
|
+
provider: 'google',
|
|
175
|
+
mapId: 'DEMO_MAP_ID',
|
|
176
|
+
initialZoom: 10,
|
|
177
|
+
zoomEnabled: true,
|
|
178
|
+
scrollEnabled: true,
|
|
179
|
+
rotateEnabled: true,
|
|
180
|
+
pitchEnabled: true,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
private mapInstance: google.maps.Map | null = null;
|
|
184
|
+
|
|
185
|
+
private handleMapReady = (map: google.maps.Map) => {
|
|
186
|
+
this.mapInstance = map;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
moveCamera(coordinate: Coordinate, options: MoveCameraOptions) {
|
|
190
|
+
const map = this.mapInstance;
|
|
191
|
+
if (!map) return;
|
|
192
|
+
|
|
193
|
+
const { zoom, duration = -1 } = options;
|
|
194
|
+
const center = { lat: coordinate.latitude, lng: coordinate.longitude };
|
|
195
|
+
|
|
196
|
+
if (duration === 0) {
|
|
197
|
+
map.moveCamera({ center, zoom });
|
|
198
|
+
} else {
|
|
199
|
+
const currentZoom = map.getZoom();
|
|
200
|
+
const zoomChanged = zoom !== undefined && zoom !== currentZoom;
|
|
201
|
+
|
|
202
|
+
if (zoomChanged) {
|
|
203
|
+
map.setZoom(zoom);
|
|
204
|
+
}
|
|
205
|
+
map.panTo(center);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fitCoordinates(coordinates: Coordinate[], options?: FitCoordinatesOptions) {
|
|
210
|
+
const map = this.mapInstance;
|
|
211
|
+
const first = coordinates[0];
|
|
212
|
+
if (!map || !first) return;
|
|
213
|
+
|
|
214
|
+
const { padding, duration = -1 } = options ?? {};
|
|
215
|
+
|
|
216
|
+
if (coordinates.length === 1) {
|
|
217
|
+
const zoom = this.props.initialZoom ?? 10;
|
|
218
|
+
this.moveCamera(first, { zoom, duration });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const bounds = new google.maps.LatLngBounds();
|
|
223
|
+
coordinates.forEach((coord) => {
|
|
224
|
+
bounds.extend({ lat: coord.latitude, lng: coord.longitude });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
map.fitBounds(bounds, {
|
|
228
|
+
top: padding?.top ?? 0,
|
|
229
|
+
left: padding?.left ?? 0,
|
|
230
|
+
bottom: padding?.bottom ?? 0,
|
|
231
|
+
right: padding?.right ?? 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
render() {
|
|
236
|
+
const {
|
|
237
|
+
mapId,
|
|
238
|
+
initialCoordinate,
|
|
239
|
+
initialZoom,
|
|
240
|
+
minZoom,
|
|
241
|
+
maxZoom,
|
|
242
|
+
zoomEnabled,
|
|
243
|
+
scrollEnabled,
|
|
244
|
+
pitchEnabled,
|
|
245
|
+
padding,
|
|
246
|
+
userLocationEnabled,
|
|
247
|
+
onCameraMove,
|
|
248
|
+
onCameraIdle,
|
|
249
|
+
onReady,
|
|
250
|
+
children,
|
|
251
|
+
style,
|
|
252
|
+
} = this.props;
|
|
253
|
+
|
|
254
|
+
const gestureHandling =
|
|
255
|
+
scrollEnabled === false && zoomEnabled === false
|
|
256
|
+
? 'none'
|
|
257
|
+
: scrollEnabled === false
|
|
258
|
+
? 'none'
|
|
259
|
+
: 'auto';
|
|
260
|
+
|
|
261
|
+
const defaultCenter = initialCoordinate
|
|
262
|
+
? { lat: initialCoordinate.latitude, lng: initialCoordinate.longitude }
|
|
263
|
+
: undefined;
|
|
264
|
+
|
|
265
|
+
// Separate map children (Marker, Polyline) from overlay children (regular Views)
|
|
266
|
+
const mapChildren: ReactNode[] = [];
|
|
267
|
+
const overlayChildren: ReactNode[] = [];
|
|
268
|
+
|
|
269
|
+
Children.forEach(children, (child) => {
|
|
270
|
+
if (!isValidElement(child)) return;
|
|
271
|
+
if (isMapComponent(child)) {
|
|
272
|
+
mapChildren.push(child);
|
|
273
|
+
} else {
|
|
274
|
+
overlayChildren.push(child);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const mapContainerStyle: ViewStyle = {
|
|
279
|
+
position: 'absolute',
|
|
280
|
+
top: padding?.top ?? 0,
|
|
281
|
+
left: padding?.left ?? 0,
|
|
282
|
+
right: padding?.right ?? 0,
|
|
283
|
+
bottom: padding?.bottom ?? 0,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const mapStyle: CSSProperties = {
|
|
287
|
+
width: '100%',
|
|
288
|
+
height: '100%',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<View style={style}>
|
|
293
|
+
<View style={mapContainerStyle}>
|
|
294
|
+
<Map
|
|
295
|
+
mapId={mapId}
|
|
296
|
+
defaultCenter={defaultCenter}
|
|
297
|
+
defaultZoom={initialZoom}
|
|
298
|
+
minZoom={minZoom}
|
|
299
|
+
maxZoom={maxZoom}
|
|
300
|
+
gestureHandling={gestureHandling}
|
|
301
|
+
disableDefaultUI
|
|
302
|
+
tilt={pitchEnabled === false ? 0 : undefined}
|
|
303
|
+
style={mapStyle}
|
|
304
|
+
>
|
|
305
|
+
<MapController
|
|
306
|
+
onMapReady={this.handleMapReady}
|
|
307
|
+
onCameraMove={onCameraMove}
|
|
308
|
+
onCameraIdle={onCameraIdle}
|
|
309
|
+
onReady={onReady}
|
|
310
|
+
/>
|
|
311
|
+
<UserLocationMarker enabled={userLocationEnabled} />
|
|
312
|
+
{mapChildren}
|
|
313
|
+
</Map>
|
|
314
|
+
</View>
|
|
315
|
+
{overlayChildren}
|
|
316
|
+
</View>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -25,6 +25,10 @@ export interface MarkerProps {
|
|
|
25
25
|
* Anchor point for custom marker views
|
|
26
26
|
*/
|
|
27
27
|
anchor?: Point;
|
|
28
|
+
/**
|
|
29
|
+
* Z-index for marker ordering. Higher values render on top.
|
|
30
|
+
*/
|
|
31
|
+
zIndex?: number;
|
|
28
32
|
/**
|
|
29
33
|
* Custom marker view
|
|
30
34
|
*/
|
|
@@ -33,12 +37,12 @@ export interface MarkerProps {
|
|
|
33
37
|
|
|
34
38
|
export class Marker extends React.Component<MarkerProps> {
|
|
35
39
|
render() {
|
|
36
|
-
const { name, coordinate, title, description, anchor, children } =
|
|
40
|
+
const { name, coordinate, title, description, anchor, zIndex, children } =
|
|
37
41
|
this.props;
|
|
38
42
|
|
|
39
43
|
return (
|
|
40
44
|
<LuggMarkerViewNativeComponent
|
|
41
|
-
style={styles.marker}
|
|
45
|
+
style={[{ zIndex }, styles.marker]}
|
|
42
46
|
name={name}
|
|
43
47
|
coordinate={coordinate}
|
|
44
48
|
title={title}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AdvancedMarker } from '@vis.gl/react-google-maps';
|
|
3
|
+
import type { MarkerProps } from './Marker';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts point to % anchor for web.
|
|
7
|
+
* e.g. `0.5` to `-50%`
|
|
8
|
+
*/
|
|
9
|
+
const toWebAnchor = (value: number) => `-${value * 100}%`;
|
|
10
|
+
|
|
11
|
+
export class Marker extends React.Component<MarkerProps> {
|
|
12
|
+
render() {
|
|
13
|
+
const { coordinate, title, anchor, zIndex, children } = this.props;
|
|
14
|
+
|
|
15
|
+
const position = {
|
|
16
|
+
lat: coordinate.latitude,
|
|
17
|
+
lng: coordinate.longitude,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<AdvancedMarker
|
|
22
|
+
position={position}
|
|
23
|
+
title={title}
|
|
24
|
+
zIndex={zIndex}
|
|
25
|
+
anchorLeft={anchor ? toWebAnchor(anchor.x) : undefined}
|
|
26
|
+
anchorTop={anchor ? toWebAnchor(anchor.y) : undefined}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</AdvancedMarker>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -21,6 +21,10 @@ export interface PolylineProps {
|
|
|
21
21
|
* Animate the polyline with a snake effect
|
|
22
22
|
*/
|
|
23
23
|
animated?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Z-index for layering polylines
|
|
26
|
+
*/
|
|
27
|
+
zIndex?: number;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export class Polyline extends React.Component<PolylineProps> {
|
|
@@ -30,11 +34,12 @@ export class Polyline extends React.Component<PolylineProps> {
|
|
|
30
34
|
strokeColors,
|
|
31
35
|
strokeWidth,
|
|
32
36
|
animated = false,
|
|
37
|
+
zIndex,
|
|
33
38
|
} = this.props;
|
|
34
39
|
|
|
35
40
|
return (
|
|
36
41
|
<LuggPolylineViewNativeComponent
|
|
37
|
-
style={styles.polyline}
|
|
42
|
+
style={[{ zIndex }, styles.polyline]}
|
|
38
43
|
coordinates={coordinates}
|
|
39
44
|
strokeColors={strokeColors}
|
|
40
45
|
strokeWidth={strokeWidth}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { useMap } from '@vis.gl/react-google-maps';
|
|
10
|
+
import type { PolylineProps } from './Polyline';
|
|
11
|
+
|
|
12
|
+
const ANIMATION_DURATION = 1500;
|
|
13
|
+
|
|
14
|
+
function interpolateColor(color1: string, color2: string, t: number): string {
|
|
15
|
+
const hex = (c: string) => parseInt(c, 16);
|
|
16
|
+
const r1 = hex(color1.slice(1, 3));
|
|
17
|
+
const g1 = hex(color1.slice(3, 5));
|
|
18
|
+
const b1 = hex(color1.slice(5, 7));
|
|
19
|
+
const r2 = hex(color2.slice(1, 3));
|
|
20
|
+
const g2 = hex(color2.slice(3, 5));
|
|
21
|
+
const b2 = hex(color2.slice(5, 7));
|
|
22
|
+
|
|
23
|
+
const r = Math.round(r1 + (r2 - r1) * t);
|
|
24
|
+
const g = Math.round(g1 + (g2 - g1) * t);
|
|
25
|
+
const b = Math.round(b1 + (b2 - b1) * t);
|
|
26
|
+
|
|
27
|
+
return `#${r.toString(16).padStart(2, '0')}${g
|
|
28
|
+
.toString(16)
|
|
29
|
+
.padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getGradientColor(colors: string[], position: number): string {
|
|
33
|
+
if (colors.length === 0) return '#000000';
|
|
34
|
+
if (colors.length === 1 || position <= 0) return colors[0]!;
|
|
35
|
+
if (position >= 1) return colors[colors.length - 1]!;
|
|
36
|
+
|
|
37
|
+
const scaledPos = position * (colors.length - 1);
|
|
38
|
+
const index = Math.floor(scaledPos);
|
|
39
|
+
const t = scaledPos - index;
|
|
40
|
+
|
|
41
|
+
return interpolateColor(colors[index]!, colors[index + 1]!, t);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function PolylineImpl({
|
|
45
|
+
coordinates,
|
|
46
|
+
strokeColors,
|
|
47
|
+
strokeWidth = 1,
|
|
48
|
+
animated,
|
|
49
|
+
zIndex,
|
|
50
|
+
}: PolylineProps) {
|
|
51
|
+
const resolvedZIndex = zIndex ?? (animated ? 1 : 0);
|
|
52
|
+
const map = useMap();
|
|
53
|
+
const polylinesRef = useRef<google.maps.Polyline[]>([]);
|
|
54
|
+
const animationRef = useRef<number>(0);
|
|
55
|
+
|
|
56
|
+
const colors = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
strokeColors && strokeColors.length > 0
|
|
59
|
+
? (strokeColors as string[])
|
|
60
|
+
: ['#000000'],
|
|
61
|
+
[strokeColors]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const hasGradient = colors.length > 1;
|
|
65
|
+
|
|
66
|
+
// Refs for animation loop access
|
|
67
|
+
const propsRef = useRef({
|
|
68
|
+
map,
|
|
69
|
+
colors,
|
|
70
|
+
strokeWidth,
|
|
71
|
+
hasGradient,
|
|
72
|
+
zIndex: resolvedZIndex,
|
|
73
|
+
});
|
|
74
|
+
const [mapReady, setMapReady] = useState(!!map);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
propsRef.current = {
|
|
78
|
+
map,
|
|
79
|
+
colors,
|
|
80
|
+
strokeWidth,
|
|
81
|
+
hasGradient,
|
|
82
|
+
zIndex: resolvedZIndex,
|
|
83
|
+
};
|
|
84
|
+
if (map && !mapReady) setMapReady(true);
|
|
85
|
+
}, [map, colors, strokeWidth, hasGradient, resolvedZIndex, mapReady]);
|
|
86
|
+
|
|
87
|
+
const updatePath = useCallback((path: google.maps.LatLngLiteral[]) => {
|
|
88
|
+
const {
|
|
89
|
+
map: currentMap,
|
|
90
|
+
colors: currentColors,
|
|
91
|
+
strokeWidth: currentStrokeWidth,
|
|
92
|
+
hasGradient: currentHasGradient,
|
|
93
|
+
zIndex: currentZIndex,
|
|
94
|
+
} = propsRef.current;
|
|
95
|
+
if (!currentMap || path.length < 2) return;
|
|
96
|
+
|
|
97
|
+
const neededSegments = currentHasGradient ? path.length - 1 : 1;
|
|
98
|
+
const existing = polylinesRef.current;
|
|
99
|
+
|
|
100
|
+
// Update or create segments
|
|
101
|
+
for (let i = 0; i < neededSegments; i++) {
|
|
102
|
+
const segmentPath = currentHasGradient ? [path[i]!, path[i + 1]!] : path;
|
|
103
|
+
const color = currentHasGradient
|
|
104
|
+
? getGradientColor(currentColors, i / (path.length - 1))
|
|
105
|
+
: currentColors[0]!;
|
|
106
|
+
|
|
107
|
+
const segment = existing[i];
|
|
108
|
+
if (segment) {
|
|
109
|
+
segment.setPath(segmentPath);
|
|
110
|
+
segment.setOptions({ strokeColor: color });
|
|
111
|
+
} else {
|
|
112
|
+
existing.push(
|
|
113
|
+
new google.maps.Polyline({
|
|
114
|
+
path: segmentPath,
|
|
115
|
+
strokeColor: color,
|
|
116
|
+
strokeWeight: currentStrokeWidth,
|
|
117
|
+
strokeOpacity: 1,
|
|
118
|
+
zIndex: currentZIndex,
|
|
119
|
+
map: currentMap,
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Remove extra segments
|
|
126
|
+
for (let i = neededSegments; i < existing.length; i++) {
|
|
127
|
+
existing[i]?.setMap(null);
|
|
128
|
+
}
|
|
129
|
+
existing.length = neededSegments;
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// Cleanup on unmount
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const polylines = polylinesRef.current;
|
|
135
|
+
return () => {
|
|
136
|
+
cancelAnimationFrame(animationRef.current);
|
|
137
|
+
polylines.forEach((p) => p.setMap(null));
|
|
138
|
+
};
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
// Main effect
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!propsRef.current.map || coordinates.length === 0) return;
|
|
144
|
+
|
|
145
|
+
const fullPath = coordinates.map((c) => ({
|
|
146
|
+
lat: c.latitude,
|
|
147
|
+
lng: c.longitude,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
cancelAnimationFrame(animationRef.current);
|
|
151
|
+
|
|
152
|
+
if (!animated) {
|
|
153
|
+
updatePath(fullPath);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const totalPoints = fullPath.length;
|
|
158
|
+
const cycleDuration = ANIMATION_DURATION * 2;
|
|
159
|
+
|
|
160
|
+
const animate = (time: number) => {
|
|
161
|
+
const progress = (time % cycleDuration) / ANIMATION_DURATION;
|
|
162
|
+
const startIdx = progress <= 1 ? 0 : (progress - 1) * totalPoints;
|
|
163
|
+
const endIdx = progress <= 1 ? progress * totalPoints : totalPoints;
|
|
164
|
+
|
|
165
|
+
const partialPath: google.maps.LatLngLiteral[] = [];
|
|
166
|
+
const startFloor = Math.floor(startIdx);
|
|
167
|
+
const endFloor = Math.floor(endIdx);
|
|
168
|
+
|
|
169
|
+
// Start point (interpolated)
|
|
170
|
+
if (startFloor < totalPoints) {
|
|
171
|
+
const frac = startIdx - startFloor;
|
|
172
|
+
const from = fullPath[startFloor]!;
|
|
173
|
+
const to = fullPath[Math.min(startFloor + 1, totalPoints - 1)]!;
|
|
174
|
+
partialPath.push(
|
|
175
|
+
frac > 0
|
|
176
|
+
? {
|
|
177
|
+
lat: from.lat + (to.lat - from.lat) * frac,
|
|
178
|
+
lng: from.lng + (to.lng - from.lng) * frac,
|
|
179
|
+
}
|
|
180
|
+
: from
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Middle points
|
|
185
|
+
for (
|
|
186
|
+
let i = startFloor + 1;
|
|
187
|
+
i <= Math.min(endFloor, totalPoints - 1);
|
|
188
|
+
i++
|
|
189
|
+
) {
|
|
190
|
+
partialPath.push(fullPath[i]!);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// End point (interpolated)
|
|
194
|
+
if (endFloor < totalPoints - 1) {
|
|
195
|
+
const frac = endIdx - endFloor;
|
|
196
|
+
const from = fullPath[endFloor]!;
|
|
197
|
+
const to = fullPath[endFloor + 1]!;
|
|
198
|
+
if (frac > 0) {
|
|
199
|
+
partialPath.push({
|
|
200
|
+
lat: from.lat + (to.lat - from.lat) * frac,
|
|
201
|
+
lng: from.lng + (to.lng - from.lng) * frac,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
updatePath(partialPath);
|
|
207
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
211
|
+
|
|
212
|
+
return () => cancelAnimationFrame(animationRef.current);
|
|
213
|
+
}, [coordinates, animated, hasGradient, updatePath, mapReady]);
|
|
214
|
+
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class Polyline extends Component<PolylineProps> {
|
|
219
|
+
render() {
|
|
220
|
+
return <PolylineImpl {...this.props} />;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -66,7 +66,10 @@ interface NativeCommands {
|
|
|
66
66
|
fitCoordinates: (
|
|
67
67
|
viewRef: React.ElementRef<ComponentType>,
|
|
68
68
|
coordinates: Coordinate[],
|
|
69
|
-
|
|
69
|
+
paddingTop: Double,
|
|
70
|
+
paddingLeft: Double,
|
|
71
|
+
paddingBottom: Double,
|
|
72
|
+
paddingRight: Double,
|
|
70
73
|
duration: Double
|
|
71
74
|
) => void;
|
|
72
75
|
}
|
|
@@ -67,7 +67,10 @@ interface NativeCommands {
|
|
|
67
67
|
fitCoordinates: (
|
|
68
68
|
viewRef: React.ElementRef<ComponentType>,
|
|
69
69
|
coordinates: Coordinate[],
|
|
70
|
-
|
|
70
|
+
paddingTop: Double,
|
|
71
|
+
paddingLeft: Double,
|
|
72
|
+
paddingBottom: Double,
|
|
73
|
+
paddingRight: Double,
|
|
71
74
|
duration: Double
|
|
72
75
|
) => void;
|
|
73
76
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { MapView } from './MapView';
|
|
2
|
+
export { MapProvider } from './MapProvider';
|
|
3
|
+
export type { MapProviderProps } from './MapProvider.types';
|
|
2
4
|
export * from './components';
|
|
3
5
|
export type {
|
|
4
6
|
MapViewProps,
|
|
@@ -7,4 +9,9 @@ export type {
|
|
|
7
9
|
FitCoordinatesOptions,
|
|
8
10
|
CameraEventPayload,
|
|
9
11
|
} from './MapView.types';
|
|
10
|
-
export type {
|
|
12
|
+
export type {
|
|
13
|
+
MapProvider as MapProviderType,
|
|
14
|
+
Coordinate,
|
|
15
|
+
Point,
|
|
16
|
+
EdgeInsets,
|
|
17
|
+
} from './types';
|
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { MapView } from './MapView.web';
|
|
2
|
+
export { MapProvider } from './MapProvider.web';
|
|
3
|
+
export type { MapProviderProps } from './MapProvider.types';
|
|
4
|
+
export * from './components/index.web';
|
|
5
|
+
export type {
|
|
6
|
+
MapViewProps,
|
|
7
|
+
MapViewRef,
|
|
8
|
+
MoveCameraOptions,
|
|
9
|
+
FitCoordinatesOptions,
|
|
10
|
+
CameraEventPayload,
|
|
11
|
+
} from './MapView.types';
|
|
12
|
+
export type {
|
|
13
|
+
MapProvider as MapProviderType,
|
|
14
|
+
Coordinate,
|
|
15
|
+
Point,
|
|
16
|
+
EdgeInsets,
|
|
17
|
+
} from './types';
|