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, 2026 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 /** 900 * Take the value of a neighbour Block that has just been emptied, if this 901 * Block could not decide between its occupied neighbours when it became 902 * occupied and that neighbour was one of them. The value is only taken 903 * while this Block is still occupied. 904 * 905 * @param b the neighbour Block that has become unoccupied 906 * @return true if the value of b was taken 907 */ 908 boolean setAsEntryBlockIfPossible(Block b) { 909 if (getState() != OCCUPIED) { 910 // this Block has been emptied since, the train that entered it has gone 911 resetCandidateEntrancePaths(); 912 return false; 913 } 914 for (int i = 0; i < cntOfPossibleEntrancePaths; i++) { 915 Block candidateBlock = pListOfPossibleEntrancePaths[i].getBlock(); 916 if (candidateBlock == b) { 917 setValue(candidateBlock.getValue()); 918 setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection()); 919 log.info("Block {} gets LATE new value from {}, direction= {}", 920 getDisplayName(), candidateBlock.getDisplayName(), Path.decodeDirection(getDirection())); 921 resetCandidateEntrancePaths(); 922 return true; 923 } 924 } 925 return false; 926 } 927 928 /** 929 * Handle change in sensor state. 930 * <p> 931 * Defers real work to goingActive, goingInactive methods. 932 * 933 * @param e the event 934 */ 935 void handleSensorChange(PropertyChangeEvent e) { 936 Sensor s = getSensor(); 937 if ( Sensor.PROPERTY_KNOWN_STATE.equals( e.getPropertyName()) && s != null ) { 938 int state = s.getState(); 939 switch (state) { 940 case Sensor.ACTIVE: 941 goingActive(); 942 break; 943 case Sensor.INACTIVE: 944 goingInactive(); 945 break; 946 case Sensor.UNKNOWN: 947 goingUnknown(); 948 break; 949 default: 950 goingInconsistent(); 951 break; 952 } 953 } 954 } 955 956 /** 957 * Handles Block sensor going UNKNOWN: the value is cleared. 958 * <p> 959 * The time is recorded as for a Block going unoccupied, so that the value 960 * is restored if the sensor goes active again within a short time, for 961 * example after a brief loss of the connection. 962 */ 963 public void goingUnknown() { 964 setValue(null); 965 setState(UNKNOWN); 966 _timeLastInactive = Instant.now(); 967 } 968 969 /** 970 * Handles Block sensor going INCONSISTENT: the value is cleared. 971 * <p> 972 * The time is recorded as for a Block going unoccupied, so that the value 973 * is restored if the sensor goes active again within a short time. 974 */ 975 public void goingInconsistent() { 976 setValue(null); 977 setState(INCONSISTENT); 978 _timeLastInactive = Instant.now(); 979 } 980 981 /** 982 * Handle change in Reporter value. 983 * 984 * @param e PropertyChangeEvent 985 */ 986 void handleReporterChange(PropertyChangeEvent e) { 987 if ((_reportingCurrent && Reporter.PROPERTY_CURRENT_REPORT.equals(e.getPropertyName())) 988 || (!_reportingCurrent && Reporter.PROPERTY_LAST_REPORT.equals(e.getPropertyName()))) { 989 setValue(e.getNewValue()); 990 } 991 } 992 993 private Instant _timeLastInactive; 994 995 /** 996 * Handles Block sensor going INACTIVE: this block is empty 997 */ 998 public void goingInactive() { 999 log.debug("Block {} goes UNOCCUPIED", getDisplayName()); 1000 for (Path path : paths) { 1001 Block b = path.getBlock(); 1002 if (b != null) { 1003 b.setAsEntryBlockIfPossible(this); 1004 } 1005 } 1006 // the train that could not be identified when it entered has left 1007 resetCandidateEntrancePaths(); 1008 setValue(null); 1009 setDirection(Path.NONE); 1010 setState(UNOCCUPIED); 1011 _timeLastInactive = Instant.now(); 1012 } 1013 1014 private static final int MAXINFOMESSAGES = 5; 1015 private int infoMessageCount = 0; 1016 1017 /** 1018 * Handles Block sensor going ACTIVE: this block is now occupied, figure out 1019 * from who and copy their value. 1020 */ 1021 public void goingActive() { 1022 if (getState() == OCCUPIED) { 1023 return; 1024 } 1025 log.debug("Block {} goes OCCUPIED", getDisplayName()); 1026 resetCandidateEntrancePaths(); 1027 // index through the paths, counting 1028 int count = 0; 1029 Path next = null; 1030 // get statuses of everything once 1031 int currPathCnt = paths.size(); 1032 Path[] pList = new Path[currPathCnt]; 1033 boolean[] isSet = new boolean[currPathCnt]; 1034 boolean[] isActive = new boolean[currPathCnt]; 1035 int[] pDir = new int[currPathCnt]; 1036 int[] pFromDir = new int[currPathCnt]; 1037 for (int i = 0; i < currPathCnt; i++) { 1038 pList[i] = paths.get(i); 1039 isSet[i] = pList[i].checkPathSet(); 1040 Block b = pList[i].getBlock(); 1041 if (b != null) { 1042 isActive[i] = b.getState() == OCCUPIED; 1043 pDir[i] = b.getDirection(); 1044 } else { 1045 isActive[i] = false; 1046 pDir[i] = -1; 1047 } 1048 pFromDir[i] = pList[i].getFromBlockDirection(); 1049 if (isSet[i] && isActive[i]) { 1050 count++; 1051 next = pList[i]; 1052 } 1053 } 1054 // sort on number of neighbors 1055 switch (count) { 1056 case 0: 1057 if (null != _previousValue) { 1058 // restore the previous value under either of these circumstances: 1059 // 1. the block has been 'unoccupied' only very briefly 1060 // 2. power has just come back on 1061 Instant tn = Instant.now(); 1062 BlockManager bm = InstanceManager.getDefault(BlockManager.class); 1063 if ( bm.timeSinceLastLayoutPowerOn() < 5000 || 1064 (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) { 1065 setValue(_previousValue); 1066 if (infoMessageCount < MAXINFOMESSAGES) { 1067 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}." 1068 +" Restoring previous value.", getDisplayName()); 1069 infoMessageCount++; 1070 } 1071 } else if (log.isDebugEnabled()) { 1072 if (null != _timeLastInactive) { 1073 log.debug("not restoring previous value, block {} has been inactive for too long ({}ms)" 1074 + " and layout power has not just been restored ({}ms ago)", 1075 getDisplayName(), tn.toEpochMilli() - _timeLastInactive.toEpochMilli(), 1076 bm.timeSinceLastLayoutPowerOn()); 1077 } else { 1078 log.debug("not restoring previous value, block {} has been inactive since the " 1079 + "start of this session and layout power has not just been restored ({}ms ago)", 1080 getDisplayName(), bm.timeSinceLastLayoutPowerOn()); 1081 } 1082 } 1083 } else { 1084 if (infoMessageCount < MAXINFOMESSAGES) { 1085 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.", 1086 getDisplayName()); 1087 infoMessageCount++; 1088 } 1089 } 1090 break; 1091 case 1: 1092 // simple case 1093 if ((next != null) && (next.getBlock() != null)) { 1094 // normal case, transfer value object 1095 setValue(next.getBlock().getValue()); 1096 setDirection(next.getFromBlockDirection()); 1097 log.debug("Block {} gets new value '{}' from {}, direction={}", 1098 getDisplayName(), 1099 next.getBlock().getValue(), 1100 next.getBlock().getDisplayName(), 1101 Path.decodeDirection(getDirection())); 1102 } else if (next == null) { 1103 log.error("unexpected next==null processing block {}", getDisplayName()); 1104 } else { 1105 log.error("unexpected next.getBlock()=null processing block {}", getDisplayName()); 1106 } 1107 break; 1108 default: 1109 // count > 1, check for one with proper direction 1110 // this time, count ones with proper direction 1111 log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count); 1112 next = null; 1113 count = 0; 1114 // true until it's found that some neighbor blocks contain different contents (trains) 1115 boolean allNeighborsAgree = true; 1116 1117 // scan for neighbors without matching direction 1118 for (int i = 0; i < currPathCnt; i++) { 1119 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1120 log.debug("comparing {} ({}) to {} ({})", 1121 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1122 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1123 //use bitwise comparison to support combination directions such as "North, West" 1124 if ((pDir[i] & pFromDir[i]) > 0) { 1125 if (next != null && next.getBlock() != null ) { 1126 Object value = next.getBlock().getValue(); 1127 if ( value != null && !value.equals(pList[i].getBlock().getValue())) { 1128 allNeighborsAgree = false; 1129 } 1130 } 1131 count++; 1132 next = pList[i]; 1133 } 1134 } 1135 } 1136 1137 // If loop above didn't find neighbors with matching direction, scan w/out direction for neighbors 1138 // This is used when directions are not being used 1139 if (next == null) { 1140 for (int i = 0; i < currPathCnt; i++) { 1141 if (isSet[i] && isActive[i]) { 1142 if (next != null && next.getBlock() != null ) { 1143 Object value = next.getBlock().getValue(); 1144 if ( value != null && ! value.equals(pList[i].getBlock().getValue())) { 1145 allNeighborsAgree = false; 1146 } 1147 } 1148 count++; 1149 next = pList[i]; 1150 } 1151 } 1152 } 1153 1154 if (next != null && count == 1) { 1155 // found one block with proper direction, use it 1156 setValue(next.getBlock().getValue()); 1157 setDirection(next.getFromBlockDirection()); 1158 log.debug("Block {} gets new value '{}' from {}, direction {}", 1159 getDisplayName(), next.getBlock().getValue(), 1160 next.getBlock().getDisplayName(), Path.decodeDirection(getDirection())); 1161 } else { 1162 // handle merging trains: All neighbors with same content (train ID) 1163 if (allNeighborsAgree && next != null) { 1164 setValue(next.getBlock().getValue()); 1165 setDirection(next.getFromBlockDirection()); 1166 } else { 1167 // don't all agree, so can't determine unique value 1168 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for" 1169 + " block {} but maybe it can be determined when another block becomes free", 1170 count, getDisplayName()); 1171 pListOfPossibleEntrancePaths = new Path[currPathCnt]; 1172 cntOfPossibleEntrancePaths = 0; 1173 for (int i = 0; i < currPathCnt; i++) { 1174 if (isSet[i] && isActive[i]) { 1175 pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i]; 1176 cntOfPossibleEntrancePaths++; 1177 } 1178 } 1179 } 1180 } 1181 break; 1182 } 1183 setState(OCCUPIED); 1184 } 1185 1186 /** 1187 * Find which path this Block became Active, without actually modifying the 1188 * state of this block. 1189 * <p> 1190 * (this is largely a copy of the 'Search' part of the logic from 1191 * goingActive()) 1192 * 1193 * @return the next path 1194 */ 1195 @CheckForNull 1196 public Path findFromPath() { 1197 // index through the paths, counting 1198 int count = 0; 1199 Path next = null; 1200 // get statuses of everything once 1201 int currPathCnt = paths.size(); 1202 Path[] pList = new Path[currPathCnt]; 1203 boolean[] isSet = new boolean[currPathCnt]; 1204 boolean[] isActive = new boolean[currPathCnt]; 1205 int[] pDir = new int[currPathCnt]; 1206 int[] pFromDir = new int[currPathCnt]; 1207 for (int i = 0; i < currPathCnt; i++) { 1208 pList[i] = paths.get(i); 1209 isSet[i] = pList[i].checkPathSet(); 1210 Block b = pList[i].getBlock(); 1211 if (b != null) { 1212 isActive[i] = b.getState() == OCCUPIED; 1213 pDir[i] = b.getDirection(); 1214 } else { 1215 isActive[i] = false; 1216 pDir[i] = -1; 1217 } 1218 pFromDir[i] = pList[i].getFromBlockDirection(); 1219 if (isSet[i] && isActive[i]) { 1220 count++; 1221 next = pList[i]; 1222 } 1223 } 1224 // sort on number of neighbors 1225 if ((count == 0) || (count == 1)) { 1226 // do nothing. OK to return null from this function. "next" is already set. 1227 } else { 1228 // count > 1, check for one with proper direction 1229 // this time, count ones with proper direction 1230 log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count); 1231 next = null; 1232 count = 0; 1233 for (int i = 0; i < currPathCnt; i++) { 1234 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1235 log.debug("comparing {} ({}) to {} ({})", 1236 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1237 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1238 // Use bitwise comparison to support combination directions such as "North, West" 1239 if ((pDir[i] & pFromDir[i]) > 0) { 1240 count++; 1241 next = pList[i]; 1242 } 1243 } 1244 } 1245 if (next == null) { 1246 log.debug("next is null!"); 1247 } 1248 if (next != null && count == 1) { 1249 // found one block with proper direction, assume that 1250 } else { 1251 // no unique path with correct direction - this happens frequently from noise in block detectors!! 1252 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}", 1253 count, getDisplayName()); 1254 } 1255 } 1256 // in any case, go OCCUPIED 1257 if (log.isDebugEnabled()) { // avoid potentially expensive non-logging 1258 log.debug("Block {} with direction {} gets new value from {} + (informational. No state change)", 1259 getDisplayName(), Path.decodeDirection(getDirection()), 1260 (next != null ? next.getBlock().getDisplayName() : "(no next block)")); 1261 } 1262 return next; 1263 } 1264 1265 /** 1266 * This allows the layout block to inform any listeners to the block 1267 * that the higher level layout block has been set to "useExtraColor" which is an 1268 * indication that it has been allocated to a section by the AutoDispatcher. 1269 * The value set is not retained in any form by the block, 1270 * it is purely to trigger a propertyChangeEvent. 1271 * @param boo Allocation status 1272 */ 1273 public void setAllocated(Boolean boo) { 1274 firePropertyChange(PROPERTY_ALLOCATED, !boo, boo); 1275 } 1276 1277 // Methods to implmement PhysicalLocationReporter Interface 1278 // 1279 // If we have a Reporter that is also a PhysicalLocationReporter, 1280 // we will defer to that Reporter's methods. 1281 // Else we will assume a LocoNet style message to be parsed. 1282 1283 /** 1284 * Parse a given string and return the LocoAddress value that is presumed 1285 * stored within it based on this object's protocol. The Class Block 1286 * implementation defers to its associated Reporter, if it exists. 1287 * 1288 * @param rep String to be parsed 1289 * @return LocoAddress address parsed from string, or null if this Block 1290 * isn't associated with a Reporter, or is associated with a 1291 * Reporter that is not also a PhysicalLocationReporter 1292 */ 1293 @Override 1294 public LocoAddress getLocoAddress(String rep) { 1295 // Defer parsing to our associated Reporter if we can. 1296 if (rep == null) { 1297 log.warn("String input is null!"); 1298 return null; 1299 } 1300 Reporter testReporter = this.getReporter(); 1301 if ( testReporter instanceof PhysicalLocationReporter ) { 1302 return ((PhysicalLocationReporter)testReporter).getLocoAddress(rep); 1303 } else { 1304 // Assume a LocoNet-style report. This is (nascent) support for handling of Faller cars 1305 // for Dave Merrill's project. 1306 log.debug("report string: {}", rep); 1307 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1308 // Match a number followed by the word "enter". This is the LocoNet pattern. 1309 Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); 1310 Matcher m = lnp.matcher(rep); 1311 if (m.find()) { 1312 log.debug("Parsed address: {}", m.group(1)); 1313 return new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC); 1314 } else { 1315 return null; 1316 } 1317 } 1318 } 1319 1320 /** 1321 * Parses out a (possibly old) LnReporter-generated report string to extract 1322 * the direction from within it based on this object's protocol. The Class 1323 * Block implementation defers to its associated Reporter, if it exists. 1324 * 1325 * @param rep String to be parsed 1326 * @return PhysicalLocationReporter.Direction direction parsed from string, 1327 * or null if this Block isn't associated with a Reporter, or is 1328 * associated with a Reporter that is not also a 1329 * PhysicalLocationReporter 1330 */ 1331 @Override 1332 public PhysicalLocationReporter.Direction getDirection(String rep) { 1333 if (rep == null) { 1334 log.warn("String input is null!"); 1335 return (null); 1336 } 1337 // Defer parsing to our associated Reporter if we can. 1338 Reporter testReporter = this.getReporter(); 1339 if ( testReporter instanceof PhysicalLocationReporter ) { 1340 return ((PhysicalLocationReporter)testReporter).getDirection(rep); 1341 } else { 1342 log.debug("report string: {}", rep); 1343 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1344 // Match a number followed by the word "enter". This is the LocoNet pattern. 1345 Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?"); 1346 Matcher m = lnp.matcher(rep); 1347 if (m.find()) { 1348 log.debug("Parsed direction: {}", m.group(2)); 1349 switch (m.group(2)) { 1350 case "enter": 1351 // LocoNet Enter message 1352 return PhysicalLocationReporter.Direction.ENTER; 1353 case "seen": 1354 // Lissy message. Treat them all as "entry" messages. 1355 return PhysicalLocationReporter.Direction.ENTER; 1356 default: 1357 return PhysicalLocationReporter.Direction.EXIT; 1358 } 1359 } else { 1360 return PhysicalLocationReporter.Direction.UNKNOWN; 1361 } 1362 } 1363 } 1364 1365 /** 1366 * Return this Block's physical location, if it exists. 1367 * Defers actual work to the helper methods in class PhysicalLocation. 1368 * 1369 * @return PhysicalLocation : this Block's location. 1370 */ 1371 @Override 1372 public PhysicalLocation getPhysicalLocation() { 1373 // We have our own PhysicalLocation. That's the point. No need to defer to the Reporter. 1374 return PhysicalLocation.getBeanPhysicalLocation(this); 1375 } 1376 1377 /** 1378 * Return this Block's physical location, if it exists. 1379 * Does not use the parameter s. 1380 * Defers actual work to the helper methods in class PhysicalLocation 1381 * 1382 * @param s (this parameter is ignored) 1383 * @return PhysicalLocation : this Block's location. 1384 */ 1385 @Override 1386 public PhysicalLocation getPhysicalLocation(String s) { 1387 // We have our own PhysicalLocation. That's the point. No need to defer to the Reporter. 1388 // Intentionally ignore the String s 1389 return PhysicalLocation.getBeanPhysicalLocation(this); 1390 } 1391 1392 @Override 1393 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 1394 if (Manager.PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) { 1395 if (evt.getOldValue() instanceof Sensor 1396 && evt.getOldValue().equals(getSensor())) { 1397 throw new PropertyVetoException(getDisplayName(), evt); 1398 } 1399 if (evt.getOldValue() instanceof Reporter 1400 && evt.getOldValue().equals(getReporter())) { 1401 throw new PropertyVetoException(getDisplayName(), evt); 1402 } 1403 } else if (Manager.PROPERTY_DO_DELETE.equals(evt.getPropertyName())) { 1404 if (evt.getOldValue() instanceof Sensor 1405 && evt.getOldValue().equals(getSensor())) { 1406 setSensor(null); 1407 } 1408 if (evt.getOldValue() instanceof Reporter 1409 && evt.getOldValue().equals(getReporter())) { 1410 setReporter(null); 1411 } 1412 } 1413 } 1414 1415 @Override 1416 public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) { 1417 List<NamedBeanUsageReport> report = new ArrayList<>(); 1418 if (bean != null) { 1419 if (bean.equals(getSensor())) { 1420 report.add(new NamedBeanUsageReport("BlockSensor")); // NOI18N 1421 } 1422 if (bean.equals(getReporter())) { 1423 report.add(new NamedBeanUsageReport("BlockReporter")); // NOI18N 1424 } 1425 // Block paths 1426 getPaths().forEach( path -> { 1427 if (bean.equals(path.getBlock())) { 1428 report.add(new NamedBeanUsageReport("BlockPathNeighbor")); // NOI18N 1429 } 1430 path.getSettings().forEach( setting -> { 1431 if (bean.equals(setting.getBean())) { 1432 report.add(new NamedBeanUsageReport("BlockPathTurnout")); // NOI18N 1433 } 1434 }); 1435 }); 1436 } 1437 return report; 1438 } 1439 1440 @Override 1441 public String getBeanType() { 1442 return Bundle.getMessage("BeanNameBlock"); 1443 } 1444 1445 /** {@inheritDoc} */ 1446 @Override 1447 @Nonnull 1448 public String describeState(int state) { 1449 switch (state) { 1450 case Block.OCCUPIED: 1451 return Bundle.getMessage("BlockOccupied"); 1452 case Block.UNOCCUPIED: 1453 return Bundle.getMessage("BlockUnOccupied"); 1454 case Block.UNDETECTED: 1455 return Bundle.getMessage("BlockUndetected"); 1456 default: // state unknown, state inconsistent, state unexpected 1457 return super.describeState(state); 1458 } 1459 } 1460 1461 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Block.class); 1462}