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}