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