From 69c67a16363706d34fbb62a533b28b392a21df8c Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 17 Jun 2026 20:49:30 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20s=E1=BB=ADa=20l=E1=BA=A1i=20c=C3=A1c=20t?= =?UTF-8?q?uy=E1=BA=BFn=20=C4=91=C6=B0=E1=BB=9Dng=20=C4=91=E1=BB=81=20xu?= =?UTF-8?q?=E1=BA=A5t=20tr=C3=AAn=20b=E1=BA=A3n=20=C4=91=E1=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/TourDetailPage.tsx | 155 +++++++++++++++++++++++--- 1 file changed, 142 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/TourDetailPage.tsx b/frontend/src/pages/TourDetailPage.tsx index 8dc35b3..475d448 100644 --- a/frontend/src/pages/TourDetailPage.tsx +++ b/frontend/src/pages/TourDetailPage.tsx @@ -49,6 +49,26 @@ import L from 'leaflet'; // Định nghĩa kiểu dữ liệu cho Địa điểm để khớp với Schema Prisma type LocationType = 'MOVE' | 'VISIT' | 'REST' | 'EAT'; +// Định nghĩa kiểu dữ liệu cho OSRM Route +interface OSRMRoute { + geometry: { + coordinates: [number, number][]; // [lng, lat] + }; + distance: number; // meters + duration: number; // seconds + legs: { distance: number; duration: number; }[]; // OSRM's internal legs for a route +} + +// Định nghĩa kiểu dữ liệu cho điểm (bao gồm cả userLocation khi được chuyển đổi) +interface LocationPoint { + latitude: number; + longitude: number; + id: string; + name: string; + type: string; + legId: string; +} + // Fix lỗi icon mặc định của Leaflet cho môi trường Vite delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ @@ -69,6 +89,53 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km } +// Helper function to combine segment routes into a single overall route +const combineSegmentRoutes = (segmentRoutes: OSRMRoute[][], selectedIndices: number[]): OSRMRoute | null => { + if (segmentRoutes.length === 0) return null; + + let combinedGeometry: [number, number][] = []; + let combinedDistance = 0; + let combinedDuration = 0; + const combinedLegs: { distance: number; duration: number; }[] = []; + + for (let i = 0; i < segmentRoutes.length; i++) { + const segmentIndex = selectedIndices[i] !== undefined ? selectedIndices[i] : 0; // Default to first alternative + const chosenRoute = segmentRoutes[i][segmentIndex]; + + if (!chosenRoute) { + // If a segment has no chosen route (e.g., no alternatives or API failed), + // we cannot form a complete route. + return null; + } + + // Concatenate geometry, avoiding duplicate points at segment junctions + if (i > 0 && combinedGeometry.length > 0 && chosenRoute.geometry.coordinates.length > 0) { + const lastPointOfPrev = combinedGeometry[combinedGeometry.length - 1]; + const firstPointOfCurrent = chosenRoute.geometry.coordinates[0]; + // OSRM coordinates are [lng, lat] + if (Math.abs(lastPointOfPrev[0] - firstPointOfCurrent[0]) < 1e-6 && + Math.abs(lastPointOfPrev[1] - firstPointOfCurrent[1]) < 1e-6) { + combinedGeometry = combinedGeometry.concat(chosenRoute.geometry.coordinates.slice(1)); + } else { + combinedGeometry = combinedGeometry.concat(chosenRoute.geometry.coordinates); + } + } else { + combinedGeometry = combinedGeometry.concat(chosenRoute.geometry.coordinates); + } + + combinedDistance += chosenRoute.distance; + combinedDuration += chosenRoute.duration; + combinedLegs.push(...chosenRoute.legs); // OSRM legs are sub-segments within a route + } + + return { + geometry: { coordinates: combinedGeometry }, + distance: combinedDistance, + duration: combinedDuration, + legs: combinedLegs, + }; +}; + // Component Helper để tự động điều chỉnh khung nhìn bản đồ bao phủ toàn bộ lộ trình const MapTourBounds = ({ locations }: { locations: any[] }) => { const map = useMap(); @@ -524,16 +591,30 @@ export const TourDetailPage = ({ const allLocations = useMemo(() => legs.flatMap(l => l.locations), [legs]); // 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(() => { - const locsKey = allLocations.map(l => `${l.id}-${l.latitude}-${l.longitude}`).join('|'); - const userKey = userLocation ? `${userLocation[0].toFixed(5)}-${userLocation[1].toFixed(5)}` : 'no-user'; - return `${locsKey}-${userKey}`; - }, [allLocations, userLocation]); + const routeKey = useMemo(() => JSON.stringify({ + locations: allLocations.map(l => ({ id: l.id, lat: l.latitude, lon: l.longitude })), + user: userLocation ? { lat: userLocation[0].toFixed(5), lon: userLocation[1].toFixed(5) } : null, + travelMode: travelMode, + selectedRouteIndex: selectedRouteIndex // Include selectedRouteIndex to force re-render when alternative is chosen + }), [allLocations, userLocation, travelMode, selectedRouteIndex]); // 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 fetchRoutes = async () => { - if (allLocations.length === 0 || (allLocations.length < 2 && !userLocation)) { + // Prepare points: userLocation + allLocations + const currentPoints: LocationPoint[] = [...allLocations]; + if (userLocation) { + currentPoints.unshift({ + latitude: userLocation[0], + longitude: userLocation[1], + id: 'user-location', // Dummy ID + name: 'Vị trí hiện tại', // Dummy name + type: 'MOVE', // Dummy type + legId: '', // Dummy legId + }); + } + + if (currentPoints.length < 2) { setRoutes([]); setSelectedRouteIndex(0); setDrivingRoute([]); @@ -552,16 +633,64 @@ export const TourDetailPage = ({ setIsRoutingLoading(true); try { - // Yêu cầu tối đa 3 phương án lộ trình. Lưu ý: OSRM chỉ hỗ trợ alternatives cho hành trình 2 điểm. - 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) { - 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); + const segmentPromises: Promise[] = []; + for (let i = 0; i < currentPoints.length - 1; i++) { + const p1 = currentPoints[i]; + const p2 = currentPoints[i + 1]; + const coordsString = `${p1.longitude},${p1.latitude};${p2.longitude},${p2.latitude}`; + + segmentPromises.push( + fetch(`https://router.project-osrm.org/route/v1/${travelMode}/${coordsString}?overview=full&geometries=geojson&alternatives=true`) + .then(res => { + if (!res.ok) throw new Error(`OSRM API error: ${res.status}`); + return res.json(); + }) + .then(data => { + if (data.code === 'Ok' && data.routes.length > 0) { + return data.routes; // Array of OSRMRoute for this segment + } + return null; // No routes for this segment + }) + .catch(error => { + console.error(`Lỗi lấy lộ trình cho phân đoạn ${i}-${i+1}:`, error); + return null; + }) + ); } + + const allSegmentAlternativesRaw = await Promise.all(segmentPromises); + const allSegmentAlternatives: OSRMRoute[][] = allSegmentAlternativesRaw.filter((seg): seg is OSRMRoute[] => seg !== null); + + if (allSegmentAlternatives.length === 0) { + setRoutes([]); + setSelectedRouteIndex(0); + return; + } + + // Now, construct overall alternative routes from segment alternatives + // Heuristic: Take the first alternative of each segment to form the primary route. + // Then, for subsequent overall alternatives, try different alternatives for the first segment, + // keeping other segments at their primary alternative. + + const overallAlternativeRoutes: OSRMRoute[] = []; + // Limit overall alternatives based on the number of alternatives for the first segment, up to 3 + const maxOverallAlternatives = Math.min(3, allSegmentAlternatives[0]?.length || 1); + + for (let altIdx = 0; altIdx < maxOverallAlternatives; altIdx++) { + const selectedSegmentIndices: number[] = allSegmentAlternatives.map((_, segIdx) => + segIdx === 0 ? altIdx : 0 // Use altIdx for first segment, 0 for others + ); + const combinedRoute = combineSegmentRoutes(allSegmentAlternatives, selectedSegmentIndices); + if (combinedRoute) { + overallAlternativeRoutes.push(combinedRoute); + } + } + setRoutes(overallAlternativeRoutes); + setSelectedRouteIndex(0); // Always select the first overall alternative by default + } catch (error) { console.error(`Lỗi lấy lộ trình ${travelMode}:`, error); + setRoutes([]); } finally { setIsRoutingLoading(false); }