Files
rutero/route-planner/src/app/shared/utils/route-sequencing.utils.ts
T

119 lines
4.1 KiB
TypeScript

import { RouteAssignedResponse } from '../../core/models/route.model';
export interface SequencedRoute {
route: RouteAssignedResponse;
origin: [number, number];
destination: [number, number];
order: number;
totalStops: number;
previousPdvName: string | null;
}
export interface DailyRouteSummary {
key: string;
userId: string;
userName: string;
day: string;
routes: SequencedRoute[];
distanceKm: number;
travelHours: number;
workHours: number;
capacityHours: number;
totalHours: number;
exceedsCapacity: boolean;
}
export function sequenceRoutes(routes: RouteAssignedResponse[]): SequencedRoute[] {
const groups = new Map<string, RouteAssignedResponse[]>();
for (const route of routes) {
const key = `${route.json.usuario_id}\u0000${route.json.dia}`;
const group = groups.get(key) ?? [];
group.push(route);
groups.set(key, group);
}
const sequenced: SequencedRoute[] = [];
for (const group of groups.values()) {
const first = group[0].json;
let current: [number, number] = [Number(first.longitud_usuario), Number(first.latitud_usuario)];
let previousPdvName: string | null = null;
const hasManualOrder = group.every(route => Number.isFinite(route.json.visit_order));
const remaining = hasManualOrder
? [...group].sort((a, b) => Number(a.json.visit_order) - Number(b.json.visit_order))
: [...group];
while (remaining.length) {
let nearestIndex = 0;
if (!hasManualOrder) {
let nearestDistance = Number.POSITIVE_INFINITY;
for (let index = 0; index < remaining.length; index++) {
const candidate = remaining[index].json;
const distance = haversineKm(current, [Number(candidate.longitud_pdv), Number(candidate.latitud_pdv)]);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestIndex = index;
}
}
}
const [route] = remaining.splice(nearestIndex, 1);
const destination: [number, number] = [Number(route.json.longitud_pdv), Number(route.json.latitud_pdv)];
sequenced.push({
route,
origin: current,
destination,
order: group.length - remaining.length,
totalStops: group.length,
previousPdvName
});
current = destination;
previousPdvName = route.json.nombre_pdv;
}
}
return sequenced;
}
export function summarizeDailyRoutes(routes: RouteAssignedResponse[]): DailyRouteSummary[] {
const sequences = sequenceRoutes(routes);
const grouped = new Map<string, SequencedRoute[]>();
for (const segment of sequences) {
const key = `${segment.route.json.usuario_id}\u0000${segment.route.json.dia}`;
const group = grouped.get(key) ?? [];
group.push(segment);
grouped.set(key, group);
}
return Array.from(grouped, ([key, segments]) => {
const first = segments[0].route.json;
const distanceKm = segments.reduce((sum, segment) => sum + haversineKm(segment.origin, segment.destination), 0);
const travelHours = segments.reduce((sum, segment) => sum + Number(segment.route.json.horas_desplazamiento || 0), 0);
const workHours = segments.reduce((sum, segment) => sum + Number(segment.route.json.horas_trabajo || segment.route.json.hora_minima_semanal_pdv || 0), 0);
const capacityHours = Number(first.hora_laboral_semanal_usuario || 0) / 5;
const totalHours = workHours + travelHours;
return {
key,
userId: String(first.usuario_id),
userName: first.nombre_usuario,
day: first.dia,
routes: segments,
distanceKm,
travelHours,
workHours,
capacityHours,
totalHours,
exceedsCapacity: totalHours > capacityHours
};
});
}
export function haversineKm(origin: [number, number], destination: [number, number]): number {
const toRadians = (value: number) => value * Math.PI / 180;
const [lon1, lat1] = origin;
const [lon2, lat2] = destination;
const dLat = toRadians(lat2 - lat1);
const dLon = toRadians(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) ** 2;
return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}