Reading and acting
Read material, composition and passengers
Compare configured and current profiles and calculate occupancy only from available data.
Choose the profile that answers the question
This path assumes a batch train query. includeCharacteristics requests characteristics, includeComposition requests composition and includePassengers requests occupants. Enable only what your screen uses; these options answer independent needs.
| Profile | Use |
|---|---|
| configured | Material configured for the train. Useful for the planned or purchased specification. |
| current | Currently observed material. Useful for current capabilities and passenger capacity. |
| purchasedDynamics / currentDynamics | Driving profiles in the targeted JVM read, independent of one another. |
Preserve SDK units
| Property | Unit | Interpretation |
|---|---|---|
| maximumSpeedMps / maximumSpeedKmh | m/s / km/h | Material maximum speed; conversion to km/h is already exposed. |
| maximumAccelerationMps2 | m/s² | Profile maximum acceleration, not measured acceleration. |
| tractiveForceN | N | Tractive force; divide by 1,000 for kN. |
| powerW | W | Power; divide by 1,000 for kW. |
| emptyMassKg | kg | Empty mass, not a loaded mass. |
| lengthM | m | Train-profile length. |
| passengerCapacity | people | Declared capacity, distinct from current occupants. |
Keep native units in your models and convert for display. In a Native card, observed speed is in m/s; the JVM batch Train row uses speedKmh. The targeted JVM read uses speedMps again. Its TrainDynamics uses maxSpeedMps, maxAccelerationMps2 and tractiveEffortN, and also exposes serviceBrakingMps2 and emergencyBrakingMps2 in m/s². Those names belong to the targeted type, not to TrainCharacteristics.
Preserve vehicle order and identity
TrainCharacteristics.composition is a nullable list of TrainVehicle. Its order and repeated modelId values describe the formation: do not turn it into a set. Each vehicle carries its model and modelId. The batch also exposes vehicleModels for displaying referenced models; unknown labels do not erase model identity.
VehicleModel exposes available model information, including its source label. That label is not an SDK-provided translation. Distinguish composition-item count from car count: they describe different levels and cannot automatically substitute for one another.
Calculate occupancy without inventing zero
An occupancy percentage requires a known occupant count and known, strictly positive current capacity. Zero capacity makes the ratio undefined; missing data does not become zero. The result may exceed 100% if observed values imply it: do not silently clamp it.
Produce testable material cards
package wiki.trainmaterial
import nimby.*
data class MaterialCard(
val id: TrainId,
val configured: TrainCharacteristics?,
val current: TrainCharacteristics?,
val passengers: Int?,
val occupancyPercent: Double?,
)
fun readMaterial(context: ToolContext): List<MaterialCard> {
val snapshot = context.trains(TrainQuery(
includeService = false,
includeLocations = false,
includeCharacteristics = true,
includeComposition = true,
includePassengers = true,
))
return snapshot.trains.map { train ->
MaterialCard(
train.trainId, train.configured, train.current, train.passengers,
occupancyPercent(train.passengers, train.current),
)
}
}
// No substitute capacity when current material is unknown.
fun occupancyPercent(passengers: Int?, current: TrainCharacteristics?): Double? {
val capacity = current?.passengerCapacity ?: return null
if (passengers == null || capacity <= 0) return null
return passengers.toDouble() * 100.0 / capacity
}
// Model order and repetitions describe the composition.
fun orderedModelIds(profile: TrainCharacteristics?): List<VehicleModelId>? =
profile?.composition?.map { it.modelId }
package wiki.jvmtrainmaterial
import fr.nimby.sdk.*
data class MaterialCard(
val id: TrainId,
val configured: TrainCharacteristics?,
val current: TrainCharacteristics?,
val passengers: Int?,
val occupancyPercent: Double?,
)
fun readMaterial(game: Game): List<MaterialCard> {
val snapshot = game.trains.snapshot(query = TrainQuery(
includeService = false,
includeLocations = false,
includeCharacteristics = true,
includeComposition = true,
includePassengers = true,
))
return snapshot.trains.mapNotNull { train ->
val record = snapshot.train(train.trainId) ?: return@mapNotNull null
val current = record.metadata?.current
val passengers = record.details?.passengers
MaterialCard(
train.trainId, record.metadata?.configured, current, passengers,
occupancyPercent(passengers, current),
)
}
}
// An unknown occupant count never becomes zero.
fun occupancyPercent(passengers: Int?, current: TrainCharacteristics?): Double? {
val capacity = current?.passengerCapacity ?: return null
if (passengers == null || capacity <= 0) return null
return passengers.toDouble() * 100.0 / capacity
}
fun orderedModelIds(profile: TrainCharacteristics?): List<VehicleModelId>? =
profile?.composition?.map { it.modelId }
The functions return a card per train while retaining both profiles. occupancyPercent is pure: test missing occupants, a missing profile, zero capacity and positive capacity. orderedModelIds preserves duplicates and order; a null result means composition is unknown.