@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
382 lines (381 loc) • 17.2 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 { AgentHasNoPlan, AgentNotFound, NoItemsProvided, ItemsNotUnique } from "../../models";
import { RouteMatrixHelper } from "./strategies/preserve-order/utils/route-matrix-helper";
import { RoutingHelper } from "./strategies/preserve-order/utils/routing-helper";
import { RoutePlanner } from "../../route-planner";
import { Utils } from "../utils";
import { LegRecalculator } from "./strategies";
const MISSING_ROUTE_METRIC = -1;
/**
* Base class for route result editors with shared functionality
*/
export class RouteResultEditorBase {
constructor(rawData, callOptions, routingOptions) {
this.rawData = rawData;
this.callOptions = callOptions;
this.routingOptions = routingOptions;
this.rawData = rawData;
}
validateAgent(agentIndex) {
const agentFound = this.rawData.properties.params.agents[agentIndex];
if (!agentFound) {
throw new AgentNotFound(`Agent with index ${agentIndex} not found`, agentIndex);
}
}
ensureItemsProvided(indexes, itemType) {
if (indexes.length === 0) {
throw new NoItemsProvided(`No ${itemType} provided`, itemType);
}
}
ensureItemsUnique(indexes, itemType) {
if (indexes.length !== new Set(indexes).size) {
const capitalized = itemType.charAt(0).toUpperCase() + itemType.slice(1);
throw new ItemsNotUnique(`${capitalized} are not unique`, itemType);
}
}
ensureNewItemsValid(items, itemType) {
if (items.length === 0) {
throw new NoItemsProvided(`No ${itemType} provided`, itemType);
}
if (items.length !== new Set(items).size) {
const capitalized = itemType.charAt(0).toUpperCase() + itemType.slice(1);
throw new ItemsNotUnique(`${capitalized} are not unique`, itemType);
}
}
getRawData() {
return this.rawData;
}
cloneInputData() {
return Utils.cloneObject(this.rawData.properties.params);
}
executePlan(inputData) {
return __awaiter(this, void 0, void 0, function* () {
const planner = new RoutePlanner(this.callOptions, inputData);
const newResult = yield planner.plan();
this.updateResult(newResult);
return true;
});
}
executeAgentPlan(agentIndex, inputData) {
var _a, _b, _c, _d;
return __awaiter(this, void 0, void 0, function* () {
if (!((_a = inputData.shipments) === null || _a === void 0 ? void 0 : _a.length) && !((_b = inputData.jobs) === null || _b === void 0 ? void 0 : _b.length)) {
this.updateAgentAsUnassigned(agentIndex);
}
else {
const planner = new RoutePlanner(this.callOptions, inputData);
const newResult = yield planner.plan();
if (((_c = newResult.getRaw().features) === null || _c === void 0 ? void 0 : _c.length) === 0) {
this.updateAgentAsUnassigned(agentIndex);
return true;
}
const newAgentData = (_d = newResult.getRaw().features) === null || _d === void 0 ? void 0 : _d[0];
newAgentData.properties.agent_index = agentIndex;
const featureIndex = this.rawData.features.findIndex((feature) => feature.properties.agent_index === agentIndex);
if (featureIndex === -1) {
this.rawData.features.push(newAgentData);
}
else {
this.rawData.features[featureIndex] = newAgentData;
}
}
return true;
});
}
updateIssues() {
var _a, _b;
const issues = this.rawData.properties.issues || (this.rawData.properties.issues = {});
const params = this.rawData.properties.params;
const assignedAgentIndexes = new Set();
const assignedJobIndexes = new Set();
const assignedShipmentIndexes = new Set();
for (const feature of this.rawData.features || []) {
const agentIndex = (_a = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _a === void 0 ? void 0 : _a.agent_index;
if (typeof agentIndex === "number") {
assignedAgentIndexes.add(agentIndex);
}
for (const action of ((_b = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _b === void 0 ? void 0 : _b.actions) || []) {
if (typeof action.job_index === "number") {
assignedJobIndexes.add(action.job_index);
}
if (typeof action.shipment_index === "number") {
assignedShipmentIndexes.add(action.shipment_index);
}
}
}
issues.unassigned_agents = this.getUnassignedIndexes(params.agents.length, assignedAgentIndexes);
issues.unassigned_jobs = this.getUnassignedIndexes((params.jobs || []).length, assignedJobIndexes);
issues.unassigned_shipments = this.getUnassignedIndexes((params.shipments || []).length, assignedShipmentIndexes);
}
updateAgentAsUnassigned(agentIndex) {
const featureIndex = this.rawData.features.findIndex((feature) => feature.properties.agent_index === agentIndex);
if (featureIndex !== -1) {
this.rawData.features.splice(featureIndex, 1);
}
}
getUnassignedIndexes(totalCount, assignedIndexes) {
const unassigned = [];
for (let index = 0; index < totalCount; index++) {
if (!assignedIndexes.has(index)) {
unassigned.push(index);
}
}
return unassigned;
}
updateResult(newResult) {
this.rawData.features = newResult.getRaw().features;
this.rawData.properties.issues = newResult.getRaw().properties.issues;
}
getMatrixHelper() {
return new RouteMatrixHelper(this.callOptions, this.routingOptions);
}
getRoutingHelper() {
return new RoutingHelper(this.callOptions, this.routingOptions);
}
getAgentFeature(agentIndex) {
const rawData = this.rawData;
const agentFeature = rawData.features.find((f) => f.properties.agent_index === agentIndex);
if (!agentFeature) {
throw new AgentHasNoPlan(`Agent with index ${agentIndex} has no Plan`, agentIndex);
}
return agentFeature;
}
getOrCreateAgentFeature(agentIndex) {
return __awaiter(this, void 0, void 0, function* () {
const rawData = this.rawData;
let agentFeature = rawData.features.find((f) => f.properties.agent_index === agentIndex);
let isNewAgentFeature = false;
if (!agentFeature) {
// Create a minimal feature structure for unassigned agents
const agent = rawData.properties.params.agents[agentIndex];
agentFeature = this.createEmptyAgentFeature(agentIndex, agent);
isNewAgentFeature = true;
rawData.features.push(agentFeature);
// Remove from unassigned list if present
if (rawData.properties.issues && rawData.properties.issues.unassigned_agents) {
const unassignedIndex = rawData.properties.issues.unassigned_agents.indexOf(agentIndex);
if (unassignedIndex !== -1) {
rawData.properties.issues.unassigned_agents.splice(unassignedIndex, 1);
}
}
}
if (isNewAgentFeature) {
yield LegRecalculator.fillMissingLegData(this, agentFeature);
}
return agentFeature;
});
}
createEmptyAgentFeature(agentIndex, agent) {
const startTime = (agent.time_windows && agent.time_windows.length > 0 && agent.time_windows[0].length > 0)
? agent.time_windows[0][0]
: 0;
const startLocation = this.resolveAgentLocation(agentIndex, agent, "start");
const endLocation = this.resolveAgentLocation(agentIndex, agent, "end");
const hasStartLocation = !!startLocation;
const hasEndLocation = !!endLocation;
const hasSeparateStartEnd = hasStartLocation && hasEndLocation;
const effectiveStart = startLocation !== null && startLocation !== void 0 ? startLocation : endLocation;
const effectiveEnd = endLocation !== null && endLocation !== void 0 ? endLocation : startLocation;
if (!effectiveStart || !effectiveEnd) {
throw new Error(`Agent ${agentIndex} must have start_location(_index) or end_location(_index)`);
}
const startAction = {
type: "start",
index: 0,
start_time: startTime,
duration: 0,
location_index: effectiveStart.locationIndex,
location_id: effectiveStart.locationId,
waypoint_index: 0
};
const endAction = hasEndLocation
? {
type: "end",
index: 1,
start_time: startTime,
duration: 0,
location_index: effectiveEnd.locationIndex,
location_id: effectiveEnd.locationId,
waypoint_index: hasSeparateStartEnd ? 1 : 0
}
: undefined;
const waypoints = hasSeparateStartEnd
? [
{
original_location: effectiveStart.location,
original_location_index: effectiveStart.locationIndex,
original_location_id: effectiveStart.locationId,
start_time: startTime,
duration: 0,
actions: [Object.assign({}, startAction)],
prev_leg_index: undefined,
next_leg_index: 0
},
{
original_location: effectiveEnd.location,
original_location_index: effectiveEnd.locationIndex,
original_location_id: effectiveEnd.locationId,
start_time: startTime,
duration: 0,
actions: endAction ? [Object.assign({}, endAction)] : [],
prev_leg_index: 0,
next_leg_index: undefined
}
]
: [
{
original_location: effectiveStart.location,
original_location_index: effectiveStart.locationIndex,
original_location_id: effectiveStart.locationId,
start_time: startTime,
duration: 0,
actions: endAction ? [Object.assign({}, startAction), Object.assign({}, endAction)] : [Object.assign({}, startAction)],
prev_leg_index: undefined,
next_leg_index: undefined
}
];
const legs = hasSeparateStartEnd
? [
{
from_waypoint_index: 0,
to_waypoint_index: 1,
time: MISSING_ROUTE_METRIC,
distance: MISSING_ROUTE_METRIC,
steps: []
}
]
: [];
const geometryCoordinates = hasSeparateStartEnd
? [[effectiveStart.location, effectiveEnd.location]] // this should be overwritten later by locations from Routing API
: [];
return {
type: 'Feature',
geometry: {
type: 'MultiLineString',
coordinates: geometryCoordinates
},
properties: {
agent_index: agentIndex,
agent_id: agent.id || `agent-${agentIndex}`,
mode: agent.mode || this.rawData.properties.params.mode || 'drive',
waypoints,
legs,
time: 0,
start_time: startTime,
end_time: startTime,
distance: 0,
actions: endAction ? [startAction, endAction] : [startAction]
}
};
}
resolveAgentLocation(agentIndex, agent, boundary) {
const locationIndex = boundary === "start" ? agent.start_location_index : agent.end_location_index;
if (locationIndex !== undefined) {
const indexedLocation = this.rawData.properties.params.locations[locationIndex];
if (!indexedLocation || !indexedLocation.location) {
throw new Error(`Agent ${agentIndex} has invalid ${boundary}_location_index ${locationIndex}`);
}
return {
location: indexedLocation.location,
locationIndex,
locationId: indexedLocation.id
};
}
const location = boundary === "start" ? agent.start_location : agent.end_location;
if (location) {
return { location };
}
return undefined;
}
findEndActionIndex(actions) {
return actions.findIndex((a) => a.type === 'end');
}
reindexActions(actions) {
actions.forEach((action, idx) => {
action.index = idx;
});
}
addAgentCapabilities(agents) {
for (let agentIndex = 0; agentIndex < agents.length; agentIndex++) {
const agent = agents[agentIndex];
const capabilityName = `assign-agent-${agentIndex}`;
if (!agent.capabilities) {
agent.capabilities = [];
}
if (!agent.capabilities.includes(capabilityName)) {
agent.capabilities.push(capabilityName);
}
}
}
getAgentIndexForShipment(shipmentIndex) {
const features = this.getRawData().features;
for (const feature of features) {
for (const action of feature.properties.actions) {
if (action.shipment_index === shipmentIndex) {
return feature.properties.agent_index;
}
}
}
return undefined;
}
getAgentShipments(agentIndex) {
const agentFeature = this.getRawData().features.find(feature => feature.properties.agent_index === agentIndex);
if (!agentFeature) {
return [];
}
const shipmentIndexes = [];
for (const action of agentFeature.properties.actions) {
if (action.shipment_index !== undefined && !shipmentIndexes.includes(action.shipment_index)) {
shipmentIndexes.push(action.shipment_index);
}
}
return shipmentIndexes;
}
getAgentIndexForJob(jobIndex) {
for (const agentFeature of this.getRawData().features) {
for (const action of agentFeature.properties.actions) {
if (action.job_index === jobIndex) {
return agentFeature.properties.agent_index;
}
}
}
return undefined;
}
getAgentJobs(agentIndex) {
const agentFeature = this.getRawData().features.find(feature => feature.properties.agent_index === agentIndex);
if (!agentFeature) {
return [];
}
const jobIndexes = [];
for (const action of agentFeature.properties.actions) {
if (action.job_index !== undefined && !jobIndexes.includes(action.job_index)) {
jobIndexes.push(action.job_index);
}
}
return jobIndexes;
}
getAgentActions(agentIndex) {
const agentFeature = this.getRawData().features.find(feature => feature.properties.agent_index === agentIndex);
return agentFeature ? agentFeature.properties.actions : [];
}
getAgentWaypoints(agentIndex) {
const agentFeature = this.getRawData().features.find(feature => feature.properties.agent_index === agentIndex);
return agentFeature ? agentFeature.properties.waypoints : [];
}
getExistingConsecutiveTravelTimes(agentIndex) {
var _a;
const agentFeature = this.getRawData().features.find(feature => feature.properties.agent_index === agentIndex);
if (!((_a = agentFeature === null || agentFeature === void 0 ? void 0 : agentFeature.properties) === null || _a === void 0 ? void 0 : _a.legs)) {
return [];
}
return agentFeature.properties.legs.map(leg => leg.time);
}
}