Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.ts
Marshal e0c3044933 feat(train-sets): implement multi-locomotive support for train sets
- Added TrainSetLocomotive entity to link multiple locomotives to a train set.
- Updated TrainSet entity to include a OneToMany relationship with TrainSetLocomotive.
- Modified the train scheduling logic to require at least two locomotives for a train set.
- Enhanced the UI components to support selecting multiple locomotives.
- Introduced new permissions for viewing customs clearance.
- Updated migrations to create the train_set_locomotives table and backfill existing data.
- Implemented utility functions for managing train numbers based on cargo type and direction.
- Added tests for train number utilities to ensure correct functionality.
2026-06-24 23:54:45 +00:00

110 lines
3.5 KiB
TypeScript

/** Physical dimensions used when deriving how many wagons a locomotive can pull. */
export type WagonTypeDimensions = {
lengthMeters: number;
capacityTons: number;
};
export type LocomotiveLimits = {
maxPullWeightTons: number;
maxTrainLengthMeters: number;
};
export type DerivedTrainCapacity = {
maxWeightTons: number;
maxLengthMeters: number;
maxWagonSlots: number;
};
const DEFAULT_WAGON_LENGTH_M = 14;
const DEFAULT_WAGON_CAPACITY_T = 70;
/**
* Derive train capacity from locomotive pull weight and train length.
* Wagon count is NOT a fixed 53 — it is the minimum of:
* - floor(maxLength / shortest wagon type length)
* - floor(maxWeight / lightest wagon type capacity)
*/
export function deriveTrainCapacityFromLocomotive(
locomotive: LocomotiveLimits,
wagonTypes: WagonTypeDimensions[],
ruleCaps?: { maxTrainWeightTons?: number; maxTrainLengthMeters?: number },
): DerivedTrainCapacity {
const maxWeightTons = Math.min(
Number(locomotive.maxPullWeightTons) || Infinity,
ruleCaps?.maxTrainWeightTons ?? Infinity,
);
const maxLengthMeters = Math.min(
Number(locomotive.maxTrainLengthMeters) || Infinity,
ruleCaps?.maxTrainLengthMeters ?? Infinity,
);
const types =
wagonTypes.length > 0
? wagonTypes
: [{ lengthMeters: DEFAULT_WAGON_LENGTH_M, capacityTons: DEFAULT_WAGON_CAPACITY_T }];
const minLength = Math.min(...types.map((w) => Number(w.lengthMeters) || DEFAULT_WAGON_LENGTH_M));
const minCapacity = Math.min(
...types.map((w) => Number(w.capacityTons) || DEFAULT_WAGON_CAPACITY_T),
);
const byLength =
minLength > 0 && Number.isFinite(maxLengthMeters)
? Math.floor(maxLengthMeters / minLength)
: 0;
const byWeight =
minCapacity > 0 && Number.isFinite(maxWeightTons)
? Math.floor(maxWeightTons / minCapacity)
: byLength;
const maxWagonSlots = Math.max(0, Math.min(byLength, byWeight));
return {
maxWeightTons: Number.isFinite(maxWeightTons) ? maxWeightTons : MAX_FALLBACK_WEIGHT,
maxLengthMeters: Number.isFinite(maxLengthMeters) ? maxLengthMeters : MAX_FALLBACK_LENGTH,
maxWagonSlots,
};
}
export const MAX_FALLBACK_WEIGHT = 3500;
export const MAX_FALLBACK_LENGTH = 760;
/**
* Effective pull limits for a train set with multiple locomotives: the weakest
* locomotive caps the train, so take the minimum pull weight and minimum length
* across all assigned locomotives. Returns null when no locomotives are given.
*/
export function minLocomotiveLimits(
locomotives: Array<Pick<LocomotiveLimits, 'maxPullWeightTons' | 'maxTrainLengthMeters'>>,
): LocomotiveLimits | null {
if (!locomotives.length) return null;
return {
maxPullWeightTons: Math.min(
...locomotives.map((l) => Number(l.maxPullWeightTons) || Infinity),
),
maxTrainLengthMeters: Math.min(
...locomotives.map((l) => Number(l.maxTrainLengthMeters) || Infinity),
),
};
}
/** Per-booking train length from wagon count and freight-specific wagon type length. */
export function bookingTrainLengthMeters(
freightType: string | null | undefined,
wagonCount: number,
lengths: { container: number; bulk: number },
): number {
const perWagon = freightType === 'BULK' ? lengths.bulk : lengths.container;
return wagonCount * perWagon;
}
export function wagonTypeDimensionsFromEntity(wt: {
lengthMeters?: number | string | null;
capacityTons?: number | string | null;
}): WagonTypeDimensions {
return {
lengthMeters: Number(wt.lengthMeters) || DEFAULT_WAGON_LENGTH_M,
capacityTons: Number(wt.capacityTons) || DEFAULT_WAGON_CAPACITY_T,
};
}