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                boolean result = lbm.getLayoutBlockConnectivityTools().checkValidDest(
497                    fromPd.getFacing(), pro, toPd.getFacing(), toProt,
498                        LayoutBlockConnectivityTools.Routing.SENSORTOSENSOR);
499                if (result) {
500                    List<LayoutBlock> blkList = lbm.getLayoutBlockConnectivityTools().getLayoutBlocks(fromPd.getFacing(), toPd.getFacing(), pro, cleardown, LayoutBlockConnectivityTools.Routing.NONE);
501                    if (!blkList.isEmpty()) {
502                        if (log.isDebugEnabled()) {
503                            log.debug("[setMultiPointRoute] blocks and sensors");
504                            for (LayoutBlock blk : blkList) {
505                                log.debug("  blk = {}", blk.getDisplayName());
506                            }
507                        }
508                        List<jmri.NamedBean> beanList = lbm.getLayoutBlockConnectivityTools().getBeansInPath(blkList, null, jmri.Sensor.class);
509                        PointDetails fromPoint = fromPd;
510                        refCounter++;
511                        if (!beanList.isEmpty()) {
512                            if (log.isDebugEnabled()) {
513                                for (NamedBean xnb : beanList) {
514                                    log.debug("  sensor = {}", xnb.getDisplayName());
515                                }
516                            }
517                            for (int i = 1; i < beanList.size(); i++) {
518                                NamedBean nb = beanList.get(i);
519                                PointDetails cur = getPointDetails(nb, fromPd.getPanel());
520                                Source s = nxpair.get(fromPoint);
521                                if (s != null) {
522                                    routesToSet.add(new SourceToDest(s, s.getDestForPoint(cur), false, refCounter));
523                                }
524                                fromPoint = cur;
525                            }
526                        }
527                        Source s = nxpair.get(fromPoint);
528                        if (s != null) {
529                            if (s.getDestForPoint(toPd) != null) {
530                                routesToSet.add(new SourceToDest(s, s.getDestForPoint(toPd), false, refCounter));
531                            }
532                        }
533                        log.debug("[setMultiPointRoute] Invoke processRoutesToSet");
534                        processRoutesToSet();
535                        log.debug("[setMultiPointRoute] processRoutesToSet is done");
536                        return;
537                    }
538                }
539            } catch (jmri.JmriException e) {
540                // Can be considered normal if route is blocked
541                JmriJOptionPane.showMessageDialog(null,
542                        Bundle.getMessage("MultiPointBlocked"),  // NOI18N
543                        Bundle.getMessage("WarningTitle"),  // NOI18N
544                        JmriJOptionPane.WARNING_MESSAGE);
545            }
546        }
547        fromPd.setNXButtonState(NXBUTTONINACTIVE);
548        toPd.setNXButtonState(NXBUTTONINACTIVE);
549        log.debug("[setMultiPointRoute] Done, from = {}, to = {}", fromPd.getSensor().getDisplayName(), toPd.getSensor().getDisplayName());
550    }
551
552    int refCounter = 0;
553
554    /**
555     * List holding SourceToDest sets of routes between two points.
556     */
557    List<SourceToDest> routesToSet = new ArrayList<>();
558
559    /**
560     * Class to store NX sets consisting of a source point, a destination point,
561     * a direction and a reference.
562     */
563    private static class SourceToDest {
564
565        Source s = null;
566        DestinationPoints dp = null;
567        boolean direction = false;
568        int ref = -1;
569
570        /**
571         * Constructor for a SourceToDest element.
572         *
573         * @param s   a source point
574         * @param dp  a destination point
575         * @param dir a direction
576         * @param ref Integer used as reference
577         */
578        SourceToDest(Source s, DestinationPoints dp, boolean dir, int ref) {
579            this.s = s;
580            this.dp = dp;
581            this.direction = dir;
582            this.ref = ref;
583        }
584    }
585
586    int currentDealing = 0;
587
588    /**
589     * Activate each SourceToDest set in routesToSet
590     */
591    synchronized void processRoutesToSet() {
592        if (log.isDebugEnabled()) {
593            log.debug("[processRoutesToSet] Current routesToSet list");
594            for (SourceToDest sd : routesToSet) {
595                String dpName = (sd.dp == null) ? "- null -" : sd.dp.getDestPoint().getSensor().getDisplayName();
596                log.debug("  from = {}, to = {}, ref = {}", sd.s.getPoint().getSensor().getDisplayName(), dpName, sd.ref);
597            }
598        }
599
600        if (routesToSet.isEmpty()) {
601            return;
602        }
603        Source s = routesToSet.get(0).s;
604        DestinationPoints dp = routesToSet.get(0).dp;
605        boolean dir = routesToSet.get(0).direction;
606        currentDealing = routesToSet.get(0).ref;
607        routesToSet.remove(0);
608
609        dp.addPropertyChangeListener(propertyDestinationListener);
610        s.activeBean(dp, dir);
611    }
612
613    /**
614     * Remove remaining SourceToDest sets in routesToSet
615     */
616    synchronized void removeRemainingRoute() {
617        List<SourceToDest> toRemove = new ArrayList<>();
618        for (SourceToDest rts : routesToSet) {
619            if (rts.ref == currentDealing) {
620                toRemove.add(rts);
621                rts.dp.getDestPoint().setNXButtonState(NXBUTTONINACTIVE);
622            }
623        }
624        for (SourceToDest rts : toRemove) {
625            routesToSet.remove(rts);
626        }
627    }
628
629    protected PropertyChangeListener propertyDestinationListener = new PropertyChangeListener() {
630        @Override
631        public void propertyChange(PropertyChangeEvent e) {
632            ((DestinationPoints) e.getSource()).removePropertyChangeListener(this);
633            if ( DestinationPoints.PROPERTY_ACTIVE.equals(e.getPropertyName())) {
634                processRoutesToSet();
635            } else if ( DestinationPoints.PROPERTY_STACKED.equals(e.getPropertyName())
636                    || DestinationPoints.PROPERTY_FAILED.equals(e.getPropertyName())
637                    || DestinationPoints.PROPERTY_NO_CHANGE.equals(e.getPropertyName())) {
638                removeRemainingRoute();
639            }
640        }
641    };
642
643    private List<Object> destinationList = new ArrayList<>();
644
645    // Need to sort out the presentation of the name here rather than using the point ID.
646    // This is used for the creation and display of information in the table.
647    // The presentation of the name might have to be done at the table level.
648    public List<Object> getNxSource(LayoutEditor panel) {
649        List<Object> source = new ArrayList<>();
650        destinationList = new ArrayList<>();
651
652        for (Entry<PointDetails, Source> e : nxpair.entrySet()) {
653            PointDetails key = e.getKey();
654            LayoutEditor pan = key.getPanel();
655            if (pan == panel) {
656                List<PointDetails> dest = nxpair.get(key).getDestinationPoints();
657                for (int i = 0; i < dest.size(); i++) {
658                    destinationList.add(dest.get(i).getRefObject());
659                    source.add(key.getRefObject());
660                }
661            }
662        }
663        return source;
664    }
665
666    public List<Object> getNxDestination() {
667        return destinationList;
668    }
669
670    public List<LayoutEditor> getSourcePanelList() {
671        List<LayoutEditor> list = new ArrayList<>();
672
673        for (Entry<PointDetails, Source> e : nxpair.entrySet()) {
674            PointDetails key = e.getKey();
675            LayoutEditor pan = key.getPanel();
676            if (!list.contains(pan)) {
677                list.add(pan);
678            }
679        }
680        return list;
681    }
682
683    /**
684     * Return a point if it already exists, or create a new one if not.
685     */
686    private PointDetails providePoint(LayoutBlock source, List<LayoutBlock> protecting, LayoutEditor panel) {
687        PointDetails sourcePoint = getPointDetails(source, protecting, panel);
688        if (sourcePoint == null) {
689            sourcePoint = new PointDetails(source, protecting);
690            sourcePoint.setPanel(panel);
691        }
692        return sourcePoint;
693    }
694
695    /**
696     * @since 4.17.4
697     */
698    @Override
699    public void propertyChange(PropertyChangeEvent evt) {
700        firePropertyChange(PROPERTY_ACTIVE, evt.getOldValue(), evt.getNewValue());
701    }
702
703
704    public void addNXDestination(NamedBean source, NamedBean destination, LayoutEditor panel) {
705        addNXDestination(source, destination, panel, null);
706    }
707
708    /**
709     * @since 4.17.4
710     * Register in Property Change Listener.
711     * @param source the source bean.
712     * @param destination the destination bean.
713     * @param panel the layout editor panel.
714     * @param id the points details id.
715     */
716    public void addNXDestination(NamedBean source, NamedBean destination, LayoutEditor panel, String id) {
717        if (source == null) {
718            log.error("no source Object provided");  // NOI18N
719            return;
720        }
721        if (destination == null) {
722            log.error("no destination Object provided");  // NOI18N
723            return;
724        }
725        PointDetails sourcePoint = providePoint(source, panel);
726        if (sourcePoint == null) {
727            log.error("source point for {} not created addNXDes", source.getDisplayName());  // NOI18N
728            return;
729        }
730
731        sourcePoint.setPanel(panel);
732        sourcePoint.setRefObject(source);
733        PointDetails destPoint = providePoint(destination, panel);
734        if (destPoint != null) {
735            destPoint.setPanel(panel);
736            destPoint.setRefObject(destination);
737            destPoint.getSignal();
738            if (!nxpair.containsKey(sourcePoint)) {
739                Source sp = new Source(sourcePoint);
740                nxpair.put(sourcePoint, sp);
741                sp.removePropertyChangeListener(this);
742                sp.addPropertyChangeListener(this);
743            }
744            nxpair.get(sourcePoint).addDestination(destPoint, id);
745        }
746
747        firePropertyChange(PROPERTY_LENGTH, null, null);
748    }
749
750    public List<Object> getDestinationList(Object obj, LayoutEditor panel) {
751        List<Object> list = new ArrayList<>();
752        if (nxpair.containsKey(getPointDetails(obj, panel))) {
753            List<PointDetails> from = nxpair.get(getPointDetails(obj, panel)).getDestinationPoints();
754            for (int i = 0; i < from.size(); i++) {
755                list.add(from.get(i).getRefObject());
756            }
757        }
758        return list;
759    }
760
761    public void removeNXSensor(Sensor sensor) {
762        log.info("panel maintenance has resulting in the request to remove a sensor: {}", sensor.getDisplayName());
763    }
764
765    // ============ NX Pair Delete Methods ============
766    // The request will be for all NX Pairs containing a sensor or
767    // a specific entry and exit sensor pair.
768
769    /**
770     * Entry point to delete all of the NX pairs for a specific sensor.
771     * 1) Build a list of affected NX pairs.
772     * 2) Check for Conditional references.
773     * 3) If no references, do the delete process with user approval.
774     * @since 4.11.2
775     * @param sensor The sensor whose pairs should be deleted.
776     * @return true if the delete was successful. False if prevented by
777     * Conditional/LogixNG references or user choice.
778     */
779    public boolean deleteNxPair(NamedBean sensor) {
780        if (sensor == null) {
781            log.error("deleteNxPair: sensor is null");  // NOI18N
782            return false;
783        }
784        createDeletePairList(sensor);
785        if (checkNxPairs() && checkLogixNG()) {
786            // No Conditional or LogixNG references.
787            if (confirmDeletePairs()) {
788                deleteNxPairs();
789                return true;
790            }
791        }
792        return false;
793    }
794
795    /**
796     * Entry point to delete a specific NX pair.
797     *
798     * @since 4.11.2
799     * @param entrySensor The sensor that acts as the entry point.
800     * @param exitSensor The sensor that acts as the exit point.
801     * @param panel The layout editor panel that contains the entry sensor.
802     * @return true if the delete was successful. False if there are Conditional/LogixNG references.
803     */
804    public boolean deleteNxPair(NamedBean entrySensor, NamedBean exitSensor, LayoutEditor panel) {
805        if (entrySensor == null || exitSensor == null || panel == null) {
806            log.error("deleteNxPair: One or more null inputs");  // NOI18N
807            return false;
808        }
809
810        deletePairList.clear();
811        deletePairList.add(new DeletePair(entrySensor, exitSensor, panel));
812        if (checkNxPairs() && checkLogixNG()) {
813            // No Conditional or LogixNG references.
814            deleteNxPairs();  // Delete with no prompt
815            return true;
816        }
817
818        return false;
819    }
820
821    /**
822     * Find Logix Conditionals that have Variables or Actions for the affected NX Pairs.
823     * If any are found, display a dialog box listing the Conditionals and return false.
824     * <p>
825     * @since 4.11.2
826     * @return true if there are no references.
827     */
828    private boolean checkNxPairs() {
829        jmri.LogixManager mgr = InstanceManager.getDefault(jmri.LogixManager.class);
830        List<String> conditionalReferences = new ArrayList<>();
831        for (DeletePair dPair : deletePairList) {
832            if (dPair.dp == null) {
833                continue;
834            }
835            for (jmri.Logix lgx : mgr.getNamedBeanSet()) {
836                for (int i = 0; i < lgx.getNumConditionals(); i++) {
837                    String cdlName = lgx.getConditionalByNumberOrder(i);
838                    jmri.implementation.DefaultConditional cdl = (jmri.implementation.DefaultConditional) lgx.getConditional(cdlName);
839                    String cdlUserName = cdl.getUserName();
840                    if (cdlUserName == null) {
841                        cdlUserName = "";
842                    }
843                    for (jmri.ConditionalVariable var : cdl.getStateVariableList()) {
844                        if (var.getBean() == dPair.dp) {
845                            String refName = (cdlUserName.equals("")) ? cdlName : cdlName + "  ( " + cdlUserName + " )";
846                            if (!conditionalReferences.contains(refName)) {
847                                conditionalReferences.add(refName);
848                            }
849                        }
850                    }
851                    for (jmri.ConditionalAction act : cdl.getActionList()) {
852                        if (act.getBean() == dPair.dp) {
853                            String refName = (cdlUserName.equals("")) ? cdlName : cdlName + "  ( " + cdlUserName + " )";
854                            if (!conditionalReferences.contains(refName)) {
855                                conditionalReferences.add(refName);
856                            }
857                        }
858                    }
859                }
860            }
861        }
862        if (conditionalReferences.isEmpty()) {
863            return true;
864        }
865
866        conditionalReferences.sort(null);
867        StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteReferences"));
868        for (String ref : conditionalReferences) {
869            msg.append("\n    " + ref);  // NOI18N
870        }
871        JmriJOptionPane.showMessageDialog(null,
872                msg.toString(),
873                Bundle.getMessage("WarningTitle"),  // NOI18N
874                JmriJOptionPane.WARNING_MESSAGE);
875
876        return false;
877    }
878
879    /**
880     * Find LogixNG ConditionalNGs that have Expressions or Actions for the affected NX Pairs.
881     * If any are found, display a dialog box listing the details and return false.
882     * <p>
883     * @since 5.5.7
884     * @return true if there are no references.
885     */
886    private boolean checkLogixNG() {
887        List<String> conditionalReferences = new ArrayList<>();
888        for (DeletePair dPair : deletePairList) {
889            if (dPair.dp == null) {
890                continue;
891            }
892            var usage = jmri.jmrit.logixng.util.WhereUsed.whereUsed(dPair.dp);
893            if (!usage.isEmpty()) {
894                conditionalReferences.add(usage);
895            }
896        }
897        if (conditionalReferences.isEmpty()) {
898            return true;
899        }
900
901        conditionalReferences.sort(null);
902        StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteReferences"));
903        for (String ref : conditionalReferences) {
904            msg.append("\n" + ref);  // NOI18N
905        }
906        JmriJOptionPane.showMessageDialog(null,
907                msg.toString(),
908                Bundle.getMessage("WarningTitle"),  // NOI18N
909                JmriJOptionPane.WARNING_MESSAGE);
910
911        return false;
912    }
913
914    /**
915     * Display a list of pending deletes and ask for confirmation.
916     * @since 4.11.2
917     * @return true if deletion confirmation is Yes.
918     */
919    private boolean confirmDeletePairs() {
920        if (!deletePairList.isEmpty()) {
921            StringBuilder msg = new StringBuilder(Bundle.getMessage("DeletePairs"));  // NOI18N
922            for (DeletePair dPair : deletePairList) {
923                if (dPair.dp != null) {
924                    msg.append("\n    ").append(dPair.dp.getDisplayName());  // NOI18N
925                }
926            }
927            msg.append("\n").append(Bundle.getMessage("DeleteContinue"));  // NOI18N
928            int resp = JmriJOptionPane.showConfirmDialog(null,
929                    msg.toString(),
930                    Bundle.getMessage("WarningTitle"),  // NOI18N
931                    JmriJOptionPane.YES_NO_OPTION,
932                    JmriJOptionPane.QUESTION_MESSAGE);
933            if (resp != JmriJOptionPane.YES_OPTION ) {
934                return false;
935            }
936        }
937        return true;
938    }
939
940    /**
941     * Delete the pairs in the delete pair list.
942     * @since 4.11.2
943     * @since 4.17.4
944     * Remove from Change Listener.
945     */
946    private void deleteNxPairs() {
947        for (DeletePair dp : deletePairList) {
948            PointDetails sourcePoint = getPointDetails(dp.src, dp.pnl);
949            PointDetails destPoint = getPointDetails(dp.dest, dp.pnl);
950            nxpair.get(sourcePoint).removeDestination(destPoint);
951            firePropertyChange(PROPERTY_LENGTH, null, null);
952            if (nxpair.get(sourcePoint).getDestinationPoints().isEmpty()) {
953                nxpair.get(sourcePoint).removePropertyChangeListener(this);
954                nxpair.remove(sourcePoint);
955            }
956        }
957    }
958
959    /**
960     * List of NX pairs that are scheduled for deletion.
961     * @since 4.11.2
962     */
963    List<DeletePair> deletePairList = new ArrayList<>();
964
965    /**
966     * Class to store NX pair components.
967     * @since 4.11.2
968     */
969    private class DeletePair {
970        NamedBean src = null;
971        NamedBean dest = null;
972        LayoutEditor pnl = null;
973        DestinationPoints dp = null;
974
975        /**
976         * Constructor for a DeletePair row.
977         *
978         * @param src  Source sensor bean
979         * @param dest Ddestination sensor bean
980         * @param pnl  The LayoutEditor panel for the source bean
981         */
982        DeletePair(NamedBean src, NamedBean dest, LayoutEditor pnl) {
983            this.src = src;
984            this.dest = dest;
985            this.pnl = pnl;
986
987            // Get the actual destination point, if any.
988            PointDetails sourcePoint = getPointDetails(src, pnl);
989            PointDetails destPoint = getPointDetails(dest, pnl);
990            if (sourcePoint != null && destPoint != null) {
991                if (nxpair.containsKey(sourcePoint)) {
992                    this.dp = nxpair.get(sourcePoint).getDestForPoint(destPoint);
993                }
994            }
995        }
996    }
997
998    /**
999     * Rebuild the delete pair list based on the supplied sensor.
1000     * Find all of the NX pairs that use this sensor as either a source or
1001     * destination.  They will be candidates for deletion.
1002     *
1003     * @since 4.11.2
1004     * @param sensor The sensor being deleted,
1005     */
1006    void createDeletePairList(NamedBean sensor) {
1007        deletePairList.clear();
1008        nxpair.forEach((pdSrc, src) -> {
1009            Sensor sBean = pdSrc.getSensor();
1010            LayoutEditor sPanel = pdSrc.getPanel();
1011            for (PointDetails pdDest : src.getDestinationPoints()) {
1012                Sensor dBean = pdDest.getSensor();
1013                if (sensor == sBean || sensor == dBean) {
1014                    log.debug("Delete pair: {} to {}, panel = {}",  // NOI18N
1015                            sBean.getDisplayName(), dBean.getDisplayName(), sPanel.getLayoutName());
1016                    deletePairList.add(new DeletePair(sBean, dBean, sPanel));
1017                }
1018            }
1019        });
1020    }
1021
1022    // ============ End NX Pair Delete Methods ============
1023
1024    /**
1025     * Create a list of sensors that have the layout block as either
1026     * facing or protecting.
1027     * Called by {@link jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.LayoutTrackEditor#hasNxSensorPairs}.
1028     * @since 4.11.2
1029     * @param layoutBlock The layout block to be checked.
1030     * @return the a list of sensors affected by the layout block or an empty list.
1031     */
1032    public List<String> layoutBlockSensors(@Nonnull LayoutBlock layoutBlock) {
1033        log.debug("layoutBlockSensors: {}", layoutBlock.getDisplayName());
1034        List<String> blockSensors = new ArrayList<>();
1035        nxpair.forEach((pdSrc, src) -> {
1036            Sensor sBean = pdSrc.getSensor();
1037            for (LayoutBlock sProtect : pdSrc.getProtecting()) {
1038                if (layoutBlock == pdSrc.getFacing() || layoutBlock == sProtect) {
1039                    log.debug("  Source = '{}', Facing = '{}', Protecting = '{}'         ",
1040                            sBean.getDisplayName(), pdSrc.getFacing().getDisplayName(), sProtect.getDisplayName());
1041                    blockSensors.add(sBean.getDisplayName());
1042                }
1043            }
1044
1045            for (PointDetails pdDest : src.getDestinationPoints()) {
1046                Sensor dBean = pdDest.getSensor();
1047                for (LayoutBlock dProtect : pdDest.getProtecting()) {
1048                    if (layoutBlock == pdDest.getFacing() || layoutBlock == dProtect) {
1049                        log.debug("    Destination = '{}', Facing = '{}', Protecting = '{}'     ",
1050                                dBean.getDisplayName(), pdDest.getFacing().getDisplayName(), dProtect.getDisplayName());
1051                        blockSensors.add(dBean.getDisplayName());
1052                    }
1053                }
1054            }
1055        });
1056        return blockSensors;
1057    }
1058
1059    public boolean isDestinationValid(Object source, Object dest, LayoutEditor panel) {
1060        if (nxpair.containsKey(getPointDetails(source, panel))) {
1061            return nxpair.get(getPointDetails(source, panel)).isDestinationValid(getPointDetails(dest, panel));
1062        }
1063        return false;
1064    }
1065
1066    public boolean isUniDirection(Object source, LayoutEditor panel, Object dest) {
1067        if (nxpair.containsKey(getPointDetails(source, panel))) {
1068            return nxpair.get(getPointDetails(source, panel)).getUniDirection(dest, panel);
1069        }
1070        return false;
1071    }
1072
1073    public void setUniDirection(Object source, LayoutEditor panel, Object dest, boolean set) {
1074        if (nxpair.containsKey(getPointDetails(source, panel))) {
1075            nxpair.get(getPointDetails(source, panel)).setUniDirection(dest, panel, set);
1076        }
1077    }
1078
1079    public boolean canBeBiDirectional(Object source, LayoutEditor panel, Object dest) {
1080        if (nxpair.containsKey(getPointDetails(source, panel))) {
1081            return nxpair.get(getPointDetails(source, panel)).canBeBiDirection(dest, panel);
1082        }
1083        return false;
1084    }
1085
1086    public boolean isEnabled(Object source, LayoutEditor panel, Object dest) {
1087        if (nxpair.containsKey(getPointDetails(source, panel))) {
1088            return nxpair.get(getPointDetails(source, panel)).isEnabled(dest, panel);
1089        }
1090        return false;
1091    }
1092
1093    public void setEnabled(Object source, LayoutEditor panel, Object dest, boolean set) {
1094        if (nxpair.containsKey(getPointDetails(source, panel))) {
1095            nxpair.get(getPointDetails(source, panel)).setEnabled(dest, panel, set);
1096        }
1097    }
1098
1099    public void setEntryExitType(Object source, LayoutEditor panel, Object dest, int set) {
1100        if (nxpair.containsKey(getPointDetails(source, panel))) {
1101            nxpair.get(getPointDetails(source, panel)).setEntryExitType(dest, panel, set);
1102        }
1103    }
1104
1105    public int getEntryExitType(Object source, LayoutEditor panel, Object dest) {
1106        if (nxpair.containsKey(getPointDetails(source, panel))) {
1107            return nxpair.get(getPointDetails(source, panel)).getEntryExitType(dest, panel);
1108        }
1109        return 0x00;
1110    }
1111
1112    public String getUniqueId(Object source, LayoutEditor panel, Object dest) {
1113        if (nxpair.containsKey(getPointDetails(source, panel))) {
1114            return nxpair.get(getPointDetails(source, panel)).getUniqueId(dest, panel);
1115        }
1116        return null;
1117    }
1118
1119    public List<String> getEntryExitList() {
1120        List<String> destlist = new ArrayList<>();
1121        for (Source e : nxpair.values()) {
1122            destlist.addAll(e.getDestinationUniqueId());
1123        }
1124        return destlist;
1125    }
1126
1127    // protecting helps us to determine which direction we are going.
1128    // validateOnly flag is used, if all we are doing is simply checking to see if the source/destpoints are valid
1129    // when creating the pairs in the user GUI
1130    public boolean isPathActive(Object sourceObj, Object destObj, LayoutEditor panel) {
1131        PointDetails pd = getPointDetails(sourceObj, panel);
1132        if (nxpair.containsKey(pd)) {
1133            Source source = nxpair.get(pd);
1134            return source.isRouteActive(getPointDetails(destObj, panel));
1135        }
1136        return false;
1137    }
1138
1139    public void cancelInterlock(Object source, LayoutEditor panel, Object dest) {
1140        if (nxpair.containsKey(getPointDetails(source, panel))) {
1141            nxpair.get(getPointDetails(source, panel)).cancelInterlock(dest, panel);
1142        }
1143
1144    }
1145
1146    jmri.SignalMastLogicManager smlm = InstanceManager.getDefault(jmri.SignalMastLogicManager.class);
1147
1148    public final static int CANCELROUTE = 0;
1149    public final static int CLEARROUTE = 1;
1150    public final static int EXITROUTE = 2;
1151    public final static int STACKROUTE = 4;
1152
1153   /**
1154     * Return a point from a given LE Panel.
1155     *
1156     * @param obj The point object
1157     * @param panel The Layout Editor panel on which the point was placed
1158     * @return the point object, null if the point is not found
1159     */
1160    public PointDetails getPointDetails(Object obj, LayoutEditor panel) {
1161        for (int i = 0; i < pointDetails.size(); i++) {
1162            if ((pointDetails.get(i).getRefObject() == obj)) {
1163                return pointDetails.get(i);
1164
1165            }
1166        }
1167        return null;
1168    }
1169
1170    /**
1171     * Return either an existing point stored in pointDetails, or create a new one as required.
1172     *
1173     * @param source The Layout Block functioning as the source (origin)
1174     * @param destination A (list of) Layout Blocks functioning as destinations
1175     * @param panel The Layout Editor panel on which the point is to be placed
1176     * @return the point object
1177     */
1178    PointDetails getPointDetails(LayoutBlock source, List<LayoutBlock> destination, LayoutEditor panel) {
1179        PointDetails newPoint = new PointDetails(source, destination);
1180        newPoint.setPanel(panel);
1181        for (int i = 0; i < pointDetails.size(); i++) {
1182            if (pointDetails.get(i).equals(newPoint)) {
1183                return pointDetails.get(i);
1184            }
1185        }
1186        //Not found so will add
1187        pointDetails.add(newPoint);
1188        return newPoint;
1189    }
1190
1191    //No point can have multiple copies of what is the same thing.
1192    static List<PointDetails> pointDetails = new ArrayList<PointDetails>();
1193
1194    /**
1195     * Get the name of a destinationPoint on a LE Panel.
1196     *
1197     * @param obj the point object
1198     * @param panel The Layout Editor panel on which it is expected to be placed
1199     * @return the name of the point
1200     */
1201    public String getPointAsString(NamedBean obj, LayoutEditor panel) {
1202        if (obj == null) {
1203            return "null";  // NOI18N
1204        }
1205        PointDetails valid = getPointDetails(obj, panel);  //was just plain getPoint
1206        if (valid != null) {
1207            return valid.getDisplayName();
1208        }
1209        return "empty";  // NOI18N
1210    }
1211
1212    List<StackDetails> stackList = new ArrayList<>();
1213
1214    /**
1215     * If a route is requested but is currently blocked, ask user
1216     * if it should be added to stackList.
1217     *
1218     * @param dp DestinationPoints object
1219     * @param reverse true for a reversed running direction, mostly false
1220     */
1221    synchronized public void stackNXRoute(DestinationPoints dp, boolean reverse) {
1222        if (isRouteStacked(dp, reverse)) {
1223            return;
1224        }
1225        stackList.add(new StackDetails(dp, reverse));
1226        checkTimer.start();
1227        if (stackPanel == null) {
1228            stackPanel = new StackNXPanel();
1229        }
1230        if (stackDialog == null) {
1231            stackDialog = new JDialog();
1232            stackDialog.setTitle(Bundle.getMessage("WindowTitleStackRoutes"));  // NOI18N
1233            stackDialog.add(stackPanel);
1234        }
1235        stackPanel.updateGUI();
1236
1237        stackDialog.pack();
1238        stackDialog.setModal(false);
1239        stackDialog.setVisible(true);
1240    }
1241
1242    StackNXPanel stackPanel = null;
1243    JDialog stackDialog = null;
1244
1245    /**
1246     * Get a list of all stacked routes from stackList.
1247     *
1248     * @return an List containing destinationPoint elements
1249     */
1250    public List<DestinationPoints> getStackedInterlocks() {
1251        List<DestinationPoints> dpList = new ArrayList<>();
1252        for (StackDetails st : stackList) {
1253            dpList.add(st.getDestinationPoint());
1254        }
1255        return dpList;
1256    }
1257
1258    /**
1259     * Query if a stacked route is in stackList.
1260     *
1261     * @param dp DestinationPoints object
1262     * @param reverse true for a reversed running direction, mostly false
1263     * @return true if dp is in stackList
1264     */
1265    public boolean isRouteStacked(DestinationPoints dp, boolean reverse) {
1266        Iterator<StackDetails> iter = stackList.iterator();
1267        while (iter.hasNext()) {
1268            StackDetails st = iter.next();
1269            if (st.getDestinationPoint() == dp && st.getReverse() == reverse) {
1270                return true;
1271            }
1272        }
1273        return false;
1274    }
1275
1276    /**
1277     * Remove a stacked route from stackList.
1278     *
1279     * @param dp DestinationPoints object
1280     * @param reverse true for a reversed running direction, mostly false
1281     */
1282    synchronized public void cancelStackedRoute(DestinationPoints dp, boolean reverse) {
1283        Iterator<StackDetails> iter = stackList.iterator();
1284        while (iter.hasNext()) {
1285            StackDetails st = iter.next();
1286            if (st.getDestinationPoint() == dp && st.getReverse() == reverse) {
1287                iter.remove();
1288            }
1289        }
1290        stackPanel.updateGUI();
1291        if (stackList.isEmpty()) {
1292            stackDialog.setVisible(false);
1293            checkTimer.stop();
1294        }
1295    }
1296
1297    /**
1298     * Class to collect (stack) routes when they are requested but blocked.
1299     */
1300    static class StackDetails {
1301
1302        DestinationPoints dp;
1303        boolean reverse;
1304
1305        StackDetails(DestinationPoints dp, boolean reverse) {
1306            this.dp = dp;
1307            this.reverse = reverse;
1308        }
1309
1310        boolean getReverse() {
1311            return reverse;
1312        }
1313
1314        DestinationPoints getDestinationPoint() {
1315            return dp;
1316        }
1317    }
1318
1319    javax.swing.Timer checkTimer = new javax.swing.Timer(10000, (java.awt.event.ActionEvent e) -> {
1320        checkRoute();
1321    });
1322
1323    /**
1324     * Step through stackList and activate the first stacked route in line
1325     * if it is no longer blocked.
1326     */
1327    synchronized void checkRoute() {
1328        checkTimer.stop();
1329        StackDetails[] tmp = new StackDetails[stackList.size()];
1330        stackList.toArray(tmp);
1331
1332        for (StackDetails st : tmp) {
1333            if (!st.getDestinationPoint().isActive()) {
1334                // If the route is not already active, then check.
1335                // If the route does get set, then the setting process will remove the route from the stack.
1336                st.getDestinationPoint().setInterlockRoute(st.getReverse());
1337            }
1338        }
1339
1340        if (!stackList.isEmpty()) {
1341            checkTimer.start();
1342        } else {
1343            stackDialog.setVisible(false);
1344        }
1345    }
1346
1347    public void removePropertyChangeListener(PropertyChangeListener list, NamedBean obj, LayoutEditor panel) {
1348        if (obj == null) {
1349            return;
1350        }
1351        PointDetails valid = getPointDetails(obj, panel);
1352        if (valid != null) {
1353            valid.removePropertyChangeListener(list);
1354        }
1355    }
1356
1357    boolean runWhenStabilised = false;
1358    LayoutEditor toUseWhenStable;
1359    int interlockTypeToUseWhenStable;
1360
1361    /**
1362     * Discover all possible valid source and destination Signal Mast Logic pairs
1363     * on all Layout Editor panels.
1364     *
1365     * @param editor The Layout Editor panel
1366     * @param interlockType Integer value representing the type of interlocking, one of
1367     *                      SETUPTURNOUTSONLY, SETUPSIGNALMASTLOGIC or FULLINTERLOCK
1368     * @throws JmriException when an error occurs during discovery
1369     */
1370    public void automaticallyDiscoverEntryExitPairs(LayoutEditor editor, int interlockType) throws JmriException {
1371        //This is almost a duplicate of that in the DefaultSignalMastLogicManager
1372        runWhenStabilised = false;
1373        jmri.jmrit.display.layoutEditor.LayoutBlockManager lbm = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class);
1374        if (!lbm.isAdvancedRoutingEnabled()) {
1375            throw new JmriException("advanced routing not enabled");  // NOI18N
1376        }
1377        if (!lbm.routingStablised()) {
1378            runWhenStabilised = true;
1379            toUseWhenStable = editor;
1380            interlockTypeToUseWhenStable = interlockType;
1381            log.debug("Layout block routing has not yet stabilised, discovery will happen once it has");  // NOI18N
1382            return;
1383        }
1384        HashMap<NamedBean, List<NamedBean>> validPaths = lbm.getLayoutBlockConnectivityTools().
1385                discoverValidBeanPairs(null, Sensor.class, LayoutBlockConnectivityTools.Routing.SENSORTOSENSOR);
1386        EntryExitPairs eep = this;
1387        for (Entry<NamedBean, List<NamedBean>> entry : validPaths.entrySet()) {
1388            NamedBean key = entry.getKey();
1389            List<NamedBean> validDestMast = validPaths.get(key);
1390            if (!validDestMast.isEmpty()) {
1391                eep.addNXSourcePoint(key, editor);
1392                for (int i = 0; i < validDestMast.size(); i++) {
1393                    if (!eep.isDestinationValid(key, validDestMast.get(i), editor)) {
1394                        eep.addNXDestination(key, validDestMast.get(i), editor);
1395                        eep.setEntryExitType(key, editor, validDestMast.get(i), interlockType);
1396                    }
1397                }
1398            }
1399        }
1400
1401        firePropertyChange(PROPERTY_AUTO_GENERATE_COMPLETE, null, null);
1402    }
1403
1404    protected PropertyChangeListener propertyBlockManagerListener = new PropertyChangeListener() {
1405        @Override
1406        public void propertyChange(PropertyChangeEvent e) {
1407            if ( LayoutBlockManager.PROPERTY_TOPOLOGY.equals(e.getPropertyName())) {
1408                boolean newValue = (Boolean) e.getNewValue();
1409                if (newValue) {
1410                    if (runWhenStabilised) {
1411                        try {
1412                            automaticallyDiscoverEntryExitPairs(toUseWhenStable, interlockTypeToUseWhenStable);
1413                        } catch (JmriException je) {
1414                            //Considered normal if routing not enabled
1415                        }
1416                    }
1417                }
1418            }
1419        }
1420    };
1421
1422    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
1423
1424    }
1425
1426    @Override
1427    public void deleteBean(@Nonnull DestinationPoints bean, @Nonnull String property) throws PropertyVetoException {
1428
1429    }
1430
1431    @Override
1432    @Nonnull
1433    public String getBeanTypeHandled(boolean plural) {
1434        return Bundle.getMessage(plural ? "BeanNameEntryExits" : "BeanNameEntryExit");  // NOI18N
1435    }
1436
1437    /**
1438     * {@inheritDoc}
1439     */
1440    @Override
1441    public Class<DestinationPoints> getNamedBeanClass() {
1442        return DestinationPoints.class;
1443    }
1444
1445    /**
1446     * {@inheritDoc}
1447     */
1448    @Override
1449    @OverridingMethodsMustInvokeSuper
1450    public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) {
1451        if (!Manager.PROPERTY_BEANS.equals(propertyName)) {
1452            throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced.");
1453        }
1454        silencedProperties.put(propertyName, silenced);
1455        if (propertyName.equals(Manager.PROPERTY_BEANS) && !silenced) {
1456            fireIndexedPropertyChange(Manager.PROPERTY_BEANS, getNamedBeanSet().size(), null, null);
1457        }
1458    }
1459
1460    /** {@inheritDoc} */
1461    @Override
1462    public void addDataListener(ManagerDataListener<DestinationPoints> e) {
1463        if (e != null) listeners.add(e);
1464    }
1465
1466    /** {@inheritDoc} */
1467    @Override
1468    public void removeDataListener(ManagerDataListener<DestinationPoints> e) {
1469        if (e != null) listeners.remove(e);
1470    }
1471
1472    final List<ManagerDataListener<DestinationPoints>> listeners = new ArrayList<>();
1473
1474    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EntryExitPairs.class);
1475
1476}