001package jmri.jmrit.display.layoutEditor;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.List;
006import java.util.Set;
007import jmri.*;
008import jmri.jmrit.display.EditorManager;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011import org.slf4j.MDC;
012
013/**
014 * These are a series of layout block connectivity tools that can be used when
015 * the advanced layout block routing has been enabled. These tools can determine
016 * if a path from a source to destination bean is valid. If a route between two
017 * layout blocks is usable and free.
018 *
019 * @author Kevin Dickerson Copyright (C) 2011
020 * @author George Warner Copyright (c) 2017-2018
021 */
022public final class LayoutBlockConnectivityTools {
023
024    public LayoutBlockConnectivityTools() {
025    }
026
027    public enum Routing {
028        /**
029         * Constant used in the getLayoutBlocks to represent a path from one Signal
030         * Mast to another and that no mast should be in the path.
031         */
032        MASTTOMAST,
033
034        /**
035         * Constant used in the getLayoutBlocks to represent a path from one Signal
036         * Head to another and that no head should be in the path.
037         */
038        HEADTOHEAD,
039
040        /**
041         * Constant used in the getLayoutBlocks to represent a path from one Sensor
042         * to another and that no sensor should be in the path.
043         */
044        SENSORTOSENSOR,
045
046        /**
047         * Constant used in the getLayoutBlocks to represent a path from either a
048         * Signal Mast or Head to another Signal Mast or Head and that no mast of
049         * head should be in the path.
050         */
051        ANY,
052
053        /**
054         * Constant used in the getLayoutBlocks to indicate that the system
055         * should not check for signal masts or heads on the path.
056         */
057        NONE
058    }
059
060    public enum Metric {
061        HOPCOUNT,
062        METRIC,
063        DISTANCE
064    }
065
066    private static final int ttlSize = 50;
067
068
069    /**
070     * Determines if a pair of NamedBeans (Signalhead, Signalmast or Sensor)
071     * assigned to a block boundary are reachable.<br>
072     * Called by {@link jmri.jmrit.signalling.SignallingPanel} using MASTTOMAST.
073     * <p>
074     * Search all of the layout editor panels to find the facing and protecting
075     * layout blocks for each bean.  Call the 3 block+list version of checkValidDest() to finish the checks.
076     *
077     * @param sourceBean The source bean.
078     * @param destBean   The destination bean.
079     * @param pathMethod Indicates the type of path:  Signal head, signal mast or sensor.
080     * @return true if source and destination beans are reachable.
081     * @throws jmri.JmriException if no blocks can be found that related to the
082     *                            named beans.
083     */
084    public boolean checkValidDest(NamedBean sourceBean, NamedBean destBean, Routing pathMethod) throws jmri.JmriException {
085        if (log.isDebugEnabled()) {
086            log.debug("checkValidDest with source/dest bean {} {}", sourceBean.getDisplayName(), destBean.getDisplayName());
087        }
088        LayoutBlock facingBlock = null;
089        LayoutBlock protectingBlock = null;
090        LayoutBlock destFacingBlock = null;
091        List<LayoutBlock> destProtectBlock = null;
092        Set<LayoutEditor> layout = InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class);
093        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
094        for (LayoutEditor layoutEditor : layout) {
095            if (log.isDebugEnabled()) {
096                log.debug("Layout name {}", layoutEditor.getLayoutName());
097            }
098            if (facingBlock == null) {
099                facingBlock = lbm.getFacingBlockByNamedBean(sourceBean, layoutEditor);
100            }
101            if (protectingBlock == null) {
102                protectingBlock = lbm.getProtectedBlockByNamedBean(sourceBean, layoutEditor);
103            }
104            if (destFacingBlock == null) {
105                destFacingBlock = lbm.getFacingBlockByNamedBean(destBean, layoutEditor);
106            }
107            if (destProtectBlock == null) {
108                destProtectBlock = lbm.getProtectingBlocksByNamedBean(destBean, layoutEditor);
109            }
110            if ((destFacingBlock != null) && (facingBlock != null) && (protectingBlock != null)) {
111                /*Destination protecting block list is allowed to be empty, as the destination signalmast
112                 could be assigned to an end bumper */
113                // A simple to check to see if the remote signal/sensor is in the correct direction to ours.
114                try {
115                    return checkValidDest(facingBlock, protectingBlock, destFacingBlock, destProtectBlock, pathMethod);
116                } catch (JmriException e) {
117                    log.debug("Rethrowing exception from checkValidDest(..)");
118                    throw e;
119                }
120            } else {
121                log.debug("blocks not found");
122            }
123        }
124        if (log.isDebugEnabled()) {
125            log.debug("No valid route from {} to {}", sourceBean.getDisplayName(), destBean.getDisplayName());
126        }
127        throw new jmri.JmriException("Blocks Not Found");
128    }
129
130    /**
131     * The is used in conjunction with the layout block routing protocol, to
132     * discover a clear path from a source layout block through to a destination
133     * layout block. By specifying the sourceLayoutBlock and
134     * protectingLayoutBlock or sourceLayoutBlock+1, a direction of travel can
135     * then be determined, eg east to west, south to north etc.
136     * @param sourceBean    The source bean (SignalHead, SignalMast or Sensor)
137     *                     assigned to a block boundary that we are starting
138     *                     from.
139     * @param destBean      The destination bean.
140     * @param validateOnly  When set false, the system will not use layout
141     *                     blocks that are set as either reserved(useExtraColor
142     *                     set) or occupied, if it finds any then it will try to
143     *                     find an alternative path When set false, no block
144     *                     state checking is performed.
145     * @param pathMethod    Performs a check to see if any signal heads/masts
146     *                     are in the path, if there are then the system will
147     *                     try to find an alternative path. If set to NONE, then
148     *                     no checking is performed.
149     * @return an List of all the layoutblocks in the path.
150     * @throws jmri.JmriException if it can not find a valid path or the routing
151     *                            has not been enabled.
152     */
153    public List<LayoutBlock> getLayoutBlocks(NamedBean sourceBean, NamedBean destBean, boolean validateOnly, Routing pathMethod) throws jmri.JmriException {
154        Set<LayoutEditor> layout = InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class);
155        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
156        LayoutBlock facingBlock = null;
157        LayoutBlock protectingBlock = null;
158        LayoutBlock destFacingBlock = null;
159        for (LayoutEditor layoutEditor : layout) {
160            if (log.isDebugEnabled()) {
161                log.debug("Layout name {}", layoutEditor.getLayoutName());
162            }
163            if (facingBlock == null) {
164                facingBlock = lbm.getFacingBlockByNamedBean(sourceBean, layoutEditor);
165            }
166            if (protectingBlock == null) {
167                protectingBlock = lbm.getProtectedBlockByNamedBean(sourceBean, layoutEditor);
168            }
169            if (destFacingBlock == null) {
170                destFacingBlock = lbm.getFacingBlockByNamedBean(destBean, layoutEditor);
171            }
172            if ((destFacingBlock != null) && (facingBlock != null) && (protectingBlock != null)) {
173                try {
174                    return getLayoutBlocks(facingBlock, destFacingBlock, protectingBlock, validateOnly, pathMethod);
175                } catch (JmriException e) {
176                    log.debug("Rethrowing exception from getLayoutBlocks()");
177                    throw e;
178                }
179            } else {
180                log.debug("blocks not found");
181            }
182        }
183        if (log.isDebugEnabled()) {
184            log.debug("No valid route from {} to {}", sourceBean.getDisplayName(), destBean.getDisplayName());
185        }
186        throw new jmri.JmriException("Blocks Not Found");
187    }
188
189    /**
190     * Returns a list of NamedBeans (Signalhead, Signalmast or Sensor) that are
191     * assigned to block boundaries in a given list.
192     *
193     * @param blocklist The list of block in order that need to be checked.
194     * @param panel     (Optional) panel that the blocks need to be checked
195     *                  against
196     * @param T         (Optional) the class that we want to check against,
197     *                  either Sensor, SignalMast or SignalHead, set null will
198     *                  return any.
199     * @return the list of NamedBeans
200     */
201    public List<NamedBean> getBeansInPath(List<LayoutBlock> blocklist, LayoutEditor panel, Class<?> T) {
202        List<NamedBean> beansInPath = new ArrayList<>();
203        if (blocklist.size() >= 2) {
204            LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
205            for (int x = 1; x < blocklist.size(); x++) {
206                LayoutBlock facingBlock = blocklist.get(x - 1);
207                LayoutBlock protectingBlock = blocklist.get(x);
208                log.debug("x {} facing block {} protecting block {}", x, facingBlock.getBlock().getUserName(), protectingBlock.getBlock().getUserName());
209                NamedBean nb = null;
210                if (T == null) {
211                    nb = lbm.getFacingNamedBean(facingBlock.getBlock(), protectingBlock.getBlock(), panel);
212                } else if (T.equals(jmri.SignalMast.class)) {
213                    nb = lbm.getFacingSignalMast(facingBlock.getBlock(), protectingBlock.getBlock(), panel);
214                    log.debug ("x {} nb {}", x, nb);
215                } else if (T.equals(jmri.Sensor.class)) {
216                    nb = lbm.getFacingSensor(facingBlock.getBlock(), protectingBlock.getBlock(), panel);
217                } else if (T.equals(jmri.SignalHead.class)) {
218                    nb = lbm.getFacingSignalHead(facingBlock.getBlock(), protectingBlock.getBlock());
219                }
220                if (nb != null) {
221                    beansInPath.add(nb);
222                }
223            }
224        }
225        return beansInPath;
226    }
227
228    /**
229     * Determines if one set of blocks is reachable from another set of blocks
230     * based upon the directions of the set of blocks.
231     * <ul>
232     * <li>Called by {@link jmri.implementation.DefaultSignalMastLogic} using MASTTOMAST.</li>
233     * <li>Called by {@link jmri.jmrit.entryexit.DestinationPoints} using SENSORTOSENSOR.</li>
234     * <li>Called by {@link jmri.jmrit.entryexit.EntryExitPairs} using SENSORTOSENSOR.</li>
235     * </ul>
236     * Convert the destination protected block to an array list.
237     * Call the 3 block+list version of checkValidDest() to finish the checks.
238     * @param currentBlock The facing layout block for the source signal or sensor.
239     * @param nextBlock    The protected layout block for the source signal or sensor.
240     * @param destBlock    The facing layout block for the destination signal mast or sensor.
241     * @param destProBlock The protected destination block.
242     * @param pathMethod   Indicates the type of path:  Signal head, signal mast or sensor.
243     * @return true if a path to the destination is valid.
244     * @throws jmri.JmriException if any Block is null;
245     */
246    public boolean checkValidDest(LayoutBlock currentBlock, LayoutBlock nextBlock, LayoutBlock destBlock, LayoutBlock destProBlock, Routing pathMethod) throws jmri.JmriException {
247
248        List<LayoutBlock> destList = new ArrayList<>();
249        if (destProBlock != null) {
250            destList.add(destProBlock);
251        }
252        try {
253            return checkValidDest(currentBlock, nextBlock, destBlock, destList, pathMethod);
254        } catch (jmri.JmriException e) {
255            throw e;
256        }
257
258    }
259
260    /**
261     * Determines if one set of blocks is reachable from another set of blocks
262     * based upon the directions of the set of blocks.
263     * <p>
264     * This is used to help with identifying items such as signalmasts located
265     * at positionable points or turnouts are facing in the same direction as
266     * other given signalmasts.
267     * <p>
268     * Given the current block and the next block we can work out the direction
269     * of travel. Given the destBlock and the next block on, we can determine
270     * the whether the destBlock comes before the destBlock+1.
271     * <p>
272     * Note: This version is internally called by other versions that
273     * pre-process external calls.
274     * @param currentBlock The facing layout block for the source signal or
275     *                     sensor.
276     * @param nextBlock    The protected layout block for the source signal or
277     *                     sensor.
278     * @param destBlock    The facing layout block for the destination signal
279     *                     mast or sensor.
280     * @param destBlockn1  A list of protected destination blocks. Can be empty
281     *                     if the destination is at an end bumper.
282     * @param pathMethod   Indicates the type of path: Signal head, signal mast
283     *                     or sensor.
284     * @return true if a path to the destination is valid.
285     * @throws jmri.JmriException if any layout block is null or advanced
286     *                            routing is not enabled.
287     */
288    public boolean checkValidDest(LayoutBlock currentBlock, LayoutBlock nextBlock, LayoutBlock destBlock, List<LayoutBlock> destBlockn1, Routing pathMethod) throws jmri.JmriException {
289        // ----- Begin Turntable Exit Path Check -----
290        // This handles the special case for a path exiting a turntable (Path 1), where the protecting block (a ray)
291        // can be the same as the destination's facing block.
292        for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
293            for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
294                // Check if the path starts from this turntable's block
295                if (turntable.getLayoutBlock() == currentBlock) {
296                    // A path from a turntable is valid if the destination is a ray block or a neighbor of a ray block.
297                    for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
298                        TrackSegment track = ray.getConnect();
299                        if (track != null && track.getLayoutBlock() != null) {
300                            LayoutBlock rayBlock = track.getLayoutBlock();
301                            // First, check if the ray block itself is the destination.
302                            if (rayBlock == destBlock) {
303                                return true;
304                            }
305                            // Next, check if the destination block is a valid neighbor of this ray block.
306                            for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
307                                Block neighbor = rayBlock.getNeighbourAtIndex(i);
308                                if (neighbor != null && neighbor == destBlock.getBlock()) {
309                                    return true;
310                                }
311                            }
312                        }
313                    }
314                }
315            }
316        }
317        // ----- Begin Turntable Siding Path Check (Path 3) -----
318        // This handles the special case for a path entering a turntable to a buffer stop,
319        // where the destination block is the turntable itself.
320        for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
321            for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
322                if (turntable.getLayoutBlock() == destBlock) {
323                    return true;
324                }
325            }
326        }
327        // ----- Begin Traverser Exit Path Check -----
328        // This handles the special case for a path exiting a traverser (Path 1), where the protecting block (a ray)
329        // can be the same as the destination's facing block.
330        for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
331            for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
332                // Check if the path starts from this traverser's block
333                if (traverser.getLayoutBlock() == currentBlock) {
334                    // A path from a traverser is valid if the destination is a slot block or a neighbor of a slot block.
335                    for (int i = 0; i < traverser.getNumberSlots(); i++) {
336                        TrackSegment track = traverser.getSlotConnectOrdered(i);
337                        if (track != null && track.getLayoutBlock() != null) {
338                            LayoutBlock slotBlock = track.getLayoutBlock();
339                            // First, check if the slot block itself is the destination.
340                            if (slotBlock == destBlock) {
341                                return true;
342                            }
343                            // Next, check if the destination block is a valid neighbor of this slot block.
344                            for (int j = 0; j < slotBlock.getNumberOfNeighbours(); j++) {
345                                Block neighbor = slotBlock.getNeighbourAtIndex(j);
346                                if (neighbor != null && neighbor == destBlock.getBlock()) {
347                                    return true;
348                                }
349                            }
350                        }
351                    }
352                }
353            }
354        }
355        // ----- Begin Traverser Siding Path Check (Path 3) -----
356        // This handles the special case for a path entering a traverser to a buffer stop,
357        // where the destination block is the traverser itself.
358        for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
359            for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
360                if (traverser.getLayoutBlock() == destBlock) {
361                    return true;
362                }
363            }
364        }
365        // ----- End Traverser Exit Path Check -----
366        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
367        if (!lbm.isAdvancedRoutingEnabled()) {
368            log.debug("Advanced routing has not been enabled therefore we cannot use this function");
369            throw new jmri.JmriException("Advanced routing has not been enabled therefore we cannot use this function");
370        }
371
372        if (log.isDebugEnabled()) {
373            try {
374                log.debug("faci {}", currentBlock.getDisplayName());
375                log.debug("next {}", nextBlock.getDisplayName());
376                log.debug("dest {}", destBlock.getDisplayName());
377                for (LayoutBlock dp : destBlockn1) {
378                    log.debug("dest + 1 {}", dp.getDisplayName());
379                }
380            } catch (java.lang.NullPointerException e) {
381
382            }
383        }
384        if ((destBlock != null) && (currentBlock != null) && (nextBlock != null)) {
385            if (!currentBlock.isRouteToDestValid(nextBlock.getBlock(), destBlock.getBlock())) {
386                log.debug("Route to dest not valid");
387                return false;
388            }
389            if (log.isDebugEnabled()) {
390                log.debug("dest {}", destBlock.getDisplayName());
391                /*if(destBlockn1!=null)
392                 log.debug("remote prot " + destBlockn1.getDisplayName());*/
393            }
394            if (!destBlockn1.isEmpty() && currentBlock == destBlockn1.get(0) && nextBlock == destBlock) {
395                log.debug("Our dest protecting block is our current block and our protecting block is the same as our destination block");
396                return false;
397            }
398            // Do a simple test to see if one is reachable from the other.
399            int proCount = 0;
400            int desCount = 0;
401            if (!destBlockn1.isEmpty()) {
402                desCount = currentBlock.getBlockHopCount(destBlock.getBlock(), nextBlock.getBlock());
403                proCount = currentBlock.getBlockHopCount(destBlockn1.get(0).getBlock(), nextBlock.getBlock());
404                if (log.isDebugEnabled()) {
405                    log.debug("dest {} protecting {}", desCount, proCount);
406                }
407            }
408
409            if ((proCount == -1) && (desCount == -1)) {
410                // The destination block and destBlock+1 are both directly connected
411                log.debug("Dest and dest+1 are directly connected");
412                return false;
413            }
414
415            if (proCount > desCount && (proCount - 1) == desCount) {
416                // The block that we are protecting should be one hop greater than the destination count.
417                log.debug("Protecting is one hop away from destination and therefore valid.");
418                return true;
419            }
420
421            /*Need to do a more advanced check in this case as the destBlockn1
422             could be reached via a different route and therefore have a smaller
423             hop count we need to therefore step through each block until we reach
424             the end.
425             The advanced check also covers cases where the route table is inconsistent.
426             We also need to perform a more advanced check if the destBlockn1
427             is null as this indicates that the destination signal mast is assigned
428             on an end bumper*/
429
430            if (pathMethod == Routing.SENSORTOSENSOR && destBlockn1.size() == 0) {
431                // Change the pathMethod to accept the NX sensor at the end bumper.
432                pathMethod = Routing.NONE;
433            }
434
435            List<LayoutBlock> blockList = getLayoutBlocks(currentBlock, destBlock, nextBlock, true, pathMethod); // Was MASTTOMAST
436            if (log.isDebugEnabled()) {
437                log.debug("checkValidDest blockList for {}", destBlock.getDisplayName());
438                blockList.forEach(blk -> log.debug("  block = {}", blk.getDisplayName()));
439            }
440            for (LayoutBlock dp : destBlockn1) {
441                log.debug("dp = {}", dp.getDisplayName());
442                if (blockList.contains(dp) && currentBlock != dp) {
443                    log.debug("Signal mast in the wrong direction");
444                    return false;
445                }
446            }
447                /*Work on the basis that if you get the blocks from source to dest
448                 then the dest+1 block should not be included*/
449            log.debug("Signal mast in the correct direction");
450            return true;
451
452        } else if (destBlock == null) {
453            throw new jmri.JmriException("Block in Destination Field returns as invalid");
454        } else if (currentBlock == null) {
455            throw new jmri.JmriException("Block in Facing Field returns as invalid");
456        } else if (nextBlock == null) {
457            throw new jmri.JmriException("Block in Protecting Field returns as invalid");
458        }
459        throw new jmri.JmriException("BlockIsNull");
460    }
461
462    /**
463     * This uses the layout editor to check if the destination location is
464     * reachable from the source location.<br>
465     * Only used internally to the class.
466     * <p>
467     * @param facing     Layout Block that is considered our first block
468     * @param protecting Layout Block that is considered first block +1
469     * @param dest       Layout Block that we want to get to
470     * @param pathMethod the path method
471     * @return true if valid
472     * @throws JmriException during nested getProtectingBlocks operation
473     */
474    private boolean checkValidDest(LayoutBlock facing, LayoutBlock protecting, FacingProtecting dest, Routing pathMethod) throws JmriException {
475        if (facing == null || protecting == null || dest == null) {
476            return false;
477        }
478        if (log.isDebugEnabled()) {
479            log.debug("facing : {} protecting : {} dest {}", protecting.getDisplayName(), dest.getBean().getDisplayName(), facing.getDisplayName());
480        }
481
482        // In this instance it doesn't matter what the destination protecting block is so we get the first
483        /*LayoutBlock destProt = null;
484         if(!dest.getProtectingBlocks().isEmpty()){
485         destProt = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(dest.getProtectingBlocks().get(0));
486         }*/
487
488        List<LayoutBlock> destList = new ArrayList<>();
489
490         // may throw JmriException here
491        dest.getProtectingBlocks().forEach((b) -> {
492            destList.add(InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(b));
493        });
494        return checkValidDest(facing, protecting, InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(dest.getFacing()), destList, pathMethod);
495    }
496
497    /**
498     * This used in conjunction with the layout block routing protocol, to
499     * discover a clear path from a source layout block through to a destination
500     * layout block. By specifying the sourceLayoutBlock and
501     * protectingLayoutBlock or sourceLayoutBlock+1, a direction of travel can
502     * then be determined, eg east to west, south to north etc.
503     *
504     * @param sourceLayoutBlock      The layout block that we are starting from,
505     *                               can also be considered as the block facing
506     *                               a signal.
507     * @param destinationLayoutBlock The layout block that we want to get to
508     * @param protectingLayoutBlock  The next layout block connected to the
509     *                               source block, this can also be considered
510     *                               as the block being protected by a signal
511     * @param validateOnly           When set false, the system will not use
512     *                               layout blocks that are set as either
513     *                               reserved(useExtraColor set) or occupied, if
514     *                               it finds any then it will try to find an
515     *                               alternative path When set true, no block
516     *                               state checking is performed.
517     * @param pathMethod             Performs a check to see if any signal
518     *                               heads/masts are in the path, if there are
519     *                               then the system will try to find an
520     *                               alternative path. If set to NONE, then no
521     *                               checking is performed.
522     * @return an List of all the layoutblocks in the path.
523     * @throws jmri.JmriException if it can not find a valid path or the routing
524     *                            has not been enabled.
525     */
526    public List<LayoutBlock> getLayoutBlocks(LayoutBlock sourceLayoutBlock, LayoutBlock destinationLayoutBlock, LayoutBlock protectingLayoutBlock, boolean validateOnly, Routing pathMethod) throws jmri.JmriException {
527        lastErrorMessage = "Unknown Error Occured";
528        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
529
530        if (!lbm.isAdvancedRoutingEnabled()) {
531            log.debug("Advanced routing has not been enabled therefore we cannot use this function");
532            throw new jmri.JmriException("Advanced routing has not been enabled therefore we cannot use this function");
533        }
534
535        int directionOfTravel = sourceLayoutBlock.getNeighbourDirection(protectingLayoutBlock);
536        Block currentBlock = sourceLayoutBlock.getBlock();
537
538        Block destBlock = destinationLayoutBlock.getBlock();
539        log.debug("Destination Block {} {}", destinationLayoutBlock.getDisplayName(), destBlock);
540
541        Block nextBlock = protectingLayoutBlock.getBlock();
542        if (log.isDebugEnabled()) {
543            log.debug("s:{} p:{} d:{}", sourceLayoutBlock.getDisplayName(), protectingLayoutBlock.getDisplayName(), destinationLayoutBlock.getDisplayName());
544        }
545        List<BlocksTested> blocksInRoute = new ArrayList<>();
546        blocksInRoute.add(new BlocksTested(sourceLayoutBlock));
547
548        if (!validateOnly) {
549            if (canLBlockBeUsed(protectingLayoutBlock)) {
550                blocksInRoute.add(new BlocksTested(protectingLayoutBlock));
551            } else {
552                lastErrorMessage = "Block we are protecting is already occupied or reserved";
553                log.debug("will throw {}", lastErrorMessage);
554                throw new jmri.JmriException(lastErrorMessage);
555            }
556            if (!canLBlockBeUsed(destinationLayoutBlock)) {
557                lastErrorMessage = "Destination Block is already occupied or reserved";
558                log.debug("will throw {}", lastErrorMessage);
559                throw new jmri.JmriException(lastErrorMessage);
560            }
561        } else {
562            blocksInRoute.add(new BlocksTested(protectingLayoutBlock));
563        }
564        if (destinationLayoutBlock == protectingLayoutBlock) {
565            List<LayoutBlock> returnBlocks = new ArrayList<>();
566            blocksInRoute.forEach((blocksTested) -> {
567                returnBlocks.add(blocksTested.getBlock());
568            });
569            return returnBlocks;
570        }
571
572        BlocksTested bt = blocksInRoute.get(blocksInRoute.size() - 1);
573
574        int ttl = 1;
575        List<Integer> offSet = new ArrayList<>();
576        while (ttl < ttlSize) { // value should be higher but low for test!
577            log.debug("===== Ttl value = {} ======", ttl);
578            log.debug("Looking for next block");
579            int nextBlockIndex = findBestHop(currentBlock, nextBlock, destBlock, directionOfTravel, offSet, validateOnly, pathMethod);
580            if (nextBlockIndex != -1) {
581                bt.addIndex(nextBlockIndex);
582                if (log.isDebugEnabled()) {
583                    log.debug("block index returned {} Blocks in route size {}", nextBlockIndex, blocksInRoute.size());
584                }
585                // Sets the old next block to be our current block.
586                LayoutBlock currentLBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(nextBlock);
587                if (currentLBlock == null) {
588                    log.error("Unable to get block :{}: from instancemanager", nextBlock);
589                    continue;
590                }
591                offSet.clear();
592
593                directionOfTravel = currentLBlock.getRouteDirectionAtIndex(nextBlockIndex);
594
595                //allow for routes that contain more than one occurrence of a block in a route to allow change of direction.
596                Block prevBlock = currentBlock;
597                LayoutBlock prevLBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(prevBlock);
598                currentBlock = nextBlock;
599                nextBlock = currentLBlock.getRouteNextBlockAtIndex(nextBlockIndex);
600                LayoutBlock nextLBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(nextBlock);
601                if (log.isDebugEnabled()) {
602                    log.debug("Blocks in route size {}", blocksInRoute.size());
603                    log.debug("next: {} dest: {}", nextBlock.getDisplayName(), destBlock.getDisplayName());
604                }
605                if (nextBlock == currentBlock) {
606                    nextBlock = currentLBlock.getRouteDestBlockAtIndex(nextBlockIndex);
607                    log.debug("the next block to our destination we are looking for is directly connected to this one");
608                } else if (!((protectingLayoutBlock == prevLBlock)&&(protectingLayoutBlock == nextLBlock))) {
609                    if (nextLBlock != null) {
610                        log.debug("Add block {}", nextLBlock.getDisplayName());
611                    }
612                    bt = new BlocksTested(nextLBlock);
613                    blocksInRoute.add(bt);
614                }
615                if (nextBlock == destBlock) {
616                    if (!validateOnly && !checkForLevelCrossing(destinationLayoutBlock)) {
617                        throw new jmri.JmriException("Destination block is in conflict on a crossover");
618                    }
619                    List<LayoutBlock> returnBlocks = new ArrayList<>();
620                    blocksInRoute.forEach((blocksTested) -> {
621                        returnBlocks.add(blocksTested.getBlock());
622                    });
623                    returnBlocks.add(destinationLayoutBlock);
624                    if (log.isDebugEnabled()) {
625                        log.debug("Adding destination Block {}", destinationLayoutBlock.getDisplayName());
626                        log.debug("arrived at destination block");
627                        log.debug("{} Return as Long", sourceLayoutBlock.getDisplayName());
628                        returnBlocks.forEach((returnBlock) -> {
629                            log.debug("  return block {}", returnBlock.getDisplayName());
630                        });
631                        log.debug("Finished List");
632                    }
633                    return returnBlocks;
634                }
635            } else {
636                //-1 is returned when there are no more valid besthop valids found
637                // Block index is -1, so we need to go back a block and find another way.
638
639                // So we have gone back as far as our starting block so we better return.
640                int birSize = blocksInRoute.size();
641                log.debug("block in route size {}", birSize);
642                if (birSize <= 2) {
643                    log.debug("drop out here with ttl");
644                    ttl = ttlSize + 1;
645                } else {
646                    if (log.isDebugEnabled()) {
647                        for (int t = 0; t < blocksInRoute.size(); t++) {
648                            log.debug("index {} block {}", t, blocksInRoute.get(t).getBlock().getDisplayName());
649                        }
650                        log.debug("To remove last block {}", blocksInRoute.get(birSize - 1).getBlock().getDisplayName());
651                    }
652
653                    currentBlock = blocksInRoute.get(birSize - 3).getBlock().getBlock();
654                    nextBlock = blocksInRoute.get(birSize - 2).getBlock().getBlock();
655                    // take a copy, the list of tested indexes must not be cleared when we move on
656                    offSet = new ArrayList<>(blocksInRoute.get(birSize - 2).getTestedIndexes());
657                    bt = blocksInRoute.get(birSize - 2);
658                    blocksInRoute.remove(birSize - 1);
659                    ttl--;
660                }
661            }
662            ttl++;
663        }
664        if (ttl == ttlSize) {
665            lastErrorMessage = "ttlExpired";
666        }
667        // we exited the loop without either finding the destination or we had error.
668        throw new jmri.JmriException(lastErrorMessage);
669    }
670
671    static class BlocksTested {
672
673        LayoutBlock block;
674        List<Integer> indexNumber = new ArrayList<>();
675
676        BlocksTested(LayoutBlock block) {
677            this.block = block;
678        }
679
680        void addIndex(int x) {
681            indexNumber.add(x);
682        }
683
684        int getLastIndex() {
685            return indexNumber.get(indexNumber.size() - 1); // get the last one in the list
686        }
687
688        List<Integer> getTestedIndexes() {
689            return indexNumber;
690        }
691
692        LayoutBlock getBlock() {
693            return block;
694        }
695    }
696
697    private boolean canLBlockBeUsed(LayoutBlock lBlock) {
698        if (lBlock == null) {
699            return true;
700        }
701        if (lBlock.getUseExtraColor()) {
702            return false;
703        }
704        if (lBlock.getBlock().getPermissiveWorking()) {
705            return true;
706        }
707        return (lBlock.getState() != Block.OCCUPIED);
708    }
709
710    String lastErrorMessage = "Unknown Error Occured";
711
712    // We need to take into account if the returned block has a signalmast attached.
713    int findBestHop(final Block preBlock, final Block currentBlock, Block destBlock, int direction, List<Integer> offSet, boolean validateOnly, Routing pathMethod) {
714        int result = 0;
715
716        LayoutBlock currentLBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(currentBlock);
717        if (currentLBlock == null) {
718            return -1;
719        }
720        List<Integer> blkIndexTested = new ArrayList<>(5);
721        if (log.isDebugEnabled()) {
722            log.debug("In find best hop current {} previous {}", currentLBlock.getDisplayName(), preBlock.getDisplayName());
723        }
724        Block block;
725        while (result != -1) {
726            if (currentBlock == preBlock) {
727                // Basically looking for the connected block, which there should only be one of!
728                log.debug("At get ConnectedBlockRoute");
729                result = currentLBlock.getConnectedBlockRouteIndex(destBlock, direction);
730                log.trace("getConnectedBlockRouteIndex returns result {} with destBlock {}, direction {}", result, destBlock, direction);
731            } else {
732                if (log.isDebugEnabled()) {
733                    log.debug("Off Set {}", offSet);
734                }
735                result = currentLBlock.getNextBestBlock(preBlock, destBlock, offSet, Metric.METRIC);
736                log.trace("getNextBestBlock returns result {} with preBlock {}, destBlock {}", result, preBlock, destBlock);
737            }
738            if (result != -1) {
739                block = currentLBlock.getRouteNextBlockAtIndex(result);
740                LayoutBlock lBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(block);
741
742                Block blocktoCheck = block;
743                if (block == currentBlock) {
744                    log.debug("current block matches returned block therefore the next block is directly connected");
745                    blocktoCheck = destBlock;
746                }
747
748                if ((block == currentBlock) && (currentLBlock.getThroughPathIndex(preBlock, destBlock) == -1)) {
749                    lastErrorMessage = "block " + block.getDisplayName() + " is directly attached, however the route to the destination block " + destBlock.getDisplayName() + " can not be directly used";
750                    log.debug("continue after {}", lastErrorMessage);
751                } else if ((validateOnly) || ((checkForDoubleCrossover(preBlock, currentLBlock, blocktoCheck) && checkForLevelCrossing(currentLBlock)) && canLBlockBeUsed(lBlock))) {
752                    if (log.isDebugEnabled()) {
753                        log.debug("{} not occupied & not reserved but we need to check if the anchor point between the two contains a signal or not", block.getDisplayName());
754                        log.debug("  current {} {}", currentBlock.getDisplayName(), block.getDisplayName());
755                    }
756
757                    jmri.NamedBean foundBean = null;
758                    /* We change the logging level to fatal in the layout block manager as we are testing to make sure that no signalhead/mast exists
759                     this would generate an error message that is expected.*/
760                    MDC.put("loggingDisabled", LayoutBlockManager.class.getName());
761                    log.trace(" current pathMethod is {}", pathMethod, new Exception("traceback"));
762                    switch (pathMethod) {
763                        case MASTTOMAST:
764                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingSignalMast(currentBlock, blocktoCheck);
765                            break;
766                        case HEADTOHEAD:
767                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingSignalHead(currentBlock, blocktoCheck);
768                            break;
769                        case SENSORTOSENSOR:
770                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingSensor(currentBlock, blocktoCheck, null);
771                            break;
772                        case NONE:
773                            break;
774                        default:
775                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingNamedBean(currentBlock, blocktoCheck, null);
776                            break;
777                    }
778                    MDC.remove("loggingDisabled");
779                    if (foundBean == null) {
780                        log.debug("No object found so okay to return");
781                        return result;
782                    } else {
783                        lastErrorMessage = "Signal " + foundBean.getDisplayName() + " already exists between blocks " + currentBlock.getDisplayName() + " and " + blocktoCheck.getDisplayName() + " in the same direction on this path";
784                        log.debug("continue after {}", lastErrorMessage);
785                    }
786                } else {
787                    lastErrorMessage = "block " + block.getDisplayName() + " found not to be not usable";
788                    log.debug("continue after {}", lastErrorMessage);
789                }
790                if (blkIndexTested.contains(result)) {
791                    lastErrorMessage = ("No valid free path found");
792                    return -1;
793                }
794                blkIndexTested.add(result);
795                offSet.add(result);
796            } else {
797                log.debug("At this point the getNextBextBlock() has returned a -1");
798            }
799        }
800        return -1;
801    }
802
803    private boolean checkForDoubleCrossover(Block prevBlock, LayoutBlock curBlock, Block nextBlock) {
804        LayoutEditor le = curBlock.getMaxConnectedPanel();
805        ConnectivityUtil ct = le.getConnectivityUtil();
806        List<LayoutTrackExpectedState<LayoutTurnout>> turnoutList = ct.getTurnoutList(curBlock.getBlock(), prevBlock, nextBlock);
807        for (LayoutTrackExpectedState<LayoutTurnout> layoutTurnoutLayoutTrackExpectedState : turnoutList) {
808            LayoutTurnout lt = layoutTurnoutLayoutTrackExpectedState.getObject();
809            if (lt.getTurnoutType() == LayoutTurnout.TurnoutType.DOUBLE_XOVER) {
810                if (layoutTurnoutLayoutTrackExpectedState.getExpectedState() == jmri.Turnout.THROWN) {
811                    jmri.Turnout t = lt.getTurnout();
812                    if (t.getKnownState() == jmri.Turnout.THROWN) {
813                        if (lt.getLayoutBlock() == curBlock || lt.getLayoutBlockC() == curBlock) {
814                            if (!canLBlockBeUsed(lt.getLayoutBlockB()) && !canLBlockBeUsed(lt.getLayoutBlockD())) {
815                                return false;
816                            }
817                        } else if (lt.getLayoutBlockB() == curBlock || lt.getLayoutBlockD() == curBlock) {
818                            if (!canLBlockBeUsed(lt.getLayoutBlock()) && !canLBlockBeUsed(lt.getLayoutBlockC())) {
819                                return false;
820                            }
821                        }
822                    }
823                }
824            }
825        }
826        return true;
827    }
828
829    private boolean checkForLevelCrossing(LayoutBlock curBlock) {
830        LayoutEditor lay = curBlock.getMaxConnectedPanel();
831        for (LevelXing lx : lay.getLevelXings()) {
832            if (lx.getLayoutBlockAC() == curBlock
833                    || lx.getLayoutBlockBD() == curBlock) {
834                if ((lx.getLayoutBlockAC() != null)
835                        && (lx.getLayoutBlockBD() != null)
836                        && (lx.getLayoutBlockAC() != lx.getLayoutBlockBD())) {
837                    if (lx.getLayoutBlockAC() == curBlock) {
838                        if (!canLBlockBeUsed(lx.getLayoutBlockBD())) {
839                            return false;
840                        }
841                    } else if (lx.getLayoutBlockBD() == curBlock) {
842                        if (!canLBlockBeUsed(lx.getLayoutBlockAC())) {
843                            return false;
844                        }
845                    }
846                }
847            }
848        }
849        return true;
850    }
851
852    /**
853     * Discovers valid pairs of beans type T assigned to a layout editor. If no
854     * bean type is provided, then either SignalMasts or Sensors are discovered
855     * If no editor is provided, then all editors are considered
856     *
857     * @param editor     the layout editor panel
858     * @param T          the type
859     * @param pathMethod Determine whether or not we should reject pairs if
860     *                   there are other beans in the way. Constant values of
861     *                   NONE, ANY, MASTTOMAST, HEADTOHEAD
862     * @return the valid pairs
863     */
864    public HashMap<NamedBean, List<NamedBean>> discoverValidBeanPairs(LayoutEditor editor, Class<?> T, Routing pathMethod) {
865        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
866
867        // ----- Begin Turntable Path Discovery -----
868        HashMap<NamedBean, List<NamedBean>> retPairs = new HashMap<>();
869        List<SignalMast> turntableMasts = new ArrayList<>();
870        if (T == SignalMast.class) {
871            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
872                for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
873                    if (!turntable.isDispatcherManaged()) {
874                        continue;
875                    }
876                    LayoutBlock turntableBlock = turntable.getLayoutBlock();
877
878                    // Add all turntable-related masts to a list for exclusion from general discovery
879                    if (turntable.getExitSignalMast() != null) turntableMasts.add(turntable.getExitSignalMast());
880                    if (turntable.getBufferMast() != null) turntableMasts.add(turntable.getBufferMast());
881
882                    // Path 1: From turntable's Exit Mast to the next mast on a ray's track
883                    SignalMast exitMast = turntable.getExitSignalMast();
884                    if (exitMast != null) {
885                        for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
886                            TrackSegment track = ray.getConnect();
887                            if (track != null && track.getLayoutBlock() != null) {
888                                LayoutBlock rayBlock = track.getLayoutBlock();
889                                // Find the block connected to the ray that is NOT the turntable, then find the mast protecting it.
890                                for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
891                                    Block neighbor = rayBlock.getNeighbourAtIndex(i);
892                                    if (neighbor != turntableBlock.getBlock()) {
893                                        SignalMast nextMast = lbm.getFacingSignalMast(rayBlock.getBlock(), neighbor, panel);
894                                        if (nextMast != null) {
895                                            retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(nextMast);
896                                        }
897                                        break; // Assume only one exit from the ray block
898                                    }
899                                }
900                                // Also check for a buffer mast at the end of this ray's block
901                                if (rayBlock.getNumberOfNeighbours() == 1) { // Only connected to the turntable block
902                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(rayBlock.getBlock(), panel);
903                                    if (bufferMast != null) {
904                                        if (log.isDebugEnabled()) {
905                                            log.debug("Found turntable exit to buffer mast path: {} -> {}", exitMast.getDisplayName(), bufferMast.getDisplayName());
906                                        }
907                                        retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(bufferMast);
908                                    }
909                                }
910                            }
911                        }
912                    }
913
914
915                    // Path 2 & 3: Paths involving Approach and Buffer masts
916                    for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
917                        SignalMast approachMast = ray.getApproachMast();
918                        if (approachMast == null) { // this is logged elsewhere
919                            continue;
920                        }
921                        turntableMasts.add(approachMast);
922
923                        // Path 2: From a remote mast on the layout to this ray's Approach Mast
924                        TrackSegment track = ray.getConnect();
925                        if (track != null && track.getLayoutBlock() != null && turntableBlock != null) {
926                            LayoutBlock rayBlock = track.getLayoutBlock();
927                            // Find the block connected to the ray that is NOT the turntable, then find the mast protecting the ray from it.
928                            for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
929                                Block neighbor = rayBlock.getNeighbourAtIndex(i);
930                                if (neighbor != turntableBlock.getBlock()) {
931                                    SignalMast remoteMast = lbm.getFacingSignalMast(neighbor, rayBlock.getBlock(), panel);
932                                    if (remoteMast != null) {
933                                        retPairs.computeIfAbsent(remoteMast, k -> new ArrayList<>()).add(approachMast);
934                                    }
935                                    // Assume only one entry to the ray block
936                                    break;
937                                }
938                            }
939                        }
940
941                        // Path 3: From this ray's Approach Mast to the turntable's Buffer Mast (a siding path)
942                        SignalMast bufferMast = turntable.getBufferMast();
943                        if (bufferMast != null) {
944                            retPairs.computeIfAbsent(approachMast, k -> new ArrayList<>()).add(bufferMast);
945                        }
946                    }
947                }
948            }
949        }
950        // ----- Begin Traverser Path Discovery -----
951//        HashMap<NamedBean, List<NamedBean>> retPairs = new HashMap<>();
952        List<SignalMast> traverserMasts = new ArrayList<>();
953        if (T == SignalMast.class) {
954            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
955                for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
956                    if (!traverser.isDispatcherManaged()) {
957                        continue;
958                    }
959                    LayoutBlock traverserBlock = traverser.getLayoutBlock();
960
961                    // Add all traverser-related masts to a list for exclusion from general discovery
962                    if (traverser.getExitSignalMast() != null) traverserMasts.add(traverser.getExitSignalMast());
963                    if (traverser.getBufferMast() != null) traverserMasts.add(traverser.getBufferMast());
964
965                    // Path 1: From traverser's Exit Mast to the next mast on a ray's track
966                    SignalMast exitMast = traverser.getExitSignalMast();
967                    if (exitMast != null) {
968                        for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
969                            TrackSegment track = slot.getConnect();
970                            if (track != null && track.getLayoutBlock() != null) {
971                                LayoutBlock slotBlock = track.getLayoutBlock();
972                                // Find the block connected to the slot that is NOT the traverser, then find the mast protecting it.
973                                for (int i = 0; i < slotBlock.getNumberOfNeighbours(); i++) {
974                                    Block neighbor = slotBlock.getNeighbourAtIndex(i);
975                                    if (neighbor != traverserBlock.getBlock()) {
976                                        SignalMast nextMast = lbm.getFacingSignalMast(slotBlock.getBlock(), neighbor, panel);
977                                        if (nextMast != null) {
978                                            retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(nextMast);
979                                        }
980                                        break; // Assume only one exit from the slot block
981                                    }
982                                }
983                                // Also check for a buffer mast at the end of this slot's block
984                                if (slotBlock.getNumberOfNeighbours() == 1) { // Only connected to the traverser block
985                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(slotBlock.getBlock(), panel);
986                                    if (bufferMast != null) {
987                                        if (log.isDebugEnabled()) {
988                                            log.debug("Found traverser exit to buffer mast path: {} -> {}", exitMast.getDisplayName(), bufferMast.getDisplayName());
989                                        }
990                                        retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(bufferMast);
991                                    }
992                                }
993                            }
994                        }
995                    }
996
997
998                    // Path 2 & 3: Paths involving Approach and Buffer masts
999                    for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
1000                        SignalMast approachMast = slot.getApproachMast();
1001                        if (approachMast == null) { // this is logged elsewhere
1002                            continue;
1003                        }
1004                        traverserMasts.add(approachMast);
1005
1006                        // Path 2: From a remote mast on the layout to this slot's Approach Mast
1007                        TrackSegment track = slot.getConnect();
1008                        if (track != null && track.getLayoutBlock() != null && traverserBlock != null) {
1009                            LayoutBlock slotBlock = track.getLayoutBlock();
1010                            // Find the block connected to the slot that is NOT the traverser, then find the mast protecting the slot from it.
1011                            for (int i = 0; i < slotBlock.getNumberOfNeighbours(); i++) {
1012                                Block neighbor = slotBlock.getNeighbourAtIndex(i);
1013                                if (neighbor != traverserBlock.getBlock()) {
1014                                    SignalMast remoteMast = lbm.getFacingSignalMast(neighbor, slotBlock.getBlock(), panel);
1015                                    if (remoteMast != null) {
1016                                        retPairs.computeIfAbsent(remoteMast, k -> new ArrayList<>()).add(approachMast);
1017                                    }
1018                                    // Assume only one entry to the slot block
1019                                    break;
1020                                }
1021                            }
1022                        }
1023
1024                        // Path 3: From this ray's Approach Mast to the traverser's Buffer Mast (a siding path)
1025                        SignalMast bufferMast = traverser.getBufferMast();
1026                        if (bufferMast != null) {
1027                            retPairs.computeIfAbsent(approachMast, k -> new ArrayList<>()).add(bufferMast);
1028                        }
1029                    }
1030                }
1031            }
1032        }
1033        // ----- End Turntable/Traverser Path Discovery -----
1034
1035        // ----- Begin General Path Discovery (excluding turntable masts) -----
1036        List<FacingProtecting> beanList = generateBlocksWithBeans(editor, T);
1037        beanList.forEach((fp) -> {
1038            // Skip any mast that has already been handled by the turntable-specific logic above
1039            if (turntableMasts.contains(fp.getBean())) {
1040                return; // continue to next fp in forEach
1041            }
1042            // Skip any mast that has already been handled by the traverser-specific logic above
1043            if (traverserMasts.contains(fp.getBean())) {
1044                return; // continue to next fp in forEach
1045            }
1046            fp.getProtectingBlocks().stream().map((block) -> {
1047                if (log.isDebugEnabled()) {
1048                    try {
1049                        log.debug("\nSource {}", fp.getBean().getDisplayName());
1050                        log.debug("facing {}", fp.getFacing().getDisplayName());
1051                        log.debug("protecting {}", block.getDisplayName());
1052                    } catch (java.lang.NullPointerException e) {
1053                        // Can be considered normal if the signalmast is assigned to an end bumper.
1054                    }
1055                }
1056                return block;
1057            }).forEachOrdered((block) -> {
1058                LayoutBlock lFacing = lbm.getLayoutBlock(fp.getFacing());
1059                LayoutBlock lProtecting = lbm.getLayoutBlock(block);
1060                NamedBean source = fp.getBean();
1061                try {
1062                    retPairs.computeIfAbsent(source, k -> new ArrayList<>()).addAll(discoverPairDest(source, lProtecting, lFacing, beanList, pathMethod));
1063                } catch (JmriException ex) {
1064                    log.error("exception in retPairs.put", ex);
1065                }
1066            });
1067        });
1068        return retPairs;
1069    }
1070
1071    /**
1072     * Returns a list of valid destination beans reachable from a given source
1073     * bean.
1074     *
1075     * @param source     Either a SignalMast or Sensor
1076     * @param editor     The layout editor that the source is located on, if
1077     *                   null, then all editors are considered
1078     * @param T          The class of the remote destination, if null, then both
1079     *                   SignalMasts and Sensors are considered
1080     * @param pathMethod Determine whether or not we should reject pairs if
1081     *                   there are other beans in the way. Constant values of
1082     *                   NONE, ANY, MASTTOMAST, HEADTOHEAD
1083     * @return A list of all reachable NamedBeans
1084     * @throws jmri.JmriException occurring during nested readAll operation
1085     */
1086    public List<NamedBean> discoverPairDest(NamedBean source, LayoutEditor editor, Class<?> T, Routing pathMethod) throws JmriException {
1087        if (log.isDebugEnabled()) {
1088            log.debug("discover pairs from source {}", source.getDisplayName());
1089        }
1090
1091        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
1092
1093        // First, check if the source is a turntable mast. If so, handle it specially.
1094        if (T == SignalMast.class) {
1095            if (! (source instanceof SignalMast)) {
1096                throw new IllegalArgumentException("source is not a SignalMast: " + (source != null ? source.getClass().getName() : "null"));
1097            }
1098            SignalMast sourceMast = (SignalMast) source;
1099            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
1100                for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
1101                    if (!turntable.isDispatcherManaged()) {
1102                        continue;
1103                    }
1104
1105                    // Case 1: Source is the turntable's Exit Mast
1106                    if (sourceMast.equals(turntable.getExitSignalMast())) {
1107                        log.debug("Source is an exit mast for turntable {}", turntable.getName());
1108                        List<NamedBean> destinations = new ArrayList<>();
1109                        LayoutBlock turntableBlock = turntable.getLayoutBlock();
1110                        for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
1111                            TrackSegment track = ray.getConnect();
1112                            if (track != null && track.getLayoutBlock() != null) {
1113                                LayoutBlock rayBlock = track.getLayoutBlock();
1114                                for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
1115                                    Block neighbor = rayBlock.getNeighbourAtIndex(i);
1116                                    if (neighbor != turntableBlock.getBlock()) {
1117                                        SignalMast nextMast = lbm.getFacingSignalMast(rayBlock.getBlock(), neighbor, panel);
1118                                        if (nextMast != null) destinations.add(nextMast);
1119                                    }
1120                                }
1121                                if (rayBlock.getNumberOfNeighbours() == 1) { // End of line buffer
1122                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(rayBlock.getBlock(), panel);
1123                                    if (bufferMast != null) destinations.add(bufferMast);
1124                                }
1125                            }
1126                        }
1127                        return destinations;
1128                    }
1129
1130                    // Case 2: Source is an Approach Mast for one of the rays
1131                    for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
1132                        if (sourceMast.equals(ray.getApproachMast())) {
1133                            log.debug("Source is an approach mast for turntable {}", turntable.getName());
1134                            List<NamedBean> destinations = new ArrayList<>();
1135                            if (turntable.getBufferMast() != null) destinations.add(turntable.getBufferMast());
1136                            return destinations;
1137                        }
1138                    }
1139                }
1140            }
1141        }
1142        // First, check if the source is a traverser mast. If so, handle it specially.
1143        if (T == SignalMast.class) {
1144            if (! (source instanceof SignalMast)) {
1145                throw new IllegalArgumentException("source is not a SignalMast: " + (source != null ? source.getClass().getName() : "null"));
1146            }
1147            SignalMast sourceMast = (SignalMast) source;
1148            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
1149                for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
1150                    if (!traverser.isDispatcherManaged()) continue;
1151
1152                    // Case 1: Source is the traverser's Exit Mast
1153                    if (sourceMast.equals(traverser.getExitSignalMast())) {
1154                        List<NamedBean> destinations = new ArrayList<>();
1155                        LayoutBlock traverserBlock = traverser.getLayoutBlock();
1156                        for (int i = 0; i < traverser.getNumberSlots(); i++) {
1157                            TrackSegment track = traverser.getSlotConnectOrdered(i);
1158                            if (track != null && track.getLayoutBlock() != null) {
1159                                LayoutBlock slotBlock = track.getLayoutBlock();
1160                                for (int j = 0; j < slotBlock.getNumberOfNeighbours(); j++) {
1161                                    Block neighbor = slotBlock.getNeighbourAtIndex(j);
1162                                    if (neighbor != traverserBlock.getBlock()) {
1163                                        SignalMast nextMast = lbm.getFacingSignalMast(slotBlock.getBlock(), neighbor, panel);
1164                                        if (nextMast != null) destinations.add(nextMast);
1165                                    }
1166                                }
1167                                if (slotBlock.getNumberOfNeighbours() == 1) { // End of line buffer
1168                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(slotBlock.getBlock(), panel);
1169                                    if (bufferMast != null) destinations.add(bufferMast);
1170                                }
1171                            }
1172                        }
1173                        return destinations;
1174                    }
1175
1176                    // Case 2: Source is an Approach Mast for one of the slots
1177                    for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
1178                        if (sourceMast.equals(slot.getApproachMast())) {
1179                            log.debug("Source is an approach mast for traverser {}", traverser.getName());
1180                            List<NamedBean> destinations = new ArrayList<>();
1181                            if (traverser.getBufferMast() != null) destinations.add(traverser.getBufferMast());
1182                            return destinations;
1183                        }
1184                    }
1185                }
1186            }
1187        }
1188
1189        LayoutBlock lFacing = lbm.getFacingBlockByNamedBean(source, editor);
1190        List<LayoutBlock> lProtecting = lbm.getProtectingBlocksByNamedBean(source, editor);
1191        List<NamedBean> ret = new ArrayList<>();
1192        List<FacingProtecting> beanList = generateBlocksWithBeans(editor, T);
1193
1194        // may throw JmriException here
1195        for (LayoutBlock lb : lProtecting) {
1196            ret.addAll(discoverPairDest(source, lb, lFacing, beanList, pathMethod));
1197        }
1198        return ret;
1199    }
1200
1201    List<NamedBean> discoverPairDest(NamedBean source, LayoutBlock lProtecting, LayoutBlock lFacing, List<FacingProtecting> blockList, Routing pathMethod) throws JmriException {
1202        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
1203        if (!lbm.isAdvancedRoutingEnabled()) {
1204            throw new JmriException("advanced routing not enabled");
1205        }
1206        if (!lbm.routingStablised()) {
1207            throw new JmriException("routing not stabilised");
1208        }
1209        List<NamedBean> validDestBean = new ArrayList<>();
1210        for (FacingProtecting facingProtecting : blockList) {
1211            if (facingProtecting.getBean() != source) {
1212                NamedBean destObj = facingProtecting.getBean();
1213                if (log.isDebugEnabled()) {
1214                    log.debug("looking for pair {} {}", source.getDisplayName(), destObj.getDisplayName());
1215                }
1216                try {
1217                    if (checkValidDest(lFacing, lProtecting, facingProtecting, pathMethod)) {
1218                        if (log.isDebugEnabled()) {
1219                            log.debug("Valid pair {} {}", source.getDisplayName(), destObj.getDisplayName());
1220                        }
1221                        LayoutBlock ldstBlock = lbm.getLayoutBlock(facingProtecting.getFacing());
1222                        try {
1223                            List<LayoutBlock> lblks = getLayoutBlocks(lFacing, ldstBlock, lProtecting, true, pathMethod);
1224                            if (log.isDebugEnabled()) {
1225                                log.debug("Adding block {} to paths, current size {}", destObj.getDisplayName(), lblks.size());
1226                            }
1227                            validDestBean.add(destObj);
1228                        } catch (JmriException e) {  // Considered normal if route not found.
1229                            log.debug("not a valid route through {} - {}", source.getDisplayName(), destObj.getDisplayName());
1230                        }
1231                    }
1232                } catch (JmriException ex) {
1233                    log.debug("caught exception in discoverPairsDest", ex);
1234                }
1235            }
1236        }
1237        return validDestBean;
1238    }
1239
1240    List<FacingProtecting> generateBlocksWithBeans(LayoutEditor editor, Class<?> T) {
1241        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
1242        List<FacingProtecting> beanList = new ArrayList<>();
1243
1244        for (LayoutBlock curLblk : lbm.getNamedBeanSet()) {
1245            Block curBlk = curLblk.getBlock();
1246            LayoutEditor useEdit = editor;
1247            if (editor == null) {
1248                useEdit = curLblk.getMaxConnectedPanel();
1249            }
1250            if (curBlk != null) {
1251                int noNeigh = curLblk.getNumberOfNeighbours();
1252                for (int x = 0; x < noNeigh; x++) {
1253                    Block blk = curLblk.getNeighbourAtIndex(x);
1254                    List<Block> proBlk = new ArrayList<>();
1255                    NamedBean bean = null;
1256                    if (T == null) {
1257                        proBlk.add(blk);
1258                        bean = lbm.getFacingNamedBean(curBlk, blk, useEdit);
1259                    } else if (T.equals(SignalMast.class)) {
1260                        bean = lbm.getFacingSignalMast(curBlk, blk, useEdit);
1261                        if (bean != null) {
1262                            if (log.isDebugEnabled()) {
1263                                log.debug("Get list of protecting blocks for {} facing {}", bean.getDisplayName(), curBlk.getDisplayName());
1264                            }
1265                            List<LayoutBlock> lProBlk = lbm.getProtectingBlocksByNamedBean(bean, useEdit);
1266                            for (LayoutBlock lb : lProBlk) {
1267                                if (lb != null) {
1268                                    proBlk.add(lb.getBlock());
1269                                }
1270                            }
1271                        }
1272                    } else if (T.equals(Sensor.class)) {
1273                        bean = lbm.getFacingSensor(curBlk, blk, useEdit);
1274                        if (bean != null) {
1275                            if (log.isDebugEnabled()) {
1276                                log.debug("Get list of protecting blocks for {}", bean.getDisplayName());
1277                            }
1278                            List<LayoutBlock> lProBlk = lbm.getProtectingBlocksByNamedBean(bean, useEdit);
1279                            for (LayoutBlock lb : lProBlk) {
1280                                if (lb != null) {
1281                                    proBlk.add(lb.getBlock());
1282                                }
1283                            }
1284                        }
1285                    } else {
1286                        log.error("Past bean type is unknown {}", T);
1287                    }
1288                    if (bean != null) {
1289                        FacingProtecting toadd = new FacingProtecting(curBlk, proBlk, bean);
1290                        boolean found = false;
1291                        for (FacingProtecting fp : beanList) {
1292                            if (fp.equals(toadd)) {
1293                                found = true;
1294                                break;
1295                            }
1296                        }
1297                        if (!found) {
1298                            beanList.add(toadd);
1299                        }
1300                    }
1301                }
1302                if (noNeigh == 1) {
1303                    NamedBean bean = null;
1304                    if (log.isDebugEnabled()) {
1305                        log.debug("We have a dead end {}", curBlk.getDisplayName());
1306                    }
1307                    if (T == null) {
1308                        bean = lbm.getNamedBeanAtEndBumper(curBlk, useEdit);
1309                    } else if (T.equals(SignalMast.class)) {
1310                        bean = lbm.getSignalMastAtEndBumper(curBlk, useEdit);
1311                    } else if (T.equals(Sensor.class)) {
1312                        bean = lbm.getSensorAtEndBumper(curBlk, useEdit);
1313                    } else {
1314                        log.error("Past bean type is unknown {}", T);
1315                    }
1316                    if (bean != null) {
1317                        FacingProtecting toadd = new FacingProtecting(curBlk, null, bean);
1318                        boolean found = false;
1319                        for (FacingProtecting fp : beanList) {
1320                            if (fp.equals(toadd)) {
1321                                found = true;
1322                                break;
1323                            }
1324                        }
1325                        if (!found) {
1326                            beanList.add(toadd);
1327                        }
1328                    }
1329                }
1330            }
1331        }
1332        return beanList;
1333    }
1334
1335    static class FacingProtecting {
1336
1337        Block facing;
1338        List<Block> protectingBlocks;
1339        NamedBean bean;
1340
1341        FacingProtecting(Block facing, List<Block> protecting, NamedBean bean) {
1342            this.facing = facing;
1343            if (protecting == null) {
1344                this.protectingBlocks = new ArrayList<>(0);
1345            } else {
1346                this.protectingBlocks = protecting;
1347            }
1348            this.bean = bean;
1349        }
1350
1351        Block getFacing() {
1352            return facing;
1353        }
1354
1355        List<Block> getProtectingBlocks() {
1356            return protectingBlocks;
1357        }
1358
1359        NamedBean getBean() {
1360            return bean;
1361        }
1362
1363        @Override
1364        public boolean equals(Object obj) {
1365
1366            if (obj == this) {
1367                return true;
1368            }
1369            if (obj == null) {
1370                return false;
1371            }
1372
1373            if (!(getClass() == obj.getClass())) {
1374                return false;
1375            } else {
1376                FacingProtecting tmp = (FacingProtecting) obj;
1377                if (tmp.getBean() != this.bean) {
1378                    return false;
1379                }
1380                if (tmp.getFacing() != this.facing) {
1381                    return false;
1382                }
1383                if (!tmp.getProtectingBlocks().equals(this.protectingBlocks)) {
1384                    return false;
1385                }
1386            }
1387            return true;
1388        }
1389
1390        @Override
1391        public int hashCode() {
1392            int hash = 7;
1393            hash = 37 * hash + (this.bean != null ? this.bean.hashCode() : 0);
1394            hash = 37 * hash + (this.facing != null ? this.facing.hashCode() : 0);
1395            hash = 37 * hash + (this.protectingBlocks != null ? this.protectingBlocks.hashCode() : 0);
1396            return hash;
1397        }
1398    }
1399
1400    private static final Logger log
1401            = LoggerFactory.getLogger(LayoutBlockConnectivityTools.class);
1402}