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