mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
leg-aware capacity and wagon sharing
This commit is contained in:
@@ -665,6 +665,14 @@ export function validateContainerPlacements(
|
||||
wagonPlan: WagonPlanSlot[],
|
||||
placements: ContainerPlacementInput[],
|
||||
rules?: ContainerPlacementRules,
|
||||
/**
|
||||
* Leg-aware occupancy (cross-leg TEU sharing): booking id → stop-index leg.
|
||||
* With legs, a wagon's TEU/weight caps hold PER CORRIDOR EDGE — an intercity
|
||||
* 20ft and an export 20ft coexist on one wagon when their edges allow it.
|
||||
* Omitted → one edge, byte-identical to the whole-route check.
|
||||
*/
|
||||
legs?: Map<string, { from: number; to: number }>,
|
||||
edgeCount?: number,
|
||||
): string[] {
|
||||
const violations: string[] = [];
|
||||
const units = expandBookingContainerUnits(containerBookings);
|
||||
@@ -721,8 +729,18 @@ export function validateContainerPlacements(
|
||||
}
|
||||
}
|
||||
|
||||
const slotTeuUsed = new Map<number, number>();
|
||||
const slotWeightUsed = new Map<number, number>();
|
||||
// TEU and weight are tracked PER EDGE of a unit's leg; without legs there is
|
||||
// a single edge and this is exactly the old whole-route accounting.
|
||||
const edges = Math.max(1, edgeCount ?? 1);
|
||||
const legOf = (bookingId: string): { from: number; to: number } => {
|
||||
const leg = legs?.get(bookingId);
|
||||
if (!leg || leg.from < 0 || leg.to > edges || leg.from >= leg.to) {
|
||||
return { from: 0, to: edges };
|
||||
}
|
||||
return leg;
|
||||
};
|
||||
const slotTeuUsed = new Map<number, number[]>();
|
||||
const slotWeightUsed = new Map<number, number[]>();
|
||||
const slotBySeq = new Map(wagonPlan.map((s) => [s.sequenceNo, s]));
|
||||
|
||||
for (const placement of placements) {
|
||||
@@ -734,22 +752,38 @@ export function validateContainerPlacements(
|
||||
if (!unit) continue;
|
||||
|
||||
const teu = unit.teuSlots ?? teuSlotsForSizeFt(unit.sizeFt ?? 20);
|
||||
const usedTeu = slotTeuUsed.get(placement.sequenceNo) ?? 0;
|
||||
if (usedTeu + teu > MAX_TEU_SLOTS_PER_WAGON) {
|
||||
const leg = legOf(unit.bookingId);
|
||||
const teuRow =
|
||||
slotTeuUsed.get(placement.sequenceNo) ?? new Array<number>(edges).fill(0);
|
||||
let teuFits = true;
|
||||
for (let e = leg.from; e < leg.to; e += 1) {
|
||||
if ((teuRow[e] ?? 0) + teu > MAX_TEU_SLOTS_PER_WAGON) {
|
||||
teuFits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!teuFits) {
|
||||
violations.push(
|
||||
`Wagon #${placement.sequenceNo} cannot fit another ${unit.containerTypeCode} (max 1×40ft or 2×20ft per wagon)`,
|
||||
);
|
||||
} else {
|
||||
slotTeuUsed.set(placement.sequenceNo, usedTeu + teu);
|
||||
for (let e = leg.from; e < leg.to; e += 1) teuRow[e] = (teuRow[e] ?? 0) + teu;
|
||||
slotTeuUsed.set(placement.sequenceNo, teuRow);
|
||||
}
|
||||
|
||||
const slot = slotBySeq.get(placement.sequenceNo);
|
||||
if (slot) {
|
||||
const weight = roundTons(slotWeightUsed.get(placement.sequenceNo) ?? 0) + unit.grossWeightTons;
|
||||
slotWeightUsed.set(placement.sequenceNo, weight);
|
||||
if (weight > slot.capacityTons) {
|
||||
const weightRow =
|
||||
slotWeightUsed.get(placement.sequenceNo) ?? new Array<number>(edges).fill(0);
|
||||
let heaviestEdge = 0;
|
||||
for (let e = leg.from; e < leg.to; e += 1) {
|
||||
weightRow[e] = roundTons((weightRow[e] ?? 0) + unit.grossWeightTons);
|
||||
heaviestEdge = Math.max(heaviestEdge, weightRow[e]);
|
||||
}
|
||||
slotWeightUsed.set(placement.sequenceNo, weightRow);
|
||||
if (heaviestEdge > slot.capacityTons) {
|
||||
violations.push(
|
||||
`Wagon #${placement.sequenceNo} total container weight ${weight}T exceeds capacity ${slot.capacityTons}T`,
|
||||
`Wagon #${placement.sequenceNo} total container weight ${heaviestEdge}T exceeds capacity ${slot.capacityTons}T`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user