@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
232 lines (231 loc) • 13.5 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());
});
};
import { PreserveOrderBaseHelper } from "./preserve-order-base-helper";
import { InsertPositionResolver } from "../utils/insert-position-resolver";
import { RouteEditorHelper } from "../utils/route-editor-helper";
import { InsertionCostCalculator } from "../utils/insertion-cost-calculator";
const LOCATION_EPSILON = 1e-6;
export class PreserveOrderShipmentHelper extends PreserveOrderBaseHelper {
static determineShipmentInsertPositions(context, agentIndex, shipmentIndex, options) {
return __awaiter(this, void 0, void 0, function* () {
// append: true (no position) → Append to end
if (InsertPositionResolver.shouldAppend(options)) {
const positions = yield this.getAppendToEndPositions(context, agentIndex);
return Object.assign(Object.assign({}, positions), { createPickupWaypoint: true, createDeliveryWaypoint: true });
}
// afterId/afterWaypointIndex + append: true → Insert at specified position
if (InsertPositionResolver.hasExplicitInsertPosition(options)) {
const pickupPosition = InsertPositionResolver.resolveInsertPosition(options);
return {
pickup: pickupPosition,
// Pickup waypoint is inserted first, so delivery should go to the next index.
delivery: pickupPosition + 1,
createPickupWaypoint: true,
createDeliveryWaypoint: true
};
}
// afterId/afterWaypointIndex + append: false → Optimize after position
if (options.afterWaypointIndex !== undefined && !options.append) {
const minWaypointPosition = options.afterWaypointIndex;
return yield this.findOptimalShipmentPositionsAfter(context, agentIndex, shipmentIndex, minWaypointPosition);
}
// No position params → Use Route Matrix API to find optimal positions anywhere
return yield this.findOptimalShipmentPositions(context, agentIndex, shipmentIndex);
});
}
static getAppendToEndPositions(context, agentIndex) {
return __awaiter(this, void 0, void 0, function* () {
const agentFeature = yield context.getOrCreateAgentFeature(agentIndex);
const waypoints = agentFeature.properties.waypoints || [];
if (waypoints.length === 0) {
return { pickup: 0, delivery: 1 };
}
const hasEndAction = waypoints[waypoints.length - 1].actions.some(action => action.type === 'end');
if (hasEndAction) {
return {
// Insert pickup right before end; after pickup insertion, end shifts by +1,
// so delivery should be inserted at the original end index.
pickup: waypoints.length - 1,
delivery: waypoints.length
};
}
else {
return { pickup: waypoints.length, delivery: waypoints.length + 1 };
}
});
}
static findOptimalShipmentPositions(context, agentIndex, shipmentIndex) {
return __awaiter(this, void 0, void 0, function* () {
const shipment = RouteEditorHelper.getShipmentByIndex(context, shipmentIndex);
const pickupLocation = RouteEditorHelper.resolveShipmentStepLocation(context, shipment.pickup);
const deliveryLocation = RouteEditorHelper.resolveShipmentStepLocation(context, shipment.delivery);
const agentFeature = context.getAgentFeature(agentIndex);
const routeLocations = InsertPositionResolver.extractRouteLocations(agentFeature);
const waypoints = agentFeature.properties.waypoints || [];
const pickupExistingWaypointIndex = this.findExistingWaypointByOriginalLocation(waypoints, pickupLocation, 0);
const deliveryExistingWaypointIndex = this.findExistingWaypointByOriginalLocation(waypoints, deliveryLocation, 0);
let createPickupWaypoint = true;
let createDeliveryWaypoint = true;
let pickup = 0;
let delivery = 0;
let pickupTravelTimes;
// find pickup position
if (pickupExistingWaypointIndex !== -1) {
createPickupWaypoint = false;
pickup = pickupExistingWaypointIndex;
}
else if (deliveryExistingWaypointIndex !== -1) {
const pickupRouteBeforeDelivery = routeLocations.slice(0, deliveryExistingWaypointIndex + 1);
pickupTravelTimes = yield this.calculateTravelTimes(context, pickupRouteBeforeDelivery, pickupLocation);
const pickupIndex = yield InsertionCostCalculator.findOptimalInsertionPoint(context, agentIndex, pickupRouteBeforeDelivery, pickupLocation, { canInsertBeforeFirst: !this.hasAgentStartLocation(context, agentIndex),
canInsertAfterLast: false,
travelTimes: pickupTravelTimes });
pickup = pickupIndex;
}
else if (routeLocations.length === 0) {
pickup = 0;
}
else if (routeLocations.length === 1) {
pickup = this.hasAgentStartLocation(context, agentIndex) ? 1 : 0;
}
else {
pickupTravelTimes = yield this.calculateTravelTimes(context, routeLocations, pickupLocation);
const pickupIndex = yield InsertionCostCalculator.findOptimalInsertionPoint(context, agentIndex, routeLocations, pickupLocation, { canInsertBeforeFirst: !this.hasAgentStartLocation(context, agentIndex),
canInsertAfterLast: !this.hasAgentEndLocation(context, agentIndex),
travelTimes: pickupTravelTimes });
pickup = pickupIndex;
}
const canReuseDeliveryWaypoint = deliveryExistingWaypointIndex !== -1 &&
deliveryExistingWaypointIndex >= pickup;
// find delivery position
if (canReuseDeliveryWaypoint) {
createDeliveryWaypoint = false;
delivery = deliveryExistingWaypointIndex;
}
else {
const routeWithPickup = [...routeLocations];
if (createPickupWaypoint) {
routeWithPickup.splice(pickup, 0, pickupLocation);
}
const deliveryTravelTimes = yield this.calculateTravelTimes(context, routeWithPickup.slice(pickup), deliveryLocation, pickupTravelTimes);
const deliveryIndex = yield InsertionCostCalculator.findOptimalInsertionPoint(context, agentIndex, routeWithPickup.slice(pickup), deliveryLocation, { canInsertBeforeFirst: false, travelTimes: deliveryTravelTimes });
delivery = pickup + deliveryIndex;
}
return {
pickup,
delivery,
createPickupWaypoint,
createDeliveryWaypoint
};
});
}
static findOptimalShipmentPositionsAfter(context, agentIndex, shipmentIndex, minPosition) {
return __awaiter(this, void 0, void 0, function* () {
const shipment = RouteEditorHelper.getShipmentByIndex(context, shipmentIndex);
const pickupLocation = RouteEditorHelper.resolveShipmentStepLocation(context, shipment.pickup);
const deliveryLocation = RouteEditorHelper.resolveShipmentStepLocation(context, shipment.delivery);
const agentFeature = context.getAgentFeature(agentIndex);
const allRouteLocations = InsertPositionResolver.extractRouteLocations(agentFeature);
const waypoints = agentFeature.properties.waypoints || [];
const pickupExistingWaypointIndex = this.findExistingWaypointByOriginalLocation(waypoints, pickupLocation, 0);
const deliveryExistingWaypointIndex = this.findExistingWaypointByOriginalLocation(waypoints, deliveryLocation, 0);
let createPickupWaypoint = true;
let createDeliveryWaypoint = true;
let pickup = 0;
let delivery = 0;
let pickupRouteIndex = -1;
let pickupTravelTimes;
if (pickupExistingWaypointIndex !== -1) {
if (pickupExistingWaypointIndex >= minPosition) {
createPickupWaypoint = false;
pickup = pickupExistingWaypointIndex;
pickupRouteIndex = pickupExistingWaypointIndex;
}
}
if (pickupRouteIndex === -1) {
const routeStartIndex = Math.max(0, minPosition);
const hasDeliveryAfterMin = deliveryExistingWaypointIndex !== -1 && deliveryExistingWaypointIndex >= routeStartIndex;
let pickupIndexRelative;
const routeLocationsAfter = hasDeliveryAfterMin
? allRouteLocations.slice(routeStartIndex, deliveryExistingWaypointIndex + 1)
: allRouteLocations.slice(routeStartIndex);
if (routeLocationsAfter.length === 0) {
pickup = routeStartIndex;
pickupRouteIndex = routeStartIndex;
}
else {
const pickupOptions = hasDeliveryAfterMin
? { canInsertBeforeFirst: false, canInsertAfterLast: false }
: { canInsertBeforeFirst: false, canInsertAfterLast: !this.hasAgentEndLocation(context, agentIndex) };
pickupTravelTimes = yield this.calculateTravelTimes(context, routeLocationsAfter, pickupLocation);
pickupIndexRelative = yield InsertionCostCalculator.findOptimalInsertionPoint(context, agentIndex, routeLocationsAfter, pickupLocation, Object.assign(Object.assign({}, pickupOptions), { travelTimes: pickupTravelTimes }));
pickupRouteIndex = routeStartIndex + pickupIndexRelative;
pickup = pickupRouteIndex;
}
}
const canReuseDeliveryWaypoint = deliveryExistingWaypointIndex !== -1 &&
deliveryExistingWaypointIndex >= pickupRouteIndex;
if (canReuseDeliveryWaypoint) {
createDeliveryWaypoint = false;
delivery = createPickupWaypoint ? deliveryExistingWaypointIndex + 1 : deliveryExistingWaypointIndex;
}
else {
const routeWithPickup = [...allRouteLocations];
if (createPickupWaypoint) {
routeWithPickup.splice(pickupRouteIndex, 0, pickupLocation);
}
const deliveryTravelTimes = yield this.calculateTravelTimes(context, routeWithPickup.slice(pickupRouteIndex), deliveryLocation, pickupTravelTimes);
const deliveryIndex = yield InsertionCostCalculator.findOptimalInsertionPoint(context, agentIndex, routeWithPickup.slice(pickupRouteIndex), deliveryLocation, { canInsertBeforeFirst: false, travelTimes: deliveryTravelTimes });
delivery = createPickupWaypoint ? pickup + deliveryIndex + 1 : pickup + deliveryIndex;
}
return {
pickup,
delivery,
createPickupWaypoint,
createDeliveryWaypoint
};
});
}
static findExistingWaypointByOriginalLocation(waypoints, targetLocation, minWaypointIndex) {
for (let i = Math.max(0, minWaypointIndex); i < waypoints.length; i++) {
if (this.sameLocation(waypoints[i].original_location, targetLocation)) {
return i;
}
}
return -1;
}
static sameLocation(a, b) {
return Math.abs(a[0] - b[0]) <= LOCATION_EPSILON &&
Math.abs(a[1] - b[1]) <= LOCATION_EPSILON;
}
static calculateTravelTimes(context, route, newLocation, existing) {
return __awaiter(this, void 0, void 0, function* () {
const matrixHelper = context.getMatrixHelper();
const [timesToNew, timesFromNew] = yield Promise.all([
matrixHelper.calculateTimesToLocation(route, newLocation),
matrixHelper.calculateTimesFromLocation(newLocation, route)
]);
const travelTimes = existing || [];
for (let i = 0; i < route.length; i++) {
travelTimes.push({
locationFrom: route[i],
locationTo: newLocation,
time: timesToNew[i]
});
travelTimes.push({
locationFrom: newLocation,
locationTo: route[i],
time: timesFromNew[i]
});
}
return travelTimes;
});
}
}