feat: Thêm nút định vị người dùng trên bản đồ điều hướng mobile
- Thêm nút 'Vị trí của tôi' ở góc dưới bên trái bản đồ - Khi bật: map tự động center theo vị trí GPS của người dùng (dùng watchPosition) - Cho phép thu phóng tự do ngay cả khi đang định vị - Khi người dùng kéo thả bản đồ thủ công: tự động tắt chế độ định vị - Nút hoạt động độc lập với nút la bàn (compass) - Sửa import leaflet.css bị lỗi path
This commit is contained in:
@@ -114,8 +114,6 @@ const MapSizeHandler = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | nul
|
||||
|
||||
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 prevZoom = map.getZoom();
|
||||
const prevCenter = map.getCenter();
|
||||
const userZoom = (map as any)._userZoomLevel as number | undefined;
|
||||
|
||||
map.invalidateSize({ animate: false });
|
||||
@@ -135,6 +133,7 @@ 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 [currentHeading, setCurrentHeading] = useState(0);
|
||||
const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -149,6 +148,49 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
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);
|
||||
@@ -245,9 +287,9 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
|
||||
const destIcon = useMemo(() => L.divIcon({
|
||||
className: '!bg-transparent !border-none',
|
||||
html: `<div class="w-8 h-8 bg-red-600 rounded-full border-2 border-white shadow-xl flex items-center justify-center text-white text-xs font-black">Đ</div>`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16]
|
||||
html: `<div class="w-10 h-10 bg-red-600 rounded-full border-2 border-white shadow-xl flex items-center justify-center text-white text-xs font-black">Đ</div>`,
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20]
|
||||
}), []);
|
||||
|
||||
if (!routeData) return null;
|
||||
@@ -256,6 +298,22 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
? [(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 (
|
||||
<div className="fixed inset-0 bg-slate-950 flex flex-col overflow-hidden select-none text-white antialiased">
|
||||
<div className="w-full bg-[#1e293b]/95 backdrop-blur-md border-b border-slate-800 px-4 py-3.5 flex items-center gap-3 z-50 shrink-0">
|
||||
@@ -289,7 +347,7 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution="" />
|
||||
<MapRefSetter mapRef={mapRef} />
|
||||
<MapInteractionWatcher />
|
||||
<FitBounds coords={routeGeometry || []} canFitRef={canFitRef} />
|
||||
{/* <FitBounds coords={routeGeometry || []} canFitRef={canFitRef} /> */}
|
||||
{routeGeometry && (
|
||||
<Polyline positions={routeGeometry} color="#2563eb" weight={6} opacity={0.9} smoothFactor={1} />
|
||||
)}
|
||||
@@ -298,6 +356,27 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
||||
<CompassInteractionDetector onUserInteraction={handleUserInteraction} />
|
||||
</MapContainer>
|
||||
|
||||
{/* Locate User Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLocatingUser(!isLocatingUser);
|
||||
if (!isLocatingUser) {
|
||||
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"
|
||||
>
|
||||
<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" />
|
||||
<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