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}