001package jmri; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.beans.PropertyVetoException; 006import java.time.Instant; 007import java.util.ArrayList; 008import java.util.List; 009import java.util.Objects; 010import java.util.regex.Matcher; 011import java.util.regex.Pattern; 012 013import javax.annotation.CheckForNull; 014import javax.annotation.Nonnull; 015 016import jmri.implementation.AbstractNamedBean; 017import jmri.implementation.SignalSpeedMap; 018import jmri.util.PhysicalLocation; 019 020/** 021 * Represents a particular piece of track, more informally a "Block". 022 * <p> 023 * A Block (at least in this implementation) corresponds exactly to the track 024 * covered by at most one sensor. That could be generalized in the future. 025 * <p> 026 * As trains move around the layout, a set of Block objects that are attached to 027 * sensors can interact to keep track of which train is where, going in which 028 * direction. 029 * As a result of this, the set of Block objects pass around "token" 030 * (value) Objects representing the trains. 031 * This could be e.g. a Throttle to control the train, or something else. 032 * <p> 033 * A block maintains a "direction" flag that is set from the direction of the 034 * incoming train. 035 * When an arriving train is detected via the connected sensor 036 * and the Block's status information is sufficient to determine that it is 037 * arriving via a particular Path, that Path's getFromBlockDirection 038 * becomes the direction of the train in this Block. 039 * <p> 040 * Optionally, a Block can be associated with a Reporter. 041 * In this case, the Reporter will provide the Block with the "token" (value). 042 * This could be e.g an RFID reader reading an ID tag attached to a locomotive. 043 * Depending on the specific Reporter implementation, 044 * either the current reported value or the last reported value will be relevant, 045 * this can be configured. 046 * <p> 047 * Objects of this class are Named Beans, so can be manipulated through tables, 048 * have listeners, etc. 049 * <p> 050 * The type letter used in the System Name is 'B' for 'Block'. 051 * The default implementation is not system-specific, so a system letter 052 * of 'I' is appropriate. This leads to system names like "IB201". 053 * <p> 054 * Issues: 055 * <ul> 056 * <li>The tracking doesn't handle a train pulling in behind another well: 057 * <ul> 058 * <li>When the 2nd train arrives, the Sensor is already active, so the value is 059 * unchanged (but the value can only be a single object anyway) 060 * <li>When the 1st train leaves, the Sensor stays active, so the value remains 061 * that of the 1st train 062 * </ul> 063 * <li> The assumption is that a train will only go through a set turnout. 064 * For example, a train could come into the turnout block from the main even if the 065 * turnout is set to the siding. (Ignoring those layouts where this would cause 066 * a short; it doesn't do so on all layouts) 067 * <li> Does not handle closely-following trains where there is only one 068 * electrical block per signal. 069 * To do this, it probably needs some type of "assume a train doesn't back up" logic. 070 * A better solution is to have multiple 071 * sensors and Block objects between each signal head. 072 * <li> If a train reverses in a block and goes back the way it came 073 * (e.g. b1 to b2 to b1), 074 * the block that's re-entered will get an updated direction, 075 * but the direction of this block (b2 in the example) is not updated. 076 * In other words, 077 * we're not noticing that the train must have reversed to go back out. 078 * </ul> 079 * <p> 080 * Do not assume that a Block object uniquely represents a piece of track. 081 * To allow independent development, it must be possible for multiple Block objects 082 * to take care of a particular section of track. 083 * <p> 084 * Possible state values: 085 * <ul> 086 * <li>UNKNOWN - The sensor shows UNKNOWN, so this block doesn't know if it's 087 * occupied or not. 088 * <li>INCONSISTENT - The sensor shows INCONSISTENT, so this block doesn't know 089 * if it's occupied or not. 090 * <li>OCCUPIED - This sensor went active. Note that OCCUPIED will be set even 091 * if the logic is unable to figure out which value to take. 092 * <li>UNOCCUPIED - No content, because the sensor has determined this block is 093 * unoccupied. 094 * <li>UNDETECTED - No sensor configured. 095 * </ul> 096 * <p> 097 * Possible Curvature attributes (optional) 098 * User can set the curvature if desired for use in automatic running of trains, 099 * to indicate where slow down is required. 100 * <ul> 101 * <li>NONE - No curvature in Block track, or Not entered. 102 * <li>GRADUAL - Gradual curve - no action by engineer is warranted - full speed 103 * OK 104 * <li>TIGHT - Tight curve in Block track - Train should slow down some 105 * <li>SEVERE - Severe curve in Block track - Train should slow down a lot 106 * </ul> 107 * <p> 108 * The length of the block may also optionally be entered if desired. 109 * This attribute is for use in automatic running of trains. 110 * Length should be the actual length of model railroad track in the block. 111 * It is always stored here in millimeter units. 112 * A length of 0.0 indicates no entry of length by the user. 113 * 114 * <p><a href="doc-files/Block.png"><img src="doc-files/Block.png" alt="State diagram for train tracking" height="33%" width="33%"></a> 115 * 116 * @author Bob Jacobsen Copyright (C) 2006, 2008, 2014 117 * @author Dave Duchamp Copywright (C) 2009 118 */ 119 120/* 121 * @startuml jmri/doc-files/Block.png 122 * hide empty description 123 * note as N1 #E0E0FF 124 * State diagram for tracking through sequential blocks with train 125 * direction information. "Left" and "Right" refer to blocks on either 126 * side. There's one state machine associated with each block. 127 * Assumes never more than one train in a block, e.g. due to signals. 128 * end note 129 * 130 * state Empty 131 * 132 * state "Train >>>" as TR 133 * 134 * state "<<< Train" as TL 135 * 136 * [*] --> Empty 137 * 138 * TR -up-> Empty : Goes Unoccupied 139 * Empty -down-> TR : Goes Occupied & Left >>> 140 * note on link #FFAAAA: Copy Train From Left 141 * 142 * Empty -down-> TL : Goes Occupied & Right <<< 143 * note on link #FFAAAA: Copy Train From Right 144 * TL -up-> Empty : Goes Unoccupied 145 146 * TL -right-> TR : Tracked train changes direction to >>> 147 * TR -left-> TL : Tracked train changes direction to <<< 148 * 149 * state "Intervention Required" as IR 150 * note bottom of IR #FFAAAA : Something else needs to set Train ID and Direction in Block 151 * 152 * Empty -right-> IR : Goes Occupied & ! (Left >>> | Right <<<) 153 * @enduml 154 */ 155 156public class Block extends AbstractNamedBean implements PhysicalLocationReporter { 157 158 /** 159 * Create a new Block. 160 * @param systemName Block System Name. 161 */ 162 public Block(String systemName) { 163 super(systemName); 164 } 165 166 /** 167 * Create a new Block. 168 * @param systemName system name. 169 * @param userName user name. 170 */ 171 public Block(String systemName, String userName) { 172 super(systemName, userName); 173 } 174 175 public static final int OCCUPIED = Sensor.ACTIVE; 176 public static final int UNOCCUPIED = Sensor.INACTIVE; 177 178 /** 179 * Undetected status, i.e a "Dark" block. 180 * A Block with unknown status could be waiting on feedback from a Sensor, 181 * hence undetected may be more appropriate if no Sensor. 182 * <p> 183 * OBlocks use this constant in combination with other OBlock status flags. 184 * Block uses this constant as initial status, also when a Sensor is unset 185 * from the block. 186 * 187 */ 188 public static final int UNDETECTED = 0x100; // bit coded, just in case; really should be enum 189 190 /** 191 * No Curvature. 192 */ 193 public static final int NONE = 0x00; 194 195 /** 196 * Gradual Curvature. 197 */ 198 public static final int GRADUAL = 0x01; 199 200 /** 201 * Tight Curvature. 202 */ 203 public static final int TIGHT = 0x02; 204 205 /** 206 * Severe Curvature. 207 */ 208 public static final int SEVERE = 0x04; 209 210 /** 211 * Create a Debug String, 212 * this should only be used for debugging... 213 * @return Block User name, System name, current state as string value. 214 */ 215 public String toDebugString() { 216 return getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME) 217 + " " + describeState(getState()); 218 } 219 220 /** 221 * Property name change fired when a Sensor is set to / removed from a Block. 222 * The fired event includes 223 * old value: Sensor Bean Object if previously set, else null 224 * new value: Sensor Bean Object if being set, may be null if Sensor removed. 225 */ 226 public static final String OCC_SENSOR_CHANGE = "OccupancySensorChange"; // NOI18N 227 228 /** 229 * Set the sensor by name. 230 * Fires propertyChange "OccupancySensorChange" when changed. 231 * @param pName the name of the Sensor to set 232 * @return true if a Sensor is set and is not null; false otherwise 233 */ 234 public boolean setSensor(String pName) { 235 Sensor oldSensor = getSensor(); 236 if (pName == null || pName.isEmpty()) { 237 if (oldSensor!=null) { 238 setNamedSensor(null); 239 firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, null); 240 } 241 return false; 242 } 243 if (InstanceManager.getNullableDefault(SensorManager.class) != null) { 244 try { 245 Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName); 246 if (sensor.equals(oldSensor)) { 247 return false; 248 } 249 setNamedSensor(InstanceManager.getDefault( 250 NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor)); 251 firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, sensor); 252 return true; 253 } catch (IllegalArgumentException ex) { 254 setNamedSensor(null); 255 firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, null); 256 log.error("Sensor '{}' not available", pName); 257 } 258 } else { 259 log.error("No SensorManager for this protocol"); 260 } 261 return false; 262 } 263 264 /** 265 * Set Block Occupancy Sensor. 266 * If Sensor set, Adds PCL, sets Block Occupancy Status to Sensor. 267 * Block State PropertyChange Event will fire. 268 * Does NOT route initial Sensor Status via goingUnknown() / goingActive() etc. 269 * <p> 270 * If Sensor null, removes PCL on previous Sensor, sets Block status to UNDETECTED. 271 * @param s Handle for Sensor. 272 */ 273 public void setNamedSensor(@CheckForNull NamedBeanHandle<Sensor> s) { 274 if ( _namedSensor != null && _sensorListener != null) { 275 _namedSensor.getBean().removePropertyChangeListener(_sensorListener); 276 _sensorListener = null; 277 } 278 _namedSensor = s; 279 280 if (_namedSensor != null) { 281 _sensorListener = this::handleSensorChange; 282 _namedSensor.getBean().addPropertyChangeListener(_sensorListener, 283 s.getName(), "Block Sensor " + getDisplayName()); 284 setState(_namedSensor.getBean().getState()); 285 // At present does NOT route via goingUnknown() / goingActive() etc. 286 } else { 287 setState(UNDETECTED); // Does NOT route via goingUnknown() / goingActive() etc. 288 } 289 } 290 291 /** 292 * Get the Block Occupancy Sensor. 293 * @return Sensor if one attached to Block, may be null. 294 */ 295 @CheckForNull 296 public Sensor getSensor() { 297 if (_namedSensor != null) { 298 return _namedSensor.getBean(); 299 } 300 return null; 301 } 302 303 @CheckForNull 304 public NamedBeanHandle<Sensor> getNamedSensor() { 305 return _namedSensor; 306 } 307 308 /** 309 * Property name change fired when a Sensor is set to / removed from a Block. 310 * The fired event includes 311 * old value: Sensor Bean Object if previously set, else null 312 * new value: Sensor Bean Object if being set, may be null if Sensor removed. 313 */ 314 public static final String BLOCK_REPORTER_CHANGE = "BlockReporterChange"; // NOI18N 315 316 /** 317 * Set the Reporter that should provide the data value for this block. 318 * Fires propertyChange "BlockReporterChange" when changed. 319 * @see Reporter 320 * @param reporter Reporter object to link, or null to clear 321 */ 322 public void setReporter(@CheckForNull Reporter reporter) { 323 if (Objects.equals(reporter,_reporter)) { 324 return; 325 } 326 if (_reporter != null && _reporterListener != null) { 327 _reporter.removePropertyChangeListener(_reporterListener); 328 _reporterListener = null; 329 } 330 Reporter oldReporter = _reporter; 331 _reporter = reporter; 332 if (_reporter != null) { 333 _reporterListener = this::handleReporterChange; 334 _reporter.addPropertyChangeListener( _reporterListener ); 335 } 336 firePropertyChange(BLOCK_REPORTER_CHANGE, oldReporter, reporter); 337 } 338 339 /** 340 * Retrieve the Reporter that is linked to this Block 341 * 342 * @see Reporter 343 * @return linked Reporter object, or null if not linked 344 */ 345 @CheckForNull 346 public Reporter getReporter() { 347 return _reporter; 348 } 349 350 /** 351 * Property name change fired when the Block reporting Current flag changes. 352 * The fired event includes 353 * old value: previous value, Boolean. 354 * new value: new value, Boolean. 355 */ 356 public static final String BLOCK_REPORTING_CURRENT = "BlockReportingCurrent"; // NOI18N 357 358 /** 359 * Define if the Block's value should be populated from the 360 * {@link Reporter#getCurrentReport() current report} or from the 361 * {@link Reporter#getLastReport() last report}. 362 * Fires propertyChange "BlockReportingCurrent" when changed. 363 * @see Reporter 364 * @param reportingCurrent true if to use current report; false if to use 365 * last report 366 */ 367 public void setReportingCurrent(boolean reportingCurrent) { 368 if (_reportingCurrent != reportingCurrent) { 369 _reportingCurrent = reportingCurrent; 370 firePropertyChange(BLOCK_REPORTING_CURRENT, !reportingCurrent, reportingCurrent); 371 } 372 } 373 374 /** 375 * Determine if the Block's value is being populated from the 376 * {@link Reporter#getCurrentReport() current report} or from the 377 * {@link Reporter#getLastReport() last report}. 378 * 379 * @see Reporter 380 * @return true if populated by 381 * {@link Reporter#getCurrentReport() current report}; false if from 382 * {@link Reporter#getLastReport() last report}. 383 */ 384 public boolean isReportingCurrent() { 385 return _reportingCurrent; 386 } 387 388 /** 389 * Get the Block State. 390 * OBlocks may well return a combination of states, 391 * Blocks will return a single State. 392 * @return Block state. 393 */ 394 @Override 395 public int getState() { 396 return _current; 397 } 398 399 private final ArrayList<Path> paths = new ArrayList<>(); 400 401 /** 402 * Add a Path to List of Paths. 403 * @param p Path to add, not null. 404 */ 405 public void addPath(@Nonnull Path p) { 406 if (p == null) { 407 throw new IllegalArgumentException("Can't add null path"); 408 } 409 paths.add(p); 410 } 411 412 /** 413 * Remove a Path from the Block. 414 * @param p Path to remove. 415 */ 416 public void removePath(Path p) { 417 int j = -1; 418 for (int i = 0; i < paths.size(); i++) { 419 if (p == paths.get(i)) { 420 j = i; 421 } 422 } 423 if (j > -1) { 424 paths.remove(j); 425 } 426 } 427 428 /** 429 * Check if Block has a particular Path. 430 * @param p Path to test against. 431 * @return true if Block has the Path, else false. 432 */ 433 public boolean hasPath(Path p) { 434 return paths.stream().anyMatch( t -> t.equals(p) ); 435 } 436 437 /** 438 * Get a copy of the list of Paths. 439 * 440 * @return the paths or an empty list 441 */ 442 @Nonnull 443 public List<Path> getPaths() { 444 return new ArrayList<>(paths); 445 } 446 447 /** 448 * Provide a general method for updating the report. 449 * Fires propertyChange "state" when called. 450 * 451 * @param v the new state 452 */ 453 @SuppressWarnings("deprecation") // The method getId() from the type Thread is deprecated since version 19 454 // The replacement Thread.threadId() isn't available before version 19 455 @Override 456 public void setState(int v) { 457 int old = _current; 458 _current = v; 459 // notify 460 461 // It is rather unpleasant that the following needs to be done in a try-catch, but exceptions have been observed 462 try { 463 firePropertyChange("state", old, _current); 464 } catch (Exception e) { 465 log.error("{} got exception during firePropertyChange({},{}) in thread {} {}", 466 getDisplayName(), old, _current, 467 Thread.currentThread().getName(), Thread.currentThread().getId(), e); 468 } 469 } 470 471 /** 472 * Set the value retained by this Block. 473 * Also used when the Block itself gathers a value from an adjacent Block. 474 * This can be overridden in a subclass if 475 * e.g. you want to keep track of Blocks elsewhere, 476 * but make sure you also eventually invoke the super.setValue() here. 477 * Fires propertyChange "value" when changed. 478 * 479 * @param value The new Object resident in this block, or null if none 480 */ 481 public void setValue(Object value) { 482 //ignore if unchanged 483 if (value != _value) { 484 log.debug("Block {} value changed from '{}' to '{}'", getDisplayName(), _value, value); 485 _previousValue = _value; 486 _value = value; 487 firePropertyChange("value", _previousValue, _value); // NOI18N 488 } 489 } 490 491 /** 492 * Get the Block Contents Value. 493 * @return object with current value, could be null. 494 */ 495 @CheckForNull 496 public Object getValue() { 497 return _value; 498 } 499 500 /** 501 * Set Block Direction of Travel. 502 * Fires propertyChange "direction" when changed. 503 * @param direction Path Constant form, see {@link Path Path.java} 504 */ 505 public void setDirection(int direction) { 506 //ignore if unchanged 507 if (direction != _direction) { 508 log.debug("Block {} direction changed from {} to {}", getDisplayName(), 509 Path.decodeDirection(_direction), Path.decodeDirection(direction)); 510 int oldDirection = _direction; 511 _direction = direction; 512 // this is a bound parameter 513 firePropertyChange("direction", oldDirection, direction); // NOI18N 514 } 515 } 516 517 /** 518 * Get Block Direction of Travel. 519 * @return direction in Path Constant form, see {@link Path Path.java} 520 */ 521 public int getDirection() { 522 return _direction; 523 } 524 525 //Deny traffic entering from this block 526 private final ArrayList<NamedBeanHandle<Block>> blockDenyList = new ArrayList<>(1); 527 528 /** 529 * Add to the Block Deny List. 530 * 531 * The block deny list, is used by higher level code, to determine if 532 * traffic/trains should be allowed to enter from an attached block, the 533 * list only deals with blocks that access should be denied from. 534 * <p> 535 * If we want to prevent traffic from following from this Block to another, 536 * then this Block must be added to the deny list of the other Block. 537 * By default no Block is barred, so traffic flow is bi-directional. 538 * @param pName name of the block to add, which must exist 539 */ 540 public void addBlockDenyList(@Nonnull String pName) { 541 Block blk = InstanceManager.getDefault(BlockManager.class).getBlock(pName); 542 if (blk == null) { 543 throw new IllegalArgumentException("addBlockDenyList requests block \"" + pName + "\" exists"); 544 } 545 NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault( 546 NamedBeanHandleManager.class).getNamedBeanHandle(pName, blk); 547 if (!blockDenyList.contains(namedBlock)) { 548 blockDenyList.add(namedBlock); 549 } 550 } 551 552 public void addBlockDenyList(@Nonnull Block blk) { 553 NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault( 554 NamedBeanHandleManager.class).getNamedBeanHandle(blk.getDisplayName(), blk); 555 if (!blockDenyList.contains(namedBlock)) { 556 blockDenyList.add(namedBlock); 557 } 558 } 559 560 public void removeBlockDenyList(String blk) { 561 NamedBeanHandle<Block> toremove = null; 562 for (NamedBeanHandle<Block> bean : blockDenyList) { 563 if (bean.getName().equals(blk)) { 564 toremove = bean; 565 } 566 } 567 if (toremove != null) { 568 blockDenyList.remove(toremove); 569 } 570 } 571 572 public void removeBlockDenyList(Block blk) { 573 NamedBeanHandle<Block> toremove = null; 574 for (NamedBeanHandle<Block> bean : blockDenyList) { 575 if (bean.getBean() == blk) { 576 toremove = bean; 577 } 578 } 579 if (toremove != null) { 580 blockDenyList.remove(toremove); 581 } 582 } 583 584 public List<String> getDeniedBlocks() { 585 List<String> list = new ArrayList<>(blockDenyList.size()); 586 blockDenyList.forEach( bean -> list.add(bean.getName()) ); 587 return list; 588 } 589 590 public boolean isBlockDenied(String deny) { 591 return blockDenyList.stream().anyMatch( bean -> bean.getName().equals(deny)); 592 } 593 594 public boolean isBlockDenied(Block deny) { 595 return blockDenyList.stream().anyMatch( bean -> bean.getBean() == deny); 596 } 597 598 /** 599 * Get if Block can have permissive working. 600 * Blocks default to non-permissive, i.e. false. 601 * @return true if permissive, else false. 602 */ 603 public boolean getPermissiveWorking() { 604 return _permissiveWorking; 605 } 606 607 /** 608 * Property name change fired when the Block Permissive Status changes. 609 * The fired event includes 610 * old value: previous permissive status. 611 * new value: new permissive status. 612 */ 613 public static final String BLOCK_PERMISSIVE_CHANGE = "BlockPermissiveWorking"; // NOI18N 614 615 /** 616 * Set Block as permissive. 617 * Fires propertyChange "BlockPermissiveWorking" when changed. 618 * @param w true permissive, false NOT permissive 619 */ 620 public void setPermissiveWorking(boolean w) { 621 if (_permissiveWorking != w) { 622 _permissiveWorking = w; 623 firePropertyChange(BLOCK_PERMISSIVE_CHANGE, !w, w); // NOI18N 624 } 625 } 626 627 private boolean _permissiveWorking = false; 628 629 /** 630 * Get if Block is a ghost. 631 * Blocks default to non-ghost, i.e. false. 632 * @return true if ghost, else false. 633 */ 634 public boolean getIsGhost() { 635 return _ghost; 636 } 637 638 /** 639 * Property name change fired when the Block ghost Status changes. 640 * The fired event includes 641 * old value: previous ghost status. 642 * new value: new ghost status. 643 */ 644 public static final String GHOST_CHANGE = "BlockGhost"; // NOI18N 645 646 /** 647 * Set if the block is a ghost 648 * Fires propertyChange "BlockGhost" when changed. 649 * @param w true ghost, false NOT ghost 650 */ 651 public void setIsGhost(boolean w) { 652 if (_ghost != w) { 653 _ghost = w; 654 firePropertyChange(GHOST_CHANGE, !w, w); // NOI18N 655 } 656 } 657 658 private boolean _ghost = false; 659 660 public float getSpeedLimit() { 661 if ((_blockSpeed == null) || (_blockSpeed.isEmpty())) { 662 return -1; 663 } 664 String speed = _blockSpeed; 665 if ( "Global".equals( _blockSpeed)) { 666 speed = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed(); 667 } 668 669 try { 670 return Float.parseFloat(speed); 671 } catch (NumberFormatException nx) { 672 //considered normal if the speed is not a number. 673 } 674 try { 675 return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 676 } catch (IllegalArgumentException ex) { 677 return -1; 678 } 679 } 680 681 private String _blockSpeed = ""; 682 683 public String getBlockSpeed() { 684 if ( "Global".equals( _blockSpeed)) { 685 return (Bundle.getMessage("UseGlobal", "Global") + " " 686 + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); 687 // Ensure the word "Global" is always in the speed name for later comparison 688 } 689 return _blockSpeed; 690 } 691 692 /** 693 * Property name change fired when the Block Speed changes. 694 * The fired event includes 695 * old value: previous speed String. 696 * new value: new speed String. 697 */ 698 public static final String BLOCK_SPEED_CHANGE = "BlockSpeedChange"; // NOI18N 699 700 /** 701 * Set the Block Speed Name. 702 * <p> 703 * Does not perform name validity checking. 704 * Does not send Property Change Event. 705 * @param s new Speed Name String. 706 */ 707 public void setBlockSpeedName(String s) { 708 if (s == null) { 709 _blockSpeed = ""; 710 } else { 711 _blockSpeed = s; 712 } 713 } 714 715 /** 716 * Set the Block Speed, preferred method. 717 * <p> 718 * Fires propertyChange "BlockSpeedChange" when changed. 719 * @param s Speed String 720 * @throws JmriException if Value of requested block speed is not valid. 721 */ 722 public void setBlockSpeed(final String s) throws JmriException { 723 if ((s == null) || (_blockSpeed.equals(s))) { 724 return; 725 } 726 String newSpeed = s; 727 if (s.contains("Global")) { 728 newSpeed = "Global"; 729 } else { 730 try { 731 Float.valueOf(s); 732 } catch (NumberFormatException nx) { 733 try { 734 InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s); 735 } catch (IllegalArgumentException ex) { 736 throw new JmriException("Block \"" + getDisplayName() 737 + "\" requested speed value \"" + s + "\" invalid."); 738 } 739 } 740 } 741 String oldSpeed = _blockSpeed; 742 _blockSpeed = newSpeed; 743 firePropertyChange(BLOCK_SPEED_CHANGE, oldSpeed, s); 744 } 745 746 /** 747 * Property name change fired when the Block Curvature changes. 748 * The fired event includes 749 * old value: previous Block Curvature Constant. 750 * new value: new Block Curvature Constant. 751 */ 752 public static final String BLOCK_CURVATURE_CHANGE = "BlockCurvatureChange"; // NOI18N 753 754 /** 755 * Set Block Curvature Constant. 756 * Valid values : 757 * Block.NONE, Block.GRADUAL, Block.TIGHT, Block.SEVERE 758 * Fires propertyChange "BlockCurvatureChange" when changed. 759 * @param c Constant, e.g. Block.GRADUAL 760 */ 761 public void setCurvature(int c) { 762 if (_curvature!=c) { 763 int oldCurve = _curvature; 764 _curvature = c; 765 firePropertyChange(BLOCK_CURVATURE_CHANGE, oldCurve, c); 766 } 767 } 768 769 /** 770 * Get Block Curvature Constant. 771 * Defaults to Block.NONE 772 * @return constant, e.g. Block.TIGHT 773 */ 774 public int getCurvature() { 775 return _curvature; 776 } 777 778 /** 779 * Property name change fired when the Block Length changes. 780 * The fired event includes 781 * old value: previous float length (mm). 782 * new value: new float length (mm). 783 */ 784 public static final String BLOCK_LENGTH_CHANGE = "BlockLengthChange"; // NOI18N 785 786 /** 787 * Set length in millimeters. 788 * Paths will inherit this length, if their length is not specifically set. 789 * This length is the maximum length of any Path in the block. 790 * Path lengths exceeding this will be set to the default length. 791 * <p> 792 * Fires propertyChange "BlockLengthChange" when changed, float values in mm. 793 * @param l length in millimeters 794 */ 795 public void setLength(float l) { 796 float oldLen = getLengthMm(); 797 if (Math.abs(oldLen - l) > 0.0001){ // length value is different 798 _length = l; 799 getPaths().stream().forEach(p -> { 800 if (p.getLength() > l) { 801 p.setLength(0); // set to default 802 } 803 }); 804 firePropertyChange(BLOCK_LENGTH_CHANGE, oldLen, l); 805 } 806 } 807 808 /** 809 * Get Block Length in Millimetres. 810 * Default 0.0f. 811 * @return length in mm. 812 */ 813 public float getLengthMm() { 814 return _length; 815 } 816 817 /** 818 * Get Block Length in Centimetres. 819 * Courtesy method using result from getLengthMm. 820 * @return length in centimetres. 821 */ 822 public float getLengthCm() { 823 return (_length / 10.0f); 824 } 825 826 /** 827 * Get Block Length in Inches. 828 * Courtesy method using result from getLengthMm. 829 * @return length in inches. 830 */ 831 public float getLengthIn() { 832 return (_length / 25.4f); 833 } 834 835 /** 836 * Note: this has to make choices about identity values (always the same) 837 * and operation values (can change as the block works). Might be missing 838 * some identity values. 839 */ 840 @Override 841 public boolean equals(Object obj) { 842 if (obj == this) { 843 return true; 844 } 845 if (obj == null) { 846 return false; 847 } 848 849 if ( getClass() != obj.getClass() ) { 850 return false; 851 } else { 852 Block b = (Block) obj; 853 return b.getSystemName().equals(this.getSystemName()); 854 } 855 } 856 857 @Override 858 // This can't change, so can't include mutable values 859 public int hashCode() { 860 return this.getSystemName().hashCode(); 861 } 862 863 // internal data members 864 private int _current = UNDETECTED; // state until sensor is set 865 private NamedBeanHandle<Sensor> _namedSensor = null; 866 private PropertyChangeListener _sensorListener = null; 867 private Object _value; 868 private Object _previousValue; 869 private int _direction; 870 private int _curvature = NONE; 871 private float _length = 0.0f; // always stored in millimeters 872 private Reporter _reporter = null; 873 private PropertyChangeListener _reporterListener = null; 874 private boolean _reportingCurrent = false; 875 876 private Path[] pListOfPossibleEntrancePaths = null; 877 private int cntOfPossibleEntrancePaths = 0; 878 879 void resetCandidateEntrancePaths() { 880 pListOfPossibleEntrancePaths = null; 881 cntOfPossibleEntrancePaths = 0; 882 } 883 884 boolean setAsEntryBlockIfPossible(Block b) { 885 for (int i = 0; i < cntOfPossibleEntrancePaths; i++) { 886 Block candidateBlock = pListOfPossibleEntrancePaths[i].getBlock(); 887 if (candidateBlock == b) { 888 setValue(candidateBlock.getValue()); 889 setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection()); 890 log.info("Block {} gets LATE new value from {}, direction= {}", 891 getDisplayName(), candidateBlock.getDisplayName(), Path.decodeDirection(getDirection())); 892 resetCandidateEntrancePaths(); 893 return true; 894 } 895 } 896 return false; 897 } 898 899 /** 900 * Handle change in sensor state. 901 * <p> 902 * Defers real work to goingActive, goingInactive methods. 903 * 904 * @param e the event 905 */ 906 void handleSensorChange(PropertyChangeEvent e) { 907 Sensor s = getSensor(); 908 if ( "KnownState".equals( e.getPropertyName()) && s != null ) { 909 int state = s.getState(); 910 switch (state) { 911 case Sensor.ACTIVE: 912 goingActive(); 913 break; 914 case Sensor.INACTIVE: 915 goingInactive(); 916 break; 917 case Sensor.UNKNOWN: 918 goingUnknown(); 919 break; 920 default: 921 goingInconsistent(); 922 break; 923 } 924 } 925 } 926 927 public void goingUnknown() { 928 setValue(null); 929 setState(UNKNOWN); 930 } 931 932 public void goingInconsistent() { 933 setValue(null); 934 setState(INCONSISTENT); 935 } 936 937 /** 938 * Handle change in Reporter value. 939 * 940 * @param e PropertyChangeEvent 941 */ 942 void handleReporterChange(PropertyChangeEvent e) { 943 if ((_reportingCurrent && "currentReport".equals(e.getPropertyName())) 944 || (!_reportingCurrent && "lastReport".equals(e.getPropertyName()))) { 945 setValue(e.getNewValue()); 946 } 947 } 948 949 private Instant _timeLastInactive; 950 951 /** 952 * Handles Block sensor going INACTIVE: this block is empty 953 */ 954 public void goingInactive() { 955 log.debug("Block {} goes UNOCCUPIED", getDisplayName()); 956 for (Path path : paths) { 957 Block b = path.getBlock(); 958 if (b != null) { 959 b.setAsEntryBlockIfPossible(this); 960 } 961 } 962 setValue(null); 963 setDirection(Path.NONE); 964 setState(UNOCCUPIED); 965 _timeLastInactive = Instant.now(); 966 } 967 968 private static final int MAXINFOMESSAGES = 5; 969 private int infoMessageCount = 0; 970 971 /** 972 * Handles Block sensor going ACTIVE: this block is now occupied, figure out 973 * from who and copy their value. 974 */ 975 public void goingActive() { 976 if (getState() == OCCUPIED) { 977 return; 978 } 979 log.debug("Block {} goes OCCUPIED", getDisplayName()); 980 resetCandidateEntrancePaths(); 981 // index through the paths, counting 982 int count = 0; 983 Path next = null; 984 // get statuses of everything once 985 int currPathCnt = paths.size(); 986 Path[] pList = new Path[currPathCnt]; 987 boolean[] isSet = new boolean[currPathCnt]; 988 boolean[] isActive = new boolean[currPathCnt]; 989 int[] pDir = new int[currPathCnt]; 990 int[] pFromDir = new int[currPathCnt]; 991 for (int i = 0; i < currPathCnt; i++) { 992 pList[i] = paths.get(i); 993 isSet[i] = pList[i].checkPathSet(); 994 Block b = pList[i].getBlock(); 995 if (b != null) { 996 isActive[i] = b.getState() == OCCUPIED; 997 pDir[i] = b.getDirection(); 998 } else { 999 isActive[i] = false; 1000 pDir[i] = -1; 1001 } 1002 pFromDir[i] = pList[i].getFromBlockDirection(); 1003 if (isSet[i] && isActive[i]) { 1004 count++; 1005 next = pList[i]; 1006 } 1007 } 1008 // sort on number of neighbors 1009 switch (count) { 1010 case 0: 1011 if (null != _previousValue) { 1012 // restore the previous value under either of these circumstances: 1013 // 1. the block has been 'unoccupied' only very briefly 1014 // 2. power has just come back on 1015 Instant tn = Instant.now(); 1016 BlockManager bm = InstanceManager.getDefault(BlockManager.class); 1017 if ( bm.timeSinceLastLayoutPowerOn() < 5000 || 1018 (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) { 1019 setValue(_previousValue); 1020 if (infoMessageCount < MAXINFOMESSAGES) { 1021 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}." 1022 +" Restoring previous value.", getDisplayName()); 1023 infoMessageCount++; 1024 } 1025 } else if (log.isDebugEnabled()) { 1026 if (null != _timeLastInactive) { 1027 log.debug("not restoring previous value, block {} has been inactive for too long ({}ms)" 1028 + " and layout power has not just been restored ({}ms ago)", 1029 getDisplayName(), tn.toEpochMilli() - _timeLastInactive.toEpochMilli(), 1030 bm.timeSinceLastLayoutPowerOn()); 1031 } else { 1032 log.debug("not restoring previous value, block {} has been inactive since the " 1033 + "start of this session and layout power has not just been restored ({}ms ago)", 1034 getDisplayName(), bm.timeSinceLastLayoutPowerOn()); 1035 } 1036 } 1037 } else { 1038 if (infoMessageCount < MAXINFOMESSAGES) { 1039 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.", 1040 getDisplayName()); 1041 infoMessageCount++; 1042 } 1043 } 1044 break; 1045 case 1: 1046 // simple case 1047 if ((next != null) && (next.getBlock() != null)) { 1048 // normal case, transfer value object 1049 setValue(next.getBlock().getValue()); 1050 setDirection(next.getFromBlockDirection()); 1051 log.debug("Block {} gets new value '{}' from {}, direction={}", 1052 getDisplayName(), 1053 next.getBlock().getValue(), 1054 next.getBlock().getDisplayName(), 1055 Path.decodeDirection(getDirection())); 1056 } else if (next == null) { 1057 log.error("unexpected next==null processing block {}", getDisplayName()); 1058 } else { 1059 log.error("unexpected next.getBlock()=null processing block {}", getDisplayName()); 1060 } 1061 break; 1062 default: 1063 // count > 1, check for one with proper direction 1064 // this time, count ones with proper direction 1065 log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count); 1066 next = null; 1067 count = 0; 1068 // true until it's found that some neighbor blocks contain different contents (trains) 1069 boolean allNeighborsAgree = true; 1070 1071 // scan for neighbors without matching direction 1072 for (int i = 0; i < currPathCnt; i++) { 1073 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1074 log.debug("comparing {} ({}) to {} ({})", 1075 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1076 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1077 //use bitwise comparison to support combination directions such as "North, West" 1078 if ((pDir[i] & pFromDir[i]) > 0) { 1079 if (next != null && next.getBlock() != null ) { 1080 Object value = next.getBlock().getValue(); 1081 if ( value != null && !value.equals(pList[i].getBlock().getValue())) { 1082 allNeighborsAgree = false; 1083 } 1084 } 1085 count++; 1086 next = pList[i]; 1087 } 1088 } 1089 } 1090 1091 // If loop above didn't find neighbors with matching direction, scan w/out direction for neighbors 1092 // This is used when directions are not being used 1093 if (next == null) { 1094 for (int i = 0; i < currPathCnt; i++) { 1095 if (isSet[i] && isActive[i]) { 1096 if (next != null && next.getBlock() != null ) { 1097 Object value = next.getBlock().getValue(); 1098 if ( value != null && ! value.equals(pList[i].getBlock().getValue())) { 1099 allNeighborsAgree = false; 1100 } 1101 } 1102 count++; 1103 next = pList[i]; 1104 } 1105 } 1106 } 1107 1108 if (next != null && count == 1) { 1109 // found one block with proper direction, use it 1110 setValue(next.getBlock().getValue()); 1111 setDirection(next.getFromBlockDirection()); 1112 log.debug("Block {} gets new value '{}' from {}, direction {}", 1113 getDisplayName(), next.getBlock().getValue(), 1114 next.getBlock().getDisplayName(), Path.decodeDirection(getDirection())); 1115 } else { 1116 // handle merging trains: All neighbors with same content (train ID) 1117 if (allNeighborsAgree && next != null) { 1118 setValue(next.getBlock().getValue()); 1119 setDirection(next.getFromBlockDirection()); 1120 } else { 1121 // don't all agree, so can't determine unique value 1122 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for" 1123 + " block {} but maybe it can be determined when another block becomes free", 1124 count, getDisplayName()); 1125 pListOfPossibleEntrancePaths = new Path[currPathCnt]; 1126 cntOfPossibleEntrancePaths = 0; 1127 for (int i = 0; i < currPathCnt; i++) { 1128 if (isSet[i] && isActive[i]) { 1129 pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i]; 1130 cntOfPossibleEntrancePaths++; 1131 } 1132 } 1133 } 1134 } 1135 break; 1136 } 1137 setState(OCCUPIED); 1138 } 1139 1140 /** 1141 * Find which path this Block became Active, without actually modifying the 1142 * state of this block. 1143 * <p> 1144 * (this is largely a copy of the 'Search' part of the logic from 1145 * goingActive()) 1146 * 1147 * @return the next path 1148 */ 1149 @CheckForNull 1150 public Path findFromPath() { 1151 // index through the paths, counting 1152 int count = 0; 1153 Path next = null; 1154 // get statuses of everything once 1155 int currPathCnt = paths.size(); 1156 Path[] pList = new Path[currPathCnt]; 1157 boolean[] isSet = new boolean[currPathCnt]; 1158 boolean[] isActive = new boolean[currPathCnt]; 1159 int[] pDir = new int[currPathCnt]; 1160 int[] pFromDir = new int[currPathCnt]; 1161 for (int i = 0; i < currPathCnt; i++) { 1162 pList[i] = paths.get(i); 1163 isSet[i] = pList[i].checkPathSet(); 1164 Block b = pList[i].getBlock(); 1165 if (b != null) { 1166 isActive[i] = b.getState() == OCCUPIED; 1167 pDir[i] = b.getDirection(); 1168 } else { 1169 isActive[i] = false; 1170 pDir[i] = -1; 1171 } 1172 pFromDir[i] = pList[i].getFromBlockDirection(); 1173 if (isSet[i] && isActive[i]) { 1174 count++; 1175 next = pList[i]; 1176 } 1177 } 1178 // sort on number of neighbors 1179 if ((count == 0) || (count == 1)) { 1180 // do nothing. OK to return null from this function. "next" is already set. 1181 } else { 1182 // count > 1, check for one with proper direction 1183 // this time, count ones with proper direction 1184 log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count); 1185 next = null; 1186 count = 0; 1187 for (int i = 0; i < currPathCnt; i++) { 1188 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1189 log.debug("comparing {} ({}) to {} ({})", 1190 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1191 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1192 // Use bitwise comparison to support combination directions such as "North, West" 1193 if ((pDir[i] & pFromDir[i]) > 0) { 1194 count++; 1195 next = pList[i]; 1196 } 1197 } 1198 } 1199 if (next == null) { 1200 log.debug("next is null!"); 1201 } 1202 if (next != null && count == 1) { 1203 // found one block with proper direction, assume that 1204 } else { 1205 // no unique path with correct direction - this happens frequently from noise in block detectors!! 1206 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}", 1207 count, getDisplayName()); 1208 } 1209 } 1210 // in any case, go OCCUPIED 1211 if (log.isDebugEnabled()) { // avoid potentially expensive non-logging 1212 log.debug("Block {} with direction {} gets new value from {} + (informational. No state change)", 1213 getDisplayName(), Path.decodeDirection(getDirection()), 1214 (next != null ? next.getBlock().getDisplayName() : "(no next block)")); 1215 } 1216 return next; 1217 } 1218 1219 /** 1220 * This allows the layout block to inform any listeners to the block 1221 * that the higher level layout block has been set to "useExtraColor" which is an 1222 * indication that it has been allocated to a section by the AutoDispatcher. 1223 * The value set is not retained in any form by the block, 1224 * it is purely to trigger a propertyChangeEvent. 1225 * @param boo Allocation status 1226 */ 1227 public void setAllocated(Boolean boo) { 1228 firePropertyChange("allocated", !boo, boo); 1229 } 1230 1231 // Methods to implmement PhysicalLocationReporter Interface 1232 // 1233 // If we have a Reporter that is also a PhysicalLocationReporter, 1234 // we will defer to that Reporter's methods. 1235 // Else we will assume a LocoNet style message to be parsed. 1236 1237 /** 1238 * Parse a given string and return the LocoAddress value that is presumed 1239 * stored within it based on this object's protocol. The Class Block 1240 * implementation defers to its associated Reporter, if it exists. 1241 * 1242 * @param rep String to be parsed 1243 * @return LocoAddress address parsed from string, or null if this Block 1244 * isn't associated with a Reporter, or is associated with a 1245 * Reporter that is not also a PhysicalLocationReporter 1246 */ 1247 @Override 1248 public LocoAddress getLocoAddress(String rep) { 1249 // Defer parsing to our associated Reporter if we can. 1250 if (rep == null) { 1251 log.warn("String input is null!"); 1252 return null; 1253 } 1254 Reporter testReporter = this.getReporter(); 1255 if ( testReporter instanceof PhysicalLocationReporter ) { 1256 return ((PhysicalLocationReporter)testReporter).getLocoAddress(rep); 1257 } else { 1258 // Assume a LocoNet-style report. This is (nascent) support for handling of Faller cars 1259 // for Dave Merrill's project. 1260 log.debug("report string: {}", rep); 1261 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1262 // Match a number followed by the word "enter". This is the LocoNet pattern. 1263 Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); 1264 Matcher m = lnp.matcher(rep); 1265 if (m.find()) { 1266 log.debug("Parsed address: {}", m.group(1)); 1267 return new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC); 1268 } else { 1269 return null; 1270 } 1271 } 1272 } 1273 1274 /** 1275 * Parses out a (possibly old) LnReporter-generated report string to extract 1276 * the direction from within it based on this object's protocol. The Class 1277 * Block implementation defers to its associated Reporter, if it exists. 1278 * 1279 * @param rep String to be parsed 1280 * @return PhysicalLocationReporter.Direction direction parsed from string, 1281 * or null if this Block isn't associated with a Reporter, or is 1282 * associated with a Reporter that is not also a 1283 * PhysicalLocationReporter 1284 */ 1285 @Override 1286 public PhysicalLocationReporter.Direction getDirection(String rep) { 1287 if (rep == null) { 1288 log.warn("String input is null!"); 1289 return (null); 1290 } 1291 // Defer parsing to our associated Reporter if we can. 1292 Reporter testReporter = this.getReporter(); 1293 if ( testReporter instanceof PhysicalLocationReporter ) { 1294 return ((PhysicalLocationReporter)testReporter).getDirection(rep); 1295 } else { 1296 log.debug("report string: {}", rep); 1297 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1298 // Match a number followed by the word "enter". This is the LocoNet pattern. 1299 Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); 1300 Matcher m = lnp.matcher(rep); 1301 if (m.find()) { 1302 log.debug("Parsed direction: {}", m.group(2)); 1303 switch (m.group(2)) { 1304 case "enter": 1305 // LocoNet Enter message 1306 return PhysicalLocationReporter.Direction.ENTER; 1307 case "seen": 1308 // Lissy message. Treat them all as "entry" messages. 1309 return PhysicalLocationReporter.Direction.ENTER; 1310 default: 1311 return PhysicalLocationReporter.Direction.EXIT; 1312 } 1313 } else { 1314 return PhysicalLocationReporter.Direction.UNKNOWN; 1315 } 1316 } 1317 } 1318 1319 /** 1320 * Return this Block's physical location, if it exists. 1321 * Defers actual work to the helper methods in class PhysicalLocation. 1322 * 1323 * @return PhysicalLocation : this Block's location. 1324 */ 1325 @Override 1326 public PhysicalLocation getPhysicalLocation() { 1327 // We have our won PhysicalLocation. That's the point. No need to defer to the Reporter. 1328 return PhysicalLocation.getBeanPhysicalLocation(this); 1329 } 1330 1331 /** 1332 * Return this Block's physical location, if it exists. 1333 * Does not use the parameter s. 1334 * Defers actual work to the helper methods in class PhysicalLocation 1335 * 1336 * @param s (this parameter is ignored) 1337 * @return PhysicalLocation : this Block's location. 1338 */ 1339 @Override 1340 public PhysicalLocation getPhysicalLocation(String s) { 1341 // We have our won PhysicalLocation. That's the point. No need to defer to the Reporter. 1342 // Intentionally ignore the String s 1343 return PhysicalLocation.getBeanPhysicalLocation(this); 1344 } 1345 1346 @Override 1347 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 1348 if ("CanDelete".equals(evt.getPropertyName())) { // No I18N 1349 if (evt.getOldValue() instanceof Sensor 1350 && evt.getOldValue().equals(getSensor())) { 1351 throw new PropertyVetoException(getDisplayName(), evt); 1352 } 1353 if (evt.getOldValue() instanceof Reporter 1354 && evt.getOldValue().equals(getReporter())) { 1355 throw new PropertyVetoException(getDisplayName(), evt); 1356 } 1357 } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N 1358 if (evt.getOldValue() instanceof Sensor 1359 && evt.getOldValue().equals(getSensor())) { 1360 setSensor(null); 1361 } 1362 if (evt.getOldValue() instanceof Reporter 1363 && evt.getOldValue().equals(getReporter())) { 1364 setReporter(null); 1365 } 1366 } 1367 } 1368 1369 @Override 1370 public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) { 1371 List<NamedBeanUsageReport> report = new ArrayList<>(); 1372 if (bean != null) { 1373 if (bean.equals(getSensor())) { 1374 report.add(new NamedBeanUsageReport("BlockSensor")); // NOI18N 1375 } 1376 if (bean.equals(getReporter())) { 1377 report.add(new NamedBeanUsageReport("BlockReporter")); // NOI18N 1378 } 1379 // Block paths 1380 getPaths().forEach( path -> { 1381 if (bean.equals(path.getBlock())) { 1382 report.add(new NamedBeanUsageReport("BlockPathNeighbor")); // NOI18N 1383 } 1384 path.getSettings().forEach( setting -> { 1385 if (bean.equals(setting.getBean())) { 1386 report.add(new NamedBeanUsageReport("BlockPathTurnout")); // NOI18N 1387 } 1388 }); 1389 }); 1390 } 1391 return report; 1392 } 1393 1394 @Override 1395 public String getBeanType() { 1396 return Bundle.getMessage("BeanNameBlock"); 1397 } 1398 1399 /** {@inheritDoc} */ 1400 @Override 1401 @Nonnull 1402 public String describeState(int state) { 1403 switch (state) { 1404 case Block.OCCUPIED: 1405 return Bundle.getMessage("BlockOccupied"); 1406 case Block.UNOCCUPIED: 1407 return Bundle.getMessage("BlockUnOccupied"); 1408 case Block.UNDETECTED: 1409 return Bundle.getMessage("BlockUndetected"); 1410 default: // state unknown, state inconsistent, state unexpected 1411 return super.describeState(state); 1412 } 1413 } 1414 1415 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Block.class); 1416}