react-native-maplibre-lite 0.1.9 → 0.2.1
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 +92 -6
- package/lib/module/components/MapView.js +167 -48
- package/lib/module/components/MapView.js.map +1 -1
- package/lib/module/components/NavigatorHud.js +152 -0
- package/lib/module/components/NavigatorHud.js.map +1 -0
- package/lib/module/components/NavigatorRecenterButton.js +48 -0
- package/lib/module/components/NavigatorRecenterButton.js.map +1 -0
- package/lib/module/components/NavigatorVoiceControl.js +173 -0
- package/lib/module/components/NavigatorVoiceControl.js.map +1 -0
- package/lib/module/components/navigatorChromeTheme.js +98 -0
- package/lib/module/components/navigatorChromeTheme.js.map +1 -0
- package/lib/module/components/navigatorManeuverIcon.js +210 -0
- package/lib/module/components/navigatorManeuverIcon.js.map +1 -0
- package/lib/module/components/navigatorVoiceCatalog.js +225 -0
- package/lib/module/components/navigatorVoiceCatalog.js.map +1 -0
- package/lib/module/components/navigatorVoiceKeys.js +14 -0
- package/lib/module/components/navigatorVoiceKeys.js.map +1 -0
- package/lib/module/components/navigatorVoicePlayer.js +100 -0
- package/lib/module/components/navigatorVoicePlayer.js.map +1 -0
- package/lib/module/components/navigatorVoiceStrings.js +31 -0
- package/lib/module/components/navigatorVoiceStrings.js.map +1 -0
- package/lib/module/components/types.js +47 -0
- package/lib/module/components/types.js.map +1 -1
- package/lib/module/components/useNavigatorVoice.js +78 -0
- package/lib/module/components/useNavigatorVoice.js.map +1 -0
- package/lib/module/components/utils.js +26 -0
- package/lib/module/components/utils.js.map +1 -1
- package/lib/module/components/webMapBuild.js +1 -1
- package/lib/module/components/webMapBuild.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/MapView.d.ts +15 -2
- package/lib/typescript/src/components/MapView.d.ts.map +1 -1
- package/lib/typescript/src/components/NavigatorHud.d.ts +13 -0
- package/lib/typescript/src/components/NavigatorHud.d.ts.map +1 -0
- package/lib/typescript/src/components/NavigatorRecenterButton.d.ts +11 -0
- package/lib/typescript/src/components/NavigatorRecenterButton.d.ts.map +1 -0
- package/lib/typescript/src/components/NavigatorVoiceControl.d.ts +20 -0
- package/lib/typescript/src/components/NavigatorVoiceControl.d.ts.map +1 -0
- package/lib/typescript/src/components/navigatorChromeTheme.d.ts +19 -0
- package/lib/typescript/src/components/navigatorChromeTheme.d.ts.map +1 -0
- package/lib/typescript/src/components/navigatorManeuverIcon.d.ts +20 -0
- package/lib/typescript/src/components/navigatorManeuverIcon.d.ts.map +1 -0
- package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts +50 -0
- package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts.map +1 -0
- package/lib/typescript/src/components/navigatorVoiceKeys.d.ts +10 -0
- package/lib/typescript/src/components/navigatorVoiceKeys.d.ts.map +1 -0
- package/lib/typescript/src/components/navigatorVoicePlayer.d.ts +15 -0
- package/lib/typescript/src/components/navigatorVoicePlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/navigatorVoiceStrings.d.ts +19 -0
- package/lib/typescript/src/components/navigatorVoiceStrings.d.ts.map +1 -0
- package/lib/typescript/src/components/types.d.ts +205 -12
- package/lib/typescript/src/components/types.d.ts.map +1 -1
- package/lib/typescript/src/components/useNavigatorVoice.d.ts +20 -0
- package/lib/typescript/src/components/useNavigatorVoice.d.ts.map +1 -0
- package/lib/typescript/src/components/utils.d.ts +9 -0
- package/lib/typescript/src/components/utils.d.ts.map +1 -1
- package/lib/typescript/src/components/webMapBuild.d.ts +1 -1
- package/lib/typescript/src/components/webMapBuild.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +16 -7
- package/resources/README.md +62 -0
- package/resources/map.html +797 -0
- package/src/components/MapView.tsx +209 -58
- package/src/components/NavigatorHud.tsx +166 -0
- package/src/components/NavigatorRecenterButton.tsx +45 -0
- package/src/components/NavigatorVoiceControl.tsx +198 -0
- package/src/components/navigatorChromeTheme.ts +118 -0
- package/src/components/navigatorManeuverIcon.tsx +177 -0
- package/src/components/navigatorVoiceCatalog.ts +275 -0
- package/src/components/navigatorVoiceKeys.ts +132 -0
- package/src/components/navigatorVoicePlayer.tsx +126 -0
- package/src/components/navigatorVoiceStrings.ts +42 -0
- package/src/components/types.ts +198 -16
- package/src/components/useNavigatorVoice.ts +96 -0
- package/src/components/utils.ts +28 -0
- package/src/components/webMapBuild.ts +1 -1
- package/src/index.tsx +19 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
6
|
useImperativeHandle,
|
|
7
|
+
useMemo,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from 'react';
|
|
@@ -21,31 +22,42 @@ import KeepAwake from '@sayem314/react-native-keep-awake';
|
|
|
21
22
|
|
|
22
23
|
import MapPlaceholder from './MapPlaceholder';
|
|
23
24
|
import MapSelectPoint, { type MapSelectPointType } from './MapSelectPoint';
|
|
25
|
+
import { NavigatorHud } from './NavigatorHud';
|
|
26
|
+
import { NavigatorRecenterButton } from './NavigatorRecenterButton';
|
|
27
|
+
import { NavigatorVoiceControl } from './NavigatorVoiceControl';
|
|
28
|
+
import { resolveNavigatorChromeTheme } from './navigatorChromeTheme';
|
|
29
|
+
import { keysToClipUrls, volumeLevelToGain } from './navigatorVoiceCatalog';
|
|
30
|
+
import { NavigatorVoicePlayer, type NavigatorVoicePlayerRef } from './navigatorVoicePlayer';
|
|
31
|
+
import { navigatorUiStrings } from './navigatorVoiceStrings';
|
|
32
|
+
import { useNavigatorVoice } from './useNavigatorVoice';
|
|
24
33
|
import {
|
|
25
34
|
type EventParams,
|
|
35
|
+
type MapLiteInitParams,
|
|
36
|
+
type MapLiteMapErrorEventParams,
|
|
37
|
+
type MapLiteMarkerParams,
|
|
38
|
+
type MapLitePolygonParams,
|
|
39
|
+
type MapLitePolylineParams,
|
|
40
|
+
type MapLiteRemoveOverlayParams,
|
|
41
|
+
type MapLiteSetNavigatorPositionParams,
|
|
42
|
+
type MapLiteUpdateParams,
|
|
26
43
|
type MapLiteWebError,
|
|
27
44
|
type MarkerProps,
|
|
45
|
+
type NativeToWebCommand,
|
|
28
46
|
type NavigatorChromeParams,
|
|
47
|
+
type NavigatorHudState,
|
|
29
48
|
type NavigatorInstructionParams,
|
|
30
49
|
type NavigatorLang,
|
|
31
50
|
type NavigatorProfile,
|
|
32
51
|
type NavigatorPositionSetParams,
|
|
33
52
|
type NavigatorRouteSetParams,
|
|
53
|
+
type NavigatorVoicePlayParams,
|
|
34
54
|
type PolygonProps,
|
|
35
55
|
type PolylineProps,
|
|
56
|
+
type WebToNativeMessage,
|
|
36
57
|
} from './types';
|
|
37
|
-
import { loadResources } from './utils';
|
|
58
|
+
import { loadResources, getNativeMapHtmlUri } from './utils';
|
|
38
59
|
import { MAP_HTML } from './webMapBuild';
|
|
39
60
|
|
|
40
|
-
interface UpdateProps {
|
|
41
|
-
center?: [number, number];
|
|
42
|
-
zoom?: number;
|
|
43
|
-
minZoom?: number;
|
|
44
|
-
maxZoom?: number;
|
|
45
|
-
zoomEnabled?: boolean;
|
|
46
|
-
scrollEnabled?: boolean;
|
|
47
|
-
mapStyle?: string;
|
|
48
|
-
}
|
|
49
61
|
|
|
50
62
|
interface MapViewProps {
|
|
51
63
|
children?: React.ReactNode;
|
|
@@ -73,6 +85,8 @@ interface MapViewProps {
|
|
|
73
85
|
onZoomStart?: (eventParams: EventParams) => void;
|
|
74
86
|
onZoomEnd?: (eventParams: EventParams) => void;
|
|
75
87
|
onIdle?: (eventParams: EventParams) => void;
|
|
88
|
+
/** MapLibre `error` из WebView (`event: 'error'`). */
|
|
89
|
+
onMapError?: (params: MapLiteMapErrorEventParams) => void;
|
|
76
90
|
showSelectPoint?: boolean;
|
|
77
91
|
selectPointColor?: string;
|
|
78
92
|
selectPointBackgroundColor?: string;
|
|
@@ -102,12 +116,23 @@ interface MapViewProps {
|
|
|
102
116
|
* Sent once in WebView `init` as `navigatorChrome`.
|
|
103
117
|
*/
|
|
104
118
|
navigatorChrome?: NavigatorChromeParams;
|
|
119
|
+
/**
|
|
120
|
+
* URL каталога голосов навигатора (`.../voices/data.json`). Каталог,
|
|
121
|
+
* манифест, выбор голоса/громкости и воспроизведение — на нативной
|
|
122
|
+
* стороне. Без него озвучка и FAB выбора голоса отключены.
|
|
123
|
+
*/
|
|
124
|
+
navigatorVoiceUrl?: string;
|
|
105
125
|
onNavigatorRouteSet?: (params: NavigatorRouteSetParams) => void;
|
|
106
126
|
onNavigatorInstruction?: (params: NavigatorInstructionParams) => void;
|
|
107
127
|
onNavigatorPositionSet?: (params: NavigatorPositionSetParams) => void;
|
|
108
128
|
/** Ошибки команд WebView (`type: 'error'` из карты). */
|
|
109
129
|
onMapLiteError?: (err: MapLiteWebError) => void;
|
|
110
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Загружать WebView из `map.html` в нативных ресурсах приложения вместо inline `MAP_HTML`.
|
|
133
|
+
* Размещение файла — см. `resources/README.md`.
|
|
134
|
+
*/
|
|
135
|
+
useNativeMapHtml?: boolean;
|
|
111
136
|
|
|
112
137
|
developerLocalhostBundleUrl?: string;
|
|
113
138
|
}
|
|
@@ -122,7 +147,7 @@ export type MapViewRef = {
|
|
|
122
147
|
/**
|
|
123
148
|
* Обновить «текущую позицию» (GPS): snap / reroute / прибытие — на стороне WebView.
|
|
124
149
|
*/
|
|
125
|
-
setNavigatorPosition: (latitude: number, longitude: number) => void;
|
|
150
|
+
setNavigatorPosition: (latitude: number, longitude: number, accuracy?: number) => void;
|
|
126
151
|
/** Режим «клик по карте = новая позиция» (удобно в dev). */
|
|
127
152
|
pickNavigatorPosition: () => void;
|
|
128
153
|
};
|
|
@@ -169,6 +194,36 @@ const getBoundsFromCoords = (
|
|
|
169
194
|
];
|
|
170
195
|
};
|
|
171
196
|
|
|
197
|
+
const toWebMarkerParams = (props: MarkerProps): MapLiteMarkerParams => ({
|
|
198
|
+
uniqueId: props.uniqueId,
|
|
199
|
+
latitude: props.latitude,
|
|
200
|
+
longitude: props.longitude,
|
|
201
|
+
color: props.color,
|
|
202
|
+
iconUrl: props.iconUrl,
|
|
203
|
+
iconWidth: props.iconWidth,
|
|
204
|
+
iconHeight: props.iconHeight,
|
|
205
|
+
html: props.html,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const toWebPolylineParams = (props: PolylineProps): MapLitePolylineParams => ({
|
|
209
|
+
uniqueId: props.uniqueId,
|
|
210
|
+
coordinates: props.coordinates,
|
|
211
|
+
color: props.color,
|
|
212
|
+
width: props.width,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const toWebPolygonParams = (props: PolygonProps): MapLitePolygonParams => ({
|
|
216
|
+
uniqueId: props.uniqueId,
|
|
217
|
+
coordinates: props.coordinates,
|
|
218
|
+
fillColor: props.fillColor,
|
|
219
|
+
fillOpacity: props.fillOpacity,
|
|
220
|
+
strokeColor: props.strokeColor,
|
|
221
|
+
strokeOpacity: props.strokeOpacity,
|
|
222
|
+
strokeWidth: props.strokeWidth,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const toWebRemoveParams = (uniqueId: string): MapLiteRemoveOverlayParams => ({ uniqueId });
|
|
226
|
+
|
|
172
227
|
/** Минимальный интервал между отправками позиции в WebView (мост RN↔JS). */
|
|
173
228
|
const NAVIGATOR_GPS_FORWARD_MIN_MS = 500;
|
|
174
229
|
|
|
@@ -181,16 +236,56 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
181
236
|
initedRef.current = inited;
|
|
182
237
|
}, [inited]);
|
|
183
238
|
|
|
239
|
+
/** View-model HUD маршрута, приходит из WebView (`navigatorHud`). */
|
|
240
|
+
const [hudState, setHudState] = useState<NavigatorHudState | null>(null);
|
|
241
|
+
const voicePlayerRef = useRef<NavigatorVoicePlayerRef | null>(null);
|
|
242
|
+
|
|
243
|
+
/** Озвучка ограничена русским набором фраз (как в веб-навигаторе). */
|
|
244
|
+
const voiceSupported = props.navigator === true && props.navigatorLang !== 'en';
|
|
245
|
+
const voice = useNavigatorVoice(voiceSupported ? props.navigatorVoiceUrl : undefined);
|
|
246
|
+
|
|
247
|
+
const chromeTheme = useMemo(
|
|
248
|
+
() => resolveNavigatorChromeTheme(props.navigatorChrome),
|
|
249
|
+
[props.navigatorChrome]
|
|
250
|
+
);
|
|
251
|
+
const navigatorStrings = useMemo(
|
|
252
|
+
() => navigatorUiStrings(props.navigatorLang),
|
|
253
|
+
[props.navigatorLang]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
voicePlayerRef.current?.setVolume(volumeLevelToGain(voice.volumeLevel));
|
|
258
|
+
}, [voice.volumeLevel]);
|
|
259
|
+
|
|
260
|
+
const handleNavigatorVoice = (params: NavigatorVoicePlayParams) => {
|
|
261
|
+
const player = voicePlayerRef.current;
|
|
262
|
+
if (!player) return;
|
|
263
|
+
if (params.action === 'stop') {
|
|
264
|
+
player.stop();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (params.action === 'prefetch') return;
|
|
268
|
+
if (!voice.voiceEnabled || !voice.manifest || !voice.clipBaseUrl) return;
|
|
269
|
+
const urls = keysToClipUrls(voice.manifest, voice.clipBaseUrl, params.keys);
|
|
270
|
+
if (urls.length > 0) player.playUrls(urls);
|
|
271
|
+
};
|
|
272
|
+
|
|
184
273
|
const coordsInMapRef = useRef<Record<string, [number, number][]>>({});
|
|
185
274
|
const markersClickHandlers = useRef<Record<string, () => void>>({});
|
|
186
275
|
const mapSelectPointRef = useRef<MapSelectPointType | null>(null);
|
|
187
276
|
const performanceMode = props.performanceMode ?? (Platform.OS === 'android' ? 'balanced' : 'quality');
|
|
188
277
|
|
|
278
|
+
const webViewSource = props.developerLocalhostBundleUrl
|
|
279
|
+
? { uri: props.developerLocalhostBundleUrl }
|
|
280
|
+
: props.useNativeMapHtml
|
|
281
|
+
? { uri: getNativeMapHtmlUri() }
|
|
282
|
+
: { html: MAP_HTML };
|
|
283
|
+
|
|
189
284
|
const fallbackPixelRatio = performanceMode === 'performance'
|
|
190
285
|
? 0.85
|
|
191
286
|
: (performanceMode === 'balanced' && Platform.OS === 'android' ? 1 : undefined);
|
|
192
287
|
|
|
193
|
-
const sendToWebView = (message:
|
|
288
|
+
const sendToWebView = (message: NativeToWebCommand) => {
|
|
194
289
|
if (__DEV__) {
|
|
195
290
|
console.log('MapView: sendToWebView', message);
|
|
196
291
|
}
|
|
@@ -209,33 +304,31 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
209
304
|
const mapStyleText = await loadResources(props.mapStyle);
|
|
210
305
|
const mapStyle = JSON.parse(mapStyleText);
|
|
211
306
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
},
|
|
238
|
-
});
|
|
307
|
+
const initParams: MapLiteInitParams = {
|
|
308
|
+
mapStyle: mapStyle,
|
|
309
|
+
zoomEnabled: props.zoomEnabled ?? false,
|
|
310
|
+
scrollEnabled: props.scrollEnabled ?? false,
|
|
311
|
+
center: props.center,
|
|
312
|
+
zoom: props.zoom,
|
|
313
|
+
minZoom: props.minZoom,
|
|
314
|
+
maxZoom: props.maxZoom,
|
|
315
|
+
antialias: false,
|
|
316
|
+
crossSourceCollisions: performanceMode !== 'performance',
|
|
317
|
+
fadeDuration: performanceMode === 'performance' ? 0 : 120,
|
|
318
|
+
pixelRatio: props.pixelRatio ?? fallbackPixelRatio,
|
|
319
|
+
simplifyStyle: performanceMode !== 'quality',
|
|
320
|
+
aggressiveSimplifyStyle: performanceMode === 'performance',
|
|
321
|
+
maxPitch: performanceMode === 'performance' ? 0 : 45,
|
|
322
|
+
renderWorldCopies: performanceMode === 'quality',
|
|
323
|
+
turboWhileMoving: props.turboWhileMoving ?? (performanceMode === 'performance'),
|
|
324
|
+
debugMode: props.debugMode ?? false,
|
|
325
|
+
navigator: props.navigator === true,
|
|
326
|
+
graphhopperUrl: props.graphhopperUrl,
|
|
327
|
+
navigatorLang: props.navigatorLang,
|
|
328
|
+
navigatorProfile: props.navigatorProfile,
|
|
329
|
+
navigatorChrome: props.navigatorChrome,
|
|
330
|
+
};
|
|
331
|
+
sendToWebView({ function: 'init', params: initParams });
|
|
239
332
|
};
|
|
240
333
|
|
|
241
334
|
const updateMarkerClickHandler = (propsMarker: MarkerProps) => {
|
|
@@ -253,14 +346,14 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
253
346
|
|
|
254
347
|
updateMarkerClickHandler(propsMarker);
|
|
255
348
|
|
|
256
|
-
sendToWebView({ function: 'addMarker', params: propsMarker });
|
|
349
|
+
sendToWebView({ function: 'addMarker', params: toWebMarkerParams(propsMarker) });
|
|
257
350
|
scheduleAutoFitBounds();
|
|
258
351
|
};
|
|
259
352
|
|
|
260
353
|
const removeMarker = (propsMarker: MarkerProps) => {
|
|
261
354
|
delete coordsInMapRef.current[propsMarker.uniqueId];
|
|
262
355
|
delete markersClickHandlers.current[propsMarker.uniqueId];
|
|
263
|
-
sendToWebView({ function: 'removeMarker', params: propsMarker });
|
|
356
|
+
sendToWebView({ function: 'removeMarker', params: toWebRemoveParams(propsMarker.uniqueId) });
|
|
264
357
|
scheduleAutoFitBounds();
|
|
265
358
|
};
|
|
266
359
|
|
|
@@ -268,13 +361,13 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
268
361
|
if (propsPolyline.coordinates && !propsPolyline.ignoreFitBounds) {
|
|
269
362
|
coordsInMapRef.current[propsPolyline.uniqueId] = propsPolyline.coordinates;
|
|
270
363
|
}
|
|
271
|
-
sendToWebView({ function: 'addPolyline', params: propsPolyline });
|
|
364
|
+
sendToWebView({ function: 'addPolyline', params: toWebPolylineParams(propsPolyline) });
|
|
272
365
|
scheduleAutoFitBounds();
|
|
273
366
|
};
|
|
274
367
|
|
|
275
368
|
const removePolyline = (propsPolyline: PolylineProps) => {
|
|
276
369
|
delete coordsInMapRef.current[propsPolyline.uniqueId];
|
|
277
|
-
sendToWebView({ function: 'removePolyline', params: propsPolyline });
|
|
370
|
+
sendToWebView({ function: 'removePolyline', params: toWebRemoveParams(propsPolyline.uniqueId) });
|
|
278
371
|
scheduleAutoFitBounds();
|
|
279
372
|
};
|
|
280
373
|
|
|
@@ -282,13 +375,13 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
282
375
|
if (propsPolygon.coordinates && !propsPolygon.ignoreFitBounds) {
|
|
283
376
|
coordsInMapRef.current[propsPolygon.uniqueId] = propsPolygon.coordinates;
|
|
284
377
|
}
|
|
285
|
-
sendToWebView({ function: 'addPolygon', params: propsPolygon });
|
|
378
|
+
sendToWebView({ function: 'addPolygon', params: toWebPolygonParams(propsPolygon) });
|
|
286
379
|
scheduleAutoFitBounds();
|
|
287
380
|
};
|
|
288
381
|
|
|
289
382
|
const removePolygon = (propsPolygon: PolygonProps) => {
|
|
290
383
|
delete coordsInMapRef.current[propsPolygon.uniqueId];
|
|
291
|
-
sendToWebView({ function: 'removePolygon', params: propsPolygon });
|
|
384
|
+
sendToWebView({ function: 'removePolygon', params: toWebRemoveParams(propsPolygon.uniqueId) });
|
|
292
385
|
scheduleAutoFitBounds();
|
|
293
386
|
};
|
|
294
387
|
|
|
@@ -328,14 +421,22 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
328
421
|
sendToWebView({ function: 'advanceNavigatorInstruction', params: {} });
|
|
329
422
|
};
|
|
330
423
|
|
|
331
|
-
const setNavigatorPosition = (latitude: number, longitude: number) => {
|
|
332
|
-
|
|
424
|
+
const setNavigatorPosition = (latitude: number, longitude: number, accuracy?: number) => {
|
|
425
|
+
const params: MapLiteSetNavigatorPositionParams = { latitude, longitude };
|
|
426
|
+
if (typeof accuracy === 'number' && Number.isFinite(accuracy) && accuracy >= 0) {
|
|
427
|
+
params.accuracy = accuracy;
|
|
428
|
+
}
|
|
429
|
+
sendToWebView({ function: 'setNavigatorPosition', params });
|
|
333
430
|
};
|
|
334
431
|
|
|
335
432
|
const pickNavigatorPosition = () => {
|
|
336
433
|
sendToWebView({ function: 'pickNavigatorPosition', params: {} });
|
|
337
434
|
};
|
|
338
435
|
|
|
436
|
+
const recenterNavigatorCamera = () => {
|
|
437
|
+
sendToWebView({ function: 'recenterNavigatorCamera', params: {} });
|
|
438
|
+
};
|
|
439
|
+
|
|
339
440
|
useImperativeHandle(ref, () => ({
|
|
340
441
|
fitBounds,
|
|
341
442
|
flyTo,
|
|
@@ -350,7 +451,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
350
451
|
const lastPropsRef = useRef<MapViewProps>(props);
|
|
351
452
|
|
|
352
453
|
useEffect(() => {
|
|
353
|
-
const updateProps:
|
|
454
|
+
const updateProps: MapLiteUpdateParams = {};
|
|
354
455
|
|
|
355
456
|
if (lastPropsRef.current.minZoom !== props.minZoom) {
|
|
356
457
|
updateProps.minZoom = props.minZoom;
|
|
@@ -369,7 +470,9 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
369
470
|
}
|
|
370
471
|
|
|
371
472
|
if (lastPropsRef.current.mapStyle !== props.mapStyle) {
|
|
372
|
-
|
|
473
|
+
void loadResources(props.mapStyle).then((mapStyleText) => {
|
|
474
|
+
sendToWebView({ function: 'update', params: { mapStyle: JSON.parse(mapStyleText) } });
|
|
475
|
+
});
|
|
373
476
|
}
|
|
374
477
|
|
|
375
478
|
lastPropsRef.current = props;
|
|
@@ -382,7 +485,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
382
485
|
|
|
383
486
|
const onReceiveMessageFromWebView = (data: string) => {
|
|
384
487
|
try {
|
|
385
|
-
const msg = JSON.parse(data);
|
|
488
|
+
const msg = JSON.parse(data) as WebToNativeMessage;
|
|
386
489
|
|
|
387
490
|
if (__DEV__) {
|
|
388
491
|
console.log('MapView: event', msg);
|
|
@@ -392,20 +495,23 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
392
495
|
switch (msg.event) {
|
|
393
496
|
case 'movestart':
|
|
394
497
|
mapSelectPointRef.current?.up();
|
|
395
|
-
props.onMoveStart?.(msg.params);
|
|
498
|
+
props.onMoveStart?.(msg.params as EventParams);
|
|
396
499
|
break;
|
|
397
500
|
case 'moveend':
|
|
398
|
-
props.onMoveEnd?.(msg.params);
|
|
501
|
+
props.onMoveEnd?.(msg.params as EventParams);
|
|
399
502
|
mapSelectPointRef.current?.down();
|
|
400
503
|
break;
|
|
401
504
|
case 'zoomstart':
|
|
402
|
-
props.onZoomStart?.(msg.params);
|
|
505
|
+
props.onZoomStart?.(msg.params as EventParams);
|
|
403
506
|
break;
|
|
404
507
|
case 'zoomend':
|
|
405
|
-
props.onZoomEnd?.(msg.params);
|
|
508
|
+
props.onZoomEnd?.(msg.params as EventParams);
|
|
406
509
|
break;
|
|
407
510
|
case 'idle':
|
|
408
|
-
props.onIdle?.(msg.params);
|
|
511
|
+
props.onIdle?.(msg.params as EventParams);
|
|
512
|
+
break;
|
|
513
|
+
case 'error':
|
|
514
|
+
props.onMapError?.(msg.params as MapLiteMapErrorEventParams);
|
|
409
515
|
break;
|
|
410
516
|
case 'navigatorRouteSet':
|
|
411
517
|
props.onNavigatorRouteSet?.(msg.params as NavigatorRouteSetParams);
|
|
@@ -416,12 +522,18 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
416
522
|
case 'navigatorPositionSet':
|
|
417
523
|
props.onNavigatorPositionSet?.(msg.params as NavigatorPositionSetParams);
|
|
418
524
|
break;
|
|
525
|
+
case 'navigatorHud':
|
|
526
|
+
setHudState(msg.params as NavigatorHudState);
|
|
527
|
+
break;
|
|
528
|
+
case 'navigatorVoice':
|
|
529
|
+
handleNavigatorVoice(msg.params as NavigatorVoicePlayParams);
|
|
530
|
+
break;
|
|
419
531
|
}
|
|
420
532
|
return;
|
|
421
533
|
}
|
|
422
534
|
|
|
423
535
|
if (msg.type === 'error' && msg.data) {
|
|
424
|
-
props.onMapLiteError?.(msg.data
|
|
536
|
+
props.onMapLiteError?.(msg.data);
|
|
425
537
|
return;
|
|
426
538
|
}
|
|
427
539
|
|
|
@@ -465,6 +577,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
465
577
|
|
|
466
578
|
const lat = position.coords.latitude;
|
|
467
579
|
const lng = position.coords.longitude;
|
|
580
|
+
const accuracy = position.coords.accuracy;
|
|
468
581
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
469
582
|
return;
|
|
470
583
|
}
|
|
@@ -475,9 +588,13 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
475
588
|
}
|
|
476
589
|
lastForwardedAt = now;
|
|
477
590
|
|
|
591
|
+
const params: MapLiteSetNavigatorPositionParams = { latitude: lat, longitude: lng };
|
|
592
|
+
if (typeof accuracy === 'number' && Number.isFinite(accuracy) && accuracy >= 0) {
|
|
593
|
+
params.accuracy = accuracy;
|
|
594
|
+
}
|
|
478
595
|
sendToWebViewRef.current({
|
|
479
596
|
function: 'setNavigatorPosition',
|
|
480
|
-
params
|
|
597
|
+
params,
|
|
481
598
|
});
|
|
482
599
|
},
|
|
483
600
|
(error) => {
|
|
@@ -518,7 +635,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
518
635
|
ref={webViewRef}
|
|
519
636
|
style={{ flex: 1, backgroundColor: 'transparent' }}
|
|
520
637
|
originWhitelist={['*']}
|
|
521
|
-
source={
|
|
638
|
+
source={webViewSource}
|
|
522
639
|
onMessage={event => {
|
|
523
640
|
onReceiveMessageFromWebView(event.nativeEvent.data);
|
|
524
641
|
}}
|
|
@@ -549,6 +666,40 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
|
|
|
549
666
|
/>
|
|
550
667
|
</View>
|
|
551
668
|
)}
|
|
669
|
+
|
|
670
|
+
{inited && props.navigator && (
|
|
671
|
+
<>
|
|
672
|
+
<NavigatorHud
|
|
673
|
+
state={hudState}
|
|
674
|
+
theme={chromeTheme}
|
|
675
|
+
pickHint={navigatorStrings.pickHint}
|
|
676
|
+
/>
|
|
677
|
+
<NavigatorRecenterButton
|
|
678
|
+
theme={chromeTheme}
|
|
679
|
+
accessibilityLabel={navigatorStrings.recenterAria}
|
|
680
|
+
onPress={recenterNavigatorCamera}
|
|
681
|
+
/>
|
|
682
|
+
{voiceSupported && (
|
|
683
|
+
<NavigatorVoiceControl
|
|
684
|
+
theme={chromeTheme}
|
|
685
|
+
strings={navigatorStrings}
|
|
686
|
+
catalog={voice.catalog}
|
|
687
|
+
selectedDir={voice.selectedDir}
|
|
688
|
+
voiceEnabled={voice.voiceEnabled}
|
|
689
|
+
volumeLevel={voice.volumeLevel}
|
|
690
|
+
onSelectVoice={voice.selectVoice}
|
|
691
|
+
onSelectVolume={voice.selectVolume}
|
|
692
|
+
onDisable={voice.disableVoice}
|
|
693
|
+
/>
|
|
694
|
+
)}
|
|
695
|
+
{voiceSupported && (
|
|
696
|
+
<NavigatorVoicePlayer
|
|
697
|
+
ref={voicePlayerRef}
|
|
698
|
+
initialVolume={volumeLevelToGain(voice.volumeLevel)}
|
|
699
|
+
/>
|
|
700
|
+
)}
|
|
701
|
+
</>
|
|
702
|
+
)}
|
|
552
703
|
</MapViewContext.Provider>
|
|
553
704
|
{!inited && (
|
|
554
705
|
<MapPlaceholder theme={props.placeholderTheme ?? 'light'} />
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Text, View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { NavigatorManeuverIcon } from './navigatorManeuverIcon';
|
|
4
|
+
import type { NavigatorChromeTheme } from './navigatorChromeTheme';
|
|
5
|
+
import type { NavigatorHudState } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Нативная панель навигатора (HUD): порт DOM-плашки из веб-навигатора.
|
|
9
|
+
* Получает уже отформатированную view-model (`navigatorHud`) и тему из
|
|
10
|
+
* `navigatorChrome`; вся локализация/форматирование остаются в вебе.
|
|
11
|
+
*/
|
|
12
|
+
export function NavigatorHud({
|
|
13
|
+
state,
|
|
14
|
+
theme,
|
|
15
|
+
pickHint,
|
|
16
|
+
}: {
|
|
17
|
+
state: NavigatorHudState | null;
|
|
18
|
+
theme: NavigatorChromeTheme;
|
|
19
|
+
pickHint: string;
|
|
20
|
+
}) {
|
|
21
|
+
if (!state || !state.visible) return null;
|
|
22
|
+
|
|
23
|
+
if (state.pick) {
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
pointerEvents="none"
|
|
27
|
+
style={{
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
top: 12,
|
|
30
|
+
left: 12,
|
|
31
|
+
right: 12,
|
|
32
|
+
padding: 14,
|
|
33
|
+
borderRadius: 14,
|
|
34
|
+
backgroundColor: 'rgba(120, 53, 15, 0.92)',
|
|
35
|
+
borderWidth: 2,
|
|
36
|
+
borderColor: '#facc15',
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Text
|
|
40
|
+
style={{
|
|
41
|
+
textAlign: 'center',
|
|
42
|
+
fontSize: 14,
|
|
43
|
+
fontWeight: '600',
|
|
44
|
+
color: '#fef3c7',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{pickHint}
|
|
48
|
+
</Text>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const speedLimit = state.speedLimitKmh;
|
|
54
|
+
const showSpeed = typeof speedLimit === 'number' && Number.isFinite(speedLimit) && speedLimit > 0;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View
|
|
58
|
+
pointerEvents="none"
|
|
59
|
+
style={{
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
top: 12,
|
|
62
|
+
left: 12,
|
|
63
|
+
right: 12,
|
|
64
|
+
flexDirection: 'row',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
gap: 12,
|
|
67
|
+
padding: 14,
|
|
68
|
+
borderRadius: 14,
|
|
69
|
+
backgroundColor: theme.background,
|
|
70
|
+
shadowColor: '#0f172a',
|
|
71
|
+
shadowOpacity: 0.32,
|
|
72
|
+
shadowRadius: 24,
|
|
73
|
+
shadowOffset: { width: 0, height: 10 },
|
|
74
|
+
elevation: 6,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
width: 44,
|
|
80
|
+
height: 44,
|
|
81
|
+
borderRadius: 12,
|
|
82
|
+
backgroundColor: theme.iconBackground,
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{typeof state.sign === 'number' ? (
|
|
88
|
+
<NavigatorManeuverIcon sign={state.sign} color={theme.iconForeground} />
|
|
89
|
+
) : null}
|
|
90
|
+
</View>
|
|
91
|
+
|
|
92
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
93
|
+
{state.distanceLabel ? (
|
|
94
|
+
<Text
|
|
95
|
+
numberOfLines={1}
|
|
96
|
+
style={{
|
|
97
|
+
fontSize: 18,
|
|
98
|
+
fontWeight: '700',
|
|
99
|
+
lineHeight: 20,
|
|
100
|
+
color: theme.foreground,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{state.distanceLabel}
|
|
104
|
+
</Text>
|
|
105
|
+
) : null}
|
|
106
|
+
|
|
107
|
+
{state.maneuverText ? (
|
|
108
|
+
<Text
|
|
109
|
+
numberOfLines={1}
|
|
110
|
+
style={{ fontSize: 14, fontWeight: '500', color: theme.subtle }}
|
|
111
|
+
>
|
|
112
|
+
{state.maneuverText}
|
|
113
|
+
</Text>
|
|
114
|
+
) : null}
|
|
115
|
+
|
|
116
|
+
{state.streetName ? (
|
|
117
|
+
<Text numberOfLines={1} style={{ fontSize: 12, color: theme.muted }}>
|
|
118
|
+
{state.streetName}
|
|
119
|
+
</Text>
|
|
120
|
+
) : null}
|
|
121
|
+
|
|
122
|
+
{state.summaryText ? (
|
|
123
|
+
<Text
|
|
124
|
+
style={{
|
|
125
|
+
marginTop: 4,
|
|
126
|
+
paddingTop: 6,
|
|
127
|
+
borderTopWidth: 1,
|
|
128
|
+
borderTopColor: theme.divider,
|
|
129
|
+
fontSize: 12,
|
|
130
|
+
fontWeight: '600',
|
|
131
|
+
color: theme.summary,
|
|
132
|
+
lineHeight: 16,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{state.summaryText}
|
|
136
|
+
</Text>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
139
|
+
{state.extrasText ? (
|
|
140
|
+
<Text style={{ marginTop: 2, fontSize: 11, fontWeight: '500', color: theme.muted }}>
|
|
141
|
+
{state.extrasText}
|
|
142
|
+
</Text>
|
|
143
|
+
) : null}
|
|
144
|
+
</View>
|
|
145
|
+
|
|
146
|
+
{showSpeed ? (
|
|
147
|
+
<View
|
|
148
|
+
style={{
|
|
149
|
+
width: 52,
|
|
150
|
+
height: 52,
|
|
151
|
+
borderRadius: 26,
|
|
152
|
+
borderWidth: 3,
|
|
153
|
+
borderColor: '#dc2626',
|
|
154
|
+
backgroundColor: '#ffffff',
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'center',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<Text style={{ fontSize: 14, fontWeight: '800', color: '#0f172a' }}>
|
|
160
|
+
{Math.round(speedLimit as number)}
|
|
161
|
+
</Text>
|
|
162
|
+
</View>
|
|
163
|
+
) : null}
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
}
|