001package jmri.jmrit.logix; 002 003import java.beans.PropertyChangeListener; 004import java.beans.PropertyChangeSupport; 005import java.util.ArrayList; 006import java.util.List; 007import jmri.Block; 008import jmri.InstanceManager; 009import jmri.NamedBean; 010import jmri.SignalHead; 011import jmri.SignalMast; 012import jmri.implementation.SignalSpeedMap; 013 014import javax.annotation.Nonnull; 015import javax.annotation.CheckForNull; 016import javax.annotation.OverridingMethodsMustInvokeSuper; 017 018import org.slf4j.Logger; 019import org.slf4j.LoggerFactory; 020 021/** 022 * A Portal is a boundary between two Blocks. 023 * <p> 024 * A Portal has Lists of the {@link OPath}s that connect through it. 025 * The direction of trains passing through the portal is managed from the 026 * BlockOrders of the Warrant the train is running under. 027 * The Portal fires a PropertyChangeEvent that a 028 * {@link jmri.jmrit.display.controlPanelEditor.PortalIcon} can listen 029 * for to set direction arrows for a given route. 030 * 031 * The Portal also supplies speed information from any signals set at its 032 * location that the Warrant passes on the Engineer. 033 * 034 * @author Pete Cressman Copyright (C) 2009 035 */ 036public class Portal { 037 038 private static final String NAME_CHANGE = "NameChange"; 039 private static final String SIGNAL_CHANGE = "signalChange"; 040 private static final String ENTRANCE = "entrance"; 041 private final ArrayList<OPath> _fromPaths = new ArrayList<>(); 042 private OBlock _fromBlock; 043 private NamedBean _fromSignal; // may be either SignalHead or SignalMast 044 private float _fromSignalOffset; // adjustment distance for speed change 045 private final ArrayList<OPath> _toPaths = new ArrayList<>(); 046 private OBlock _toBlock; 047 private NamedBean _toSignal; // may be either SignalHead or SignalMast 048 private float _toSignalOffset; // adjustment distance for speed change 049 private String _name; 050 private int _state = UNKNOWN; 051 private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); 052 053 public static final int UNKNOWN = 0x01; 054 public static final int ENTER_TO_BLOCK = 0x02; 055 public static final int ENTER_FROM_BLOCK = 0x04; 056 057 public Portal(String uName) { 058 _name = uName; 059 } 060 061 /** 062 * Determine which list the Path belongs to and add it to that list. 063 * 064 * @param path OPath to add 065 * @return false if Path does not have a matching block for this Portal 066 */ 067 public boolean addPath(@Nonnull OPath path) { 068 Block block = path.getBlock(); 069 if (block == null) { 070 log.error("Path \"{}\" has no block.", path.getName()); 071 return false; 072 } 073 if (!this.equals(path.getFromPortal()) 074 && !this.equals(path.getToPortal())) { 075 return false; 076 } 077 if ((_fromBlock != null) && _fromBlock.equals(block)) { 078 return addPath(_fromPaths, path); 079 } else if ((_toBlock != null) && _toBlock.equals(block)) { 080 return addPath(_toPaths, path); 081 } 082 // portal is incomplete or path block not in this portal 083 return false; 084 } 085 086 /** 087 * Utility for both path lists. 088 * Checks for duplicate name. 089 */ 090 private boolean addPath(@Nonnull List<OPath> list, @Nonnull OPath path) { 091 String pName = path.getName(); 092 for (OPath p : list) { 093 if (p.equals(path)) { 094 if (pName.equals(p.getName())) { 095 return true; // OK, everything equal 096 } else { 097 log.warn("Path \"{}\" is duplicate of path \"{}\" in Portal \"{}\" from block {}.", path.getName(), p.getName(), _name, path.getBlock().getDisplayName()); 098 return false; 099 } 100 } else if (pName.equals(p.getName())) { 101 log.warn("Path \"{}\" is duplicate name for another path in Portal \"{}\" from block {}.", path.getName(), _name, path.getBlock().getDisplayName()); 102 return false; 103 } 104 } 105 list.add(path); 106 return true; 107 } 108 109 /** 110 * Remove an OPath from this Portal. 111 * Checks both the _fromBlock list as the _toBlock list. 112 * 113 * @param path the OPath to remove 114 */ 115 public void removePath(@Nonnull OPath path) { 116 Block block = path.getBlock(); 117 if (block == null) { 118 log.error("Path \"{}\" has no block.", path.getName()); 119 return; 120 } 121 log.debug("removePath: {}", this); 122 if (!this.equals(path.getFromPortal()) 123 && !this.equals(path.getToPortal())) { 124 return; 125 } 126 if (_fromBlock != null && _fromBlock.equals(block)) { 127 _fromPaths.remove(path); 128 } else if (_toBlock != null && _toBlock.equals(block)) { 129 _toPaths.remove(path); 130 } 131// pcs.firePropertyChange("RemovePath", block, path); not needed 132 } 133 134 /** 135 * Set userName of this Portal. Checks if name is available. 136 * 137 * @param newName name for path 138 * @return return error message, null if name change is OK 139 */ 140 public String setName(String newName) { 141 if (newName == null || newName.length() == 0) { 142 return null; 143 } 144 String oldName = _name; 145 if (newName.equals(oldName)) { 146 return null; 147 } 148 Portal p = InstanceManager.getDefault(PortalManager.class).getPortal(newName); 149 if (p != null) { 150 return Bundle.getMessage("DuplicatePortalName", newName, p.getDescription()); 151 } 152 _name = newName; 153 InstanceManager.getDefault(WarrantManager.class).portalNameChange(oldName, newName); 154 155 // for some unknown reason, PortalManager firePropertyChange is not read by PortalTableModel 156 // so let OBlock do it 157 if (_toBlock != null) { 158 _toBlock.pseudoPropertyChange(NAME_CHANGE, oldName, this); 159 } else if (_fromBlock != null) { 160 _fromBlock.pseudoPropertyChange(NAME_CHANGE, oldName, this); 161 } 162 // CircuitBuilder PortalList needs this property change 163 pcs.firePropertyChange(NAME_CHANGE, oldName, newName); 164 return null; 165 } 166 167 public String getName() { 168 return _name; 169 } 170 171 /** 172 * Set this portal's toBlock. Remove this portal from old toBlock, if any. 173 * Add this portal in the new toBlock's list of portals. 174 * 175 * @param block to be the new toBlock 176 * @param changePaths if true, set block in paths. If false, 177 * verify that all toPaths are contained in the block. 178 * @return false if paths are not in the block 179 */ 180 public boolean setToBlock(OBlock block, boolean changePaths) { 181 if (((block != null) && block.equals(_toBlock)) || ((block == null) && (_toBlock == null))) { 182 return true; 183 } 184 if (changePaths) { 185 // Switch paths to new block. User will need to verify connections 186 for (OPath opa : _toPaths) { 187 opa.setBlock(block); 188 } 189 } else if (!verify(_toPaths, block)) { 190 return false; 191 } 192 log.debug("setToBlock: oldBlock= \"{}\" newBlock \"{}\".", getToBlockName(), 193 (block != null ? block.getDisplayName() : null)); 194 OBlock oldBlock = _toBlock; 195 if (_toBlock != null) { 196 _toBlock.removePortal(this); // may should not 197 } 198 _toBlock = block; 199 if (_toBlock != null) { 200 _toBlock.addPortal(this); 201 } 202 pcs.firePropertyChange("BlockChanged", oldBlock, _toBlock); 203 return true; 204 } 205 206 public OBlock getToBlock() { 207 return _toBlock; 208 } 209 210 public String getToBlockName() { 211 return (_toBlock != null ? _toBlock.getDisplayName() : null); 212 } 213 214 public List<OPath> getToPaths() { 215 return _toPaths; 216 } 217 218 /** 219 * Set this portal's fromBlock. Remove this portal from old fromBlock, if any. 220 * Add this portal in the new toBlock's list of portals. 221 * 222 * @param block to be the new fromBlock 223 * @param changePaths if true, set block in paths. If false, 224 * verify that all toPaths are contained in the block. 225 * @return false if paths are not in the block 226 */ 227 public boolean setFromBlock(OBlock block, boolean changePaths) { 228 if ((block != null && block.equals(_fromBlock)) || (block == null && _fromBlock == null)) { 229 return true; 230 } 231 if (changePaths) { 232 //Switch paths to new block. User will need to verify connections 233 for (OPath fromPath : _fromPaths) { 234 fromPath.setBlock(block); 235 } 236 } else if (!verify(_fromPaths, block)) { 237 return false; 238 } 239 log.debug("setFromBlock: oldBlock= \"{}\" newBlock \"{}\".", getFromBlockName(), 240 (block != null ? block.getDisplayName() : null)); 241 OBlock oldBlock = _fromBlock; 242 if (_fromBlock != null) { 243 _fromBlock.removePortal(this); 244 } 245 _fromBlock = block; 246 if (_fromBlock != null) { 247 _fromBlock.addPortal(this); 248 } 249 pcs.firePropertyChange("BlockChanged", oldBlock, _fromBlock); 250 return true; 251 } 252 253 public OBlock getFromBlock() { 254 return _fromBlock; 255 } 256 257 public String getFromBlockName() { 258 return (_fromBlock != null ? _fromBlock.getDisplayName() : null); 259 } 260 261 public List<OPath> getFromPaths() { 262 return _fromPaths; 263 } 264 265 /** 266 * Set a signal to protect an OBlock. Warrants look ahead for speed changes 267 * and change the train speed accordingly. 268 * 269 * @param signal either a SignalMast or a SignalHead. Set to null to remove (previous) signal from Portal 270 * @param length offset length in millimeters. This is additional 271 * entrance space for the block. This distance added to or subtracted 272 * from the calculation of the ramp distance when a warrant must slow 273 * the train in response to the aspect or appearance of the signal. 274 * @param protectedBlock OBlock the signal protects 275 * @return true if signal is set 276 */ 277 public boolean setProtectSignal(@CheckForNull NamedBean signal, float length, OBlock protectedBlock) { 278 if (protectedBlock == null) { 279 return false; 280 } 281 boolean ret = false; 282 if ((_fromBlock != null) && _fromBlock.equals(protectedBlock)) { 283 _toSignal = signal; 284 _toSignalOffset = length; 285 log.debug("OPortal FromBlock Offset set to {} on signal {}", _toSignalOffset, 286 (_toSignal != null ? _toSignal.getDisplayName() : "<removed>")); 287 ret = true; 288 } 289 if ((_toBlock != null) && _toBlock.equals(protectedBlock)) { 290 _fromSignal = signal; 291 _fromSignalOffset = length; 292 log.debug("OPortal ToBlock Offset set to {} on signal {}", _fromSignalOffset, 293 (_fromSignal != null ? _fromSignal.getDisplayName() : "<removed>")); 294 ret = true; 295 } 296 if (ret) { 297 protectedBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, true); 298 pcs.firePropertyChange(SIGNAL_CHANGE, false, true); 299 log.debug("setProtectSignal: \"{}\" for Block= {} at Portal {}", 300 (signal != null ? signal.getDisplayName() : "null"), protectedBlock.getDisplayName(), _name); 301 } 302 return ret; 303 } 304 305 /** 306 * Get the signal (either a SignalMast or a SignalHead) protecting an OBlock. 307 * 308 * @param block is the direction of entry, i.e. the protected block 309 * @return signal protecting block, if block is protected, otherwise null. 310 */ 311 public NamedBean getSignalProtectingBlock(@Nonnull OBlock block) { 312 if (block.equals(_toBlock)) { 313 return _fromSignal; 314 } else if (block.equals(_fromBlock)) { 315 return _toSignal; 316 } 317 return null; 318 } 319 320 /** 321 * Get the OBlock protected by a signal. 322 * 323 * @param signal is the signal, either a SignalMast or a SignalHead 324 * @return Protected OBlock, if it is protected, otherwise null. 325 */ 326 public OBlock getProtectedBlock(NamedBean signal) { 327 if (signal == null) { 328 return null; 329 } 330 if (signal.equals(_fromSignal)) { 331 return _toBlock; 332 } else if (signal.equals(_toSignal)) { 333 return _fromBlock; 334 } 335 return null; 336 } 337 338 public NamedBean getFromSignal() { 339 return _fromSignal; 340 } 341 342 public String getFromSignalName() { 343 return (_fromSignal != null ? _fromSignal.getDisplayName() : null); 344 } 345 346 public float getFromSignalOffset() { 347 return _fromSignalOffset; // it seems clear that this method should return what is asks 348 } 349 350 public NamedBean getToSignal() { 351 return _toSignal; 352 } 353 354 public String getToSignalName() { 355 return (_toSignal != null ? _toSignal.getDisplayName() : null); 356 } 357 358 public float getToSignalOffset() { 359 return _toSignalOffset; 360 } 361 362 public void deleteSignal(@Nonnull NamedBean signal) { 363 if (signal.equals(_toSignal)) { 364 _toSignal = null; // set the 2 _tos 365 _toSignalOffset = 0; 366 if (_fromBlock != null) { 367 _fromBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, false); 368 pcs.firePropertyChange(SIGNAL_CHANGE, false, false); 369 } 370 } else if (signal.equals(_fromSignal)) { 371 _fromSignal = null; // set the 2 _froms 372 _fromSignalOffset = 0; 373 if (_toBlock != null) { 374 _toBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, false); 375 pcs.firePropertyChange(SIGNAL_CHANGE, false, false); 376 } 377 } 378 } 379 380 public static NamedBean getSignal(String name) { 381 NamedBean signal = InstanceManager.getDefault(jmri.SignalMastManager.class).getSignalMast(name); 382 if (signal == null) { 383 signal = InstanceManager.getDefault(jmri.SignalHeadManager.class).getSignalHead(name); 384 } 385 return signal; 386 } 387 388 /** 389 * Get the paths to the portal within the connected OBlock i.e. the paths in 390 * this (the param) block through the Portal. 391 * 392 * @param block OBlock 393 * @return null if portal does not connect to block 394 */ 395 public List<OPath> getPathsWithinBlock(OBlock block) { 396 if (block == null) { 397 return null; 398 } 399 if (block.equals(_fromBlock)) { 400 return _fromPaths; 401 } else if (block.equals(_toBlock)) { 402 return _toPaths; 403 } 404 return null; 405 } 406 407 /** 408 * Get the OBlock on the other side of the Portal from the given 409 * OBlock. 410 * 411 * @param block starting OBlock 412 * @return the opposite block 413 */ 414 public OBlock getOpposingBlock(@Nonnull OBlock block) { 415 if (block.equals(_fromBlock)) { 416 return _toBlock; 417 } else if (block.equals(_toBlock)) { 418 return _fromBlock; 419 } 420 return null; 421 } 422 423 /** 424 * Get the paths from the portal in the next connected OBlock i.e. paths in 425 * the block on the other side of the portal from this (the param) block. 426 * 427 * @param block OBlock 428 * @return null if portal does not connect to block 429 */ 430 public List<OPath> getPathsFromOpposingBlock(@Nonnull OBlock block) { 431 if (block.equals(_fromBlock)) { 432 return _toPaths; 433 } else if (block.equals(_toBlock)) { 434 return _fromPaths; 435 } 436 return null; 437 } 438 439 /** 440 * Call is from BlockOrder when setting the path. 441 * 442 * @param block OBlock 443 */ 444 protected void setEntryState(OBlock block) { 445 if (block == null) { 446 _state = UNKNOWN; 447 } else if (block.equals(_fromBlock)) { 448 setState(ENTER_FROM_BLOCK); 449 } else if (block.equals(_toBlock)) { 450 setState(ENTER_TO_BLOCK); 451 } 452 } 453 454 public void setState(int s) { 455 int old = _state; 456 _state = s; 457 pcs.firePropertyChange("Direction", old, _state); 458 } 459 460 public int getState() { 461 return _state; 462 } 463 464 @OverridingMethodsMustInvokeSuper 465 public synchronized void addPropertyChangeListener(PropertyChangeListener listener) { 466 pcs.addPropertyChangeListener(listener); 467 } 468 469 @OverridingMethodsMustInvokeSuper 470 public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { 471 pcs.removePropertyChangeListener(listener); 472 } 473 474 /** 475 * Set the distance (plus or minus) in millimeters from the portal gap 476 * where the speed change indicated by the signal should be completed. 477 * 478 * @param block a protected OBlock 479 * @param distance length in millimeters, called Offset in the OBlock Signal Table 480 */ 481 public void setEntranceSpaceForBlock(@Nonnull OBlock block, float distance) { 482 if (block.equals(_toBlock)) { 483 if (_fromSignal != null) { 484 _fromSignalOffset = distance; 485 } 486 } else if (block.equals(_fromBlock)) { 487 if (_toSignal != null) { 488 _toSignalOffset = distance; 489 } 490 } 491 } 492 493 /** 494 * Get the distance (plus or minus) in millimeters from the portal gap 495 * where the speed change indicated by the signal should be completed. 496 * Property is called Offset in the OBlock Signal Table. 497 * 498 * @param block a protected OBlock 499 * @return distance 500 */ 501 public float getEntranceSpaceForBlock(@Nonnull OBlock block) { 502 if (block.equals(_toBlock)) { 503 if (_fromSignal != null) { 504 return _fromSignalOffset; 505 } 506 } else if (block.equals(_fromBlock)) { 507 if (_toSignal != null) { 508 return _toSignalOffset; 509 } 510 } 511 return 0; 512 } 513 514 /** 515 * Check signals, if any, for speed into/out of a given block. The signal that protects 516 * the "to" block is the signal facing the "from" Block, i.e. the "from" 517 * signal. (and vice-versa) 518 * 519 * @param block is the direction of entry, "from" block 520 * @param entrance true for EntranceSpeed, false for ExitSpeed 521 * @return permissible speed, null if no signal 522 */ 523 public String getPermissibleSpeed(@Nonnull OBlock block, boolean entrance) { 524 String speed = null; 525 String blockName = block.getDisplayName(); 526 if (block.equals(_toBlock)) { 527 if (_fromSignal != null) { 528 if (_fromSignal instanceof SignalHead) { 529 speed = getPermissibleSignalSpeed((SignalHead) _fromSignal, entrance); 530 } else { 531 speed = getPermissibleSignalSpeed((SignalMast) _fromSignal, entrance); 532 } 533 } 534 } else if (block.equals(_fromBlock)) { 535 if (_toSignal != null) { 536 if (_toSignal instanceof SignalHead) { 537 speed = getPermissibleSignalSpeed((SignalHead) _toSignal, entrance); 538 } else { 539 speed = getPermissibleSignalSpeed((SignalMast) _toSignal, entrance); 540 } 541 } 542 } else { 543 log.error("Block \"{}\" is not in Portal \"{}\".", blockName, _name); 544 } 545 if (log.isDebugEnabled()) { 546 if (speed != null) { 547 log.debug("Portal \"{}\" has {} speed= {} into \"{}\" from signal.", 548 _name, (entrance ? "ENTRANCE" : "EXIT"), speed, blockName); 549 } 550 } 551 // no signals, proceed at recorded speed 552 return speed; 553 } 554 555 /** 556 * Get entrance or exit speed set on signal head. 557 * 558 * @param signal signal head to query 559 * @param entrance true for EntranceSpeed, false for ExitSpeed 560 * @return permissible speed, Restricted if no speed set on signal 561 */ 562 private static @Nonnull String getPermissibleSignalSpeed(@Nonnull SignalHead signal, boolean entrance) { 563 int appearance = signal.getAppearance(); 564 String speed = InstanceManager.getDefault(SignalSpeedMap.class).getAppearanceSpeed(signal.getAppearanceName(appearance)); 565 // on head, speed is the same for entry and exit 566 if (speed == null) { 567 log.error("SignalHead \"{}\" has no {} speed specified for appearance \"{}\"! - Restricting Movement!", 568 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), signal.getAppearanceName(appearance)); 569 speed = "Restricted"; 570 } 571 log.debug("SignalHead \"{}\" has {} speed notch= {} from appearance \"{}\"", 572 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), speed, signal.getAppearanceName(appearance)); 573 return speed; 574 } 575 576 /** 577 * Get entrance or exit speed set on signal mast. 578 * 579 * @param signal signal mast to query 580 * @param entrance true for EntranceSpeed, false for ExitSpeed 581 * @return permissible speed, Restricted if no speed set on signal 582 */ 583 private static @Nonnull String getPermissibleSignalSpeed(@Nonnull SignalMast signal, boolean entrance) { 584 String aspect = signal.getAspect(); 585 String signalAspect = ( aspect == null ? "" : aspect ); 586 String speed; 587 if (entrance) { 588 speed = InstanceManager.getDefault(SignalSpeedMap.class).getAspectSpeed(signalAspect, signal.getSignalSystem()); 589 } else { 590 speed = InstanceManager.getDefault(SignalSpeedMap.class).getAspectExitSpeed(signalAspect, signal.getSignalSystem()); 591 } 592 if (speed == null) { 593 log.error("SignalMast \"{}\" has no {} speed specified for aspect \"{}\"! - Restricting Movement!", 594 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), aspect); 595 speed = "Restricted"; 596 } 597 log.debug("SignalMast \"{}\" has {} speed notch= {} from aspect \"{}\"", 598 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), speed, aspect); 599 return speed; 600 } 601 602 /** 603 * Verify that each path has this potential block as its owning block. 604 * Block is a potential _toBlock and Paths are the current _toPaths 605 * or 606 * Block is a potential _fromBlock and Paths are the current _fromPaths 607 */ 608 private static boolean verify(List<OPath> paths, OBlock block) { 609 if (block == null) { 610 return (paths.isEmpty()); 611 } 612 String name = block.getSystemName(); 613 for (OPath path : paths) { 614 Block blk = path.getBlock(); 615 if (blk == null) { 616 log.error("Path \"{}\" belongs to null block. Cannot verify set block to \"{}\"", 617 path.getName(), name); 618 return false; 619 } 620 String pathName = blk.getSystemName(); 621 if (!pathName.equals(name)) { 622 log.warn("Path \"{}\" belongs to block \"{}\". Cannot verify set block to \"{}\"", 623 path.getName(), pathName, name); 624 return false; 625 } 626 } 627 return true; 628 } 629 630 /** 631 * Check if path connects to Portal. 632 * 633 * @param path OPath to test 634 * @return true if valid 635 */ 636 public boolean isValidPath(OPath path) { 637 String name = path.getName(); 638 for (OPath toPath : _toPaths) { 639 if (toPath.getName().equals(name)) { 640 return true; 641 } 642 } 643 for (OPath fromPath : _fromPaths) { 644 if (fromPath.getName().equals(name)) { 645 return true; 646 } 647 } 648 return false; 649 } 650 651 /** 652 * Check portal has both blocks and they are different blocks. 653 * 654 * @return true if valid 655 */ 656 public boolean isValid() { 657 if (_toBlock == null || _fromBlock==null) { 658 return false; 659 } 660 return (!_toBlock.equals(_fromBlock)); 661 } 662 663 @OverridingMethodsMustInvokeSuper 664 public boolean dispose() { 665 if (!InstanceManager.getDefault(jmri.jmrit.logix.WarrantManager.class).okToRemovePortal(this)) { 666 return false; 667 } 668 if (_toBlock != null) { 669 _toBlock.removePortal(this); 670 } 671 if (_fromBlock != null) { 672 _fromBlock.removePortal(this); 673 } 674 pcs.firePropertyChange("portalDelete", true, false); 675 PropertyChangeListener[] listeners = pcs.getPropertyChangeListeners(); 676 for (PropertyChangeListener l : listeners) { 677 pcs.removePropertyChangeListener(l); 678 } 679 return true; 680 } 681 682 public String getDescription() { 683 return Bundle.getMessage("PortalDescription", 684 _name, getFromBlockName(), getToBlockName()); 685 } 686 687 @Override 688 @Nonnull 689 public String toString() { 690 StringBuilder sb = new StringBuilder("Portal \""); 691 sb.append(_name); 692 sb.append("\" from block \""); 693 sb.append(getFromBlockName()); 694 sb.append("\" to block \""); 695 sb.append(getToBlockName()); 696 sb.append("\""); 697 return sb.toString(); 698 } 699 700 private static final Logger log = LoggerFactory.getLogger(Portal.class); 701 702}