import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { MapContainer, TileLayer, Marker, Polyline, Popup, useMap, useMapEvents } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { ChevronLeft, Compass } from 'lucide-react'; interface NavigationRouteData { origin: { lat: number; lng: number }; destination: { lat: number; lng: number; name: string }; tourTitle?: string; } interface TourNavigationPageProps { tourId: string; routeData: NavigationRouteData | null; onBack: () => void; } const FitBounds = ({ coords, destination, hasInteractedRef }: { coords: [number, number][], destination: { lat: number; lng: number; name: string } | null, hasInteractedRef: React.MutableRefObject }) => { const map = useMap(); useEffect(() => { if (coords.length > 0 && !hasInteractedRef.current && destination) { map.fitBounds(coords, { padding: [60, 60], maxZoom: 15 }); } }, [map, coords, destination, hasInteractedRef]); return null; }; const MapRotationHandler = ({ rotation }: { rotation: number }) => { const map = useMap(); const cumulativeRotationRef = useRef(0); const prevRotationRef = useRef(0); useEffect(() => { const container = map.getContainer(); container.style.transformOrigin = 'center center'; container.style.willChange = 'transform'; if (rotation === 0) { cumulativeRotationRef.current = 0; prevRotationRef.current = 0; container.style.transform = ''; return; } let delta = rotation - prevRotationRef.current; if (delta > 180) delta -= 360; else if (delta < -180) delta += 360; cumulativeRotationRef.current += delta; prevRotationRef.current = rotation; container.style.transform = `rotate(${cumulativeRotationRef.current}deg) scale(2.2) translateZ(0)`; container.style.transition = 'transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1)'; }, [rotation, map]); return null; }; const CompassInteractionDetector = ({ onUserInteraction, hasInteractedRef }: { onUserInteraction: () => void; hasInteractedRef: React.MutableRefObject; }) => { useMapEvents({ movestart: () => { hasInteractedRef.current = true; }, zoomstart: () => { hasInteractedRef.current = true; }, dragstart: () => { hasInteractedRef.current = true; }, }); return null; }; const MapRefSetter = ({ mapRef }: { mapRef: React.MutableRefObject }) => { const map = useMap(); useEffect(() => { mapRef.current = map; }, [map, mapRef]); return null; }; const MapInteractionWatcher = ({ hasInteractedRef }: { hasInteractedRef: React.MutableRefObject }) => { const map = useMap(); useEffect(() => { const onZoomEnd = () => { map._userZoomLevel = map.getZoom(); hasInteractedRef.current = true; }; const onMoveEnd = () => { map._userCenter = map.getCenter(); hasInteractedRef.current = true; }; map.on('zoomend', onZoomEnd); map.on('moveend', onMoveEnd); return () => { map.off('zoomend', onZoomEnd); map.off('moveend', onMoveEnd); }; }, [map, hasInteractedRef]); return null; }; const MapSizeHandler = ({ mapRef }: { mapRef: React.MutableRefObject }) => { const map = useMap(); const prevSizeRef = useRef<{ width: number; height: number } | null>(null); useEffect(() => { const container = map.getContainer(); if (!container) return; prevSizeRef.current = { width: container.clientWidth, height: container.clientHeight }; const observer = new ResizeObserver(() => { const newWidth = container.clientWidth; const newHeight = container.clientHeight; const prev = prevSizeRef.current; if ((prev && (Math.abs(newWidth - prev.width) > 2 || Math.abs(newHeight - prev.height) > 2)) || newWidth === 0 || newHeight === 0) { prevSizeRef.current = { width: newWidth, height: newHeight }; const userZoom = (map as any)._userZoomLevel as number | undefined; map.invalidateSize({ animate: false }); if (userZoom !== undefined && Math.abs(map.getZoom() - userZoom) > 0.01) { map.setZoom(userZoom, { animate: false }); } } }); observer.observe(container); return () => observer.disconnect(); }, [map, mapRef]); return null; }; export const TourNavigationPage: React.FC = ({ tourId, routeData, onBack }) => { const [isCompassActive, setIsCompassActive] = useState(false); const [isLocatingUser, setIsLocatingUser] = useState(false); const [currentHeading, setCurrentHeading] = useState(0); const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null); const [error, setError] = useState(null); const mapRef = useRef(null); const watchIdRef = useRef(null); const fetchLockRef = useRef(false); const hasInteractedRef = useRef(false); const originLat = routeData?.origin?.lat; const originLng = routeData?.origin?.lng; const destLat = routeData?.destination?.lat; const destLng = routeData?.destination?.lng; const stopLocating = useCallback(() => { setIsLocatingUser(false); }, []); useEffect(() => { const map = mapRef.current; if (!map) return; const onMoveStart = () => { if (isLocatingUser) { stopLocating(); } }; map.on('movestart', onMoveStart); return () => { map.off('movestart', onMoveStart); }; }, [isLocatingUser, stopLocating]); useEffect(() => { if (!isLocatingUser || !mapRef.current) return; if (navigator.geolocation) { watchIdRef.current = navigator.geolocation.watchPosition( (position) => { if (mapRef.current) { const currentZoom = mapRef.current.getZoom(); mapRef.current.setView([position.coords.latitude, position.coords.longitude], currentZoom, { animate: true }); } }, (err) => console.error("User location tracking error:", err), { enableHighAccuracy: true } ); } return () => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); watchIdRef.current = null; } }; }, [isLocatingUser]); useEffect(() => { if (!routeData || !originLat || !originLng || !destLat || !destLng) { setRouteGeometry(null); setError(null); return; } if (fetchLockRef.current) return; const calculateOptimalRoute = async () => { try { fetchLockRef.current = true; const url = `https://router.project-osrm.org/route/v1/driving/${originLng},${originLat};${destLng},${destLat}?overview=full&geometries=geojson`; const res = await fetch(url); if (!res.ok) throw new Error(`OSRM error: ${res.status}`); const data: { code: string; routes: Array<{ geometry: { coordinates: number[][] } }> } = await res.json(); if (data.code !== 'Ok' || !data.routes?.length) throw new Error('Không tìm thấy lộ trình phù hợp'); const coords = data.routes[0].geometry.coordinates.map((c: number[]) => [c[1], c[0]] as [number, number]); setRouteGeometry(coords); } catch (err: any) { setError(err.message); } finally { fetchLockRef.current = false; } }; calculateOptimalRoute(); return () => { fetchLockRef.current = false; }; }, [routeData, originLat, originLng, destLat, destLng]); const handleUserInteraction = useCallback(() => { hasInteractedRef.current = true; if (isCompassActive) { setIsCompassActive(false); } }, [isCompassActive]); useEffect(() => { if (!isCompassActive) { setCurrentHeading(0); return; } if (navigator.geolocation) { watchIdRef.current = navigator.geolocation.watchPosition( (position) => { if (mapRef.current) { mapRef.current.setView([position.coords.latitude, position.coords.longitude], undefined, { animate: true }); } if (position.coords.heading !== null) { setCurrentHeading(position.coords.heading); } }, (err) => console.error("Compass tracking acquisition error:", err), { enableHighAccuracy: true } ); } const handleOrientation = (event: any) => { let heading: number | null = null; if (event.webkitCompassHeading !== undefined && event.webkitCompassHeading !== null) { heading = event.webkitCompassHeading; } else if (event.alpha !== null && event.alpha !== undefined) { if (event.absolute === true || event.type === 'deviceorientationabsolute') { heading = (360 - event.alpha) % 360; } } if (heading !== null) { setCurrentHeading(heading); } }; window.addEventListener('deviceorientation', handleOrientation, true); window.addEventListener('deviceorientationabsolute', handleOrientation, true); return () => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); } window.removeEventListener('deviceorientation', handleOrientation, true); window.removeEventListener('deviceorientationabsolute', handleOrientation, true); }; }, [isCompassActive]); const userIcon = useMemo(() => L.divIcon({ className: '!bg-transparent !border-none', html: `
`, iconSize: [32, 32], iconAnchor: [16, 16] }), []); const destIcon = useMemo(() => L.divIcon({ className: '!bg-transparent !border-none', html: `
Đ
`, iconSize: [40, 40], iconAnchor: [20, 20] }), []); if (!routeData) return null; const center: [number, number] = routeData.origin && routeData.destination ? [(routeData.origin.lat + routeData.destination.lat) / 2, (routeData.origin.lng + routeData.destination.lng) / 2] : [0, 0]; const centerOnUser = () => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { if (mapRef.current) { const currentZoom = mapRef.current.getZoom(); mapRef.current.setView([position.coords.latitude, position.coords.longitude], currentZoom, { animate: true }); canFitRef.current = false; } }, (err) => console.error("Cannot get user location:", err), { enableHighAccuracy: true } ); } }; return (

{routeData.tourTitle || "Bản đồ chỉ đường"}

{routeGeometry && ( )} {/* Locate User Button */} {error && (
{error}
)}
); };