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}