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