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