react-native-maplibre-lite 0.2.5 → 0.2.6

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 (51) hide show
  1. package/README.md +32 -12
  2. package/lib/module/components/MapView.js +19 -22
  3. package/lib/module/components/MapView.js.map +1 -1
  4. package/lib/module/components/NavigatorVoiceControl.js +9 -10
  5. package/lib/module/components/NavigatorVoiceControl.js.map +1 -1
  6. package/lib/module/components/navigatorVoicePlayer.js +2 -4
  7. package/lib/module/components/navigatorVoicePlayer.js.map +1 -1
  8. package/lib/module/components/types.js +6 -2
  9. package/lib/module/components/types.js.map +1 -1
  10. package/lib/module/components/useNavigatorTts.js +154 -0
  11. package/lib/module/components/useNavigatorTts.js.map +1 -0
  12. package/lib/module/components/webMapBuild.js +1 -1
  13. package/lib/module/components/webMapBuild.js.map +1 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/typescript/src/components/MapView.d.ts +8 -5
  16. package/lib/typescript/src/components/MapView.d.ts.map +1 -1
  17. package/lib/typescript/src/components/NavigatorVoiceControl.d.ts +6 -7
  18. package/lib/typescript/src/components/NavigatorVoiceControl.d.ts.map +1 -1
  19. package/lib/typescript/src/components/navigatorVoicePlayer.d.ts.map +1 -1
  20. package/lib/typescript/src/components/types.d.ts +8 -4
  21. package/lib/typescript/src/components/types.d.ts.map +1 -1
  22. package/lib/typescript/src/components/useNavigatorTts.d.ts +28 -0
  23. package/lib/typescript/src/components/useNavigatorTts.d.ts.map +1 -0
  24. package/lib/typescript/src/components/webMapBuild.d.ts +1 -1
  25. package/lib/typescript/src/components/webMapBuild.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts +1 -1
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/resources/map.html +42 -43
  30. package/src/components/MapView.tsx +34 -25
  31. package/src/components/NavigatorVoiceControl.tsx +13 -14
  32. package/src/components/navigatorVoicePlayer.tsx +2 -4
  33. package/src/components/types.ts +13 -4
  34. package/src/components/useNavigatorTts.ts +195 -0
  35. package/src/components/webMapBuild.ts +1 -1
  36. package/src/index.tsx +2 -0
  37. package/lib/module/components/navigatorVoiceCatalog.js +0 -261
  38. package/lib/module/components/navigatorVoiceCatalog.js.map +0 -1
  39. package/lib/module/components/navigatorVoiceKeys.js +0 -14
  40. package/lib/module/components/navigatorVoiceKeys.js.map +0 -1
  41. package/lib/module/components/useNavigatorVoice.js +0 -78
  42. package/lib/module/components/useNavigatorVoice.js.map +0 -1
  43. package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts +0 -50
  44. package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts.map +0 -1
  45. package/lib/typescript/src/components/navigatorVoiceKeys.d.ts +0 -10
  46. package/lib/typescript/src/components/navigatorVoiceKeys.d.ts.map +0 -1
  47. package/lib/typescript/src/components/useNavigatorVoice.d.ts +0 -20
  48. package/lib/typescript/src/components/useNavigatorVoice.d.ts.map +0 -1
  49. package/src/components/navigatorVoiceCatalog.ts +0 -316
  50. package/src/components/navigatorVoiceKeys.ts +0 -132
  51. package/src/components/useNavigatorVoice.ts +0 -96
@@ -26,10 +26,9 @@ import { NavigatorHud } from './NavigatorHud';
26
26
  import { NavigatorRecenterButton } from './NavigatorRecenterButton';
27
27
  import { NavigatorVoiceControl } from './NavigatorVoiceControl';
28
28
  import { resolveNavigatorChromeTheme } from './navigatorChromeTheme';
29
- import { keysToClipUrls, volumeLevelToGain } from './navigatorVoiceCatalog';
30
29
  import { NavigatorVoicePlayer, type NavigatorVoicePlayerRef } from './navigatorVoicePlayer';
31
30
  import { navigatorUiStrings } from './navigatorVoiceStrings';
32
- import { useNavigatorVoice } from './useNavigatorVoice';
31
+ import { useNavigatorTts, volumeLevelToGain } from './useNavigatorTts';
33
32
  import {
34
33
  type EventParams,
35
34
  type MapLiteInitParams,
@@ -50,6 +49,8 @@ import {
50
49
  type NavigatorProfile,
51
50
  type NavigatorPositionSetParams,
52
51
  type NavigatorRouteSetParams,
52
+ type NavigatorTtsHandler,
53
+ type NavigatorTtsVoices,
53
54
  type NavigatorVoicePlayParams,
54
55
  type PolygonProps,
55
56
  type PolylineProps,
@@ -117,11 +118,14 @@ interface MapViewProps {
117
118
  */
118
119
  navigatorChrome?: NavigatorChromeParams;
119
120
  /**
120
- * URL каталога голосов навигатора (`.../voices/data.json`). Каталог,
121
- * манифест, выбор голоса/громкости и воспроизведение на нативной
122
- * стороне. Без него озвучка и FAB выбора голоса отключены.
121
+ * Голоса TTS: ключ подпись в UI. Вместе с `ttsHandler` включает
122
+ * озвучку и FAB выбора голоса.
123
123
  */
124
- navigatorVoiceUrl?: string;
124
+ ttsVoices?: NavigatorTtsVoices;
125
+ /**
126
+ * Синтез фразы: текст + ключ голоса → URL аудио для воспроизведения.
127
+ */
128
+ ttsHandler?: NavigatorTtsHandler;
125
129
  /**
126
130
  * Dev-only: автоматически вести маркер по построенному маршруту
127
131
  * (симуляция поездки для отладки). Требует `navigator: true`. Пока
@@ -249,9 +253,14 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
249
253
  const [hudState, setHudState] = useState<NavigatorHudState | null>(null);
250
254
  const voicePlayerRef = useRef<NavigatorVoicePlayerRef | null>(null);
251
255
 
252
- /** Озвучка ограничена русским набором фраз (как в веб-навигаторе). */
253
- const voiceSupported = props.navigator === true && props.navigatorLang !== 'en';
254
- const voice = useNavigatorVoice(voiceSupported ? props.navigatorVoiceUrl : undefined);
256
+ const ttsEnabled =
257
+ props.navigator === true &&
258
+ props.ttsHandler != null &&
259
+ Object.keys(props.ttsVoices ?? {}).length > 0;
260
+ const tts = useNavigatorTts(
261
+ ttsEnabled ? props.ttsVoices : undefined,
262
+ ttsEnabled ? props.ttsHandler : undefined
263
+ );
255
264
 
256
265
  const chromeTheme = useMemo(
257
266
  () => resolveNavigatorChromeTheme(props.navigatorChrome),
@@ -263,8 +272,8 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
263
272
  );
264
273
 
265
274
  useEffect(() => {
266
- voicePlayerRef.current?.setVolume(volumeLevelToGain(voice.volumeLevel));
267
- }, [voice.volumeLevel]);
275
+ voicePlayerRef.current?.setVolume(volumeLevelToGain(tts.volumeLevel));
276
+ }, [tts.volumeLevel]);
268
277
 
269
278
  const handleNavigatorVoice = (params: NavigatorVoicePlayParams) => {
270
279
  const player = voicePlayerRef.current;
@@ -273,10 +282,10 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
273
282
  player.stop();
274
283
  return;
275
284
  }
276
- if (params.action === 'prefetch') return;
277
- if (!voice.voiceEnabled || !voice.manifest || !voice.clipBaseUrl) return;
278
- const urls = keysToClipUrls(voice.manifest, voice.clipBaseUrl, params.keys);
279
- if (urls.length > 0) player.playUrls(urls);
285
+ if (params.action !== 'play' || !params.text?.trim()) return;
286
+ void tts.synthesize(params.text).then((url) => {
287
+ if (url) player.playUrls([url]);
288
+ });
280
289
  };
281
290
 
282
291
  const coordsInMapRef = useRef<Record<string, [number, number][]>>({});
@@ -703,23 +712,23 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
703
712
  accessibilityLabel={navigatorStrings.recenterAria}
704
713
  onPress={recenterNavigatorCamera}
705
714
  />
706
- {voiceSupported && (
715
+ {ttsEnabled && (
707
716
  <NavigatorVoiceControl
708
717
  theme={chromeTheme}
709
718
  strings={navigatorStrings}
710
- catalog={voice.catalog}
711
- selectedDir={voice.selectedDir}
712
- voiceEnabled={voice.voiceEnabled}
713
- volumeLevel={voice.volumeLevel}
714
- onSelectVoice={voice.selectVoice}
715
- onSelectVolume={voice.selectVolume}
716
- onDisable={voice.disableVoice}
719
+ voiceEntries={tts.voiceEntries}
720
+ selectedVoiceKey={tts.selectedVoiceKey}
721
+ voiceEnabled={tts.voiceEnabled}
722
+ volumeLevel={tts.volumeLevel}
723
+ onSelectVoice={tts.selectVoice}
724
+ onSelectVolume={tts.selectVolume}
725
+ onDisable={tts.disableVoice}
717
726
  />
718
727
  )}
719
- {voiceSupported && (
728
+ {ttsEnabled && (
720
729
  <NavigatorVoicePlayer
721
730
  ref={voicePlayerRef}
722
- initialVolume={volumeLevelToGain(voice.volumeLevel)}
731
+ initialVolume={volumeLevelToGain(tts.volumeLevel)}
723
732
  />
724
733
  )}
725
734
  </>
@@ -3,19 +3,18 @@ import { Pressable, ScrollView, Text, View } from 'react-native';
3
3
 
4
4
  import { NavigatorVoiceIcon } from './navigatorManeuverIcon';
5
5
  import type { NavigatorChromeTheme } from './navigatorChromeTheme';
6
- import type { VoiceCatalogEntry, VoiceVolumeLevel } from './navigatorVoiceCatalog';
7
6
  import type { NavigatorUiStrings } from './navigatorVoiceStrings';
7
+ import type { VoiceVolumeLevel } from './useNavigatorTts';
8
8
 
9
9
  /**
10
10
  * FAB выбора голоса + меню (список голосов, громкость 1–5, «отключить»).
11
- * Порт DOM-меню из веб-навигатора. Каталог/манифест и персист — в
12
- * `useNavigatorVoice`; компонент только презентационный.
11
+ * Порт DOM-меню из веб-навигатора. Персист и TTS — в `useNavigatorTts`.
13
12
  */
14
13
  export function NavigatorVoiceControl({
15
14
  theme,
16
15
  strings,
17
- catalog,
18
- selectedDir,
16
+ voiceEntries,
17
+ selectedVoiceKey,
19
18
  voiceEnabled,
20
19
  volumeLevel,
21
20
  onSelectVoice,
@@ -24,17 +23,17 @@ export function NavigatorVoiceControl({
24
23
  }: {
25
24
  theme: NavigatorChromeTheme;
26
25
  strings: NavigatorUiStrings;
27
- catalog: VoiceCatalogEntry[];
28
- selectedDir: string | null;
26
+ voiceEntries: [string, string][];
27
+ selectedVoiceKey: string | null;
29
28
  voiceEnabled: boolean;
30
29
  volumeLevel: VoiceVolumeLevel;
31
- onSelectVoice: (dir: string) => void;
30
+ onSelectVoice: (voiceKey: string) => void;
32
31
  onSelectVolume: (level: VoiceVolumeLevel) => void;
33
32
  onDisable: () => void;
34
33
  }) {
35
34
  const [open, setOpen] = useState(false);
36
35
 
37
- if (catalog.length === 0) return null;
36
+ if (voiceEntries.length === 0) return null;
38
37
 
39
38
  return (
40
39
  <>
@@ -84,15 +83,15 @@ export function NavigatorVoiceControl({
84
83
  }}
85
84
  >
86
85
  <ScrollView style={{ maxHeight: 280 }}>
87
- {catalog.map((entry) => {
88
- const active = voiceEnabled && entry.dir === selectedDir;
86
+ {voiceEntries.map(([voiceKey, label]) => {
87
+ const active = voiceEnabled && voiceKey === selectedVoiceKey;
89
88
  return (
90
89
  <Pressable
91
- key={entry.dir}
90
+ key={voiceKey}
92
91
  accessibilityRole="menuitem"
93
92
  onPress={() => {
94
93
  setOpen(false);
95
- onSelectVoice(entry.dir);
94
+ onSelectVoice(voiceKey);
96
95
  }}
97
96
  style={{ paddingVertical: 10, paddingHorizontal: 16 }}
98
97
  >
@@ -103,7 +102,7 @@ export function NavigatorVoiceControl({
103
102
  fontWeight: active ? '600' : '400',
104
103
  }}
105
104
  >
106
- {entry.name}
105
+ {label}
107
106
  </Text>
108
107
  </Pressable>
109
108
  );
@@ -9,10 +9,8 @@ import {
9
9
  import Video, { type VideoRef } from 'react-native-video';
10
10
 
11
11
  /**
12
- * Последовательный проигрыватель клипов озвучки навигатора. Порт логики
13
- * очереди из `webProject/src/map/navigationVoicePlayer.ts` на `react-native-video`:
14
- * веб присылает список ключей фраз, нативная часть собирает из них URL и
15
- * проигрывает их по очереди с короткой паузой между клипами.
12
+ * Проигрыватель озвучки навигатора на `react-native-video`.
13
+ * Нативная часть передаёт URL аудио (из `ttsHandler`) и проигрывает очередь.
16
14
  *
17
15
  * Один экземпляр `<Video>` на всю сессию — между клипами меняется только source
18
16
  * (или seek(0) при повторе того же URL), без remount.
@@ -154,17 +154,26 @@ export type NavigatorHudState = {
154
154
  }
155
155
 
156
156
  /** Действие озвучки в событии `navigatorVoice`. */
157
- export type NavigatorVoiceAction = 'play' | 'prefetch' | 'stop'
157
+ export type NavigatorVoiceAction = 'play' | 'stop'
158
158
 
159
159
  /**
160
- * Web → RN: `navigatorVoice` event. Веб шлёт абстрактные ключи фраз;
161
- * нативная часть мапит их в URL клипов и воспроизводит.
160
+ * Web → RN: `navigatorVoice` event. Веб собирает полную фразу и шлёт текст;
161
+ * нативная часть вызывает `ttsHandler` и воспроизводит URL аудио.
162
162
  */
163
163
  export type NavigatorVoicePlayParams = {
164
164
  action: NavigatorVoiceAction,
165
- keys: string[],
165
+ text?: string,
166
166
  }
167
167
 
168
+ /** Голоса TTS: ключ → подпись в UI выбора голоса. */
169
+ export type NavigatorTtsVoices = Record<string, string>
170
+
171
+ /** Колбэк TTS: текст фразы + ключ голоса → URL аудио для воспроизведения. */
172
+ export type NavigatorTtsHandler = (
173
+ text: string,
174
+ voiceKey: string
175
+ ) => Promise<string>
176
+
168
177
  export type MapLiteWebError = {
169
178
  target: string,
170
179
  message: string,
@@ -0,0 +1,195 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import AsyncStorage from '@react-native-async-storage/async-storage';
4
+
5
+ import type { NavigatorTtsHandler, NavigatorTtsVoices } from './types';
6
+
7
+ const LS_TTS_VOICE_PREF = 'maplite-navigator-tts-voice-pref';
8
+ const LS_VOICE_VOLUME = 'maplite-navigator-voice-volume';
9
+
10
+ export type VoiceVolumeLevel = 1 | 2 | 3 | 4 | 5;
11
+
12
+ const DEFAULT_VOICE_VOLUME_LEVEL: VoiceVolumeLevel = 5;
13
+
14
+ export function volumeLevelToGain(level: VoiceVolumeLevel): number {
15
+ return level * 0.2;
16
+ }
17
+
18
+ export async function readVoiceVolumeLevel(): Promise<VoiceVolumeLevel> {
19
+ try {
20
+ const raw = await AsyncStorage.getItem(LS_VOICE_VOLUME);
21
+ if (!raw) return DEFAULT_VOICE_VOLUME_LEVEL;
22
+ const n = Number.parseInt(raw, 10);
23
+ if (n >= 1 && n <= 5) return n as VoiceVolumeLevel;
24
+ } catch {
25
+ /* ignore */
26
+ }
27
+ return DEFAULT_VOICE_VOLUME_LEVEL;
28
+ }
29
+
30
+ export async function writeVoiceVolumeLevel(level: VoiceVolumeLevel): Promise<void> {
31
+ try {
32
+ await AsyncStorage.setItem(LS_VOICE_VOLUME, String(level));
33
+ } catch {
34
+ /* ignore */
35
+ }
36
+ }
37
+
38
+ export type TtsVoicePref = { voiceKey: string } | { disabled: true };
39
+
40
+ export async function readTtsVoicePref(): Promise<TtsVoicePref | null> {
41
+ try {
42
+ const raw = await AsyncStorage.getItem(LS_TTS_VOICE_PREF);
43
+ if (!raw) return null;
44
+ const parsed = JSON.parse(raw) as unknown;
45
+ if (
46
+ parsed &&
47
+ typeof parsed === 'object' &&
48
+ 'disabled' in parsed &&
49
+ (parsed as { disabled: unknown }).disabled === true
50
+ ) {
51
+ return { disabled: true };
52
+ }
53
+ if (
54
+ parsed &&
55
+ typeof parsed === 'object' &&
56
+ 'voiceKey' in parsed &&
57
+ typeof (parsed as { voiceKey: unknown }).voiceKey === 'string'
58
+ ) {
59
+ const voiceKey = (parsed as { voiceKey: string }).voiceKey.trim();
60
+ if (voiceKey) return { voiceKey };
61
+ }
62
+ } catch {
63
+ /* ignore */
64
+ }
65
+ return null;
66
+ }
67
+
68
+ export async function writeTtsVoicePref(pref: TtsVoicePref): Promise<void> {
69
+ try {
70
+ await AsyncStorage.setItem(LS_TTS_VOICE_PREF, JSON.stringify(pref));
71
+ } catch {
72
+ /* ignore */
73
+ }
74
+ }
75
+
76
+ function defaultVoiceKey(voices: NavigatorTtsVoices): string | null {
77
+ const keys = Object.keys(voices);
78
+ return keys.length > 0 ? keys[0]! : null;
79
+ }
80
+
81
+ function resolveVoiceKey(voices: NavigatorTtsVoices, pref: TtsVoicePref | null): string | null {
82
+ if (pref && 'voiceKey' in pref && pref.voiceKey in voices) {
83
+ return pref.voiceKey;
84
+ }
85
+ return defaultVoiceKey(voices);
86
+ }
87
+
88
+ /**
89
+ * Нативное владение TTS навигатора: выбор голоса/громкости, персист,
90
+ * кэш URL по (voiceKey, text). Веб шлёт готовый текст фразы.
91
+ */
92
+ export type NavigatorTtsController = {
93
+ voiceEntries: [string, string][];
94
+ selectedVoiceKey: string | null;
95
+ voiceEnabled: boolean;
96
+ volumeLevel: VoiceVolumeLevel;
97
+ synthesize: (text: string) => Promise<string | null>;
98
+ selectVoice: (voiceKey: string) => Promise<void>;
99
+ selectVolume: (level: VoiceVolumeLevel) => void;
100
+ disableVoice: () => Promise<void>;
101
+ };
102
+
103
+ export function useNavigatorTts(
104
+ ttsVoices: NavigatorTtsVoices | undefined,
105
+ ttsHandler: NavigatorTtsHandler | undefined
106
+ ): NavigatorTtsController {
107
+ const [selectedVoiceKey, setSelectedVoiceKey] = useState<string | null>(null);
108
+ const [voiceEnabled, setVoiceEnabled] = useState(true);
109
+ const [volumeLevel, setVolumeLevel] = useState<VoiceVolumeLevel>(5);
110
+ const urlCacheRef = useRef(new Map<string, string>());
111
+
112
+ const voiceEntries = Object.entries(ttsVoices ?? {});
113
+ const ttsActive = voiceEntries.length > 0 && typeof ttsHandler === 'function';
114
+
115
+ useEffect(() => {
116
+ let cancelled = false;
117
+ void readVoiceVolumeLevel().then((lvl) => {
118
+ if (!cancelled) setVolumeLevel(lvl);
119
+ });
120
+ if (!ttsActive) {
121
+ setSelectedVoiceKey(null);
122
+ setVoiceEnabled(false);
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }
127
+ void readTtsVoicePref().then((pref) => {
128
+ if (cancelled) return;
129
+ if (pref && 'disabled' in pref) {
130
+ setVoiceEnabled(false);
131
+ setSelectedVoiceKey(resolveVoiceKey(ttsVoices!, pref));
132
+ return;
133
+ }
134
+ setVoiceEnabled(true);
135
+ setSelectedVoiceKey(resolveVoiceKey(ttsVoices!, pref));
136
+ });
137
+ return () => {
138
+ cancelled = true;
139
+ };
140
+ }, [ttsActive, ttsVoices]);
141
+
142
+ const selectVoice = useCallback(
143
+ async (voiceKey: string) => {
144
+ if (!ttsVoices || !(voiceKey in ttsVoices)) return;
145
+ setSelectedVoiceKey(voiceKey);
146
+ setVoiceEnabled(true);
147
+ await writeTtsVoicePref({ voiceKey });
148
+ },
149
+ [ttsVoices]
150
+ );
151
+
152
+ const selectVolume = useCallback((level: VoiceVolumeLevel) => {
153
+ setVolumeLevel(level);
154
+ void writeVoiceVolumeLevel(level);
155
+ }, []);
156
+
157
+ const disableVoice = useCallback(async () => {
158
+ await writeTtsVoicePref({ disabled: true });
159
+ setVoiceEnabled(false);
160
+ }, []);
161
+
162
+ const synthesize = useCallback(
163
+ async (text: string): Promise<string | null> => {
164
+ if (!ttsHandler || !voiceEnabled || !selectedVoiceKey) return null;
165
+ const trimmed = text.trim();
166
+ if (!trimmed) return null;
167
+ const cacheKey = `${selectedVoiceKey}\0${trimmed}`;
168
+ const cached = urlCacheRef.current.get(cacheKey);
169
+ if (cached) return cached;
170
+ try {
171
+ const url = await ttsHandler(trimmed, selectedVoiceKey);
172
+ if (typeof url === 'string' && url.trim()) {
173
+ const u = url.trim();
174
+ urlCacheRef.current.set(cacheKey, u);
175
+ return u;
176
+ }
177
+ } catch (err) {
178
+ console.warn('[maplite] navigator TTS failed', err);
179
+ }
180
+ return null;
181
+ },
182
+ [ttsHandler, voiceEnabled, selectedVoiceKey]
183
+ );
184
+
185
+ return {
186
+ voiceEntries: ttsActive ? voiceEntries : [],
187
+ selectedVoiceKey: ttsActive ? selectedVoiceKey : null,
188
+ voiceEnabled: ttsActive && voiceEnabled,
189
+ volumeLevel,
190
+ synthesize,
191
+ selectVoice,
192
+ selectVolume,
193
+ disableVoice,
194
+ };
195
+ }