301 lines
9.1 KiB
TypeScript
301 lines
9.1 KiB
TypeScript
import React, {
|
|
useRef,
|
|
useEffect,
|
|
useCallback,
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
} from "react";
|
|
import { StyleSheet } from "react-native";
|
|
import { WebView } from "react-native-webview";
|
|
|
|
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
|
|
|
|
export interface TomTomMarker {
|
|
id: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
color: string;
|
|
label?: string;
|
|
description?: string;
|
|
isSelected?: boolean;
|
|
}
|
|
|
|
export interface TomTomRoute {
|
|
coordinates: { latitude: number; longitude: number }[];
|
|
color?: string;
|
|
}
|
|
|
|
export interface TomTomDestination {
|
|
latitude: number;
|
|
longitude: number;
|
|
color?: string;
|
|
}
|
|
|
|
export interface TomTomMapRef {
|
|
fitAllMarkers: () => void;
|
|
fitToCoordinates: (
|
|
coords: { latitude: number; longitude: number }[],
|
|
) => void;
|
|
}
|
|
|
|
interface TomTomMapProps {
|
|
style?: any;
|
|
markers?: TomTomMarker[];
|
|
route?: TomTomRoute | null;
|
|
destination?: TomTomDestination | null;
|
|
initialCenter?: { latitude: number; longitude: number };
|
|
initialZoom?: number;
|
|
onMarkerPress?: (markerId: string) => void;
|
|
}
|
|
|
|
const TomTomMap = forwardRef<TomTomMapRef, TomTomMapProps>(
|
|
(
|
|
{
|
|
style,
|
|
markers = [],
|
|
route,
|
|
destination,
|
|
initialCenter,
|
|
initialZoom = 12,
|
|
onMarkerPress,
|
|
},
|
|
ref,
|
|
) => {
|
|
const webViewRef = useRef<WebView>(null);
|
|
const isReady = useRef(false);
|
|
const pendingMessages = useRef<string[]>([]);
|
|
|
|
const sendMessage = useCallback((msg: object) => {
|
|
const json = JSON.stringify(msg);
|
|
if (isReady.current) {
|
|
webViewRef.current?.injectJavaScript(
|
|
`handleMessage(${json}); true;`,
|
|
);
|
|
} else {
|
|
pendingMessages.current.push(json);
|
|
}
|
|
}, []);
|
|
|
|
useImperativeHandle(
|
|
ref,
|
|
() => ({
|
|
fitAllMarkers: () => sendMessage({ type: "fitAll" }),
|
|
fitToCoordinates: (coords) =>
|
|
sendMessage({ type: "fitCoords", coords }),
|
|
}),
|
|
[sendMessage],
|
|
);
|
|
|
|
useEffect(() => {
|
|
sendMessage({ type: "updateMarkers", markers });
|
|
}, [markers, sendMessage]);
|
|
|
|
useEffect(() => {
|
|
sendMessage({
|
|
type: "updateRoute",
|
|
route: route || null,
|
|
destination: destination || null,
|
|
});
|
|
}, [route, destination, sendMessage]);
|
|
|
|
const onMessage = useCallback(
|
|
(event: any) => {
|
|
try {
|
|
const data = JSON.parse(event.nativeEvent.data);
|
|
if (data.type === "ready") {
|
|
isReady.current = true;
|
|
// flush pending
|
|
for (const msg of pendingMessages.current) {
|
|
webViewRef.current?.injectJavaScript(
|
|
`handleMessage(${msg}); true;`,
|
|
);
|
|
}
|
|
pendingMessages.current = [];
|
|
// Auto fit markers after ready + flush
|
|
setTimeout(() => {
|
|
sendMessage({ type: "fitAll" });
|
|
}, 500);
|
|
} else if (data.type === "markerPress" && onMarkerPress) {
|
|
onMarkerPress(data.id);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
},
|
|
[onMarkerPress],
|
|
);
|
|
|
|
const center =
|
|
initialCenter ||
|
|
(markers.length > 0
|
|
? {
|
|
latitude: markers[0].latitude,
|
|
longitude: markers[0].longitude,
|
|
}
|
|
: { latitude: 48.8566, longitude: 2.3522 });
|
|
|
|
const html = buildHtml(center, initialZoom);
|
|
|
|
return (
|
|
<WebView
|
|
ref={webViewRef}
|
|
style={[styles.map, style]}
|
|
source={{ html }}
|
|
onMessage={onMessage}
|
|
javaScriptEnabled
|
|
domStorageEnabled
|
|
scrollEnabled={false}
|
|
bounces={false}
|
|
originWhitelist={["*"]}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
|
|
function buildHtml(
|
|
center: { latitude: number; longitude: number },
|
|
zoom: number,
|
|
) {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<link rel="stylesheet" href="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps.css">
|
|
<script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps-web.min.js"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; }
|
|
html, body, #map { width: 100%; height: 100%; }
|
|
.marker-dot {
|
|
width: 28px; height: 28px; border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
border: 2px solid rgba(255,255,255,0.8);
|
|
font-size: 12px; color: white; cursor: pointer;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
|
}
|
|
.marker-dot.selected {
|
|
width: 36px; height: 36px;
|
|
border: 3px solid #7c3aed;
|
|
box-shadow: 0 0 12px rgba(124,58,237,0.6);
|
|
}
|
|
.dest-marker {
|
|
width: 24px; height: 24px; border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
border: 2px solid white;
|
|
font-size: 10px; color: white;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="map"></div>
|
|
<script>
|
|
var map = tt.map({
|
|
key: '${TOMTOM_API_KEY}',
|
|
container: 'map',
|
|
center: [${center.longitude}, ${center.latitude}],
|
|
zoom: ${zoom},
|
|
stylesVisibility: { trafficFlow: false, trafficIncidents: false }
|
|
});
|
|
|
|
var markers = {};
|
|
var routeLayer = null;
|
|
var destMarker = null;
|
|
|
|
function clearMarkers() {
|
|
Object.values(markers).forEach(function(m) { m.remove(); });
|
|
markers = {};
|
|
}
|
|
|
|
function clearRoute() {
|
|
if (routeLayer && map.getSource('route')) {
|
|
map.removeLayer('route-line');
|
|
map.removeSource('route');
|
|
routeLayer = null;
|
|
}
|
|
if (destMarker) { destMarker.remove(); destMarker = null; }
|
|
}
|
|
|
|
function handleMessage(data) {
|
|
if (data.type === 'updateMarkers') {
|
|
clearMarkers();
|
|
(data.markers || []).forEach(function(m) {
|
|
var el = document.createElement('div');
|
|
el.className = 'marker-dot' + (m.isSelected ? ' selected' : '');
|
|
el.style.backgroundColor = m.color || '#7c3aed';
|
|
el.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
|
|
el.addEventListener('click', function() {
|
|
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'markerPress', id: m.id }));
|
|
});
|
|
var popup = new tt.Popup({ offset: 20, closeButton: false })
|
|
.setHTML('<div style="padding:4px 8px;font-size:12px;"><b>' + (m.label || m.id) + '</b>' + (m.description ? '<br>' + m.description : '') + '</div>');
|
|
var marker = new tt.Marker({ element: el })
|
|
.setLngLat([m.longitude, m.latitude])
|
|
.setPopup(popup)
|
|
.addTo(map);
|
|
markers[m.id] = marker;
|
|
});
|
|
}
|
|
|
|
if (data.type === 'updateRoute') {
|
|
clearRoute();
|
|
if (data.destination) {
|
|
var el = document.createElement('div');
|
|
el.className = 'dest-marker';
|
|
el.style.backgroundColor = data.destination.color || '#ef4444';
|
|
el.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="white"><path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/></svg>';
|
|
destMarker = new tt.Marker({ element: el })
|
|
.setLngLat([data.destination.longitude, data.destination.latitude])
|
|
.addTo(map);
|
|
}
|
|
if (data.route && data.route.coordinates && data.route.coordinates.length > 1) {
|
|
var coords = data.route.coordinates.map(function(c) { return [c.longitude, c.latitude]; });
|
|
map.addSource('route', {
|
|
type: 'geojson',
|
|
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords }, properties: {} }
|
|
});
|
|
map.addLayer({
|
|
id: 'route-line',
|
|
type: 'line',
|
|
source: 'route',
|
|
paint: {
|
|
'line-color': data.route.color || '#4285F4',
|
|
'line-width': 4,
|
|
'line-opacity': 0.8
|
|
}
|
|
});
|
|
routeLayer = true;
|
|
}
|
|
}
|
|
|
|
if (data.type === 'fitAll') {
|
|
var allM = Object.values(markers);
|
|
if (allM.length > 0) {
|
|
var bounds = new tt.LngLatBounds();
|
|
allM.forEach(function(m) { bounds.extend(m.getLngLat()); });
|
|
map.fitBounds(bounds, { padding: 60, maxZoom: 15 });
|
|
}
|
|
}
|
|
|
|
if (data.type === 'fitCoords') {
|
|
if (data.coords && data.coords.length > 0) {
|
|
var bounds = new tt.LngLatBounds();
|
|
data.coords.forEach(function(c) { bounds.extend([c.longitude, c.latitude]); });
|
|
map.fitBounds(bounds, { padding: 80, maxZoom: 15 });
|
|
}
|
|
}
|
|
}
|
|
|
|
map.on('load', function() {
|
|
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'ready' }));
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
map: { flex: 1 },
|
|
});
|
|
|
|
export default TomTomMap;
|