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                    offSet = blocksInRoute.get(birSize - 2).getTestedIndexes();
656                    bt = blocksInRoute.get(birSize - 2);
657                    blocksInRoute.remove(birSize - 1);
658                    ttl--;
659                }
660            }
661            ttl++;
662        }
663        if (ttl == ttlSize) {
664            lastErrorMessage = "ttlExpired";
665        }
666        // we exited the loop without either finding the destination or we had error.
667        throw new jmri.JmriException(lastErrorMessage);
668    }
669
670    static class BlocksTested {
671
672        LayoutBlock block;
673        List<Integer> indexNumber = new ArrayList<>();
674
675        BlocksTested(LayoutBlock block) {
676            this.block = block;
677        }
678
679        void addIndex(int x) {
680            indexNumber.add(x);
681        }
682
683        int getLastIndex() {
684            return indexNumber.get(indexNumber.size() - 1); // get the last one in the list
685        }
686
687        List<Integer> getTestedIndexes() {
688            return indexNumber;
689        }
690
691        LayoutBlock getBlock() {
692            return block;
693        }
694    }
695
696    private boolean canLBlockBeUsed(LayoutBlock lBlock) {
697        if (lBlock == null) {
698            return true;
699        }
700        if (lBlock.getUseExtraColor()) {
701            return false;
702        }
703        if (lBlock.getBlock().getPermissiveWorking()) {
704            return true;
705        }
706        return (lBlock.getState() != Block.OCCUPIED);
707    }
708
709    String lastErrorMessage = "Unknown Error Occured";
710
711    // We need to take into account if the returned block has a signalmast attached.
712    int findBestHop(final Block preBlock, final Block currentBlock, Block destBlock, int direction, List<Integer> offSet, boolean validateOnly, Routing pathMethod) {
713        int result = 0;
714
715        LayoutBlock currentLBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(currentBlock);
716        if (currentLBlock == null) {
717            return -1;
718        }
719        List<Integer> blkIndexTested = new ArrayList<>(5);
720        if (log.isDebugEnabled()) {
721            log.debug("In find best hop current {} previous {}", currentLBlock.getDisplayName(), preBlock.getDisplayName());
722        }
723        Block block;
724        while (result != -1) {
725            if (currentBlock == preBlock) {
726                // Basically looking for the connected block, which there should only be one of!
727                log.debug("At get ConnectedBlockRoute");
728                result = currentLBlock.getConnectedBlockRouteIndex(destBlock, direction);
729                log.trace("getConnectedBlockRouteIndex returns result {} with destBlock {}, direction {}", result, destBlock, direction);
730            } else {
731                if (log.isDebugEnabled()) {
732                    log.debug("Off Set {}", offSet);
733                }
734                result = currentLBlock.getNextBestBlock(preBlock, destBlock, offSet, Metric.METRIC);
735                log.trace("getNextBestBlock returns result {} with preBlock {}, destBlock {}", result, preBlock, destBlock);
736            }
737            if (result != -1) {
738                block = currentLBlock.getRouteNextBlockAtIndex(result);
739                LayoutBlock lBlock = InstanceManager.getDefault(LayoutBlockManager.class).getLayoutBlock(block);
740
741                Block blocktoCheck = block;
742                if (block == currentBlock) {
743                    log.debug("current block matches returned block therefore the next block is directly connected");
744                    blocktoCheck = destBlock;
745                }
746
747                if ((block == currentBlock) && (currentLBlock.getThroughPathIndex(preBlock, destBlock) == -1)) {
748                    lastErrorMessage = "block " + block.getDisplayName() + " is directly attached, however the route to the destination block " + destBlock.getDisplayName() + " can not be directly used";
749                    log.debug("continue after {}", lastErrorMessage);
750                } else if ((validateOnly) || ((checkForDoubleCrossover(preBlock, currentLBlock, blocktoCheck) && checkForLevelCrossing(currentLBlock)) && canLBlockBeUsed(lBlock))) {
751                    if (log.isDebugEnabled()) {
752                        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());
753                        log.debug("  current {} {}", currentBlock.getDisplayName(), block.getDisplayName());
754                    }
755
756                    jmri.NamedBean foundBean = null;
757                    /* We change the logging level to fatal in the layout block manager as we are testing to make sure that no signalhead/mast exists
758                     this would generate an error message that is expected.*/
759                    MDC.put("loggingDisabled", LayoutBlockManager.class.getName());
760                    log.trace(" current pathMethod is {}", pathMethod, new Exception("traceback"));
761                    switch (pathMethod) {
762                        case MASTTOMAST:
763                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingSignalMast(currentBlock, blocktoCheck);
764                            break;
765                        case HEADTOHEAD:
766                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingSignalHead(currentBlock, blocktoCheck);
767                            break;
768                        case SENSORTOSENSOR:
769                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingSensor(currentBlock, blocktoCheck, null);
770                            break;
771                        case NONE:
772                            break;
773                        default:
774                            foundBean = InstanceManager.getDefault(LayoutBlockManager.class).getFacingNamedBean(currentBlock, blocktoCheck, null);
775                            break;
776                    }
777                    MDC.remove("loggingDisabled");
778                    if (foundBean == null) {
779                        log.debug("No object found so okay to return");
780                        return result;
781                    } else {
782                        lastErrorMessage = "Signal " + foundBean.getDisplayName() + " already exists between blocks " + currentBlock.getDisplayName() + " and " + blocktoCheck.getDisplayName() + " in the same direction on this path";
783                        log.debug("continue after {}", lastErrorMessage);
784                    }
785                } else {
786                    lastErrorMessage = "block " + block.getDisplayName() + " found not to be not usable";
787                    log.debug("continue after {}", lastErrorMessage);
788                }
789                if (blkIndexTested.contains(result)) {
790                    lastErrorMessage = ("No valid free path found");
791                    return -1;
792                }
793                blkIndexTested.add(result);
794                offSet.add(result);
795            } else {
796                log.debug("At this point the getNextBextBlock() has returned a -1");
797            }
798        }
799        return -1;
800    }
801
802    private boolean checkForDoubleCrossover(Block prevBlock, LayoutBlock curBlock, Block nextBlock) {
803        LayoutEditor le = curBlock.getMaxConnectedPanel();
804        ConnectivityUtil ct = le.getConnectivityUtil();
805        List<LayoutTrackExpectedState<LayoutTurnout>> turnoutList = ct.getTurnoutList(curBlock.getBlock(), prevBlock, nextBlock);
806        for (LayoutTrackExpectedState<LayoutTurnout> layoutTurnoutLayoutTrackExpectedState : turnoutList) {
807            LayoutTurnout lt = layoutTurnoutLayoutTrackExpectedState.getObject();
808            if (lt.getTurnoutType() == LayoutTurnout.TurnoutType.DOUBLE_XOVER) {
809                if (layoutTurnoutLayoutTrackExpectedState.getExpectedState() == jmri.Turnout.THROWN) {
810                    jmri.Turnout t = lt.getTurnout();
811                    if (t.getKnownState() == jmri.Turnout.THROWN) {
812                        if (lt.getLayoutBlock() == curBlock || lt.getLayoutBlockC() == curBlock) {
813                            if (!canLBlockBeUsed(lt.getLayoutBlockB()) && !canLBlockBeUsed(lt.getLayoutBlockD())) {
814                                return false;
815                            }
816                        } else if (lt.getLayoutBlockB() == curBlock || lt.getLayoutBlockD() == curBlock) {
817                            if (!canLBlockBeUsed(lt.getLayoutBlock()) && !canLBlockBeUsed(lt.getLayoutBlockC())) {
818                                return false;
819                            }
820                        }
821                    }
822                }
823            }
824        }
825        return true;
826    }
827
828    private boolean checkForLevelCrossing(LayoutBlock curBlock) {
829        LayoutEditor lay = curBlock.getMaxConnectedPanel();
830        for (LevelXing lx : lay.getLevelXings()) {
831            if (lx.getLayoutBlockAC() == curBlock
832                    || lx.getLayoutBlockBD() == curBlock) {
833                if ((lx.getLayoutBlockAC() != null)
834                        && (lx.getLayoutBlockBD() != null)
835                        && (lx.getLayoutBlockAC() != lx.getLayoutBlockBD())) {
836                    if (lx.getLayoutBlockAC() == curBlock) {
837                        if (!canLBlockBeUsed(lx.getLayoutBlockBD())) {
838                            return false;
839                        }
840                    } else if (lx.getLayoutBlockBD() == curBlock) {
841                        if (!canLBlockBeUsed(lx.getLayoutBlockAC())) {
842                            return false;
843                        }
844                    }
845                }
846            }
847        }
848        return true;
849    }
850
851    /**
852     * Discovers valid pairs of beans type T assigned to a layout editor. If no
853     * bean type is provided, then either SignalMasts or Sensors are discovered
854     * If no editor is provided, then all editors are considered
855     *
856     * @param editor     the layout editor panel
857     * @param T          the type
858     * @param pathMethod Determine whether or not we should reject pairs if
859     *                   there are other beans in the way. Constant values of
860     *                   NONE, ANY, MASTTOMAST, HEADTOHEAD
861     * @return the valid pairs
862     */
863    public HashMap<NamedBean, List<NamedBean>> discoverValidBeanPairs(LayoutEditor editor, Class<?> T, Routing pathMethod) {
864        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
865
866        // ----- Begin Turntable Path Discovery -----
867        HashMap<NamedBean, List<NamedBean>> retPairs = new HashMap<>();
868        List<SignalMast> turntableMasts = new ArrayList<>();
869        if (T == SignalMast.class) {
870            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
871                for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
872                    if (!turntable.isDispatcherManaged()) {
873                        continue;
874                    }
875                    LayoutBlock turntableBlock = turntable.getLayoutBlock();
876
877                    // Add all turntable-related masts to a list for exclusion from general discovery
878                    if (turntable.getExitSignalMast() != null) turntableMasts.add(turntable.getExitSignalMast());
879                    if (turntable.getBufferMast() != null) turntableMasts.add(turntable.getBufferMast());
880
881                    // Path 1: From turntable's Exit Mast to the next mast on a ray's track
882                    SignalMast exitMast = turntable.getExitSignalMast();
883                    if (exitMast != null) {
884                        for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
885                            TrackSegment track = ray.getConnect();
886                            if (track != null && track.getLayoutBlock() != null) {
887                                LayoutBlock rayBlock = track.getLayoutBlock();
888                                // Find the block connected to the ray that is NOT the turntable, then find the mast protecting it.
889                                for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
890                                    Block neighbor = rayBlock.getNeighbourAtIndex(i);
891                                    if (neighbor != turntableBlock.getBlock()) {
892                                        SignalMast nextMast = lbm.getFacingSignalMast(rayBlock.getBlock(), neighbor, panel);
893                                        if (nextMast != null) {
894                                            retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(nextMast);
895                                        }
896                                        break; // Assume only one exit from the ray block
897                                    }
898                                }
899                                // Also check for a buffer mast at the end of this ray's block
900                                if (rayBlock.getNumberOfNeighbours() == 1) { // Only connected to the turntable block
901                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(rayBlock.getBlock(), panel);
902                                    if (bufferMast != null) {
903                                        if (log.isDebugEnabled()) {
904                                            log.debug("Found turntable exit to buffer mast path: {} -> {}", exitMast.getDisplayName(), bufferMast.getDisplayName());
905                                        }
906                                        retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(bufferMast);
907                                    }
908                                }
909                            }
910                        }
911                    }
912
913
914                    // Path 2 & 3: Paths involving Approach and Buffer masts
915                    for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
916                        SignalMast approachMast = ray.getApproachMast();
917                        if (approachMast == null) { // this is logged elsewhere
918                            continue;
919                        }
920                        turntableMasts.add(approachMast);
921
922                        // Path 2: From a remote mast on the layout to this ray's Approach Mast
923                        TrackSegment track = ray.getConnect();
924                        if (track != null && track.getLayoutBlock() != null && turntableBlock != null) {
925                            LayoutBlock rayBlock = track.getLayoutBlock();
926                            // Find the block connected to the ray that is NOT the turntable, then find the mast protecting the ray from it.
927                            for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
928                                Block neighbor = rayBlock.getNeighbourAtIndex(i);
929                                if (neighbor != turntableBlock.getBlock()) {
930                                    SignalMast remoteMast = lbm.getFacingSignalMast(neighbor, rayBlock.getBlock(), panel);
931                                    if (remoteMast != null) {
932                                        retPairs.computeIfAbsent(remoteMast, k -> new ArrayList<>()).add(approachMast);
933                                    }
934                                    // Assume only one entry to the ray block
935                                    break;
936                                }
937                            }
938                        }
939
940                        // Path 3: From this ray's Approach Mast to the turntable's Buffer Mast (a siding path)
941                        SignalMast bufferMast = turntable.getBufferMast();
942                        if (bufferMast != null) {
943                            retPairs.computeIfAbsent(approachMast, k -> new ArrayList<>()).add(bufferMast);
944                        }
945                    }
946                }
947            }
948        }
949        // ----- Begin Traverser Path Discovery -----
950//        HashMap<NamedBean, List<NamedBean>> retPairs = new HashMap<>();
951        List<SignalMast> traverserMasts = new ArrayList<>();
952        if (T == SignalMast.class) {
953            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
954                for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
955                    if (!traverser.isDispatcherManaged()) {
956                        continue;
957                    }
958                    LayoutBlock traverserBlock = traverser.getLayoutBlock();
959
960                    // Add all traverser-related masts to a list for exclusion from general discovery
961                    if (traverser.getExitSignalMast() != null) traverserMasts.add(traverser.getExitSignalMast());
962                    if (traverser.getBufferMast() != null) traverserMasts.add(traverser.getBufferMast());
963
964                    // Path 1: From traverser's Exit Mast to the next mast on a ray's track
965                    SignalMast exitMast = traverser.getExitSignalMast();
966                    if (exitMast != null) {
967                        for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
968                            TrackSegment track = slot.getConnect();
969                            if (track != null && track.getLayoutBlock() != null) {
970                                LayoutBlock slotBlock = track.getLayoutBlock();
971                                // Find the block connected to the slot that is NOT the traverser, then find the mast protecting it.
972                                for (int i = 0; i < slotBlock.getNumberOfNeighbours(); i++) {
973                                    Block neighbor = slotBlock.getNeighbourAtIndex(i);
974                                    if (neighbor != traverserBlock.getBlock()) {
975                                        SignalMast nextMast = lbm.getFacingSignalMast(slotBlock.getBlock(), neighbor, panel);
976                                        if (nextMast != null) {
977                                            retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(nextMast);
978                                        }
979                                        break; // Assume only one exit from the slot block
980                                    }
981                                }
982                                // Also check for a buffer mast at the end of this slot's block
983                                if (slotBlock.getNumberOfNeighbours() == 1) { // Only connected to the traverser block
984                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(slotBlock.getBlock(), panel);
985                                    if (bufferMast != null) {
986                                        if (log.isDebugEnabled()) {
987                                            log.debug("Found traverser exit to buffer mast path: {} -> {}", exitMast.getDisplayName(), bufferMast.getDisplayName());
988                                        }
989                                        retPairs.computeIfAbsent(exitMast, k -> new ArrayList<>()).add(bufferMast);
990                                    }
991                                }
992                            }
993                        }
994                    }
995
996
997                    // Path 2 & 3: Paths involving Approach and Buffer masts
998                    for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
999                        SignalMast approachMast = slot.getApproachMast();
1000                        if (approachMast == null) { // this is logged elsewhere
1001                            continue;
1002                        }
1003                        traverserMasts.add(approachMast);
1004
1005                        // Path 2: From a remote mast on the layout to this slot's Approach Mast
1006                        TrackSegment track = slot.getConnect();
1007                        if (track != null && track.getLayoutBlock() != null && traverserBlock != null) {
1008                            LayoutBlock slotBlock = track.getLayoutBlock();
1009                            // Find the block connected to the slot that is NOT the traverser, then find the mast protecting the slot from it.
1010                            for (int i = 0; i < slotBlock.getNumberOfNeighbours(); i++) {
1011                                Block neighbor = slotBlock.getNeighbourAtIndex(i);
1012                                if (neighbor != traverserBlock.getBlock()) {
1013                                    SignalMast remoteMast = lbm.getFacingSignalMast(neighbor, slotBlock.getBlock(), panel);
1014                                    if (remoteMast != null) {
1015                                        retPairs.computeIfAbsent(remoteMast, k -> new ArrayList<>()).add(approachMast);
1016                                    }
1017                                    // Assume only one entry to the slot block
1018                                    break;
1019                                }
1020                            }
1021                        }
1022
1023                        // Path 3: From this ray's Approach Mast to the traverser's Buffer Mast (a siding path)
1024                        SignalMast bufferMast = traverser.getBufferMast();
1025                        if (bufferMast != null) {
1026                            retPairs.computeIfAbsent(approachMast, k -> new ArrayList<>()).add(bufferMast);
1027                        }
1028                    }
1029                }
1030            }
1031        }
1032        // ----- End Turntable/Traverser Path Discovery -----
1033
1034        // ----- Begin General Path Discovery (excluding turntable masts) -----
1035        List<FacingProtecting> beanList = generateBlocksWithBeans(editor, T);
1036        beanList.forEach((fp) -> {
1037            // Skip any mast that has already been handled by the turntable-specific logic above
1038            if (turntableMasts.contains(fp.getBean())) {
1039                return; // continue to next fp in forEach
1040            }
1041            // Skip any mast that has already been handled by the traverser-specific logic above
1042            if (traverserMasts.contains(fp.getBean())) {
1043                return; // continue to next fp in forEach
1044            }
1045            fp.getProtectingBlocks().stream().map((block) -> {
1046                if (log.isDebugEnabled()) {
1047                    try {
1048                        log.debug("\nSource {}", fp.getBean().getDisplayName());
1049                        log.debug("facing {}", fp.getFacing().getDisplayName());
1050                        log.debug("protecting {}", block.getDisplayName());
1051                    } catch (java.lang.NullPointerException e) {
1052                        // Can be considered normal if the signalmast is assigned to an end bumper.
1053                    }
1054                }
1055                return block;
1056            }).forEachOrdered((block) -> {
1057                LayoutBlock lFacing = lbm.getLayoutBlock(fp.getFacing());
1058                LayoutBlock lProtecting = lbm.getLayoutBlock(block);
1059                NamedBean source = fp.getBean();
1060                try {
1061                    retPairs.computeIfAbsent(source, k -> new ArrayList<>()).addAll(discoverPairDest(source, lProtecting, lFacing, beanList, pathMethod));
1062                } catch (JmriException ex) {
1063                    log.error("exception in retPairs.put", ex);
1064                }
1065            });
1066        });
1067        return retPairs;
1068    }
1069
1070    /**
1071     * Returns a list of valid destination beans reachable from a given source
1072     * bean.
1073     *
1074     * @param source     Either a SignalMast or Sensor
1075     * @param editor     The layout editor that the source is located on, if
1076     *                   null, then all editors are considered
1077     * @param T          The class of the remote destination, if null, then both
1078     *                   SignalMasts and Sensors are considered
1079     * @param pathMethod Determine whether or not we should reject pairs if
1080     *                   there are other beans in the way. Constant values of
1081     *                   NONE, ANY, MASTTOMAST, HEADTOHEAD
1082     * @return A list of all reachable NamedBeans
1083     * @throws jmri.JmriException occurring during nested readAll operation
1084     */
1085    public List<NamedBean> discoverPairDest(NamedBean source, LayoutEditor editor, Class<?> T, Routing pathMethod) throws JmriException {
1086        if (log.isDebugEnabled()) {
1087            log.debug("discover pairs from source {}", source.getDisplayName());
1088        }
1089
1090        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
1091
1092        // First, check if the source is a turntable mast. If so, handle it specially.
1093        if (T == SignalMast.class) {
1094            if (! (source instanceof SignalMast)) {
1095                throw new IllegalArgumentException("source is not a SignalMast: " + (source != null ? source.getClass().getName() : "null"));
1096            }
1097            SignalMast sourceMast = (SignalMast) source;
1098            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
1099                for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
1100                    if (!turntable.isDispatcherManaged()) {
1101                        continue;
1102                    }
1103
1104                    // Case 1: Source is the turntable's Exit Mast
1105                    if (sourceMast.equals(turntable.getExitSignalMast())) {
1106                        log.debug("Source is an exit mast for turntable {}", turntable.getName());
1107                        List<NamedBean> destinations = new ArrayList<>();
1108                        LayoutBlock turntableBlock = turntable.getLayoutBlock();
1109                        for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
1110                            TrackSegment track = ray.getConnect();
1111                            if (track != null && track.getLayoutBlock() != null) {
1112                                LayoutBlock rayBlock = track.getLayoutBlock();
1113                                for (int i = 0; i < rayBlock.getNumberOfNeighbours(); i++) {
1114                                    Block neighbor = rayBlock.getNeighbourAtIndex(i);
1115                                    if (neighbor != turntableBlock.getBlock()) {
1116                                        SignalMast nextMast = lbm.getFacingSignalMast(rayBlock.getBlock(), neighbor, panel);
1117                                        if (nextMast != null) destinations.add(nextMast);
1118                                    }
1119                                }
1120                                if (rayBlock.getNumberOfNeighbours() == 1) { // End of line buffer
1121                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(rayBlock.getBlock(), panel);
1122                                    if (bufferMast != null) destinations.add(bufferMast);
1123                                }
1124                            }
1125                        }
1126                        return destinations;
1127                    }
1128
1129                    // Case 2: Source is an Approach Mast for one of the rays
1130                    for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
1131                        if (sourceMast.equals(ray.getApproachMast())) {
1132                            log.debug("Source is an approach mast for turntable {}", turntable.getName());
1133                            List<NamedBean> destinations = new ArrayList<>();
1134                            if (turntable.getBufferMast() != null) destinations.add(turntable.getBufferMast());
1135                            return destinations;
1136                        }
1137                    }
1138                }
1139            }
1140        }
1141        // First, check if the source is a traverser mast. If so, handle it specially.
1142        if (T == SignalMast.class) {
1143            if (! (source instanceof SignalMast)) {
1144                throw new IllegalArgumentException("source is not a SignalMast: " + (source != null ? source.getClass().getName() : "null"));
1145            }
1146            SignalMast sourceMast = (SignalMast) source;
1147            for (LayoutEditor panel : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
1148                for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
1149                    if (!traverser.isDispatcherManaged()) continue;
1150
1151                    // Case 1: Source is the traverser's Exit Mast
1152                    if (sourceMast.equals(traverser.getExitSignalMast())) {
1153                        List<NamedBean> destinations = new ArrayList<>();
1154                        LayoutBlock traverserBlock = traverser.getLayoutBlock();
1155                        for (int i = 0; i < traverser.getNumberSlots(); i++) {
1156                            TrackSegment track = traverser.getSlotConnectOrdered(i);
1157                            if (track != null && track.getLayoutBlock() != null) {
1158                                LayoutBlock slotBlock = track.getLayoutBlock();
1159                                for (int j = 0; j < slotBlock.getNumberOfNeighbours(); j++) {
1160                                    Block neighbor = slotBlock.getNeighbourAtIndex(j);
1161                                    if (neighbor != traverserBlock.getBlock()) {
1162                                        SignalMast nextMast = lbm.getFacingSignalMast(slotBlock.getBlock(), neighbor, panel);
1163                                        if (nextMast != null) destinations.add(nextMast);
1164                                    }
1165                                }
1166                                if (slotBlock.getNumberOfNeighbours() == 1) { // End of line buffer
1167                                    SignalMast bufferMast = lbm.getSignalMastAtEndBumper(slotBlock.getBlock(), panel);
1168                                    if (bufferMast != null) destinations.add(bufferMast);
1169                                }
1170                            }
1171                        }
1172                        return destinations;
1173                    }
1174
1175                    // Case 2: Source is an Approach Mast for one of the slots
1176                    for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
1177                        if (sourceMast.equals(slot.getApproachMast())) {
1178                            log.debug("Source is an approach mast for traverser {}", traverser.getName());
1179                            List<NamedBean> destinations = new ArrayList<>();
1180                            if (traverser.getBufferMast() != null) destinations.add(traverser.getBufferMast());
1181                            return destinations;
1182                        }
1183                    }
1184                }
1185            }
1186        }
1187
1188        LayoutBlock lFacing = lbm.getFacingBlockByNamedBean(source, editor);
1189        List<LayoutBlock> lProtecting = lbm.getProtectingBlocksByNamedBean(source, editor);
1190        List<NamedBean> ret = new ArrayList<>();
1191        List<FacingProtecting> beanList = generateBlocksWithBeans(editor, T);
1192
1193        // may throw JmriException here
1194        for (LayoutBlock lb : lProtecting) {
1195            ret.addAll(discoverPairDest(source, lb, lFacing, beanList, pathMethod));
1196        }
1197        return ret;
1198    }
1199
1200    List<NamedBean> discoverPairDest(NamedBean source, LayoutBlock lProtecting, LayoutBlock lFacing, List<FacingProtecting> blockList, Routing pathMethod) throws JmriException {
1201        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
1202        if (!lbm.isAdvancedRoutingEnabled()) {
1203            throw new JmriException("advanced routing not enabled");
1204        }
1205        if (!lbm.routingStablised()) {
1206            throw new JmriException("routing not stabilised");
1207        }
1208        List<NamedBean> validDestBean = new ArrayList<>();
1209        for (FacingProtecting facingProtecting : blockList) {
1210            if (facingProtecting.getBean() != source) {
1211                NamedBean destObj = facingProtecting.getBean();
1212                if (log.isDebugEnabled()) {
1213                    log.debug("looking for pair {} {}", source.getDisplayName(), destObj.getDisplayName());
1214                }
1215                try {
1216                    if (checkValidDest(lFacing, lProtecting, facingProtecting, pathMethod)) {
1217                        if (log.isDebugEnabled()) {
1218                            log.debug("Valid pair {} {}", source.getDisplayName(), destObj.getDisplayName());
1219                        }
1220                        LayoutBlock ldstBlock = lbm.getLayoutBlock(facingProtecting.getFacing());
1221                        try {
1222                            List<LayoutBlock> lblks = getLayoutBlocks(lFacing, ldstBlock, lProtecting, true, pathMethod);
1223                            if (log.isDebugEnabled()) {
1224                                log.debug("Adding block {} to paths, current size {}", destObj.getDisplayName(), lblks.size());
1225                            }
1226                            validDestBean.add(destObj);
1227                        } catch (JmriException e) {  // Considered normal if route not found.
1228                            log.debug("not a valid route through {} - {}", source.getDisplayName(), destObj.getDisplayName());
1229                        }
1230                    }
1231                } catch (JmriException ex) {
1232                    log.debug("caught exception in discoverPairsDest", ex);
1233                }
1234            }
1235        }
1236        return validDestBean;
1237    }
1238
1239    List<FacingProtecting> generateBlocksWithBeans(LayoutEditor editor, Class<?> T) {
1240        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
1241        List<FacingProtecting> beanList = new ArrayList<>();
1242
1243        for (LayoutBlock curLblk : lbm.getNamedBeanSet()) {
1244            Block curBlk = curLblk.getBlock();
1245            LayoutEditor useEdit = editor;
1246            if (editor == null) {
1247                useEdit = curLblk.getMaxConnectedPanel();
1248            }
1249            if (curBlk != null) {
1250                int noNeigh = curLblk.getNumberOfNeighbours();
1251                for (int x = 0; x < noNeigh; x++) {
1252                    Block blk = curLblk.getNeighbourAtIndex(x);
1253                    List<Block> proBlk = new ArrayList<>();
1254                    NamedBean bean = null;
1255                    if (T == null) {
1256                        proBlk.add(blk);
1257                        bean = lbm.getFacingNamedBean(curBlk, blk, useEdit);
1258                    } else if (T.equals(SignalMast.class)) {
1259                        bean = lbm.getFacingSignalMast(curBlk, blk, useEdit);
1260                        if (bean != null) {
1261                            if (log.isDebugEnabled()) {
1262                                log.debug("Get list of protecting blocks for {} facing {}", bean.getDisplayName(), curBlk.getDisplayName());
1263                            }
1264                            List<LayoutBlock> lProBlk = lbm.getProtectingBlocksByNamedBean(bean, useEdit);
1265                            for (LayoutBlock lb : lProBlk) {
1266                                if (lb != null) {
1267                                    proBlk.add(lb.getBlock());
1268                                }
1269                            }
1270                        }
1271                    } else if (T.equals(Sensor.class)) {
1272                        bean = lbm.getFacingSensor(curBlk, blk, useEdit);
1273                        if (bean != null) {
1274                            if (log.isDebugEnabled()) {
1275                                log.debug("Get list of protecting blocks for {}", bean.getDisplayName());
1276                            }
1277                            List<LayoutBlock> lProBlk = lbm.getProtectingBlocksByNamedBean(bean, useEdit);
1278                            for (LayoutBlock lb : lProBlk) {
1279                                if (lb != null) {
1280                                    proBlk.add(lb.getBlock());
1281                                }
1282                            }
1283                        }
1284                    } else {
1285                        log.error("Past bean type is unknown {}", T);
1286                    }
1287                    if (bean != null) {
1288                        FacingProtecting toadd = new FacingProtecting(curBlk, proBlk, bean);
1289                        boolean found = false;
1290                        for (FacingProtecting fp : beanList) {
1291                            if (fp.equals(toadd)) {
1292                                found = true;
1293                                break;
1294                            }
1295                        }
1296                        if (!found) {
1297                            beanList.add(toadd);
1298                        }
1299                    }
1300                }
1301                if (noNeigh == 1) {
1302                    NamedBean bean = null;
1303                    if (log.isDebugEnabled()) {
1304                        log.debug("We have a dead end {}", curBlk.getDisplayName());
1305                    }
1306                    if (T == null) {
1307                        bean = lbm.getNamedBeanAtEndBumper(curBlk, useEdit);
1308                    } else if (T.equals(SignalMast.class)) {
1309                        bean = lbm.getSignalMastAtEndBumper(curBlk, useEdit);
1310                    } else if (T.equals(Sensor.class)) {
1311                        bean = lbm.getSensorAtEndBumper(curBlk, useEdit);
1312                    } else {
1313                        log.error("Past bean type is unknown {}", T);
1314                    }
1315                    if (bean != null) {
1316                        FacingProtecting toadd = new FacingProtecting(curBlk, null, bean);
1317                        boolean found = false;
1318                        for (FacingProtecting fp : beanList) {
1319                            if (fp.equals(toadd)) {
1320                                found = true;
1321                                break;
1322                            }
1323                        }
1324                        if (!found) {
1325                            beanList.add(toadd);
1326                        }
1327                    }
1328                }
1329            }
1330        }
1331        return beanList;
1332    }
1333
1334    static class FacingProtecting {
1335
1336        Block facing;
1337        List<Block> protectingBlocks;
1338        NamedBean bean;
1339
1340        FacingProtecting(Block facing, List<Block> protecting, NamedBean bean) {
1341            this.facing = facing;
1342            if (protecting == null) {
1343                this.protectingBlocks = new ArrayList<>(0);
1344            } else {
1345                this.protectingBlocks = protecting;
1346            }
1347            this.bean = bean;
1348        }
1349
1350        Block getFacing() {
1351            return facing;
1352        }
1353
1354        List<Block> getProtectingBlocks() {
1355            return protectingBlocks;
1356        }
1357
1358        NamedBean getBean() {
1359            return bean;
1360        }
1361
1362        @Override
1363        public boolean equals(Object obj) {
1364
1365            if (obj == this) {
1366                return true;
1367            }
1368            if (obj == null) {
1369                return false;
1370            }
1371
1372            if (!(getClass() == obj.getClass())) {
1373                return false;
1374            } else {
1375                FacingProtecting tmp = (FacingProtecting) obj;
1376                if (tmp.getBean() != this.bean) {
1377                    return false;
1378                }
1379                if (tmp.getFacing() != this.facing) {
1380                    return false;
1381                }
1382                if (!tmp.getProtectingBlocks().equals(this.protectingBlocks)) {
1383                    return false;
1384                }
1385            }
1386            return true;
1387        }
1388
1389        @Override
1390        public int hashCode() {
1391            int hash = 7;
1392            hash = 37 * hash + (this.bean != null ? this.bean.hashCode() : 0);
1393            hash = 37 * hash + (this.facing != null ? this.facing.hashCode() : 0);
1394            hash = 37 * hash + (this.protectingBlocks != null ? this.protectingBlocks.hashCode() : 0);
1395            return hash;
1396        }
1397    }
1398
1399    private static final Logger log
1400            = LoggerFactory.getLogger(LayoutBlockConnectivityTools.class);
1401}