File size: 3,615 Bytes
5c876be | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | import React, { useRef, useCallback, useEffect } from 'react';
import { StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
import type { LatLng } from '../../types/location';
import { getLeafletMapHtml } from './LeafletMapHtml';
import type { LeafletMapMarker, LeafletMapPolyline } from './LeafletMapHtml';
export interface LeafletMapProps {
/** Center coordinate of the map */
center: LatLng;
/** Initial zoom level (default 14) */
zoom?: number;
/** Markers to display on the map */
markers?: LeafletMapMarker[];
/** Polylines to draw on the map */
polylines?: LeafletMapPolyline[];
/** Whether to show the pulsing user-location marker */
showCurrentLocation?: boolean;
/** Whether the map is interactive (default true) */
interactive?: boolean;
/** Fired when a marker is tapped */
onMarkerTap?: (data: { lat: number; lng: number; title?: string; type?: string }) => void;
/** Fired once the Leaflet map is ready */
onMapReady?: () => void;
/** Fired on map move (center / zoom change) */
onMapMove?: (data: { lat: number; lng: number; zoom: number }) => void;
/** Optional style override for the WebView container */
style?: object;
}
/**
* WebView wrapper that renders a Leaflet.js map via injected HTML.
* Handles two-way communication (postMessage / onMessage) between the
* WebView and React Native.
*/
export function LeafletMap({
center,
zoom = 14,
markers = [],
polylines = [],
showCurrentLocation = false,
interactive = true,
onMarkerTap,
onMapReady,
onMapMove,
style,
}: LeafletMapProps) {
const webViewRef = useRef<WebView>(null);
// Regenerate HTML when core options change
const html = getLeafletMapHtml({
centerLat: center.lat,
centerLng: center.lng,
zoom,
markers,
polylines,
showCurrentLocation,
interactive,
});
const handleMessage = useCallback(
(event: { nativeEvent: { data: string } }) => {
try {
const msg = JSON.parse(event.nativeEvent.data);
switch (msg.event) {
case 'map_ready':
onMapReady?.();
break;
case 'marker_tap':
onMarkerTap?.(msg.data);
break;
case 'map_move':
onMapMove?.(msg.data);
break;
}
} catch {
// ignore malformed messages
}
},
[onMapReady, onMarkerTap, onMapMove],
);
// Public imperative methods exposed via ref-forwarding
// These are called by injecting JS into the WebView.
const runJS = useCallback((js: string) => {
webViewRef.current?.injectJavaScript(js);
}, []);
// Expose updateCurrentLocation as a side-effect (if needed)
// The parent can call `webViewRef.current.injectJavaScript(...)` directly.
useEffect(() => {
// No-op on mount; the map is fully rendered via the html prop.
}, [center.lat, center.lng]);
return (
<WebView
ref={webViewRef}
source={{ html }}
style={[styles.webview, style]}
originWhitelist={['*']}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
scrollEnabled={false}
bounces={false}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
cacheEnabled
cacheMode="LOAD_DEFAULT"
androidLayerType="hardware"
// @ts-expect-error – mixed content mode needed for OSM tiles
mixedContentMode="always"
pointerEvents={interactive ? 'auto' : 'none'}
/>
);
}
const styles = StyleSheet.create({
webview: {
flex: 1,
backgroundColor: 'transparent',
},
});
|