• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

traintastic / traintastic / 23364060587

20 Mar 2026 09:51PM UTC coverage: 28.011% (+0.03%) from 27.986%
23364060587

push

github

reinder
[train] added acceleration/braking rate properties

10 of 12 new or added lines in 1 file covered. (83.33%)

1 existing line in 1 file now uncovered.

8182 of 29210 relevant lines covered (28.01%)

194.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.27
/server/src/train/train.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2019-2026 Reinder Feenstra
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
 */
21

22
#include "train.hpp"
23
#include "trainlist.hpp"
24
#include "trainvehiclelist.hpp"
25
#include "../world/world.hpp"
26
#include "trainblockstatus.hpp"
27
#include "trainlisttablemodel.hpp"
28
#include "../core/attributes.hpp"
29
#include "../core/errorcode.hpp"
30
#include "../core/method.tpp"
31
#include "../core/objectproperty.tpp"
32
#include "../core/objectvectorproperty.tpp"
33
#include "../core/eventloop.hpp"
34
#include "../board/tile/rail/blockrailtile.hpp"
35
#include "../vehicle/rail/poweredrailvehicle.hpp"
36
#include "../hardware/decoder/decoder.hpp"
37
#include "../log/log.hpp"
38
#include "../throttle/throttle.hpp"
39
#include "../utils/almostzero.hpp"
40
#include "../utils/displayname.hpp"
41
#include "../utils/unit.hpp"
42
#include "../zone/zone.hpp"
43

44
CREATE_IMPL(Train)
22✔
45

46
namespace {
47

48
constexpr double accelerationRateDefault = 1.0;
49
constexpr double accelerationRateMin = 0.1;
50
constexpr double accelerationRateMax = 10.0;
51
constexpr double accelerationRateStep = 0.1;
52

53
constexpr double brakingRateDefault = 0.5;
54
constexpr double brakingRateMin = 0.1;
55
constexpr double brakingRateMax = 10.0;
56
constexpr double brakingRateStep = 0.1;
57

58
}
59

UNCOV
60
static inline bool isPowered(const RailVehicle& vehicle)
×
61
{
62
  return dynamic_cast<const PoweredRailVehicle*>(&vehicle);
×
63
}
64

65
Train::Train(World& world, std::string_view _id) :
22✔
66
  IdObject(world, _id),
67
  m_speedTimer{EventLoop::ioContext()},
22✔
68
  name{this, "name", id, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
22✔
69
  length{*this, "length", 0, LengthUnit::MilliMeter, PropertyFlags::ReadWrite | PropertyFlags::Store},
22✔
70
  overrideLength{this, "override_length", false, PropertyFlags::ReadWrite | PropertyFlags::Store,
44✔
71
    [this](bool value)
22✔
72
    {
73
      Attributes::setEnabled(length, value);
×
74
      if(!value)
×
75
        updateLength();
×
76
    }},
×
77
  direction{this, "direction", Direction::Forward, PropertyFlags::ReadWrite | PropertyFlags::StoreState,
44✔
78
    [this](Direction value)
22✔
79
    {
80
      // update train direction from the block perspective:
81
      for(const auto& status : *blocks)
×
82
        status->direction.setValueInternal(!status->direction.value());
×
83
      blocks.reverseInternal(); // index 0 is head of train
×
84

85
      for(const auto& vehicle : m_poweredVehicles)
×
86
        vehicle->setDirection(value);
×
87
      updateEnabled();
×
88
    },
×
89
    [](Direction& value)
22✔
90
    {
91
      // only accept valid direction values, don't accept Unknown direction
92
      return value == Direction::Forward || value == Direction::Reverse;
×
93
    }},
94
  isStopped{this, "is_stopped", true, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
95
  speed{*this, "speed", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore},
22✔
96
  speedMax{*this, "speed_max", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
97
  speedLimit{*this, "speed_limit", SpeedLimitProperty::noLimitValue, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
98
  throttleSpeed{*this, "throttle_speed", 0, SpeedUnit::KiloMeterPerHour, PropertyFlags::ReadWrite | PropertyFlags::StoreState,
44✔
99
    [this](double value, SpeedUnit unit)
22✔
100
    {
101
      const double currentSpeed = speed.getValue(unit);
×
102

103
      emergencyStop.setValueInternal(false);
×
104

105
      if(value > currentSpeed) // Accelerate
×
106
      {
107
        if(m_speedState == SpeedState::Accelerate)
×
108
          return;
×
109

110
        m_speedTimer.cancel();
×
111
        m_speedState = SpeedState::Accelerate;
×
112
        updateSpeed();
×
113
      }
114
      else if(value < currentSpeed) // brake
×
115
      {
116
        if(m_speedState == SpeedState::Braking)
×
117
          return;
×
118

119
        m_speedTimer.cancel();
×
120
        m_speedState = SpeedState::Braking;
×
121
        updateSpeed();
×
122
      }
123
    }},
124
  stop{*this, "stop", MethodFlags::ScriptCallable,
44✔
125
    [this]()
22✔
126
    {
127
      throttleSpeed.setValue(0);
×
128
    }},
×
129
  emergencyStop{this, "emergency_stop", true, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadWrite,
44✔
130
    [this](bool value)
22✔
131
    {
132
      if(value)
×
133
      {
134
        m_speedState = SpeedState::Idle;
×
135
        m_speedTimer.cancel();
×
136
        throttleSpeed.setValueInternal(0);
×
137
        speed.setValueInternal(0);
×
138
        isStopped.setValueInternal(true);
×
139
        updateEnabled();
×
140
      }
141

142
      if(active)
×
143
      {
144
        //Propagate to all vehicles in this Train
145
        for(const auto& vehicle : m_poweredVehicles)
×
146
          vehicle->setEmergencyStop(value);
×
147
      }
148
    }},
×
149
  weight{*this, "weight", 0, WeightUnit::Ton, PropertyFlags::ReadWrite | PropertyFlags::Store},
22✔
150
  overrideWeight{this, "override_weight", false, PropertyFlags::ReadWrite | PropertyFlags::Store,
44✔
151
    [this](bool value)
22✔
152
    {
153
      Attributes::setEnabled(weight, value);
×
154
      if(!value)
×
155
        updateWeight();
×
156
    }},
×
157
  accelerationRate{this, "acceleration_rate", accelerationRateDefault, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
22✔
158
  brakingRate{this, "braking_rate", brakingRateDefault, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
22✔
159
  vehicles{this, "vehicles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::Store | PropertyFlags::SubObject},
22✔
160
  powered{this, "powered", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
22✔
161
  active{this, "active", false, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly,
44✔
162
    [this](bool value)
22✔
163
    {
164
      updateSpeed();
24✔
165
      if(!value && m_throttle)
24✔
166
      {
167
        m_throttle->release();
×
168
      }
169
    },
24✔
170
    std::bind(&Train::setTrainActive, this, std::placeholders::_1)}
44✔
171
  , mode{this, "mode", TrainMode::ManualUnprotected, PropertyFlags::ReadWrite | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
22✔
172
  , mute{this, "mute", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
22✔
173
  , noSmoke{this, "no_smoke", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
22✔
174
  , hasThrottle{this, "has_throttle", false, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
22✔
175
  , throttleName{this, "throttle_name", "", PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly}
44✔
176
  , blocks{*this, "blocks", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
22✔
177
  , zones{*this, "zones", {}, PropertyFlags::ReadOnly | PropertyFlags::StoreState | PropertyFlags::ScriptReadOnly}
22✔
178
  , notes{this, "notes", "", PropertyFlags::ReadWrite | PropertyFlags::Store}
44✔
179
  , onBlockAssigned{*this, "on_block_assigned", EventFlags::Scriptable}
22✔
180
  , onBlockReserved{*this, "on_block_reserved", EventFlags::Scriptable}
22✔
181
  , onBlockEntered{*this, "on_block_entered", EventFlags::Scriptable}
22✔
182
  , onBlockLeft{*this, "on_block_left", EventFlags::Scriptable}
22✔
183
  , onBlockRemoved{*this, "on_block_removed", EventFlags::Scriptable}
22✔
184
  , onZoneAssigned{*this, "on_zone_assigned", EventFlags::Scriptable}
22✔
185
  , onZoneEntering{*this, "on_zone_entering", EventFlags::Scriptable}
22✔
186
  , onZoneEntered{*this, "on_zone_entered", EventFlags::Scriptable}
22✔
187
  , onZoneLeaving{*this, "on_zone_leaving", EventFlags::Scriptable}
22✔
188
  , onZoneLeft{*this, "on_zone_left", EventFlags::Scriptable}
22✔
189
  , onZoneRemoved{*this, "on_zone_removed", EventFlags::Scriptable}
66✔
190
{
191
  vehicles.setValueInternal(std::make_shared<TrainVehicleList>(*this, vehicles.name()));
22✔
192

193
  Attributes::addDisplayName(name, DisplayName::Object::name);
22✔
194
  Attributes::addEnabled(name, false);
22✔
195
  m_interfaceItems.add(name);
22✔
196
  Attributes::addEnabled(length, overrideLength);
22✔
197
  m_interfaceItems.add(length);
22✔
198
  m_interfaceItems.add(overrideLength);
22✔
199
  Attributes::addEnabled(direction, false);
22✔
200
  Attributes::addValues(direction, DirectionValues);
22✔
201
  Attributes::addObjectEditor(direction, false);
22✔
202
  m_interfaceItems.add(direction);
22✔
203

204
  Attributes::addObjectEditor(isStopped, false);
22✔
205
  m_interfaceItems.add(isStopped);
22✔
206

207
  Attributes::addObjectEditor(speed, false);
22✔
208
  Attributes::addMinMax(speed, 0., 0., SpeedUnit::KiloMeterPerHour);
22✔
209
  m_interfaceItems.add(speed);
22✔
210
  m_interfaceItems.add(speedMax);
22✔
211

212
  Attributes::addObjectEditor(speedLimit, false);
22✔
213
  m_interfaceItems.add(speedLimit);
22✔
214

215
  Attributes::addMinMax(throttleSpeed, 0., 0., SpeedUnit::KiloMeterPerHour);
22✔
216
  Attributes::addEnabled(throttleSpeed, false);
22✔
217
  Attributes::addObjectEditor(throttleSpeed, false);
22✔
218
  m_interfaceItems.add(throttleSpeed);
22✔
219

220
  Attributes::addEnabled(stop, false);
22✔
221
  Attributes::addObjectEditor(stop, false);
22✔
222
  m_interfaceItems.add(stop);
22✔
223

224
  Attributes::addEnabled(emergencyStop, false);
22✔
225
  Attributes::addObjectEditor(emergencyStop, false);
22✔
226
  m_interfaceItems.add(emergencyStop);
22✔
227

228
  Attributes::addEnabled(weight, overrideWeight);
22✔
229
  m_interfaceItems.add(weight);
22✔
230
  m_interfaceItems.add(overrideWeight);
22✔
231

232
  Attributes::addMinMax(accelerationRate, accelerationRateMin, accelerationRateMax);
22✔
233
  Attributes::addStep(accelerationRate, accelerationRateStep);
22✔
234
  Attributes::addUnit(accelerationRate, Unit::meterPerSecondSquared);
22✔
235
  m_interfaceItems.add(accelerationRate);
22✔
236

237
  Attributes::addMinMax(brakingRate, brakingRateMin, brakingRateMax);
22✔
238
  Attributes::addStep(brakingRate, brakingRateStep);
22✔
239
  Attributes::addUnit(brakingRate, Unit::meterPerSecondSquared);
22✔
240
  m_interfaceItems.add(brakingRate);
22✔
241

242
  m_interfaceItems.add(vehicles);
22✔
243

244
  Attributes::addEnabled(active, true);
22✔
245
  m_interfaceItems.add(active);
22✔
246

247
  Attributes::addValues(mode, trainModeValues);
22✔
248
  Attributes::addObjectEditor(mode, false);
22✔
249
  m_interfaceItems.add(mode);
22✔
250

251
  Attributes::addObjectEditor(mute, false);
22✔
252
  m_interfaceItems.add(mute);
22✔
253

254
  Attributes::addObjectEditor(noSmoke, false);
22✔
255
  m_interfaceItems.add(noSmoke);
22✔
256

257
  Attributes::addObjectEditor(hasThrottle, false);
22✔
258
  m_interfaceItems.add(hasThrottle);
22✔
259

260
  Attributes::addObjectEditor(throttleName, false);
22✔
261
  m_interfaceItems.add(throttleName);
22✔
262

263
  Attributes::addObjectEditor(blocks, false);
22✔
264
  m_interfaceItems.add(blocks);
22✔
265

266
  Attributes::addObjectEditor(zones, false);
22✔
267
  m_interfaceItems.add(zones);
22✔
268

269
  Attributes::addObjectEditor(powered, false);
22✔
270
  m_interfaceItems.add(powered);
22✔
271
  Attributes::addDisplayName(notes, DisplayName::Object::notes);
22✔
272
  m_interfaceItems.add(notes);
22✔
273

274
  m_interfaceItems.add(onBlockAssigned);
22✔
275
  m_interfaceItems.add(onBlockReserved);
22✔
276
  m_interfaceItems.add(onBlockEntered);
22✔
277
  m_interfaceItems.add(onBlockLeft);
22✔
278
  m_interfaceItems.add(onBlockRemoved);
22✔
279
  m_interfaceItems.add(onZoneAssigned);
22✔
280
  m_interfaceItems.add(onZoneEntering);
22✔
281
  m_interfaceItems.add(onZoneEntered);
22✔
282
  m_interfaceItems.add(onZoneLeaving);
22✔
283
  m_interfaceItems.add(onZoneLeft);
22✔
284
  m_interfaceItems.add(onZoneRemoved);
22✔
285

286
  updateEnabled();
22✔
287
  updateMute();
22✔
288
  updateNoSmoke();
22✔
289
}
22✔
290

291
void Train::updateMute()
29✔
292
{
293
  bool value = contains(m_world.state, WorldState::Mute);
29✔
294
  if(!value)
29✔
295
  {
296
    for(const auto& zoneStatus : zones)
30✔
297
    {
298
      if(zoneStatus->zone->mute)
4✔
299
      {
300
        value = true;
3✔
301
        break;
3✔
302
      }
303
    }
304
  }
305
  if(value != mute)
29✔
306
  {
307
    mute.setValueInternal(value);
6✔
308
    if(active)
6✔
309
    {
310
      for(const auto& vehicle : *vehicles)
8✔
311
      {
312
        vehicle->updateMute();
4✔
313
      }
314
    }
315
  }
316
}
29✔
317

318
void Train::updateNoSmoke()
29✔
319
{
320
  bool value = contains(m_world.state, WorldState::NoSmoke);
29✔
321
  if(!value)
29✔
322
  {
323
    for(const auto& zoneStatus : zones)
30✔
324
    {
325
      if(zoneStatus->zone->noSmoke)
4✔
326
      {
327
        value = true;
3✔
328
        break;
3✔
329
      }
330
    }
331
  }
332
  if(value != noSmoke)
29✔
333
  {
334
    noSmoke.setValueInternal(value);
6✔
335
    if(active)
6✔
336
    {
337
      for(const auto& vehicle : *vehicles)
8✔
338
      {
339
        vehicle->updateNoSmoke();
4✔
340
      }
341
    }
342
  }
343
}
29✔
344

345
void Train::updateSpeedLimit()
6✔
346
{
347
  double value = SpeedLimitProperty::noLimitValue;
6✔
348
  SpeedUnit unit = speedLimit.unit();
6✔
349
  for(const auto& zoneStatus : zones)
10✔
350
  {
351
    const auto& zoneSpeedLimit = zoneStatus->zone->speedLimit;
4✔
352
    if(zoneSpeedLimit.getValue(unit) < value)
4✔
353
    {
354
      unit = zoneSpeedLimit.unit();
3✔
355
      value = zoneSpeedLimit.getValue(unit);
3✔
356
    }
357
  }
358
  if(value != speedLimit.getValue(unit))
6✔
359
  {
360
    speedLimit.setValueInternal(value, unit);
6✔
361
    //! \todo Apply to train
362
  }
363
}
6✔
364

365
void Train::addToWorld()
22✔
366
{
367
  IdObject::addToWorld();
22✔
368
  m_world.trains->addObject(shared_ptr<Train>());
22✔
369
}
22✔
370

371
void Train::destroying()
22✔
372
{
373
  auto self = shared_ptr<Train>();
22✔
374
  for(const auto& vehicle : *vehicles)
37✔
375
  {
376
    vehicle->trains.removeInternal(self);
15✔
377
  }
378
  m_world.trains->removeObject(self);
22✔
379
  IdObject::destroying();
22✔
380
}
22✔
381

382
void Train::loaded()
1✔
383
{
384
  IdObject::loaded();
1✔
385

386
  Attributes::setEnabled(length, overrideLength);
1✔
387
  Attributes::setEnabled(weight, overrideWeight);
1✔
388

389
  auto self = shared_ptr<Train>();
1✔
390
  for(auto& vehicle : *vehicles)
2✔
391
  {
392
    vehicle->trains.appendInternal(self);
1✔
393
  }
394

395
  vehiclesChanged();
1✔
396
  updateMute();
1✔
397
  updateNoSmoke();
1✔
398

399
  if(active)
1✔
400
  {
401
    for(const auto& vehicle : m_poweredVehicles)
×
402
    {
403
      vehicle->setDirection(direction);
×
404
    }
405
  }
406
}
1✔
407

408
void Train::worldEvent(WorldState state, WorldEvent event)
7✔
409
{
410
  IdObject::worldEvent(state, event);
7✔
411

412
  switch(event)
7✔
413
  {
414
    case WorldEvent::EditEnabled:
×
415
    case WorldEvent::EditDisabled:
416
      updateEnabled();
×
417
      break;
×
418

419
    case WorldEvent::Mute:
×
420
    case WorldEvent::Unmute:
421
      updateMute();
×
422
      break;
×
423

424
    case WorldEvent::NoSmoke:
×
425
    case WorldEvent::Smoke:
426
      updateNoSmoke();
×
427
      break;
×
428

429
    default:
7✔
430
      break;
7✔
431
  }
432
}
7✔
433

434
void Train::setSpeed(const double kmph)
×
435
{
436
  for(const auto& vehicle : m_poweredVehicles)
×
437
    vehicle->setSpeed(kmph);
×
438
  speed.setValueInternal(convertUnit(kmph, SpeedUnit::KiloMeterPerHour, speed.unit()));
×
439
  updateEnabled();
×
440
}
×
441

442
void Train::updateSpeed()
24✔
443
{
444
  if(m_speedState == SpeedState::Idle)
24✔
445
    return;
24✔
446

447
  if(m_speedState == SpeedState::Accelerate && !active)
×
448
    return;
×
449

450
  const double targetSpeed = throttleSpeed.getValue(SpeedUnit::MeterPerSecond);
×
451
  double currentSpeed = speed.getValue(SpeedUnit::MeterPerSecond);
×
452

453
  double acceleration = 0;
×
454
  if(m_speedState == SpeedState::Accelerate)
×
455
  {
456
    //! \todo add realistic acceleration
NEW
457
    acceleration = accelerationRate; // m/s^2
×
458
  }
459
  else if(m_speedState == SpeedState::Braking)
×
460
  {
461
    //! \todo add realistic braking
NEW
462
    acceleration = -brakingRate; // m/s^2
×
463
  }
464
  else
465
    assert(false);
×
466

467
  currentSpeed += acceleration * 0.1; // x 100ms
×
468

469
  if((m_speedState == SpeedState::Accelerate && currentSpeed >= targetSpeed) ||
×
470
      (m_speedState == SpeedState::Braking && currentSpeed <= targetSpeed))
×
471
  {
472
    m_speedState = SpeedState::Idle;
×
473
    setSpeed(convertUnit(targetSpeed, SpeedUnit::MeterPerSecond, SpeedUnit::KiloMeterPerHour));
×
474
    currentSpeed = targetSpeed;
×
475
  }
476
  else
477
  {
478
    using namespace std::literals;
479
    setSpeed(convertUnit(currentSpeed, SpeedUnit::MeterPerSecond, SpeedUnit::KiloMeterPerHour));
×
480
    m_speedTimer.expires_after(100ms);
×
481
    m_speedTimer.async_wait(
×
482
      [this](const boost::system::error_code& ec)
×
483
      {
484
        if(!ec)
×
485
          updateSpeed();
×
486
      });
×
487
  }
488

489
  const bool currentValue = isStopped;
×
490
  isStopped.setValueInternal(m_speedState == SpeedState::Idle && almostZero(currentSpeed) && almostZero(targetSpeed));
×
491
  if(currentValue != isStopped)
×
492
    updateEnabled();
×
493
}
494

495
void Train::vehiclesChanged()
17✔
496
{
497
  updateLength();
17✔
498
  updateWeight();
17✔
499
  updatePowered();
17✔
500
  updateSpeedMax();
17✔
501
  updateEnabled();
17✔
502
}
17✔
503

504
void Train::updateLength()
17✔
505
{
506
  if(overrideLength)
17✔
507
    return;
×
508

509
  double mm = 0;
17✔
510
  for(const auto& vehicle : *vehicles)
33✔
511
    mm += vehicle->length.getValue(LengthUnit::MilliMeter);
16✔
512
  length.setValueInternal(convertUnit(mm, LengthUnit::MilliMeter, length.unit()));
17✔
513
}
514

515
void Train::updateWeight()
17✔
516
{
517
  if(overrideWeight)
17✔
518
    return;
×
519

520
  double ton = 0;
17✔
521
  for(const auto& vehicle : *vehicles)
33✔
522
    ton += vehicle->totalWeight.getValue(WeightUnit::Ton);
16✔
523
  weight.setValueInternal(convertUnit(ton, WeightUnit::Ton, weight.unit()));
17✔
524
}
525

526
void Train::updatePowered()
17✔
527
{
528
  m_poweredVehicles.clear();
17✔
529
  for(const auto& vehicle : *vehicles)
33✔
530
    if(auto poweredVehicle = std::dynamic_pointer_cast<PoweredRailVehicle>(vehicle))
16✔
531
      m_poweredVehicles.emplace_back(poweredVehicle);
16✔
532
  powered.setValueInternal(!m_poweredVehicles.empty());
17✔
533
}
17✔
534

535
void Train::updateSpeedMax()
17✔
536
{
537
  if(!vehicles->empty() && powered)
17✔
538
  {
539
    const auto itEnd = vehicles->end();
16✔
540
    auto it = vehicles->begin();
16✔
541
    double kmph = (*it)->speedMax.getValue(SpeedUnit::KiloMeterPerHour);
16✔
542
    for(; it != itEnd; ++it)
32✔
543
    {
544
      const double v = (*it)->speedMax.getValue(SpeedUnit::KiloMeterPerHour);
16✔
545
      if((v > 0 || isPowered(**it)) && v < kmph)
16✔
546
        kmph = v;
×
547
    }
548
    speedMax.setValueInternal(convertUnit(kmph, SpeedUnit::KiloMeterPerHour, speedMax.unit()));
16✔
549
  }
550
  else
551
    speedMax.setValueInternal(0);
1✔
552

553
  speed.setUnit(speedMax.unit());
17✔
554
  Attributes::setMax(speed, speedMax.value(), speedMax.unit());
17✔
555
  throttleSpeed.setUnit(speedMax.unit());
17✔
556
  Attributes::setMax(throttleSpeed, speedMax.value(), speedMax.unit());
17✔
557
}
17✔
558

559
void Train::updateEnabled()
39✔
560
{
561
  const bool stopped = isStopped;
39✔
562
  const bool editable = contains(m_world.state, WorldState::Edit);
39✔
563

564
  Attributes::setEnabled(name, stopped && editable);
39✔
565
  Attributes::setEnabled(direction, stopped && powered);
39✔
566
  Attributes::setEnabled(throttleSpeed, powered);
39✔
567
  Attributes::setEnabled(stop, powered);
39✔
568
  Attributes::setEnabled(emergencyStop, powered);
39✔
569
  Attributes::setEnabled(vehicles->add, stopped);
39✔
570
  Attributes::setEnabled(vehicles->remove, stopped);
39✔
571
  Attributes::setEnabled(vehicles->move, stopped);
39✔
572
  Attributes::setEnabled(vehicles->reverse, stopped);
39✔
573
}
39✔
574

575
bool Train::setTrainActive(bool val)
25✔
576
{
577
  auto self = this->shared_ptr<Train>();
25✔
578

579
  if(val)
25✔
580
  {
581
    if(vehicles->empty())
13✔
582
    {
583
      return false; // activating a train without vehicles is useless.
1✔
584
    }
585

586
    //To activate a train, ensure all vehicles are stopped and free
587
    for(const auto& vehicle : *vehicles)
24✔
588
    {
589
      assert(vehicle->activeTrain.value() != self);
12✔
590
      if(vehicle->activeTrain.value())
12✔
591
      {
592
        return false; //Not free
×
593
      }
594

595
      if(auto decoder = vehicle->decoder.value(); decoder && !almostZero(decoder->throttle.value()))
12✔
596
      {
597
        return false; //Already running
×
598
      }
12✔
599
    }
600

601
    //Now really activate
602
    //Register this train as activeTrain
603
    for(const auto& vehicle : *vehicles)
24✔
604
    {
605
      vehicle->setActiveTrain(self);
12✔
606
    }
607

608
    //Sync Emergency Stop state
609
    const bool stopValue = emergencyStop;
12✔
610
    for(const auto& vehicle : m_poweredVehicles)
24✔
611
      vehicle->setEmergencyStop(stopValue);
12✔
612
  }
613
  else
614
  {
615
    //To deactivate a Train it must be stopped first
616
    if(!isStopped)
12✔
617
      return false;
×
618

619
    //Deactivate all vehicles
620
    for(const auto& vehicle : *vehicles)
24✔
621
    {
622
      assert(vehicle->activeTrain.value() == self);
12✔
623
      vehicle->setActiveTrain(nullptr);
12✔
624
    }
625
  }
626

627
  return true;
24✔
628
}
25✔
629

630
std::error_code Train::acquire(Throttle& throttle, bool steal)
×
631
{
632
  if(m_throttle)
×
633
  {
634
    if(!steal)
×
635
    {
636
      return make_error_code(ErrorCode::AlreadyAcquired);
×
637
    }
638
    m_throttle->release();
×
639
  }
640
  if(!active)
×
641
  {
642
    try
643
    {
644
      active = true; // TODO: activate();
×
645
    }
646
    catch(...)
×
647
    {
648
    }
×
649
    if(!active)
×
650
    {
651
      return make_error_code(ErrorCode::CanNotActivateTrain);
×
652
    }
653
  }
654
  assert(!m_throttle);
×
655
  m_throttle = throttle.shared_ptr<Throttle>();
×
656
  hasThrottle.setValueInternal(true);
×
657
  throttleName.setValueInternal(m_throttle->name);
×
658
  return {};
×
659
}
660

661
std::error_code Train::release(Throttle& throttle)
×
662
{
663
  if(m_throttle.get() != &throttle)
×
664
  {
665
    return make_error_code(ErrorCode::InvalidThrottle);
×
666
  }
667
  m_throttle.reset();
×
668
  hasThrottle.setValueInternal(false);
×
669
  throttleName.setValueInternal("");
×
670
  if(isStopped && blocks.empty())
×
671
  {
672
    active = false; // deactive train if it is stopped and not assigned to a block
×
673
  }
674
  return {};
×
675
}
676

677
std::error_code Train::setSpeed(Throttle& throttle, double value)
×
678
{
679
  if(m_throttle.get() != &throttle)
×
680
  {
681
    return make_error_code(ErrorCode::InvalidThrottle);
×
682
  }
683
  assert(active);
×
684

685
  value = std::clamp(value, Attributes::getMin(speed), Attributes::getMax(speed));
×
686

687
  setSpeed(convertUnit(value, speed.unit(), SpeedUnit::KiloMeterPerHour));
×
688
  throttleSpeed.setValue(convertUnit(value, speed.unit(), throttleSpeed.unit()));
×
689
  m_speedTimer.cancel();
×
690
  m_speedState = SpeedState::Idle;
×
691

692
  const bool currentValue = isStopped;
×
693
  isStopped.setValueInternal(m_speedState == SpeedState::Idle && almostZero(speed.value()) && almostZero(throttleSpeed.value()));
×
694
  if(currentValue != isStopped)
×
695
  {
696
    updateEnabled();
×
697
  }
698
  return {};
×
699
}
700

701
std::error_code Train::setTargetSpeed(Throttle& throttle, double value)
×
702
{
703
  if(m_throttle.get() != &throttle)
×
704
  {
705
    return make_error_code(ErrorCode::InvalidThrottle);
×
706
  }
707
  assert(active);
×
708
  throttleSpeed.setValue(std::clamp(value, Attributes::getMin(throttleSpeed), Attributes::getMax(throttleSpeed)));
×
709
  return {};
×
710
}
711

712
std::error_code Train::setDirection(Throttle& throttle, Direction value)
×
713
{
714
  if(m_throttle.get() != &throttle)
×
715
  {
716
    return make_error_code(ErrorCode::InvalidThrottle);
×
717
  }
718
  if(direction != value)
×
719
  {
720
    if(!isStopped)
×
721
    {
722
      return make_error_code(ErrorCode::TrainMustBeStoppedToChangeDirection);
×
723
    }
724
    assert(active);
×
725
    direction = value;
×
726
  }
727
  return {};
×
728
}
729

730
void Train::fireBlockAssigned(const std::shared_ptr<BlockRailTile>& block)
10✔
731
{
732
  if(m_world.debugTrainEvents)
10✔
733
  {
734
    Log::log(*this, LogMessage::D3010_TRAIN_X_ASSIGNED_TO_BLOCK_X, name.value(), block->name.value());
×
735
  }
736
  fireEvent(
20✔
737
    onBlockAssigned,
10✔
738
    shared_ptr<Train>(),
20✔
739
    block);
740
}
10✔
741

742
void Train::fireBlockReserved(const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection trainDirection)
5✔
743
{
744
  if(m_world.debugTrainEvents)
5✔
745
  {
746
    Log::log(*this, LogMessage::D3011_TRAIN_X_RESERVED_BLOCK_X, name.value(), block->name.value());
×
747
  }
748
  fireEvent(
10✔
749
    onBlockReserved,
5✔
750
    shared_ptr<Train>(),
10✔
751
    block,
752
    trainDirection);
753
}
5✔
754

755
void Train::fireBlockEntered(const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection trainDirection)
×
756
{
757
  if(m_world.debugTrainEvents)
×
758
  {
759
    Log::log(*this, LogMessage::D3012_TRAIN_X_ENTERED_BLOCK_X, name.value(), block->name.value());
×
760
  }
761
  fireEvent(
×
762
    onBlockEntered,
×
763
    shared_ptr<Train>(),
×
764
    block,
765
    trainDirection);
766
}
×
767

768
void Train::fireBlockLeft(const std::shared_ptr<BlockRailTile>& block, BlockTrainDirection trainDirection)
×
769
{
770
  if(m_world.debugTrainEvents)
×
771
  {
772
    Log::log(*this, LogMessage::D3013_TRAIN_X_LEFT_BLOCK_X, name.value(), block->name.value());
×
773
  }
774
  fireEvent(
×
775
    onBlockLeft,
×
776
    shared_ptr<Train>(),
×
777
    block,
778
    trainDirection);
779
}
×
780

781
void Train::fireBlockRemoved(const std::shared_ptr<BlockRailTile>& block)
3✔
782
{
783
  if(m_world.debugTrainEvents)
3✔
784
  {
785
    Log::log(*this, LogMessage::D3014_TRAIN_X_REMOVED_FROM_BLOCK_X, name.value(), block->name.value());
×
786
  }
787
  fireEvent(
6✔
788
    onBlockRemoved,
3✔
789
    shared_ptr<Train>(),
6✔
790
    block);
791
}
3✔
792

793
void Train::fireZoneAssigned(const std::shared_ptr<Zone>& zone)
3✔
794
{
795
  if(m_world.debugTrainEvents)
3✔
796
  {
797
    Log::log(*this, LogMessage::D3020_TRAIN_X_ASSIGNED_TO_ZONE_X, name.value(), zone->name.value());
×
798
  }
799
  fireEvent(onZoneAssigned, shared_ptr<Train>(), zone);
3✔
800
}
3✔
801

802
void Train::fireZoneEntering(const std::shared_ptr<Zone>& zone)
×
803
{
804
  if(m_world.debugTrainEvents)
×
805
  {
806
    Log::log(*this, LogMessage::D3021_TRAIN_X_ENTERING_ZONE_X, name.value(), zone->name.value());
×
807
  }
808
  fireEvent(onZoneEntering, shared_ptr<Train>(), zone);
×
809
}
×
810

811
void Train::fireZoneEntered(const std::shared_ptr<Zone>& zone)
×
812
{
813
  if(m_world.debugTrainEvents)
×
814
  {
815
    Log::log(*this, LogMessage::D3022_TRAIN_X_ENTERED_ZONE_X, name.value(), zone->name.value());
×
816
  }
817
  fireEvent(onZoneEntered, shared_ptr<Train>(), zone);
×
818
}
×
819

820
void Train::fireZoneLeaving(const std::shared_ptr<Zone>& zone)
×
821
{
822
  if(m_world.debugTrainEvents)
×
823
  {
824
    Log::log(*this, LogMessage::D3023_TRAIN_X_LEAVING_ZONE_X, name.value(), zone->name.value());
×
825
  }
826
  fireEvent(onZoneLeaving, shared_ptr<Train>(), zone);
×
827
}
×
828

829
void Train::fireZoneLeft(const std::shared_ptr<Zone>& zone)
×
830
{
831
  if(m_world.debugTrainEvents)
×
832
  {
833
    Log::log(*this, LogMessage::D3024_TRAIN_X_LEFT_ZONE_X, name.value(), zone->name.value());
×
834
  }
835
  fireEvent(onZoneLeft, shared_ptr<Train>(), zone);
×
836
}
×
837

838
void Train::fireZoneRemoved(const std::shared_ptr<Zone>& zone)
3✔
839
{
840
  if(m_world.debugTrainEvents)
3✔
841
  {
842
    Log::log(*this, LogMessage::D3025_TRAIN_X_REMOVED_FROM_ZONE_X, name.value(), zone->name.value());
×
843
  }
844
  fireEvent(onZoneRemoved, shared_ptr<Train>(), zone);
3✔
845
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc