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