UNPKG

@geoapify/route-planner-sdk

Version:

TypeScript SDK for the Geoapify Route Planner API. Supports route optimization, delivery planning, and timeline visualization in browser and Node.js

148 lines (147 loc) 7.42 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; const LOCATION_EPSILON = 1e-6; /** * Calculates optimal insertion points for new locations in existing routes. * Uses existing leg data only. */ export class InsertionCostCalculator { static findOptimalInsertionPoint(context, agentIndex, route, newLocation, options) { return __awaiter(this, void 0, void 0, function* () { if (route.length === 0) { return 0; } const insertionPositions = this.getInsertionPositions(route.length, (options === null || options === void 0 ? void 0 : options.canInsertBeforeFirst) || false, (options === null || options === void 0 ? void 0 : options.canInsertAfterLast) || false); if (insertionPositions.length === 1) { return insertionPositions[0]; } const travelTimeMap = this.buildTravelTimeMap((options === null || options === void 0 ? void 0 : options.travelTimes) || []); const consecutiveTimes = yield this.getConsecutiveTimes(context, agentIndex, route, travelTimeMap); let bestPosition = insertionPositions[0]; let minCost = Number.POSITIVE_INFINITY; for (const position of insertionPositions) { const cost = this.calculateInsertionCost(position, route, newLocation, travelTimeMap, consecutiveTimes); if (cost < minCost) { minCost = cost; bestPosition = position; } } return bestPosition; }); } static getConsecutiveTimes(context, agentIndex, routeLocations, travelTimeMap) { return __awaiter(this, void 0, void 0, function* () { if (routeLocations.length < 2) { return []; } const agentFeature = context.getAgentFeature(agentIndex); const legs = agentFeature.properties.legs || []; const waypointLocations = (agentFeature.properties.waypoints || []).map((waypoint) => waypoint.location || waypoint.original_location); const subRouteStartIndex = this.findSubRouteStartIndex(waypointLocations, routeLocations); const result = []; for (let i = 0; i < routeLocations.length - 1; i++) { const fromLocation = routeLocations[i]; const toLocation = routeLocations[i + 1]; if (this.sameLocation(fromLocation, toLocation)) { result.push(0); continue; } // 1. try to get from existing legs if (subRouteStartIndex !== -1) { const fromWaypointIndex = subRouteStartIndex + i; const toWaypointIndex = fromWaypointIndex + 1; const leg = legs.find((candidate) => candidate.from_waypoint_index === fromWaypointIndex && candidate.to_waypoint_index === toWaypointIndex); if (leg && typeof leg.time === "number" && leg.time >= 0) { result.push(leg.time); continue; } } // 2. try to get from matrix const key = this.getTravelTimeKey(fromLocation, toLocation); const time = travelTimeMap.get(key); if (time !== undefined) { result.push(time); continue; } // 3. get from RoutingHelper (single pair) const calculatedTimes = yield context.getRoutingHelper().calculateConsecutiveTravelTimes([fromLocation, toLocation]); const computedTime = calculatedTimes[0]; if (typeof computedTime !== "number") { throw new Error(`Unable to calculate travel time between ${fromLocation[0]},${fromLocation[1]} and ${toLocation[0]},${toLocation[1]}.`); } result.push(computedTime); } return result; }); } static findSubRouteStartIndex(waypointLocations, routeLocations) { if (routeLocations.length === 0 || waypointLocations.length < routeLocations.length) { return -1; } const maxStart = waypointLocations.length - routeLocations.length; for (let start = 0; start <= maxStart; start++) { let matches = true; for (let offset = 0; offset < routeLocations.length; offset++) { if (!this.sameLocation(waypointLocations[start + offset], routeLocations[offset])) { matches = false; break; } } if (matches) { return start; } } return -1; } static sameLocation(a, b) { return Math.abs(a[0] - b[0]) <= LOCATION_EPSILON && Math.abs(a[1] - b[1]) <= LOCATION_EPSILON; } static calculateInsertionCost(insertionPosition, route, newLocation, travelTimeMap, consecutiveTimes) { if (insertionPosition <= 0) { return this.getTravelTime(travelTimeMap, newLocation, route[0]); } if (insertionPosition >= route.length) { return this.getTravelTime(travelTimeMap, route[route.length - 1], newLocation); } const fromIndex = insertionPosition - 1; return this.getTravelTime(travelTimeMap, route[fromIndex], newLocation) + this.getTravelTime(travelTimeMap, newLocation, route[insertionPosition]) - consecutiveTimes[fromIndex]; } static buildTravelTimeMap(travelTimes) { const map = new Map(); for (const travelTime of travelTimes) { map.set(this.getTravelTimeKey(travelTime.locationFrom, travelTime.locationTo), travelTime.time); } return map; } static getTravelTime(travelTimeMap, locationFrom, locationTo) { const key = this.getTravelTimeKey(locationFrom, locationTo); const travelTime = travelTimeMap.get(key); if (travelTime === undefined) { throw new Error(`Missing travel time for pair ${key}.`); } return travelTime; } static getTravelTimeKey(locationFrom, locationTo) { return `${locationFrom[0]},${locationFrom[1]}->${locationTo[0]},${locationTo[1]}`; } static getInsertionPositions(routeLength, canInsertBeforeFirst, canInsertAfterLast) { const startPosition = canInsertBeforeFirst ? 0 : 1; const endPosition = canInsertAfterLast ? routeLength : routeLength - 1; const result = []; for (let position = startPosition; position <= endPosition; position++) { result.push(position); } return result; } }