001package jmri.jmrit.entryexit;
002
003import java.awt.Color;
004import java.awt.event.MouseAdapter;
005import java.awt.event.MouseEvent;
006import java.beans.PropertyChangeEvent;
007import java.beans.PropertyChangeListener;
008import java.beans.PropertyVetoException;
009import java.util.*;
010import java.util.Map.Entry;
011
012import javax.annotation.CheckReturnValue;
013import javax.annotation.Nonnull;
014import javax.annotation.OverridingMethodsMustInvokeSuper;
015import javax.swing.JDialog;
016import javax.swing.JPanel;
017
018import jmri.*;
019import jmri.beans.VetoableChangeSupport;
020import jmri.jmrit.display.EditorManager;
021import jmri.jmrit.display.layoutEditor.LayoutBlock;
022import jmri.jmrit.display.layoutEditor.LayoutBlockConnectivityTools;
023import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
024import jmri.jmrit.display.layoutEditor.LayoutEditor;
025import jmri.jmrix.internal.InternalSystemConnectionMemo;
026import jmri.util.swing.JmriJOptionPane;
027
028/**
029 * Implements an Entry Exit based method of setting turnouts, setting up signal
030 * logic and allocating blocks through a path based on the Layout Editor.
031 * <p>
032 * The route is based upon having a sensor assigned at a known location on the
033 * panel (set at the boundary of two different blocks) through to a sensor at a
034 * remote location on the same panel. Using the layout block routing, a path can
035 * then be set between the two sensors so long as one exists and no
036 * section of track is set occupied. If available an alternative route will be
037 * used when the direct path is occupied (blocked).
038 * <p>
039 * Initial implementation only handles the setting up of turnouts on a path.
040 *
041 * @author Kevin Dickerson Copyright (C) 2011
042 */
043public class EntryExitPairs extends VetoableChangeSupport implements Manager<DestinationPoints>, jmri.InstanceManagerAutoDefault,
044        PropertyChangeListener {
045
046    public LayoutBlockConnectivityTools.Metric routingMethod = LayoutBlockConnectivityTools.Metric.METRIC;
047
048    public static final int NXBUTTONSELECTED = 0x08;
049    public static final int NXBUTTONACTIVE = Sensor.ACTIVE;
050    public static final int NXBUTTONINACTIVE = Sensor.INACTIVE;
051    private final SystemConnectionMemo memo;
052    private final Map<String, Boolean> silencedProperties = new HashMap<>();
053
054    private int settingTimer = 2000;
055
056    public int getSettingTimer() {
057        return settingTimer;
058    }
059
060    public void setSettingTimer(int i) {
061        settingTimer = i;
062    }
063
064    private Color settingRouteColor = null;
065
066    public boolean useDifferentColorWhenSetting() {
067        return (settingRouteColor != null);
068    }
069
070    public Color getSettingRouteColor() {
071        return settingRouteColor;
072    }
073
074    public void setSettingRouteColor(Color col) {
075        settingRouteColor = col;
076    }
077
078    /**
079     * Constant value to represent that the entryExit will only set up the
080     * turnouts between two different points.
081     */
082    public static final int SETUPTURNOUTSONLY = 0x00;
083
084    /**
085     * Constant value to represent that the entryExit will set up the turnouts
086     * between two different points and configure the Signal Mast Logic to use
087     * the correct blocks.
088     */
089    public static final int SETUPSIGNALMASTLOGIC = 0x01;
090
091    /**
092     * Constant value to represent that the entryExit will do full interlocking.
093     * It will set the turnouts and "reserve" the blocks.
094     */
095    public static final int FULLINTERLOCK = 0x02;
096
097    boolean allocateToDispatcher = false;
098    boolean absSignalMode = false;
099    boolean quietFail = false;      // Suppress the dialog when a route allocation fails.
100    boolean skipGuiFix = false;     // Skip the thread for setRoute.  This prevents double-click chaining.
101
102    public static final int PROMPTUSER = 0x00;
103    public static final int AUTOCLEAR = 0x01;
104    public static final int AUTOCANCEL = 0x02;
105    public static final int AUTOSTACK = 0x03;
106
107    public static final int OVERLAP_CANCEL = 0x01;
108    public static final int OVERLAP_STACK = 0x02;
109
110    /**
111     * String constant for auto generate complete.
112     */
113    public static final String PROPERTY_AUTO_GENERATE_COMPLETE = "autoGenerateComplete";
114
115    /**
116     * String constant for active.
117     */
118    public static final String PROPERTY_ACTIVE = "active";
119
120    int routeClearOption = PROMPTUSER;
121    int routeOverlapOption = PROMPTUSER;
122    String memoryOption = "";     // Optional memory variable to receive allocation messages
123    int memoryClearDelay = 0;     // Delay before clearing memory, 0 for clearing disabled
124
125    static JPanel glassPane = new JPanel();
126
127    /**
128     * Delay between issuing Turnout commands
129     */
130    public int turnoutSetDelay = 0;
131
132    /**
133     * Constructor for creating an EntryExitPairs object and create a transparent JPanel for it.
134     */
135    public EntryExitPairs() {
136        memo = InstanceManager.getDefault(InternalSystemConnectionMemo.class);
137        InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> cm.registerUser(this));
138        InstanceManager.getDefault(LayoutBlockManager.class).addPropertyChangeListener(propertyBlockManagerListener);
139
140        glassPane.setOpaque(false);
141        glassPane.setLayout(null);
142        glassPane.addMouseListener(new MouseAdapter() {
143            @Override
144            public void mousePressed(MouseEvent e) {
145                e.consume();
146            }
147        });
148    }
149
150    public void setDispatcherIntegration(boolean boo) {
151        allocateToDispatcher = boo;
152    }
153
154    public boolean getDispatcherIntegration() {
155        return allocateToDispatcher;
156    }
157
158    public void setAbsSignalMode(boolean absMode) {
159        absSignalMode = absMode;
160    }
161
162    public boolean isAbsSignalMode() {
163        return absSignalMode;
164    }
165
166    public void setQuietFail(boolean quiet) {
167        quietFail = quiet;
168    }
169
170    public boolean isQuietFail() {
171        return quietFail;
172    }
173
174    public void setSkipGuiFix(boolean skip) {
175        skipGuiFix = skip;
176    }
177
178    public boolean isSkipGuiFix() {
179        return skipGuiFix;
180    }
181
182    /**
183     * Get the transparent JPanel for this EntryExitPairs.
184     * @return JPanel overlay
185     */
186    public JPanel getGlassPane() {
187        return glassPane;
188    }
189
190    HashMap<PointDetails, Source> nxpair = new HashMap<>();
191
192    public void addNXSourcePoint(LayoutBlock facing, List<LayoutBlock> protecting, NamedBean loc, LayoutEditor panel) {
193        PointDetails point = providePoint(facing, protecting, panel);
194        point.setRefObject(loc);
195    }
196
197    public void addNXSourcePoint(NamedBean source) {
198        PointDetails point = null;
199        for (LayoutEditor editor : InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class)) {
200            point = providePoint(source, editor);
201        }
202        if (point == null) {
203            log.error("Unable to find a location on any panel for item {}", source.getDisplayName());  // NOI18N
204        }
205    }
206
207    public void addNXSourcePoint(NamedBean source, LayoutEditor panel) {
208        if (source == null) {
209            log.error("source bean supplied is null");  // NOI18N
210            return;
211        }
212        if (panel == null) {
213            log.error("panel supplied is null");  // NOI18N
214            return;
215        }
216        PointDetails point;
217        point = providePoint(source, panel);
218        if (point == null) {
219            log.error("Unable to find a location on the panel {} for item {}",
220                panel.getLayoutName(), source.getDisplayName());
221        }
222    }
223
224    public Object getEndPointLocation(NamedBean source, LayoutEditor panel) {
225        if (source == null) {
226            log.error("Source bean past is null");  // NOI18N
227            return null;
228        }
229        if (panel == null) {
230            log.error("panel passed is null");  // NOI18N
231            return null;
232        }
233        PointDetails sourcePoint = getPointDetails(source, panel);
234        if (sourcePoint == null) {
235            log.error("Point is not located");  // NOI18N
236            return null;
237        }
238        return sourcePoint.getRefLocation();
239    }
240
241    /** {@inheritDoc} */
242    @Override
243    public int getXMLOrder() {
244        return ENTRYEXIT;
245    }
246
247    /** {@inheritDoc} */
248    @Override
249    public DestinationPoints getBySystemName(String systemName) {
250        for (Source e : nxpair.values()) {
251            DestinationPoints pd = e.getByUniqueId(systemName);
252            if (pd != null) {
253                return pd;
254            }
255        }
256        return null;
257    }
258
259    /** {@inheritDoc} */
260    @Override
261    public DestinationPoints getByUserName(@Nonnull String userName) {
262        for (Source e : nxpair.values()) {
263            DestinationPoints pd = e.getByUserName(userName);
264            if (pd != null) {
265                return pd;
266            }
267        }
268        return null;
269    }
270
271    /** {@inheritDoc} */
272    @Override
273    public DestinationPoints getNamedBean(@Nonnull String name) {
274        DestinationPoints b = getByUserName(name);
275        if (b != null) {
276            return b;
277        }
278        return getBySystemName(name);
279    }
280
281    /** {@inheritDoc} */
282    @Nonnull
283    @Override
284    public SystemConnectionMemo getMemo() {
285        return memo;
286    }
287
288    /** {@inheritDoc} */
289    @Override
290    @Nonnull
291    public String getSystemPrefix() {
292        return memo.getSystemPrefix();
293    }
294
295    /** {@inheritDoc} */
296    @Override
297    public char typeLetter() {
298        throw new UnsupportedOperationException("Not supported yet.");  // NOI18N
299    }
300
301    /** {@inheritDoc} */
302    @Override
303    @Nonnull
304    public String makeSystemName(@Nonnull String s) {
305        throw new UnsupportedOperationException("Not supported yet.");  // NOI18N
306    }
307
308    /** {@inheritDoc} */
309    @Override
310    @CheckReturnValue
311    public int getObjectCount() {
312        return getNamedBeanSet().size();
313    }
314
315    /**
316     * Implemented to support the Conditional combo box name list
317     * @since 4.9.3
318     * @return a list of Destination Point beans
319     */
320    @Override
321    @Nonnull
322    public SortedSet<DestinationPoints> getNamedBeanSet() {
323        TreeSet<DestinationPoints> beanList = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
324        for (Source e : nxpair.values()) {
325            List<String> uidList = e.getDestinationUniqueId();
326            for (String uid : uidList) {
327                beanList.add(e.getByUniqueId(uid));
328            }
329        }
330        return beanList;
331    }
332
333    /** {@inheritDoc} */
334    @Override
335    public void register(@Nonnull DestinationPoints n) {
336        throw new UnsupportedOperationException("Not supported yet.");  // NOI18N
337    }
338
339    /** {@inheritDoc} */
340    @Override
341    public void deregister(@Nonnull DestinationPoints n) {
342        throw new UnsupportedOperationException("Not supported yet.");  // NOI18N
343    }
344
345    public void setClearDownOption(int i) {
346        routeClearOption = i;
347    }
348
349    public int getClearDownOption() {
350        return routeClearOption;
351    }
352
353    public void setOverlapOption(int i) {
354        routeOverlapOption = i;
355    }
356
357    public int getOverlapOption() {
358        return routeOverlapOption;
359    }
360
361    public void setMemoryOption(String memoryName) {
362        memoryOption = memoryName;
363    }
364
365    public String getMemoryOption() {
366        return memoryOption;
367    }
368
369    public void setMemoryClearDelay(int secs) {
370        memoryClearDelay = secs;
371    }
372
373    public int getMemoryClearDelay() {
374        return memoryClearDelay;
375    }
376
377    /** {@inheritDoc} */
378    @Override
379    public void dispose() {
380        InstanceManager.getDefault(LayoutBlockManager.class).removePropertyChangeListener(propertyBlockManagerListener);
381    }
382
383    /**
384     * Generate the point details, given a known source and a
385     * Layout Editor panel.
386     *
387     * @param source Origin of movement
388     * @param panel  A Layout Editor panel
389     * @return A PointDetails object
390     */
391    public PointDetails providePoint(NamedBean source, LayoutEditor panel) {
392        PointDetails sourcePoint = getPointDetails(source, panel);
393        if (sourcePoint == null) {
394            LayoutBlock facing = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).getFacingBlockByNamedBean(source, null);
395            List<LayoutBlock> protecting = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).getProtectingBlocksByNamedBean(source, null);
396//             log.info("facing = {}, protecting = {}", facing, protecting);
397            if (facing == null && protecting.isEmpty()) {
398                log.error("Unable to find facing and protecting blocks");  // NOI18N
399                return null;
400            }
401            sourcePoint = providePoint(facing, protecting, panel);
402            sourcePoint.setRefObject(source);
403        }
404        return sourcePoint;
405    }
406
407    /**
408     * Return a list of all source (origin) points on a given
409     * Layout Editor panel.
410     *
411     * @param panel  A Layout Editor panel
412     * @return A list of source objects
413     */
414    public List<Object> getSourceList(LayoutEditor panel) {
415        List<Object> list = new ArrayList<>();
416
417        for (Entry<PointDetails, Source> e : nxpair.entrySet()) {
418            Object obj = (e.getKey()).getRefObject();
419            LayoutEditor pan = (e.getKey()).getPanel();
420            if (pan == panel && !list.contains(obj)) {
421                list.add(obj);
422            }
423        }
424        return list;
425    }
426
427    public Source getSourceForPoint(PointDetails pd) {
428        return nxpair.get(pd);
429    }
430
431    public int getNxPairNumbers(LayoutEditor panel) {
432        int total = 0;
433        for (Entry<PointDetails, Source> e : nxpair.entrySet()) {
434            PointDetails key = e.getKey();
435            LayoutEditor pan = key.getPanel();
436            if (pan == panel) {
437                total += e.getValue().getNumberOfDestinations();
438            } // end while
439        }
440
441        return total;
442    }
443
444    /**
445     * Set a reversed route between two points.  Special case to support a LogixNG action.
446     * @since 5.5.7
447     * @param nxPair The system or user name of the destination point.
448     */
449    public void setReversedRoute(String nxPair) {
450        DestinationPoints dp = getNamedBean(nxPair);
451        if (dp != null) {
452            String destUUID = dp.getUniqueId();
453            nxpair.forEach((pd, src) -> {
454                for (String srcUUID : src.getDestinationUniqueId()) {
455                    if (destUUID.equals(srcUUID)) {
456                        log.debug("Found the correct reverse route source: src = {}, dest = {}",
457                                pd.getSensor().getDisplayName(), dp.getDestPoint().getSensor().getDisplayName());
458                        refCounter++;
459                        routesToSet.add(new SourceToDest(src, dp, true, refCounter));
460                        processRoutesToSet();
461                        return;
462                    }
463                }
464            });
465        }
466    }
467
468    /**
469     * Set the route between the two points represented by the Destination Point name.
470     *
471     * @since 4.11.1
472     * @param nxPair The system or user name of the destination point.
473     */
474    public void setSingleSegmentRoute(String nxPair) {
475        DestinationPoints dp = getNamedBean(nxPair);
476        if (dp != null) {
477            String destUUID = dp.getUniqueId();
478            nxpair.forEach((pd, src) -> {
479                for (String srcUUID : src.getDestinationUniqueId()) {
480                    if (destUUID.equals(srcUUID)) {
481                        log.debug("Found the correct source: src = {}, dest = {}",
482                                pd.getSensor().getDisplayName(), dp.getDestPoint().getSensor().getDisplayName());
483                        setMultiPointRoute(pd, dp.getDestPoint());
484                        return;
485                    }
486                }
487            });
488        }
489    }
490
491    public void setMultiPointRoute(PointDetails requestpd, LayoutEditor panel) {
492        for (PointDetails pd : pointDetails) {
493            if ( pd != requestpd && pd.getNXState() == NXBUTTONSELECTED ) {
494                setMultiPointRoute(pd, requestpd);
495                return;
496            }
497        }
498    }
499
500    private void setMultiPointRoute(PointDetails fromPd, PointDetails toPd) {
501        log.debug("[setMultiPointRoute] Start, from = {}, to = {}", fromPd.getSensor().getDisplayName(), toPd.getSensor().getDisplayName());
502        boolean cleardown = false;
503        if (fromPd.isRouteFromPointSet() && toPd.isRouteToPointSet()) {
504            cleardown = true;
505        }
506        for (LayoutBlock pro : fromPd.getProtecting()) {
507            try {
508                jmri.jmrit.display.layoutEditor.LayoutBlockManager lbm = InstanceManager.getDefault(
509                    jmri.jmrit.display.layoutEditor.LayoutBlockManager.class);
510                LayoutBlock toProt = null;
511                if (!toPd.getProtecting().isEmpty()) {
512                    toProt = toPd.getProtecting().get(0);
513                }
514                log.trace("Calling checkValidDest");
515                boolean result = lbm.getLayoutBlockConnectivityTools().checkValidDest(
516                    fromPd.getFacing(), pro, toPd.getFacing(), toProt,
517                        LayoutBlockConnectivityTools.Routing.NONE); // Was SENSORTOSENSOR, needs to be consistent with three lines below
518                                                                    // See also around line 353 in LayoutBlockConnectivityTools
519                log.trace("         checkValidDest returned {}", result);
520                if (result) {
521                    List<LayoutBlock> blkList = lbm.getLayoutBlockConnectivityTools().getLayoutBlocks(fromPd.getFacing(), toPd.getFacing(), pro, cleardown, LayoutBlockConnectivityTools.Routing.NONE);
522                    if (!blkList.isEmpty()) {
523                        if (log.isDebugEnabled()) {
524                            log.debug("[setMultiPointRoute] blocks and sensors");
525                            for (LayoutBlock blk : blkList) {
526                                log.debug("  blk = {}", blk.getDisplayName());
527                            }
528                        }
529                        List<jmri.NamedBean> beanList = lbm.getLayoutBlockConnectivityTools().getBeansInPath(blkList, null, jmri.Sensor.class);
530                        PointDetails fromPoint = fromPd;
531                        refCounter++;
532                        if (!beanList.isEmpty()) {
533                            if (log.isDebugEnabled()) {
534                                for (NamedBean xnb : beanList) {
535                                    log.debug("  sensor = {}", xnb.getDisplayName());
536                                }
537                            }
538                            for (int i = 1; i < beanList.size(); i++) {
539                                NamedBean nb = beanList.get(i);
540                                PointDetails cur = getPointDetails(nb, fromPd.getPanel());
541                                Source s = nxpair.get(fromPoint);
542                                if (s != null) {
543                                    routesToSet.add(new SourceToDest(s, s.getDestForPoint(cur), false, refCounter));
544                                }
545                                fromPoint = cur;
546                            }
547                        }
548                        Source s = nxpair.get(fromPoint);
549                        if (s != null) {
550                            if (s.getDestForPoint(toPd) != null) {
551                                routesToSet.add(new SourceToDest(s, s.getDestForPoint(toPd), false, refCounter));
552                            }
553                        }
554                        log.debug("[setMultiPointRoute] Invoke processRoutesToSet");
555                        processRoutesToSet();
556                        log.debug("[setMultiPointRoute] processRoutesToSet is done");
557                        return;
558                    }
559                }
560            } catch (jmri.JmriException e) {
561                log.debug("setMultiPointRoute catches exception", e);
562                // Can be considered normal if route is blocked
563                if (!isQuietFail()) {
564                    JmriJOptionPane.showMessageDialog(null,
565                            Bundle.getMessage("MultiPointBlocked"),  // NOI18N
566                            Bundle.getMessage("WarningTitle"),  // NOI18N
567                            JmriJOptionPane.WARNING_MESSAGE);
568                }
569            }
570        }
571        fromPd.setNXButtonState(NXBUTTONINACTIVE);
572        toPd.setNXButtonState(NXBUTTONINACTIVE);
573        log.debug("[setMultiPointRoute] Done, from = {}, to = {}", fromPd.getSensor().getDisplayName(), toPd.getSensor().getDisplayName());
574    }
575
576    int refCounter = 0;
577
578    /**
579     * List holding SourceToDest sets of routes between two points.
580     */
581    List<SourceToDest> routesToSet = new ArrayList<>();
582
583    /**
584     * Class to store NX sets consisting of a source point, a destination point,
585     * a direction and a reference.
586     */
587    private static class SourceToDest {
588
589        Source s = null;
590        DestinationPoints dp = null;
591        boolean direction = false;
592        int ref = -1;
593
594        /**
595         * Constructor for a SourceToDest element.
596         *
597         * @param s   a source point
598         * @param dp  a destination point
599         * @param dir a direction
600         * @param ref Integer used as reference
601         */
602        SourceToDest(Source s, DestinationPoints dp, boolean dir, int ref) {
603            this.s = s;
604            this.dp = dp;
605            this.direction = dir;
606            this.ref = ref;
607        }
608    }
609
610    int currentDealing = 0;
611
612    /**
613     * Activate each SourceToDest set in routesToSet
614     */
615    synchronized void processRoutesToSet() {
616        if (log.isDebugEnabled()) {
617            log.debug("[processRoutesToSet] Current routesToSet list");
618            for (SourceToDest sd : routesToSet) {
619                String dpName = (sd.dp == null) ? "- null -" : sd.dp.getDestPoint().getSensor().getDisplayName();
620                log.debug("  from = {}, to = {}, ref = {}", sd.s.getPoint().getSensor().getDisplayName(), dpName, sd.ref);
621            }
622        }
623
624        if (routesToSet.isEmpty()) {
625            return;
626        }
627        Source s = routesToSet.get(0).s;
628        DestinationPoints dp = routesToSet.get(0).dp;
629        boolean dir = routesToSet.get(0).direction;
630        currentDealing = routesToSet.get(0).ref;
631        routesToSet.remove(0);
632
633        dp.addPropertyChangeListener(propertyDestinationListener);
634        s.activeBean(dp, dir);
635    }
636
637    /**
638     * Remove remaining SourceToDest sets in routesToSet
639     */
640    synchronized void removeRemainingRoute() {
641        List<SourceToDest> toRemove = new ArrayList<>();
642        for (SourceToDest rts : routesToSet) {
643            if (rts.ref == currentDealing) {
644                toRemove.add(rts);
645                rts.dp.getDestPoint().setNXButtonState(NXBUTTONINACTIVE);
646            }
647        }
648        for (SourceToDest rts : toRemove) {
649            routesToSet.remove(rts);
650        }
651    }
652
653    protected PropertyChangeListener propertyDestinationListener = new PropertyChangeListener() {
654        @Override
655        public void propertyChange(PropertyChangeEvent e) {
656            ((DestinationPoints) e.getSource()).removePropertyChangeListener(this);
657            if ( DestinationPoints.PROPERTY_ACTIVE.equals(e.getPropertyName())) {
658                processRoutesToSet();
659            } else if ( DestinationPoints.PROPERTY_STACKED.equals(e.getPropertyName())
660                    || DestinationPoints.PROPERTY_FAILED.equals(e.getPropertyName())
661                    || DestinationPoints.PROPERTY_NO_CHANGE.equals(e.getPropertyName())) {
662                removeRemainingRoute();
663            }
664        }
665    };
666
667    private List<Object> destinationList = new ArrayList<>();
668
669    // Need to sort out the presentation of the name here rather than using the point ID.
670    // This is used for the creation and display of information in the table.
671    // The presentation of the name might have to be done at the table level.
672    public List<Object> getNxSource(LayoutEditor panel) {
673        List<Object> source = new ArrayList<>();
674        destinationList = new ArrayList<>();
675
676        for (Entry<PointDetails, Source> e : nxpair.entrySet()) {
677            PointDetails key = e.getKey();
678            LayoutEditor pan = key.getPanel();
679            if (pan == panel) {
680                List<PointDetails> dest = nxpair.get(key).getDestinationPoints();
681                for (int i = 0; i < dest.size(); i++) {
682                    destinationList.add(dest.get(i).getRefObject());
683                    source.add(key.getRefObject());
684                }
685            }
686        }
687        return source;
688    }
689
690    public List<Object> getNxDestination() {
691        return destinationList;
692    }
693
694    public List<LayoutEditor> getSourcePanelList() {
695        List<LayoutEditor> list = new ArrayList<>();
696
697        for (Entry<PointDetails, Source> e : nxpair.entrySet()) {
698            PointDetails key = e.getKey();
699            LayoutEditor pan = key.getPanel();
700            if (!list.contains(pan)) {
701                list.add(pan);
702            }
703        }
704        return list;
705    }
706
707    /**
708     * Return a point if it already exists, or create a new one if not.
709     */
710    private PointDetails providePoint(LayoutBlock source, List<LayoutBlock> protecting, LayoutEditor panel) {
711        PointDetails sourcePoint = getPointDetails(source, protecting, panel);
712        if (sourcePoint == null) {
713            sourcePoint = new PointDetails(source, protecting);
714            sourcePoint.setPanel(panel);
715        }
716        return sourcePoint;
717    }
718
719    /**
720     * @since 4.17.4
721     */
722    @Override
723    public void propertyChange(PropertyChangeEvent evt) {
724        firePropertyChange(PROPERTY_ACTIVE, evt.getOldValue(), evt.getNewValue());
725    }
726
727
728    public void addNXDestination(NamedBean source, NamedBean destination, LayoutEditor panel) {
729        addNXDestination(source, destination, panel, null);
730    }
731
732    /**
733     * @since 4.17.4
734     * Register in Property Change Listener.
735     * @param source the source bean.
736     * @param destination the destination bean.
737     * @param panel the layout editor panel.
738     * @param id the points details id.
739     */
740    public void addNXDestination(NamedBean source, NamedBean destination, LayoutEditor panel, String id) {
741        if (source == null) {
742            log.error("no source Object provided");  // NOI18N
743            return;
744        }
745        if (destination == null) {
746            log.error("no destination Object provided");  // NOI18N
747            return;
748        }
749        PointDetails sourcePoint = providePoint(source, panel);
750        if (sourcePoint == null) {
751            log.error("source point for {} not created addNXDes", source.getDisplayName());  // NOI18N
752            return;
753        }
754
755        sourcePoint.setPanel(panel);
756        sourcePoint.setRefObject(source);
757        PointDetails destPoint = providePoint(destination, panel);
758        if (destPoint != null) {
759            destPoint.setPanel(panel);
760            destPoint.setRefObject(destination);
761            destPoint.getSignal();
762            if (!nxpair.containsKey(sourcePoint)) {
763                Source sp = new Source(sourcePoint);
764                nxpair.put(sourcePoint, sp);
765                sp.removePropertyChangeListener(this);
766                sp.addPropertyChangeListener(this);
767            }
768            nxpair.get(sourcePoint).addDestination(destPoint, id);
769        }
770
771        firePropertyChange(PROPERTY_LENGTH, null, null);
772    }
773
774    public List<Object> getDestinationList(Object obj, LayoutEditor panel) {
775        List<Object> list = new ArrayList<>();
776        if (nxpair.containsKey(getPointDetails(obj, panel))) {
777            List<PointDetails> from = nxpair.get(getPointDetails(obj, panel)).getDestinationPoints();
778            for (int i = 0; i < from.size(); i++) {
779                list.add(from.get(i).getRefObject());
780            }
781        }
782        return list;
783    }
784
785    public void removeNXSensor(Sensor sensor) {
786        log.info("panel maintenance has resulting in the request to remove a sensor: {}", sensor.getDisplayName());
787    }
788
789    // ============ NX Pair Delete Methods ============
790    // The request will be for all NX Pairs containing a sensor or
791    // a specific entry and exit sensor pair.
792
793    /**
794     * Entry point to delete all of the NX pairs for a specific sensor.
795     * 1) Build a list of affected NX pairs.
796     * 2) Check for Conditional references.
797     * 3) If no references, do the delete process with user approval.
798     * @since 4.11.2
799     * @param sensor The sensor whose pairs should be deleted.
800     * @return true if the delete was successful. False if prevented by
801     * Conditional/LogixNG references or user choice.
802     */
803    public boolean deleteNxPair(NamedBean sensor) {
804        if (sensor == null) {
805            log.error("deleteNxPair: sensor is null");  // NOI18N
806            return false;
807        }
808        createDeletePairList(sensor);
809        if (checkNxPairs() && checkLogixNG()) {
810            // No Conditional or LogixNG references.
811            if (confirmDeletePairs()) {
812                deleteNxPairs();
813                return true;
814            }
815        }
816        return false;
817    }
818
819    /**
820     * Entry point to delete a specific NX pair.
821     *
822     * @since 4.11.2
823     * @param entrySensor The sensor that acts as the entry point.
824     * @param exitSensor The sensor that acts as the exit point.
825     * @param panel The layout editor panel that contains the entry sensor.
826     * @return true if the delete was successful. False if there are Conditional/LogixNG references.
827     */
828    public boolean deleteNxPair(NamedBean entrySensor, NamedBean exitSensor, LayoutEditor panel) {
829        if (entrySensor == null || exitSensor == null || panel == null) {
830            log.error("deleteNxPair: One or more null inputs");  // NOI18N
831            return false;
832        }
833
834        deletePairList.clear();
835        deletePairList.add(new DeletePair(entrySensor, exitSensor, panel));
836        if (checkNxPairs() && checkLogixNG()) {
837            // No Conditional or LogixNG references.
838            deleteNxPairs();  // Delete with no prompt
839            return true;
840        }
841
842        return false;
843    }
844
845    /**
846     * Find Logix Conditionals that have Variables or Actions for the affected NX Pairs.
847     * If any are found, display a dialog box listing the Conditionals and return false.
848     * <p>
849     * @since 4.11.2
850     * @return true if there are no references.
851     */
852    private boolean checkNxPairs() {
853        jmri.LogixManager mgr = InstanceManager.getDefault(jmri.LogixManager.class);
854        List<String> conditionalReferences = new ArrayList<>();
855        for (DeletePair dPair : deletePairList) {
856            if (dPair.dp == null) {
857                continue;
858            }
859            for (jmri.Logix lgx : mgr.getNamedBeanSet()) {
860                for (int i = 0; i < lgx.getNumConditionals(); i++) {
861                    String cdlName = lgx.getConditionalByNumberOrder(i);
862                    jmri.implementation.DefaultConditional cdl = (jmri.implementation.DefaultConditional) lgx.getConditional(cdlName);
863                    String cdlUserName = cdl.getUserName();
864                    if (cdlUserName == null) {
865                        cdlUserName = "";
866                    }
867                    for (jmri.ConditionalVariable var : cdl.getStateVariableList()) {
868                        if (var.getBean() == dPair.dp) {
869                            String refName = (cdlUserName.equals("")) ? cdlName : cdlName + "  ( " + cdlUserName + " )";
870                            if (!conditionalReferences.contains(refName)) {
871                                conditionalReferences.add(refName);
872                            }
873                        }
874                    }
875                    for (jmri.ConditionalAction act : cdl.getActionList()) {
876                        if (act.getBean() == dPair.dp) {
877                            String refName = (cdlUserName.equals("")) ? cdlName : cdlName + "  ( " + cdlUserName + " )";
878                            if (!conditionalReferences.contains(refName)) {
879                                conditionalReferences.add(refName);
880                            }
881                        }
882                    }
883                }
884            }
885        }
886        if (conditionalReferences.isEmpty()) {
887            return true;
888        }
889
890        conditionalReferences.sort(null);
891        StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteReferences"));
892        for (String ref : conditionalReferences) {
893            msg.append("\n    " + ref);  // NOI18N
894        }
895        JmriJOptionPane.showMessageDialog(null,
896                msg.toString(),
897                Bundle.getMessage("WarningTitle"),  // NOI18N
898                JmriJOptionPane.WARNING_MESSAGE);
899
900        return false;
901    }
902
903    /**
904     * Find LogixNG ConditionalNGs that have Expressions or Actions for the affected NX Pairs.
905     * If any are found, display a dialog box listing the details and return false.
906     * <p>
907     * @since 5.5.7
908     * @return true if there are no references.
909     */
910    private boolean checkLogixNG() {
911        List<String> conditionalReferences = new ArrayList<>();
912        for (DeletePair dPair : deletePairList) {
913            if (dPair.dp == null) {
914                continue;
915            }
916            var usage = jmri.jmrit.logixng.util.WhereUsed.whereUsed(dPair.dp);
917            if (!usage.isEmpty()) {
918                conditionalReferences.add(usage);
919            }
920        }
921        if (conditionalReferences.isEmpty()) {
922            return true;
923        }
924
925        conditionalReferences.sort(null);
926        StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteReferences"));
927        for (String ref : conditionalReferences) {
928            msg.append("\n" + ref);  // NOI18N
929        }
930        JmriJOptionPane.showMessageDialog(null,
931                msg.toString(),
932                Bundle.getMessage("WarningTitle"),  // NOI18N
933                JmriJOptionPane.WARNING_MESSAGE);
934
935        return false;
936    }
937
938    /**
939     * Display a list of pending deletes and ask for confirmation.
940     * @since 4.11.2
941     * @return true if deletion confirmation is Yes.
942     */
943    private boolean confirmDeletePairs() {
944        if (!deletePairList.isEmpty()) {
945            StringBuilder msg = new StringBuilder(Bundle.getMessage("DeletePairs"));  // NOI18N
946            for (DeletePair dPair : deletePairList) {
947                if (dPair.dp != null) {
948                    msg.append("\n    ").append(dPair.dp.getDisplayName());  // NOI18N
949                }
950            }
951            msg.append("\n").append(Bundle.getMessage("DeleteContinue"));  // NOI18N
952            int resp = JmriJOptionPane.showConfirmDialog(null,
953                    msg.toString(),
954                    Bundle.getMessage("WarningTitle"),  // NOI18N
955                    JmriJOptionPane.YES_NO_OPTION,
956                    JmriJOptionPane.QUESTION_MESSAGE);
957            if (resp != JmriJOptionPane.YES_OPTION ) {
958                return false;
959            }
960        }
961        return true;
962    }
963
964    /**
965     * Delete the pairs in the delete pair list.
966     * @since 4.11.2
967     * @since 4.17.4
968     * Remove from Change Listener.
969     */
970    private void deleteNxPairs() {
971        for (DeletePair dp : deletePairList) {
972            PointDetails sourcePoint = getPointDetails(dp.src, dp.pnl);
973            PointDetails destPoint = getPointDetails(dp.dest, dp.pnl);
974            nxpair.get(sourcePoint).removeDestination(destPoint);
975            firePropertyChange(PROPERTY_LENGTH, null, null);
976            if (nxpair.get(sourcePoint).getDestinationPoints().isEmpty()) {
977                nxpair.get(sourcePoint).removePropertyChangeListener(this);
978                nxpair.remove(sourcePoint);
979            }
980        }
981    }
982
983    /**
984     * List of NX pairs that are scheduled for deletion.
985     * @since 4.11.2
986     */
987    List<DeletePair> deletePairList = new ArrayList<>();
988
989    /**
990     * Class to store NX pair components.
991     * @since 4.11.2
992     */
993    private class DeletePair {
994        NamedBean src = null;
995        NamedBean dest = null;
996        LayoutEditor pnl = null;
997        DestinationPoints dp = null;
998
999        /**
1000         * Constructor for a DeletePair row.
1001         *
1002         * @param src  Source sensor bean
1003         * @param dest Ddestination sensor bean
1004         * @param pnl  The LayoutEditor panel for the source bean
1005         */
1006        DeletePair(NamedBean src, NamedBean dest, LayoutEditor pnl) {
1007            this.src = src;
1008            this.dest = dest;
1009            this.pnl = pnl;
1010
1011            // Get the actual destination point, if any.
1012            PointDetails sourcePoint = getPointDetails(src, pnl);
1013            PointDetails destPoint = getPointDetails(dest, pnl);
1014            if (sourcePoint != null && destPoint != null) {
1015                if (nxpair.containsKey(sourcePoint)) {
1016                    this.dp = nxpair.get(sourcePoint).getDestForPoint(destPoint);
1017                }
1018            }
1019        }
1020    }
1021
1022    /**
1023     * Rebuild the delete pair list based on the supplied sensor.
1024     * Find all of the NX pairs that use this sensor as either a source or
1025     * destination.  They will be candidates for deletion.
1026     *
1027     * @since 4.11.2
1028     * @param sensor The sensor being deleted,
1029     */
1030    void createDeletePairList(NamedBean sensor) {
1031        deletePairList.clear();
1032        nxpair.forEach((pdSrc, src) -> {
1033            Sensor sBean = pdSrc.getSensor();
1034            LayoutEditor sPanel = pdSrc.getPanel();
1035            for (PointDetails pdDest : src.getDestinationPoints()) {
1036                Sensor dBean = pdDest.getSensor();
1037                if (sensor == sBean || sensor == dBean) {
1038                    log.debug("Delete pair: {} to {}, panel = {}",  // NOI18N
1039                            sBean.getDisplayName(), dBean.getDisplayName(), sPanel.getLayoutName());
1040                    deletePairList.add(new DeletePair(sBean, dBean, sPanel));
1041                }
1042            }
1043        });
1044    }
1045
1046    // ============ End NX Pair Delete Methods ============
1047
1048    /**
1049     * Create a list of sensors that have the layout block as either
1050     * facing or protecting.
1051     * Called by {@link jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.LayoutTrackEditor#hasNxSensorPairs}.
1052     * @since 4.11.2
1053     * @param layoutBlock The layout block to be checked.
1054     * @return the a list of sensors affected by the layout block or an empty list.
1055     */
1056    public List<String> layoutBlockSensors(@Nonnull LayoutBlock layoutBlock) {
1057        log.debug("layoutBlockSensors: {}", layoutBlock.getDisplayName());
1058        List<String> blockSensors = new ArrayList<>();
1059        nxpair.forEach((pdSrc, src) -> {
1060            Sensor sBean = pdSrc.getSensor();
1061            for (LayoutBlock sProtect : pdSrc.getProtecting()) {
1062                if (layoutBlock == pdSrc.getFacing() || layoutBlock == sProtect) {
1063                    log.debug("  Source = '{}', Facing = '{}', Protecting = '{}'         ",
1064                            sBean.getDisplayName(), pdSrc.getFacing().getDisplayName(), sProtect.getDisplayName());
1065                    blockSensors.add(sBean.getDisplayName());
1066                }
1067            }
1068
1069            for (PointDetails pdDest : src.getDestinationPoints()) {
1070                Sensor dBean = pdDest.getSensor();
1071                for (LayoutBlock dProtect : pdDest.getProtecting()) {
1072                    if (layoutBlock == pdDest.getFacing() || layoutBlock == dProtect) {
1073                        log.debug("    Destination = '{}', Facing = '{}', Protecting = '{}'     ",
1074                                dBean.getDisplayName(), pdDest.getFacing().getDisplayName(), dProtect.getDisplayName());
1075                        blockSensors.add(dBean.getDisplayName());
1076                    }
1077                }
1078            }
1079        });
1080        return blockSensors;
1081    }
1082
1083    public boolean isDestinationValid(Object source, Object dest, LayoutEditor panel) {
1084        if (nxpair.containsKey(getPointDetails(source, panel))) {
1085            return nxpair.get(getPointDetails(source, panel)).isDestinationValid(getPointDetails(dest, panel));
1086        }
1087        return false;
1088    }
1089
1090    public boolean isUniDirection(Object source, LayoutEditor panel, Object dest) {
1091        if (nxpair.containsKey(getPointDetails(source, panel))) {
1092            return nxpair.get(getPointDetails(source, panel)).getUniDirection(dest, panel);
1093        }
1094        return false;
1095    }
1096
1097    public void setUniDirection(Object source, LayoutEditor panel, Object dest, boolean set) {
1098        if (nxpair.containsKey(getPointDetails(source, panel))) {
1099            nxpair.get(getPointDetails(source, panel)).setUniDirection(dest, panel, set);
1100        }
1101    }
1102
1103    public boolean canBeBiDirectional(Object source, LayoutEditor panel, Object dest) {
1104        if (nxpair.containsKey(getPointDetails(source, panel))) {
1105            return nxpair.get(getPointDetails(source, panel)).canBeBiDirection(dest, panel);
1106        }
1107        return false;
1108    }
1109
1110    public boolean isEnabled(Object source, LayoutEditor panel, Object dest) {
1111        if (nxpair.containsKey(getPointDetails(source, panel))) {
1112            return nxpair.get(getPointDetails(source, panel)).isEnabled(dest, panel);
1113        }
1114        return false;
1115    }
1116
1117    public void setEnabled(Object source, LayoutEditor panel, Object dest, boolean set) {
1118        if (nxpair.containsKey(getPointDetails(source, panel))) {
1119            nxpair.get(getPointDetails(source, panel)).setEnabled(dest, panel, set);
1120        }
1121    }
1122
1123    public void setEntryExitType(Object source, LayoutEditor panel, Object dest, int set) {
1124        if (nxpair.containsKey(getPointDetails(source, panel))) {
1125            nxpair.get(getPointDetails(source, panel)).setEntryExitType(dest, panel, set);
1126        }
1127    }
1128
1129    public int getEntryExitType(Object source, LayoutEditor panel, Object dest) {
1130        if (nxpair.containsKey(getPointDetails(source, panel))) {
1131            return nxpair.get(getPointDetails(source, panel)).getEntryExitType(dest, panel);
1132        }
1133        return 0x00;
1134    }
1135
1136    public String getUniqueId(Object source, LayoutEditor panel, Object dest) {
1137        if (nxpair.containsKey(getPointDetails(source, panel))) {
1138            return nxpair.get(getPointDetails(source, panel)).getUniqueId(dest, panel);
1139        }
1140        return null;
1141    }
1142
1143    public List<String> getEntryExitList() {
1144        List<String> destlist = new ArrayList<>();
1145        for (Source e : nxpair.values()) {
1146            destlist.addAll(e.getDestinationUniqueId());
1147        }
1148        return destlist;
1149    }
1150
1151    // protecting helps us to determine which direction we are going.
1152    // validateOnly flag is used, if all we are doing is simply checking to see if the source/destpoints are valid
1153    // when creating the pairs in the user GUI
1154    public boolean isPathActive(Object sourceObj, Object destObj, LayoutEditor panel) {
1155        PointDetails pd = getPointDetails(sourceObj, panel);
1156        if (nxpair.containsKey(pd)) {
1157            Source source = nxpair.get(pd);
1158            return source.isRouteActive(getPointDetails(destObj, panel));
1159        }
1160        return false;
1161    }
1162
1163    public void cancelInterlock(Object source, LayoutEditor panel, Object dest) {
1164        if (nxpair.containsKey(getPointDetails(source, panel))) {
1165            nxpair.get(getPointDetails(source, panel)).cancelInterlock(dest, panel);
1166        }
1167
1168    }
1169
1170    jmri.SignalMastLogicManager smlm = InstanceManager.getDefault(jmri.SignalMastLogicManager.class);
1171
1172    public final static int CANCELROUTE = 0;
1173    public final static int CLEARROUTE = 1;
1174    public final static int EXITROUTE = 2;
1175    public final static int STACKROUTE = 4;
1176
1177   /**
1178     * Return a point from a given LE Panel.
1179     *
1180     * @param obj The point object
1181     * @param panel The Layout Editor panel on which the point was placed
1182     * @return the point object, null if the point is not found
1183     */
1184    public PointDetails getPointDetails(Object obj, LayoutEditor panel) {
1185        for (int i = 0; i < pointDetails.size(); i++) {
1186            if ((pointDetails.get(i).getRefObject() == obj)) {
1187                return pointDetails.get(i);
1188
1189            }
1190        }
1191        return null;
1192    }
1193
1194    /**
1195     * Return either an existing point stored in pointDetails, or create a new one as required.
1196     *
1197     * @param source The Layout Block functioning as the source (origin)
1198     * @param destination A (list of) Layout Blocks functioning as destinations
1199     * @param panel The Layout Editor panel on which the point is to be placed
1200     * @return the point object
1201     */
1202    PointDetails getPointDetails(LayoutBlock source, List<LayoutBlock> destination, LayoutEditor panel) {
1203        PointDetails newPoint = new PointDetails(source, destination);
1204        newPoint.setPanel(panel);
1205        for (int i = 0; i < pointDetails.size(); i++) {
1206            if (pointDetails.get(i).equals(newPoint)) {
1207                return pointDetails.get(i);
1208            }
1209        }
1210        //Not found so will add
1211        pointDetails.add(newPoint);
1212        return newPoint;
1213    }
1214
1215    //No point can have multiple copies of what is the same thing.
1216    static List<PointDetails> pointDetails = new ArrayList<PointDetails>();
1217
1218    /**
1219     * Get the name of a destinationPoint on a LE Panel.
1220     *
1221     * @param obj the point object
1222     * @param panel The Layout Editor panel on which it is expected to be placed
1223     * @return the name of the point
1224     */
1225    public String getPointAsString(NamedBean obj, LayoutEditor panel) {
1226        if (obj == null) {
1227            return "null";  // NOI18N
1228        }
1229        PointDetails valid = getPointDetails(obj, panel);  //was just plain getPoint
1230        if (valid != null) {
1231            return valid.getDisplayName();
1232        }
1233        return "empty";  // NOI18N
1234    }
1235
1236    List<StackDetails> stackList = new ArrayList<>();
1237
1238    /**
1239     * If a route is requested but is currently blocked, ask user
1240     * if it should be added to stackList.
1241     *
1242     * @param dp DestinationPoints object
1243     * @param reverse true for a reversed running direction, mostly false
1244     */
1245    synchronized public void stackNXRoute(DestinationPoints dp, boolean reverse) {
1246        if (isRouteStacked(dp, reverse)) {
1247            return;
1248        }
1249        stackList.add(new StackDetails(dp, reverse));
1250        checkTimer.start();
1251        if (stackPanel == null) {
1252            stackPanel = new StackNXPanel();
1253        }
1254        if (stackDialog == null) {
1255            stackDialog = new JDialog();
1256            stackDialog.setTitle(Bundle.getMessage("WindowTitleStackRoutes"));  // NOI18N
1257            stackDialog.add(stackPanel);
1258        }
1259        stackPanel.updateGUI();
1260
1261        stackDialog.pack();
1262        stackDialog.setModal(false);
1263        stackDialog.setVisible(true);
1264    }
1265
1266    StackNXPanel stackPanel = null;
1267    JDialog stackDialog = null;
1268
1269    /**
1270     * Get a list of all stacked routes from stackList.
1271     *
1272     * @return an List containing destinationPoint elements
1273     */
1274    public List<DestinationPoints> getStackedInterlocks() {
1275        List<DestinationPoints> dpList = new ArrayList<>();
1276        for (StackDetails st : stackList) {
1277            dpList.add(st.getDestinationPoint());
1278        }
1279        return dpList;
1280    }
1281
1282    /**
1283     * Query if a stacked route is in stackList.
1284     *
1285     * @param dp DestinationPoints object
1286     * @param reverse true for a reversed running direction, mostly false
1287     * @return true if dp is in stackList
1288     */
1289    public boolean isRouteStacked(DestinationPoints dp, boolean reverse) {
1290        Iterator<StackDetails> iter = stackList.iterator();
1291        while (iter.hasNext()) {
1292            StackDetails st = iter.next();
1293            if (st.getDestinationPoint() == dp && st.getReverse() == reverse) {
1294                return true;
1295            }
1296        }
1297        return false;
1298    }
1299
1300    /**
1301     * Remove a stacked route from stackList.
1302     *
1303     * @param dp DestinationPoints object
1304     * @param reverse true for a reversed running direction, mostly false
1305     */
1306    synchronized public void cancelStackedRoute(DestinationPoints dp, boolean reverse) {
1307        Iterator<StackDetails> iter = stackList.iterator();
1308        while (iter.hasNext()) {
1309            StackDetails st = iter.next();
1310            if (st.getDestinationPoint() == dp && st.getReverse() == reverse) {
1311                iter.remove();
1312            }
1313        }
1314        stackPanel.updateGUI();
1315        if (stackList.isEmpty()) {
1316            stackDialog.setVisible(false);
1317            checkTimer.stop();
1318        }
1319    }
1320
1321    /**
1322     * Class to collect (stack) routes when they are requested but blocked.
1323     */
1324    static class StackDetails {
1325
1326        DestinationPoints dp;
1327        boolean reverse;
1328
1329        StackDetails(DestinationPoints dp, boolean reverse) {
1330            this.dp = dp;
1331            this.reverse = reverse;
1332        }
1333
1334        boolean getReverse() {
1335            return reverse;
1336        }
1337
1338        DestinationPoints getDestinationPoint() {
1339            return dp;
1340        }
1341    }
1342
1343    javax.swing.Timer checkTimer = new javax.swing.Timer(10000, (java.awt.event.ActionEvent e) -> {
1344        checkRoute();
1345    });
1346
1347    /**
1348     * Step through stackList and activate the first stacked route in line
1349     * if it is no longer blocked.
1350     */
1351    synchronized void checkRoute() {
1352        checkTimer.stop();
1353        StackDetails[] tmp = new StackDetails[stackList.size()];
1354        stackList.toArray(tmp);
1355
1356        for (StackDetails st : tmp) {
1357            if (!st.getDestinationPoint().isActive()) {
1358                // If the route is not already active, then check.
1359                // If the route does get set, then the setting process will remove the route from the stack.
1360                st.getDestinationPoint().setInterlockRoute(st.getReverse());
1361            }
1362        }
1363
1364        if (!stackList.isEmpty()) {
1365            checkTimer.start();
1366        } else {
1367            stackDialog.setVisible(false);
1368        }
1369    }
1370
1371    public void removePropertyChangeListener(PropertyChangeListener list, NamedBean obj, LayoutEditor panel) {
1372        if (obj == null) {
1373            return;
1374        }
1375        PointDetails valid = getPointDetails(obj, panel);
1376        if (valid != null) {
1377            valid.removePropertyChangeListener(list);
1378        }
1379    }
1380
1381    boolean runWhenStabilised = false;
1382    LayoutEditor toUseWhenStable;
1383    int interlockTypeToUseWhenStable;
1384
1385    /**
1386     * Discover all possible valid source and destination Signal Mast Logic pairs
1387     * on all Layout Editor panels.
1388     *
1389     * @param editor The Layout Editor panel
1390     * @param interlockType Integer value representing the type of interlocking, one of
1391     *                      SETUPTURNOUTSONLY, SETUPSIGNALMASTLOGIC or FULLINTERLOCK
1392     * @throws JmriException when an error occurs during discovery
1393     */
1394    public void automaticallyDiscoverEntryExitPairs(LayoutEditor editor, int interlockType) throws JmriException {
1395        //This is almost a duplicate of that in the DefaultSignalMastLogicManager
1396        runWhenStabilised = false;
1397        jmri.jmrit.display.layoutEditor.LayoutBlockManager lbm = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class);
1398        if (!lbm.isAdvancedRoutingEnabled()) {
1399            throw new JmriException("advanced routing not enabled");  // NOI18N
1400        }
1401        if (!lbm.routingStablised()) {
1402            runWhenStabilised = true;
1403            toUseWhenStable = editor;
1404            interlockTypeToUseWhenStable = interlockType;
1405            log.debug("Layout block routing has not yet stabilised, discovery will happen once it has");  // NOI18N
1406            return;
1407        }
1408        HashMap<NamedBean, List<NamedBean>> validPaths = lbm.getLayoutBlockConnectivityTools().
1409                discoverValidBeanPairs(null, Sensor.class, LayoutBlockConnectivityTools.Routing.SENSORTOSENSOR);
1410        EntryExitPairs eep = this;
1411        for (Entry<NamedBean, List<NamedBean>> entry : validPaths.entrySet()) {
1412            NamedBean key = entry.getKey();
1413            List<NamedBean> validDestMast = validPaths.get(key);
1414            if (!validDestMast.isEmpty()) {
1415                eep.addNXSourcePoint(key, editor);
1416                for (int i = 0; i < validDestMast.size(); i++) {
1417                    if (!eep.isDestinationValid(key, validDestMast.get(i), editor)) {
1418                        eep.addNXDestination(key, validDestMast.get(i), editor);
1419                        eep.setEntryExitType(key, editor, validDestMast.get(i), interlockType);
1420                    }
1421                }
1422            }
1423        }
1424
1425        firePropertyChange(PROPERTY_AUTO_GENERATE_COMPLETE, null, null);
1426    }
1427
1428    protected PropertyChangeListener propertyBlockManagerListener = new PropertyChangeListener() {
1429        @Override
1430        public void propertyChange(PropertyChangeEvent e) {
1431            if ( LayoutBlockManager.PROPERTY_TOPOLOGY.equals(e.getPropertyName())) {
1432                boolean newValue = (Boolean) e.getNewValue();
1433                if (newValue) {
1434                    if (runWhenStabilised) {
1435                        try {
1436                            automaticallyDiscoverEntryExitPairs(toUseWhenStable, interlockTypeToUseWhenStable);
1437                        } catch (JmriException je) {
1438                            //Considered normal if routing not enabled
1439                        }
1440                    }
1441                }
1442            }
1443        }
1444    };
1445
1446    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
1447
1448    }
1449
1450    @Override
1451    public void deleteBean(@Nonnull DestinationPoints bean, @Nonnull String property) throws PropertyVetoException {
1452
1453    }
1454
1455    @Override
1456    @Nonnull
1457    public String getBeanTypeHandled(boolean plural) {
1458        return Bundle.getMessage(plural ? "BeanNameEntryExits" : "BeanNameEntryExit");  // NOI18N
1459    }
1460
1461    /**
1462     * {@inheritDoc}
1463     */
1464    @Override
1465    public Class<DestinationPoints> getNamedBeanClass() {
1466        return DestinationPoints.class;
1467    }
1468
1469    /**
1470     * {@inheritDoc}
1471     */
1472    @Override
1473    @OverridingMethodsMustInvokeSuper
1474    public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) {
1475        if (!Manager.PROPERTY_BEANS.equals(propertyName)) {
1476            throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced.");
1477        }
1478        silencedProperties.put(propertyName, silenced);
1479        if (propertyName.equals(Manager.PROPERTY_BEANS) && !silenced) {
1480            fireIndexedPropertyChange(Manager.PROPERTY_BEANS, getNamedBeanSet().size(), null, null);
1481        }
1482    }
1483
1484    /** {@inheritDoc} */
1485    @Override
1486    public void addDataListener(ManagerDataListener<DestinationPoints> e) {
1487        if (e != null) listeners.add(e);
1488    }
1489
1490    /** {@inheritDoc} */
1491    @Override
1492    public void removeDataListener(ManagerDataListener<DestinationPoints> e) {
1493        if (e != null) listeners.remove(e);
1494    }
1495
1496    final List<ManagerDataListener<DestinationPoints>> listeners = new ArrayList<>();
1497
1498    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EntryExitPairs.class);
1499
1500}