/** * Driving Distance, Duration & Route Polyline Calculator for DoorMile * Uses OSRM as primary routing engine with GeoJSON polyline extraction, * and falls back seamlessly to Haversine with a 1.3 road winding multiplier. */ import { coordsOf } from '@/lib/coords'; let lastRouteDurationMin = null; export const getLastRouteDurationMin = () => lastRouteDurationMin; /** * Calculates complete driving route with distance, duration, and road polyline * @param {Object} origin - { latitude, longitude } * @param {Object} destination - { latitude, longitude } * @returns {Promise<{ distance: number, minutes: number, polyline: Array<[number, number]>, resolved: boolean }>} */ export const calculateDrivingRoute = async (origin, destination) => { // Validated through lib/coords rather than with Number.isFinite. // // The previous guard was `Number.isFinite(Number(value))`, which accepts // null, '' and false — all three coerce to 0, and 0 is finite. A missing pin // therefore reached OSRM as (0, 0). OSRM answers HTTP 200 with no `routes` // for a point in open ocean, so control fell through to the Haversine // fallback below and a Coimbatore pickup measured against 0,0 returned // roughly 11,000 km. That number becomes the row's charge and the backend // honours it verbatim, so the guard being wrong here was a billing defect, // not a display one. const from = coordsOf(origin); const to = coordsOf(destination); if (!from || !to) { throw new Error("Invalid coordinates"); } const lat1 = from.latitude; const lon1 = from.longitude; const lat2 = to.latitude; const lon2 = to.longitude; // 1. Try OSRM API with full GeoJSON geometry for live polyline map preview const osrmBaseUrl = (typeof import.meta !== 'undefined' && import.meta?.env?.VITE_OSRM_URL) || 'https://router.project-osrm.org'; try { const url = `${osrmBaseUrl}/route/v1/driving/${lon1},${lat1};${lon2},${lat2}?overview=full&geometries=geojson`; const response = await fetch(url); if (response.ok) { const data = await response.json(); if (data.routes && data.routes.length > 0) { const route = data.routes[0]; const distanceKm = Math.round(route.distance / 1000); const durationMin = Number.isFinite(route.duration) ? Math.round(route.duration / 60) : null; // OSRM geojson coordinates are [lng, lat] -> convert to Leaflet [lat, lng] const polyline = (route.geometry?.coordinates || []).map(([lng, lat]) => [lat, lng]); lastRouteDurationMin = durationMin; return { distance: Math.max(0, distanceKm), minutes: durationMin, polyline: polyline.length > 0 ? polyline : [[lat1, lon1], [lat2, lon2]], resolved: true }; } } } catch (error) { console.warn("OSRM API failed, falling back to Haversine math:", error); } // 2. Fallback: Haversine Formula with 1.3x road factor const toRad = (val) => (val * Math.PI) / 180; const R = 6371; // Earth radius in km const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const aerialDistance = Math.round(R * c * 1.3); lastRouteDurationMin = null; return { distance: aerialDistance, minutes: null, polyline: [[lat1, lon1], [lat2, lon2]], resolved: true }; }; /** * Calculates distance (in km) between origin and destination */ export const calculateDrivingDistance = async (origin, destination) => { const res = await calculateDrivingRoute(origin, destination); return res.distance; }; /** * Calculates total charge based on distance and pricing tier */ export const calculateTotalCharge = (distanceKm, basePrice, pricePerKm, minKm) => { const dist = Number(distanceKm) || 0; const base = Number(basePrice) || 40; const perKm = Number(pricePerKm) || 10; const min = Number(minKm) || 2; if (dist <= min) { return base; } return (dist - min) * perKm + base; };