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