From e664a3797e80fa15033cb5b9088869db1ad868ed Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 17 Jun 2026 18:00:45 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20th=C3=AAm=20t=C3=ADnh=20n=C4=83ng=20?= =?UTF-8?q?=C4=91=C6=B0=E1=BB=9Dng=20d=C3=A0nh=20cho=20xe=20m=C3=A1y=20v?= =?UTF-8?q?=C3=A0=20=C4=91i=20b=E1=BB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/TourDetailPage.tsx | 188 ++++++++++++++++++++++---- 1 file changed, 164 insertions(+), 24 deletions(-) diff --git a/frontend/src/pages/TourDetailPage.tsx b/frontend/src/pages/TourDetailPage.tsx index 8aa88ea..d3bf435 100644 --- a/frontend/src/pages/TourDetailPage.tsx +++ b/frontend/src/pages/TourDetailPage.tsx @@ -28,6 +28,9 @@ import { MapPin, Search, Loader2, + Car, + Bike, + Footprints, Flag, Clock, Check, @@ -270,6 +273,11 @@ export const TourDetailPage = ({ const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); + const [isRoutingLoading, setIsRoutingLoading] = useState(false); + const [travelMode, setTravelMode] = useState<'driving' | 'bike' | 'foot'>('driving'); + const [routes, setRoutes] = useState([]); + const [selectedRouteIndex, setSelectedRouteIndex] = useState(0); + const [routeMenu, setRouteMenu] = useState<{ x: number, y: number, index: number } | null>(null); const [drivingRoute, setDrivingRoute] = useState<[number, number][]>([]); const [segmentDistances, setSegmentDistances] = useState([]); @@ -423,10 +431,15 @@ export const TourDetailPage = ({ // Memoize danh sách tọa độ để MapTourBounds không bị trigger thừa const allLocations = useMemo(() => legs.flatMap(l => l.locations), [legs]); - // Tự động tìm quãng đường di chuyển bằng ô tô thực tế và vẽ lên bản đồ + // Tạo key định danh cho lộ trình để buộc bản đồ vẽ lại khi dữ liệu thay đổi + const routeKey = useMemo(() => allLocations.map(l => `${l.id}-${l.latitude}-${l.longitude}`).join('|'), [allLocations]); + + // Tự động tìm các quãng đường di chuyển thực tế theo phương tiện và vẽ lên bản đồ useEffect(() => { - const fetchDrivingRoute = async () => { + const fetchRoutes = async () => { if (allLocations.length < 2) { + setRoutes([]); + setSelectedRouteIndex(0); setDrivingRoute([]); setSegmentDistances([]); return; @@ -436,23 +449,37 @@ export const TourDetailPage = ({ .map(loc => `${loc.longitude},${loc.latitude}`) .join(';'); + setIsRoutingLoading(true); try { - const response = await fetch(`https://router.project-osrm.org/route/v1/driving/${coordsString}?overview=full&geometries=geojson`); + // Thêm alternatives=3 để yêu cầu tối đa 3 phương án lộ trình khác từ OSRM + const response = await fetch(`https://router.project-osrm.org/route/v1/${travelMode}/${coordsString}?overview=full&geometries=geojson&alternatives=3`); const data = await response.json(); if (data.code === 'Ok' && data.routes.length > 0) { - // OSRM trả về [lng, lat], cần đổi sang [lat, lng] cho Leaflet - const mappedCoords: [number, number][] = data.routes[0].geometry.coordinates.map((c: any) => [c[1], c[0]]); - setDrivingRoute(mappedCoords); - // Lưu quãng đường từng chặng (đơn vị km) - setSegmentDistances(data.routes[0].legs.map((leg: any) => leg.distance / 1000)); + setRoutes(data.routes); + // Reset về lộ trình đầu tiên khi danh sách điểm đến thay đổi hoàn toàn + setSelectedRouteIndex(0); } } catch (error) { - console.error("Lỗi lấy lộ trình di chuyển ô tô:", error); + console.error(`Lỗi lấy lộ trình ${travelMode}:`, error); + } finally { + setIsRoutingLoading(false); } }; - fetchDrivingRoute(); - }, [allLocations]); + fetchRoutes(); + }, [allLocations, travelMode]); + + // Cập nhật dữ liệu lộ trình hiển thị khi người dùng chọn phương án khác + useEffect(() => { + if (routes.length > 0 && routes[selectedRouteIndex]) { + const route = routes[selectedRouteIndex]; + // OSRM trả về [lng, lat], cần đổi sang [lat, lng] cho Leaflet + const mappedCoords: [number, number][] = route.geometry.coordinates.map((c: any) => [c[1], c[0]]); + setDrivingRoute(mappedCoords); + // Lưu quãng đường từng chặng của lộ trình được chọn + setSegmentDistances(route.legs.map((leg: any) => leg.distance / 1000)); + } + }, [selectedRouteIndex, routes]); useEffect(() => { if (initialViewState) { @@ -1128,21 +1155,45 @@ export const TourDetailPage = ({ preferCanvas={true} > - {canEdit && !isPublicView && } {/* Hide map context menu in public view */} + {canEdit && !isPublicView && setRouteMenu(null)} />} {/* Hide map context menu in public view */} {/* Tự động đóng khung Start và End khi dữ liệu thay đổi */} - {/* Vẽ đường đi ô tô thực tế (Driving route) nếu lấy được dữ liệu, nếu không dùng đường thẳng nét đứt */} - {drivingRoute.length > 0 ? ( - + {/* Vẽ tất cả lộ trình: Vẽ các đường phụ trước, đường chính sau để hiển thị đè lên trên */} + {routes.length > 0 ? ( + [...routes] + .map((r, i) => ({ data: r, index: i })) + .sort((a, b) => { + if (a.index === selectedRouteIndex) return 1; + if (b.index === selectedRouteIndex) return -1; + return 0; + }) + .map(({ data, index }) => ( + [c[1], c[0]])} + color={index === selectedRouteIndex ? "#2563eb" : "#94a3b8"} + weight={index === selectedRouteIndex ? 6 : 4} + opacity={index === selectedRouteIndex ? 0.9 : 0.4} + dashArray={index === selectedRouteIndex ? undefined : "10, 10"} + smoothFactor={1} + eventHandlers={{ + click: (e) => { + L.DomEvent.stopPropagation(e as any); + setRouteMenu(null); + setSelectedRouteIndex(index); + }, + contextmenu: (e) => { + L.DomEvent.stopPropagation(e as any); + L.DomEvent.preventDefault(e as any); + setRouteMenu({ x: (e as any).containerPoint.x, y: (e as any).containerPoint.y, index }); + } + }} + /> + )) ) : allLocations.length > 1 && ( + /* Vẽ đường thẳng nét đứt nếu không lấy được dữ liệu lộ trình thực tế */ [l.latitude, l.longitude]) as any} color="#3b82f6" @@ -1201,7 +1252,11 @@ export const TourDetailPage = ({
- {drivingDist ? 'Đường bộ từ điểm trước' : 'Khoảng cách chim bay'} + + {drivingDist + ? `${travelMode === 'driving' ? 'Đường ô tô' : travelMode === 'bike' ? 'Đường xe máy' : 'Đường đi bộ'} từ điểm trước` + : 'Khoảng cách chim bay'} + {drivingDist ? drivingDist.toFixed(1) : distanceToPrev} km
@@ -1213,8 +1268,93 @@ export const TourDetailPage = ({ })} -
- {isPublicView ? 'Xem chi tiết lộ trình' : 'Mẹo: Nhấn giữ (Mobile) hoặc Chuột phải để ghim địa điểm'} + + {/* Menu ngữ cảnh khi click chuột phải vào con đường */} + {routeMenu && ( +
e.stopPropagation()} + > + +
+ )} + + {/* Overlay điều khiển trên bản đồ */} +
+
+ + + +
+ + {/* Chỉ báo đang tìm đường */} + {isRoutingLoading && ( +
+ + Đang tìm đường tối ưu... +
+ )} + + {/* Danh sách lựa chọn lộ trình đề xuất */} + {routes.length > 1 && ( +
+
Lộ trình đề xuất
+
+ {routes.map((route, idx) => ( + + ))} +
+
+ )} + +
+ {isPublicView ? 'Xem chi tiết lộ trình' : 'Mẹo: Nhấn giữ hoặc Chuột phải để ghim'} +
)}