fix: bản đồ không zoom hay thu nhỏ được, không hiển thị nút vị trí của tôi trên bản đồ
This commit is contained in:
@@ -177,8 +177,11 @@ const MapTourBounds = ({ locations }: { locations: any[] }) => {
|
||||
// Component Helper để di chuyển tâm bản đồ về vị trí người dùng khi nhấn nút
|
||||
const RecenterUser = ({ position, trigger }: { position: [number, number] | null, trigger: number }) => {
|
||||
const map = useMap();
|
||||
const lastTriggerRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (position && trigger > 0) {
|
||||
if (position && trigger > lastTriggerRef.current) {
|
||||
lastTriggerRef.current = trigger;
|
||||
map.setView(position, 16, { animate: true });
|
||||
}
|
||||
}, [trigger, position, map]);
|
||||
@@ -2481,6 +2484,16 @@ export const TourDetailPage = ({
|
||||
})}
|
||||
</MarkerClusterGroup>
|
||||
</MapContainer>
|
||||
|
||||
{/* Floating Locate User Button */}
|
||||
<button
|
||||
onClick={() => setLocateTrigger(prev => prev + 1)}
|
||||
disabled={!userLocation}
|
||||
className={`absolute bottom-4 right-4 z-[1001] w-10 h-10 bg-white/90 backdrop-blur-md rounded-xl border border-white shadow-xl text-blue-600 hover:bg-blue-50 transition-all active:scale-95 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||
title="Vị trí của tôi"
|
||||
>
|
||||
<LocateFixed className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Menu ngữ cảnh khi click chuột phải vào con đường */}
|
||||
{routeMenu && (
|
||||
@@ -3268,6 +3281,16 @@ export const TourDetailPage = ({
|
||||
})}
|
||||
</MapContainer>
|
||||
|
||||
{/* Floating Locate User Button (Fullscreen) */}
|
||||
<button
|
||||
onClick={() => setLocateTrigger(prev => prev + 1)}
|
||||
disabled={!userLocation}
|
||||
className={`absolute bottom-8 right-6 z-[1001] w-12 h-12 bg-white/90 backdrop-blur-md rounded-2xl border border-white shadow-xl text-blue-600 hover:bg-blue-50 transition-all active:scale-95 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed`}
|
||||
title="Vị trí của tôi"
|
||||
>
|
||||
<LocateFixed className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Overlay điều khiển trên bản đồ toàn màn hình */}
|
||||
<div className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] right-4 z-[1001] flex flex-col gap-2">
|
||||
<button
|
||||
|
||||
@@ -137,62 +137,24 @@ const MapSizeHandler = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | nul
|
||||
export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId, routeData, onBack }) => {
|
||||
const [isCompassActive, setIsCompassActive] = useState(false);
|
||||
const [isLocatingUser, setIsLocatingUser] = useState(false);
|
||||
const [isTrackingLocation, setIsTrackingLocation] = useState(false);
|
||||
const [currentHeading, setCurrentHeading] = useState(0);
|
||||
const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const mapRef = useRef<L.Map | null>(null);
|
||||
const watchIdRef = useRef<number | null>(null);
|
||||
const trackingWatchIdRef = useRef<number | null>(null);
|
||||
const compassWatchIdRef = useRef<number | null>(null);
|
||||
const fetchLockRef = useRef(false);
|
||||
const hasInteractedRef = useRef(false);
|
||||
const isProgrammaticMoveRef = 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) {
|
||||
@@ -228,6 +190,74 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
};
|
||||
}, [routeData, originLat, originLng, destLat, destLng]);
|
||||
|
||||
// Effect 1: Live tracking using watchPosition
|
||||
useEffect(() => {
|
||||
if (isTrackingLocation) {
|
||||
if (navigator.geolocation) {
|
||||
console.log("GPS Location Tracking engaged. Syncing viewport to center...");
|
||||
|
||||
trackingWatchIdRef.current = navigator.geolocation.watchPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
if (mapRef.current) {
|
||||
const currentZoom = mapRef.current.getZoom();
|
||||
isProgrammaticMoveRef.current = true;
|
||||
mapRef.current.setView([latitude, longitude], Math.max(currentZoom, 16), { animate: true });
|
||||
setTimeout(() => {
|
||||
isProgrammaticMoveRef.current = false;
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
(error) => console.error("GPS stream tracking lost:", error),
|
||||
{ enableHighAccuracy: true }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (trackingWatchIdRef.current !== null) {
|
||||
navigator.geolocation.clearWatch(trackingWatchIdRef.current);
|
||||
trackingWatchIdRef.current = null;
|
||||
console.log("GPS Location Tracking disabled. Map control released to user.");
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (trackingWatchIdRef.current !== null) {
|
||||
navigator.geolocation.clearWatch(trackingWatchIdRef.current);
|
||||
trackingWatchIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isTrackingLocation]);
|
||||
|
||||
// Effect 2: Gesture detection to auto-toggle tracking OFF when user interacts
|
||||
useEffect(() => {
|
||||
if (!mapRef.current) return;
|
||||
const map = mapRef.current;
|
||||
|
||||
const breakTrackingOnGesture = () => {
|
||||
if (isProgrammaticMoveRef.current) {
|
||||
return;
|
||||
}
|
||||
if (isTrackingLocation) {
|
||||
console.log("User touch map interaction detected. Disengaging auto-center lock.");
|
||||
setIsTrackingLocation(false);
|
||||
}
|
||||
if (isCompassActive) {
|
||||
console.log("User touch map interaction detected. Disengaging compass lock.");
|
||||
setIsCompassActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
map.on('dragstart', breakTrackingOnGesture);
|
||||
map.on('zoomstart', breakTrackingOnGesture);
|
||||
map.on('movestart', breakTrackingOnGesture);
|
||||
|
||||
return () => {
|
||||
map.off('dragstart', breakTrackingOnGesture);
|
||||
map.off('zoomstart', breakTrackingOnGesture);
|
||||
map.off('movestart', breakTrackingOnGesture);
|
||||
};
|
||||
}, [isTrackingLocation, isCompassActive]);
|
||||
|
||||
const handleUserInteraction = useCallback(() => {
|
||||
hasInteractedRef.current = true;
|
||||
if (isCompassActive) {
|
||||
@@ -242,10 +272,14 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
}
|
||||
|
||||
if (navigator.geolocation) {
|
||||
watchIdRef.current = navigator.geolocation.watchPosition(
|
||||
compassWatchIdRef.current = navigator.geolocation.watchPosition(
|
||||
(position) => {
|
||||
if (mapRef.current) {
|
||||
isProgrammaticMoveRef.current = true;
|
||||
mapRef.current.setView([position.coords.latitude, position.coords.longitude], undefined, { animate: true });
|
||||
setTimeout(() => {
|
||||
isProgrammaticMoveRef.current = false;
|
||||
}, 100);
|
||||
}
|
||||
if (position.coords.heading !== null) {
|
||||
setCurrentHeading(position.coords.heading);
|
||||
@@ -274,8 +308,9 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
window.addEventListener('deviceorientationabsolute', handleOrientation, true);
|
||||
|
||||
return () => {
|
||||
if (watchIdRef.current !== null) {
|
||||
navigator.geolocation.clearWatch(watchIdRef.current);
|
||||
if (compassWatchIdRef.current !== null) {
|
||||
navigator.geolocation.clearWatch(compassWatchIdRef.current);
|
||||
compassWatchIdRef.current = null;
|
||||
}
|
||||
window.removeEventListener('deviceorientation', handleOrientation, true);
|
||||
window.removeEventListener('deviceorientationabsolute', handleOrientation, true);
|
||||
@@ -304,15 +339,24 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
|
||||
const centerOnUser = () => {
|
||||
if (navigator.geolocation) {
|
||||
setIsLocatingUser(true);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
setIsLocatingUser(false);
|
||||
if (mapRef.current) {
|
||||
const currentZoom = mapRef.current.getZoom();
|
||||
mapRef.current.setView([position.coords.latitude, position.coords.longitude], currentZoom, { animate: true });
|
||||
canFitRef.current = false;
|
||||
isProgrammaticMoveRef.current = true;
|
||||
mapRef.current.setView([position.coords.latitude, position.coords.longitude], Math.max(currentZoom, 16), { animate: true });
|
||||
setTimeout(() => {
|
||||
isProgrammaticMoveRef.current = false;
|
||||
}, 100);
|
||||
hasInteractedRef.current = true;
|
||||
}
|
||||
},
|
||||
(err) => console.error("Cannot get user location:", err),
|
||||
(err) => {
|
||||
setIsLocatingUser(false);
|
||||
console.error("Cannot get user location:", err);
|
||||
},
|
||||
{ enableHighAccuracy: true }
|
||||
);
|
||||
}
|
||||
@@ -362,18 +406,13 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
|
||||
{/* Locate User Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLocatingUser(!isLocatingUser);
|
||||
if (!isLocatingUser) {
|
||||
centerOnUser();
|
||||
}
|
||||
}}
|
||||
onClick={centerOnUser}
|
||||
className={`absolute bottom-8 left-6 z-[999] w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
|
||||
isLocatingUser
|
||||
? 'bg-blue-600 border-blue-400 text-white animate-pulse'
|
||||
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-blue-400'
|
||||
}`}
|
||||
title="Vị trí của tôi"
|
||||
title="Định vị vị trí hiện tại của bạn"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
@@ -381,6 +420,22 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* FLOATING LIVE GPS TRACKING CENTER LOCK BUTTON */}
|
||||
<button
|
||||
onClick={() => setIsTrackingLocation(!isTrackingLocation)}
|
||||
className={`absolute bottom-24 right-6 z-[999] w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
|
||||
isTrackingLocation
|
||||
? 'bg-green-600 border-green-400 text-white animate-pulse'
|
||||
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-green-500'
|
||||
}`}
|
||||
title={isTrackingLocation ? "Tắt tự động định tâm vị trí" : "Bật tự động định tâm theo vị trí của bạn"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsCompassActive(!isCompassActive)}
|
||||
className={`absolute bottom-8 right-6 z-[999] w-14 h-14 rounded-full border-2 flex items-center justify-center shadow-2xl transition-all active:scale-95 ${
|
||||
|
||||
Reference in New Issue
Block a user