diff --git a/frontend/src/pages/TourDetailPage.tsx b/frontend/src/pages/TourDetailPage.tsx index 3f513fb..490c86f 100644 --- a/frontend/src/pages/TourDetailPage.tsx +++ b/frontend/src/pages/TourDetailPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; import { io } from 'socket.io-client'; import { ItineraryTimeline } from '../components/ItineraryTimeline'; import { ExpenseManager } from '../components/ExpenseManager'; @@ -166,14 +166,15 @@ const RecenterUser = ({ position, trigger }: { position: [number, number] | null }, [trigger, position, map]); return null; }; -// Component Helper để xử lý xoay bản đồ theo hướng di chuyển hoặc hướng Bắc + +// Component Helper để xử lý xoay bản đồ theo hướng di chuyển const MapRotationHandler = ({ rotation }: { rotation: number }) => { const map = useMap(); useEffect(() => { const container = map.getContainer(); - // Xoay container bản đồ và scale nhẹ để tránh lộ khoảng trắng ở các góc khi xoay - container.style.transform = `rotate(${rotation}deg) scale(${rotation === 0 ? 1 : 1.2})`; - container.style.transition = 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)'; + // Tăng scale lên 1.8 để đảm bảo bao phủ toàn bộ màn hình điện thoại (vốn có tỉ lệ dài) khi xoay ở mọi góc độ + container.style.transform = `rotate(${rotation}deg) scale(${rotation === 0 ? 1 : 1.8})`; + container.style.transition = 'transform 0.3s ease-out'; }, [rotation, map]); return null; }; @@ -314,7 +315,20 @@ export const TourDetailPage = ({ const publicTours = useTourStore(state => state.publicTours); const userRole = useTourStore(state => state.userRole); const mapCenter = useTourStore(state => state.mapCenter); - const [userLocation, setUserLocation] = useState<[number, number] | null>(null); + const [userLocation, setUserLocation] = useState<[number, number] | null>(null); // Vị trí hiện tại của người dùng + const [gpsHeading, setGpsHeading] = useState(null); // Hướng di chuyển từ GPS + const [userSpeed, setUserSpeed] = useState(null); // Tốc độ di chuyển từ GPS + + // Di chuyển khai báo state lên trên useEffect để tránh lỗi "before initialization" + const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan'); + const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline'); + const [isHeadingMode, setIsHeadingMode] = useState(false); + const [mapRotation, setMapRotation] = useState(0); + const [isAddLocationOpen, setIsAddLocationOpen] = useState(false); + const [isMapFullscreen, setIsMapFullscreen] = useState(false); + const [isAddMemberOpen, setIsAddMemberOpen] = useState(false); + const [isAddPhotoOpen, setIsAddPhotoOpen] = useState(false); + // Định nghĩa Icons bên trong Component bằng useMemo để đảm bảo tính ổn định và tránh lỗi render const mapIcons = useMemo(() => ({ @@ -350,22 +364,82 @@ export const TourDetailPage = ({ }), []); useEffect(() => { + // Theo dõi vị trí GPS của người dùng (bao gồm hướng và tốc độ khi di chuyển) if (!isPublicView && navigator.geolocation) { const watchId = navigator.geolocation.watchPosition( - (pos) => setUserLocation([pos.coords.latitude, pos.coords.longitude]), + (pos) => { + setUserLocation([pos.coords.latitude, pos.coords.longitude]); + setGpsHeading(pos.coords.heading); + setUserSpeed(pos.coords.speed); + }, (err) => console.warn("Lỗi định vị người dùng:", err), { enableHighAccuracy: true } ); return () => navigator.geolocation.clearWatch(watchId); + } else { + setGpsHeading(null); + setUserSpeed(null); } - }, [isPublicView]); + }, [isPublicView]); // Chỉ phụ thuộc vào isPublicView + + // State mới cho hướng thiết bị (la bàn) + const [deviceOrientationHeading, setDeviceOrientationHeading] = useState(null); + const rotationUpdateTimer = useRef(null); + + // Theo dõi hướng thiết bị (la bàn) + useEffect(() => { + if (window.DeviceOrientationEvent) { + const handleDeviceOrientation = (event: DeviceOrientationEvent) => { + // Sử dụng webkitCompassHeading cho iOS nếu có, nếu không thì dùng alpha + const heading = (event as any).webkitCompassHeading || event.alpha; + if (heading !== null && heading !== undefined) { + setDeviceOrientationHeading(heading); + } + }; + + window.addEventListener('deviceorientation', handleDeviceOrientation); + return () => { + window.removeEventListener('deviceorientation', handleDeviceOrientation); + }; + } else { + setDeviceOrientationHeading(null); + } + }, []); // Chạy một lần khi component mount + + // Logic kết hợp để xác định hướng xoay bản đồ + useEffect(() => { + if (rotationUpdateTimer.current) { + clearTimeout(rotationUpdateTimer.current); + } + + if (!isHeadingMode) { + setMapRotation(0); // Đặt lại hướng Bắc nếu chế độ xoay tắt + return; + } + + let newRotation: number | null = null; + // Ưu tiên hướng GPS nếu có và người dùng đang di chuyển (tốc độ > 0.5 m/s) + if (gpsHeading !== null && userSpeed !== null && userSpeed > 0.5) { + newRotation = -gpsHeading; + } else if (deviceOrientationHeading !== null) { + // Nếu không di chuyển hoặc không có hướng GPS, dùng hướng thiết bị + newRotation = -deviceOrientationHeading; + } + + if (newRotation !== null) { + // Debounce cập nhật xoay để làm mượt chuyển động + rotationUpdateTimer.current = setTimeout(() => { + setMapRotation(newRotation!); + }, 100); // Trì hoãn 100ms + } + + return () => { + if (rotationUpdateTimer.current) { + clearTimeout(rotationUpdateTimer.current); + } + }; + }, [isHeadingMode, gpsHeading, userSpeed, deviceOrientationHeading]); - const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan'); - const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline'); - const [isAddLocationOpen, setIsAddLocationOpen] = useState(false); - const [isMapFullscreen, setIsMapFullscreen] = useState(false); - const [isAddMemberOpen, setIsAddMemberOpen] = useState(false); - const [isAddPhotoOpen, setIsAddPhotoOpen] = useState(false); const [isStartPointAction, setIsStartPointAction] = useState(false); const [isEndPointAction, setIsEndPointAction] = useState(false); const [targetLegId, setTargetLegId] = useState(null); @@ -431,8 +505,6 @@ export const TourDetailPage = ({ const [isSearching, setIsSearching] = useState(false); const [locateTrigger, setLocateTrigger] = useState(0); const [isMapControlsOpen, setIsMapControlsOpen] = useState(false); - const [isHeadingMode, setIsHeadingMode] = useState(false); - const [mapRotation, setMapRotation] = useState(0); const [isRoutingLoading, setIsRoutingLoading] = useState(false); const [travelMode, setTravelMode] = useState<'driving' | 'bike' | 'foot'>('driving'); const [routes, setRoutes] = useState([]); @@ -1475,12 +1547,12 @@ export const TourDetailPage = ({ {/* Tự động đóng khung Start và End khi dữ liệu thay đổi */} + {/* Xử lý xoay bản đồ theo Heading */} + + {/* Xử lý di chuyển tâm bản đồ về phía người dùng */} - {/* Xử lý xoay bản đồ */} - - {/* Hiển thị vị trí hiện tại của người dùng */} {userLocation && ( @@ -1683,17 +1755,15 @@ export const TourDetailPage = ({ {/* Nút La bàn / Xoay bản đồ */} {/* Nút Tìm tôi */} @@ -2094,7 +2164,7 @@ export const TourDetailPage = ({ {/* Fullscreen Map Overlay - Hiển thị khi click vào nút trạng thái/chỉ đường */} {isMapFullscreen && ( -
+
{/* Nút X để quay lại */} +
)}