feat(freight): offer built-train wagons per boarding yard on multi-yard consists

This commit is contained in:
Marshal
2026-08-18 08:27:18 +00:00
parent 1ca7776143
commit c723b660e2
33 changed files with 1234 additions and 231 deletions

View File

@@ -20,17 +20,53 @@ import type { CorridorLeg } from './corridor-capacity.util';
* Gelan→Adama never competes for stock with an export on Adama→Doraleh.
*/
export class WagonStockLedger {
/**
* Usage rows keyed by pool. A single-yard train has one pool (''), so this is
* exactly the original per-type accounting. A multi-yard consist keys by
* boarding yard as well, because the Dire wagons and the Mojo wagons are
* disjoint sets of steel: 5 Dire wagons riding the whole corridor occupy the
* Mojo→Addis edge, but they must not shrink what Mojo itself can offer.
*/
private readonly usedPerEdge = new Map<string, number[]>();
constructor(
private readonly remainingByTypeId: Map<string, number>,
private readonly edgeCount: number,
/**
* Multi-yard consist only (see {@link WagonStock.byYardId}): the wagons
* standing at each yard. When present, a leg is served ONLY by the wagons
* standing at the yard it boards from — a Dire→Addis booking on a train
* whose wagons sit 20 in Dire and 33 in Mojo sees 20, and a Mojo→Addis
* booking sees 33, never the Dire wagons that ride past empty.
*/
private readonly byYardId?: Map<string, Map<string, number>>,
/** Ordered corridor stops, parallel to the edges — maps an edge to its yard. */
private readonly stops: readonly string[] = [],
) {}
/** The yard a leg boards from, or '' when the train is not split across yards. */
private poolYardOf(leg: CorridorLeg): string {
if (!this.byYardId) return '';
return this.stops[leg.fromEdge] ?? '';
}
/** Usage-row key: one row per (pool, wagon type). */
private rowKey(wagonTypeId: string, leg: CorridorLeg): string {
const pool = this.poolYardOf(leg);
return pool ? `${pool}\u0000${wagonTypeId}` : wagonTypeId;
}
/** Wagons of one type offered at the yard a leg boards from. */
private totalForType(wagonTypeId: string, leg: CorridorLeg): number {
const pool = this.poolYardOf(leg);
if (!pool) return this.remainingByTypeId.get(wagonTypeId) ?? 0;
return this.byYardId?.get(pool)?.get(wagonTypeId) ?? 0;
}
/** Free wagons of ONE type on a leg: total minus its busiest edge within that leg. */
private availableForType(wagonTypeId: string, leg: CorridorLeg): number {
const total = this.remainingByTypeId.get(wagonTypeId) ?? 0;
const row = this.usedPerEdge.get(wagonTypeId);
const total = this.totalForType(wagonTypeId, leg);
const row = this.usedPerEdge.get(this.rowKey(wagonTypeId, leg));
if (!row) return total;
let busiest = 0;
for (let edge = leg.fromEdge; edge < leg.toEdge; edge += 1) {
@@ -69,10 +105,11 @@ export class WagonStockLedger {
if (!deepest) break;
const take = Math.min(outstanding, deepest.free);
let row = this.usedPerEdge.get(deepest.id);
const key = this.rowKey(deepest.id, leg);
let row = this.usedPerEdge.get(key);
if (!row) {
row = new Array<number>(this.edgeCount).fill(0);
this.usedPerEdge.set(deepest.id, row);
this.usedPerEdge.set(key, row);
}
for (let edge = leg.fromEdge; edge < leg.toEdge; edge += 1) {
row[edge] = (row[edge] ?? 0) + take;