NRF SDK 0.9

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.

ProfileUse
configuredMaterial configured for the train. Useful for the planned or purchased specification.
currentCurrently observed material. Useful for current capabilities and passenger capacity.
purchasedDynamics / currentDynamicsDriving profiles in the targeted JVM read, independent of one another.

Preserve SDK units

PropertyUnitInterpretation
maximumSpeedMps / maximumSpeedKmhm/s / km/hMaterial maximum speed; conversion to km/h is already exposed.
maximumAccelerationMps2m/s²Profile maximum acceleration, not measured acceleration.
tractiveForceNNTractive force; divide by 1,000 for kN.
powerWWPower; divide by 1,000 for kW.
emptyMassKgkgEmpty mass, not a loaded mass.
lengthMmTrain-profile length.
passengerCapacitypeopleDeclared 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

Native: profiles, capacity and model order
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 }
JVM: the same questions with explicit batch joins
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.