leg-aware capacity and wagon sharing

This commit is contained in:
Marshal
2026-07-30 19:08:06 +00:00
parent acef6870e9
commit f891f6abe2
7 changed files with 113 additions and 25 deletions

View File

@@ -559,11 +559,14 @@ export function TrainCompositionDiagram({
// ceiling the allocation engine spends from.
const totalTare = normalized.reduce((s, w) => s + w.tareWeightTons, 0);
const grossWeight = totalWeight + totalTare;
// Weakest locomotive caps the set — same rule the allocation engine applies.
// Engine rule (combinedLocomotiveLimits): coupled locomotives pull
// TOGETHER, so their pull limits SUM.
const pullLimits = locos
.map((l) => Number(l.maxPullWeightTons))
.filter((v) => Number.isFinite(v) && v > 0);
const pullLimit = pullLimits.length ? Math.min(...pullLimits) : null;
const pullLimit = pullLimits.length
? pullLimits.reduce((sum, v) => sum + v, 0)
: null;
return {
total: normalized.length,
assigned,

View File

@@ -132,17 +132,24 @@ export const TrainConsistView = ({
0,
);
const tareUsed = wagons.reduce((sum, w) => sum + (w.tareWeightTons ?? 0), 0);
const weightUsed = cargoUsed + tareUsed;
const lengthUsed = wagons.reduce((sum, w) => sum + (w.lengthMeters ?? 0), 0);
// Prefer the server's heaviest-edge figures: on a multi-stop corridor a
// wagon reused across legs is several slots but one physical wagon, so the
// plain sums over-report the train against the pull/length/slot caps.
const heaviest = trainSet?.heaviestLeg;
const weightUsed = heaviest?.grossWeightTons ?? cargoUsed + tareUsed;
const lengthUsed =
heaviest?.lengthMeters ?? wagons.reduce((sum, w) => sum + (w.lengthMeters ?? 0), 0);
const wagonsUsed = heaviest?.loadedWagonCount ?? loadedCount;
// Weakest locomotive caps the set — same rule the allocation engine applies.
// Engine rule (combinedLocomotiveLimits): coupled locomotives pull TOGETHER,
// so pull caps SUM; the track doesn't lengthen, so the length cap is the MIN.
const locos = trainSet?.locomotives?.length
? trainSet.locomotives
: trainSet?.locomotive
? [trainSet.locomotive]
: [];
const weightMax = locos.length
? Math.min(...locos.map((l) => l.maxPullWeightTons))
? locos.reduce((sum, l) => sum + l.maxPullWeightTons, 0)
: null;
const lengthCaps = locos
.map((l) => l.maxTrainLengthMeters)
@@ -156,7 +163,7 @@ export const TrainConsistView = ({
weightMax={weightMax}
lengthUsed={lengthUsed}
lengthMax={lengthMax}
wagonCount={loadedCount}
wagonCount={wagonsUsed}
wagonMax={maxWagons}
/>

View File

@@ -1103,19 +1103,31 @@ export default function TrainScheduleV2DetailPage() {
},
{
label: "Wagons / load",
// Gross: cargo load + the tare of every wagon in the consist — the
// weight the locomotive actually hauls.
value: `${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
Math.round(
((schedule.trainSet?.totalWeightTons ?? 0) +
(schedule.trainSet?.wagons ?? []).reduce(
(sum, w) => sum + (Number(w.tareWeightTons) || 0),
0,
)) *
100,
) / 100
}T`,
hint: "gross · wagon tare + cargo",
// Gross: cargo load + wagon tare — the weight the locomotives
// actually haul. Heaviest corridor edge when the server sends it;
// plain consist sums over-report a multi-stop train (cross-leg
// wagon sharing counts one physical wagon as several slots).
value: (() => {
const heaviest = schedule.trainSet?.heaviestLeg;
const wagonCount =
heaviest?.loadedWagonCount ??
schedule.trainSet?.wagonCount ??
displayWagonPlan.length;
const grossTons =
heaviest?.grossWeightTons ??
Math.round(
((schedule.trainSet?.totalWeightTons ?? 0) +
(schedule.trainSet?.wagons ?? []).reduce(
(sum, w) => sum + (Number(w.tareWeightTons) || 0),
0,
)) *
100,
) / 100;
return `${wagonCount} · ${grossTons}T`;
})(),
hint: schedule.trainSet?.heaviestLeg
? "heaviest leg · wagon tare + cargo"
: "gross · wagon tare + cargo",
icon: Weight,
},
{

View File

@@ -608,6 +608,16 @@ export interface TrainScheduleDetail {
wagonCount: number;
totalWeightTons: number;
totalLengthMeters: number;
/**
* Usage on the corridor's heaviest edge — what the locomotives actually
* haul. Cross-leg wagon sharing makes plain slot sums over-report a
* multi-stop train. Null without a train set.
*/
heaviestLeg?: {
grossWeightTons: number;
lengthMeters: number;
loadedWagonCount: number;
} | null;
locomotive?: {
id: string;
code: string;