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