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     * Set the sensor by name.
230     * Fires propertyChange "OccupancySensorChange" when changed.
231     * @param pName the name of the Sensor to set
232     * @return true if a Sensor is set and is not null; false otherwise
233     */
234    public boolean setSensor(String pName) {
235        Sensor oldSensor = getSensor();
236        if (pName == null || pName.isEmpty()) {
237                if (oldSensor!=null) {
238                    setNamedSensor(null);
239                    firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, null);
240                }
241                return false;
242        }
243        if (InstanceManager.getNullableDefault(SensorManager.class) != null) {
244            try {
245                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
246                if (sensor.equals(oldSensor)) {
247                    return false;
248                }
249                setNamedSensor(InstanceManager.getDefault(
250                    NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
251                firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, sensor);
252                return true;
253            } catch (IllegalArgumentException ex) {
254                setNamedSensor(null);
255                firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, null);
256                log.error("Sensor '{}' not available", pName);
257            }
258        } else {
259            log.error("No SensorManager for this protocol");
260        }
261        return false;
262    }
263
264    /**
265     * Set Block Occupancy Sensor.
266     * If Sensor set, Adds PCL, sets Block Occupancy Status to Sensor.
267     * Block State PropertyChange Event will fire.
268     * Does NOT route initial Sensor Status via goingUnknown() / goingActive() etc.
269     * <p>
270     * If Sensor null, removes PCL on previous Sensor, sets Block status to UNDETECTED.
271     * @param s Handle for Sensor.
272     */
273    public void setNamedSensor(@CheckForNull NamedBeanHandle<Sensor> s) {
274        if ( _namedSensor != null && _sensorListener != null) {
275            _namedSensor.getBean().removePropertyChangeListener(_sensorListener);
276            _sensorListener = null;
277        }
278        _namedSensor = s;
279
280        if (_namedSensor != null) {
281            _sensorListener = this::handleSensorChange;
282            _namedSensor.getBean().addPropertyChangeListener(_sensorListener,
283                s.getName(), "Block Sensor " + getDisplayName());
284            setState(_namedSensor.getBean().getState());
285            // At present does NOT route via goingUnknown() / goingActive() etc.
286        } else {
287            setState(UNDETECTED); // Does NOT route via goingUnknown() / goingActive() etc.
288        }
289    }
290
291    /**
292     * Get the Block Occupancy Sensor.
293     * @return Sensor if one attached to Block, may be null.
294     */
295    @CheckForNull
296    public Sensor getSensor() {
297        if (_namedSensor != null) {
298            return _namedSensor.getBean();
299        }
300        return null;
301    }
302
303    @CheckForNull
304    public NamedBeanHandle<Sensor> getNamedSensor() {
305        return _namedSensor;
306    }
307
308    /**
309     * Property name change fired when a Sensor is set to / removed from a Block.
310     * The fired event includes
311     * old value: Sensor Bean Object if previously set, else null
312     * new value: Sensor Bean Object if being set, may be null if Sensor removed.
313     */
314    public static final String BLOCK_REPORTER_CHANGE = "BlockReporterChange"; // NOI18N
315
316    /**
317     * Set the Reporter that should provide the data value for this block.
318     * Fires propertyChange "BlockReporterChange" when changed.
319     * @see Reporter
320     * @param reporter Reporter object to link, or null to clear
321     */
322    public void setReporter(@CheckForNull Reporter reporter) {
323        if (Objects.equals(reporter,_reporter)) {
324            return;
325        }
326        if (_reporter != null && _reporterListener != null) {
327            _reporter.removePropertyChangeListener(_reporterListener);
328            _reporterListener = null;
329        }
330        Reporter oldReporter = _reporter;
331        _reporter = reporter;
332        if (_reporter != null) {
333            _reporterListener = this::handleReporterChange;
334            _reporter.addPropertyChangeListener( _reporterListener );
335        }
336        firePropertyChange(BLOCK_REPORTER_CHANGE, oldReporter, reporter);
337    }
338
339    /**
340     * Retrieve the Reporter that is linked to this Block
341     *
342     * @see Reporter
343     * @return linked Reporter object, or null if not linked
344     */
345    @CheckForNull
346    public Reporter getReporter() {
347        return _reporter;
348    }
349
350    /**
351     * Property name change fired when the Block reporting Current flag changes.
352     * The fired event includes
353     * old value: previous value, Boolean.
354     * new value: new value, Boolean.
355     */
356    public static final String BLOCK_REPORTING_CURRENT = "BlockReportingCurrent"; // NOI18N
357
358    /**
359     * Define if the Block's value should be populated from the
360     * {@link Reporter#getCurrentReport() current report} or from the
361     * {@link Reporter#getLastReport() last report}.
362     * Fires propertyChange "BlockReportingCurrent" when changed.
363     * @see Reporter
364     * @param reportingCurrent true if to use current report; false if to use
365     *                         last report
366     */
367    public void setReportingCurrent(boolean reportingCurrent) {
368        if (_reportingCurrent != reportingCurrent) {
369            _reportingCurrent = reportingCurrent;
370            firePropertyChange(BLOCK_REPORTING_CURRENT, !reportingCurrent, reportingCurrent);
371        }
372    }
373
374    /**
375     * Determine if the Block's value is being populated from the
376     * {@link Reporter#getCurrentReport() current report} or from the
377     * {@link Reporter#getLastReport() last report}.
378     *
379     * @see Reporter
380     * @return true if populated by
381     *         {@link Reporter#getCurrentReport() current report}; false if from
382     *         {@link Reporter#getLastReport() last report}.
383     */
384    public boolean isReportingCurrent() {
385        return _reportingCurrent;
386    }
387
388    /**
389     * Get the Block State.
390     * OBlocks may well return a combination of states,
391     * Blocks will return a single State.
392     * @return Block state.
393     */
394    @Override
395    public int getState() {
396        return _current;
397    }
398
399    private final ArrayList<Path> paths = new ArrayList<>();
400
401    /**
402     * Add a Path to List of Paths.
403     * @param p Path to add, not null.
404     */
405    public void addPath(@Nonnull Path p) {
406        if (p == null) {
407            throw new IllegalArgumentException("Can't add null path");
408        }
409        paths.add(p);
410    }
411
412    /**
413     * Remove a Path from the Block.
414     * @param p Path to remove.
415     */
416    public void removePath(Path p) {
417        int j = -1;
418        for (int i = 0; i < paths.size(); i++) {
419            if (p == paths.get(i)) {
420                j = i;
421            }
422        }
423        if (j > -1) {
424            paths.remove(j);
425        }
426    }
427
428    /**
429     * Check if Block has a particular Path.
430     * @param p Path to test against.
431     * @return true if Block has the Path, else false.
432     */
433    public boolean hasPath(Path p) {
434        return paths.stream().anyMatch( t -> t.equals(p) );
435    }
436
437    /**
438     * Get a copy of the list of Paths.
439     *
440     * @return the paths or an empty list
441     */
442    @Nonnull
443    public List<Path> getPaths() {
444        return new ArrayList<>(paths);
445    }
446
447    /**
448     * Provide a general method for updating the report.
449     * Fires propertyChange "state" when called.
450     *
451     * @param v the new state
452     */
453    @SuppressWarnings("deprecation")    // The method getId() from the type Thread is deprecated since version 19
454                                        // The replacement Thread.threadId() isn't available before version 19
455    @Override
456    public void setState(int v) {
457        int old = _current;
458        _current = v;
459        // notify
460
461        // It is rather unpleasant that the following needs to be done in a try-catch, but exceptions have been observed
462        try {
463            firePropertyChange("state", old, _current);
464        } catch (Exception e) {
465            log.error("{} got exception during firePropertyChange({},{}) in thread {} {}",
466                getDisplayName(), old, _current,
467                Thread.currentThread().getName(), Thread.currentThread().getId(), e);
468        }
469    }
470
471    /**
472     * Set the value retained by this Block.
473     * Also used when the Block itself gathers a value from an adjacent Block.
474     * This can be overridden in a subclass if
475     * e.g. you want to keep track of Blocks elsewhere,
476     * but make sure you also eventually invoke the super.setValue() here.
477     * Fires propertyChange "value" when changed.
478     *
479     * @param value The new Object resident in this block, or null if none
480     */
481    public void setValue(Object value) {
482        //ignore if unchanged
483        if (value != _value) {
484            log.debug("Block {} value changed from '{}' to '{}'", getDisplayName(), _value, value);
485            _previousValue = _value;
486            _value = value;
487            firePropertyChange("value", _previousValue, _value); // NOI18N
488        }
489    }
490
491    /**
492     * Get the Block Contents Value.
493     * @return object with current value, could be null.
494     */
495    @CheckForNull
496    public Object getValue() {
497        return _value;
498    }
499
500    /**
501     * Set Block Direction of Travel.
502     * Fires propertyChange "direction" when changed.
503     * @param direction Path Constant form, see {@link Path Path.java}
504     */
505    public void setDirection(int direction) {
506        //ignore if unchanged
507        if (direction != _direction) {
508            log.debug("Block {} direction changed from {} to {}", getDisplayName(),
509                Path.decodeDirection(_direction), Path.decodeDirection(direction));
510            int oldDirection = _direction;
511            _direction = direction;
512            // this is a bound parameter
513            firePropertyChange("direction", oldDirection, direction); // NOI18N
514        }
515    }
516
517    /**
518     * Get Block Direction of Travel.
519     * @return direction in Path Constant form, see {@link Path Path.java}
520     */
521    public int getDirection() {
522        return _direction;
523    }
524
525    //Deny traffic entering from this block
526    private final ArrayList<NamedBeanHandle<Block>> blockDenyList = new ArrayList<>(1);
527
528    /**
529     * Add to the Block Deny List.
530     *
531     * The block deny list, is used by higher level code, to determine if
532     * traffic/trains should be allowed to enter from an attached block, the
533     * list only deals with blocks that access should be denied from.
534     * <p>
535     * If we want to prevent traffic from following from this Block to another,
536     * then this Block must be added to the deny list of the other Block.
537     * By default no Block is barred, so traffic flow is bi-directional.
538     * @param pName name of the block to add, which must exist
539     */
540    public void addBlockDenyList(@Nonnull String pName) {
541        Block blk = InstanceManager.getDefault(BlockManager.class).getBlock(pName);
542        if (blk == null) {
543            throw new IllegalArgumentException("addBlockDenyList requests block \"" + pName + "\" exists");
544        }
545        NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault(
546            NamedBeanHandleManager.class).getNamedBeanHandle(pName, blk);
547        if (!blockDenyList.contains(namedBlock)) {
548            blockDenyList.add(namedBlock);
549        }
550    }
551
552    public void addBlockDenyList(@Nonnull Block blk) {
553        NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault(
554            NamedBeanHandleManager.class).getNamedBeanHandle(blk.getDisplayName(), blk);
555        if (!blockDenyList.contains(namedBlock)) {
556            blockDenyList.add(namedBlock);
557        }
558    }
559
560    public void removeBlockDenyList(String blk) {
561        NamedBeanHandle<Block> toremove = null;
562        for (NamedBeanHandle<Block> bean : blockDenyList) {
563            if (bean.getName().equals(blk)) {
564                toremove = bean;
565            }
566        }
567        if (toremove != null) {
568            blockDenyList.remove(toremove);
569        }
570    }
571
572    public void removeBlockDenyList(Block blk) {
573        NamedBeanHandle<Block> toremove = null;
574        for (NamedBeanHandle<Block> bean : blockDenyList) {
575            if (bean.getBean() == blk) {
576                toremove = bean;
577            }
578        }
579        if (toremove != null) {
580            blockDenyList.remove(toremove);
581        }
582    }
583
584    public List<String> getDeniedBlocks() {
585        List<String> list = new ArrayList<>(blockDenyList.size());
586        blockDenyList.forEach( bean -> list.add(bean.getName()) );
587        return list;
588    }
589
590    public boolean isBlockDenied(String deny) {
591        return blockDenyList.stream().anyMatch( bean -> bean.getName().equals(deny));
592    }
593
594    public boolean isBlockDenied(Block deny) {
595        return blockDenyList.stream().anyMatch( bean -> bean.getBean() == deny);
596    }
597
598    /**
599     * Get if Block can have permissive working.
600     * Blocks default to non-permissive, i.e. false.
601     * @return true if permissive, else false.
602     */
603    public boolean getPermissiveWorking() {
604        return _permissiveWorking;
605    }
606
607    /**
608     * Property name change fired when the Block Permissive Status changes.
609     * The fired event includes
610     * old value: previous permissive status.
611     * new value: new permissive status.
612     */
613    public static final String BLOCK_PERMISSIVE_CHANGE = "BlockPermissiveWorking"; // NOI18N
614
615    /**
616     * Set Block as permissive.
617     * Fires propertyChange "BlockPermissiveWorking" when changed.
618     * @param w true permissive, false NOT permissive
619     */
620    public void setPermissiveWorking(boolean w) {
621        if (_permissiveWorking != w) {
622            _permissiveWorking = w;
623            firePropertyChange(BLOCK_PERMISSIVE_CHANGE, !w, w); // NOI18N
624        }
625    }
626
627    private boolean _permissiveWorking = false;
628
629    /**
630     * Get if Block is a ghost.
631     * Blocks default to non-ghost, i.e. false.
632     * @return true if ghost, else false.
633     */
634    public boolean getIsGhost() {
635        return _ghost;
636    }
637
638    /**
639     * Property name change fired when the Block ghost Status changes.
640     * The fired event includes
641     * old value: previous ghost status.
642     * new value: new ghost status.
643     */
644    public static final String GHOST_CHANGE = "BlockGhost"; // NOI18N
645
646    /**
647     * Set if the block is a ghost
648     * Fires propertyChange "BlockGhost" when changed.
649     * @param w true ghost, false NOT ghost
650     */
651    public void setIsGhost(boolean w) {
652        if (_ghost != w) {
653            _ghost = w;
654            firePropertyChange(GHOST_CHANGE, !w, w); // NOI18N
655        }
656    }
657
658    private boolean _ghost = false;
659
660    public float getSpeedLimit() {
661        if ((_blockSpeed == null) || (_blockSpeed.isEmpty())) {
662            return -1;
663        }
664        String speed = _blockSpeed;
665        if ( "Global".equals( _blockSpeed)) {
666            speed = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed();
667        }
668
669        try {
670            return Float.parseFloat(speed);
671        } catch (NumberFormatException nx) {
672            //considered normal if the speed is not a number.
673        }
674        try {
675            return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
676        } catch (IllegalArgumentException ex) {
677            return -1;
678        }
679    }
680
681    private String _blockSpeed = "";
682
683    public String getBlockSpeed() {
684        if ( "Global".equals( _blockSpeed)) {
685            return (Bundle.getMessage("UseGlobal", "Global") + " "
686                + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed());
687            // Ensure the word "Global" is always in the speed name for later comparison
688        }
689        return _blockSpeed;
690    }
691
692    /**
693     * Property name change fired when the Block Speed changes.
694     * The fired event includes
695     * old value: previous speed String.
696     * new value: new speed String.
697     */
698    public static final String BLOCK_SPEED_CHANGE = "BlockSpeedChange"; // NOI18N
699
700    /**
701     * Set the Block Speed Name.
702     * <p>
703     * Does not perform name validity checking.
704     * Does not send Property Change Event.
705     * @param s new Speed Name String.
706     */
707    public void setBlockSpeedName(String s) {
708        if (s == null) {
709            _blockSpeed = "";
710        } else {
711            _blockSpeed = s;
712        }
713    }
714
715    /**
716     * Set the Block Speed, preferred method.
717     * <p>
718     * Fires propertyChange "BlockSpeedChange" when changed.
719     * @param s Speed String
720     * @throws JmriException if Value of requested block speed is not valid.
721     */
722    public void setBlockSpeed(final String s) throws JmriException {
723        if ((s == null) || (_blockSpeed.equals(s))) {
724            return;
725        }
726        String newSpeed = s;
727        if (s.contains("Global")) {
728            newSpeed = "Global";
729        } else {
730            try {
731                Float.valueOf(s);
732            } catch (NumberFormatException nx) {
733                try {
734                    InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s);
735                } catch (IllegalArgumentException ex) {
736                    throw new JmriException("Block \"" + getDisplayName()
737                        + "\" requested speed value \"" + s + "\" invalid.");
738                }
739            }
740        }
741        String oldSpeed = _blockSpeed;
742        _blockSpeed = newSpeed;
743        firePropertyChange(BLOCK_SPEED_CHANGE, oldSpeed, s);
744    }
745
746    /**
747     * Property name change fired when the Block Curvature changes.
748     * The fired event includes
749     * old value: previous Block Curvature Constant.
750     * new value: new Block Curvature Constant.
751     */
752    public static final String BLOCK_CURVATURE_CHANGE = "BlockCurvatureChange"; // NOI18N
753
754    /**
755     * Set Block Curvature Constant.
756     * Valid values :
757     * Block.NONE, Block.GRADUAL, Block.TIGHT, Block.SEVERE
758     * Fires propertyChange "BlockCurvatureChange"  when changed.
759     * @param c Constant, e.g. Block.GRADUAL
760     */
761    public void setCurvature(int c) {
762        if (_curvature!=c) {
763            int oldCurve = _curvature;
764            _curvature = c;
765            firePropertyChange(BLOCK_CURVATURE_CHANGE, oldCurve, c);
766        }
767    }
768
769    /**
770     * Get Block Curvature Constant.
771     * Defaults to Block.NONE
772     * @return constant, e.g. Block.TIGHT
773     */
774    public int getCurvature() {
775        return _curvature;
776    }
777
778    /**
779     * Property name change fired when the Block Length changes.
780     * The fired event includes
781     * old value: previous float length (mm).
782     * new value: new float length (mm).
783     */
784    public static final String BLOCK_LENGTH_CHANGE = "BlockLengthChange"; // NOI18N
785
786    /**
787     * Set length in millimeters.
788     * Paths will inherit this length, if their length is not specifically set.
789     * This length is the maximum length of any Path in the block.
790     * Path lengths exceeding this will be set to the default length.
791     * <p>
792     * Fires propertyChange "BlockLengthChange"  when changed, float values in mm.
793     * @param l length in millimeters
794     */
795    public void setLength(float l) {
796        float oldLen = getLengthMm();
797        if (Math.abs(oldLen - l) > 0.0001){ // length value is different
798            _length = l;
799            getPaths().stream().forEach(p -> {
800                if (p.getLength() > l) {
801                    p.setLength(0); // set to default
802                }
803            });
804            firePropertyChange(BLOCK_LENGTH_CHANGE, oldLen, l);
805        }
806    }
807
808    /**
809     * Get Block Length in Millimetres.
810     * Default 0.0f.
811     * @return length in mm.
812     */
813    public float getLengthMm() {
814        return _length;
815    }
816
817    /**
818     * Get Block Length in Centimetres.
819     * Courtesy method using result from getLengthMm.
820     * @return length in centimetres.
821     */
822    public float getLengthCm() {
823        return (_length / 10.0f);
824    }
825
826    /**
827     * Get Block Length in Inches.
828     * Courtesy method using result from getLengthMm.
829     * @return length in inches.
830     */
831    public float getLengthIn() {
832        return (_length / 25.4f);
833    }
834
835    /**
836     * Note: this has to make choices about identity values (always the same)
837     * and operation values (can change as the block works). Might be missing
838     * some identity values.
839     */
840    @Override
841    public boolean equals(Object obj) {
842        if (obj == this) {
843            return true;
844        }
845        if (obj == null) {
846            return false;
847        }
848
849        if ( getClass() != obj.getClass() ) {
850            return false;
851        } else {
852            Block b = (Block) obj;
853            return b.getSystemName().equals(this.getSystemName());
854        }
855    }
856
857    @Override
858    // This can't change, so can't include mutable values
859    public int hashCode() {
860        return this.getSystemName().hashCode();
861    }
862
863    // internal data members
864    private int _current = UNDETECTED; // state until sensor is set
865    private NamedBeanHandle<Sensor> _namedSensor = null;
866    private PropertyChangeListener _sensorListener = null;
867    private Object _value;
868    private Object _previousValue;
869    private int _direction;
870    private int _curvature = NONE;
871    private float _length = 0.0f;  // always stored in millimeters
872    private Reporter _reporter = null;
873    private PropertyChangeListener _reporterListener = null;
874    private boolean _reportingCurrent = false;
875
876    private Path[] pListOfPossibleEntrancePaths = null;
877    private int cntOfPossibleEntrancePaths = 0;
878
879    void resetCandidateEntrancePaths() {
880        pListOfPossibleEntrancePaths = null;
881        cntOfPossibleEntrancePaths = 0;
882    }
883
884    boolean setAsEntryBlockIfPossible(Block b) {
885        for (int i = 0; i < cntOfPossibleEntrancePaths; i++) {
886            Block candidateBlock = pListOfPossibleEntrancePaths[i].getBlock();
887            if (candidateBlock == b) {
888                setValue(candidateBlock.getValue());
889                setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection());
890                log.info("Block {} gets LATE new value from {}, direction= {}",
891                    getDisplayName(), candidateBlock.getDisplayName(), Path.decodeDirection(getDirection()));
892                resetCandidateEntrancePaths();
893                return true;
894            }
895        }
896        return false;
897    }
898
899    /**
900     * Handle change in sensor state.
901     * <p>
902     * Defers real work to goingActive, goingInactive methods.
903     *
904     * @param e the event
905     */
906    void handleSensorChange(PropertyChangeEvent e) {
907        Sensor s = getSensor();
908        if ( "KnownState".equals( e.getPropertyName()) && s != null ) {
909            int state = s.getState();
910            switch (state) {
911                case Sensor.ACTIVE:
912                    goingActive();
913                    break;
914                case Sensor.INACTIVE:
915                    goingInactive();
916                    break;
917                case Sensor.UNKNOWN:
918                    goingUnknown();
919                    break;
920                default:
921                    goingInconsistent();
922                    break;
923            }
924        }
925    }
926
927    public void goingUnknown() {
928        setValue(null);
929        setState(UNKNOWN);
930    }
931
932    public void goingInconsistent() {
933        setValue(null);
934        setState(INCONSISTENT);
935    }
936
937    /**
938     * Handle change in Reporter value.
939     *
940     * @param e PropertyChangeEvent
941     */
942    void handleReporterChange(PropertyChangeEvent e) {
943        if ((_reportingCurrent && "currentReport".equals(e.getPropertyName()))
944            || (!_reportingCurrent && "lastReport".equals(e.getPropertyName()))) {
945            setValue(e.getNewValue());
946        }
947    }
948
949    private Instant _timeLastInactive;
950
951    /**
952     * Handles Block sensor going INACTIVE: this block is empty
953     */
954    public void goingInactive() {
955        log.debug("Block {} goes UNOCCUPIED", getDisplayName());
956        for (Path path : paths) {
957            Block b = path.getBlock();
958            if (b != null) {
959                b.setAsEntryBlockIfPossible(this);
960            }
961        }
962        setValue(null);
963        setDirection(Path.NONE);
964        setState(UNOCCUPIED);
965        _timeLastInactive = Instant.now();
966    }
967
968    private static final int MAXINFOMESSAGES = 5;
969    private int infoMessageCount = 0;
970
971    /**
972     * Handles Block sensor going ACTIVE: this block is now occupied, figure out
973     * from who and copy their value.
974     */
975    public void goingActive() {
976        if (getState() == OCCUPIED) {
977            return;
978        }
979        log.debug("Block {} goes OCCUPIED", getDisplayName());
980        resetCandidateEntrancePaths();
981        // index through the paths, counting
982        int count = 0;
983        Path next = null;
984        // get statuses of everything once
985        int currPathCnt = paths.size();
986        Path[] pList = new Path[currPathCnt];
987        boolean[] isSet = new boolean[currPathCnt];
988        boolean[] isActive = new boolean[currPathCnt];
989        int[] pDir = new int[currPathCnt];
990        int[] pFromDir = new int[currPathCnt];
991        for (int i = 0; i < currPathCnt; i++) {
992            pList[i] = paths.get(i);
993            isSet[i] = pList[i].checkPathSet();
994            Block b = pList[i].getBlock();
995            if (b != null) {
996                isActive[i] = b.getState() == OCCUPIED;
997                pDir[i] = b.getDirection();
998            } else {
999                isActive[i] = false;
1000                pDir[i] = -1;
1001            }
1002            pFromDir[i] = pList[i].getFromBlockDirection();
1003            if (isSet[i] && isActive[i]) {
1004                count++;
1005                next = pList[i];
1006            }
1007        }
1008        // sort on number of neighbors
1009        switch (count) {
1010            case 0:
1011                if (null != _previousValue) {
1012                    // restore the previous value under either of these circumstances:
1013                    // 1. the block has been 'unoccupied' only very briefly
1014                    // 2. power has just come back on
1015                    Instant tn = Instant.now();
1016                    BlockManager bm = InstanceManager.getDefault(BlockManager.class);
1017                    if ( bm.timeSinceLastLayoutPowerOn() < 5000 ||
1018                        (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) {
1019                        setValue(_previousValue);
1020                        if (infoMessageCount < MAXINFOMESSAGES) {
1021                            log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}."
1022                                +" Restoring previous value.", getDisplayName());
1023                            infoMessageCount++;
1024                        }
1025                    } else if (log.isDebugEnabled()) {
1026                        if (null != _timeLastInactive) {
1027                            log.debug("not restoring previous value, block {} has been inactive for too long ({}ms)"
1028                                + " and layout power has not just been restored ({}ms ago)",
1029                                getDisplayName(), tn.toEpochMilli() - _timeLastInactive.toEpochMilli(),
1030                                bm.timeSinceLastLayoutPowerOn());
1031                        } else {
1032                            log.debug("not restoring previous value, block {} has been inactive since the "
1033                                + "start of this session and layout power has not just been restored ({}ms ago)",
1034                                getDisplayName(), bm.timeSinceLastLayoutPowerOn());
1035                        }
1036                    }
1037                } else {
1038                    if (infoMessageCount < MAXINFOMESSAGES) {
1039                        log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.",
1040                            getDisplayName());
1041                        infoMessageCount++;
1042                    }
1043                }
1044                break;
1045            case 1:
1046                // simple case
1047                if ((next != null) && (next.getBlock() != null)) {
1048                    // normal case, transfer value object
1049                    setValue(next.getBlock().getValue());
1050                    setDirection(next.getFromBlockDirection());
1051                    log.debug("Block {} gets new value '{}' from {}, direction={}",
1052                            getDisplayName(),
1053                            next.getBlock().getValue(),
1054                            next.getBlock().getDisplayName(),
1055                            Path.decodeDirection(getDirection()));
1056                } else if (next == null) {
1057                    log.error("unexpected next==null processing block {}", getDisplayName());
1058                } else {
1059                    log.error("unexpected next.getBlock()=null processing block {}", getDisplayName());
1060                }
1061                break;
1062            default:
1063                // count > 1, check for one with proper direction
1064                // this time, count ones with proper direction
1065                log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count);
1066                next = null;
1067                count = 0;
1068                // true until it's found that some neighbor blocks contain different contents (trains)
1069                boolean allNeighborsAgree = true;
1070
1071                // scan for neighbors without matching direction
1072                for (int i = 0; i < currPathCnt; i++) {
1073                    if (isSet[i] && isActive[i]) {  //only consider active reachable blocks
1074                        log.debug("comparing {} ({}) to {} ({})",
1075                                pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]),
1076                                getDisplayName(), Path.decodeDirection(pFromDir[i]));
1077                        //use bitwise comparison to support combination directions such as "North, West"
1078                        if ((pDir[i] & pFromDir[i]) > 0) {
1079                            if (next != null  && next.getBlock() != null ) {
1080                                Object value = next.getBlock().getValue();
1081                                if ( value != null && !value.equals(pList[i].getBlock().getValue())) {
1082                                    allNeighborsAgree = false;
1083                                }
1084                            }
1085                            count++;
1086                            next = pList[i];
1087                        }
1088                    }
1089                }
1090
1091                // If loop above didn't find neighbors with matching direction, scan w/out direction for neighbors
1092                // This is used when directions are not being used
1093                if (next == null) {
1094                    for (int i = 0; i < currPathCnt; i++) {
1095                        if (isSet[i] && isActive[i]) {
1096                            if (next != null && next.getBlock() != null ) {
1097                                Object value = next.getBlock().getValue();
1098                                if ( value != null && ! value.equals(pList[i].getBlock().getValue())) {
1099                                    allNeighborsAgree = false;
1100                                }
1101                            }
1102                            count++;
1103                            next = pList[i];
1104                        }
1105                    }
1106                }
1107
1108                if (next != null && count == 1) {
1109                    // found one block with proper direction, use it
1110                    setValue(next.getBlock().getValue());
1111                    setDirection(next.getFromBlockDirection());
1112                    log.debug("Block {} gets new value '{}' from {}, direction {}",
1113                            getDisplayName(), next.getBlock().getValue(),
1114                            next.getBlock().getDisplayName(), Path.decodeDirection(getDirection()));
1115                } else {
1116                    // handle merging trains: All neighbors with same content (train ID)
1117                    if (allNeighborsAgree && next != null) {
1118                        setValue(next.getBlock().getValue());
1119                        setDirection(next.getFromBlockDirection());
1120                    } else {
1121                    // don't all agree, so can't determine unique value
1122                        log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for"
1123                            + " block {} but maybe it can be determined when another block becomes free",
1124                            count, getDisplayName());
1125                        pListOfPossibleEntrancePaths = new Path[currPathCnt];
1126                        cntOfPossibleEntrancePaths = 0;
1127                        for (int i = 0; i < currPathCnt; i++) {
1128                            if (isSet[i] && isActive[i]) {
1129                                pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i];
1130                                cntOfPossibleEntrancePaths++;
1131                            }
1132                        }
1133                    }
1134                }
1135                break;
1136        }
1137        setState(OCCUPIED);
1138    }
1139
1140    /**
1141     * Find which path this Block became Active, without actually modifying the
1142     * state of this block.
1143     * <p>
1144     * (this is largely a copy of the 'Search' part of the logic from
1145     * goingActive())
1146     *
1147     * @return the next path
1148     */
1149    @CheckForNull
1150    public Path findFromPath() {
1151        // index through the paths, counting
1152        int count = 0;
1153        Path next = null;
1154        // get statuses of everything once
1155        int currPathCnt = paths.size();
1156        Path[] pList = new Path[currPathCnt];
1157        boolean[] isSet = new boolean[currPathCnt];
1158        boolean[] isActive = new boolean[currPathCnt];
1159        int[] pDir = new int[currPathCnt];
1160        int[] pFromDir = new int[currPathCnt];
1161        for (int i = 0; i < currPathCnt; i++) {
1162            pList[i] = paths.get(i);
1163            isSet[i] = pList[i].checkPathSet();
1164            Block b = pList[i].getBlock();
1165            if (b != null) {
1166                isActive[i] = b.getState() == OCCUPIED;
1167                pDir[i] = b.getDirection();
1168            } else {
1169                isActive[i] = false;
1170                pDir[i] = -1;
1171            }
1172            pFromDir[i] = pList[i].getFromBlockDirection();
1173            if (isSet[i] && isActive[i]) {
1174                count++;
1175                next = pList[i];
1176            }
1177        }
1178        // sort on number of neighbors
1179        if ((count == 0) || (count == 1)) {
1180            // do nothing.  OK to return null from this function.  "next" is already set.
1181        } else {
1182            // count > 1, check for one with proper direction
1183            // this time, count ones with proper direction
1184            log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count);
1185            next = null;
1186            count = 0;
1187            for (int i = 0; i < currPathCnt; i++) {
1188                if (isSet[i] && isActive[i]) {  //only consider active reachable blocks
1189                    log.debug("comparing {} ({}) to {} ({})",
1190                            pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]),
1191                            getDisplayName(), Path.decodeDirection(pFromDir[i]));
1192                    // Use bitwise comparison to support combination directions such as "North, West"
1193                    if ((pDir[i] & pFromDir[i]) > 0) {
1194                        count++;
1195                        next = pList[i];
1196                    }
1197                }
1198            }
1199            if (next == null) {
1200                log.debug("next is null!");
1201            }
1202            if (next != null && count == 1) {
1203                // found one block with proper direction, assume that
1204            } else {
1205                // no unique path with correct direction - this happens frequently from noise in block detectors!!
1206                log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}",
1207                    count, getDisplayName());
1208            }
1209        }
1210        // in any case, go OCCUPIED
1211        if (log.isDebugEnabled()) { // avoid potentially expensive non-logging
1212            log.debug("Block {} with direction {} gets new value from {} + (informational. No state change)",
1213                getDisplayName(), Path.decodeDirection(getDirection()),
1214                (next != null ? next.getBlock().getDisplayName() : "(no next block)"));
1215        }
1216        return next;
1217    }
1218
1219    /**
1220     * This allows the layout block to inform any listeners to the block
1221     * that the higher level layout block has been set to "useExtraColor" which is an
1222     * indication that it has been allocated to a section by the AutoDispatcher.
1223     * The value set is not retained in any form by the block,
1224     * it is purely to trigger a propertyChangeEvent.
1225     * @param boo Allocation status
1226     */
1227    public void setAllocated(Boolean boo) {
1228        firePropertyChange("allocated", !boo, boo);
1229    }
1230
1231    // Methods to implmement PhysicalLocationReporter Interface
1232    //
1233    // If we have a Reporter that is also a PhysicalLocationReporter,
1234    // we will defer to that Reporter's methods.
1235    // Else we will assume a LocoNet style message to be parsed.
1236
1237    /**
1238     * Parse a given string and return the LocoAddress value that is presumed
1239     * stored within it based on this object's protocol. The Class Block
1240     * implementation defers to its associated Reporter, if it exists.
1241     *
1242     * @param rep String to be parsed
1243     * @return LocoAddress address parsed from string, or null if this Block
1244     *         isn't associated with a Reporter, or is associated with a
1245     *         Reporter that is not also a PhysicalLocationReporter
1246     */
1247    @Override
1248    public LocoAddress getLocoAddress(String rep) {
1249        // Defer parsing to our associated Reporter if we can.
1250        if (rep == null) {
1251            log.warn("String input is null!");
1252            return null;
1253        }
1254        Reporter testReporter = this.getReporter();
1255        if ( testReporter instanceof PhysicalLocationReporter ) {
1256            return ((PhysicalLocationReporter)testReporter).getLocoAddress(rep);
1257        } else {
1258            // Assume a LocoNet-style report.  This is (nascent) support for handling of Faller cars
1259            // for Dave Merrill's project.
1260            log.debug("report string: {}", rep);
1261            // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter
1262            // Match a number followed by the word "enter".  This is the LocoNet pattern.
1263            Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");
1264            Matcher m = lnp.matcher(rep);
1265            if (m.find()) {
1266                log.debug("Parsed address: {}", m.group(1));
1267                return new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC);
1268            } else {
1269                return null;
1270            }
1271        }
1272    }
1273
1274    /**
1275     * Parses out a (possibly old) LnReporter-generated report string to extract
1276     * the direction from within it based on this object's protocol. The Class
1277     * Block implementation defers to its associated Reporter, if it exists.
1278     *
1279     * @param rep String to be parsed
1280     * @return PhysicalLocationReporter.Direction direction parsed from string,
1281     *         or null if this Block isn't associated with a Reporter, or is
1282     *         associated with a Reporter that is not also a
1283     *         PhysicalLocationReporter
1284     */
1285    @Override
1286    public PhysicalLocationReporter.Direction getDirection(String rep) {
1287        if (rep == null) {
1288            log.warn("String input is null!");
1289            return (null);
1290        }
1291        // Defer parsing to our associated Reporter if we can.
1292        Reporter testReporter = this.getReporter();
1293        if ( testReporter instanceof PhysicalLocationReporter ) {
1294            return ((PhysicalLocationReporter)testReporter).getDirection(rep);
1295        } else {
1296            log.debug("report string: {}", rep);
1297            // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter
1298            // Match a number followed by the word "enter".  This is the LocoNet pattern.
1299            Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");
1300            Matcher m = lnp.matcher(rep);
1301            if (m.find()) {
1302                log.debug("Parsed direction: {}", m.group(2));
1303                switch (m.group(2)) {
1304                    case "enter":
1305                        // LocoNet Enter message
1306                        return PhysicalLocationReporter.Direction.ENTER;
1307                    case "seen":
1308                        // Lissy message.  Treat them all as "entry" messages.
1309                        return PhysicalLocationReporter.Direction.ENTER;
1310                    default:
1311                        return PhysicalLocationReporter.Direction.EXIT;
1312                }
1313            } else {
1314                return PhysicalLocationReporter.Direction.UNKNOWN;
1315            }
1316        }
1317    }
1318
1319    /**
1320     * Return this Block's physical location, if it exists.
1321     * Defers actual work to the helper methods in class PhysicalLocation.
1322     *
1323     * @return PhysicalLocation : this Block's location.
1324     */
1325    @Override
1326    public PhysicalLocation getPhysicalLocation() {
1327        // We have our won PhysicalLocation. That's the point.  No need to defer to the Reporter.
1328        return PhysicalLocation.getBeanPhysicalLocation(this);
1329    }
1330
1331    /**
1332     * Return this Block's physical location, if it exists.
1333     * Does not use the parameter s.
1334     * Defers actual work to the helper methods in class PhysicalLocation
1335     *
1336     * @param s (this parameter is ignored)
1337     * @return PhysicalLocation : this Block's location.
1338     */
1339    @Override
1340    public PhysicalLocation getPhysicalLocation(String s) {
1341        // We have our won PhysicalLocation. That's the point.  No need to defer to the Reporter.
1342        // Intentionally ignore the String s
1343        return PhysicalLocation.getBeanPhysicalLocation(this);
1344    }
1345
1346    @Override
1347    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
1348        if ("CanDelete".equals(evt.getPropertyName())) { // No I18N
1349            if (evt.getOldValue() instanceof Sensor
1350                && evt.getOldValue().equals(getSensor())) {
1351                throw new PropertyVetoException(getDisplayName(), evt);
1352            }
1353            if (evt.getOldValue() instanceof Reporter
1354                && evt.getOldValue().equals(getReporter())) {
1355                throw new PropertyVetoException(getDisplayName(), evt);
1356            }
1357        } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
1358            if (evt.getOldValue() instanceof Sensor
1359                && evt.getOldValue().equals(getSensor())) {
1360                setSensor(null);
1361            }
1362            if (evt.getOldValue() instanceof Reporter
1363                && evt.getOldValue().equals(getReporter())) {
1364                setReporter(null);
1365            }
1366        }
1367    }
1368
1369    @Override
1370    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
1371        List<NamedBeanUsageReport> report = new ArrayList<>();
1372        if (bean != null) {
1373            if (bean.equals(getSensor())) {
1374                report.add(new NamedBeanUsageReport("BlockSensor"));  // NOI18N
1375            }
1376            if (bean.equals(getReporter())) {
1377                report.add(new NamedBeanUsageReport("BlockReporter"));  // NOI18N
1378            }
1379            // Block paths
1380            getPaths().forEach( path -> {
1381                if (bean.equals(path.getBlock())) {
1382                    report.add(new NamedBeanUsageReport("BlockPathNeighbor"));  // NOI18N
1383                }
1384                path.getSettings().forEach( setting -> {
1385                    if (bean.equals(setting.getBean())) {
1386                        report.add(new NamedBeanUsageReport("BlockPathTurnout"));  // NOI18N
1387                    }
1388                });
1389            });
1390        }
1391        return report;
1392    }
1393
1394    @Override
1395    public String getBeanType() {
1396        return Bundle.getMessage("BeanNameBlock");
1397    }
1398
1399    /** {@inheritDoc} */
1400    @Override
1401    @Nonnull
1402    public String describeState(int state) {
1403        switch (state) {
1404            case Block.OCCUPIED:
1405                return Bundle.getMessage("BlockOccupied");
1406            case Block.UNOCCUPIED:
1407                return Bundle.getMessage("BlockUnOccupied");
1408            case Block.UNDETECTED:
1409                return Bundle.getMessage("BlockUndetected");
1410            default:  // state unknown, state inconsistent, state unexpected
1411                return super.describeState(state);
1412        }
1413    }
1414
1415    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Block.class);
1416}