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