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 * Property name change fired when a Sensor is set to / removed from a Block. 230 * The fired event includes 231 * old value: Sensor Bean Object if previously set, else null 232 * new value: Sensor Bean Object if being set, may be null if Sensor removed. 233 */ 234 public static final String BLOCK_REPORTER_CHANGE = "BlockReporterChange"; // NOI18N 235 236 /** 237 * Property name change fired when the Block reporting Current flag changes. 238 * The fired event includes 239 * old value: previous value, Boolean. 240 * new value: new value, Boolean. 241 */ 242 public static final String BLOCK_REPORTING_CURRENT = "BlockReportingCurrent"; // NOI18N 243 244 /** 245 * Property name change fired when the Block Permissive Status changes. 246 * The fired event includes 247 * old value: previous permissive status. 248 * new value: new permissive status. 249 */ 250 public static final String BLOCK_PERMISSIVE_CHANGE = "BlockPermissiveWorking"; // NOI18N 251 252 /** 253 * Property name change fired when the Block ghost Status changes. 254 * The fired event includes 255 * old value: previous ghost status. 256 * new value: new ghost status. 257 */ 258 public static final String GHOST_CHANGE = "BlockGhost"; // NOI18N 259 260 /** 261 * Property name change fired when the Block Speed changes. 262 * The fired event includes 263 * old value: previous speed String. 264 * new value: new speed String. 265 */ 266 public static final String BLOCK_SPEED_CHANGE = "BlockSpeedChange"; // NOI18N 267 268 /** 269 * Property name change fired when the Block Curvature changes. 270 * The fired event includes 271 * old value: previous Block Curvature Constant. 272 * new value: new Block Curvature Constant. 273 */ 274 public static final String BLOCK_CURVATURE_CHANGE = "BlockCurvatureChange"; // NOI18N 275 276 /** 277 * Property name change fired when the Block Length changes. 278 * The fired event includes 279 * old value: previous float length (mm). 280 * new value: new float length (mm). 281 */ 282 public static final String BLOCK_LENGTH_CHANGE = "BlockLengthChange"; // NOI18N 283 284 /** 285 * String constant for property changes to value. 286 */ 287 public static final String PROPERTY_VALUE = "value"; 288 289 /** 290 * String constant for property changes to direction. 291 */ 292 public static final String PROPERTY_DIRECTION = "direction"; 293 294 /** 295 * String constant for property changes to allocated. 296 */ 297 public static final String PROPERTY_ALLOCATED = "allocated"; 298 299 /** 300 * Set the sensor by name. 301 * Fires propertyChange "OccupancySensorChange" when changed. 302 * @param pName the name of the Sensor to set 303 * @return true if a Sensor is set and is not null; false otherwise 304 */ 305 public boolean setSensor(String pName) { 306 Sensor oldSensor = getSensor(); 307 if (pName == null || pName.isEmpty()) { 308 if (oldSensor!=null) { 309 setNamedSensor(null); 310 firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, null); 311 } 312 return false; 313 } 314 if (InstanceManager.getNullableDefault(SensorManager.class) != null) { 315 try { 316 Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName); 317 if (sensor.equals(oldSensor)) { 318 return false; 319 } 320 setNamedSensor(InstanceManager.getDefault( 321 NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor)); 322 firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, sensor); 323 return true; 324 } catch (IllegalArgumentException ex) { 325 setNamedSensor(null); 326 firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, null); 327 log.error("Sensor '{}' not available", pName); 328 } 329 } else { 330 log.error("No SensorManager for this protocol"); 331 } 332 return false; 333 } 334 335 /** 336 * Set Block Occupancy Sensor. 337 * If Sensor set, Adds PCL, sets Block Occupancy Status to Sensor. 338 * Block State PropertyChange Event will fire. 339 * Does NOT route initial Sensor Status via goingUnknown() / goingActive() etc. 340 * <p> 341 * If Sensor null, removes PCL on previous Sensor, sets Block status to UNDETECTED. 342 * @param s Handle for Sensor. 343 */ 344 public void setNamedSensor(@CheckForNull NamedBeanHandle<Sensor> s) { 345 if ( _namedSensor != null && _sensorListener != null) { 346 _namedSensor.getBean().removePropertyChangeListener(_sensorListener); 347 _sensorListener = null; 348 } 349 _namedSensor = s; 350 351 if (_namedSensor != null) { 352 _sensorListener = this::handleSensorChange; 353 _namedSensor.getBean().addPropertyChangeListener(_sensorListener, 354 _namedSensor.getName(), "Block Sensor " + getDisplayName()); 355 setState(_namedSensor.getBean().getState()); 356 // At present does NOT route via goingUnknown() / goingActive() etc. 357 } else { 358 setState(UNDETECTED); // Does NOT route via goingUnknown() / goingActive() etc. 359 } 360 } 361 362 /** 363 * Get the Block Occupancy Sensor. 364 * @return Sensor if one attached to Block, may be null. 365 */ 366 @CheckForNull 367 public Sensor getSensor() { 368 if (_namedSensor != null) { 369 return _namedSensor.getBean(); 370 } 371 return null; 372 } 373 374 @CheckForNull 375 public NamedBeanHandle<Sensor> getNamedSensor() { 376 return _namedSensor; 377 } 378 379 /** 380 * Set the Reporter that should provide the data value for this block. 381 * Fires propertyChange "BlockReporterChange" when changed. 382 * @see Reporter 383 * @param reporter Reporter object to link, or null to clear 384 */ 385 public void setReporter(@CheckForNull Reporter reporter) { 386 if (Objects.equals(reporter,_reporter)) { 387 return; 388 } 389 if (_reporter != null && _reporterListener != null) { 390 _reporter.removePropertyChangeListener(_reporterListener); 391 _reporterListener = null; 392 } 393 Reporter oldReporter = _reporter; 394 _reporter = reporter; 395 if (_reporter != null) { 396 _reporterListener = this::handleReporterChange; 397 _reporter.addPropertyChangeListener( _reporterListener ); 398 } 399 firePropertyChange(BLOCK_REPORTER_CHANGE, oldReporter, reporter); 400 } 401 402 /** 403 * Retrieve the Reporter that is linked to this Block 404 * 405 * @see Reporter 406 * @return linked Reporter object, or null if not linked 407 */ 408 @CheckForNull 409 public Reporter getReporter() { 410 return _reporter; 411 } 412 413 /** 414 * Define if the Block's value should be populated from the 415 * {@link Reporter#getCurrentReport() current report} or from the 416 * {@link Reporter#getLastReport() last report}. 417 * Fires propertyChange "BlockReportingCurrent" when changed. 418 * @see Reporter 419 * @param reportingCurrent true if to use current report; false if to use 420 * last report 421 */ 422 public void setReportingCurrent(boolean reportingCurrent) { 423 if (_reportingCurrent != reportingCurrent) { 424 _reportingCurrent = reportingCurrent; 425 firePropertyChange(BLOCK_REPORTING_CURRENT, !reportingCurrent, reportingCurrent); 426 } 427 } 428 429 /** 430 * Determine if the Block's value is being populated from the 431 * {@link Reporter#getCurrentReport() current report} or from the 432 * {@link Reporter#getLastReport() last report}. 433 * 434 * @see Reporter 435 * @return true if populated by 436 * {@link Reporter#getCurrentReport() current report}; false if from 437 * {@link Reporter#getLastReport() last report}. 438 */ 439 public boolean isReportingCurrent() { 440 return _reportingCurrent; 441 } 442 443 /** 444 * Get the Block State. 445 * OBlocks may well return a combination of states, 446 * Blocks will return a single State. 447 * @return Block state. 448 */ 449 @Override 450 public int getState() { 451 return _current; 452 } 453 454 private final ArrayList<Path> paths = new ArrayList<>(); 455 456 /** 457 * Add a Path to List of Paths. 458 * @param p Path to add, not null. 459 */ 460 public void addPath(@Nonnull Path p) { 461 if (p == null) { 462 throw new IllegalArgumentException("Can't add null path"); 463 } 464 paths.add(p); 465 } 466 467 /** 468 * Remove a Path from the Block. 469 * @param p Path to remove. 470 */ 471 public void removePath(Path p) { 472 int j = -1; 473 for (int i = 0; i < paths.size(); i++) { 474 if (p == paths.get(i)) { 475 j = i; 476 } 477 } 478 if (j > -1) { 479 paths.remove(j); 480 } 481 } 482 483 /** 484 * Check if Block has a particular Path. 485 * @param p Path to test against. 486 * @return true if Block has the Path, else false. 487 */ 488 public boolean hasPath(Path p) { 489 return paths.stream().anyMatch( t -> t.equals(p) ); 490 } 491 492 /** 493 * Get a copy of the list of Paths. 494 * 495 * @return the paths or an empty list 496 */ 497 @Nonnull 498 public List<Path> getPaths() { 499 return new ArrayList<>(paths); 500 } 501 502 /** 503 * Provide a general method for updating the report. 504 * Fires propertyChange "state" when called. 505 * 506 * @param v the new state 507 */ 508 @SuppressWarnings("deprecation") // The method getId() from the type Thread is deprecated since version 19 509 // The replacement Thread.threadId() isn't available before version 19 510 @Override 511 public void setState(int v) { 512 int old = _current; 513 _current = v; 514 // notify 515 516 // It is rather unpleasant that the following needs to be done in a try-catch, but exceptions have been observed 517 try { 518 firePropertyChange(PROPERTY_STATE, old, _current); 519 } catch (Exception e) { 520 log.error("{} got exception during firePropertyChange({},{}) in thread {} {}", 521 getDisplayName(), old, _current, 522 Thread.currentThread().getName(), Thread.currentThread().getId(), e); 523 } 524 } 525 526 /** 527 * Set the value retained by this Block. 528 * Also used when the Block itself gathers a value from an adjacent Block. 529 * This can be overridden in a subclass if 530 * e.g. you want to keep track of Blocks elsewhere, 531 * but make sure you also eventually invoke the super.setValue() here. 532 * Fires propertyChange "value" when changed. 533 * 534 * @param value The new Object resident in this block, or null if none 535 */ 536 public void setValue(Object value) { 537 //ignore if unchanged 538 if (value != _value) { 539 log.debug("Block {} value changed from '{}' to '{}'", getDisplayName(), _value, value); 540 _previousValue = _value; 541 _value = value; 542 firePropertyChange(PROPERTY_VALUE, _previousValue, _value); // NOI18N 543 } 544 } 545 546 /** 547 * Get the Block Contents Value. 548 * @return object with current value, could be null. 549 */ 550 @CheckForNull 551 public Object getValue() { 552 return _value; 553 } 554 555 /** 556 * Set Block Direction of Travel. 557 * Fires propertyChange "direction" when changed. 558 * @param direction Path Constant form, see {@link Path Path.java} 559 */ 560 public void setDirection(int direction) { 561 //ignore if unchanged 562 if (direction != _direction) { 563 log.debug("Block {} direction changed from {} to {}", getDisplayName(), 564 Path.decodeDirection(_direction), Path.decodeDirection(direction)); 565 int oldDirection = _direction; 566 _direction = direction; 567 // this is a bound parameter 568 firePropertyChange(PROPERTY_DIRECTION, oldDirection, direction); // NOI18N 569 } 570 } 571 572 /** 573 * Get Block Direction of Travel. 574 * @return direction in Path Constant form, see {@link Path Path.java} 575 */ 576 public int getDirection() { 577 return _direction; 578 } 579 580 //Deny traffic entering from this block 581 private final ArrayList<NamedBeanHandle<Block>> blockDenyList = new ArrayList<>(1); 582 583 /** 584 * Add to the Block Deny List. 585 * 586 * The block deny list, is used by higher level code, to determine if 587 * traffic/trains should be allowed to enter from an attached block, the 588 * list only deals with blocks that access should be denied from. 589 * <p> 590 * If we want to prevent traffic from following from this Block to another, 591 * then this Block must be added to the deny list of the other Block. 592 * By default no Block is barred, so traffic flow is bi-directional. 593 * @param pName name of the block to add, which must exist 594 */ 595 public void addBlockDenyList(@Nonnull String pName) { 596 Block blk = InstanceManager.getDefault(BlockManager.class).getBlock(pName); 597 if (blk == null) { 598 throw new IllegalArgumentException("addBlockDenyList requests block \"" + pName + "\" exists"); 599 } 600 NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault( 601 NamedBeanHandleManager.class).getNamedBeanHandle(pName, blk); 602 if (!blockDenyList.contains(namedBlock)) { 603 blockDenyList.add(namedBlock); 604 } 605 } 606 607 public void addBlockDenyList(@Nonnull Block blk) { 608 NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault( 609 NamedBeanHandleManager.class).getNamedBeanHandle(blk.getDisplayName(), blk); 610 if (!blockDenyList.contains(namedBlock)) { 611 blockDenyList.add(namedBlock); 612 } 613 } 614 615 public void removeBlockDenyList(String blk) { 616 NamedBeanHandle<Block> toremove = null; 617 for (NamedBeanHandle<Block> bean : blockDenyList) { 618 if (bean.getName().equals(blk)) { 619 toremove = bean; 620 } 621 } 622 if (toremove != null) { 623 blockDenyList.remove(toremove); 624 } 625 } 626 627 public void removeBlockDenyList(Block blk) { 628 NamedBeanHandle<Block> toremove = null; 629 for (NamedBeanHandle<Block> bean : blockDenyList) { 630 if (bean.getBean() == blk) { 631 toremove = bean; 632 } 633 } 634 if (toremove != null) { 635 blockDenyList.remove(toremove); 636 } 637 } 638 639 public List<String> getDeniedBlocks() { 640 List<String> list = new ArrayList<>(blockDenyList.size()); 641 blockDenyList.forEach( bean -> list.add(bean.getName()) ); 642 return list; 643 } 644 645 public boolean isBlockDenied(String deny) { 646 return blockDenyList.stream().anyMatch( bean -> bean.getName().equals(deny)); 647 } 648 649 public boolean isBlockDenied(Block deny) { 650 return blockDenyList.stream().anyMatch( bean -> bean.getBean() == deny); 651 } 652 653 /** 654 * Get if Block can have permissive working. 655 * Blocks default to non-permissive, i.e. false. 656 * @return true if permissive, else false. 657 */ 658 public boolean getPermissiveWorking() { 659 return _permissiveWorking; 660 } 661 662 /** 663 * Set Block as permissive. 664 * Fires propertyChange "BlockPermissiveWorking" when changed. 665 * @param w true permissive, false NOT permissive 666 */ 667 public void setPermissiveWorking(boolean w) { 668 if (_permissiveWorking != w) { 669 _permissiveWorking = w; 670 firePropertyChange(BLOCK_PERMISSIVE_CHANGE, !w, w); // NOI18N 671 } 672 } 673 674 private boolean _permissiveWorking = false; 675 676 /** 677 * Get if Block is a ghost. 678 * Blocks default to non-ghost, i.e. false. 679 * @return true if ghost, else false. 680 */ 681 public boolean getIsGhost() { 682 return _ghost; 683 } 684 685 /** 686 * Set if the block is a ghost 687 * Fires propertyChange "BlockGhost" when changed. 688 * @param w true ghost, false NOT ghost 689 */ 690 public void setIsGhost(boolean w) { 691 if (_ghost != w) { 692 _ghost = w; 693 firePropertyChange(GHOST_CHANGE, !w, w); // NOI18N 694 } 695 } 696 697 private boolean _ghost = false; 698 699 public float getSpeedLimit() { 700 if ((_blockSpeed == null) || (_blockSpeed.isEmpty())) { 701 return -1; 702 } 703 String speed = _blockSpeed; 704 if ( "Global".equals( _blockSpeed)) { 705 speed = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed(); 706 } 707 708 try { 709 return Float.parseFloat(speed); 710 } catch (NumberFormatException nx) { 711 //considered normal if the speed is not a number. 712 } 713 try { 714 return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 715 } catch (IllegalArgumentException ex) { 716 return -1; 717 } 718 } 719 720 private String _blockSpeed = ""; 721 722 public String getBlockSpeed() { 723 if ( "Global".equals( _blockSpeed)) { 724 return (Bundle.getMessage("UseGlobal", "Global") + " " 725 + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); 726 // Ensure the word "Global" is always in the speed name for later comparison 727 } 728 return _blockSpeed; 729 } 730 731 /** 732 * Set the Block Speed Name. 733 * <p> 734 * Does not perform name validity checking. 735 * Does not send Property Change Event. 736 * @param s new Speed Name String. 737 */ 738 public void setBlockSpeedName(String s) { 739 if (s == null) { 740 _blockSpeed = ""; 741 } else { 742 _blockSpeed = s; 743 } 744 } 745 746 /** 747 * Set the Block Speed, preferred method. 748 * <p> 749 * Fires propertyChange "BlockSpeedChange" when changed. 750 * @param s Speed String 751 * @throws JmriException if Value of requested block speed is not valid. 752 */ 753 public void setBlockSpeed(final String s) throws JmriException { 754 if ((s == null) || (_blockSpeed.equals(s))) { 755 return; 756 } 757 String newSpeed = s; 758 if (s.contains("Global")) { 759 newSpeed = "Global"; 760 } else { 761 try { 762 Float.valueOf(s); 763 } catch (NumberFormatException nx) { 764 try { 765 InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s); 766 } catch (IllegalArgumentException ex) { 767 throw new JmriException("Block \"" + getDisplayName() 768 + "\" requested speed value \"" + s + "\" invalid."); 769 } 770 } 771 } 772 String oldSpeed = _blockSpeed; 773 _blockSpeed = newSpeed; 774 firePropertyChange(BLOCK_SPEED_CHANGE, oldSpeed, s); 775 } 776 777 /** 778 * Set Block Curvature Constant. 779 * Valid values : 780 * Block.NONE, Block.GRADUAL, Block.TIGHT, Block.SEVERE 781 * Fires propertyChange "BlockCurvatureChange" when changed. 782 * @param c Constant, e.g. Block.GRADUAL 783 */ 784 public void setCurvature(int c) { 785 if (_curvature!=c) { 786 int oldCurve = _curvature; 787 _curvature = c; 788 firePropertyChange(BLOCK_CURVATURE_CHANGE, oldCurve, c); 789 } 790 } 791 792 /** 793 * Get Block Curvature Constant. 794 * Defaults to Block.NONE 795 * @return constant, e.g. Block.TIGHT 796 */ 797 public int getCurvature() { 798 return _curvature; 799 } 800 801 /** 802 * Set length in millimeters. 803 * Paths will inherit this length, if their length is not specifically set. 804 * This length is the maximum length of any Path in the block. 805 * Path lengths exceeding this will be set to the default length. 806 * <p> 807 * Fires propertyChange "BlockLengthChange" when changed, float values in mm. 808 * @param l length in millimeters 809 */ 810 public void setLength(float l) { 811 float oldLen = getLengthMm(); 812 if (Math.abs(oldLen - l) > 0.0001){ // length value is different 813 _length = l; 814 getPaths().stream().forEach(p -> { 815 if (p.getLength() > l) { 816 p.setLength(0); // set to default 817 } 818 }); 819 firePropertyChange(BLOCK_LENGTH_CHANGE, oldLen, l); 820 } 821 } 822 823 /** 824 * Get Block Length in Millimetres. 825 * Default 0.0f. 826 * @return length in mm. 827 */ 828 public float getLengthMm() { 829 return _length; 830 } 831 832 /** 833 * Get Block Length in Centimetres. 834 * Courtesy method using result from getLengthMm. 835 * @return length in centimetres. 836 */ 837 public float getLengthCm() { 838 return (_length / 10.0f); 839 } 840 841 /** 842 * Get Block Length in Inches. 843 * Courtesy method using result from getLengthMm. 844 * @return length in inches. 845 */ 846 public float getLengthIn() { 847 return (_length / 25.4f); 848 } 849 850 /** 851 * Note: this has to make choices about identity values (always the same) 852 * and operation values (can change as the block works). Might be missing 853 * some identity values. 854 */ 855 @Override 856 public boolean equals(Object obj) { 857 if (obj == this) { 858 return true; 859 } 860 if (obj == null) { 861 return false; 862 } 863 864 if ( getClass() != obj.getClass() ) { 865 return false; 866 } else { 867 Block b = (Block) obj; 868 return b.getSystemName().equals(this.getSystemName()); 869 } 870 } 871 872 @Override 873 // This can't change, so can't include mutable values 874 public int hashCode() { 875 return this.getSystemName().hashCode(); 876 } 877 878 // internal data members 879 private int _current = UNDETECTED; // state until sensor is set 880 private NamedBeanHandle<Sensor> _namedSensor = null; 881 private PropertyChangeListener _sensorListener = null; 882 private Object _value; 883 private Object _previousValue; 884 private int _direction; 885 private int _curvature = NONE; 886 private float _length = 0.0f; // always stored in millimeters 887 private Reporter _reporter = null; 888 private PropertyChangeListener _reporterListener = null; 889 private boolean _reportingCurrent = false; 890 891 private Path[] pListOfPossibleEntrancePaths = null; 892 private int cntOfPossibleEntrancePaths = 0; 893 894 void resetCandidateEntrancePaths() { 895 pListOfPossibleEntrancePaths = null; 896 cntOfPossibleEntrancePaths = 0; 897 } 898 899 boolean setAsEntryBlockIfPossible(Block b) { 900 for (int i = 0; i < cntOfPossibleEntrancePaths; i++) { 901 Block candidateBlock = pListOfPossibleEntrancePaths[i].getBlock(); 902 if (candidateBlock == b) { 903 setValue(candidateBlock.getValue()); 904 setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection()); 905 log.info("Block {} gets LATE new value from {}, direction= {}", 906 getDisplayName(), candidateBlock.getDisplayName(), Path.decodeDirection(getDirection())); 907 resetCandidateEntrancePaths(); 908 return true; 909 } 910 } 911 return false; 912 } 913 914 /** 915 * Handle change in sensor state. 916 * <p> 917 * Defers real work to goingActive, goingInactive methods. 918 * 919 * @param e the event 920 */ 921 void handleSensorChange(PropertyChangeEvent e) { 922 Sensor s = getSensor(); 923 if ( Sensor.PROPERTY_KNOWN_STATE.equals( e.getPropertyName()) && s != null ) { 924 int state = s.getState(); 925 switch (state) { 926 case Sensor.ACTIVE: 927 goingActive(); 928 break; 929 case Sensor.INACTIVE: 930 goingInactive(); 931 break; 932 case Sensor.UNKNOWN: 933 goingUnknown(); 934 break; 935 default: 936 goingInconsistent(); 937 break; 938 } 939 } 940 } 941 942 public void goingUnknown() { 943 setValue(null); 944 setState(UNKNOWN); 945 } 946 947 public void goingInconsistent() { 948 setValue(null); 949 setState(INCONSISTENT); 950 } 951 952 /** 953 * Handle change in Reporter value. 954 * 955 * @param e PropertyChangeEvent 956 */ 957 void handleReporterChange(PropertyChangeEvent e) { 958 if ((_reportingCurrent && Reporter.PROPERTY_CURRENT_REPORT.equals(e.getPropertyName())) 959 || (!_reportingCurrent && Reporter.PROPERTY_LAST_REPORT.equals(e.getPropertyName()))) { 960 setValue(e.getNewValue()); 961 } 962 } 963 964 private Instant _timeLastInactive; 965 966 /** 967 * Handles Block sensor going INACTIVE: this block is empty 968 */ 969 public void goingInactive() { 970 log.debug("Block {} goes UNOCCUPIED", getDisplayName()); 971 for (Path path : paths) { 972 Block b = path.getBlock(); 973 if (b != null) { 974 b.setAsEntryBlockIfPossible(this); 975 } 976 } 977 setValue(null); 978 setDirection(Path.NONE); 979 setState(UNOCCUPIED); 980 _timeLastInactive = Instant.now(); 981 } 982 983 private static final int MAXINFOMESSAGES = 5; 984 private int infoMessageCount = 0; 985 986 /** 987 * Handles Block sensor going ACTIVE: this block is now occupied, figure out 988 * from who and copy their value. 989 */ 990 public void goingActive() { 991 if (getState() == OCCUPIED) { 992 return; 993 } 994 log.debug("Block {} goes OCCUPIED", getDisplayName()); 995 resetCandidateEntrancePaths(); 996 // index through the paths, counting 997 int count = 0; 998 Path next = null; 999 // get statuses of everything once 1000 int currPathCnt = paths.size(); 1001 Path[] pList = new Path[currPathCnt]; 1002 boolean[] isSet = new boolean[currPathCnt]; 1003 boolean[] isActive = new boolean[currPathCnt]; 1004 int[] pDir = new int[currPathCnt]; 1005 int[] pFromDir = new int[currPathCnt]; 1006 for (int i = 0; i < currPathCnt; i++) { 1007 pList[i] = paths.get(i); 1008 isSet[i] = pList[i].checkPathSet(); 1009 Block b = pList[i].getBlock(); 1010 if (b != null) { 1011 isActive[i] = b.getState() == OCCUPIED; 1012 pDir[i] = b.getDirection(); 1013 } else { 1014 isActive[i] = false; 1015 pDir[i] = -1; 1016 } 1017 pFromDir[i] = pList[i].getFromBlockDirection(); 1018 if (isSet[i] && isActive[i]) { 1019 count++; 1020 next = pList[i]; 1021 } 1022 } 1023 // sort on number of neighbors 1024 switch (count) { 1025 case 0: 1026 if (null != _previousValue) { 1027 // restore the previous value under either of these circumstances: 1028 // 1. the block has been 'unoccupied' only very briefly 1029 // 2. power has just come back on 1030 Instant tn = Instant.now(); 1031 BlockManager bm = InstanceManager.getDefault(BlockManager.class); 1032 if ( bm.timeSinceLastLayoutPowerOn() < 5000 || 1033 (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) { 1034 setValue(_previousValue); 1035 if (infoMessageCount < MAXINFOMESSAGES) { 1036 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}." 1037 +" Restoring previous value.", getDisplayName()); 1038 infoMessageCount++; 1039 } 1040 } else if (log.isDebugEnabled()) { 1041 if (null != _timeLastInactive) { 1042 log.debug("not restoring previous value, block {} has been inactive for too long ({}ms)" 1043 + " and layout power has not just been restored ({}ms ago)", 1044 getDisplayName(), tn.toEpochMilli() - _timeLastInactive.toEpochMilli(), 1045 bm.timeSinceLastLayoutPowerOn()); 1046 } else { 1047 log.debug("not restoring previous value, block {} has been inactive since the " 1048 + "start of this session and layout power has not just been restored ({}ms ago)", 1049 getDisplayName(), bm.timeSinceLastLayoutPowerOn()); 1050 } 1051 } 1052 } else { 1053 if (infoMessageCount < MAXINFOMESSAGES) { 1054 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.", 1055 getDisplayName()); 1056 infoMessageCount++; 1057 } 1058 } 1059 break; 1060 case 1: 1061 // simple case 1062 if ((next != null) && (next.getBlock() != null)) { 1063 // normal case, transfer value object 1064 setValue(next.getBlock().getValue()); 1065 setDirection(next.getFromBlockDirection()); 1066 log.debug("Block {} gets new value '{}' from {}, direction={}", 1067 getDisplayName(), 1068 next.getBlock().getValue(), 1069 next.getBlock().getDisplayName(), 1070 Path.decodeDirection(getDirection())); 1071 } else if (next == null) { 1072 log.error("unexpected next==null processing block {}", getDisplayName()); 1073 } else { 1074 log.error("unexpected next.getBlock()=null processing block {}", getDisplayName()); 1075 } 1076 break; 1077 default: 1078 // count > 1, check for one with proper direction 1079 // this time, count ones with proper direction 1080 log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count); 1081 next = null; 1082 count = 0; 1083 // true until it's found that some neighbor blocks contain different contents (trains) 1084 boolean allNeighborsAgree = true; 1085 1086 // scan for neighbors without matching direction 1087 for (int i = 0; i < currPathCnt; i++) { 1088 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1089 log.debug("comparing {} ({}) to {} ({})", 1090 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1091 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1092 //use bitwise comparison to support combination directions such as "North, West" 1093 if ((pDir[i] & pFromDir[i]) > 0) { 1094 if (next != null && next.getBlock() != null ) { 1095 Object value = next.getBlock().getValue(); 1096 if ( value != null && !value.equals(pList[i].getBlock().getValue())) { 1097 allNeighborsAgree = false; 1098 } 1099 } 1100 count++; 1101 next = pList[i]; 1102 } 1103 } 1104 } 1105 1106 // If loop above didn't find neighbors with matching direction, scan w/out direction for neighbors 1107 // This is used when directions are not being used 1108 if (next == null) { 1109 for (int i = 0; i < currPathCnt; i++) { 1110 if (isSet[i] && isActive[i]) { 1111 if (next != null && next.getBlock() != null ) { 1112 Object value = next.getBlock().getValue(); 1113 if ( value != null && ! value.equals(pList[i].getBlock().getValue())) { 1114 allNeighborsAgree = false; 1115 } 1116 } 1117 count++; 1118 next = pList[i]; 1119 } 1120 } 1121 } 1122 1123 if (next != null && count == 1) { 1124 // found one block with proper direction, use it 1125 setValue(next.getBlock().getValue()); 1126 setDirection(next.getFromBlockDirection()); 1127 log.debug("Block {} gets new value '{}' from {}, direction {}", 1128 getDisplayName(), next.getBlock().getValue(), 1129 next.getBlock().getDisplayName(), Path.decodeDirection(getDirection())); 1130 } else { 1131 // handle merging trains: All neighbors with same content (train ID) 1132 if (allNeighborsAgree && next != null) { 1133 setValue(next.getBlock().getValue()); 1134 setDirection(next.getFromBlockDirection()); 1135 } else { 1136 // don't all agree, so can't determine unique value 1137 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for" 1138 + " block {} but maybe it can be determined when another block becomes free", 1139 count, getDisplayName()); 1140 pListOfPossibleEntrancePaths = new Path[currPathCnt]; 1141 cntOfPossibleEntrancePaths = 0; 1142 for (int i = 0; i < currPathCnt; i++) { 1143 if (isSet[i] && isActive[i]) { 1144 pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i]; 1145 cntOfPossibleEntrancePaths++; 1146 } 1147 } 1148 } 1149 } 1150 break; 1151 } 1152 setState(OCCUPIED); 1153 } 1154 1155 /** 1156 * Find which path this Block became Active, without actually modifying the 1157 * state of this block. 1158 * <p> 1159 * (this is largely a copy of the 'Search' part of the logic from 1160 * goingActive()) 1161 * 1162 * @return the next path 1163 */ 1164 @CheckForNull 1165 public Path findFromPath() { 1166 // index through the paths, counting 1167 int count = 0; 1168 Path next = null; 1169 // get statuses of everything once 1170 int currPathCnt = paths.size(); 1171 Path[] pList = new Path[currPathCnt]; 1172 boolean[] isSet = new boolean[currPathCnt]; 1173 boolean[] isActive = new boolean[currPathCnt]; 1174 int[] pDir = new int[currPathCnt]; 1175 int[] pFromDir = new int[currPathCnt]; 1176 for (int i = 0; i < currPathCnt; i++) { 1177 pList[i] = paths.get(i); 1178 isSet[i] = pList[i].checkPathSet(); 1179 Block b = pList[i].getBlock(); 1180 if (b != null) { 1181 isActive[i] = b.getState() == OCCUPIED; 1182 pDir[i] = b.getDirection(); 1183 } else { 1184 isActive[i] = false; 1185 pDir[i] = -1; 1186 } 1187 pFromDir[i] = pList[i].getFromBlockDirection(); 1188 if (isSet[i] && isActive[i]) { 1189 count++; 1190 next = pList[i]; 1191 } 1192 } 1193 // sort on number of neighbors 1194 if ((count == 0) || (count == 1)) { 1195 // do nothing. OK to return null from this function. "next" is already set. 1196 } else { 1197 // count > 1, check for one with proper direction 1198 // this time, count ones with proper direction 1199 log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count); 1200 next = null; 1201 count = 0; 1202 for (int i = 0; i < currPathCnt; i++) { 1203 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1204 log.debug("comparing {} ({}) to {} ({})", 1205 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1206 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1207 // Use bitwise comparison to support combination directions such as "North, West" 1208 if ((pDir[i] & pFromDir[i]) > 0) { 1209 count++; 1210 next = pList[i]; 1211 } 1212 } 1213 } 1214 if (next == null) { 1215 log.debug("next is null!"); 1216 } 1217 if (next != null && count == 1) { 1218 // found one block with proper direction, assume that 1219 } else { 1220 // no unique path with correct direction - this happens frequently from noise in block detectors!! 1221 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}", 1222 count, getDisplayName()); 1223 } 1224 } 1225 // in any case, go OCCUPIED 1226 if (log.isDebugEnabled()) { // avoid potentially expensive non-logging 1227 log.debug("Block {} with direction {} gets new value from {} + (informational. No state change)", 1228 getDisplayName(), Path.decodeDirection(getDirection()), 1229 (next != null ? next.getBlock().getDisplayName() : "(no next block)")); 1230 } 1231 return next; 1232 } 1233 1234 /** 1235 * This allows the layout block to inform any listeners to the block 1236 * that the higher level layout block has been set to "useExtraColor" which is an 1237 * indication that it has been allocated to a section by the AutoDispatcher. 1238 * The value set is not retained in any form by the block, 1239 * it is purely to trigger a propertyChangeEvent. 1240 * @param boo Allocation status 1241 */ 1242 public void setAllocated(Boolean boo) { 1243 firePropertyChange(PROPERTY_ALLOCATED, !boo, boo); 1244 } 1245 1246 // Methods to implmement PhysicalLocationReporter Interface 1247 // 1248 // If we have a Reporter that is also a PhysicalLocationReporter, 1249 // we will defer to that Reporter's methods. 1250 // Else we will assume a LocoNet style message to be parsed. 1251 1252 /** 1253 * Parse a given string and return the LocoAddress value that is presumed 1254 * stored within it based on this object's protocol. The Class Block 1255 * implementation defers to its associated Reporter, if it exists. 1256 * 1257 * @param rep String to be parsed 1258 * @return LocoAddress address parsed from string, or null if this Block 1259 * isn't associated with a Reporter, or is associated with a 1260 * Reporter that is not also a PhysicalLocationReporter 1261 */ 1262 @Override 1263 public LocoAddress getLocoAddress(String rep) { 1264 // Defer parsing to our associated Reporter if we can. 1265 if (rep == null) { 1266 log.warn("String input is null!"); 1267 return null; 1268 } 1269 Reporter testReporter = this.getReporter(); 1270 if ( testReporter instanceof PhysicalLocationReporter ) { 1271 return ((PhysicalLocationReporter)testReporter).getLocoAddress(rep); 1272 } else { 1273 // Assume a LocoNet-style report. This is (nascent) support for handling of Faller cars 1274 // for Dave Merrill's project. 1275 log.debug("report string: {}", rep); 1276 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1277 // Match a number followed by the word "enter". This is the LocoNet pattern. 1278 Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); 1279 Matcher m = lnp.matcher(rep); 1280 if (m.find()) { 1281 log.debug("Parsed address: {}", m.group(1)); 1282 return new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC); 1283 } else { 1284 return null; 1285 } 1286 } 1287 } 1288 1289 /** 1290 * Parses out a (possibly old) LnReporter-generated report string to extract 1291 * the direction from within it based on this object's protocol. The Class 1292 * Block implementation defers to its associated Reporter, if it exists. 1293 * 1294 * @param rep String to be parsed 1295 * @return PhysicalLocationReporter.Direction direction parsed from string, 1296 * or null if this Block isn't associated with a Reporter, or is 1297 * associated with a Reporter that is not also a 1298 * PhysicalLocationReporter 1299 */ 1300 @Override 1301 public PhysicalLocationReporter.Direction getDirection(String rep) { 1302 if (rep == null) { 1303 log.warn("String input is null!"); 1304 return (null); 1305 } 1306 // Defer parsing to our associated Reporter if we can. 1307 Reporter testReporter = this.getReporter(); 1308 if ( testReporter instanceof PhysicalLocationReporter ) { 1309 return ((PhysicalLocationReporter)testReporter).getDirection(rep); 1310 } else { 1311 log.debug("report string: {}", rep); 1312 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1313 // Match a number followed by the word "enter". This is the LocoNet pattern. 1314 Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); 1315 Matcher m = lnp.matcher(rep); 1316 if (m.find()) { 1317 log.debug("Parsed direction: {}", m.group(2)); 1318 switch (m.group(2)) { 1319 case "enter": 1320 // LocoNet Enter message 1321 return PhysicalLocationReporter.Direction.ENTER; 1322 case "seen": 1323 // Lissy message. Treat them all as "entry" messages. 1324 return PhysicalLocationReporter.Direction.ENTER; 1325 default: 1326 return PhysicalLocationReporter.Direction.EXIT; 1327 } 1328 } else { 1329 return PhysicalLocationReporter.Direction.UNKNOWN; 1330 } 1331 } 1332 } 1333 1334 /** 1335 * Return this Block's physical location, if it exists. 1336 * Defers actual work to the helper methods in class PhysicalLocation. 1337 * 1338 * @return PhysicalLocation : this Block's location. 1339 */ 1340 @Override 1341 public PhysicalLocation getPhysicalLocation() { 1342 // We have our won PhysicalLocation. That's the point. No need to defer to the Reporter. 1343 return PhysicalLocation.getBeanPhysicalLocation(this); 1344 } 1345 1346 /** 1347 * Return this Block's physical location, if it exists. 1348 * Does not use the parameter s. 1349 * Defers actual work to the helper methods in class PhysicalLocation 1350 * 1351 * @param s (this parameter is ignored) 1352 * @return PhysicalLocation : this Block's location. 1353 */ 1354 @Override 1355 public PhysicalLocation getPhysicalLocation(String s) { 1356 // We have our won PhysicalLocation. That's the point. No need to defer to the Reporter. 1357 // Intentionally ignore the String s 1358 return PhysicalLocation.getBeanPhysicalLocation(this); 1359 } 1360 1361 @Override 1362 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 1363 if (Manager.PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) { 1364 if (evt.getOldValue() instanceof Sensor 1365 && evt.getOldValue().equals(getSensor())) { 1366 throw new PropertyVetoException(getDisplayName(), evt); 1367 } 1368 if (evt.getOldValue() instanceof Reporter 1369 && evt.getOldValue().equals(getReporter())) { 1370 throw new PropertyVetoException(getDisplayName(), evt); 1371 } 1372 } else if (Manager.PROPERTY_DO_DELETE.equals(evt.getPropertyName())) { 1373 if (evt.getOldValue() instanceof Sensor 1374 && evt.getOldValue().equals(getSensor())) { 1375 setSensor(null); 1376 } 1377 if (evt.getOldValue() instanceof Reporter 1378 && evt.getOldValue().equals(getReporter())) { 1379 setReporter(null); 1380 } 1381 } 1382 } 1383 1384 @Override 1385 public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) { 1386 List<NamedBeanUsageReport> report = new ArrayList<>(); 1387 if (bean != null) { 1388 if (bean.equals(getSensor())) { 1389 report.add(new NamedBeanUsageReport("BlockSensor")); // NOI18N 1390 } 1391 if (bean.equals(getReporter())) { 1392 report.add(new NamedBeanUsageReport("BlockReporter")); // NOI18N 1393 } 1394 // Block paths 1395 getPaths().forEach( path -> { 1396 if (bean.equals(path.getBlock())) { 1397 report.add(new NamedBeanUsageReport("BlockPathNeighbor")); // NOI18N 1398 } 1399 path.getSettings().forEach( setting -> { 1400 if (bean.equals(setting.getBean())) { 1401 report.add(new NamedBeanUsageReport("BlockPathTurnout")); // NOI18N 1402 } 1403 }); 1404 }); 1405 } 1406 return report; 1407 } 1408 1409 @Override 1410 public String getBeanType() { 1411 return Bundle.getMessage("BeanNameBlock"); 1412 } 1413 1414 /** {@inheritDoc} */ 1415 @Override 1416 @Nonnull 1417 public String describeState(int state) { 1418 switch (state) { 1419 case Block.OCCUPIED: 1420 return Bundle.getMessage("BlockOccupied"); 1421 case Block.UNOCCUPIED: 1422 return Bundle.getMessage("BlockUnOccupied"); 1423 case Block.UNDETECTED: 1424 return Bundle.getMessage("BlockUndetected"); 1425 default: // state unknown, state inconsistent, state unexpected 1426 return super.describeState(state); 1427 } 1428 } 1429 1430 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Block.class); 1431}