@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
221 lines (220 loc) • 11.4 kB
JavaScript
import { TimeWindowViolation, BreakViolation, AgentPickupCapacityExceeded, AgentDeliveryCapacityExceeded, AgentMissingCapability } from '../../../models';
export class RouteViolationValidator {
static validate(context, agentIndex) {
const rawData = context.getRawData();
const violations = [];
let timeWindowViolations = this.validateTimeWindows(context, agentIndex);
violations.push(...timeWindowViolations);
let breakViolations = this.validateBreaks(context, agentIndex);
violations.push(...breakViolations);
let capacityViolations = this.validateCapacity(context, agentIndex);
violations.push(...capacityViolations);
let capabilityViolations = this.validateCapabilities(context, agentIndex);
violations.push(...capabilityViolations);
this.addViolationsToResult(rawData, agentIndex, violations);
}
static validateTimeWindows(context, agentIndex) {
var _a;
const result = [];
const rawData = context.getRawData();
const agent = rawData.properties.params.agents[agentIndex];
const actions = context.getAgentActions(agentIndex);
for (const action of actions) {
if (action.type === this.ACTION_TYPE_START ||
action.type === this.ACTION_TYPE_END ||
action.type === this.ACTION_TYPE_BREAK ||
action.type === this.ACTION_TYPE_DELAY) {
continue;
}
const actionTimeWindow = this.getActionTimeWindow(action);
const itemTimeWindows = this.getItemTimeWindows(rawData, action);
if (itemTimeWindows.length > 0 && !this.isWithinAnyTimeWindow(actionTimeWindow, itemTimeWindows)) {
result.push(new TimeWindowViolation(`Action at time ${action.start_time} is outside ${action.type} time windows`, agentIndex));
}
if (((_a = agent.time_windows) === null || _a === void 0 ? void 0 : _a.length) > 0 && !this.isWithinAnyTimeWindow(actionTimeWindow, agent.time_windows)) {
result.push(new TimeWindowViolation(`Action at time ${action.start_time} is outside agent time windows`, agentIndex));
}
}
return result;
}
static validateBreaks(context, agentIndex) {
var _a;
const result = [];
const rawData = context.getRawData();
const agent = rawData.properties.params.agents[agentIndex];
const actions = context.getAgentActions(agentIndex);
if (!((_a = agent.breaks) === null || _a === void 0 ? void 0 : _a.length)) {
return result;
}
for (const action of actions) {
if (action.type === this.ACTION_TYPE_START ||
action.type === this.ACTION_TYPE_END ||
action.type === this.ACTION_TYPE_BREAK ||
action.type === this.ACTION_TYPE_DELAY) {
continue;
}
const actionTimeWindow = this.getActionTimeWindow(action);
for (const breakPeriod of agent.breaks) {
if (this.intersectsAnyTimeWindow(actionTimeWindow, breakPeriod.time_windows)) {
result.push(new BreakViolation(`Action at time ${action.start_time} conflicts with agent break`, agentIndex));
break;
}
}
}
return result;
}
static validateCapacity(context, agentIndex) {
const violations = [];
const rawData = context.getRawData();
const agent = rawData.properties.params.agents[agentIndex];
const actions = context.getAgentActions(agentIndex);
let totalPickupAmount = 0;
let totalDeliveryAmount = 0;
for (const action of actions) {
if (action.type === this.ACTION_TYPE_PICKUP) {
totalPickupAmount += this.getActionAmount(rawData, action);
}
else if (action.type === this.ACTION_TYPE_DELIVERY) {
totalDeliveryAmount += this.getActionAmount(rawData, action);
}
else if (action.type === this.ACTION_TYPE_JOB) {
totalPickupAmount += this.getJobPickupAmount(rawData, action);
totalDeliveryAmount += this.getJobDeliveryAmount(rawData, action);
}
}
const pickupCapacity = this.normalizeCapacity(agent.pickup_capacity);
if (agent.pickup_capacity !== undefined && pickupCapacity === undefined) {
violations.push(new AgentPickupCapacityExceeded(`Agent pickup capacity is invalid (${String(agent.pickup_capacity)})`, agentIndex, totalPickupAmount, 0));
}
else if (pickupCapacity !== undefined && totalPickupAmount > pickupCapacity) {
violations.push(new AgentPickupCapacityExceeded(`Total pickup amount (${totalPickupAmount}) exceeds agent pickup capacity (${pickupCapacity})`, agentIndex, totalPickupAmount, pickupCapacity));
}
const deliveryCapacity = this.normalizeCapacity(agent.delivery_capacity);
if (agent.delivery_capacity !== undefined && deliveryCapacity === undefined) {
violations.push(new AgentDeliveryCapacityExceeded(`Agent delivery capacity is invalid (${String(agent.delivery_capacity)})`, agentIndex, totalDeliveryAmount, 0));
}
else if (deliveryCapacity !== undefined && totalDeliveryAmount > deliveryCapacity) {
violations.push(new AgentDeliveryCapacityExceeded(`Total delivery amount (${totalDeliveryAmount}) exceeds agent delivery capacity (${deliveryCapacity})`, agentIndex, totalDeliveryAmount, deliveryCapacity));
}
return violations;
}
static validateCapabilities(context, agentIndex) {
var _a, _b;
const violations = [];
const rawData = context.getRawData();
const agent = rawData.properties.params.agents[agentIndex];
const actions = context.getAgentActions(agentIndex);
const missingCapabilities = new Set();
for (const action of actions) {
if (action.job_index !== undefined) {
const job = (_a = rawData.properties.params.jobs) === null || _a === void 0 ? void 0 : _a[action.job_index];
this.collectMissingRequirements(agent, job === null || job === void 0 ? void 0 : job.requirements, missingCapabilities);
}
if (action.shipment_index !== undefined) {
const shipment = (_b = rawData.properties.params.shipments) === null || _b === void 0 ? void 0 : _b[action.shipment_index];
this.collectMissingRequirements(agent, shipment === null || shipment === void 0 ? void 0 : shipment.requirements, missingCapabilities);
}
}
if (missingCapabilities.size === 0) {
return violations;
}
const missing = Array.from(missingCapabilities);
const message = missing.length === 1
? `Agent is missing required capability: '${missing[0]}'`
: `Agent is missing required capabilities: ${missing.join(', ')}`;
violations.push(new AgentMissingCapability(message, agentIndex, missing));
return violations;
}
static collectMissingRequirements(agent, requirements, target) {
var _a;
if (!(requirements === null || requirements === void 0 ? void 0 : requirements.length)) {
return;
}
for (const requirement of requirements) {
if (!((_a = agent.capabilities) === null || _a === void 0 ? void 0 : _a.includes(requirement))) {
target.add(requirement);
}
}
}
static getActionTimeWindow(action) {
return [action.start_time, action.start_time + (action.duration || 0)];
}
static getItemTimeWindows(rawData, action) {
var _a, _b;
if (action.job_index !== undefined) {
const job = rawData.properties.params.jobs[action.job_index];
return (job === null || job === void 0 ? void 0 : job.time_windows) || [];
}
if (action.shipment_index !== undefined) {
const shipment = rawData.properties.params.shipments[action.shipment_index];
if (action.type === this.ACTION_TYPE_PICKUP) {
return ((_a = shipment === null || shipment === void 0 ? void 0 : shipment.pickup) === null || _a === void 0 ? void 0 : _a.time_windows) || [];
}
if (action.type === this.ACTION_TYPE_DELIVERY) {
return ((_b = shipment === null || shipment === void 0 ? void 0 : shipment.delivery) === null || _b === void 0 ? void 0 : _b.time_windows) || [];
}
}
return [];
}
static getActionAmount(rawData, action) {
if (action.shipment_index !== undefined) {
const shipment = rawData.properties.params.shipments[action.shipment_index];
return this.normalizeAmount(shipment === null || shipment === void 0 ? void 0 : shipment.amount);
}
return 0;
}
static getJobPickupAmount(rawData, action) {
if (action.job_index !== undefined) {
const job = rawData.properties.params.jobs[action.job_index];
return this.normalizeAmount(job === null || job === void 0 ? void 0 : job.pickup_amount);
}
return 0;
}
static getJobDeliveryAmount(rawData, action) {
if (action.job_index !== undefined) {
const job = rawData.properties.params.jobs[action.job_index];
return this.normalizeAmount(job === null || job === void 0 ? void 0 : job.delivery_amount);
}
return 0;
}
static normalizeAmount(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return 0;
}
return value;
}
static normalizeCapacity(value) {
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return undefined;
}
return value;
}
static isWithinAnyTimeWindow(actionWindow, timeWindows) {
const [actionStart, actionEnd] = actionWindow;
return timeWindows.some(([windowStart, windowEnd]) => actionStart >= windowStart && actionEnd <= windowEnd);
}
static intersectsAnyTimeWindow(actionWindow, timeWindows) {
const [actionStart, actionEnd] = actionWindow;
return timeWindows.some(([windowStart, windowEnd]) => actionStart < windowEnd && actionEnd > windowStart);
}
static addViolationsToResult(rawData, agentIndex, violations) {
rawData.properties.violations = (rawData.properties.violations || []).filter((violation) => violation.agentIndex !== agentIndex);
if (violations.length === 0) {
if (!rawData.properties.violations.length) {
delete rawData.properties.violations;
}
return;
}
rawData.properties.violations.push(...violations);
}
}
RouteViolationValidator.ACTION_TYPE_PICKUP = 'pickup';
RouteViolationValidator.ACTION_TYPE_DELIVERY = 'delivery';
RouteViolationValidator.ACTION_TYPE_START = 'start';
RouteViolationValidator.ACTION_TYPE_END = 'end';
RouteViolationValidator.ACTION_TYPE_BREAK = 'break';
RouteViolationValidator.ACTION_TYPE_DELAY = 'delay';
RouteViolationValidator.ACTION_TYPE_JOB = 'job';