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 // initialize response structure 139 List<LayoutTrackExpectedState<LayoutTurnout>> turnoutListForAllocatedSection = new ArrayList<>(); 140 // validate input and initialize 141 Transit tran = at.getTransit(); 142 if ((s == null) || (seqNum > tran.getMaxSequence()) || (!tran.containsSection(s))) { 143 log.error("Invalid argument when checking or setting turnouts in Section."); 144 return null; 145 } 146 int direction = at.getAllocationDirectionFromSectionAndSeq(s, seqNum); 147 if (direction == 0) { 148 log.error("Invalid Section/sequence arguments when checking or setting turnouts"); 149 return null; 150 } 151 // Did have this set to include SignalMasts as part of the && statement 152 //Sections created using Signal masts will generally only have a single entry/exit point. 153 // check for no turnouts in this section 154 if (_dispatcher.getSignalType() == DispatcherFrame.SIGNALHEAD && (s.getForwardEntryPointList().size() <= 1) && (s.getReverseEntryPointList().size() <= 1)) { 155 log.debug("No entry points lists"); 156 // no possibility of turnouts 157 return turnoutListForAllocatedSection; 158 } 159 // initialize connectivity utilities and beginning block pointers 160 EntryPoint entryPt = null; 161 if (prevSection != null) { 162 entryPt = s.getEntryPointFromSection(prevSection, direction); 163 } else if (!s.containsBlock(at.getStartBlock())) { 164 entryPt = s.getEntryPointFromBlock(at.getStartBlock(), direction); 165 } 166 EntryPoint exitPt = null; 167 if (nextSection != null) { 168 exitPt = s.getExitPointToSection(nextSection, direction); 169 } 170 Block curBlock; // must be in the section 171 Block prevBlock = null; // must start outside the section or be null 172 int curBlockSeqNum; // sequence number of curBlock in Section 173 if (entryPt != null) { 174 curBlock = entryPt.getBlock(); 175 prevBlock = entryPt.getFromBlock(); 176 curBlockSeqNum = s.getBlockSequenceNumber(curBlock); 177 } else if ( !at.isAllocationReversed() && s.containsBlock(at.getStartBlock())) { 178 curBlock = at.getStartBlock(); 179 curBlockSeqNum = s.getBlockSequenceNumber(curBlock); 180 //Get the previous block so that we can set the turnouts in the current block correctly. 181 if (direction == Section.FORWARD) { 182 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum - 1); 183 } else if (direction == Section.REVERSE) { 184 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum + 1); 185 } 186 } else if (at.isAllocationReversed() && s.containsBlock(at.getEndBlock())) { 187 curBlock = at.getEndBlock(); 188 curBlockSeqNum = s.getBlockSequenceNumber(curBlock); 189 //Get the previous block so that we can set the turnouts in the current block correctly. 190 if (direction == Section.REVERSE) { 191 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum + 1); 192 } else if (direction == Section.FORWARD) { 193 prevBlock = s.getBlockBySequenceNumber(curBlockSeqNum - 1); 194 } 195 } else { 196 197 //if (_dispatcher.getSignalType() == DispatcherFrame.SIGNALMAST) { 198 // //This can be considered normal where SignalMast Logic is used. 199 // return true; 200 //} 201 // this is an error but is it? It only happens when system is under stress 202 // which would point to a threading issue. 203 try { 204 log.error("[{}]direction[{}] Section[{}]Error in turnout check/set request - initial Block[{}] and Section[{}] mismatch", 205 at.getActiveTrainName(),at.isAllocationReversed(),s.getDisplayName(USERSYS), 206 at.getStartBlock().getUserName(),at.getEndBlock().getDisplayName(USERSYS)); 207 } catch (Exception ex ) { 208 log.warn("Exception while creating log error : {}", ex.getLocalizedMessage()); 209 } 210 return turnoutListForAllocatedSection; 211 } 212 213 Block nextBlock = null; 214 // may be either in the section or the first block in the next section 215 int nextBlockSeqNum = -1; // sequence number of nextBlock in Section (-1 indicates outside Section) 216 if (exitPt != null && curBlock == exitPt.getBlock()) { 217 // next Block is outside of the Section 218 nextBlock = exitPt.getFromBlock(); 219 } else { 220 // next Block is inside the Section 221 if (direction == Section.FORWARD) { 222 nextBlock = s.getBlockBySequenceNumber(curBlockSeqNum + 1); 223 nextBlockSeqNum = curBlockSeqNum + 1; 224 } else if (direction == Section.REVERSE) { 225 nextBlock = s.getBlockBySequenceNumber(curBlockSeqNum - 1); 226 nextBlockSeqNum = curBlockSeqNum - 1; 227 } 228 if ((nextBlock == null && 229 ((!at.isAllocationReversed() && curBlock != at.getEndBlock()) || 230 (at.isAllocationReversed() && curBlock != at.getStartBlock())))) { 231 log.error("[{}]Error in block sequence numbers when setting/checking turnouts.", 232 curBlock.getDisplayName(USERSYS)); 233 return null; 234 } 235 } 236 237 List<LayoutTrackExpectedState<LayoutTurnout>> turnoutList = new ArrayList<>(); 238 // get turnouts by Block 239 boolean turnoutsOK = true; 240 241 var layoutBlockManger = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class); 242 while (curBlock != null) { 243 /*No point in getting the list if the previous block is null as it will return empty and generate an error, 244 this will only happen on the first run. Plus working on the basis that the turnouts in the current block would have already of 245 been set correctly for the train to have arrived in the first place. 246 */ 247 248 if (prevBlock != null) { 249 var blockName = curBlock.getUserName(); 250 if (blockName != null) { 251 var lblock = layoutBlockManger.getLayoutBlock(blockName); 252 if (lblock != null) { 253 var panel = lblock.getMaxConnectedPanel(); 254 if (panel != null) { 255 var connection = new ConnectivityUtil(panel); 256 turnoutList = connection.getTurnoutList(curBlock, prevBlock, nextBlock, true); 257 } 258 } 259 } 260 } 261 // loop over turnouts checking and optionally setting turnouts 262 for (int i = 0; i < turnoutList.size(); i++) { 263 Turnout to = turnoutList.get(i).getObject().getTurnout(); 264 if (to == null ) { 265 // this should not happen due to prior selection 266 log.error("Found null Turnout reference at {}: {}", i, turnoutList.get(i).getObject()); 267 continue; // move to next loop, what else can we do? 268 } 269 // save for return 270 turnoutListForAllocatedSection.add(turnoutList.get(i)); 271 int setting = turnoutList.get(i).getExpectedState(); 272 if (turnoutList.get(i).getObject() instanceof LayoutSlip) { 273 setting = ((LayoutSlip) turnoutList.get(i).getObject()).getTurnoutState(turnoutList.get(i).getExpectedState()); 274 } 275 // check or ignore current setting based on flag, set in Options 276 if (!trustKnownTurnouts && set) { 277 log.debug("{}: setting turnout {} to {}", at.getTrainName(), to.getDisplayName(USERSYS), 278 (setting == Turnout.CLOSED ? closedText : thrownText)); 279 if (checkTurnoutsCanBeSet(turnoutList.get(i).getObject(), setting, s, curBlock, at)) { 280 log.debug("{}: setting turnout {} to {}", at.getTrainName(), to.getDisplayName(USERSYS), 281 (setting == Turnout.CLOSED ? closedText : thrownText)); 282 if (useTurnoutConnectionDelay) { 283 to.setCommandedStateAtInterval(setting); 284 } else { 285 to.setCommandedState(setting); 286 } 287 try { 288 Thread.sleep(100); 289 } catch (InterruptedException ex) { 290 } //TODO: Check if this is needed, shouldnt turnout delays be handled at a lower level. 291 } 292 } else { 293 if (to.getKnownState() != setting) { 294 // turnout is not set correctly 295 if (set) { 296 // setting has been requested, is Section free and Block unoccupied 297 if (checkTurnoutsCanBeSet(turnoutList.get(i).getObject(), setting, s, curBlock, at)) { 298 // send setting command 299 log.debug("{}: turnout {} commanded to {}", at.getTrainName(), to.getDisplayName(), 300 (setting == Turnout.CLOSED ? closedText : thrownText)); 301 if (useTurnoutConnectionDelay) { 302 to.setCommandedStateAtInterval(setting); 303 } else { 304 to.setCommandedState(setting); 305 } 306 try { 307 Thread.sleep(100); 308 } catch (InterruptedException ex) { 309 } //TODO: move this to separate thread 310 } else { 311 turnoutsOK = false; 312 } 313 } else { 314 turnoutsOK = false; 315 } 316 } else { 317 log.debug("{}: turnout {} already {}, skipping", at.getTrainName(), to.getDisplayName(USERSYS), 318 (setting == Turnout.CLOSED ? closedText : thrownText)); 319 } 320 } 321 if (turnoutList.get(i).getObject() instanceof LayoutSlip) { 322 //Look at the state of the second turnout in the slip 323 setting = ((LayoutSlip) turnoutList.get(i).getObject()).getTurnoutBState(turnoutList.get(i).getExpectedState()); 324 to = ((LayoutSlip) turnoutList.get(i).getObject()).getTurnoutB(); 325 if (!trustKnownTurnouts) { 326 if (useTurnoutConnectionDelay) { 327 to.setCommandedStateAtInterval(setting); 328 } else { 329 to.setCommandedState(setting); 330 } 331 } else if (to.getKnownState() != setting) { 332 // turnout is not set correctly 333 if (set) { 334 // setting has been requested, is Section free and Block unoccupied 335 if ((s.getState() == Section.FREE) && (curBlock.getState() != Block.OCCUPIED)) { 336 // send setting command 337 if (useTurnoutConnectionDelay) { 338 to.setCommandedStateAtInterval(setting); 339 } else { 340 to.setCommandedState(setting); 341 } 342 } else { 343 turnoutsOK = false; 344 } 345 } else { 346 turnoutsOK = false; 347 } 348 } 349 } 350 } 351 if (turnoutsOK) { 352 // move to next Block if any 353 if (nextBlockSeqNum >= 0) { 354 prevBlock = curBlock; 355 curBlock = nextBlock; 356 if ((exitPt != null) && (curBlock == exitPt.getBlock())) { 357 // next block is outside of the Section 358 nextBlock = exitPt.getFromBlock(); 359 nextBlockSeqNum = -1; 360 } else { 361 if (direction == Section.FORWARD) { 362 nextBlockSeqNum++; 363 } else { 364 nextBlockSeqNum--; 365 } 366 nextBlock = s.getBlockBySequenceNumber(nextBlockSeqNum); 367 if (nextBlock == null) { 368 // there is no next Block 369 nextBlockSeqNum = -1; 370 } 371 } 372 } else { 373 curBlock = null; 374 } 375 } else { 376 curBlock = null; 377 } 378 } 379 if (turnoutsOK) { 380 return turnoutListForAllocatedSection; 381 } 382 return null; 383 } 384 385 /* 386 * Check that the turnout is safe to change. 387 */ 388 private boolean checkTurnoutsCanBeSet(LayoutTurnout layoutTurnout, int setting, Section s, Block b, ActiveTrain at) { 389 if (layoutTurnout instanceof LayoutDoubleXOver) { 390 LayoutDoubleXOver lds = (LayoutDoubleXOver) layoutTurnout; 391 if ((lds.getLayoutBlock().getBlock().getState() == Block.OCCUPIED) 392 || (lds.getLayoutBlockB().getBlock().getState() == Block.OCCUPIED) 393 || (lds.getLayoutBlockC().getBlock().getState() == Block.OCCUPIED) 394 || (lds.getLayoutBlockD().getBlock().getState() == Block.OCCUPIED)) { 395 log.debug("{}: turnout {} cannot be set to {} DoubleXOver occupied.", 396 at.getTrainName(),layoutTurnout.getTurnout().getDisplayName(), 397 (setting == Turnout.CLOSED ? closedText : thrownText)); 398 return(false); 399 } 400 if ((_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlock().getBlock(), s)) 401 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockB().getBlock(), s)) 402 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockC().getBlock(), s)) 403 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockD().getBlock(), s))) { 404 log.debug("{}: turnout {} cannot be set to {} DoubleXOver already allocated to another train.", 405 at.getTrainName(), layoutTurnout.getTurnout().getDisplayName(), 406 (setting == Turnout.CLOSED ? closedText : thrownText)); 407 return(false); 408 } 409 } else if (layoutTurnout instanceof LayoutRHXOver) { 410 LayoutRHXOver lds = (LayoutRHXOver) layoutTurnout; 411 if ((lds.getLayoutBlock().getBlock().getState() == Block.OCCUPIED) 412 || (lds.getLayoutBlockC().getBlock().getState() == Block.OCCUPIED)) { 413 log.debug("{}: turnout {} cannot be set to {} RHXOver occupied.", 414 at.getTrainName(),layoutTurnout.getTurnout().getDisplayName(), 415 (setting == Turnout.CLOSED ? closedText : thrownText)); 416 return(false); 417 } 418 if ((_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlock().getBlock(), s)) 419 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockC().getBlock(), s))) { 420 log.debug("{}: turnout {} cannot be set to {} RHXOver already allocated to another train.", 421 at.getTrainName(), layoutTurnout.getTurnout().getDisplayName(), 422 (setting == Turnout.CLOSED ? closedText : thrownText)); 423 return(false); 424 } 425 } else if (layoutTurnout instanceof LayoutLHXOver) { 426 LayoutLHXOver lds = (LayoutLHXOver) layoutTurnout; 427 if ((lds.getLayoutBlockB().getBlock().getState() == Block.OCCUPIED) 428 || (lds.getLayoutBlockD().getBlock().getState() == Block.OCCUPIED)) { 429 log.debug("{}: turnout {} cannot be set to {} LHXOver occupied.", 430 at.getTrainName(),layoutTurnout.getTurnout().getDisplayName(), 431 (setting == Turnout.CLOSED ? closedText : thrownText)); 432 return(false); 433 } 434 if ((_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockB().getBlock(), s)) 435 || (_dispatcher.checkForBlockInAllocatedSection(lds.getLayoutBlockD().getBlock(), s))) { 436 log.debug("{}: turnout {} cannot be set to {} RHXOver already allocated to another train.", 437 at.getTrainName(), layoutTurnout.getTurnout().getDisplayName(), 438 (setting == Turnout.CLOSED ? closedText : thrownText)); 439 return(false); 440 } 441 } 442 443 if (s.getState() == Section.FREE && b.getState() != Block.OCCUPIED) { 444 return true; 445 } 446 return false; 447 } 448 449 private final static Logger log = LoggerFactory.getLogger(AutoTurnouts.class); 450}