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.
Files changed (78) hide show
  1. package/README.md +92 -6
  2. package/lib/module/components/MapView.js +167 -48
  3. package/lib/module/components/MapView.js.map +1 -1
  4. package/lib/module/components/NavigatorHud.js +152 -0
  5. package/lib/module/components/NavigatorHud.js.map +1 -0
  6. package/lib/module/components/NavigatorRecenterButton.js +48 -0
  7. package/lib/module/components/NavigatorRecenterButton.js.map +1 -0
  8. package/lib/module/components/NavigatorVoiceControl.js +173 -0
  9. package/lib/module/components/NavigatorVoiceControl.js.map +1 -0
  10. package/lib/module/components/navigatorChromeTheme.js +98 -0
  11. package/lib/module/components/navigatorChromeTheme.js.map +1 -0
  12. package/lib/module/components/navigatorManeuverIcon.js +210 -0
  13. package/lib/module/components/navigatorManeuverIcon.js.map +1 -0
  14. package/lib/module/components/navigatorVoiceCatalog.js +225 -0
  15. package/lib/module/components/navigatorVoiceCatalog.js.map +1 -0
  16. package/lib/module/components/navigatorVoiceKeys.js +14 -0
  17. package/lib/module/components/navigatorVoiceKeys.js.map +1 -0
  18. package/lib/module/components/navigatorVoicePlayer.js +100 -0
  19. package/lib/module/components/navigatorVoicePlayer.js.map +1 -0
  20. package/lib/module/components/navigatorVoiceStrings.js +31 -0
  21. package/lib/module/components/navigatorVoiceStrings.js.map +1 -0
  22. package/lib/module/components/types.js +47 -0
  23. package/lib/module/components/types.js.map +1 -1
  24. package/lib/module/components/useNavigatorVoice.js +78 -0
  25. package/lib/module/components/useNavigatorVoice.js.map +1 -0
  26. package/lib/module/components/utils.js +26 -0
  27. package/lib/module/components/utils.js.map +1 -1
  28. package/lib/module/components/webMapBuild.js +1 -1
  29. package/lib/module/components/webMapBuild.js.map +1 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/typescript/src/components/MapView.d.ts +15 -2
  32. package/lib/typescript/src/components/MapView.d.ts.map +1 -1
  33. package/lib/typescript/src/components/NavigatorHud.d.ts +13 -0
  34. package/lib/typescript/src/components/NavigatorHud.d.ts.map +1 -0
  35. package/lib/typescript/src/components/NavigatorRecenterButton.d.ts +11 -0
  36. package/lib/typescript/src/components/NavigatorRecenterButton.d.ts.map +1 -0
  37. package/lib/typescript/src/components/NavigatorVoiceControl.d.ts +20 -0
  38. package/lib/typescript/src/components/NavigatorVoiceControl.d.ts.map +1 -0
  39. package/lib/typescript/src/components/navigatorChromeTheme.d.ts +19 -0
  40. package/lib/typescript/src/components/navigatorChromeTheme.d.ts.map +1 -0
  41. package/lib/typescript/src/components/navigatorManeuverIcon.d.ts +20 -0
  42. package/lib/typescript/src/components/navigatorManeuverIcon.d.ts.map +1 -0
  43. package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts +50 -0
  44. package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts.map +1 -0
  45. package/lib/typescript/src/components/navigatorVoiceKeys.d.ts +10 -0
  46. package/lib/typescript/src/components/navigatorVoiceKeys.d.ts.map +1 -0
  47. package/lib/typescript/src/components/navigatorVoicePlayer.d.ts +15 -0
  48. package/lib/typescript/src/components/navigatorVoicePlayer.d.ts.map +1 -0
  49. package/lib/typescript/src/components/navigatorVoiceStrings.d.ts +19 -0
  50. package/lib/typescript/src/components/navigatorVoiceStrings.d.ts.map +1 -0
  51. package/lib/typescript/src/components/types.d.ts +205 -12
  52. package/lib/typescript/src/components/types.d.ts.map +1 -1
  53. package/lib/typescript/src/components/useNavigatorVoice.d.ts +20 -0
  54. package/lib/typescript/src/components/useNavigatorVoice.d.ts.map +1 -0
  55. package/lib/typescript/src/components/utils.d.ts +9 -0
  56. package/lib/typescript/src/components/utils.d.ts.map +1 -1
  57. package/lib/typescript/src/components/webMapBuild.d.ts +1 -1
  58. package/lib/typescript/src/components/webMapBuild.d.ts.map +1 -1
  59. package/lib/typescript/src/index.d.ts +1 -1
  60. package/lib/typescript/src/index.d.ts.map +1 -1
  61. package/package.json +16 -7
  62. package/resources/README.md +62 -0
  63. package/resources/map.html +797 -0
  64. package/src/components/MapView.tsx +209 -58
  65. package/src/components/NavigatorHud.tsx +166 -0
  66. package/src/components/NavigatorRecenterButton.tsx +45 -0
  67. package/src/components/NavigatorVoiceControl.tsx +198 -0
  68. package/src/components/navigatorChromeTheme.ts +118 -0
  69. package/src/components/navigatorManeuverIcon.tsx +177 -0
  70. package/src/components/navigatorVoiceCatalog.ts +275 -0
  71. package/src/components/navigatorVoiceKeys.ts +132 -0
  72. package/src/components/navigatorVoicePlayer.tsx +126 -0
  73. package/src/components/navigatorVoiceStrings.ts +42 -0
  74. package/src/components/types.ts +198 -16
  75. package/src/components/useNavigatorVoice.ts +96 -0
  76. package/src/components/utils.ts +28 -0
  77. package/src/components/webMapBuild.ts +1 -1
  78. 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: { function: string; params: any }) => {
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
- sendToWebView({
213
- function: 'init',
214
- params: {
215
- mapStyle: mapStyle,
216
- zoomEnabled: props.zoomEnabled ?? false,
217
- scrollEnabled: props.scrollEnabled ?? false,
218
- center: props.center,
219
- zoom: props.zoom,
220
- minZoom: props.minZoom,
221
- maxZoom: props.maxZoom,
222
- antialias: false,
223
- crossSourceCollisions: performanceMode !== 'performance',
224
- fadeDuration: performanceMode === 'performance' ? 0 : 120,
225
- pixelRatio: props.pixelRatio ?? fallbackPixelRatio,
226
- simplifyStyle: performanceMode !== 'quality',
227
- aggressiveSimplifyStyle: performanceMode === 'performance',
228
- maxPitch: performanceMode === 'performance' ? 0 : 45,
229
- renderWorldCopies: performanceMode === 'quality',
230
- turboWhileMoving: props.turboWhileMoving ?? (performanceMode === 'performance'),
231
- debugMode: props.debugMode ?? false,
232
- navigator: props.navigator === true,
233
- graphhopperUrl: props.graphhopperUrl,
234
- navigatorLang: props.navigatorLang,
235
- navigatorProfile: props.navigatorProfile,
236
- navigatorChrome: props.navigatorChrome,
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
- sendToWebView({ function: 'setNavigatorPosition', params: { latitude, longitude } });
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: 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
- updateProps.mapStyle = props.mapStyle;
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 as MapLiteWebError);
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: { latitude: lat, longitude: lng },
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={!!props.developerLocalhostBundleUrl ? { uri: props.developerLocalhostBundleUrl } : { html: MAP_HTML }}
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
+ }