smarteta
Version:
Smart urban ETA estimator based on time, distance, vehicle type and real-world corrections
96 lines (80 loc) • 3.48 kB
text/typescript
// smart_eta/src/index.ts
export type VehicleType = 'car' | 'scooter';
export interface HistoricalEntry {
fromLat: number;
fromLng: number;
toLat: number;
toLng: number;
actualMinutes: number;
}
export interface EstimateInput {
startLat: number;
startLng: number;
endLat: number;
endLng: number;
vehicleType: VehicleType;
now?: Date;
historicalData?: HistoricalEntry[];
urbanOverride?: boolean;
}
export interface EstimateResult {
min: number;
avg: number;
max: number;
}
// Helper function for Haversine distance calculation
function haversineDistance(coords1: [number, number], coords2: [number, number]): number {
const R = 6371; // Earth's radius in kilometers
const dLat = (coords2[0] - coords1[0]) * Math.PI / 180;
const dLon = (coords2[1] - coords1[1]) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(coords1[0] * Math.PI / 180) * Math.cos(coords2[0] * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in km
}
export function estimateSmartETA(input: EstimateInput): EstimateResult {
// Helper function for additional delays
function extraDelay(vehicleType: VehicleType, isUrban: boolean, distanceKm: number): number {
if (isUrban && distanceKm < 6) return vehicleType === 'car' ? 4 : 2; // For short urban trips
return vehicleType === 'car' ? 1 : 0.5; // Reduced for inter-city to avoid overestimation
}
// 1. Input reception
const start: [number, number] = [input.startLat, input.startLng];
const end: [number, number] = [input.endLat, input.endLng];
const vehicleType = input.vehicleType; // 'car' or 'scooter'
const now = input.now instanceof Date ? input.now : new Date();
const urbanOverride = input.urbanOverride;
// 2. Calculate air distance in km
const distanceKm = haversineDistance(start, end);
// 3. Determine if urban trip
const isUrban = typeof urbanOverride === 'boolean' ? urbanOverride : distanceKm < 15;
// 4. Determine base speed based on distance and time of day
const hour = now.getHours() + now.getMinutes() / 60;
let baseSpeed: number;
if (distanceKm > 40) {
// Tuned for ~47 km (Be'er Sheva to Ashdod) to take ~60 minutes
baseSpeed = vehicleType === 'car' ? 48 : 55; // Adjusted from 45 to 48 km/h
} else if (distanceKm < 6) {
// Tuned for ~5 km (within Be'er Sheva) to take ~15 minutes
baseSpeed = vehicleType === 'car' ? 20 : 25;
} else {
// Fallback for other distances
baseSpeed = vehicleType === 'car' ? 30 : 40;
}
// 5. Day-of-week correction (Wednesday/Thursday busier)
const day = now.getDay(); // 0 = Sunday, ..., 6 = Saturday
const speedCorrectionFactor = day === 3 || day === 4 ? 0.95 : 1.0;
const correctedSpeed = baseSpeed * speedCorrectionFactor;
// 6. Calculate base ETA
let baseETA = (distanceKm / correctedSpeed) * 60 + extraDelay(vehicleType, isUrban, distanceKm);
// 7. Ensure minimum time
baseETA = Math.max(baseETA, 2);
// 8. Add deviation range
return {
min: Math.round(Math.max(baseETA * 0.9, 2) * 10) / 10,
avg: Math.round(baseETA * 10) / 10,
max: Math.round(Math.max(baseETA * 1.1, 2.1) * 10) / 10
};
}