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}