mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
leg-aware capacity and wagon sharing
This commit is contained in:
@@ -53,18 +53,25 @@ export type FlexPlanResult = {
|
||||
|
||||
type OpenSlot = {
|
||||
slot: WagonPlanSlot;
|
||||
teuUsed: number;
|
||||
/**
|
||||
* TEU occupied PER CORRIDOR EDGE. Containers on different legs share the
|
||||
* same physical wagon as long as no single edge exceeds the wagon's TEU
|
||||
* geometry — an intercity 20ft alighting at Adama frees its slot for a 20ft
|
||||
* boarding there, and two overlapping-leg 20fts coexist while both ride.
|
||||
*/
|
||||
teuPerEdge: number[];
|
||||
kind: SlotLoadType;
|
||||
/** Kind purity: a bulk wagon carries ONE cargo type at a time. */
|
||||
cargoTypeId: string | null;
|
||||
freeCapacityTons: number;
|
||||
/**
|
||||
* Corridor leg this slot rides (`"from-to"` stop indexes). Bookings only
|
||||
* share a slot when their legs are identical — mixing corridors in one slot
|
||||
* would degrade it to a whole-route slot (see stampSlotLegs) and silently
|
||||
* re-occupy edges the cargo never rides.
|
||||
* Leg of the FIRST booking placed (`"from-to"` stop indexes). Containers
|
||||
* prefer a same-leg slot but may extend onto a different-leg one (span
|
||||
* grows to the union); bulk still shares only on an identical leg.
|
||||
*/
|
||||
legKey: string;
|
||||
/** Contiguous stop-index span this wagon physically rides (union of its cargo legs). */
|
||||
covered: { from: number; to: number };
|
||||
};
|
||||
|
||||
/** Stop-index range a booking occupies: edges `from..to-1` of the corridor. */
|
||||
@@ -227,16 +234,54 @@ export function planWagonsWithStock(params: {
|
||||
for (let e = leg.from; e < leg.to; e += 1) row[e] = (row[e] ?? 0) + 1;
|
||||
const open: OpenSlot = {
|
||||
slot: slotFromWagonType(chosen, kind),
|
||||
teuUsed: 0,
|
||||
teuPerEdge: new Array<number>(edgeCount).fill(0),
|
||||
kind,
|
||||
cargoTypeId,
|
||||
freeCapacityTons: Number(chosen.capacityTons),
|
||||
legKey: legKeyOf(leg),
|
||||
covered: { ...leg },
|
||||
};
|
||||
openSlots.push(open);
|
||||
return open;
|
||||
};
|
||||
|
||||
/** TEU room on every edge of the unit's leg. */
|
||||
const teuFits = (open: OpenSlot, leg: BookingLeg, teu: number): boolean => {
|
||||
for (let e = leg.from; e < leg.to; e += 1) {
|
||||
if ((open.teuPerEdge[e] ?? 0) + teu > MAX_TEU_SLOTS_PER_WAGON) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the slot's ridden span can grow to include this leg: every NEW
|
||||
* edge (outside the current span) must still have a physical wagon of the
|
||||
* slot's type spare — extending the span puts this wagon on those edges.
|
||||
*/
|
||||
const canExtendSpan = (open: OpenSlot, leg: BookingLeg): boolean => {
|
||||
const total = stock.remainingByTypeId.get(open.slot.wagonTypeId) ?? 0;
|
||||
const row = usedPerEdge.get(open.slot.wagonTypeId);
|
||||
const from = Math.min(open.covered.from, leg.from);
|
||||
const to = Math.max(open.covered.to, leg.to);
|
||||
for (let e = from; e < to; e += 1) {
|
||||
if (e >= open.covered.from && e < open.covered.to) continue;
|
||||
if (total - (row?.[e] ?? 0) <= 0) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Grow the slot's span onto the leg's new edges, consuming stock there. */
|
||||
const extendSpan = (open: OpenSlot, leg: BookingLeg): void => {
|
||||
const row = usedRow(open.slot.wagonTypeId);
|
||||
const from = Math.min(open.covered.from, leg.from);
|
||||
const to = Math.max(open.covered.to, leg.to);
|
||||
for (let e = from; e < to; e += 1) {
|
||||
if (e >= open.covered.from && e < open.covered.to) continue;
|
||||
row[e] = (row[e] ?? 0) + 1;
|
||||
}
|
||||
open.covered = { from, to };
|
||||
};
|
||||
|
||||
const tryPlaceBooking = (booking: Booking): PlacementProblem | null => {
|
||||
const leg = legFor(booking);
|
||||
const legKey = legKeyOf(leg);
|
||||
@@ -260,17 +305,23 @@ export function planWagonsWithStock(params: {
|
||||
}
|
||||
const allowedIds = new Set(candidates.map((wt) => wt.id));
|
||||
const teu = unit.teuSlots ?? teuSlotsForSizeFt(unit.sizeFt ?? 20);
|
||||
let target = openSlots.find(
|
||||
(open) =>
|
||||
open.kind === 'CONTAINER' &&
|
||||
open.legKey === legKey &&
|
||||
allowedIds.has(open.slot.wagonTypeId) &&
|
||||
open.teuUsed + teu <= MAX_TEU_SLOTS_PER_WAGON,
|
||||
);
|
||||
const fitsSlot = (open: OpenSlot): boolean =>
|
||||
open.kind === 'CONTAINER' &&
|
||||
allowedIds.has(open.slot.wagonTypeId) &&
|
||||
teuFits(open, leg, teu) &&
|
||||
canExtendSpan(open, leg);
|
||||
// Same-leg slots first (keeps legacy packing byte-identical), then any
|
||||
// open wagon with per-edge TEU room — an intercity 20ft rides an
|
||||
// export wagon's spare slot instead of appending a new wagon.
|
||||
let target =
|
||||
openSlots.find((open) => open.legKey === legKey && fitsSlot(open)) ??
|
||||
openSlots.find(fitsSlot);
|
||||
if (!target) {
|
||||
const openedSlot = openSlot(candidates, 'CONTAINER', null, leg);
|
||||
if ('message' in openedSlot) return openedSlot;
|
||||
target = openedSlot;
|
||||
} else {
|
||||
extendSpan(target, leg);
|
||||
}
|
||||
addAllocation(
|
||||
target.slot,
|
||||
@@ -279,7 +330,9 @@ export function planWagonsWithStock(params: {
|
||||
unit.grossWeightTons,
|
||||
AllocationLoadType.Container,
|
||||
);
|
||||
target.teuUsed += teu;
|
||||
for (let e = leg.from; e < leg.to; e += 1) {
|
||||
target.teuPerEdge[e] = (target.teuPerEdge[e] ?? 0) + teu;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -343,7 +396,8 @@ export function planWagonsWithStock(params: {
|
||||
);
|
||||
const slotCountSnapshot = openSlots.length;
|
||||
const slotStateSnapshot = openSlots.map((open) => ({
|
||||
teuUsed: open.teuUsed,
|
||||
teuPerEdge: [...open.teuPerEdge],
|
||||
covered: { ...open.covered },
|
||||
freeCapacityTons: open.freeCapacityTons,
|
||||
assignedWeightTons: open.slot.assignedWeightTons,
|
||||
allocationCount: open.slot.allocations.length,
|
||||
@@ -363,7 +417,8 @@ export function planWagonsWithStock(params: {
|
||||
openSlots.forEach((open, index) => {
|
||||
const snap = slotStateSnapshot[index];
|
||||
if (!snap) return;
|
||||
open.teuUsed = snap.teuUsed;
|
||||
open.teuPerEdge = [...snap.teuPerEdge];
|
||||
open.covered = { ...snap.covered };
|
||||
open.freeCapacityTons = snap.freeCapacityTons;
|
||||
open.slot.assignedWeightTons = snap.assignedWeightTons;
|
||||
open.slot.allocations.length = snap.allocationCount;
|
||||
|
||||
Reference in New Issue
Block a user