@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
JavaScript
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;
}
}