fix: sửa lại các tuyến đường đề xuất trên bản đồ
This commit is contained in:
@@ -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
|
// Định nghĩa kiểu dữ liệu cho Địa điểm để khớp với Schema Prisma
|
||||||
type LocationType = 'MOVE' | 'VISIT' | 'REST' | 'EAT';
|
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
|
// Fix lỗi icon mặc định của Leaflet cho môi trường Vite
|
||||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||||
L.Icon.Default.mergeOptions({
|
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
|
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
|
// 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 MapTourBounds = ({ locations }: { locations: any[] }) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
@@ -524,16 +591,30 @@ export const TourDetailPage = ({
|
|||||||
const allLocations = useMemo(() => legs.flatMap(l => l.locations), [legs]);
|
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
|
// 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 routeKey = useMemo(() => JSON.stringify({
|
||||||
const locsKey = allLocations.map(l => `${l.id}-${l.latitude}-${l.longitude}`).join('|');
|
locations: allLocations.map(l => ({ id: l.id, lat: l.latitude, lon: l.longitude })),
|
||||||
const userKey = userLocation ? `${userLocation[0].toFixed(5)}-${userLocation[1].toFixed(5)}` : 'no-user';
|
user: userLocation ? { lat: userLocation[0].toFixed(5), lon: userLocation[1].toFixed(5) } : null,
|
||||||
return `${locsKey}-${userKey}`;
|
travelMode: travelMode,
|
||||||
}, [allLocations, userLocation]);
|
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 đồ
|
// 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(() => {
|
useEffect(() => {
|
||||||
const fetchRoutes = async () => {
|
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([]);
|
setRoutes([]);
|
||||||
setSelectedRouteIndex(0);
|
setSelectedRouteIndex(0);
|
||||||
setDrivingRoute([]);
|
setDrivingRoute([]);
|
||||||
@@ -552,16 +633,64 @@ export const TourDetailPage = ({
|
|||||||
|
|
||||||
setIsRoutingLoading(true);
|
setIsRoutingLoading(true);
|
||||||
try {
|
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 segmentPromises: Promise<OSRMRoute[] | null>[] = [];
|
||||||
const response = await fetch(`https://router.project-osrm.org/route/v1/${travelMode}/${coordsString}?overview=full&geometries=geojson&alternatives=3`);
|
for (let i = 0; i < currentPoints.length - 1; i++) {
|
||||||
const data = await response.json();
|
const p1 = currentPoints[i];
|
||||||
if (data.code === 'Ok' && data.routes.length > 0) {
|
const p2 = currentPoints[i + 1];
|
||||||
setRoutes(data.routes);
|
const coordsString = `${p1.longitude},${p1.latitude};${p2.longitude},${p2.latitude}`;
|
||||||
// Reset về lộ trình đầu tiên khi danh sách điểm đến thay đổi hoàn toàn
|
|
||||||
setSelectedRouteIndex(0);
|
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) {
|
} catch (error) {
|
||||||
console.error(`Lỗi lấy lộ trình ${travelMode}:`, error);
|
console.error(`Lỗi lấy lộ trình ${travelMode}:`, error);
|
||||||
|
setRoutes([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRoutingLoading(false);
|
setIsRoutingLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user