canvas-website/src/open-mapping/services/OptimizationService.ts

88 lines
4.1 KiB
TypeScript

/**
* OptimizationService - Route optimization using VROOM
*/
import type { Waypoint, Coordinate, OptimizationServiceConfig } from '../types';
export interface OptimizationResult {
orderedWaypoints: Waypoint[];
totalDistance: number;
totalDuration: number;
estimatedCost: { fuel: number; time: number; total: number; currency: string };
}
export interface CostParameters {
fuelPricePerLiter: number;
fuelConsumptionPer100km: number;
valueOfTimePerHour: number;
currency: string;
}
const DEFAULT_COST_PARAMS: CostParameters = { fuelPricePerLiter: 1.5, fuelConsumptionPer100km: 8, valueOfTimePerHour: 20, currency: 'EUR' };
export class OptimizationService {
private config: OptimizationServiceConfig;
private costParams: CostParameters;
constructor(config: OptimizationServiceConfig, costParams = DEFAULT_COST_PARAMS) {
this.config = config;
this.costParams = costParams;
}
async optimizeRoute(waypoints: Waypoint[]): Promise<OptimizationResult> {
if (waypoints.length <= 2) return { orderedWaypoints: waypoints, totalDistance: 0, totalDuration: 0, estimatedCost: { fuel: 0, time: 0, total: 0, currency: this.costParams.currency } };
if (this.config.provider === 'vroom') {
return this.optimizeWithVROOM(waypoints);
}
return this.nearestNeighbor(waypoints);
}
estimateCosts(distance: number, duration: number) {
const km = distance / 1000, hours = duration / 3600;
const fuel = (km / 100) * this.costParams.fuelConsumptionPer100km * this.costParams.fuelPricePerLiter;
const time = hours * this.costParams.valueOfTimePerHour;
return { fuel: Math.round(fuel * 100) / 100, time: Math.round(time * 100) / 100, total: Math.round((fuel + time) * 100) / 100, currency: this.costParams.currency };
}
private async optimizeWithVROOM(waypoints: Waypoint[]): Promise<OptimizationResult> {
const jobs = waypoints.map((wp, i) => ({ id: i, location: [wp.coordinate.lng, wp.coordinate.lat] }));
const vehicles = [{ id: 0, start: [waypoints[0].coordinate.lng, waypoints[0].coordinate.lat] }];
try {
const res = await fetch(this.config.baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jobs, vehicles }) });
const data = await res.json() as { code: number; error?: string; routes: { steps: { type: string; job: number }[] }[]; summary: { distance: number; duration: number } };
if (data.code !== 0) throw new Error(data.error);
const indices = data.routes[0].steps.filter((s) => s.type === 'job').map((s) => s.job);
return { orderedWaypoints: indices.map((i: number) => waypoints[i]), totalDistance: data.summary.distance, totalDuration: data.summary.duration, estimatedCost: this.estimateCosts(data.summary.distance, data.summary.duration) };
} catch { return this.nearestNeighbor(waypoints); }
}
private nearestNeighbor(waypoints: Waypoint[]): OptimizationResult {
const remaining = [...waypoints], ordered: Waypoint[] = [];
let current = remaining.shift()!;
ordered.push(current);
while (remaining.length) {
let nearest = 0, minDist = Infinity;
for (let i = 0; i < remaining.length; i++) {
const d = this.haversine(current.coordinate, remaining[i].coordinate);
if (d < minDist) { minDist = d; nearest = i; }
}
current = remaining.splice(nearest, 1)[0];
ordered.push(current);
}
let dist = 0;
for (let i = 0; i < ordered.length - 1; i++) dist += this.haversine(ordered[i].coordinate, ordered[i + 1].coordinate);
const dur = (dist / 50000) * 3600;
return { orderedWaypoints: ordered, totalDistance: dist, totalDuration: dur, estimatedCost: this.estimateCosts(dist, dur) };
}
private haversine(a: Coordinate, b: Coordinate): number {
const R = 6371000, lat1 = (a.lat * Math.PI) / 180, lat2 = (b.lat * Math.PI) / 180;
const dLat = ((b.lat - a.lat) * Math.PI) / 180, dLng = ((b.lng - a.lng) * Math.PI) / 180;
const x = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));
}
}
export default OptimizationService;