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}