001package jmri.jmrit.dispatcher; 002 003import java.util.ArrayList; 004import java.util.List; 005import jmri.Block; 006import jmri.EntryPoint; 007import jmri.InstanceManager; 008import jmri.Section; 009import jmri.Transit; 010import jmri.Turnout; 011import jmri.NamedBean.DisplayOptions; 012import jmri.jmrit.display.layoutEditor.ConnectivityUtil; 013import jmri.jmrit.display.layoutEditor.LayoutDoubleXOver; 014import jmri.jmrit.display.layoutEditor.LayoutLHXOver; 015import jmri.jmrit.display.layoutEditor.LayoutRHXOver; 016import jmri.jmrit.display.layoutEditor.LayoutSlip; 017import jmri.jmrit.display.layoutEditor.LayoutTrackExpectedState; 018import jmri.jmrit.display.layoutEditor.LayoutTurnout; 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022/** 023 * Handles automatic checking and setting of turnouts when Dispatcher allocates 024 * a Section in a specific direction. 025 * <p> 026 * This file is part of JMRI. 027 * <p> 028 * JMRI is open source software; you can redistribute it and/or modify it under 029 * the terms of version 2 of the GNU General Public License as published by the 030 * Free Software Foundation. See the "COPYING" file for a copy of this license. 031 * <p> 032 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 033 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 034 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 035 * 036 * @author Dave Duchamp Copyright (C) 2008-2009 037 */ 038public class AutoTurnouts { 039 040 public AutoTurnouts(DispatcherFrame d) { 041 _dispatcher = d; 042 } 043 044 private static final DisplayOptions USERSYS = DisplayOptions.USERNAME_SYSTEMNAME; 045 private final String closedText = InstanceManager.turnoutManagerInstance().getClosedText(); 046 private final String thrownText = InstanceManager.turnoutManagerInstance().getThrownText(); 047 048 // operational variables 049 protected DispatcherFrame _dispatcher = null; 050 boolean userInformed = false; 051 052 /** 053 * Check that all turnouts are correctly set for travel in the designated 054 * Section to the next Section. NOTE: This method requires use of the 055 * connectivity stored in a Layout Editor panel. 056 * 057 * NOTE: This method removes the need to specify the LayoutEditor panel. 058 * 059 * @param s the section to check 060 * @param seqNum sequence number for the section 061 * @param nextSection the following section 062 * @param at the associated train 063 * @param prevSection the prior section 064 * @param useTurnoutConnectionDelay true if the turnout connection delay should be applied 065 * @return list of turnouts and their expected states if affected turnouts are correctly set; null otherwise. 066 */ 067 protected List<LayoutTrackExpectedState<LayoutTurnout>> checkTurnoutsInSection(Section s, int seqNum, Section nextSection, 068 ActiveTrain at, Section prevSection, boolean useTurnoutConnectionDelay) { 069 return turnoutUtil(s, seqNum, nextSection, at, false, false, prevSection, useTurnoutConnectionDelay); 070 } 071 072 073 /** 074 * Set all turnouts for travel in the designated Section to the next 075 * Section. 076 * 077 * Checks that all turnouts are correctly set for travel in this Section to 078 * the next Section, and sets any turnouts that are not correct. The Section 079 * must be FREE to set its turnouts. Testing for FREE only occurs if a 080 * command needs to be issued. For a command to be issued to set a turnout, 081 * the Block containing that turnout must be unoccupied. NOTE: This method 082 * does not wait for turnout feedback--it assumes the turnout will be set 083 * correctly if a command is issued. 084 * 085 * NOTE: This method removes the need to specify the LayoutEditor panel. 086 * 087 * 088 * @param s the section to check 089 * @param seqNum sequence number for the section 090 * @param nextSection the following section 091 * @param at the associated train 092 * @param trustKnownTurnouts true to trust known turnouts 093 * @param prevSection the prior section 094 * @param useTurnoutConnectionDelay true if the turnout connection delay should be applied 095 * 096 * @return list of turnouts and their expected states if affected turnouts are correctly set or commands have been 097 * issued to set any that aren't set correctly; null if a needed 098 * command could not be issued because the turnout's Block is 099 * occupied 100 */ 101 protected List<LayoutTrackExpectedState<LayoutTurnout>> setTurnoutsInSection(Section s, int seqNum, Section nextSection, 102 ActiveTrain at, boolean trustKnownTurnouts, Section prevSection, boolean useTurnoutConnectionDelay) { 103 return turnoutUtil(s, seqNum, nextSection, at, trustKnownTurnouts, true, prevSection, useTurnoutConnectionDelay); 104 } 105 106 protected Turnout checkStateAgainstList(List<LayoutTrackExpectedState<LayoutTurnout>> turnoutList) { 107 if (turnoutList != null) { 108 for (LayoutTrackExpectedState<LayoutTurnout> tes : turnoutList) { 109 Turnout to = tes.getObject().getTurnout(); 110 int setting = tes.getExpectedState(); 111 if (tes.getObject() instanceof LayoutSlip) { 112 setting = ((LayoutSlip) tes.getObject()).getTurnoutState(tes.getExpectedState()); 113 } 114 if (to.getKnownState() != setting) { 115 return to; 116 } 117 if (tes.getObject() instanceof LayoutSlip) { 118 //Look at the state of the second turnout in the slip 119 setting = ((LayoutSlip) tes.getObject()).getTurnoutBState(tes.getExpectedState()); 120 to = ((LayoutSlip) tes.getObject()).getTurnoutB(); 121 if (to.getKnownState() != setting) { 122 return to; 123 } 124 } 125 } 126 } 127 return null; 128 } 129 130 /** 131 * Internal method implementing the above two methods Returns 'true' if 132 * turnouts are set correctly, 'false' otherwise If 'set' is 'true' this 133 * routine will attempt to set the turnouts, if 'false' it reports what it 134 * finds. 135 */ 136 private List<LayoutTrackExpectedState<LayoutTurnout>> turnoutUtil(Section s, int seqNum, Section nextSection, 137 ActiveTrain at, boolean trustKnownTurnouts, boolean set, Section prevSection, boolean useTurnoutConnectionDelay ) { 138 log.trace("{}:Checking LayoutTrackExpectedState Section[{}] NextSection[{}] PrevSection[{}]", 139 at.getTrainName(), (s != null) ? s.getDisplayName() : "null", 140 nextSection== null ? "Null" : nextSection.getDisplayName(), 141 prevSection == null ? "Null" : prevSection.getDisplayName()); 142 // initialize response structure 143 List<LayoutTrackExpectedState<LayoutTurnout>> turnoutListForAllocatedSection = new ArrayList<>(); 144 // validate input and initialize 145 Transit tran = at.getTransit(); 146 if ((s == null) || (seqNum > tran.getMaxSequence()) || (!tran.containsSection(s))) { 147 log.error("Invalid argument when checking or setting turnouts in Section."); 148 return null; 149 } 150 int direction = at.getAllocationDirectionFromSectionAndSeq(s, seqNum); 151 if (direction == 0) { 152 log.error("Invalid Section/sequence arguments when checking or setting turnouts"); 153 return null; 154 } 155 // Did have this set to include SignalMasts as part of the && statement 156 //Sections created using Signal masts will generally only have a single entry/exit point. 157 // check for no turnouts in this section 158 if (_dispatcher.getSignalType() == DispatcherFrame.SIGNALHEAD && (s.getForwardEntryPointList().size() <= 1) && (s.getReverseEntryPointList().size() <= 1)) { 159 log.debug("No entry points lists"); 160 // no possibility of turnouts 161 return turnoutListForAllocatedSection; 162 } 163 // initialize connectivity utilities and beginning block pointers 164 EntryPoint entryPt = null; 165 if (prevSection != null) { 166 entryPt = s.getEntryPointFromSection(prevSection, direction); 167 } else if (!s.containsBlock(at.getStartBlock())) { 168 entryPt = s.getEntryPointFromBlock(at.getStartBlock(), direction); 169 } 170 EntryPoint exitPt = null; 171 if (nextSection != null) { 172 exitPt = s.getExitPointToSection(nextSection, direction); 173 } 174 Block curBlock; // must be in the section 175 Block prevBlock = null; // must start outside the section or be null 176 int curBlockSeqNum; // sequence number of curBlock in Section 177 if (entryPt != null) { 178 curBlock = entryPt.getBlock(); 179 prevBlock = entryPt.getFromBlock(); 180 curBlockSeqNum = s.getBlockSequenceNumber(curBlock); 181 } else if ( !at.isAllocationReversed() && s.containsBlock(at.getStartBlock())) { 182 curBlock = at.getStartBlock(); 183 curBlockSeqNum = s.getBlockSequenceNumber(curBlock); 184 //Get the previous block so that we can set the turnouts in the current block correctly. 185 if (direction == Section.FORWARD) { 186 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum - 1); 187 } else if (direction == Section.REVERSE) { 188 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum + 1); 189 } 190 } else if (at.isAllocationReversed() && s.containsBlock(at.getEndBlock())) { 191 curBlock = at.getEndBlock(); 192 curBlockSeqNum = s.getBlockSequenceNumber(curBlock); 193 //Get the previous block so that we can set the turnouts in the current block correctly. 194 if (direction == Section.REVERSE) { 195 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum + 1); 196 } else if (direction == Section.FORWARD) { 197 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum - 1); 198 } 199 } else { 200 201 //if (_dispatcher.getSignalType() == DispatcherFrame.SIGNALMAST) { 202 // //This can be considered normal where SignalMast Logic is used. 203 // return true; 204 //} 205 // this is an error but is it? It only happens when system is under stress 206 // which would point to a threading issue. 207 try { 208 log.error("[{}]direction[{}] Section[{}]Error in turnout check/set request - initial Block[{}] and Section[{}] mismatch", 209 at.getActiveTrainName(),at.isAllocationReversed(),s.getDisplayName(USERSYS), 210 at.getStartBlock().getUserName(),at.getEndBlock().getDisplayName(USERSYS)); 211 } catch (Exception ex ) { 212 log.warn("Exception while creating log error : {}", ex.getLocalizedMessage()); 213 } 214 return turnoutListForAllocatedSection; 215 } 216 217 Block nextBlock = null; 218 // may be either in the section or the first block in the next section 219 int nextBlockSeqNum = -1; // sequence number of nextBlock in Section (-1 indicates outside Section) 220 if (exitPt != null && curBlock == exitPt.getBlock()) { 221 // next Block is outside of the Section 222 nextBlock = exitPt.getFromBlock(); 223 } else { 224 // next Block is inside the Section 225 if (direction == Section.FORWARD) { 226 nextBlock = s.getBlockBySequenceNumber(curBlockSeqNum + 1); 227 nextBlockSeqNum = curBlockSeqNum + 1; 228 } else if (direction == Section.REVERSE) { 229 nextBlock = s.getBlockBySequenceNumber(curBlockSeqNum - 1); 230 nextBlockSeqNum = curBlockSeqNum - 1; 231 } 232 if ((nextBlock == null && 233 ((!at.isAllocationReversed() && curBlock != at.getEndBlock()) || 234 (at.isAllocationReversed() && curBlock != at.getStartBlock())))) { 235 log.error("[{}]Error in block sequence numbers when setting/checking turnouts.", 236 curBlock.getDisplayName(USERSYS)); 237 return null; 238 } 239 } 240 241 List<LayoutTrackExpectedState<LayoutTurnout>> turnoutList = new ArrayList<>(); 242 // get turnouts by Block 243 boolean turnoutsOK = true; 244 245 var layoutBlockManger = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class); 246 while (curBlock != null) { 247 /*No point in getting the list if the previous block is null as it will return empty and generate an error, 248 this will only happen on the first run. Plus working on the basis that the turnouts in the current block would have already of 249 been set correctly for the train to have arrived in the first place. 250 */ 251 log.trace("curBlock {}: PrevBlock[{}]", curBlock.getDisplayName(), 252 prevBlock == null ? "Null" : prevBlock.getDisplayName()); 253 if (prevBlock != null) { 254 var blockName = curBlock.getUserName(); 255 if (blockName != null) { 256 var lblock = layoutBlockManger.getLayoutBlock(blockName); 257 if (lblock != null) { 258 var panel = lblock.getMaxConnectedPanel(); 259 if (panel != null) { 260 var connection = new ConnectivityUtil(panel); 261 // note turnouts for turntables are added to the list in getTurnoutList 262 turnoutList = connection.getTurnoutList(curBlock, prevBlock, nextBlock, true); 263 } 264 } 265 } 266 } else { 267 log.debug("turnoutUtil - No previous block"); 268 } 269 // loop over turnouts checking and optionally setting turnouts 270 for (int i = 0; i < turnoutList.size(); i++) { 271 Turnout to = turnoutList.get(i).getObject().getTurnout(); 272 if (to == null ) { 273 // this should not happen due to prior selection 274 log.error("Found null Turnout reference at {}: {}", i, turnoutList.get(i).getObject()); 275 continue; // move to next loop, what else can we do? 276 } 277 // save for return 278 turnoutListForAllocatedSection.add(turnoutList.get(i)); 279 int setting = turnoutList.get(i).getExpectedState(); 280 if (turnoutList.get(i).getObject() instanceof LayoutSlip) { 281 setting = ((LayoutSlip) turnoutList.get(i).getObject()).getTurnoutState(turnoutList.get(i).getExpectedState()); 282 } 283 // check or ignore current setting based on flag, set in Options 284 if (!trustKnownTurnouts && set) { 285 log.debug("{}: setting turnout {} to {}", at.getTrainName(), to.getDisplayName(USERSYS), 286 (setting == Turnout.CLOSED ? closedText : thrownText)); 287 if (checkTurnoutsCanBeSet(turnoutList.get(i).getObject(), setting, s, curBlock, at)) { 288 log.debug("{}: setting turnout {} to {}", at.getTrainName(), to.getDisplayName(USERSYS), 289 (setting == Turnout.CLOSED ? closedText : thrownText)); 290 if (useTurnoutConnectionDelay) { 291 to.setCommandedStateAtInterval(setting); 292 } else { 293 to.setCommandedState(setting); 294 } 295 try { 296 Thread.sleep(100); 297 } catch (InterruptedException ex) { 298 } //TODO: Check if this is needed, shouldnt turnout delays be handled at a lower level. 299 } 300 } else { 301 if (to.getKnownState() != setting) { 302 // turnout is not set correctly 303 if (set) { 304 // setting has been requested, is Section free and Block unoccupied 305 if (checkTurnoutsCanBeSet(turnoutList.get(i).getObject(), setting, s, curBlock, at)) { 306 // send setting command 307 log.debug("{}: turnout {} commanded to {}", at.getTrainName(), to.getDisplayName(), 308 (setting == Turnout.CLOSED ? closedText : thrownText)); 309 if (useTurnoutConnectionDelay) { 310 to.setCommandedStateAtInterval(setting); 311 } else { 312 to.setCommandedState(setting); 313 } 314 try { 315 Thread.sleep(100); 316 } catch (InterruptedException ex) { 317 } //TODO: move this to separate thread 318 } else { 319 turnoutsOK = false; 320 } 321 } else { 322 turnoutsOK = false; 323 } 324 } else { 325 log.debug("{}: turnout {} already {}, skipping", at.getTrainName(), to.getDisplayName(USERSYS), 326 (setting == Turnout.CLOSED ? closedText : thrownText)); 327 } 328 } 329 if (turnoutList.get(i).getObject() instanceof LayoutSlip) { 330 //Look at the state of the second turnout in the slip 331 setting = ((LayoutSlip) turnoutList.get(i).getObject()).getTurnoutBState(turnoutList.get(i).getExpectedState()); 332 to = ((LayoutSlip) turnoutList.get(i).getObject()).getTurnoutB(); 333 if (!trustKnownTurnouts) { 334 if (useTurnoutConnectionDelay) { 335 to.setCommandedStateAtInterval(setting); 336 } else { 337 to.setCommandedState(setting); 338 } 339 } else if (to.getKnownState() != setting) { 340 // turnout is not set correctly 341 if (set) { 342 // setting has been requested, is Section free and Block unoccupied 343 if ((s.getState() == Section.FREE) && (curBlock.getState() != Block.OCCUPIED)) { 344 // send setting command 345 if (useTurnoutConnectionDelay) { 346 to.setCommandedStateAtInterval(setting); 347 } else { 348 to.setCommandedState(setting); 349 } 350 } else { 351 turnoutsOK = false; 352 } 353 } else { 354 turnoutsOK = false; 355 } 356 } 357 } 358 } 359 if (turnoutsOK) { 360 // move to next Block if any 361 if (nextBlockSeqNum >= 0) { 362 prevBlock = curBlock; 363 curBlock = nextBlock; 364 if ((exitPt != null) && (curBlock == exitPt.getBlock())) { 365 // next block is outside of the Section 366 nextBlock = exitPt.getFromBlock(); 367 nextBlockSeqNum = -1; 368 } else { 369 if (direction == Section.FORWARD) { 370 nextBlockSeqNum++; 371 } else { 372 nextBlockSeqNum--; 373 } 374 nextBlock = s.getBlockBySequenceNumber(nextBlockSeqNum); 375 if (nextBlock == null) { 376 // there is no next Block 377 nextBlockSeqNum = -1; 378 } 379 } 380 } else { 381 curBlock = null; 382 } 383 } else { 384 curBlock = null; 385 } 386 } 387 if (turnoutsOK) { 388 return turnoutListForAllocatedSection; 389 } 390 return null; 391 } 392 393 /* 394 * Check that the turnout is safe to change. 395 */ 396 private boolean checkTurnoutsCanBeSet(LayoutTurnout layoutTurnout, int setting, Section s, Block b, ActiveTrain at) { 397 if (layoutTurnout instanceof LayoutDoubleXOver) { 398 LayoutDoubleXOver lds = (LayoutDoubleXOver) layoutTurnout; 399 if ((lds.getLayoutBlock().getBlock().getState() == Block.OCCUPIED) 400 || (lds.getLayoutBlockB().getBlock().getState() == Block.OCCUPIED) 401 || (lds.getLayoutBlockC().getBlock().getState() == Block.OCCUPIED) 402 || (lds.getLayoutBlockD().getBlock().getState() == Block.OCCUPIED)) { 403 log.debug("{}: turnout {} cannot be set to {} DoubleXOver occupied.", 404 at.getTrainName(),layoutTurnout.getTurnout().getDisplayName(), 405 (setting == Turnout.CLOSED ? closedText : thrownText)); 406 return(false); 407 } 408 if ((_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlock().getBlock(), s)) 409 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockB().getBlock(), s)) 410 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockC().getBlock(), s)) 411 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockD().getBlock(), s))) { 412 log.debug("{}: turnout {} cannot be set to {} DoubleXOver already allocated to another train.", 413 at.getTrainName(), layoutTurnout.getTurnout().getDisplayName(), 414 (setting == Turnout.CLOSED ? closedText : thrownText)); 415 return(false); 416 } 417 } else if (layoutTurnout instanceof LayoutRHXOver) { 418 LayoutRHXOver lds = (LayoutRHXOver) layoutTurnout; 419 if ((lds.getLayoutBlock().getBlock().getState() == Block.OCCUPIED) 420 || (lds.getLayoutBlockC().getBlock().getState() == Block.OCCUPIED)) { 421 log.debug("{}: turnout {} cannot be set to {} RHXOver occupied.", 422 at.getTrainName(),layoutTurnout.getTurnout().getDisplayName(), 423 (setting == Turnout.CLOSED ? closedText : thrownText)); 424 return(false); 425 } 426 if ((_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlock().getBlock(), s)) 427 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockC().getBlock(), s))) { 428 log.debug("{}: turnout {} cannot be set to {} RHXOver already allocated to another train.", 429 at.getTrainName(), layoutTurnout.getTurnout().getDisplayName(), 430 (setting == Turnout.CLOSED ? closedText : thrownText)); 431 return(false); 432 } 433 } else if (layoutTurnout instanceof LayoutLHXOver) { 434 LayoutLHXOver lds = (LayoutLHXOver) layoutTurnout; 435 if ((lds.getLayoutBlockB().getBlock().getState() == Block.OCCUPIED) 436 || (lds.getLayoutBlockD().getBlock().getState() == Block.OCCUPIED)) { 437 log.debug("{}: turnout {} cannot be set to {} LHXOver occupied.", 438 at.getTrainName(),layoutTurnout.getTurnout().getDisplayName(), 439 (setting == Turnout.CLOSED ? closedText : thrownText)); 440 return(false); 441 } 442 if ((_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockB().getBlock(), s)) 443 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockD().getBlock(), s))) { 444 log.debug("{}: turnout {} cannot be set to {} RHXOver already allocated to another train.", 445 at.getTrainName(), layoutTurnout.getTurnout().getDisplayName(), 446 (setting == Turnout.CLOSED ? closedText : thrownText)); 447 return(false); 448 } 449 } 450 451 if (s.getState() == Section.FREE && b.getState() != Block.OCCUPIED) { 452 return true; 453 } 454 return false; 455 } 456 457 private final static Logger log = LoggerFactory.getLogger(AutoTurnouts.class); 458}