001package jmri.jmrit.display.layoutEditor;
002
003import java.util.ArrayList;
004import java.util.List;
005import java.util.Set;
006import javax.annotation.CheckForNull;
007import javax.annotation.CheckReturnValue;
008import javax.annotation.Nonnull;
009
010import jmri.Block;
011import jmri.BlockManager;
012import jmri.jmrit.display.EditorManager;
013import jmri.InstanceManager;
014import jmri.JmriException;
015import jmri.Memory;
016import jmri.NamedBean;
017import jmri.NamedBeanHandle;
018import jmri.Sensor;
019import jmri.SignalHead;
020import jmri.SignalMast;
021import jmri.Turnout;
022import jmri.jmrit.roster.RosterEntry;
023import jmri.jmrix.internal.InternalSystemConnectionMemo;
024import jmri.managers.AbstractManager;
025import jmri.util.swing.JmriJOptionPane;
026import jmri.util.ThreadingUtil;
027
028/**
029 * Implementation of a Manager to handle LayoutBlocks. Note: the same
030 * LayoutBlocks may appear in multiple LayoutEditor panels.
031 * <p>
032 * This manager does not enforce any particular system naming convention.
033 * <p>
034 * LayoutBlocks are usually addressed by userName. The systemName is hidden from
035 * the user for the most part.
036 *
037 * @author Dave Duchamp Copyright (C) 2007
038 * @author George Warner Copyright (c) 2017-2018
039 */
040public class LayoutBlockManager extends AbstractManager<LayoutBlock> implements jmri.InstanceManagerAutoDefault {
041
042    public LayoutBlockManager() {
043        super(InstanceManager.getDefault(InternalSystemConnectionMemo.class));
044        InstanceManager.sensorManagerInstance().addVetoableChangeListener(LayoutBlockManager.this);
045        InstanceManager.memoryManagerInstance().addVetoableChangeListener(LayoutBlockManager.this);
046    }
047
048    /**
049     * String constant for advanced routing enabled.
050     */
051    public static final String PROPERTY_ADVANCED_ROUTING_ENABLED = "advancedRoutingEnabled";
052
053    /**
054     * String constant for the topology property.
055     */
056    public static final String PROPERTY_TOPOLOGY = "topology";
057
058    @Override
059    public int getXMLOrder() {
060        return jmri.Manager.LAYOUTBLOCKS;
061    }
062
063    @Override
064    public char typeLetter() {
065        return 'B';
066    }
067    private int blkNum = 1;
068
069    /**
070     * Create a new LayoutBlock if the LayoutBlock does not exist.
071     * <p>
072     * Note that since the userName is used to address LayoutBlocks, the user
073     * name must be present. If the user name is not present, the new
074     * LayoutBlock is not created, and null is returned.
075     *
076     * @param systemName block system name.
077     * @param userName block username, must be non-empty.
078     * @return null if a LayoutBlock with the same systemName or userName
079     *         already exists, or if there is trouble creating a new LayoutBlock
080     */
081    @CheckReturnValue
082    @CheckForNull
083    public LayoutBlock createNewLayoutBlock(
084            @CheckForNull String systemName,
085            String userName) {
086        // Check that LayoutBlock does not already exist
087        LayoutBlock result;
088
089        if ((userName == null) || userName.isEmpty()) {
090            log.error("Attempt to create a LayoutBlock with no user name");
091
092            return null;
093        }
094        result = getByUserName(userName);
095
096        if (result != null) {
097            return null;
098        }
099
100        // here if not found under user name
101        String sName = "";
102
103        if (systemName == null) {
104            //create a new unique system name
105            boolean found = true;
106
107            while (found) {
108                sName = "ILB" + blkNum;
109                blkNum++;
110                result = getBySystemName(sName);
111
112                if (result == null) {
113                    found = false;
114                }
115            }
116        } else {
117            // try the supplied system name
118            result = getBySystemName((systemName));
119
120            if (result != null) {
121                return null;
122            }
123            sName = systemName;
124        }
125
126        // LayoutBlock does not exist, create a new LayoutBlock
127        result = new LayoutBlock(sName, userName);
128
129        //save in the maps
130        register(result);
131
132        return result;
133    }
134
135    @CheckReturnValue
136    @CheckForNull
137    public LayoutBlock createNewLayoutBlock() {
138        while (true) {
139            String sName = "ILB" + blkNum;
140            LayoutBlock block = getBySystemName(sName);
141
142            if (block == null) {
143                String uName = "AUTOBLK:" + blkNum;
144                block = new LayoutBlock(sName, uName);
145                register(block);
146
147                return block;
148            }
149            blkNum++;
150        }
151    }
152
153    /**
154     * Remove an existing LayoutBlock.
155     * @param block the block to remove.
156     */
157    public void deleteLayoutBlock(LayoutBlock block) {
158        layoutBlockByBlock.values().remove(block);
159        deregister(block);
160    }
161
162    /**
163     * Get an existing LayoutBlock. First looks up assuming that name is a User
164     * Name. If this fails, looks up assuming that name is a System Name.
165     *
166     * @param name ideally block username, can be system name.
167     * @return LayoutBlock, or null if not found by either user name or system
168     *         name
169     */
170    @CheckReturnValue
171    @CheckForNull
172    public LayoutBlock getLayoutBlock(@Nonnull String name) {
173        LayoutBlock block = getByUserName(name);
174
175        if (block != null) {
176            return block;
177        }
178        return getBySystemName(name);
179    }
180
181    // LayoutBlock found for each Block, checked on every use as either may change
182    private final java.util.Map<Block, LayoutBlock> layoutBlockByBlock = new java.util.concurrent.ConcurrentHashMap<>();
183
184    @CheckReturnValue
185    @CheckForNull
186    public LayoutBlock getLayoutBlock(@CheckForNull Block block) {
187        if (block != null) {
188            LayoutBlock found = layoutBlockByBlock.get(block);
189            if ((found != null) && (found.getBlock() == block)
190                    && (getBySystemName(found.getSystemName()) == found)) {
191                return found;
192            }
193        }
194        for (LayoutBlock lb : getNamedBeanSet()) {
195            if (lb.getBlock() == block) {
196                if (block != null) {
197                    layoutBlockByBlock.put(block, lb);
198                }
199                return lb;
200            }
201        }
202        if (block != null) {
203            layoutBlockByBlock.remove(block);
204        }
205        return null;
206    }
207
208    /**
209     * Find a LayoutBlock with a specified Sensor assigned as its occupancy
210     * sensor.
211     *
212     * @param s the sensor to search for.
213     * @return the block or null if no existing LayoutBlock has the Sensor
214     *         assigned
215     */
216    @CheckReturnValue
217    @CheckForNull
218    public LayoutBlock getBlockWithSensorAssigned(@CheckForNull Sensor s) {
219        for (LayoutBlock block : getNamedBeanSet()) {
220            if (block.getOccupancySensor() == s) {
221                return block;
222            }
223        }
224        return null;
225    }
226
227    /**
228     * Find a LayoutBlock with a specified Memory assigned as its value display.
229     *
230     * @param m the memory to search for.
231     * @return the block or null if no existing LayoutBlock has the memory
232     *         assigned.
233     */
234    @CheckReturnValue
235    @CheckForNull
236    public LayoutBlock getBlockWithMemoryAssigned(Memory m) {
237        for (LayoutBlock block : getNamedBeanSet()) {
238            if (block.getMemory() == m) {
239                return block;
240            }
241        }
242        return null;
243    }
244
245    /**
246     * Initialize/check the Paths of all Blocks associated with LayoutBlocks.
247     * <p>
248     * This routine should be called when loading panels, after all Layout
249     * Editor panels have been loaded.
250     */
251    public void initializeLayoutBlockPaths() {
252        log.debug("start initializeLayoutBlockPaths");
253
254        log.debug("start initializeLayoutBlockPaths getNamedBeanSet {}", getNamedBeanSet());
255
256        // cycle through all LayoutBlocks, completing initialization of associated jmri.Blocks
257        for (LayoutBlock b : getNamedBeanSet()) {
258            log.debug("Calling block '{}({})'.initializeLayoutBlock()", b.getSystemName(), b.getDisplayName());
259            b.initializeLayoutBlock();
260        }
261
262        //cycle through all LayoutBlocks, updating Paths of associated jmri.Blocks
263        badBeanErrors = 0; // perhaps incremented via addBadBeanError(), but that's never called?
264        for (LayoutBlock b : getNamedBeanSet()) {
265            log.debug("Calling block '{}({})'.updatePaths()", b.getSystemName(), b.getDisplayName());
266
267            b.updatePaths();
268
269            if (b.getBlock().getValue() != null) {
270                b.getBlock().setValue(null);
271            }
272        }
273
274        if (badBeanErrors > 0) { // perhaps incremented via addBadBeanError(), but that's never called?
275            JmriJOptionPane.showMessageDialog(null, "" + badBeanErrors + " " + Bundle.getMessage("Warn2"),
276                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.ERROR_MESSAGE);
277        }
278        try {
279            new BlockValueFile().readBlockValues();
280        } catch (org.jdom2.JDOMException jde) {
281            log.error("JDOM Exception when retreiving block values", jde);
282        } catch (java.io.IOException ioe) {
283            log.error("I/O Exception when retreiving block values", ioe);
284        } catch (RuntimeException re) {
285            // restoring the saved block values is not worth losing the routing
286            // initialization below, which would otherwise be skipped silently
287            log.error("Exception when retreiving block values", re);
288        }
289
290        //special tests for getFacingSignalHead method - comment out next three lines unless using LayoutEditorTests
291        //LayoutEditorTests layoutEditorTests = new LayoutEditorTests();
292        //layoutEditorTests.runClinicTests();
293        //layoutEditorTests.runTestPanel3Tests();
294        initialized = true;
295        log.debug("start initializeLayoutBlockRouting");
296        initializeLayoutBlockRouting();
297        log.debug("end initializeLayoutBlockRouting and initializeLayoutBlockPaths");
298    }
299
300    private boolean initialized = false;
301
302    // Is this ever called?
303    public void addBadBeanError() {
304        badBeanErrors++;
305    }
306    private int badBeanErrors = 0;
307
308    /**
309     * Get the Signal Head facing into a specified Block from a specified
310     * protected Block.
311     * <p>
312     * This method is primarily designed for use with scripts to get information
313     * initially residing in a Layout Editor panel. If either of the input
314     * Blocks is null, or if the two blocks do not join at a block boundary, or
315     * if either of the input Blocks are not Layout Editor panel blocks, an
316     * error message is logged, and "null" is returned. If the signal at the
317     * block boundary has two heads--is located at the facing point of a
318     * turnout-- the Signal Head that applies for the current setting of turnout
319     * (THROWN or CLOSED) is returned. If the turnout state is UNKNOWN or
320     * INCONSISTENT, an error message is logged, and "null" is returned. If the
321     * signal at the block boundary has three heads--the facing point of a 3-way
322     * turnout--the Signal Head that applies for the current settings of the two
323     * turnouts of the 3-way turnout is returned. If the turnout state of either
324     * turnout is UNKNOWN or INCONSISTENT, an error is logged and "null" is
325     * returned. "null" is returned if the block boundary is between the two
326     * turnouts of a THROAT_TO_THROAT turnout or a 3-way turnout. "null" is
327     * returned for block boundaries exiting a THROAT_TO_THROAT turnout block,
328     * since there are no signals that apply there.
329     * @param facingBlock the facing block.
330     * @param protectedBlock the protected block.
331     * @return the signal head, may be null.
332     */
333    @CheckReturnValue
334    @CheckForNull
335    public SignalHead getFacingSignalHead(
336            @CheckForNull Block facingBlock,
337            @CheckForNull Block protectedBlock) {
338        //check input
339        if ((facingBlock == null) || (protectedBlock == null)) {
340            log.error("null block in call to getFacingSignalHead");
341            return null;
342        }
343
344        //non-null - check if input corresponds to Blocks in a Layout Editor panel.
345        String facingBlockName = facingBlock.getUserName();
346        if ((facingBlockName == null) || facingBlockName.isEmpty()) {
347            log.error("facingBlockName has no user name");
348            return null;
349        }
350
351        String protectedBlockName = protectedBlock.getUserName();
352        if ((protectedBlockName == null) || protectedBlockName.isEmpty()) {
353            log.error("protectedBlockName has no user name");
354            return null;
355        }
356
357        LayoutBlock fLayoutBlock = getByUserName(facingBlockName);
358        LayoutBlock pLayoutBlock = getByUserName(protectedBlockName);
359        if ((fLayoutBlock == null) || (pLayoutBlock == null)) {
360            if (fLayoutBlock == null) {
361                log.error("Block {} is not on a Layout Editor panel.", facingBlock.getDisplayName());
362            }
363
364            if (pLayoutBlock == null) {
365                log.error("Block {} is not on a Layout Editor panel.", protectedBlock.getDisplayName());
366            }
367            return null;
368        }
369
370        //input has corresponding LayoutBlocks - does it correspond to a block boundary?
371        LayoutEditor panel = fLayoutBlock.getMaxConnectedPanel();
372        List<LayoutConnectivity> c = panel.getLEAuxTools().getConnectivityList(fLayoutBlock);
373        LayoutConnectivity lc = null;
374        int i = 0;
375        boolean facingIsBlock1 = true;
376
377        while ((i < c.size()) && (lc == null)) {
378            LayoutConnectivity tlc = c.get(i);
379
380            if ((tlc.getBlock1() == fLayoutBlock) && (tlc.getBlock2() == pLayoutBlock)) {
381                lc = tlc;
382            } else if ((tlc.getBlock1() == pLayoutBlock) && (tlc.getBlock2() == fLayoutBlock)) {
383                lc = tlc;
384                facingIsBlock1 = false;
385            }
386            i++;
387        }
388
389        if (lc == null) {
390            log.error("Block {} ({}) is not connected to Block {}", facingBlock.getDisplayName(),
391                    facingBlock.getDisplayName(), protectedBlock.getDisplayName());
392            return null;
393        }
394
395        //blocks are connected, get connection item types
396        LayoutTurnout lt;
397        TrackSegment tr = lc.getTrackSegment();
398        int boundaryType;
399
400        if (tr == null) {
401            // this is an internal crossover block boundary
402            lt = lc.getXover();
403            boundaryType = lc.getXoverBoundaryType();
404
405            switch (boundaryType) {
406                case LayoutConnectivity.XOVER_BOUNDARY_AB: {
407                    if (facingIsBlock1) {
408                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
409                    } else {
410                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
411                    }
412                }
413
414                case LayoutConnectivity.XOVER_BOUNDARY_CD: {
415                    if (facingIsBlock1) {
416                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
417                    } else {
418                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
419                    }
420                }
421
422                case LayoutConnectivity.XOVER_BOUNDARY_AC: {
423                    if (facingIsBlock1) {
424                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) { //there is no signal head for diverging (crossed
425                            //over)
426                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
427                        } else { //there is a diverging (crossed over) signal head, return it
428                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
429                        }
430                    } else {
431                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTC2) == null) {
432                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
433                        } else {
434                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
435                        }
436                    }
437                }
438
439                case LayoutConnectivity.XOVER_BOUNDARY_BD: {
440                    if (facingIsBlock1) {
441                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTB2) == null) {
442                            // there is no signal head for diverging (crossed over)
443                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
444                        } else { //there is a diverging (crossed over) signal head, return it
445                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
446                        }
447                    } else {
448                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTD2) == null) {
449                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
450                        } else {
451                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTD2);
452                        }
453                    }
454                }
455
456                default: {
457                    log.error("Unhandled crossover connection type: {}", boundaryType);
458                    break;
459                }
460            } //switch
461
462            //should never reach here, but ...
463            log.error("crossover turnout block boundary not found in getFacingSignal");
464
465            return null;
466        }
467
468        //not internal crossover block boundary
469        LayoutTrack connected = lc.getConnectedObject();
470        HitPointType cType = lc.getConnectedType();
471        if (connected == null) {
472            log.error("No connectivity object found between Blocks {}, {} {}", facingBlock.getDisplayName(),
473                    protectedBlock.getDisplayName(), cType);
474
475            return null;
476        }
477
478        if (cType == HitPointType.TRACK) {
479            // block boundary is at an Anchor Point
480            //    LayoutEditorTools tools = panel.getLETools(); //TODO: Dead-code strip this
481            PositionablePoint p = panel.getFinder().findPositionablePointAtTrackSegments(tr, (TrackSegment) connected);
482            boolean block1IsWestEnd = LayoutEditorTools.isAtWestEndOfAnchor(panel, tr, p);
483
484            if ((block1IsWestEnd && facingIsBlock1) || (!block1IsWestEnd && !facingIsBlock1)) {
485                //block1 is on the west (north) end of the block boundary
486                return p.getEastBoundSignalHead();
487            } else {
488                return p.getWestBoundSignalHead();
489            }
490        }
491
492        if (cType == HitPointType.TURNOUT_A) {
493            // block boundary is at the facing point of a turnout or A connection of a crossover turnout
494            lt = (LayoutTurnout) connected;
495
496            if (lt.getLinkType() == LayoutTurnout.LinkType.NO_LINK) {
497                //standard turnout or A connection of a crossover turnout
498                if (facingIsBlock1) {
499                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) { //there is no signal head for diverging
500                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
501                    } else {
502                        //check if track segments at B or C are in protected block (block 2)
503                        if (((TrackSegment) (lt.getConnectB())).getBlockName().equals(protectedBlock.getUserName())) {
504                            //track segment connected at B matches block 2, check C
505                            if (!(((TrackSegment) lt.getConnectC()).getBlockName().equals(protectedBlock.getUserName()))) {
506                                //track segment connected at C is not in block2, return continuing signal head at A
507                                if (lt.getContinuingSense() == Turnout.CLOSED) {
508                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
509                                } else {
510                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
511                                }
512                            } else {
513                                //B and C both in block2, check turnout position to decide which signal head to return
514                                int state = lt.getTurnout().getKnownState();
515
516                                if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
517                                        || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
518                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
519                                } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
520                                        || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) { //diverging
521                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
522                                } else {
523                                    //turnout state is UNKNOWN or INCONSISTENT
524                                    log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
525                                            lt.getTurnout().getDisplayName());
526
527                                    return null;
528                                }
529                            }
530                        }
531
532                        //track segment connected at B is not in block 2
533                        if ((((TrackSegment) lt.getConnectC()).getBlockName().equals(protectedBlock.getUserName()))) {
534                            //track segment connected at C is in block 2, return diverging signal head
535                            if (lt.getContinuingSense() == Turnout.CLOSED) {
536                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
537                            } else {
538                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
539                            }
540                        } else {
541                            // neither track segment is in block 2 - will get here when layout turnout is the only item in block 2
542                            // Return signal head based on turnout position
543                            int state = lt.getTurnout().getKnownState();
544                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
545                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
546                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
547                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
548                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) { //diverging
549                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
550                            }
551
552                            // Turnout state is unknown or inconsistent
553                            return null;
554                        }
555                    }
556                } else {
557                    //check if track segments at B or C are in facing block (block 1)
558                    if (((TrackSegment) (lt.getConnectB())).getBlockName().equals(facingBlock.getUserName())) {
559                        //track segment connected at B matches block 1, check C
560                        if (!(((TrackSegment) lt.getConnectC()).getBlockName().equals(facingBlock.getDisplayName()))) {
561                            //track segment connected at C is not in block 2, return signal head at continuing end
562                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
563                        } else {
564                            //B and C both in block 1, check turnout position to decide which signal head to return
565                            int state = lt.getTurnout().getKnownState();
566
567                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
568                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
569                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
570                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
571                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) {
572                                //diverging, check for second head
573                                if (lt.getSignalHead(LayoutTurnout.Geometry.POINTC2) == null) {
574                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
575                                } else {
576                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
577                                }
578                            } else {
579                                //turnout state is UNKNOWN or INCONSISTENT
580                                log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
581                                        lt.getTurnout().getDisplayName());
582
583                                return null;
584                            }
585                        }
586                    }
587
588                    //track segment connected at B is not in block 1
589                    if (((TrackSegment) lt.getConnectC()).getBlockName().equals(facingBlock.getUserName())) {
590                        //track segment connected at C is in block 1, return diverging signal head, check for second head
591                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTC2) == null) {
592                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
593                        } else {
594                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
595                        }
596                    } else {
597                        //neither track segment is in block 1 - should never get here unless layout turnout is
598                        //the only item in block 1
599                        if (!(lt.getBlockName().equals(facingBlock.getUserName()))) {
600                            log.error("no signal faces block {}, and turnout is not in block either",
601                                    facingBlock.getDisplayName());
602                        }
603                        return null;
604                    }
605                }
606            } else if (lt.getLinkType() == LayoutTurnout.LinkType.THROAT_TO_THROAT) {
607                //There are no signals at the throat of a THROAT_TO_THROAT
608
609                //There should not be a block boundary here
610                return null;
611            } else if (lt.getLinkType() == LayoutTurnout.LinkType.FIRST_3_WAY) {
612                //3-way turnout is in its own block - block boundary is at the throat of the 3-way turnout
613                if (!facingIsBlock1) {
614                    //facing block is within the three-way turnout's block - no signals for exit of the block
615                    return null;
616                } else {
617                    //select throat signal according to state of the 3-way turnout
618                    int state = lt.getTurnout().getKnownState();
619
620                    if (state == Turnout.THROWN) {
621                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) {
622                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
623                        } else {
624                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
625                        }
626                    } else if (state == Turnout.CLOSED) {
627                        LayoutTurnout tLinked = panel.getFinder().findLayoutTurnoutByTurnoutName(lt.getLinkedTurnoutName());
628                        state = tLinked.getTurnout().getKnownState();
629
630                        if (state == Turnout.CLOSED) {
631                            if (tLinked.getContinuingSense() == Turnout.CLOSED) {
632                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
633                            } else if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA3) == null) {
634                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
635                            } else {
636                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA3);
637                            }
638                        } else if (state == Turnout.THROWN) {
639                            if (tLinked.getContinuingSense() == Turnout.THROWN) {
640                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
641                            } else if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA3) == null) {
642                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
643                            } else {
644                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA3);
645                            }
646                        } else {
647                            //should never get here - linked turnout state is UNKNOWN or INCONSISTENT
648                            log.error("Cannot choose 3-way signal head to return because turnout {} is in an UNKNOWN or INCONSISTENT state.",
649                                    tLinked.getTurnout().getSystemName());
650                            return null;
651                        }
652                    } else {
653                        //should never get here - linked turnout state is UNKNOWN or INCONSISTENT
654                        log.error("Cannot choose 3-way signal head to return because turnout {} is in an UNKNOWN or INCONSISTENT state.",
655                                lt.getTurnout().getSystemName());
656                        return null;
657                    }
658                }
659            } else if (lt.getLinkType() == LayoutTurnout.LinkType.SECOND_3_WAY) {
660                //There are no signals at the throat of the SECOND_3_WAY turnout of a 3-way turnout
661
662                //There should not be a block boundary here
663                return null;
664            }
665        }
666
667        if (cType == HitPointType.TURNOUT_B) {
668            //block boundary is at the continuing track of a turnout or B connection of a crossover turnout
669            lt = (LayoutTurnout) connected;
670
671            //check for double crossover or LH crossover
672            if (((lt.getTurnoutType() == LayoutTurnout.TurnoutType.DOUBLE_XOVER)
673                    || (lt.getTurnoutType() == LayoutTurnout.TurnoutType.LH_XOVER))) {
674                if (facingIsBlock1) {
675                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTB2) == null) { //there is only one signal at B, return it
676                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
677                    }
678
679                    //check if track segments at A or D are in protected block (block 2)
680                    if (((TrackSegment) (lt.getConnectA())).getBlockName().equals(protectedBlock.getUserName())) {
681                        //track segment connected at A matches block 2, check D
682                        if (!(((TrackSegment) lt.getConnectD()).getBlockName().equals(protectedBlock.getUserName()))) {
683                            //track segment connected at D is not in block2, return continuing signal head at B
684                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
685                        } else {
686                            //A and D both in block 2, check turnout position to decide which signal head to return
687                            int state = lt.getTurnout().getKnownState();
688
689                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
690                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
691                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
692                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
693                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) { //diverging
694                                //(crossed
695
696                                //over)
697                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
698                            } else {
699                                //turnout state is UNKNOWN or INCONSISTENT
700                                log.error("LayoutTurnout {} cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
701                                        lt, lt.getTurnout());
702                                return null;
703                            }
704                        }
705                    }
706
707                    //track segment connected at A is not in block 2
708                    if ((((TrackSegment) lt.getConnectD()).getBlockName().equals(protectedBlock.getUserName()))) { //track segment
709                        //connected at D
710                        //is in block 2,
711                        //return
712                        //diverging
713
714                        //signal head
715                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
716                    } else {
717                        //neither track segment is in block 2 - should never get here unless layout turnout is
718                        //only item in block 2
719                        if (!(lt.getBlockName().equals(protectedBlock.getUserName()))) {
720                            log.error("neither signal at B protects block {}, and turnout is not in block either",
721                                    protectedBlock.getDisplayName());
722                        }
723                        return null;
724                    }
725                } else {
726                    //check if track segments at A or D are in facing block (block 1)
727                    if (((TrackSegment) (lt.getConnectA())).getBlockName().equals(facingBlock.getUserName())) {
728                        //track segment connected at A matches block 1, check D
729                        if (!(((TrackSegment) lt.getConnectD()).getBlockName().equals(facingBlock.getUserName()))) {
730                            //track segment connected at D is not in block 2, return signal head at continuing end
731                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
732                        } else {
733                            //A and D both in block 1, check turnout position to decide which signal head to return
734                            int state = lt.getTurnout().getKnownState();
735
736                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
737                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
738                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
739                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
740                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) {
741                                //diverging, check for second head
742                                if (lt.getSignalHead(LayoutTurnout.Geometry.POINTD2) == null) {
743                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
744                                } else {
745                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTD2);
746                                }
747                            } else {
748                                //turnout state is UNKNOWN or INCONSISTENT
749                                log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
750                                        lt.getTurnout().getDisplayName());
751                                return null;
752                            }
753                        }
754                    }
755
756                    //track segment connected at A is not in block 1
757                    if (((TrackSegment) lt.getConnectD()).getBlockName().equals(facingBlock.getUserName())) {
758                        //track segment connected at D is in block 1, return diverging signal head, check for second head
759                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTD2) == null) {
760                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
761                        } else {
762                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTD2);
763                        }
764                    } else {
765                        //neither track segment is in block 1 - should never get here unless layout turnout is
766                        //the only item in block 1
767                        if (!(lt.getBlockName().equals(facingBlock.getUserName()))) {
768                            log.error("no signal faces block {}, and turnout is not in block either",
769                                    facingBlock.getDisplayName());
770                        }
771                        return null;
772                    }
773                }
774            }
775
776            //not double crossover or LH crossover
777            if ((lt.getLinkType() == LayoutTurnout.LinkType.NO_LINK) && (lt.getContinuingSense() == Turnout.CLOSED)) {
778                if (facingIsBlock1) {
779                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
780                } else {
781                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
782                }
783            } else if (lt.getLinkType() == LayoutTurnout.LinkType.NO_LINK) {
784                if (facingIsBlock1) {
785                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
786                } else {
787                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) {
788                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
789                    } else {
790                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
791                    }
792                }
793            } else if (lt.getLinkType() == LayoutTurnout.LinkType.THROAT_TO_THROAT) {
794                if (!facingIsBlock1) {
795                    //There are no signals at the throat of a THROAT_TO_THROAT
796                    return null;
797                }
798
799                //facing block is outside of the THROAT_TO_THROAT
800                if ((lt.getContinuingSense() == Turnout.CLOSED) && (lt.getSignalHead(LayoutTurnout.Geometry.POINTB2) == null)) {
801                    //there is only one signal head here - return it
802                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
803                } else if ((lt.getContinuingSense() == Turnout.THROWN) && (lt.getSignalHead(LayoutTurnout.Geometry.POINTC2) == null)) {
804                    //there is only one signal head here - return it
805                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
806                }
807
808                //There are two signals here get linked turnout and decide which to return from linked turnout state
809                LayoutTurnout tLinked = panel.getFinder().findLayoutTurnoutByTurnoutName(lt.getLinkedTurnoutName());
810                int state = tLinked.getTurnout().getKnownState();
811
812                if (state == Turnout.CLOSED) {
813                    if (lt.getContinuingSense() == Turnout.CLOSED) {
814                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
815                    } else {
816                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
817                    }
818                } else if (state == Turnout.THROWN) {
819                    if (lt.getContinuingSense() == Turnout.CLOSED) {
820                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
821                    } else {
822                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
823                    }
824                } else { //should never get here - linked turnout state is UNKNOWN or INCONSISTENT
825                    log.error("Cannot choose signal head to return because turnout {} is in an UNKNOWN or INCONSISTENT state.",
826                            tLinked.getTurnout().getDisplayName());
827                }
828                return null;
829            } else if (lt.getLinkType() == LayoutTurnout.LinkType.FIRST_3_WAY) {
830                //there is no signal at the FIRST_3_WAY turnout continuing track of a 3-way turnout
831                //there should not be a block boundary here
832                return null;
833            } else if (lt.getLinkType() == LayoutTurnout.LinkType.SECOND_3_WAY) {
834                if (facingIsBlock1) {
835                    if (lt.getContinuingSense() == Turnout.CLOSED) {
836                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
837                    } else {
838                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
839                    }
840                } else {
841                    //signal is at the linked turnout - the throat of the 3-way turnout
842                    LayoutTurnout tLinked = panel.getFinder().findLayoutTurnoutByTurnoutName(lt.getLinkedTurnoutName());
843
844                    if (lt.getContinuingSense() == Turnout.CLOSED) {
845                        return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA1);
846                    } else {
847                        if (tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA3) == null) {
848                            return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA1);
849                        } else {
850                            return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA3);
851                        }
852                    }
853                }
854            }
855        }
856
857        if (cType == HitPointType.TURNOUT_C) {
858            //block boundary is at the diverging track of a turnout or C connection of a crossover turnout
859            lt = (LayoutTurnout) connected;
860
861            //check for double crossover or RH crossover
862            if ((lt.getTurnoutType() == LayoutTurnout.TurnoutType.DOUBLE_XOVER)
863                    || (lt.getTurnoutType() == LayoutTurnout.TurnoutType.RH_XOVER)) {
864                if (facingIsBlock1) {
865                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTC2) == null) { //there is only one head at C, return it
866                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
867                    }
868
869                    //check if track segments at A or D are in protected block (block 2)
870                    if (((TrackSegment) (lt.getConnectA())).getBlockName().equals(protectedBlock.getUserName())) {
871                        //track segment connected at A matches block 2, check D
872                        if (!(((TrackSegment) lt.getConnectD()).getBlockName().equals(protectedBlock.getUserName()))) {
873                            //track segment connected at D is not in block2, return diverging signal head at C
874                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
875                        } else {
876                            //A and D both in block 2, check turnout position to decide which signal head to return
877                            int state = lt.getTurnout().getKnownState();
878
879                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
880                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
881                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
882                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
883                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) { //diverging
884                                //(crossed
885
886                                //over)
887                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
888                            } else {
889                                //turnout state is UNKNOWN or INCONSISTENT
890                                log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
891                                        lt.getTurnout().getDisplayName());
892                                return null;
893                            }
894                        }
895                    }
896
897                    //track segment connected at A is not in block 2
898                    if ((((TrackSegment) lt.getConnectD()).getBlockName().equals(protectedBlock.getUserName()))) {
899                        //track segment connected at D is in block 2, return continuing signal head
900                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
901                    } else {
902                        //neither track segment is in block 2 - should never get here unless layout turnout is
903                        //only item in block 2
904                        if (!(lt.getBlockName().equals(protectedBlock.getUserName()))) {
905                            log.error("neither signal at C protects block {}, and turnout is not in block either",
906                                    protectedBlock.getDisplayName());
907                        }
908                        return null;
909                    }
910                } else {
911                    //check if track segments at D or A are in facing block (block 1)
912                    if (((TrackSegment) (lt.getConnectD())).getBlockName().equals(facingBlock.getUserName())) {
913                        //track segment connected at D matches block 1, check A
914                        if (!(((TrackSegment) lt.getConnectA()).getBlockName().equals(facingBlock.getUserName()))) {
915                            //track segment connected at A is not in block 2, return signal head at continuing end
916                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
917                        } else {
918                            //A and D both in block 1, check turnout position to decide which signal head to return
919                            int state = lt.getTurnout().getKnownState();
920
921                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
922                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
923                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
924                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
925                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) {
926                                //diverging, check for second head
927                                if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) {
928                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
929                                } else {
930                                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
931                                }
932                            } else {
933                                //turnout state is UNKNOWN or INCONSISTENT
934                                log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
935                                        lt.getTurnout().getDisplayName());
936                                return null;
937                            }
938                        }
939                    }
940
941                    //track segment connected at D is not in block 1
942                    if (((TrackSegment) lt.getConnectA()).getBlockName().equals(facingBlock.getUserName())) {
943                        //track segment connected at A is in block 1, return diverging signal head, check for second head
944                        if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) {
945                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
946                        } else {
947                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
948                        }
949                    } else {
950                        //neither track segment is in block 1 - should never get here unless layout turnout is
951                        //the only item in block 1
952                        if (!(lt.getBlockName().equals(facingBlock.getUserName()))) {
953                            log.error("no signal faces block {}, and turnout is not in block either",
954                                    facingBlock.getDisplayName());
955                        }
956                        return null;
957                    }
958                }
959            }
960
961            //not double crossover or RH crossover
962            if ((lt.getLinkType() == LayoutTurnout.LinkType.NO_LINK) && (lt.getContinuingSense() == Turnout.CLOSED)) {
963                if (facingIsBlock1) {
964                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
965                } else if (lt.getTurnoutType() == LayoutTurnout.TurnoutType.LH_XOVER) { //LH turnout - this is continuing track for D connection
966                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
967                } else {
968                    //RH, LH or WYE turnout, this is diverging track for A connection
969                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) { //there is no signal head at the throat for diverging
970                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
971                    } else { //there is a diverging head at the throat, return it
972                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
973                    }
974                }
975            } else if (lt.getLinkType() == LayoutTurnout.LinkType.NO_LINK) {
976                if (facingIsBlock1) {
977                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
978                } else {
979                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
980                }
981            } else if (lt.getLinkType() == LayoutTurnout.LinkType.THROAT_TO_THROAT) {
982                if (!facingIsBlock1) {
983                    //There are no signals at the throat of a THROAT_TO_THROAT
984                    return null;
985                }
986
987                //facing block is outside of the THROAT_TO_THROAT
988                if ((lt.getContinuingSense() == Turnout.CLOSED) && (lt.getSignalHead(LayoutTurnout.Geometry.POINTC2) == null)) {
989                    //there is only one signal head here - return it
990                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
991                } else if ((lt.getContinuingSense() == Turnout.THROWN) && (lt.getSignalHead(LayoutTurnout.Geometry.POINTB2) == null)) {
992                    //there is only one signal head here - return it
993                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
994                }
995
996                //There are two signals here get linked turnout and decide which to return from linked turnout state
997                LayoutTurnout tLinked = panel.getFinder().findLayoutTurnoutByTurnoutName(lt.getLinkedTurnoutName());
998                int state = tLinked.getTurnout().getKnownState();
999
1000                if (state == Turnout.CLOSED) {
1001                    if (lt.getContinuingSense() == Turnout.CLOSED) {
1002                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1003                    } else {
1004                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
1005                    }
1006                } else if (state == Turnout.THROWN) {
1007                    if (lt.getContinuingSense() == Turnout.CLOSED) {
1008                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC2);
1009                    } else {
1010                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
1011                    }
1012                } else {
1013                    //should never get here - linked turnout state is UNKNOWN or INCONSISTENT
1014                    log.error("Cannot choose signal head to return because turnout {} is in an UNKNOWN or INCONSISTENT state.",
1015                            tLinked.getTurnout().getDisplayName());
1016                    return null;
1017                }
1018            } else if (lt.getLinkType() == LayoutTurnout.LinkType.FIRST_3_WAY) {
1019                if (facingIsBlock1) {
1020                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1021                } else {
1022                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) {
1023                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA1);
1024                    } else {
1025                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTA2);
1026                    }
1027                }
1028            } else if (lt.getLinkType() == LayoutTurnout.LinkType.SECOND_3_WAY) {
1029                if (facingIsBlock1) {
1030                    if (lt.getContinuingSense() == Turnout.CLOSED) {
1031                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1032                    } else {
1033                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
1034                    }
1035                } else {
1036                    //signal is at the linked turnout - the throat of the 3-way turnout
1037                    LayoutTurnout tLinked = panel.getFinder().findLayoutTurnoutByTurnoutName(lt.getLinkedTurnoutName());
1038
1039                    if (lt.getContinuingSense() == Turnout.CLOSED) {
1040                        if (tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA3) == null) {
1041                            return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA1);
1042                        } else {
1043                            return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA3);
1044                        }
1045                    } else {
1046                        if (tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA2) == null) {
1047                            return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA1);
1048                        } else {
1049                            return tLinked.getSignalHead(LayoutTurnout.Geometry.POINTA2);
1050                        }
1051                    }
1052                }
1053            }
1054        }
1055
1056        if (cType == HitPointType.TURNOUT_D) {
1057            //block boundary is at D connectin of a crossover turnout
1058            lt = (LayoutTurnout) connected;
1059
1060            if (lt.getTurnoutType() == LayoutTurnout.TurnoutType.RH_XOVER) {
1061                //no diverging route possible, this is continuing track for C connection
1062                if (facingIsBlock1) {
1063                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
1064                } else {
1065                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1066                }
1067            }
1068
1069            if (facingIsBlock1) {
1070                if (lt.getSignalHead(LayoutTurnout.Geometry.POINTD2) == null) { //there is no signal head for diverging
1071                    return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
1072                } else {
1073                    //check if track segments at C or B are in protected block (block 2)
1074                    if (((TrackSegment) (lt.getConnectC())).getBlockName().equals(protectedBlock.getUserName())) {
1075                        //track segment connected at C matches block 2, check B
1076                        if (!(((TrackSegment) lt.getConnectB()).getBlockName().equals(protectedBlock.getUserName()))) {
1077                            //track segment connected at B is not in block2, return continuing signal head at D
1078                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
1079                        } else {
1080                            //C and B both in block2, check turnout position to decide which signal head to return
1081                            int state = lt.getTurnout().getKnownState();
1082
1083                            if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
1084                                    || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
1085                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTD1);
1086                            } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
1087                                    || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) { //diverging
1088                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTD2);
1089                            } else {
1090                                //turnout state is UNKNOWN or INCONSISTENT
1091                                log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
1092                                        lt.getTurnout().getDisplayName());
1093                                return null;
1094                            }
1095                        }
1096                    }
1097
1098                    //track segment connected at C is not in block 2
1099                    if ((((TrackSegment) lt.getConnectB()).getBlockName().equals(protectedBlock.getUserName()))) {
1100                        //track segment connected at B is in block 2, return diverging signal head
1101                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTD2);
1102                    } else {
1103                        //neither track segment is in block 2 - should never get here unless layout turnout is
1104                        //the only item in block 2
1105                        if (!(lt.getBlockName().equals(protectedBlock.getUserName()))) {
1106                            log.error("neither signal at D protects block {}, and turnout is not in block either",
1107                                    protectedBlock.getDisplayName());
1108                        }
1109                        return null;
1110                    }
1111                }
1112            } else {
1113                //check if track segments at C or B are in facing block (block 1)
1114                if (((TrackSegment) (lt.getConnectC())).getBlockName().equals(facingBlock.getUserName())) {
1115                    //track segment connected at C matches block 1, check B
1116                    if (!(((TrackSegment) lt.getConnectB()).getBlockName().equals(facingBlock.getUserName()))) {
1117                        //track segment connected at B is not in block 2, return signal head at continuing end
1118                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1119                    } else {
1120                        //C and B both in block 1, check turnout position to decide which signal head to return
1121                        int state = lt.getTurnout().getKnownState();
1122
1123                        if (((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.CLOSED))
1124                                || ((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.THROWN))) { //continuing
1125                            return lt.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1126                        } else if (((state == Turnout.THROWN) && (lt.getContinuingSense() == Turnout.CLOSED))
1127                                || ((state == Turnout.CLOSED) && (lt.getContinuingSense() == Turnout.THROWN))) {
1128                            //diverging, check for second head
1129                            if (lt.getSignalHead(LayoutTurnout.Geometry.POINTB2) == null) {
1130                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
1131                            } else {
1132                                return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
1133                            }
1134                        } else {
1135                            //turnout state is UNKNOWN or INCONSISTENT
1136                            log.error("Cannot choose signal head because turnout {} is in an UNKNOWN or INCONSISTENT state.",
1137                                    lt.getTurnout().getDisplayName());
1138                            return null;
1139                        }
1140                    }
1141                }
1142
1143                //track segment connected at C is not in block 1
1144                if (((TrackSegment) lt.getConnectB()).getBlockName().equals(facingBlock.getUserName())) {
1145                    //track segment connected at B is in block 1, return diverging signal head, check for second head
1146                    if (lt.getSignalHead(LayoutTurnout.Geometry.POINTB2) == null) {
1147                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB1);
1148                    } else {
1149                        return lt.getSignalHead(LayoutTurnout.Geometry.POINTB2);
1150                    }
1151                } else {
1152                    //neither track segment is in block 1 - should never get here unless layout turnout is
1153                    //the only item in block 1
1154                    if (!(lt.getBlockName().equals(facingBlock.getUserName()))) {
1155                        log.error("no signal faces block {}, and turnout is not in block either",
1156                                facingBlock.getDisplayName());
1157                    }
1158                    return null;
1159                }
1160            }
1161        }
1162
1163        if (HitPointType.isSlipHitType(cType)) {
1164            if (!facingIsBlock1) {
1165                return null;
1166            }
1167
1168            LayoutSlip ls = (LayoutSlip) connected;
1169
1170            switch (cType) {
1171                case SLIP_A: {
1172                    if (ls.getSlipState() == LayoutSlip.STATE_AD) {
1173                        return ls.getSignalHead(LayoutTurnout.Geometry.POINTA2);
1174                    } else {
1175                        return ls.getSignalHead(LayoutTurnout.Geometry.POINTA1);
1176                    }
1177                }
1178
1179                case SLIP_B: {
1180                    if (ls.getTurnoutType() == LayoutSlip.TurnoutType.DOUBLE_SLIP) {
1181                        if (ls.getSlipState() == LayoutSlip.STATE_BC) {
1182                            return ls.getSignalHead(LayoutTurnout.Geometry.POINTB2);
1183                        } else {
1184                            return ls.getSignalHead(LayoutTurnout.Geometry.POINTB1);
1185                        }
1186                    } else {
1187                        return ls.getSignalHead(LayoutTurnout.Geometry.POINTB1);
1188                    }
1189                }
1190
1191                case SLIP_C: {
1192                    if (ls.getTurnoutType() == LayoutSlip.TurnoutType.DOUBLE_SLIP) {
1193                        if (ls.getSlipState() == LayoutSlip.STATE_BC) {
1194                            return ls.getSignalHead(LayoutTurnout.Geometry.POINTC2);
1195                        } else {
1196                            return ls.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1197                        }
1198                    } else {
1199                        return ls.getSignalHead(LayoutTurnout.Geometry.POINTC1);
1200                    }
1201                }
1202
1203                case SLIP_D: {
1204                    if (ls.getSlipState() == LayoutSlip.STATE_AD) {
1205                        return ls.getSignalHead(LayoutTurnout.Geometry.POINTD2);
1206                    } else {
1207                        return ls.getSignalHead(LayoutTurnout.Geometry.POINTD1);
1208                    }
1209                }
1210
1211                default: {
1212                    break;
1213                }
1214            } //switch
1215        }
1216
1217        //block boundary must be at a level crossing
1218        if (!HitPointType.isLevelXingHitType(cType)) {
1219            log.error("{} {} Block Boundary not identified correctly - Blocks {}, {}",
1220                    cType, connected, facingBlock.getDisplayName(), protectedBlock.getDisplayName());
1221
1222            return null;
1223        }
1224        LevelXing xing = (LevelXing) connected;
1225
1226        switch (cType) {
1227            case LEVEL_XING_A: {
1228                //block boundary is at the A connection of a level crossing
1229                if (facingIsBlock1) {
1230                    return xing.getSignalHead(LevelXing.Geometry.POINTA);
1231                } else {
1232                    return xing.getSignalHead(LevelXing.Geometry.POINTC);
1233                }
1234            }
1235
1236            case LEVEL_XING_B: {
1237                //block boundary is at the B connection of a level crossing
1238                if (facingIsBlock1) {
1239                    return xing.getSignalHead(LevelXing.Geometry.POINTB);
1240                } else {
1241                    return xing.getSignalHead(LevelXing.Geometry.POINTD);
1242                }
1243            }
1244
1245            case LEVEL_XING_C: {
1246                //block boundary is at the C connection of a level crossing
1247                if (facingIsBlock1) {
1248                    return xing.getSignalHead(LevelXing.Geometry.POINTC);
1249                } else {
1250                    return xing.getSignalHead(LevelXing.Geometry.POINTA);
1251                }
1252            }
1253
1254            case LEVEL_XING_D: {
1255                //block boundary is at the D connection of a level crossing
1256                if (facingIsBlock1) {
1257                    return xing.getSignalHead(LevelXing.Geometry.POINTD);
1258                } else {
1259                    return xing.getSignalHead(LevelXing.Geometry.POINTB);
1260                }
1261            }
1262
1263            default: {
1264                break;
1265            }
1266        }
1267        return null;
1268    }
1269
1270    /**
1271     * Get the named bean of either a Sensor or signalmast facing into a
1272     * specified Block from a specified protected Block.
1273     * @param facingBlock the facing block.
1274     * @param panel the main layout editor.
1275     * @return The assigned sensor or signal mast as a named bean
1276     */
1277    @CheckReturnValue
1278    @CheckForNull
1279    public NamedBean getNamedBeanAtEndBumper(
1280            @CheckForNull Block facingBlock,
1281            @CheckForNull LayoutEditor panel) {
1282        NamedBean bean = getSignalMastAtEndBumper(facingBlock, panel);
1283
1284        if (bean != null) {
1285            return bean;
1286        } else {
1287            return getSensorAtEndBumper(facingBlock, panel);
1288        }
1289    }
1290
1291    /**
1292     * Get a Signal Mast that is assigned to a block which has an end bumper at
1293     * one end.
1294     * @param facingBlock the facing block.
1295     * @param panel the main layout editor.
1296     * @return the signal mast.
1297     */
1298    @CheckReturnValue
1299    @CheckForNull
1300    public SignalMast getSignalMastAtEndBumper(
1301            @CheckForNull Block facingBlock,
1302            @CheckForNull LayoutEditor panel) {
1303        if (facingBlock == null) {
1304            log.error("null block in call to getFacingSignalMast");
1305            return null;
1306        }
1307        String facingBlockName = facingBlock.getUserName();
1308        if ((facingBlockName == null) || facingBlockName.isEmpty()) {
1309            log.error("facing block has no user name");
1310            return null;
1311        }
1312
1313        LayoutBlock fLayoutBlock = getByUserName(facingBlockName);
1314        if (fLayoutBlock == null) {
1315            log.error("Block {} is not on a Layout Editor panel.", facingBlock.getDisplayName());
1316
1317            return null;
1318        }
1319
1320        if (panel == null) {
1321            panel = fLayoutBlock.getMaxConnectedPanel();
1322        }
1323
1324        for (TrackSegment t : panel.getTrackSegments()) {
1325            if (t.getLayoutBlock() == fLayoutBlock) {
1326                PositionablePoint p;
1327
1328                if (t.getType1() == HitPointType.POS_POINT) {
1329                    p = (PositionablePoint) t.getConnect1();
1330
1331                    if (p.getType() == PositionablePoint.PointType.END_BUMPER) {
1332                        if (p.getEastBoundSignalMast() != null) {
1333                            return p.getEastBoundSignalMast();
1334                        }
1335
1336                        if (p.getWestBoundSignalMast() != null) {
1337                            return p.getWestBoundSignalMast();
1338                        }
1339                    }
1340                }
1341
1342                if (t.getType2() == HitPointType.POS_POINT) {
1343                    p = (PositionablePoint) t.getConnect2();
1344
1345                    if (p.getType() == PositionablePoint.PointType.END_BUMPER) {
1346                        if (p.getEastBoundSignalMast() != null) {
1347                            return p.getEastBoundSignalMast();
1348                        }
1349
1350                        if (p.getWestBoundSignalMast() != null) {
1351                            return p.getWestBoundSignalMast();
1352                        }
1353                    }
1354                }
1355            }
1356        }
1357        return null;
1358    }
1359
1360    /**
1361     * Get a Sensor facing into a specific Block. This is used for Blocks that
1362     * have an end bumper at one end.
1363     * @param facingBlock the facing block.
1364     * @param panel the main layout editor.
1365     * @return the facing sensor.
1366     */
1367    @CheckReturnValue
1368    @CheckForNull
1369    public Sensor getSensorAtEndBumper(
1370            @CheckForNull Block facingBlock,
1371            @CheckForNull LayoutEditor panel) {
1372        if (facingBlock == null) {
1373            log.error("null block in call to getFacingSensor");
1374            return null;
1375        }
1376
1377        String facingBlockName = facingBlock.getUserName();
1378        if ((facingBlockName == null) || (facingBlockName.isEmpty())) {
1379            log.error("Block {} has no user name.", facingBlock.getDisplayName());
1380            return null;
1381        }
1382        LayoutBlock fLayoutBlock = getByUserName(facingBlockName);
1383        if (fLayoutBlock == null) {
1384            log.error("Block {} is not on a Layout Editor panel.", facingBlock.getDisplayName());
1385
1386            return null;
1387        }
1388
1389        if (panel == null) {
1390            panel = fLayoutBlock.getMaxConnectedPanel();
1391        }
1392
1393        for (TrackSegment t : panel.getTrackSegments()) {
1394            if (t.getLayoutBlock() == fLayoutBlock) {
1395                PositionablePoint p;
1396
1397                if (t.getType1() == HitPointType.POS_POINT) {
1398                    p = (PositionablePoint) t.getConnect1();
1399
1400                    if (p.getType() == PositionablePoint.PointType.END_BUMPER) {
1401                        if (p.getEastBoundSensor() != null) {
1402                            return p.getEastBoundSensor();
1403                        }
1404
1405                        if (p.getWestBoundSensor() != null) {
1406                            return p.getWestBoundSensor();
1407                        }
1408                    }
1409                }
1410
1411                if (t.getType2() == HitPointType.POS_POINT) {
1412                    p = (PositionablePoint) t.getConnect2();
1413
1414                    if (p.getType() == PositionablePoint.PointType.END_BUMPER) {
1415                        if (p.getEastBoundSensor() != null) {
1416                            return p.getEastBoundSensor();
1417                        }
1418
1419                        if (p.getWestBoundSensor() != null) {
1420                            return p.getWestBoundSensor();
1421                        }
1422                    }
1423                }
1424            }
1425        }
1426        return null;
1427    }
1428
1429    /**
1430     * Get the named bean of either a Sensor or signalmast facing into a
1431     * specified Block from a specified protected Block.
1432     * @param facingBlock the facing block.
1433     * @param protectedBlock the protected block.
1434     * @param panel the main layout editor.
1435     * @return The assigned sensor or signal mast as a named bean
1436     */
1437    @CheckReturnValue
1438    @CheckForNull
1439    public NamedBean getFacingNamedBean(@CheckForNull Block facingBlock,
1440            @CheckForNull Block protectedBlock,
1441            @CheckForNull LayoutEditor panel) {
1442        NamedBean bean = getFacingBean(facingBlock, protectedBlock, panel, SignalMast.class);
1443
1444        if (bean != null) {
1445            return bean;
1446        }
1447        bean = getFacingBean(facingBlock, protectedBlock, panel, Sensor.class);
1448
1449        if (bean != null) {
1450            return bean;
1451        }
1452        return getFacingSignalHead(facingBlock, protectedBlock);
1453    }
1454
1455    @CheckReturnValue
1456    @CheckForNull
1457    public SignalMast getFacingSignalMast(
1458            @Nonnull Block facingBlock,
1459            @CheckForNull Block protectedBlock) {
1460        return getFacingSignalMast(facingBlock, protectedBlock, null);
1461    }
1462
1463    /**
1464     * Get the Signal Mast facing into a specified Block from a specified
1465     * protected Block.
1466     *
1467     * @param facingBlock the facing block.
1468     * @param protectedBlock the protected block.
1469     * @param panel the main layout editor.
1470     * @return The assigned signalMast.
1471     */
1472    @CheckReturnValue
1473    @CheckForNull
1474    public SignalMast getFacingSignalMast(
1475            @Nonnull Block facingBlock,
1476            @CheckForNull Block protectedBlock,
1477            @CheckForNull LayoutEditor panel) {
1478        log.debug("calling getFacingMast on block '{}'", facingBlock.getDisplayName());
1479        return (SignalMast) getFacingBean(facingBlock, protectedBlock, panel, SignalMast.class);
1480    }
1481
1482    /**
1483     * Get the Sensor facing into a specified Block from a specified protected
1484     * Block.
1485     * @param facingBlock the facing block.
1486     * @param protectedBlock the protected block.
1487     * @param panel the main layout editor.
1488     * @return The assigned sensor
1489     */
1490    @CheckReturnValue
1491    @CheckForNull
1492    public Sensor getFacingSensor(@CheckForNull Block facingBlock,
1493            @CheckForNull Block protectedBlock,
1494            @CheckForNull LayoutEditor panel) {
1495        return (Sensor) getFacingBean(facingBlock, protectedBlock, panel, Sensor.class);
1496    }
1497
1498    /**
1499     * Get a facing bean into a specified Block from a specified protected
1500     * Block.
1501     *
1502     * @param facingBlock the facing block.
1503     * @param protectedBlock the protected block.
1504     * @param panel the layout editor panel the block is assigned, if null then
1505     *              the maximum connected panel of the facing block is used
1506     * @param T     The class of the item that we are looking for, either
1507     *              SignalMast or Sensor
1508     * @return The assigned sensor.
1509     */
1510    @CheckReturnValue
1511    @CheckForNull
1512    public NamedBean getFacingBean(@CheckForNull Block facingBlock,
1513            @CheckForNull Block protectedBlock,
1514            @CheckForNull LayoutEditor panel, Class< ?> T) {
1515        //check input
1516        if ((facingBlock == null) || (protectedBlock == null)) {
1517            log.error("null block in call to getFacingSignalMast");
1518            return null;
1519        }
1520
1521        // ----- Begin Turntable Boundary Check -----
1522        for (LayoutEditor ed : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
1523            for (LayoutTurntable turntable : ed.getLayoutTurntables()) {
1524                LayoutBlock turntableBlock = turntable.getLayoutBlock();
1525                if (turntableBlock == null) continue;
1526
1527                // Check if one of the blocks is the turntable's block
1528                if (turntableBlock.getBlock() == facingBlock || turntableBlock.getBlock() == protectedBlock) {
1529                    Block otherBlock = (turntableBlock.getBlock() == facingBlock) ? protectedBlock : facingBlock;
1530
1531                    for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
1532                        TrackSegment connectedTrack = ray.getConnect();
1533                        if (connectedTrack != null && connectedTrack.getLayoutBlock() != null && connectedTrack.getLayoutBlock().getBlock() == otherBlock) {
1534                            // We found the correct ray. Now find the mast based on direction.
1535                            if (turntableBlock.getBlock() == protectedBlock) {
1536                                // Path 2: Moving from Ray block INTO Turntable. The facing mast is the Approach Mast.
1537                                if (T.equals(SignalMast.class)) {
1538                                    return ray.getApproachMast();
1539                                }
1540                            } else { // turntableBlock.getBlock() == facingBlock
1541                                // Path 1: Moving FROM Turntable out to Ray block. The facing mast is the exit mast for that ray.
1542                                if (T.equals(SignalMast.class)) {
1543                                    SignalMast exitMast = turntable.getExitSignalMast();
1544                                    // This is the mast protecting the path from the turntable to the ray.
1545                                    return exitMast;
1546                                }
1547                            }
1548                        }
1549                    }
1550                }
1551            }
1552        }
1553        // ----- Begin Traverser Boundary Check -----
1554        for (LayoutEditor ed : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
1555            for (LayoutTraverser traverser : ed.getLayoutTraversers()) {
1556                LayoutBlock traverserBlock = traverser.getLayoutBlock();
1557                if (traverserBlock == null) continue;
1558
1559                // Check if one of the blocks is the traverser's block
1560                if (traverserBlock.getBlock() == facingBlock || traverserBlock.getBlock() == protectedBlock) {
1561                    Block otherBlock = (traverserBlock.getBlock() == facingBlock) ? protectedBlock : facingBlock;
1562
1563                    for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
1564                        TrackSegment connectedTrack = slot.getConnect();
1565                        if (connectedTrack != null && connectedTrack.getLayoutBlock() != null && connectedTrack.getLayoutBlock().getBlock() == otherBlock) {
1566                            // We found the correct slot. Now find the mast based on direction.
1567                            if (traverserBlock.getBlock() == protectedBlock) {
1568                                // Path 2: Moving from Slot block INTO Traverser. The facing mast is the Approach Mast.
1569                                if (T.equals(SignalMast.class)) {
1570                                    return slot.getApproachMast();
1571                                }
1572                            } else { // traverserBlock.getBlock() == facingBlock
1573                                // Path 1: Moving FROM Traverser out to Slot block. The facing mast is the exit mast for that slot.
1574                                if (T.equals(SignalMast.class)) {
1575                                    SignalMast exitMast = traverser.getExitSignalMast();
1576                                    // This is the mast protecting the path from the traverser to the slot.
1577                                    return exitMast;
1578                                }
1579                            }
1580                        }
1581                    }
1582                }
1583            }
1584        }
1585        // ----- End Traverser Boundary Check -----
1586
1587        if (!T.equals(SignalMast.class) && !T.equals(Sensor.class)) {
1588            log.error("Incorrect class type called, must be either SignalMast or Sensor");
1589
1590            return null;
1591        }
1592
1593        if (log.isDebugEnabled()) {
1594            log.debug("find signal mast between facing {} ({}) - protected {} ({})",
1595                    facingBlock.getDisplayName(), facingBlock.getDisplayName(),
1596                    protectedBlock.getDisplayName(), protectedBlock.getDisplayName());
1597        }
1598
1599        //non-null - check if input corresponds to Blocks in a Layout Editor panel.
1600        String facingBlockName = facingBlock.getUserName();
1601        if ((facingBlockName == null) || facingBlockName.isEmpty()) {
1602            log.error("facing block has no user name");
1603            return null;
1604        }
1605        LayoutBlock fLayoutBlock = getByUserName(facingBlockName);
1606        String protectedBlockName = protectedBlock.getUserName();
1607        LayoutBlock pLayoutBlock = (protectedBlockName == null) ? null : getByUserName(protectedBlockName);
1608        if ((fLayoutBlock == null) || (pLayoutBlock == null)) {
1609            if (fLayoutBlock == null) {
1610                log.error("Block {} is not on a Layout Editor panel.", facingBlock.getDisplayName());
1611            }
1612
1613            if (pLayoutBlock == null) {
1614                log.error("Block {} is not on a Layout Editor panel.", protectedBlock.getDisplayName());
1615            }
1616            return null;
1617        }
1618
1619        //input has corresponding LayoutBlocks - does it correspond to a block boundary?
1620        if (panel == null) {
1621            panel = fLayoutBlock.getMaxConnectedPanel();
1622        }
1623        List<LayoutConnectivity> c = panel.getLEAuxTools().getConnectivityList(fLayoutBlock);
1624        LayoutConnectivity lc = null;
1625        int i = 0;
1626        boolean facingIsBlock1 = true;
1627
1628        while ((i < c.size()) && (lc == null)) {
1629            LayoutConnectivity tlc = c.get(i);
1630
1631            if ((tlc.getBlock1() == fLayoutBlock) && (tlc.getBlock2() == pLayoutBlock)) {
1632                lc = tlc;
1633            } else if ((tlc.getBlock1() == pLayoutBlock) && (tlc.getBlock2() == fLayoutBlock)) {
1634                lc = tlc;
1635                facingIsBlock1 = false;
1636            }
1637            i++;
1638        }
1639
1640        if (lc == null) {
1641            PositionablePoint p = panel.getFinder().findPositionableLinkPoint(fLayoutBlock);
1642
1643            if (p == null) {
1644                p = panel.getFinder().findPositionableLinkPoint(pLayoutBlock);
1645            }
1646
1647            if ((p != null) && (p.getLinkedEditor() != null)) {
1648                return getFacingBean(facingBlock, protectedBlock, p.getLinkedEditor(), T);
1649            }
1650            log.debug("Block {} is not connected to Block {} on panel {}", facingBlock.getDisplayName(),
1651                    protectedBlock.getDisplayName(), panel.getLayoutName());
1652
1653            return null;
1654        }
1655        LayoutTurnout lt;
1656        LayoutTrack connected = lc.getConnectedObject();
1657
1658        TrackSegment tr = lc.getTrackSegment();
1659        HitPointType cType = lc.getConnectedType();
1660
1661        if (connected == null) {
1662            if (lc.getXover() != null) {
1663                if (lc.getXoverBoundaryType() == LayoutConnectivity.XOVER_BOUNDARY_AB) {
1664                    if (fLayoutBlock == lc.getXover().getLayoutBlock()) {
1665                        cType = HitPointType.TURNOUT_A;
1666                    } else {
1667                        cType = HitPointType.TURNOUT_B;
1668                    }
1669                    connected = lc.getXover();
1670                } else if (lc.getXoverBoundaryType() == LayoutConnectivity.XOVER_BOUNDARY_CD) {
1671                    if (fLayoutBlock == lc.getXover().getLayoutBlockC()) {
1672                        cType = HitPointType.TURNOUT_C;
1673                    } else {
1674                        cType = HitPointType.TURNOUT_D;
1675                    }
1676                    connected = lc.getXover();
1677                } else if (lc.getXoverBoundaryType() == LayoutConnectivity.XOVER_BOUNDARY_AC) {
1678                    if (fLayoutBlock == lc.getXover().getLayoutBlock()) {
1679                        cType = HitPointType.TURNOUT_A;
1680                    } else {
1681                        cType = HitPointType.TURNOUT_C;
1682                    }
1683                    connected = lc.getXover();
1684                } else if (lc.getXoverBoundaryType() == LayoutConnectivity.XOVER_BOUNDARY_BD) {
1685                    if (fLayoutBlock == lc.getXover().getLayoutBlockB()) {
1686                        cType = HitPointType.TURNOUT_B;
1687                    } else {
1688                        cType = HitPointType.TURNOUT_D;
1689                    }
1690                    connected = lc.getXover();
1691                }
1692            }
1693        }
1694
1695        if (connected == null) {
1696            log.error("No connectivity object found between Blocks {}, {} {}", facingBlock.getDisplayName(),
1697                    protectedBlock.getDisplayName(), cType);
1698
1699            return null;
1700        }
1701
1702        if (cType == HitPointType.TRACK) {
1703            //block boundary is at an Anchor Point
1704            PositionablePoint p = panel.getFinder().findPositionablePointAtTrackSegments(tr, (TrackSegment) connected);
1705
1706            boolean block1IsWestEnd = LayoutEditorTools.isAtWestEndOfAnchor(panel, tr, p);
1707            log.debug("Track is west end? {}", block1IsWestEnd);
1708            if ((block1IsWestEnd && facingIsBlock1) || (!block1IsWestEnd && !facingIsBlock1)) {
1709                //block1 is on the west (north) end of the block boundary
1710                if (T.equals(SignalMast.class)) {
1711                    return p.getEastBoundSignalMast();
1712                } else if (T.equals(Sensor.class)) {
1713                    return p.getEastBoundSensor();
1714                }
1715            } else {
1716                if (T.equals(SignalMast.class)) {
1717                    return p.getWestBoundSignalMast();
1718                } else if (T.equals(Sensor.class)) {
1719                    return p.getWestBoundSensor();
1720                }
1721            }
1722        }
1723
1724        if (cType == HitPointType.TURNOUT_A) {
1725            lt = (LayoutTurnout) connected;
1726
1727            if ((lt.getLinkType() == LayoutTurnout.LinkType.NO_LINK) || (lt.getLinkType() == LayoutTurnout.LinkType.FIRST_3_WAY)) {
1728                if ((T.equals(SignalMast.class) && (lt.getSignalAMast() != null))
1729                        || (T.equals(Sensor.class) && (lt.getSensorA() != null))) {
1730                    if (tr == null) {
1731                        if (lt.getConnectA() instanceof TrackSegment) {
1732                            TrackSegment t = (TrackSegment) lt.getConnectA();
1733
1734                            if ((t.getLayoutBlock() != null) && (t.getLayoutBlock() == lt.getLayoutBlock())) {
1735                                if (T.equals(SignalMast.class)) {
1736                                    return lt.getSignalAMast();
1737                                } else if (T.equals(Sensor.class)) {
1738                                    return lt.getSensorA();
1739                                }
1740                            }
1741                        }
1742                    } else if (tr.getLayoutBlock().getBlock() == facingBlock) {
1743                        if (T.equals(SignalMast.class)) {
1744                            return lt.getSignalAMast();
1745                        } else if (T.equals(Sensor.class)) {
1746                            return lt.getSensorA();
1747                        }
1748                    }
1749                }
1750            }
1751            return null;
1752        }
1753
1754        if (cType == HitPointType.TURNOUT_B) {
1755            lt = (LayoutTurnout) connected;
1756
1757            if ((T.equals(SignalMast.class) && (lt.getSignalBMast() != null))
1758                    || (T.equals(Sensor.class) && (lt.getSensorB() != null))) {
1759                if (tr == null) {
1760                    if (lt.getConnectB() instanceof TrackSegment) {
1761                        TrackSegment t = (TrackSegment) lt.getConnectB();
1762
1763                        if ((t.getLayoutBlock() != null) && (t.getLayoutBlock() == lt.getLayoutBlockB())) {
1764                            if (T.equals(SignalMast.class)) {
1765                                return lt.getSignalBMast();
1766                            } else if (T.equals(Sensor.class)) {
1767                                return lt.getSensorB();
1768                            }
1769                        }
1770                    }
1771                } else if (tr.getLayoutBlock().getBlock() == facingBlock) {
1772                    if (T.equals(SignalMast.class)) {
1773                        return lt.getSignalBMast();
1774                    } else if (T.equals(Sensor.class)) {
1775                        return lt.getSensorB();
1776                    }
1777                }
1778            }
1779            return null;
1780        }
1781
1782        if (cType == HitPointType.TURNOUT_C) {
1783            lt = (LayoutTurnout) connected;
1784
1785            if ((T.equals(SignalMast.class) && (lt.getSignalCMast() != null))
1786                    || (T.equals(Sensor.class) && (lt.getSensorC() != null))) {
1787                if (tr == null) {
1788                    if (lt.getConnectC() instanceof TrackSegment) {
1789                        TrackSegment t = (TrackSegment) lt.getConnectC();
1790
1791                        if ((t.getLayoutBlock() != null) && (t.getLayoutBlock() == lt.getLayoutBlockC())) {
1792                            if (T.equals(SignalMast.class)) {
1793                                return lt.getSignalCMast();
1794                            } else if (T.equals(Sensor.class)) {
1795                                return lt.getSensorC();
1796                            }
1797                        }
1798                    }
1799                } else if (tr.getLayoutBlock().getBlock() == facingBlock) {
1800                    if (T.equals(SignalMast.class)) {
1801                        return lt.getSignalCMast();
1802                    } else if (T.equals(Sensor.class)) {
1803                        return lt.getSensorC();
1804                    }
1805                }
1806            }
1807            return null;
1808        }
1809
1810        if (cType == HitPointType.TURNOUT_D) {
1811            lt = (LayoutTurnout) connected;
1812
1813            if ((T.equals(SignalMast.class) && (lt.getSignalDMast() != null))
1814                    || (T.equals(Sensor.class) && (lt.getSensorD() != null))) {
1815                if (tr == null) {
1816                    if (lt.getConnectD() instanceof TrackSegment) {
1817                        TrackSegment t = (TrackSegment) lt.getConnectD();
1818
1819                        if ((t.getLayoutBlock() != null) && (t.getLayoutBlock() == lt.getLayoutBlockD())) {
1820                            if (T.equals(SignalMast.class)) {
1821                                return lt.getSignalDMast();
1822                            } else if (T.equals(Sensor.class)) {
1823                                return lt.getSensorD();
1824                            }
1825                        }
1826                    }
1827                } else if (tr.getLayoutBlock().getBlock() == facingBlock) {
1828                    if (T.equals(SignalMast.class)) {
1829                        return lt.getSignalDMast();
1830                    } else if (T.equals(Sensor.class)) {
1831                        return lt.getSensorD();
1832                    }
1833                }
1834            }
1835            return null;
1836        }
1837
1838        if ((tr == null) || (tr.getLayoutBlock().getBlock() != facingBlock)) {
1839            return null;
1840        }
1841
1842        if (HitPointType.isSlipHitType(cType)) {
1843            LayoutSlip ls = (LayoutSlip) connected;
1844
1845            if (cType == HitPointType.SLIP_A) {
1846                if (T.equals(SignalMast.class)) {
1847                    return ls.getSignalAMast();
1848                } else if (T.equals(Sensor.class)) {
1849                    return ls.getSensorA();
1850                }
1851            }
1852
1853            if (cType == HitPointType.SLIP_B) {
1854                if (T.equals(SignalMast.class)) {
1855                    return ls.getSignalBMast();
1856                } else if (T.equals(Sensor.class)) {
1857                    return ls.getSensorB();
1858                }
1859            }
1860
1861            if (cType == HitPointType.SLIP_C) {
1862                if (T.equals(SignalMast.class)) {
1863                    return ls.getSignalCMast();
1864                } else if (T.equals(Sensor.class)) {
1865                    return ls.getSensorC();
1866                }
1867            }
1868
1869            if (cType == HitPointType.SLIP_D) {
1870                if (T.equals(SignalMast.class)) {
1871                    return ls.getSignalDMast();
1872                } else if (T.equals(Sensor.class)) {
1873                    return ls.getSensorD();
1874                }
1875            }
1876        }
1877
1878        if (!HitPointType.isLevelXingHitType(cType)) {
1879            log.error("Block Boundary not identified correctly - Blocks {}, {}", facingBlock.getDisplayName(),
1880                    protectedBlock.getDisplayName());
1881
1882            return null;
1883        }
1884
1885        /* We don't allow signal masts on the block outward facing from the level
1886        xing, nor do we consider the signal mast, that is protecting the in block on the xing */
1887        LevelXing xing = (LevelXing) connected;
1888
1889        if (cType == HitPointType.LEVEL_XING_A) {
1890            //block boundary is at the A connection of a level crossing
1891            if (T.equals(SignalMast.class)) {
1892                return xing.getSignalAMast();
1893            } else if (T.equals(Sensor.class)) {
1894                return xing.getSensorA();
1895            }
1896        }
1897
1898        if (cType == HitPointType.LEVEL_XING_B) {
1899            //block boundary is at the B connection of a level crossing
1900            if (T.equals(SignalMast.class)) {
1901                return xing.getSignalBMast();
1902            } else if (T.equals(Sensor.class)) {
1903                return xing.getSensorB();
1904            }
1905        }
1906
1907        if (cType == HitPointType.LEVEL_XING_C) {
1908            //block boundary is at the C connection of a level crossing
1909            if (T.equals(SignalMast.class)) {
1910                return xing.getSignalCMast();
1911            } else if (T.equals(Sensor.class)) {
1912                return xing.getSensorC();
1913            }
1914        }
1915
1916        if (cType == HitPointType.LEVEL_XING_D) {
1917            if (T.equals(SignalMast.class)) {
1918                return xing.getSignalDMast();
1919            } else if (T.equals(Sensor.class)) {
1920                return xing.getSensorD();
1921            }
1922        }
1923        return null;
1924    } //getFacingBean
1925
1926    /**
1927     * In the first instance get a Signal Mast or if none exists a Signal Head
1928     * for a given facing block and protected block combination. See
1929     * #getFacingSignalMast() and #getFacingSignalHead() as to how they deal
1930     * with what each returns.
1931     * @param facingBlock the facing block to search for.
1932     * @param protectedBlock the protected block to search for.
1933     *
1934     * @return either a signalMast or signalHead
1935     */
1936    @CheckReturnValue
1937    @CheckForNull
1938    public Object getFacingSignalObject(
1939            @Nonnull Block facingBlock,
1940            @CheckForNull Block protectedBlock) {
1941        Object sig = getFacingSignalMast(facingBlock, protectedBlock, null);
1942
1943        if (sig != null) {
1944            return sig;
1945        }
1946        sig = getFacingSignalHead(facingBlock, protectedBlock);
1947        return sig;
1948    }
1949
1950    /**
1951     * Get the block that a given bean object (Sensor, SignalMast or SignalHead)
1952     * is protecting.
1953     *
1954     * @param nb    NamedBean
1955     * @param panel panel that this bean is on
1956     * @return The block that the bean object is facing
1957     */
1958    @CheckReturnValue
1959    @CheckForNull
1960    public LayoutBlock getProtectedBlockByNamedBean(
1961            @CheckForNull NamedBean nb,
1962            @CheckForNull LayoutEditor panel) {
1963        if (nb instanceof SignalHead) {
1964            return getProtectedBlock((SignalHead) nb, panel);
1965        }
1966        List<LayoutBlock> proBlocks = getProtectingBlocksByBean(nb, panel);
1967
1968        if (proBlocks.isEmpty()) {
1969            return null;
1970        }
1971        return proBlocks.get(0);
1972    } //getProtectedBlockByNamedBean
1973
1974    @CheckReturnValue
1975    @Nonnull
1976    public List<LayoutBlock> getProtectingBlocksByNamedBean(
1977            @CheckForNull NamedBean nb,
1978            @CheckForNull LayoutEditor panel) {
1979        ArrayList<LayoutBlock> ret = new ArrayList<>();
1980
1981        if (nb instanceof SignalHead) {
1982            ret.add(getProtectedBlock((SignalHead) nb, panel));
1983            return ret;
1984        }
1985        return getProtectingBlocksByBean(nb, panel);
1986    }
1987
1988    /**
1989     * If the panel variable is null, search all LE panels. This was added to
1990     * support multi panel entry/exit.
1991     *
1992     * @param bean  The sensor, mast or head to be located.
1993     * @param panel The panel to search. If null, search all LE panels.
1994     * @return a list of protected layout blocks.
1995     */
1996    @Nonnull
1997    private List<LayoutBlock> getProtectingBlocksByBean(
1998            @CheckForNull NamedBean bean,
1999            @CheckForNull LayoutEditor panel) {
2000        if (panel == null) {
2001            Set<LayoutEditor> panels = InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class);
2002            List<LayoutBlock> protectingBlocks = new ArrayList<>();
2003            for (LayoutEditor p : panels) {
2004                protectingBlocks = getProtectingBlocksByBeanByPanel(bean, p);
2005                if (!protectingBlocks.isEmpty()) {
2006                    break;
2007                }
2008            }
2009            return protectingBlocks;
2010        } else {
2011            return getProtectingBlocksByBeanByPanel(bean, panel);
2012        }
2013    }
2014
2015    @Nonnull
2016    private List<LayoutBlock> getProtectingBlocksByBeanByPanel(
2017            @CheckForNull NamedBean bean,
2018            @Nonnull LayoutEditor panel) {
2019        List<LayoutBlock> protectingBlocks = new ArrayList<>();
2020
2021        // Check for turntable approach masts first, as they are a special case. Ignore NX sensor beans.
2022        for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
2023            if (bean instanceof SignalMast && turntable.isApproachMast((SignalMast) bean)) {
2024                if (turntable.getLayoutBlock() != null) {
2025                    protectingBlocks.add(turntable.getLayoutBlock());
2026                    return protectingBlocks;
2027                }
2028            }
2029            if (bean.equals(turntable.getExitSignalMast())) {
2030                for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
2031                    TrackSegment connectedTrack = ray.getConnect();
2032                    if (connectedTrack != null && connectedTrack.getLayoutBlock() != null) {
2033                        if (!protectingBlocks.contains(connectedTrack.getLayoutBlock())) {
2034                            protectingBlocks.add(connectedTrack.getLayoutBlock());
2035                        }
2036                    }
2037                }
2038                return protectingBlocks;
2039            }
2040        }
2041
2042        // Check for traverser approach masts first, as they are a special case. Ignore NX sensor beans.
2043        for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
2044            if (bean instanceof SignalMast && traverser.isApproachMast((SignalMast) bean)) {
2045                if (traverser.getLayoutBlock() != null) {
2046                    protectingBlocks.add(traverser.getLayoutBlock());
2047                    return protectingBlocks;
2048                }
2049            }
2050            if (bean.equals(traverser.getExitSignalMast())) {
2051                for (int i=0; i < traverser.getNumberSlots(); i++) {
2052                    TrackSegment connectedTrack = traverser.getSlotConnectOrdered(i);
2053                    if (connectedTrack != null && connectedTrack.getLayoutBlock() != null) {
2054                        if (!protectingBlocks.contains(connectedTrack.getLayoutBlock())) {
2055                            protectingBlocks.add(connectedTrack.getLayoutBlock());
2056                        }
2057                    }
2058                }
2059                return protectingBlocks;
2060            }
2061        }
2062
2063        if (!(bean instanceof SignalMast) && !(bean instanceof Sensor)) {
2064            log.error("Incorrect class type called, must be either SignalMast or Sensor");
2065
2066            return protectingBlocks;
2067        }
2068
2069        PositionablePoint pp = panel.getFinder().findPositionablePointByEastBoundBean(bean);
2070        TrackSegment tr;
2071        boolean east = true;
2072
2073        if (pp == null) {
2074            pp = panel.getFinder().findPositionablePointByWestBoundBean(bean);
2075            east = false;
2076        }
2077
2078        if (pp != null) {
2079            //   LayoutEditorTools tools = panel.getLETools(); //TODO: Dead-code strip this
2080
2081            if (east) {
2082                if (LayoutEditorTools.isAtWestEndOfAnchor(panel, pp.getConnect1(), pp)) {
2083                    tr = pp.getConnect2();
2084                } else {
2085                    tr = pp.getConnect1();
2086                }
2087            } else {
2088                if (LayoutEditorTools.isAtWestEndOfAnchor(panel, pp.getConnect1(), pp)) {
2089                    tr = pp.getConnect1();
2090                } else {
2091                    tr = pp.getConnect2();
2092                }
2093            }
2094
2095            if (tr != null) {
2096                protectingBlocks.add(tr.getLayoutBlock());
2097
2098                return protectingBlocks;
2099            }
2100        }
2101
2102        LevelXing l = panel.getFinder().findLevelXingByBean(bean);
2103
2104        if (l != null) {
2105            if (bean instanceof SignalMast) {
2106                if (l.getSignalAMast() == bean) {
2107                    protectingBlocks.add(l.getLayoutBlockAC());
2108                } else if (l.getSignalBMast() == bean) {
2109                    protectingBlocks.add(l.getLayoutBlockBD());
2110                } else if (l.getSignalCMast() == bean) {
2111                    protectingBlocks.add(l.getLayoutBlockAC());
2112                } else {
2113                    protectingBlocks.add(l.getLayoutBlockBD());
2114                }
2115            } else if (bean instanceof Sensor) {
2116                if (l.getSensorA() == bean) {
2117                    protectingBlocks.add(l.getLayoutBlockAC());
2118                } else if (l.getSensorB() == bean) {
2119                    protectingBlocks.add(l.getLayoutBlockBD());
2120                } else if (l.getSensorC() == bean) {
2121                    protectingBlocks.add(l.getLayoutBlockAC());
2122                } else {
2123                    protectingBlocks.add(l.getLayoutBlockBD());
2124                }
2125            }
2126            return protectingBlocks;
2127        }
2128
2129        LayoutSlip ls = panel.getFinder().findLayoutSlipByBean(bean);
2130
2131        if (ls != null) {
2132            protectingBlocks.add(ls.getLayoutBlock());
2133
2134            return protectingBlocks;
2135        }
2136
2137        LayoutTurnout t = panel.getFinder().findLayoutTurnoutByBean(bean);
2138
2139        if (t != null) {
2140            return t.getProtectedBlocks(bean);
2141        }
2142        return protectingBlocks;
2143    } //getProtectingBlocksByBean
2144
2145    @CheckReturnValue
2146    @CheckForNull
2147    public LayoutBlock getProtectedBlockByMast(
2148            @CheckForNull SignalMast signalMast,
2149            @CheckForNull LayoutEditor panel) {
2150        List<LayoutBlock> proBlocks = getProtectingBlocksByBean(signalMast, panel);
2151
2152        if (proBlocks.isEmpty()) {
2153            return null;
2154        }
2155        return proBlocks.get(0);
2156    }
2157
2158    /**
2159     * Get the LayoutBlock that a given sensor is protecting.
2160     * @param sensorName the sensor name to search for.
2161     * @param panel the layout editor panel.
2162     * @return the layout block, may be null.
2163     */
2164    @CheckReturnValue
2165    @CheckForNull
2166    public LayoutBlock getProtectedBlockBySensor(
2167            @Nonnull String sensorName,
2168            @CheckForNull LayoutEditor panel) {
2169        Sensor sensor = InstanceManager.sensorManagerInstance().getSensor(sensorName);
2170
2171        return getProtectedBlockBySensor(sensor, panel);
2172    }
2173
2174    @Nonnull
2175    public List<LayoutBlock> getProtectingBlocksBySensor(
2176            @CheckForNull Sensor sensor, @CheckForNull LayoutEditor panel) {
2177        return getProtectingBlocksByBean(sensor, panel);
2178    }
2179
2180    @Nonnull
2181    public List<LayoutBlock> getProtectingBlocksBySensorOld(
2182            @CheckForNull Sensor sensor, @Nonnull LayoutEditor panel) {
2183        List<LayoutBlock> result = new ArrayList<>();
2184        PositionablePoint pp = panel.getFinder().findPositionablePointByEastBoundBean(sensor);
2185        TrackSegment tr;
2186        boolean east = true;
2187
2188        if (pp == null) {
2189            pp = panel.getFinder().findPositionablePointByWestBoundBean(sensor);
2190            east = false;
2191        }
2192
2193        if (pp != null) {
2194            //            LayoutEditorTools tools = panel.getLETools(); //TODO: Dead-code strip this
2195
2196            if (east) {
2197                if (LayoutEditorTools.isAtWestEndOfAnchor(panel, pp.getConnect1(), pp)) {
2198                    tr = pp.getConnect2();
2199                } else {
2200                    tr = pp.getConnect1();
2201                }
2202            } else {
2203                if (LayoutEditorTools.isAtWestEndOfAnchor(panel, pp.getConnect1(), pp)) {
2204                    tr = pp.getConnect1();
2205                } else {
2206                    tr = pp.getConnect2();
2207                }
2208            }
2209
2210            if (tr != null) {
2211                result.add(tr.getLayoutBlock());
2212
2213                return result;
2214            }
2215        }
2216
2217        LevelXing l = panel.getFinder().findLevelXingByBean(sensor);
2218
2219        if (l != null) {
2220            if (l.getSensorA() == sensor) {
2221                result.add(l.getLayoutBlockAC());
2222            } else if (l.getSensorB() == sensor) {
2223                result.add(l.getLayoutBlockBD());
2224            } else if (l.getSensorC() == sensor) {
2225                result.add(l.getLayoutBlockAC());
2226            } else {
2227                result.add(l.getLayoutBlockBD());
2228            }
2229            return result;
2230        }
2231        LayoutSlip ls = panel.getFinder().findLayoutSlipByBean(sensor);
2232
2233        if (ls != null) {
2234            result.add(ls.getLayoutBlock());
2235
2236            return result;
2237        }
2238        LayoutTurnout t = panel.getFinder().findLayoutTurnoutByBean(sensor);
2239
2240        if (t != null) {
2241            return t.getProtectedBlocks(sensor);
2242        }
2243        return result;
2244    } //getProtectingBlocksBySensorOld
2245
2246    /**
2247     * Get the LayoutBlock that a given sensor is protecting.
2248     * @param sensor sensor to search for.
2249     * @param panel layout editor panel to search.
2250     * @return the layout block, may be null.
2251     */
2252    @CheckReturnValue
2253    @CheckForNull
2254    public LayoutBlock getProtectedBlockBySensor(
2255            @CheckForNull Sensor sensor, @CheckForNull LayoutEditor panel) {
2256        List<LayoutBlock> proBlocks = getProtectingBlocksByBean(sensor, panel);
2257
2258        if (proBlocks.isEmpty()) {
2259            return null;
2260        }
2261        return proBlocks.get(0);
2262    }
2263
2264    /**
2265     * Get the block facing a given bean object (Sensor, SignalMast or
2266     * SignalHead).
2267     *
2268     * @param nb    NamedBean
2269     * @param panel panel that this bean is on
2270     * @return The block that the bean object is facing
2271     */
2272    @CheckReturnValue
2273    @CheckForNull
2274    public LayoutBlock getFacingBlockByNamedBean(
2275            @Nonnull NamedBean nb, @CheckForNull LayoutEditor panel) {
2276        if (nb instanceof SignalHead) {
2277            return getFacingBlock((SignalHead) nb, panel);
2278        }
2279        return getFacingBlockByBean(nb, panel);
2280    }
2281
2282    /**
2283     * Get the LayoutBlock that a given sensor is facing.
2284     * @param sensorName the sensor name.
2285     * @param panel the layout editor panel.
2286     * @return the facing layout block, may be null.
2287     */
2288    @CheckReturnValue
2289    @CheckForNull
2290    public LayoutBlock getFacingBlockBySensor(@Nonnull String sensorName,
2291            @CheckForNull LayoutEditor panel) {
2292        LayoutBlock result = null;  //assume failure (pessimist!)
2293        if (panel != null) {
2294            Sensor sensor = InstanceManager.sensorManagerInstance().getSensor(sensorName);
2295            result = (sensor == null) ? null : getFacingBlockBySensor(sensor, panel);
2296        }
2297        return result;
2298    }
2299
2300    /**
2301     * Get the LayoutBlock that a given signal is facing.
2302     * @param signalMast the signal mast to search for.
2303     * @param panel the layout editor panel.
2304     * @return the layout block, may be null.
2305     */
2306    @CheckReturnValue
2307    @CheckForNull
2308    public LayoutBlock getFacingBlockByMast(
2309            @Nonnull SignalMast signalMast,
2310            @Nonnull LayoutEditor panel) {
2311        return getFacingBlockByBean(signalMast, panel);
2312    }
2313
2314    /**
2315     * If the panel variable is null, search all LE panels. This was added to
2316     * support multi panel entry/exit.
2317     *
2318     * @param bean  The sensor, mast or head to be located.
2319     * @param panel The panel to search. Search all LE panels if null.
2320     * @return the facing layout block.
2321     */
2322    @CheckReturnValue
2323    @CheckForNull
2324    private LayoutBlock getFacingBlockByBean(
2325            @Nonnull NamedBean bean,
2326            LayoutEditor panel) {
2327        if (panel == null) {
2328            Set<LayoutEditor> panels = InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class);
2329            LayoutBlock returnBlock = null;
2330            for (LayoutEditor p : panels) {
2331                returnBlock = getFacingBlockByBeanByPanel(bean, p);
2332                if (returnBlock != null) {
2333                    break;
2334                }
2335            }
2336            return returnBlock;
2337        } else {
2338            return getFacingBlockByBeanByPanel(bean, panel);
2339        }
2340    }
2341
2342    @CheckReturnValue
2343    @CheckForNull
2344    private LayoutBlock getFacingBlockByBeanByPanel(
2345            @Nonnull NamedBean bean,
2346            @Nonnull LayoutEditor panel) {
2347        // Check for turntable masts first, as they are a special case. Ignore NX sensor beans.
2348        for (LayoutTurntable turntable : panel.getLayoutTurntables()) {
2349            if (bean.equals(turntable.getBufferMast())) {
2350                return turntable.getLayoutBlock();
2351            }
2352            if (bean.equals(turntable.getExitSignalMast())) {
2353                return turntable.getLayoutBlock();
2354            }
2355            if (bean instanceof SignalMast && turntable.isApproachMast((SignalMast) bean)) {
2356                for (LayoutTurntable.RayTrack ray : turntable.getRayTrackList()) {
2357                    if (bean.equals(ray.getApproachMast())) {
2358                        TrackSegment connectedTrack = ray.getConnect();
2359                        if (connectedTrack != null && connectedTrack.getLayoutBlock() != null) {
2360                            return connectedTrack.getLayoutBlock();
2361                        }
2362                    }
2363                }
2364            }
2365        }
2366        // Check for traverser masts, as they are a special case. Ignore NX sensor beans.
2367        for (LayoutTraverser traverser : panel.getLayoutTraversers()) {
2368            if (bean.equals(traverser.getBufferMast())) {
2369                return traverser.getLayoutBlock();
2370            }
2371            if (bean.equals(traverser.getExitSignalMast())) {
2372                return traverser.getLayoutBlock();
2373            }
2374            if (bean instanceof SignalMast && traverser.isApproachMast((SignalMast) bean)) {
2375                for (LayoutTraverser.SlotTrack slot : traverser.getSlotList()) {
2376                    if (bean.equals(slot.getApproachMast())) {
2377                        TrackSegment connectedTrack = slot.getConnect();
2378                        if (connectedTrack != null && connectedTrack.getLayoutBlock() != null) {
2379                            return connectedTrack.getLayoutBlock();
2380                        }
2381                    }
2382                }
2383            }
2384        }
2385
2386        PositionablePoint pp = panel.getFinder().findPositionablePointByEastBoundBean(bean);
2387        TrackSegment tr;
2388        boolean east = true;
2389
2390        //Don't think that the logic for this is the right way round
2391        if (pp == null) {
2392            pp = panel.getFinder().findPositionablePointByWestBoundBean(bean);
2393            east = false;
2394        }
2395
2396        if (pp != null) {
2397            // LayoutEditorTools tools = panel.getLETools(); //TODO: Dead-code strip this
2398
2399            if (east) {
2400                if (LayoutEditorTools.isAtWestEndOfAnchor(panel, pp.getConnect1(), pp)) {
2401                    tr = pp.getConnect1();
2402                } else {
2403                    tr = pp.getConnect2();
2404                }
2405            } else {
2406                if (LayoutEditorTools.isAtWestEndOfAnchor(panel, pp.getConnect1(), pp)) {
2407                    tr = pp.getConnect2();
2408                } else {
2409                    tr = pp.getConnect1();
2410                }
2411            }
2412
2413            if (tr != null) {
2414                log.debug("found facing block by positionable point");
2415
2416                return tr.getLayoutBlock();
2417            }
2418        }
2419        LayoutTurnout t = panel.getFinder().findLayoutTurnoutByBean(bean);
2420
2421        if (t != null) {
2422            log.debug("found signalmast at turnout {}", t.getTurnout().getDisplayName());
2423            Object connect = null;
2424
2425            if (bean instanceof SignalMast) {
2426                if (t.getSignalAMast() == bean) {
2427                    connect = t.getConnectA();
2428                } else if (t.getSignalBMast() == bean) {
2429                    connect = t.getConnectB();
2430                } else if (t.getSignalCMast() == bean) {
2431                    connect = t.getConnectC();
2432                } else {
2433                    connect = t.getConnectD();
2434                }
2435            } else if (bean instanceof Sensor) {
2436                if (t.getSensorA() == bean) {
2437                    connect = t.getConnectA();
2438                } else if (t.getSensorB() == bean) {
2439                    connect = t.getConnectB();
2440                } else if (t.getSensorC() == bean) {
2441                    connect = t.getConnectC();
2442                } else {
2443                    connect = t.getConnectD();
2444                }
2445            }
2446
2447            if (connect instanceof TrackSegment) {
2448                tr = (TrackSegment) connect;
2449                log.debug("return block {}", tr.getLayoutBlock().getDisplayName());
2450
2451                return tr.getLayoutBlock();
2452            }
2453        }
2454
2455        LevelXing l = panel.getFinder().findLevelXingByBean(bean);
2456
2457        if (l != null) {
2458            Object connect = null;
2459
2460            if (bean instanceof SignalMast) {
2461                if (l.getSignalAMast() == bean) {
2462                    connect = l.getConnectA();
2463                } else if (l.getSignalBMast() == bean) {
2464                    connect = l.getConnectB();
2465                } else if (l.getSignalCMast() == bean) {
2466                    connect = l.getConnectC();
2467                } else {
2468                    connect = l.getConnectD();
2469                }
2470            } else if (bean instanceof Sensor) {
2471                if (l.getSensorA() == bean) {
2472                    connect = l.getConnectA();
2473                } else if (l.getSensorB() == bean) {
2474                    connect = l.getConnectB();
2475                } else if (l.getSensorC() == bean) {
2476                    connect = l.getConnectC();
2477                } else {
2478                    connect = l.getConnectD();
2479                }
2480            }
2481
2482            if (connect instanceof TrackSegment) {
2483                tr = (TrackSegment) connect;
2484                log.debug("return block {}", tr.getLayoutBlock().getDisplayName());
2485
2486                return tr.getLayoutBlock();
2487            }
2488        }
2489
2490        LayoutSlip ls = panel.getFinder().findLayoutSlipByBean(bean);
2491
2492        if (ls != null) {
2493            Object connect = null;
2494
2495            if (bean instanceof SignalMast) {
2496                if (ls.getSignalAMast() == bean) {
2497                    connect = ls.getConnectA();
2498                } else if (ls.getSignalBMast() == bean) {
2499                    connect = ls.getConnectB();
2500                } else if (ls.getSignalCMast() == bean) {
2501                    connect = ls.getConnectC();
2502                } else {
2503                    connect = ls.getConnectD();
2504                }
2505            } else if (bean instanceof Sensor) {
2506                if (ls.getSensorA() == bean) {
2507                    connect = ls.getConnectA();
2508                } else if (ls.getSensorB() == bean) {
2509                    connect = ls.getConnectB();
2510                } else if (ls.getSensorC() == bean) {
2511                    connect = ls.getConnectC();
2512                } else {
2513                    connect = ls.getConnectD();
2514                }
2515            }
2516
2517            if (connect instanceof TrackSegment) {
2518                tr = (TrackSegment) connect;
2519                log.debug("return block {}", tr.getLayoutBlock().getDisplayName());
2520
2521                return tr.getLayoutBlock();
2522            }
2523        }
2524        return null;
2525    } //getFacingBlockByBean
2526
2527    /**
2528     * Get the LayoutBlock that a given sensor is facing.
2529     * @param sensor the sensor to search for.
2530     * @param panel the layout editor panel to search.
2531     * @return the layout block, may be null.
2532     */
2533    @CheckReturnValue
2534    @CheckForNull
2535    public LayoutBlock getFacingBlockBySensor(
2536            @Nonnull Sensor sensor,
2537            @Nonnull LayoutEditor panel) {
2538        return getFacingBlockByBean(sensor, panel);
2539    }
2540
2541    @CheckReturnValue
2542    @CheckForNull
2543    public LayoutBlock getProtectedBlock(
2544            @Nonnull SignalHead signalHead, @CheckForNull LayoutEditor panel) {
2545        LayoutBlock result = null;  //assume failure (pessimist!)
2546        if (panel != null) {
2547            String userName = signalHead.getUserName();
2548            result = (userName == null) ? null : getProtectedBlock(userName, panel);
2549
2550            if (result == null) {
2551                result = getProtectedBlock(signalHead.getSystemName(), panel);
2552            }
2553        }
2554        return result;
2555    }
2556
2557    /**
2558     * Get the LayoutBlock that a given signal is protecting.
2559     * @param signalName the signal name to search for.
2560     * @param panel the main layout editor panel.
2561     * @return the layout block, may be null.
2562     */
2563    /* @TODO This needs to be expanded to cover turnouts and level crossings. */
2564    @CheckReturnValue
2565    @CheckForNull
2566    public LayoutBlock getProtectedBlock(
2567            @Nonnull String signalName, @Nonnull LayoutEditor panel) {
2568        PositionablePoint pp = panel.getFinder().findPositionablePointByEastBoundSignal(signalName);
2569        TrackSegment tr;
2570
2571        if (pp == null) {
2572            pp = panel.getFinder().findPositionablePointByWestBoundSignal(signalName);
2573
2574            if (pp == null) {
2575                return null;
2576            }
2577            tr = pp.getConnect1();
2578        } else {
2579            tr = pp.getConnect2();
2580        }
2581
2582        //tr = pp.getConnect2();
2583        if (tr == null) {
2584            return null;
2585        }
2586        return tr.getLayoutBlock();
2587    }
2588
2589    @CheckReturnValue
2590    @CheckForNull
2591    public LayoutBlock getFacingBlock(
2592            @Nonnull SignalHead signalHead, @CheckForNull LayoutEditor panel) {
2593        LayoutBlock result = null;  //assume failure (pessimist!)
2594        if (panel != null) {
2595            String userName = signalHead.getUserName();
2596            result = (userName == null) ? null : getFacingBlock(userName, panel);
2597            if (result == null) {
2598                result = getFacingBlock(signalHead.getSystemName(), panel);
2599            }
2600        }
2601        return result;
2602    }
2603
2604    /**
2605     * Get the LayoutBlock that a given signal is facing.
2606     * @param signalName signal name.
2607     * @param panel layout editor panel.
2608     * @return the facing layout block.
2609     */
2610    /* @TODO This needs to be expanded to cover turnouts and level crossings. */
2611    @CheckReturnValue
2612    @CheckForNull
2613    public LayoutBlock getFacingBlock(
2614            @Nonnull String signalName, @Nonnull LayoutEditor panel) {
2615        PositionablePoint pp = panel.getFinder().findPositionablePointByWestBoundSignal(signalName);
2616        TrackSegment tr;
2617
2618        if (pp == null) {
2619            pp = panel.getFinder().findPositionablePointByWestBoundSignal(signalName);
2620
2621            if (pp == null) {
2622                return null;
2623            }
2624            tr = pp.getConnect1();
2625        } else {
2626            tr = pp.getConnect2();
2627        }
2628
2629        if (tr == null) {
2630            return null;
2631        }
2632        return tr.getLayoutBlock();
2633    }
2634
2635    private boolean warnConnectivity = true;
2636
2637    /**
2638     * Controls switching off incompatible block connectivity messages.
2639     * <p>
2640     * Warnings are always on when program starts up. Once stopped by the user,
2641     * these messages may not be switched on again until program restarts.
2642     * @return true if connectivity warning flag set, else false.
2643     */
2644    public boolean warn() {
2645        return warnConnectivity;
2646    }
2647
2648    public void turnOffWarning() {
2649        warnConnectivity = false;
2650    }
2651
2652    protected boolean enableAdvancedRouting = false;
2653
2654    /**
2655     * @return true if advanced layout block routing has been enabled
2656     */
2657    public boolean isAdvancedRoutingEnabled() {
2658        return enableAdvancedRouting;
2659    }
2660
2661    /**
2662     * Enable the advanced layout block routing protocol
2663     * <p>
2664     * The block routing protocol enables each layout block to build up a list
2665     * of all reachable blocks, along with how far away they are, which
2666     * direction they are in and which of the connected blocks they are
2667     * reachable from.
2668     */
2669    private long firstRoutingChange;
2670
2671    public void enableAdvancedRouting(boolean boo) {
2672        if (boo == enableAdvancedRouting) {
2673            return;
2674        }
2675        enableAdvancedRouting = boo;
2676
2677        if (boo && initialized) {
2678            initializeLayoutBlockRouting();
2679        }
2680        firePropertyChange(PROPERTY_ADVANCED_ROUTING_ENABLED, !enableAdvancedRouting, enableAdvancedRouting);
2681    }
2682
2683    private void initializeLayoutBlockRouting() {
2684        if (!enableAdvancedRouting || !initialized) {
2685            log.debug("initializeLayoutBlockRouting immediate return due to {} {}", enableAdvancedRouting, initialized);
2686
2687            return;
2688        }
2689        if (!ThreadingUtil.isLayoutThread()) {
2690            // routing tables are only changed on the layout thread
2691            ThreadingUtil.runOnLayout(this::initializeLayoutBlockRouting);
2692            return;
2693        }
2694        firstRoutingChange = System.nanoTime();
2695
2696        //cycle through all LayoutBlocks, completing initialization of the layout block routing
2697        java.util.Enumeration<LayoutBlock> en = _tsys.elements();
2698
2699        while (en.hasMoreElements()) {
2700            en.nextElement().initializeLayoutBlockRouting();
2701        }
2702    }
2703
2704    @Nonnull
2705    public LayoutBlockConnectivityTools getLayoutBlockConnectivityTools() {
2706        return lbct;
2707    }
2708
2709    private final LayoutBlockConnectivityTools lbct = new LayoutBlockConnectivityTools();
2710
2711    private volatile long lastRoutingChange;
2712
2713    void setLastRoutingChange() {
2714        log.debug("setLastRoutingChange");
2715        synchronized (stabilisedLock) {
2716            lastRoutingChange = System.nanoTime();
2717            stabilised = false;
2718        }
2719        setRoutingStabilised();
2720    }
2721
2722    // guards lastRoutingChange, stabilised and checking together, so that a
2723    // change can not be made between finding the tables unchanged and
2724    // reporting them as stable; the events are posted to the layout thread
2725    // while holding it, so they arrive in the order the changes were made
2726    private final Object stabilisedLock = new Object();
2727    private boolean checking = false;
2728    volatile boolean stabilised = false;
2729
2730    // how long the routing tables must be unchanged to be reported as stable
2731    private long stabilisedDelayMsec = 2000L;
2732
2733    /**
2734     * Set how long the routing tables must be unchanged before the routing
2735     * is reported as stable. For tests.
2736     *
2737     * @param msec the time in milliseconds, 2000 by default
2738     */
2739    void setStabilisedDelay(long msec) {
2740        stabilisedDelayMsec = msec;
2741    }
2742
2743    public void setRoutingStabilised() {
2744        synchronized (stabilisedLock) {
2745            if (checking) {
2746                return;
2747            }
2748            log.debug("routing table change has been initiated");
2749            checking = true;
2750            stabilised = false;
2751
2752            ThreadingUtil.runOnLayoutEventually(() -> {
2753                if (namedStabilisedIndicator != null) {
2754                    try {
2755                        namedStabilisedIndicator.getBean().setState(Sensor.INACTIVE);
2756                    } catch (JmriException ex) {
2757                        log.debug("Error setting stability indicator sensor");
2758                    }
2759                }
2760                firePropertyChange(PROPERTY_TOPOLOGY, true, false);
2761            });
2762        }
2763        long delay = stabilisedDelayMsec;
2764        Runnable r = () -> {
2765            try {
2766                long oldvalue = lastRoutingChange;
2767
2768                while (true) {
2769                    Thread.sleep(delay);
2770
2771                    synchronized (stabilisedLock) {
2772                        if (oldvalue == lastRoutingChange) {
2773                            log.debug("routing table has now been stable for {} msec", delay);
2774                            checking = false;
2775                            stabilised = true;
2776                            ThreadingUtil.runOnLayoutEventually(this::reportRoutingStabilised);
2777                            break;
2778                        }
2779                    }
2780                    long seconds = (long) ((lastRoutingChange - firstRoutingChange) / 1e9);
2781                    log.debug("routing table not stable after {} in {}",
2782                            String.format("%d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60),
2783                            Thread.currentThread().getName());
2784                    oldvalue = lastRoutingChange;
2785                }
2786            } catch (InterruptedException ex) {
2787                Thread.currentThread().interrupt();
2788                synchronized (stabilisedLock) {
2789                    checking = false;
2790                }
2791            }
2792        };
2793        thr = ThreadingUtil.newThread(r, "Routing stabilising timer");
2794        thr.start();
2795    } //setRoutingStabilised
2796
2797    private void reportRoutingStabilised() {
2798        firePropertyChange(PROPERTY_TOPOLOGY, false, true);
2799
2800        if (namedStabilisedIndicator != null) {
2801            log.debug("Setting StabilisedIndicator Sensor {} ACTIVE",
2802                    namedStabilisedIndicator.getBean().getDisplayName());
2803            try {
2804                namedStabilisedIndicator.getBean().setState(Sensor.ACTIVE);
2805            } catch (JmriException ex) {
2806                log.debug("Error setting stability indicator sensor");
2807            }
2808        } else {
2809            log.debug("Stable, no sensor to set");
2810        }
2811    }
2812
2813    private Thread thr = null;
2814
2815    private NamedBeanHandle<Sensor> namedStabilisedIndicator;
2816
2817    /**
2818     * Assign a sensor to the routing protocol, that changes state dependant
2819     * upon if the routing protocol has stabilised or is under going a change.
2820     * @param pName sensor name, will be provided if not existing.
2821     * @throws jmri.JmriException if no sensor manager.
2822     *
2823     */
2824    public void setStabilisedSensor(@Nonnull String pName) throws JmriException {
2825        if (InstanceManager.getNullableDefault(jmri.SensorManager.class) != null) {
2826            try {
2827                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
2828                namedStabilisedIndicator = InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(
2829                        pName,
2830                        sensor);
2831                try {
2832                    if (stabilised) {
2833                        sensor.setState(Sensor.ACTIVE);
2834                    } else {
2835                        sensor.setState(Sensor.INACTIVE);
2836                    }
2837                } catch (JmriException ex) {
2838                    log.error("Error setting stablilty indicator sensor");
2839                }
2840            } catch (IllegalArgumentException ex) {
2841                log.error("Sensor '{}' not available", pName);
2842                throw new JmriException("Sensor '" + pName + "' not available");
2843            }
2844        } else {
2845            log.error("No SensorManager for this protocol");
2846            throw new JmriException("No Sensor Manager Found");
2847        }
2848    }
2849
2850    /**
2851     * Get the sensor used to indicate if the routing protocol has stabilised or
2852     * not.
2853     * @return routing stability sensor, may be null.
2854     */
2855    public Sensor getStabilisedSensor() {
2856        if (namedStabilisedIndicator == null) {
2857            return null;
2858        }
2859        return namedStabilisedIndicator.getBean();
2860    }
2861
2862    /**
2863     * Get the sensor used for the stability indication.
2864     * @return stability sensor, may be null.
2865     */
2866    @CheckReturnValue
2867    @CheckForNull
2868    public NamedBeanHandle<Sensor> getNamedStabilisedSensor() {
2869        return namedStabilisedIndicator;
2870    }
2871
2872    /**
2873     * @return true if the layout block routing protocol has stabilised
2874     */
2875    public boolean routingStablised() {
2876        return stabilised;
2877    }
2878
2879    /**
2880     * @return the time when the last routing change was made, recorded as
2881     *         System.nanoTime()
2882     */
2883    public long getLastRoutingChange() {
2884        return lastRoutingChange;
2885    }
2886
2887    @Override
2888    @Nonnull
2889    public String getBeanTypeHandled(boolean plural) {
2890        return Bundle.getMessage(plural ? "BeanNameLayoutBlocks" : "BeanNameLayoutBlock");
2891    }
2892
2893    /**
2894     * {@inheritDoc}
2895     */
2896    @Override
2897    public Class<LayoutBlock> getNamedBeanClass() {
2898        return LayoutBlock.class;
2899    }
2900
2901    /**
2902     * Get a list of layout blocks which this roster entry appears to be
2903     * occupying. A layout block is assumed to contain this roster entry if the
2904     * value of the underlying block is the RosterEntry itself, or a string with
2905     * the entry's id or dcc address.
2906     *
2907     * @param re the roster entry
2908     * @return list of layout block user names
2909     */
2910    @Nonnull
2911    public List<LayoutBlock> getLayoutBlocksOccupiedByRosterEntry(
2912            @Nonnull RosterEntry re) {
2913        List<LayoutBlock> result = new ArrayList<>();
2914
2915        BlockManager bm = InstanceManager.getDefault(BlockManager.class);
2916        List<Block> blockList = bm.getBlocksOccupiedByRosterEntry(re);
2917        for (Block block : blockList) {
2918            String uname = block.getUserName();
2919            if (uname != null) {
2920                LayoutBlock lb = getByUserName(uname);
2921                if (lb != null) {
2922                    result.add(lb);
2923                }
2924            }
2925        }
2926        return result;
2927    }
2928
2929    @Override
2930    public void dispose(){
2931        InstanceManager.sensorManagerInstance().removeVetoableChangeListener(this);
2932        InstanceManager.memoryManagerInstance().removeVetoableChangeListener(this);
2933        super.dispose();
2934    }
2935
2936    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutBlockManager.class);
2937
2938}