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

221 lines (220 loc) 11.4 kB
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';