From 10ff4aa8da12caff7a38187f97b06275e8e58f74 Mon Sep 17 00:00:00 2001 From: Nicolas Piganeau Date: Fri, 20 Dec 2019 23:12:59 +0100 Subject: [PATCH 1/2] Added route creation delay --- plugins/points/points.go | 53 +++++++++++++++++++++++++++++------ simulation/loading_test.go | 2 +- simulation/routes.go | 38 +++++++++++++++++++++++-- simulation/simulation.go | 35 +++++++++++++++++++---- simulation/simulation_test.go | 2 ++ simulation/track_points.go | 44 ++++++++++++++++++++--------- simulation/trains.go | 6 ++-- 7 files changed, 146 insertions(+), 34 deletions(-) diff --git a/plugins/points/points.go b/plugins/points/points.go index 4a20ca8..17ad35b 100644 --- a/plugins/points/points.go +++ b/plugins/points/points.go @@ -20,7 +20,9 @@ package points import ( "github.com/ts2/ts2-sim-server/simulation" + "math/rand" "sync" + "time" ) // StandardManager is a points manager that performs points change @@ -31,24 +33,57 @@ type StandardManager struct { } // Direction returns the direction of the points -func (sm StandardManager) Direction(p *simulation.PointsItem) simulation.PointDirection { +func (sm *StandardManager) Direction(p *simulation.PointsItem) simulation.PointDirection { + sm.RLock() + defer sm.RUnlock() return sm.directions[p.ID()] } -// SetDirection tries to set the given PointsItem to the given direction +// SetDirection tries to set the given PointsItem to the given direction. // -// You should not assume that the direction has been set, since this can be -// delayed or failed. Call Direction to check. -func (sm *StandardManager) SetDirection(p *simulation.PointsItem, dir simulation.PointDirection) { +// Just after the function is called, the points will switch to DirectionUnknown. +// When the points are in position, the notify channel is closed. +func (sm *StandardManager) SetDirection(p *simulation.PointsItem, dir simulation.PointDirection, notify chan struct{}) { if dir == simulation.DirectionCurrent { return } + startTime := p.Simulation().CurrentTime() + delay := time.Duration(3+rand.Intn(3)) * time.Second + if sm.directions[p.ID()] == dir { + // Points are in the correct direction already + if p.PairedItem() != nil { + if sm.directions[p.PairedTiId] != dir { + sm.SetDirection(p.PairedItem(), dir, notify) + } else { + close(notify) + } + } + return + } + // Check notify != nil to prevent infinite recursion + if p.PairedItem() != nil && notify != nil { + sm.SetDirection(p.PairedItem(), dir, nil) + } + go func() { + for { + <-time.After(simulation.TimeStep) + if p.Simulation().CurrentTime().Sub(startTime.Add(delay)) > 0 { + break + } + } + sm.Lock() + defer sm.Unlock() + sm.directions[p.ID()] = dir + if p.PairedItem() != nil { + sm.directions[p.PairedItem().ID()] = dir + } + if notify != nil { + close(notify) + } + }() sm.Lock() defer sm.Unlock() - sm.directions[p.ID()] = dir - if p.PairedItem() != nil { - sm.directions[p.PairedItem().ID()] = dir - } + sm.directions[p.ID()] = simulation.DirectionUnknown } // Name returns a description of this manager that is used for the UI. diff --git a/simulation/loading_test.go b/simulation/loading_test.go index 9d61d0d..3e2709e 100644 --- a/simulation/loading_test.go +++ b/simulation/loading_test.go @@ -80,7 +80,7 @@ func TestSimulationLoading(t *testing.T) { So(ok, ShouldBeTrue) So(d1, ShouldEqual, DirectionNormal) So(r1.InitialState, ShouldEqual, Activated) - So(r1.State(), ShouldEqual, Activated) + So(r1.State(), ShouldEqual, Activating) So(sim.Routes, ShouldContainKey, "4") r4, ok := sim.Routes["4"] diff --git a/simulation/routes.go b/simulation/routes.go index 915e26c..622691b 100644 --- a/simulation/routes.go +++ b/simulation/routes.go @@ -21,6 +21,7 @@ package simulation import ( "encoding/json" "fmt" + "time" ) // A RoutesManager checks if a route is activable or deactivable. @@ -49,6 +50,9 @@ const ( // Destroying = The route is currently being destroyed by a train Destroying RouteState = 3 + + // Activating - The route is currently being actived + Activating RouteState = 4 ) // A Route is a path between two signals. @@ -67,6 +71,7 @@ type Route struct { Positions []Position `json:"-"` simulation *Simulation + activating bool triggers []func(*Route) } @@ -93,6 +98,9 @@ func (r *Route) Equals(other *Route) bool { // State returns the current state of this route func (r *Route) State() RouteState { + if r.activating { + return Activating + } if r.BeginSignal().nextActiveRoute == nil || !r.BeginSignal().nextActiveRoute.Equals(r) { for _, p := range r.Positions { if p.TrackItem().ActiveRoute() != nil && p.TrackItem().ActiveRoute().Equals(r) { @@ -125,15 +133,41 @@ func (r *Route) Activate(persistent bool) error { return fmt.Errorf("%s vetoed route activation: %s", rm.Name(), err) } } + r.activating = true for _, pos := range r.Positions { if pos.TrackItem().Equals(r.BeginSignal()) || pos.TrackItem().Equals(r.EndSignal()) { continue } pos.TrackItem().setActiveRoute(r, pos.PreviousItem()) } + r.Persistent = persistent + r.simulation.sendEvent(&Event{ + Name: RouteActivatedEvent, + Object: r, + }) + go func() { + waitLoop: + for { + <-time.After(TimeStep) + for _, pos := range r.Positions { + if points, ok := pos.TrackItem().(*PointsItem); ok { + if points.Moving() { + continue waitLoop + } + } + } + break + } + r.simulation.routesChan <- r + }() + return nil +} + +// doActivate sets the route to activated state at the end of the activation process. +func (r *Route) doActivate() { + r.activating = false r.EndSignal().previousActiveRoute = r r.BeginSignal().nextActiveRoute = r - r.Persistent = persistent for _, t := range r.triggers { t(r) } @@ -142,9 +176,9 @@ func (r *Route) Activate(persistent bool) error { Object: r, }) r.BeginSignal().updateSignalState() - return nil } + // Deactivate the given route. If the route cannot be Deactivated, an error is returned. func (r *Route) Deactivate() error { for _, rm := range routesManagers { diff --git a/simulation/simulation.go b/simulation/simulation.go index a8a67aa..a9ff995 100644 --- a/simulation/simulation.go +++ b/simulation/simulation.go @@ -28,7 +28,7 @@ import ( log "gopkg.in/inconshreveable/log15.v2" ) -const timeStep = 500 * time.Millisecond +const TimeStep = 500 * time.Millisecond // Version of the software, mostly used for file format const Version = "0.7" @@ -64,6 +64,7 @@ type Simulation struct { clockTicker *time.Ticker stopChan chan bool + routesChan chan *Route started bool } @@ -84,6 +85,7 @@ func (sim *Simulation) UnmarshalJSON(data []byte) error { sim.EventChan = make(chan *Event) sim.stopChan = make(chan bool) + sim.routesChan = make(chan *Route) var rawSim auxSim if err := json.Unmarshal(data, &rawSim); err != nil { @@ -292,7 +294,7 @@ func (sim *Simulation) Start() { // run enters the main loop of the simulation func (sim *Simulation) run() { - clockTicker := time.NewTicker(timeStep) + clockTicker := time.NewTicker(TimeStep) for { select { case <-sim.stopChan: @@ -301,9 +303,10 @@ func (sim *Simulation) run() { Logger.Info("Simulation paused") return case <-clockTicker.C: - sim.increaseTime(timeStep) - sim.sendEvent(&Event{Name: ClockEvent, Object: sim.Options.CurrentTime}) + sim.increaseTime(TimeStep) + sim.sendEvent(&Event{Name: ClockEvent, Object: sim.CurrentTime()}) sim.updateTrains() + sim.updateRoutes() } } } @@ -332,6 +335,13 @@ func (sim *Simulation) increaseTime(step time.Duration) { sim.Options.CurrentTime = sim.Options.CurrentTime.Add(time.Duration(sim.Options.TimeFactor) * step) } +// CurrentTime returns the current time in a concurrently safe manner +func (sim *Simulation) CurrentTime() Time { + sim.Options.CurrentTime.RLock() + defer sim.Options.CurrentTime.RUnlock() + return sim.Options.CurrentTime +} + // checks that all TrackItems are linked together. // Returns the first error met. func (sim *Simulation) checkTrackItemsLinks() error { @@ -371,11 +381,11 @@ func (sim *Simulation) checkTrackItemsLinks() error { // updateTrains update all trains information such as status, position, speed, etc. func (sim *Simulation) updateTrains() { for _, train := range sim.Trains { - train.activate(sim.Options.CurrentTime) + train.activate(sim.CurrentTime()) if !train.IsActive() { continue } - train.advance(timeStep * time.Duration(sim.Options.TimeFactor)) + train.advance(TimeStep * time.Duration(sim.Options.TimeFactor)) } } @@ -388,6 +398,19 @@ func (sim *Simulation) updateScore(penalty int) { }) } +// updateRoutes updates the routes that have been activated and that are send over routesChan +func (sim *Simulation) updateRoutes() { +mainLoop: + for { + select { + case route := <-sim.routesChan: + route.doActivate() + default: + break mainLoop + } + } +} + // RegisterRoutesManager registers the given route manager in the simulation. // // When several routes managers are registered, all of them are called in turn. diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 281684d..f0cdf63 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -88,10 +88,12 @@ func TestSimulationRun(t *testing.T) { So(err, ShouldBeNil) err = sim.Routes["2"].Activate(false) So(err, ShouldBeNil) + So(sim.Routes["2"].State(), ShouldEqual, simulation.Activating) So(sim.TrackItems["5"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_DANGER") sim.Start() time.Sleep(7 * time.Second) sim.Pause() + So(sim.Routes["2"].State(), ShouldEqual, simulation.Activated) So(sim.TrackItems["5"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CAUTION") So(sim.TrackItems["3"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CLEAR") }) diff --git a/simulation/track_points.go b/simulation/track_points.go index 925ad90..786e393 100644 --- a/simulation/track_points.go +++ b/simulation/track_points.go @@ -27,11 +27,11 @@ type PointsItemManager interface { Name() string // Direction returns the direction of the points Direction(*PointsItem) PointDirection - // SetDirection tries to set the given PointsItem to the given direction + // SetDirection tries to set the given PointsItem to the given direction. // - // You should not assume that the direction has been set, since this can be - // delayed or failed. Call Direction to check. - SetDirection(*PointsItem, PointDirection) + // Just after the function is called, the points will switch to DirectionUnknown. + // When the points are in position, the notify channel is closed. + SetDirection(*PointsItem, PointDirection, chan struct{}) } // PointDirection are constants that represent the "physical state" of a PointsItem @@ -146,6 +146,12 @@ func (pi *PointsItem) Reversed() bool { return dir == DirectionReversed } +// Moving returns true if the points are moving +func (pi *PointsItem) Moving() bool { + dir := pointsItemManager.Direction(pi) + return dir == DirectionUnknown +} + // IsConnected returns true if this TrackItem is connected to the given // TrackItem, false otherwise func (pi *PointsItem) IsConnected(oti TrackItem) bool { @@ -187,18 +193,28 @@ func (pi *PointsItem) FollowingItem(precedingItem TrackItem, dir PointDirection) // setActiveRoute sets the given route as active on this PointsItem. // previous gives the direction. func (pi *PointsItem) setActiveRoute(r *Route, previous TrackItem) { + notifyPaired := func() { + if pi.PairedItem() != nil { + pi.simulation.sendEvent(&Event{ + Name: TrackItemChangedEvent, + Object: pi.PairedItem(), + }) + } + } if r != nil { - pointsItemManager.SetDirection(pi, r.Directions[pi.ID()]) + pointsMoved := make(chan struct{}) + pointsItemManager.SetDirection(pi, r.Directions[pi.ID()], pointsMoved) + go func() { + <-pointsMoved + pi.simulation.sendEvent(&Event{ + Name: TrackItemChangedEvent, + Object: pi, + }) + notifyPaired() + }() } // Send event for pairedItem - if pi.PairedItem() != nil { - pi.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: pi.PairedItem(), - }) - } - // TODO We should check here whether the points have failed or not - // and delay route activation. + notifyPaired() pi.trackStruct.setActiveRoute(r, previous) } @@ -215,6 +231,7 @@ func (pi *PointsItem) MarshalJSON() ([]byte, error) { ReverseTiId string `json:"reverseTiId"` PairedTiId string `json:"pairedTiId"` Reversed bool `json:"reversed"` + Moving bool `json:"moving"` } aPI := auxPI{ jsonTrackStruct: pi.asJSONStruct(), @@ -227,6 +244,7 @@ func (pi *PointsItem) MarshalJSON() ([]byte, error) { ReverseTiId: pi.ReverseTiId, PairedTiId: pi.PairedTiId, Reversed: pi.Reversed(), + Moving: pi.Moving(), } return json.Marshal(aPI) } diff --git a/simulation/trains.go b/simulation/trains.go index a9eb1c5..c1ecde1 100644 --- a/simulation/trains.go +++ b/simulation/trains.go @@ -362,7 +362,7 @@ func (t *Train) updateSignalActions() { t.lastSignal = nextSignal } - currentTime := t.simulation.Options.CurrentTime + currentTime := t.simulation.CurrentTime() if math.Abs(t.Speed-t.ApplicableAction().Speed) < 0.1 { // We have achieved the action's target speed. if t.actionTime.IsZero() { @@ -550,7 +550,7 @@ func (t *Train) updateStatus(timeElapsed time.Duration) { return } // Train is already stopped at the place - if line.ScheduledDepartureTime.Sub(t.simulation.Options.CurrentTime) > 0 || + if line.ScheduledDepartureTime.Sub(t.simulation.CurrentTime()) > 0 || t.StoppedTime < int(t.minStopTime/time.Second) || line.ScheduledDepartureTime.IsZero() { // Conditions to depart are not met @@ -618,7 +618,7 @@ func (t *Train) logAndScoreTrainStoppedAtStation() { t.ServiceCode, place.Name(), actualPlatform, plannedPlatform), simulationMsg) } scheduledArrivalTime := serviceLine.ScheduledArrivalTime - currentTime := sim.Options.CurrentTime + currentTime := sim.CurrentTime() delay := currentTime.Sub(scheduledArrivalTime) if delay > time.Minute { playerDelay := delay - t.effInitialDelay From c4673e19cf0ed8d3ac25cb12d5478fca63bb25c0 Mon Sep 17 00:00:00 2001 From: Nicolas Piganeau Date: Sat, 21 Dec 2019 19:12:20 +0100 Subject: [PATCH 2/2] Added support for delayed route deactivation --- server/hub_test.go | 3 ++- simulation/options.go | 3 +++ simulation/routes.go | 38 ++++++++++++++++++++++++++++++--- simulation/signal_conditions.go | 4 ++-- simulation/simulation.go | 18 ++++++++++------ simulation/simulation_test.go | 22 ++++++++++++------- simulation/testdata/demo.json | 5 ++++- simulation/track_items.go | 13 +++++++---- simulation/track_points.go | 10 ++------- simulation/track_signals.go | 10 ++------- simulation/trains.go | 9 ++------ 11 files changed, 86 insertions(+), 49 deletions(-) diff --git a/server/hub_test.go b/server/hub_test.go index 8f6580d..bb3c9ba 100644 --- a/server/hub_test.go +++ b/server/hub_test.go @@ -673,7 +673,8 @@ func TestHub(t *testing.T) { So(resp.Data.Status, ShouldEqual, Ok) resp = sendRequestStatus(c, "route", "activate", `{"id": "2"}`) - So(resp.Data.Status, ShouldEqual, Ok) + // Fails because route 1 did not have time to deactivate + So(resp.Data.Status, ShouldEqual, Fail) err = c.WriteJSON(RequestListener{ Object: "server", diff --git a/simulation/options.go b/simulation/options.go index ce29d94..7ff249c 100644 --- a/simulation/options.go +++ b/simulation/options.go @@ -41,6 +41,9 @@ type Options struct { WrongPlatformPenalty int `json:"wrongPlatformPenalty"` WrongDestinationPenalty int `json:"wrongDestinationPenalty"` LatePenalty int `json:"latePenalty"` + PointsSetupDelay DelayGenerator `json:"pointsSetupDelay"` + RoutesSetupDelay DelayGenerator `json:"routesSetupDelay"` + RoutesCancelDelay DelayGenerator `json:"routesCancelDelay"` simulation *Simulation } diff --git a/simulation/routes.go b/simulation/routes.go index 622691b..f17e7c7 100644 --- a/simulation/routes.go +++ b/simulation/routes.go @@ -120,6 +120,11 @@ func (r *Route) IsActive() bool { return r.State() == Activated || r.State() == Persistent } +// IsDestroying returns true if this Route is currently being destroyed +func (r *Route) IsDestroying() bool { + return r.State() == Destroying +} + // addTrigger adds the given function to the list of function that will be // called when this Route is activated or deactivated. func (r *Route) addTrigger(trigger func(*Route)) { @@ -146,6 +151,8 @@ func (r *Route) Activate(persistent bool) error { Object: r, }) go func() { + routesDelay := r.simulation.Options.RoutesSetupDelay.Yield() + startTime := r.simulation.CurrentTime() waitLoop: for { <-time.After(TimeStep) @@ -156,9 +163,12 @@ func (r *Route) Activate(persistent bool) error { } } } + if r.simulation.CurrentTime().Sub(startTime.Add(routesDelay)) < 0 { + continue + } break } - r.simulation.routesChan <- r + r.simulation.activatedRoutesChan <- r }() return nil } @@ -178,7 +188,6 @@ func (r *Route) doActivate() { r.BeginSignal().updateSignalState() } - // Deactivate the given route. If the route cannot be Deactivated, an error is returned. func (r *Route) Deactivate() error { for _, rm := range routesManagers { @@ -186,6 +195,30 @@ func (r *Route) Deactivate() error { return fmt.Errorf("%s vetoed route deactivation", rm.Name()) } } + r.activating = true + r.simulation.sendEvent(&Event{ + Name: RouteDeactivatedEvent, + Object: r, + }) + r.BeginSignal().updateSignalState() + go func() { + routesDelay := r.simulation.Options.RoutesCancelDelay.Yield() + startTime := r.simulation.CurrentTime() + for { + <-time.After(TimeStep) + if r.simulation.CurrentTime().Sub(startTime.Add(routesDelay)) > 0 { + break + } + } + r.simulation.deactivatedRoutesChan <- r + }() + return nil +} + +// doDeactivate sets the route's status to Deactivated at the end of the deactivation process. +func (r *Route) doDeactivate() { + r.activating = false + r.Persistent = false r.BeginSignal().resetNextActiveRoute(r) r.EndSignal().resetPreviousActiveRoute(nil) for _, pos := range r.Positions { @@ -202,7 +235,6 @@ func (r *Route) Deactivate() error { Object: r, }) r.BeginSignal().updateSignalState() - return nil } // setSimulation sets the Simulation this Route is part of. diff --git a/simulation/signal_conditions.go b/simulation/signal_conditions.go index b2dcdfc..1a0da66 100644 --- a/simulation/signal_conditions.go +++ b/simulation/signal_conditions.go @@ -33,7 +33,7 @@ func (nar NextActiveRoute) Code() string { // Solve returns if the condition is met for the given SignalItem and parameters func (nar NextActiveRoute) Solve(item *SignalItem, values []string, params []string) bool { - return item.nextActiveRoute != nil + return item.nextActiveRoute != nil && item.nextActiveRoute.IsActive() } // SetupTriggers installs needed triggers for the given SignalItem, with the @@ -52,7 +52,7 @@ func (par PreviousActiveRoute) Code() string { // Solve returns if the condition is met for the given SignalItem and parameters func (par PreviousActiveRoute) Solve(item *SignalItem, values []string, params []string) bool { - return item.previousActiveRoute != nil + return item.previousActiveRoute != nil && (item.previousActiveRoute.IsActive() || item.previousActiveRoute.IsDestroying()) } // SetupTriggers installs needed triggers for the given SignalItem, with the diff --git a/simulation/simulation.go b/simulation/simulation.go index a9ff995..b8a200c 100644 --- a/simulation/simulation.go +++ b/simulation/simulation.go @@ -62,10 +62,11 @@ type Simulation struct { MessageLogger *MessageLogger EventChan chan *Event - clockTicker *time.Ticker - stopChan chan bool - routesChan chan *Route - started bool + clockTicker *time.Ticker + stopChan chan bool + activatedRoutesChan chan *Route + deactivatedRoutesChan chan *Route + started bool } // UnmarshalJSON for the Simulation type @@ -85,7 +86,8 @@ func (sim *Simulation) UnmarshalJSON(data []byte) error { sim.EventChan = make(chan *Event) sim.stopChan = make(chan bool) - sim.routesChan = make(chan *Route) + sim.activatedRoutesChan = make(chan *Route) + sim.deactivatedRoutesChan = make(chan *Route) var rawSim auxSim if err := json.Unmarshal(data, &rawSim); err != nil { @@ -398,13 +400,15 @@ func (sim *Simulation) updateScore(penalty int) { }) } -// updateRoutes updates the routes that have been activated and that are send over routesChan +// updateRoutes updates the routes that have been (de)activated func (sim *Simulation) updateRoutes() { mainLoop: for { select { - case route := <-sim.routesChan: + case route := <-sim.activatedRoutesChan: route.doActivate() + case route := <-sim.deactivatedRoutesChan: + route.doDeactivate() default: break mainLoop } diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index f0cdf63..d93aef0 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -67,26 +67,32 @@ func TestSimulationRun(t *testing.T) { So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 3) So(sim.TrackItems["3"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CLEAR") sim.Start() - time.Sleep(600 * time.Millisecond) + time.Sleep(1200 * time.Millisecond) sim.Pause() sim.Options.TrackCircuitBased = true - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:02.5")) + So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:05")) So(sim.Trains[0].TrainHead.TrackItemID, ShouldEqual, "2") So(sim.Trains[0].TrainHead.PreviousItemID, ShouldEqual, "1") - So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 18.625) - So(sim.Trains[0].Speed, ShouldEqual, 6.25) + So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 37.375) + So(sim.Trains[0].Speed, ShouldEqual, 7.5) time.Sleep(600 * time.Millisecond) - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:02.5")) - So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:02.5")) + So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:05")) + So(sim.Options.CurrentTime, ShouldResemble, simulation.ParseTime("06:00:05")) So(sim.Trains[0].TrainHead.TrackItemID, ShouldEqual, "2") So(sim.Trains[0].TrainHead.PreviousItemID, ShouldEqual, "1") - So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 18.625) - So(sim.Trains[0].Speed, ShouldEqual, 6.25) + So(sim.Trains[0].TrainHead.PositionOnTI, ShouldEqual, 37.375) + So(sim.Trains[0].Speed, ShouldEqual, 7.5) So(sim.TrackItems["3"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_DANGER") So(sim.TrackItems["5"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_CLEAR") err := sim.Routes["1"].Deactivate() So(err, ShouldBeNil) err = sim.Routes["2"].Activate(false) + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "Standard Manager vetoed route activation: conflicting route 1 is active") + sim.Start() + time.Sleep(1200*time.Millisecond) + sim.Pause() + err = sim.Routes["2"].Activate(false) So(err, ShouldBeNil) So(sim.Routes["2"].State(), ShouldEqual, simulation.Activating) So(sim.TrackItems["5"].(*simulation.SignalItem).ActiveAspect().Name, ShouldEqual, "UK_DANGER") diff --git a/simulation/testdata/demo.json b/simulation/testdata/demo.json index 8d10507..103103f 100644 --- a/simulation/testdata/demo.json +++ b/simulation/testdata/demo.json @@ -37,7 +37,10 @@ "version": "0.7", "warningSpeed": 8.34, "wrongDestinationPenalty": 100, - "wrongPlatformPenalty": 5 + "wrongPlatformPenalty": 5, + "pointsSetupDelay": [[1, 1, 100]], + "routesSetupDelay": [[1, 1, 100]], + "routesCancelDelay": [[1, 1, 100]] }, "routes": { "1": { diff --git a/simulation/track_items.go b/simulation/track_items.go index 3129c6a..efe27e6 100644 --- a/simulation/track_items.go +++ b/simulation/track_items.go @@ -185,6 +185,9 @@ type TrackItem interface { // resetActiveRoute resets route information on this item. resetActiveRoute() + // notifyChange sends a TrackItemChanged event for this item + notifyChange() + // TrainPresent returns true if at least one train is present on this TrackItem TrainPresent() bool @@ -364,10 +367,7 @@ func (t *trackStruct) underlying() *trackStruct { func (t *trackStruct) setActiveRoute(r *Route, previous TrackItem) { t.activeRoute = r t.arPreviousItem = previous - t.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: t.full(), - }) + t.notifyChange() } // ActiveRoute returns a pointer to the route currently active on this item @@ -426,6 +426,11 @@ func (t *trackStruct) TrainPresent() bool { func (t *trackStruct) resetActiveRoute() { t.activeRoute = nil t.arPreviousItem = nil + t.notifyChange() +} + +// notifyChange sends a TrackItemChangedEvent for this item +func (t *trackStruct) notifyChange() { t.simulation.sendEvent(&Event{ Name: TrackItemChangedEvent, Object: t.full(), diff --git a/simulation/track_points.go b/simulation/track_points.go index 786e393..a3cf244 100644 --- a/simulation/track_points.go +++ b/simulation/track_points.go @@ -195,10 +195,7 @@ func (pi *PointsItem) FollowingItem(precedingItem TrackItem, dir PointDirection) func (pi *PointsItem) setActiveRoute(r *Route, previous TrackItem) { notifyPaired := func() { if pi.PairedItem() != nil { - pi.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: pi.PairedItem(), - }) + pi.PairedItem().notifyChange() } } if r != nil { @@ -206,10 +203,7 @@ func (pi *PointsItem) setActiveRoute(r *Route, previous TrackItem) { pointsItemManager.SetDirection(pi, r.Directions[pi.ID()], pointsMoved) go func() { <-pointsMoved - pi.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: pi, - }) + pi.notifyChange() notifyPaired() }() } diff --git a/simulation/track_signals.go b/simulation/track_signals.go index 803f450..c565aac 100644 --- a/simulation/track_signals.go +++ b/simulation/track_signals.go @@ -316,10 +316,7 @@ func (si *SignalItem) setActiveRoute(r *Route, previous TrackItem) { // setTrainID sets the train associated with this signal train to display in berth. func (si *SignalItem) setTrain(t *Train) { si.train = t - si.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: si, - }) + si.notifyChange() } // IsOnPosition returns true if this signal item is the track item of @@ -449,10 +446,7 @@ func (si *SignalItem) updateSignalState(previous ...bool) { if previousSignal != nil { previousSignal.updateSignalState(append(previous, true)...) } - si.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: si, - }) + si.notifyChange() } // resetNextActiveRoute information. If route is not nil, do diff --git a/simulation/trains.go b/simulation/trains.go index c1ecde1..85203d0 100644 --- a/simulation/trains.go +++ b/simulation/trains.go @@ -238,10 +238,7 @@ func (t *Train) executeActions(advanceLength float64) { t.logAndScoreTrainExited() } for ti := range toNotify { - t.simulation.sendEvent(&Event{ - Name: TrackItemChangedEvent, - Object: ti, - }) + ti.notifyChange() } } @@ -435,9 +432,7 @@ func (t *Train) Reverse() error { signalAhead.setTrain(nil) } if activeRoute := t.TrainHead.TrackItem().ActiveRoute(); activeRoute != nil { - if err := activeRoute.Deactivate(); err != nil { - t.simulation.MessageLogger.addMessage(err.Error(), simulationMsg) - } + activeRoute.doDeactivate() } t.TrainHead = t.TrainTail().Reversed() if newSignalAhead := t.findNextSignal(); newSignalAhead != nil {