react-native-maplibre-lite 0.2.1 → 0.2.3

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.
@@ -122,6 +122,13 @@ interface MapViewProps {
122
122
  * стороне. Без него озвучка и FAB выбора голоса отключены.
123
123
  */
124
124
  navigatorVoiceUrl?: string;
125
+ /**
126
+ * Dev-only: автоматически вести маркер по построенному маршруту
127
+ * (симуляция поездки для отладки). Требует `navigator: true`. Пока
128
+ * включено, реальный GPS в WebView не пробрасывается, чтобы не
129
+ * конфликтовать с симуляцией.
130
+ */
131
+ navigatorSimulate?: boolean;
125
132
  onNavigatorRouteSet?: (params: NavigatorRouteSetParams) => void;
126
133
  onNavigatorInstruction?: (params: NavigatorInstructionParams) => void;
127
134
  onNavigatorPositionSet?: (params: NavigatorPositionSetParams) => void;
@@ -150,6 +157,8 @@ export type MapViewRef = {
150
157
  setNavigatorPosition: (latitude: number, longitude: number, accuracy?: number) => void;
151
158
  /** Режим «клик по карте = новая позиция» (удобно в dev). */
152
159
  pickNavigatorPosition: () => void;
160
+ /** Dev-only: включить/выключить симуляцию поездки по маршруту. */
161
+ setNavigatorSimulation: (enabled: boolean) => void;
153
162
  };
154
163
 
155
164
  type MapViewRegistry = {
@@ -437,6 +446,10 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
437
446
  sendToWebView({ function: 'recenterNavigatorCamera', params: {} });
438
447
  };
439
448
 
449
+ const setNavigatorSimulation = (enabled: boolean) => {
450
+ sendToWebView({ function: 'setNavigatorSimulation', params: { enabled } });
451
+ };
452
+
440
453
  useImperativeHandle(ref, () => ({
441
454
  fitBounds,
442
455
  flyTo,
@@ -444,6 +457,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
444
457
  advanceNavigatorInstruction,
445
458
  setNavigatorPosition,
446
459
  pickNavigatorPosition,
460
+ setNavigatorSimulation,
447
461
  }), [fitBounds]);
448
462
 
449
463
 
@@ -563,7 +577,17 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
563
577
  };
564
578
 
565
579
  useEffect(() => {
566
- if (!props.navigator) {
580
+ if (!inited || !props.navigator) {
581
+ return;
582
+ }
583
+ sendToWebViewRef.current({
584
+ function: 'setNavigatorSimulation',
585
+ params: { enabled: !!props.navigatorSimulate },
586
+ });
587
+ }, [inited, props.navigator, props.navigatorSimulate]);
588
+
589
+ useEffect(() => {
590
+ if (!props.navigator || props.navigatorSimulate) {
567
591
  return;
568
592
  }
569
593
 
@@ -615,7 +639,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
615
639
  return () => {
616
640
  Geolocation.clearWatch(watchId);
617
641
  };
618
- }, [props.navigator]);
642
+ }, [props.navigator, props.navigatorSimulate]);
619
643
 
620
644
  return (
621
645
  <View style={props.style}>
@@ -95,32 +95,73 @@ export async function writeVoicePref(pref: VoicePref): Promise<void> {
95
95
  }
96
96
  }
97
97
 
98
+ const HTTP_URL_PREFIX_RE = /^https?:\/\//i;
99
+
100
+ function trimTrailingSlashes(s: string): string {
101
+ return s.replace(/\/+$/, '');
102
+ }
103
+
104
+ /** Strips `?query` and `#hash` — catalog URLs are plain paths. */
105
+ function stripUrlQueryAndHash(url: string): string {
106
+ const q = url.indexOf('?');
107
+ const h = url.indexOf('#');
108
+ let end = url.length;
109
+ if (q >= 0) end = Math.min(end, q);
110
+ if (h >= 0) end = Math.min(end, h);
111
+ return url.slice(0, end);
112
+ }
113
+
114
+ function pathnameSegments(pathname: string): string[] {
115
+ return pathname.split('/').filter(Boolean);
116
+ }
117
+
118
+ /**
119
+ * Parses `http(s)://host[:port]/path` without the URL API (Hermes-safe).
120
+ */
121
+ function parseHttpUrl(url: string): { origin: string; pathname: string } | null {
122
+ const bare = stripUrlQueryAndHash(url.trim());
123
+ if (!HTTP_URL_PREFIX_RE.test(bare)) return null;
124
+
125
+ const schemeEnd = bare.indexOf('://');
126
+ const rest = bare.slice(schemeEnd + 3);
127
+ const slash = rest.indexOf('/');
128
+ const authority = slash === -1 ? rest : rest.slice(0, slash);
129
+ if (!authority) return null;
130
+
131
+ const pathname = slash === -1 ? '/' : rest.slice(slash);
132
+ const origin = bare.slice(0, schemeEnd + 3) + authority;
133
+ return { origin, pathname };
134
+ }
135
+
98
136
  export function resolveVoiceCatalogUrl(url: string): string | null {
99
137
  const t = url.trim();
100
138
  if (!t) return null;
101
- try {
102
- // eslint-disable-next-line no-new
103
- new URL(t);
104
- return t;
105
- } catch {
106
- return null;
107
- }
139
+ return parseHttpUrl(t) ? t : null;
108
140
  }
109
141
 
110
142
  /** Base URL for a voice directory: `.../voices/{dir}`. */
111
143
  export function voiceDirBaseUrl(catalogUrl: string, dir: string): string {
112
- const u = new URL(catalogUrl);
113
- const parts = u.pathname.split('/').filter(Boolean);
144
+ const parsed = parseHttpUrl(catalogUrl);
145
+ if (!parsed) {
146
+ throw new Error('voiceDirBaseUrl: invalid catalog URL');
147
+ }
148
+
149
+ const parts = pathnameSegments(parsed.pathname);
114
150
  if (parts.length > 0 && parts[parts.length - 1]!.toLowerCase().endsWith('.json')) {
115
151
  parts.pop();
116
152
  }
117
- parts.push(dir);
118
- u.pathname = `/${parts.join('/')}`;
119
- return u.toString().replace(/\/+$/, '');
153
+
154
+ const safeDir = dir.trim().replace(/[/\\]+/g, '');
155
+ if (!safeDir) {
156
+ throw new Error('voiceDirBaseUrl: empty dir');
157
+ }
158
+ parts.push(safeDir);
159
+
160
+ return trimTrailingSlashes(`${parsed.origin}/${parts.join('/')}`);
120
161
  }
121
162
 
122
163
  function voiceManifestUrl(baseUrl: string): string {
123
- return `${baseUrl.replace(/\/+$/, '')}/data.json`;
164
+ return `${trimTrailingSlashes(baseUrl)}/data.json`;
124
165
  }
125
166
 
126
167
  export async function fetchVoiceCatalog(catalogUrl: string): Promise<VoiceCatalogEntry[]> {
@@ -257,7 +298,7 @@ export function keysToClipUrls(
257
298
  clipBaseUrl: string,
258
299
  keys: string[]
259
300
  ): string[] {
260
- const base = clipBaseUrl.replace(/\/+$/, '');
301
+ const base = trimTrailingSlashes(clipBaseUrl);
261
302
  const variants = new Map<VoicePhraseKey, string>();
262
303
  const urls: string[] = [];
263
304
  for (const raw of keys) {
@@ -238,6 +238,11 @@ export type MapLiteSetNavigatorPositionParams = {
238
238
  accuracy?: number,
239
239
  }
240
240
 
241
+ /** RN → Web: `setNavigatorSimulation`. Отладочная симуляция поездки по маршруту. */
242
+ export type MapLiteSetNavigatorSimulationParams = {
243
+ enabled: boolean,
244
+ }
245
+
241
246
  /** RN → Web: JSON в `WebView.postMessage` (`webProject/src/map/MapLiteController.receive`). */
242
247
  export type NativeToWebCommand =
243
248
  | { function: 'init', params: MapLiteInitParams }
@@ -255,6 +260,7 @@ export type NativeToWebCommand =
255
260
  | { function: 'setNavigatorPosition', params: MapLiteSetNavigatorPositionParams }
256
261
  | { function: 'pickNavigatorPosition', params: Record<string, never> }
257
262
  | { function: 'recenterNavigatorCamera', params: Record<string, never> }
263
+ | { function: 'setNavigatorSimulation', params: MapLiteSetNavigatorSimulationParams }
258
264
 
259
265
  /** Web → RN: `postToNative` (`webProject/src/map/types.ts`). */
260
266
  export type WebToNativeMessage =