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

232 lines (231 loc) 13.5 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()); }); }; 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; }); } }