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 @Override 476 public void setState(int v) { 477 int old = _current; 478 _current = v; 479 // notify 480 481 // It is rather unpleasant that the following needs to be done in a try-catch, but exceptions have been observed 482 try { 483 firePropertyChange("state", old, _current); 484 } catch (Exception e) { 485 log.error("{} got exception during firePropertyChange({},{}) in thread {} {}", getDisplayName(), old, _current, 486 Thread.currentThread().getName(), Thread.currentThread().getId(), e); 487 } 488 } 489 490 /** 491 * Set the value retained by this Block. 492 * Also used when the Block itself gathers a value from an adjacent Block. 493 * This can be overridden in a subclass if 494 * e.g. you want to keep track of Blocks elsewhere, 495 * but make sure you also eventually invoke the super.setValue() here. 496 * Fires propertyChange "value" when changed. 497 * 498 * @param value The new Object resident in this block, or null if none 499 */ 500 public void setValue(Object value) { 501 //ignore if unchanged 502 if (value != _value) { 503 log.debug("Block {} value changed from '{}' to '{}'", getDisplayName(), _value, value); 504 _previousValue = _value; 505 _value = value; 506 firePropertyChange("value", _previousValue, _value); // NOI18N 507 } 508 } 509 510 /** 511 * Get the Block Contents Value. 512 * @return object with current value, could be null. 513 */ 514 public Object getValue() { 515 return _value; 516 } 517 518 /** 519 * Set Block Direction of Travel. 520 * Fires propertyChange "direction" when changed. 521 * @param direction Path Constant form, see {@link Path Path.java} 522 */ 523 public void setDirection(int direction) { 524 //ignore if unchanged 525 if (direction != _direction) { 526 log.debug("Block {} direction changed from {} to {}", getDisplayName(), Path.decodeDirection(_direction), Path.decodeDirection(direction)); 527 int oldDirection = _direction; 528 _direction = direction; 529 // this is a bound parameter 530 firePropertyChange("direction", oldDirection, direction); // NOI18N 531 } 532 } 533 534 /** 535 * Get Block Direction of Travel. 536 * @return direction in Path Constant form, see {@link Path Path.java} 537 */ 538 public int getDirection() { 539 return _direction; 540 } 541 542 //Deny traffic entering from this block 543 private final ArrayList<NamedBeanHandle<Block>> blockDenyList = new ArrayList<>(1); 544 545 /** 546 * Add to the Block Deny List. 547 * 548 * The block deny list, is used by higher level code, to determine if 549 * traffic/trains should be allowed to enter from an attached block, the 550 * list only deals with blocks that access should be denied from. 551 * <p> 552 * If we want to prevent traffic from following from this Block to another, 553 * then this Block must be added to the deny list of the other Block. 554 * By default no Block is barred, so traffic flow is bi-directional. 555 * @param pName name of the block to add, which must exist 556 */ 557 public void addBlockDenyList(@Nonnull String pName) { 558 Block blk = InstanceManager.getDefault(BlockManager.class).getBlock(pName); 559 if (blk == null) { 560 throw new IllegalArgumentException("addBlockDenyList requests block \"" + pName + "\" exists"); 561 } 562 NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(pName, blk); 563 if (!blockDenyList.contains(namedBlock)) { 564 blockDenyList.add(namedBlock); 565 } 566 } 567 568 public void addBlockDenyList(Block blk) { 569 NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(blk.getDisplayName(), blk); 570 if (!blockDenyList.contains(namedBlock)) { 571 blockDenyList.add(namedBlock); 572 } 573 } 574 575 public void removeBlockDenyList(String blk) { 576 NamedBeanHandle<Block> toremove = null; 577 for (NamedBeanHandle<Block> bean : blockDenyList) { 578 if (bean.getName().equals(blk)) { 579 toremove = bean; 580 } 581 } 582 if (toremove != null) { 583 blockDenyList.remove(toremove); 584 } 585 } 586 587 public void removeBlockDenyList(Block blk) { 588 NamedBeanHandle<Block> toremove = null; 589 for (NamedBeanHandle<Block> bean : blockDenyList) { 590 if (bean.getBean() == blk) { 591 toremove = bean; 592 } 593 } 594 if (toremove != null) { 595 blockDenyList.remove(toremove); 596 } 597 } 598 599 public List<String> getDeniedBlocks() { 600 List<String> list = new ArrayList<>(blockDenyList.size()); 601 blockDenyList.forEach((bean) -> { 602 list.add(bean.getName()); 603 }); 604 return list; 605 } 606 607 public boolean isBlockDenied(String deny) { 608 return blockDenyList.stream().anyMatch((bean) -> (bean.getName().equals(deny))); 609 } 610 611 public boolean isBlockDenied(Block deny) { 612 return blockDenyList.stream().anyMatch((bean) -> (bean.getBean() == deny)); 613 } 614 615 /** 616 * Get if Block can have permissive working. 617 * Blocks default to non-permissive, i.e. false. 618 * @return true if permissive, else false. 619 */ 620 public boolean getPermissiveWorking() { 621 return _permissiveWorking; 622 } 623 624 /** 625 * Property name change fired when the Block Permissive Status changes. 626 * The fired event includes 627 * old value: previous permissive status. 628 * new value: new permissive status. 629 */ 630 public final static String BLOCK_PERMISSIVE_CHANGE = "BlockPermissiveWorking"; // NOI18N 631 632 /** 633 * Set Block as permissive. 634 * Fires propertyChange "BlockPermissiveWorking" when changed. 635 * @param w true permissive, false NOT permissive 636 */ 637 public void setPermissiveWorking(boolean w) { 638 if (_permissiveWorking != w) { 639 _permissiveWorking = w; 640 firePropertyChange(BLOCK_PERMISSIVE_CHANGE, !w, w); // NOI18N 641 } 642 } 643 644 private boolean _permissiveWorking = false; 645 646 public float getSpeedLimit() { 647 if ((_blockSpeed == null) || (_blockSpeed.isEmpty())) { 648 return -1; 649 } 650 String speed = _blockSpeed; 651 if (_blockSpeed.equals("Global")) { 652 speed = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed(); 653 } 654 655 try { 656 return Float.parseFloat(speed); 657 } catch (NumberFormatException nx) { 658 //considered normal if the speed is not a number. 659 } 660 try { 661 return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed); 662 } catch (IllegalArgumentException ex) { 663 return -1; 664 } 665 } 666 667 private String _blockSpeed = ""; 668 669 public String getBlockSpeed() { 670 if (_blockSpeed.equals("Global")) { 671 return (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); 672 // Ensure the word "Global" is always in the speed name for later comparison 673 } 674 return _blockSpeed; 675 } 676 677 /** 678 * Property name change fired when the Block Speed changes. 679 * The fired event includes 680 * old value: previous speed String. 681 * new value: new speed String. 682 */ 683 public final static String BLOCK_SPEED_CHANGE = "BlockSpeedChange"; // NOI18N 684 685 /** 686 * Set the Block Speed Name. 687 * <p> 688 * Does not perform name validity checking. 689 * Does not send Property Change Event. 690 * @param s new Speed Name String. 691 */ 692 public void setBlockSpeedName(String s) { 693 if (s == null) { 694 _blockSpeed = ""; 695 } else { 696 _blockSpeed = s; 697 } 698 } 699 700 /** 701 * Set the Block Speed, preferred method. 702 * <p> 703 * Fires propertyChange "BlockSpeedChange" when changed. 704 * @param s Speed String 705 * @throws JmriException if Value of requested block speed is not valid. 706 */ 707 public void setBlockSpeed(String s) throws JmriException { 708 if ((s == null) || (_blockSpeed.equals(s))) { 709 return; 710 } 711 if (s.contains("Global")) { 712 s = "Global"; 713 } else { 714 try { 715 Float.parseFloat(s); 716 } catch (NumberFormatException nx) { 717 try { 718 InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s); 719 } catch (IllegalArgumentException ex) { 720 throw new JmriException("Value of requested block speed is not valid"); 721 } 722 } 723 } 724 String oldSpeed = _blockSpeed; 725 _blockSpeed = s; 726 firePropertyChange(BLOCK_SPEED_CHANGE, oldSpeed, s); 727 } 728 729 /** 730 * Property name change fired when the Block Curvature changes. 731 * The fired event includes 732 * old value: previous Block Curvature Constant. 733 * new value: new Block Curvature Constant. 734 */ 735 public final static String BLOCK_CURVATURE_CHANGE = "BlockCurvatureChange"; // NOI18N 736 737 /** 738 * Set Block Curvature Constant. 739 * Valid values : 740 * Block.NONE, Block.GRADUAL, Block.TIGHT, Block.SEVERE 741 * Fires propertyChange "BlockCurvatureChange" when changed. 742 * @param c Constant, e.g. Block.GRADUAL 743 */ 744 public void setCurvature(int c) { 745 if (_curvature!=c) { 746 int oldCurve = _curvature; 747 _curvature = c; 748 firePropertyChange(BLOCK_CURVATURE_CHANGE, oldCurve, c); 749 } 750 } 751 752 /** 753 * Get Block Curvature Constant. 754 * Defaults to Block.NONE 755 * @return constant, e.g. Block.TIGHT 756 */ 757 public int getCurvature() { 758 return _curvature; 759 } 760 761 /** 762 * Property name change fired when the Block Length changes. 763 * The fired event includes 764 * old value: previous float length (mm). 765 * new value: new float length (mm). 766 */ 767 public final static String BLOCK_LENGTH_CHANGE = "BlockLengthChange"; // NOI18N 768 769 /** 770 * Set length in millimeters. 771 * Paths will inherit this length, if their length is not specifically set. 772 * This length is the maximum length of any Path in the block. 773 * Path lengths exceeding this will be set to the default length. 774 * <p> 775 * Fires propertyChange "BlockLengthChange" when changed, float values in mm. 776 * @param l length in millimeters 777 */ 778 public void setLength(float l) { 779 float oldLen = getLengthMm(); 780 if (Math.abs(oldLen - l) > 0.0001){ // length value is different 781 _length = l; 782 getPaths().stream().forEach(p -> { 783 if (p.getLength() > l) { 784 p.setLength(0); // set to default 785 } 786 }); 787 firePropertyChange(BLOCK_LENGTH_CHANGE, oldLen, l); 788 } 789 } 790 791 /** 792 * Get Block Length in Millimetres. 793 * Default 0.0f. 794 * @return length in mm. 795 */ 796 public float getLengthMm() { 797 return _length; 798 } 799 800 /** 801 * Get Block Length in Centimetres. 802 * Courtesy method using result from getLengthMm. 803 * @return length in centimetres. 804 */ 805 public float getLengthCm() { 806 return (_length / 10.0f); 807 } 808 809 /** 810 * Get Block Length in Inches. 811 * Courtesy method using result from getLengthMm. 812 * @return length in inches. 813 */ 814 public float getLengthIn() { 815 return (_length / 25.4f); 816 } 817 818 /** 819 * Note: this has to make choices about identity values (always the same) 820 * and operation values (can change as the block works). Might be missing 821 * some identity values. 822 */ 823 @Override 824 public boolean equals(Object obj) { 825 if (obj == this) { 826 return true; 827 } 828 if (obj == null) { 829 return false; 830 } 831 832 if (!(getClass() == obj.getClass())) { 833 return false; 834 } else { 835 Block b = (Block) obj; 836 return b.getSystemName().equals(this.getSystemName()); 837 } 838 } 839 840 @Override 841 // This can't change, so can't include mutable values 842 public int hashCode() { 843 return this.getSystemName().hashCode(); 844 } 845 846 // internal data members 847 private int _current = UNDETECTED; // state until sensor is set 848 //private Sensor _sensor = null; 849 private NamedBeanHandle<Sensor> _namedSensor = null; 850 private PropertyChangeListener _sensorListener = null; 851 private Object _value; 852 private Object _previousValue; 853 private int _direction; 854 private int _curvature = NONE; 855 private float _length = 0.0f; // always stored in millimeters 856 private Reporter _reporter = null; 857 private PropertyChangeListener _reporterListener = null; 858 private boolean _reportingCurrent = false; 859 860 private Path[] pListOfPossibleEntrancePaths = null; 861 private int cntOfPossibleEntrancePaths = 0; 862 863 void resetCandidateEntrancePaths() { 864 pListOfPossibleEntrancePaths = null; 865 cntOfPossibleEntrancePaths = 0; 866 } 867 868 boolean setAsEntryBlockIfPossible(Block b) { 869 for (int i = 0; i < cntOfPossibleEntrancePaths; i++) { 870 Block CandidateBlock = pListOfPossibleEntrancePaths[i].getBlock(); 871 if (CandidateBlock == b) { 872 setValue(CandidateBlock.getValue()); 873 setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection()); 874 log.info("Block {} gets LATE new value from {}, direction= {}", getDisplayName(), CandidateBlock.getDisplayName(), Path.decodeDirection(getDirection())); 875 resetCandidateEntrancePaths(); 876 return true; 877 } 878 } 879 return false; 880 } 881 882 /** 883 * Handle change in sensor state. 884 * <p> 885 * Defers real work to goingActive, goingInactive methods. 886 * 887 * @param e the event 888 */ 889 void handleSensorChange(PropertyChangeEvent e) { 890 Sensor s = getSensor(); 891 if (e.getPropertyName().equals("KnownState") && s!=null) { 892 int state = s.getState(); 893 switch (state) { 894 case Sensor.ACTIVE: 895 goingActive(); 896 break; 897 case Sensor.INACTIVE: 898 goingInactive(); 899 break; 900 case Sensor.UNKNOWN: 901 goingUnknown(); 902 break; 903 default: 904 goingInconsistent(); 905 break; 906 } 907 } 908 } 909 910 public void goingUnknown() { 911 setValue(null); 912 setState(UNKNOWN); 913 } 914 915 public void goingInconsistent() { 916 setValue(null); 917 setState(INCONSISTENT); 918 } 919 920 /** 921 * Handle change in Reporter value. 922 * 923 * @param e PropertyChangeEvent 924 */ 925 void handleReporterChange(PropertyChangeEvent e) { 926 if ((_reportingCurrent && e.getPropertyName().equals("currentReport")) 927 || (!_reportingCurrent && e.getPropertyName().equals("lastReport"))) { 928 setValue(e.getNewValue()); 929 } 930 } 931 932 private Instant _timeLastInactive; 933 934 /** 935 * Handles Block sensor going INACTIVE: this block is empty 936 */ 937 public void goingInactive() { 938 log.debug("Block {} goes UNOCCUPIED", getDisplayName()); 939 for (Path path : paths) { 940 Block b = path.getBlock(); 941 if (b != null) { 942 b.setAsEntryBlockIfPossible(this); 943 } 944 } 945 setValue(null); 946 setDirection(Path.NONE); 947 setState(UNOCCUPIED); 948 _timeLastInactive = Instant.now(); 949 } 950 951 private final int maxInfoMessages = 5; 952 private int infoMessageCount = 0; 953 954 /** 955 * Handles Block sensor going ACTIVE: this block is now occupied, figure out 956 * from who and copy their value. 957 */ 958 public void goingActive() { 959 if (getState() == OCCUPIED) { 960 return; 961 } 962 log.debug("Block {} goes OCCUPIED", getDisplayName()); 963 resetCandidateEntrancePaths(); 964 // index through the paths, counting 965 int count = 0; 966 Path next = null; 967 // get statuses of everything once 968 int currPathCnt = paths.size(); 969 Path[] pList = new Path[currPathCnt]; 970 boolean[] isSet = new boolean[currPathCnt]; 971 boolean[] isActive = new boolean[currPathCnt]; 972 int[] pDir = new int[currPathCnt]; 973 int[] pFromDir = new int[currPathCnt]; 974 for (int i = 0; i < currPathCnt; i++) { 975 pList[i] = paths.get(i); 976 isSet[i] = pList[i].checkPathSet(); 977 Block b = pList[i].getBlock(); 978 if (b != null) { 979 isActive[i] = b.getState() == OCCUPIED; 980 pDir[i] = b.getDirection(); 981 } else { 982 isActive[i] = false; 983 pDir[i] = -1; 984 } 985 pFromDir[i] = pList[i].getFromBlockDirection(); 986 if (isSet[i] && isActive[i]) { 987 count++; 988 next = pList[i]; 989 } 990 } 991 // sort on number of neighbors 992 switch (count) { 993 case 0: 994 if (null != _previousValue) { 995 // restore the previous value under either of these circumstances: 996 // 1. the block has been 'unoccupied' only very briefly 997 // 2. power has just come back on 998 Instant tn = Instant.now(); 999 BlockManager bm = jmri.InstanceManager.getDefault(jmri.BlockManager.class); 1000 if (bm.timeSinceLastLayoutPowerOn() < 5000 || (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) { 1001 setValue(_previousValue); 1002 if (infoMessageCount < maxInfoMessages) { 1003 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Restoring previous value.", getDisplayName()); 1004 infoMessageCount++; 1005 } 1006 } else if (log.isDebugEnabled()) { 1007 if (null != _timeLastInactive) { 1008 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()); 1009 } else { 1010 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()); 1011 } 1012 } 1013 } else { 1014 if (infoMessageCount < maxInfoMessages) { 1015 log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.", getDisplayName()); 1016 infoMessageCount++; 1017 } 1018 } 1019 break; 1020 case 1: 1021 // simple case 1022 if ((next != null) && (next.getBlock() != null)) { 1023 // normal case, transfer value object 1024 setValue(next.getBlock().getValue()); 1025 setDirection(next.getFromBlockDirection()); 1026 log.debug("Block {} gets new value '{}' from {}, direction={}", 1027 getDisplayName(), 1028 next.getBlock().getValue(), 1029 next.getBlock().getDisplayName(), 1030 Path.decodeDirection(getDirection())); 1031 } else if (next == null) { 1032 log.error("unexpected next==null processing block {}", getDisplayName()); 1033 } else { 1034 log.error("unexpected next.getBlock()=null processing block {}", getDisplayName()); 1035 } 1036 break; 1037 default: 1038 // count > 1, check for one with proper direction 1039 // this time, count ones with proper direction 1040 log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count); 1041 next = null; 1042 count = 0; 1043 boolean allNeighborsAgree = true; // true until it's found that some neighbor blocks contain different contents (trains) 1044 1045 // scan for neighbors without matching direction 1046 for (int i = 0; i < currPathCnt; i++) { 1047 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1048 log.debug("comparing {} ({}) to {} ({})", 1049 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1050 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1051 if ((pDir[i] & pFromDir[i]) > 0) { //use bitwise comparison to support combination directions such as "North, West" 1052 if (next != null && next.getBlock() != null && next.getBlock().getValue() != null && 1053 ! next.getBlock().getValue().equals(pList[i].getBlock().getValue())) { 1054 allNeighborsAgree = false; 1055 } 1056 count++; 1057 next = pList[i]; 1058 } 1059 } 1060 } 1061 1062 // If loop above didn't find neighbors with matching direction, scan w/out direction for neighbors 1063 // This is used when directions are not being used 1064 if (next == null) { 1065 for (int i = 0; i < currPathCnt; i++) { 1066 if (isSet[i] && isActive[i]) { 1067 if (next != null && next.getBlock() != null && next.getBlock().getValue() != null && 1068 ! next.getBlock().getValue().equals(pList[i].getBlock().getValue())) { 1069 allNeighborsAgree = false; 1070 } 1071 count++; 1072 next = pList[i]; 1073 } 1074 } 1075 } 1076 1077 if (next != null && count == 1) { 1078 // found one block with proper direction, use it 1079 setValue(next.getBlock().getValue()); 1080 setDirection(next.getFromBlockDirection()); 1081 log.debug("Block {} gets new value '{}' from {}, direction {}", 1082 getDisplayName(), next.getBlock().getValue(), 1083 next.getBlock().getDisplayName(), Path.decodeDirection(getDirection())); 1084 } else { 1085 // handle merging trains: All neighbors with same content (train ID) 1086 if (allNeighborsAgree && next != null) { 1087 setValue(next.getBlock().getValue()); 1088 setDirection(next.getFromBlockDirection()); 1089 } else { 1090 // don't all agree, so can't determine unique value 1091 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()); 1092 pListOfPossibleEntrancePaths = new Path[currPathCnt]; 1093 cntOfPossibleEntrancePaths = 0; 1094 for (int i = 0; i < currPathCnt; i++) { 1095 if (isSet[i] && isActive[i]) { 1096 pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i]; 1097 cntOfPossibleEntrancePaths++; 1098 } 1099 } 1100 } 1101 } 1102 break; 1103 } 1104 setState(OCCUPIED); 1105 } 1106 1107 /** 1108 * Find which path this Block became Active, without actually modifying the 1109 * state of this block. 1110 * <p> 1111 * (this is largely a copy of the 'Search' part of the logic from 1112 * goingActive()) 1113 * 1114 * @return the next path 1115 */ 1116 public Path findFromPath() { 1117 // index through the paths, counting 1118 int count = 0; 1119 Path next = null; 1120 // get statuses of everything once 1121 int currPathCnt = paths.size(); 1122 Path[] pList = new Path[currPathCnt]; 1123 boolean[] isSet = new boolean[currPathCnt]; 1124 boolean[] isActive = new boolean[currPathCnt]; 1125 int[] pDir = new int[currPathCnt]; 1126 int[] pFromDir = new int[currPathCnt]; 1127 for (int i = 0; i < currPathCnt; i++) { 1128 pList[i] = paths.get(i); 1129 isSet[i] = pList[i].checkPathSet(); 1130 Block b = pList[i].getBlock(); 1131 if (b != null) { 1132 isActive[i] = b.getState() == OCCUPIED; 1133 pDir[i] = b.getDirection(); 1134 } else { 1135 isActive[i] = false; 1136 pDir[i] = -1; 1137 } 1138 pFromDir[i] = pList[i].getFromBlockDirection(); 1139 if (isSet[i] && isActive[i]) { 1140 count++; 1141 next = pList[i]; 1142 } 1143 } 1144 // sort on number of neighbors 1145 if ((count == 0) || (count == 1)) { 1146 // do nothing. OK to return null from this function. "next" is already set. 1147 } else { 1148 // count > 1, check for one with proper direction 1149 // this time, count ones with proper direction 1150 log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count); 1151 next = null; 1152 count = 0; 1153 for (int i = 0; i < currPathCnt; i++) { 1154 if (isSet[i] && isActive[i]) { //only consider active reachable blocks 1155 log.debug("comparing {} ({}) to {} ({})", 1156 pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]), 1157 getDisplayName(), Path.decodeDirection(pFromDir[i])); 1158 if ((pDir[i] & pFromDir[i]) > 0) { //use bitwise comparison to support combination directions such as "North, West" 1159 count++; 1160 next = pList[i]; 1161 } 1162 } 1163 } 1164 if (next == null) { 1165 log.debug("next is null!"); 1166 } 1167 if (next != null && count == 1) { 1168 // found one block with proper direction, assume that 1169 } else { 1170 // no unique path with correct direction - this happens frequently from noise in block detectors!! 1171 log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}", count, getDisplayName()); 1172 } 1173 } 1174 // in any case, go OCCUPIED 1175 if (log.isDebugEnabled()) { // avoid potentially expensive non-logging 1176 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)")); 1177 } 1178 return (next); 1179 } 1180 1181 /** 1182 * This allows the layout block to inform any listeners to the block 1183 * that the higher level layout block has been set to "useExtraColor" which is an 1184 * indication that it has been allocated to a section by the AutoDispatcher. 1185 * The value set is not retained in any form by the block, 1186 * it is purely to trigger a propertyChangeEvent. 1187 * @param boo Allocation status 1188 */ 1189 public void setAllocated(Boolean boo) { 1190 firePropertyChange("allocated", !boo, boo); 1191 } 1192 1193 // Methods to implmement PhysicalLocationReporter Interface 1194 // 1195 // If we have a Reporter that is also a PhysicalLocationReporter, 1196 // we will defer to that Reporter's methods. 1197 // Else we will assume a LocoNet style message to be parsed. 1198 1199 /** 1200 * Parse a given string and return the LocoAddress value that is presumed 1201 * stored within it based on this object's protocol. The Class Block 1202 * implementation defers to its associated Reporter, if it exists. 1203 * 1204 * @param rep String to be parsed 1205 * @return LocoAddress address parsed from string, or null if this Block 1206 * isn't associated with a Reporter, or is associated with a 1207 * Reporter that is not also a PhysicalLocationReporter 1208 */ 1209 @Override 1210 public LocoAddress getLocoAddress(String rep) { 1211 // Defer parsing to our associated Reporter if we can. 1212 if (rep == null) { 1213 log.warn("String input is null!"); 1214 return (null); 1215 } 1216 if ((this.getReporter() != null) && (this.getReporter() instanceof PhysicalLocationReporter)) { 1217 return (((PhysicalLocationReporter) this.getReporter()).getLocoAddress(rep)); 1218 } else { 1219 // Assume a LocoNet-style report. This is (nascent) support for handling of Faller cars 1220 // for Dave Merrill's project. 1221 log.debug("report string: {}", rep); 1222 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1223 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. 1224 Matcher m = ln_p.matcher(rep); 1225 if (m.find()) { 1226 log.debug("Parsed address: {}", m.group(1)); 1227 return (new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC)); 1228 } else { 1229 return (null); 1230 } 1231 } 1232 } 1233 1234 /** 1235 * Parses out a (possibly old) LnReporter-generated report string to extract 1236 * the direction from within it based on this object's protocol. The Class 1237 * Block implementation defers to its associated Reporter, if it exists. 1238 * 1239 * @param rep String to be parsed 1240 * @return PhysicalLocationReporter.Direction direction parsed from string, 1241 * or null if this Block isn't associated with a Reporter, or is 1242 * associated with a Reporter that is not also a 1243 * PhysicalLocationReporter 1244 */ 1245 @Override 1246 public PhysicalLocationReporter.Direction getDirection(String rep) { 1247 if (rep == null) { 1248 log.warn("String input is null!"); 1249 return (null); 1250 } 1251 // Defer parsing to our associated Reporter if we can. 1252 if ((this.getReporter() != null) && (this.getReporter() instanceof PhysicalLocationReporter)) { 1253 return (((PhysicalLocationReporter) this.getReporter()).getDirection(rep)); 1254 } else { 1255 log.debug("report string: {}", rep); 1256 // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter 1257 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. 1258 Matcher m = ln_p.matcher(rep); 1259 if (m.find()) { 1260 log.debug("Parsed direction: {}", m.group(2)); 1261 switch (m.group(2)) { 1262 case "enter": 1263 // LocoNet Enter message 1264 return (PhysicalLocationReporter.Direction.ENTER); 1265 case "seen": 1266 // Lissy message. Treat them all as "entry" messages. 1267 return (PhysicalLocationReporter.Direction.ENTER); 1268 default: 1269 return (PhysicalLocationReporter.Direction.EXIT); 1270 } 1271 } else { 1272 return (PhysicalLocationReporter.Direction.UNKNOWN); 1273 } 1274 } 1275 } 1276 1277 /** 1278 * Return this Block's physical location, if it exists. 1279 * Defers actual work to the helper methods in class PhysicalLocation. 1280 * 1281 * @return PhysicalLocation : this Block's location. 1282 */ 1283 @Override 1284 public PhysicalLocation getPhysicalLocation() { 1285 // We have our won PhysicalLocation. That's the point. No need to defer to the Reporter. 1286 return (PhysicalLocation.getBeanPhysicalLocation(this)); 1287 } 1288 1289 /** 1290 * Return this Block's physical location, if it exists. 1291 * Does not use the parameter s. 1292 * Defers actual work to the helper methods in class PhysicalLocation 1293 * 1294 * @param s (this parameter is ignored) 1295 * @return PhysicalLocation : this Block's location. 1296 */ 1297 @Override 1298 public PhysicalLocation getPhysicalLocation(String s) { 1299 // We have our won PhysicalLocation. That's the point. No need to defer to the Reporter. 1300 // Intentionally ignore the String s 1301 return (PhysicalLocation.getBeanPhysicalLocation(this)); 1302 } 1303 1304 @Override 1305 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 1306 if ("CanDelete".equals(evt.getPropertyName())) { // No I18N 1307 if (evt.getOldValue() instanceof Sensor) { 1308 if (evt.getOldValue().equals(getSensor())) { 1309 throw new PropertyVetoException(getDisplayName(), evt); 1310 } 1311 } 1312 if (evt.getOldValue() instanceof Reporter) { 1313 if (evt.getOldValue().equals(getReporter())) { 1314 throw new PropertyVetoException(getDisplayName(), evt); 1315 } 1316 } 1317 } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N 1318 if (evt.getOldValue() instanceof Sensor) { 1319 if (evt.getOldValue().equals(getSensor())) { 1320 setSensor(null); 1321 } 1322 } 1323 if (evt.getOldValue() instanceof Reporter) { 1324 if (evt.getOldValue().equals(getReporter())) { 1325 setReporter(null); 1326 } 1327 } 1328 } 1329 } 1330 1331 @Override 1332 public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) { 1333 List<NamedBeanUsageReport> report = new ArrayList<>(); 1334 if (bean != null) { 1335 if (bean.equals(getSensor())) { 1336 report.add(new NamedBeanUsageReport("BlockSensor")); // NOI18N 1337 } 1338 if (bean.equals(getReporter())) { 1339 report.add(new NamedBeanUsageReport("BlockReporter")); // NOI18N 1340 } 1341 // Block paths 1342 getPaths().forEach((path) -> { 1343 if (bean.equals(path.getBlock())) { 1344 report.add(new NamedBeanUsageReport("BlockPathNeighbor")); // NOI18N 1345 } 1346 path.getSettings().forEach((setting) -> { 1347 if (bean.equals(setting.getBean())) { 1348 report.add(new NamedBeanUsageReport("BlockPathTurnout")); // NOI18N 1349 } 1350 }); 1351 }); 1352 } 1353 return report; 1354 } 1355 1356 @Override 1357 public String getBeanType() { 1358 return Bundle.getMessage("BeanNameBlock"); 1359 } 1360 1361 private final static Logger log = LoggerFactory.getLogger(Block.class); 1362}