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, 2026
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    /**
900     * Take the value of a neighbour Block that has just been emptied, if this
901     * Block could not decide between its occupied neighbours when it became
902     * occupied and that neighbour was one of them. The value is only taken
903     * while this Block is still occupied.
904     *
905     * @param b the neighbour Block that has become unoccupied
906     * @return true if the value of b was taken
907     */
908    boolean setAsEntryBlockIfPossible(Block b) {
909        if (getState() != OCCUPIED) {
910            // this Block has been emptied since, the train that entered it has gone
911            resetCandidateEntrancePaths();
912            return false;
913        }
914        for (int i = 0; i < cntOfPossibleEntrancePaths; i++) {
915            Block candidateBlock = pListOfPossibleEntrancePaths[i].getBlock();
916            if (candidateBlock == b) {
917                setValue(candidateBlock.getValue());
918                setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection());
919                log.info("Block {} gets LATE new value from {}, direction= {}",
920                    getDisplayName(), candidateBlock.getDisplayName(), Path.decodeDirection(getDirection()));
921                resetCandidateEntrancePaths();
922                return true;
923            }
924        }
925        return false;
926    }
927
928    /**
929     * Handle change in sensor state.
930     * <p>
931     * Defers real work to goingActive, goingInactive methods.
932     *
933     * @param e the event
934     */
935    void handleSensorChange(PropertyChangeEvent e) {
936        Sensor s = getSensor();
937        if ( Sensor.PROPERTY_KNOWN_STATE.equals( e.getPropertyName()) && s != null ) {
938            int state = s.getState();
939            switch (state) {
940                case Sensor.ACTIVE:
941                    goingActive();
942                    break;
943                case Sensor.INACTIVE:
944                    goingInactive();
945                    break;
946                case Sensor.UNKNOWN:
947                    goingUnknown();
948                    break;
949                default:
950                    goingInconsistent();
951                    break;
952            }
953        }
954    }
955
956    /**
957     * Handles Block sensor going UNKNOWN: the value is cleared.
958     * <p>
959     * The time is recorded as for a Block going unoccupied, so that the value
960     * is restored if the sensor goes active again within a short time, for
961     * example after a brief loss of the connection.
962     */
963    public void goingUnknown() {
964        setValue(null);
965        setState(UNKNOWN);
966        _timeLastInactive = Instant.now();
967    }
968
969    /**
970     * Handles Block sensor going INCONSISTENT: the value is cleared.
971     * <p>
972     * The time is recorded as for a Block going unoccupied, so that the value
973     * is restored if the sensor goes active again within a short time.
974     */
975    public void goingInconsistent() {
976        setValue(null);
977        setState(INCONSISTENT);
978        _timeLastInactive = Instant.now();
979    }
980
981    /**
982     * Handle change in Reporter value.
983     *
984     * @param e PropertyChangeEvent
985     */
986    void handleReporterChange(PropertyChangeEvent e) {
987        if ((_reportingCurrent && Reporter.PROPERTY_CURRENT_REPORT.equals(e.getPropertyName()))
988            || (!_reportingCurrent && Reporter.PROPERTY_LAST_REPORT.equals(e.getPropertyName()))) {
989            setValue(e.getNewValue());
990        }
991    }
992
993    private Instant _timeLastInactive;
994
995    /**
996     * Handles Block sensor going INACTIVE: this block is empty
997     */
998    public void goingInactive() {
999        log.debug("Block {} goes UNOCCUPIED", getDisplayName());
1000        for (Path path : paths) {
1001            Block b = path.getBlock();
1002            if (b != null) {
1003                b.setAsEntryBlockIfPossible(this);
1004            }
1005        }
1006        // the train that could not be identified when it entered has left
1007        resetCandidateEntrancePaths();
1008        setValue(null);
1009        setDirection(Path.NONE);
1010        setState(UNOCCUPIED);
1011        _timeLastInactive = Instant.now();
1012    }
1013
1014    private static final int MAXINFOMESSAGES = 5;
1015    private int infoMessageCount = 0;
1016
1017    /**
1018     * Handles Block sensor going ACTIVE: this block is now occupied, figure out
1019     * from who and copy their value.
1020     */
1021    public void goingActive() {
1022        if (getState() == OCCUPIED) {
1023            return;
1024        }
1025        log.debug("Block {} goes OCCUPIED", getDisplayName());
1026        resetCandidateEntrancePaths();
1027        // index through the paths, counting
1028        int count = 0;
1029        Path next = null;
1030        // get statuses of everything once
1031        int currPathCnt = paths.size();
1032        Path[] pList = new Path[currPathCnt];
1033        boolean[] isSet = new boolean[currPathCnt];
1034        boolean[] isActive = new boolean[currPathCnt];
1035        int[] pDir = new int[currPathCnt];
1036        int[] pFromDir = new int[currPathCnt];
1037        for (int i = 0; i < currPathCnt; i++) {
1038            pList[i] = paths.get(i);
1039            isSet[i] = pList[i].checkPathSet();
1040            Block b = pList[i].getBlock();
1041            if (b != null) {
1042                isActive[i] = b.getState() == OCCUPIED;
1043                pDir[i] = b.getDirection();
1044            } else {
1045                isActive[i] = false;
1046                pDir[i] = -1;
1047            }
1048            pFromDir[i] = pList[i].getFromBlockDirection();
1049            if (isSet[i] && isActive[i]) {
1050                count++;
1051                next = pList[i];
1052            }
1053        }
1054        // sort on number of neighbors
1055        switch (count) {
1056            case 0:
1057                if (null != _previousValue) {
1058                    // restore the previous value under either of these circumstances:
1059                    // 1. the block has been 'unoccupied' only very briefly
1060                    // 2. power has just come back on
1061                    Instant tn = Instant.now();
1062                    BlockManager bm = InstanceManager.getDefault(BlockManager.class);
1063                    if ( bm.timeSinceLastLayoutPowerOn() < 5000 ||
1064                        (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) {
1065                        setValue(_previousValue);
1066                        if (infoMessageCount < MAXINFOMESSAGES) {
1067                            log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}."
1068                                +" Restoring previous value.", getDisplayName());
1069                            infoMessageCount++;
1070                        }
1071                    } else if (log.isDebugEnabled()) {
1072                        if (null != _timeLastInactive) {
1073                            log.debug("not restoring previous value, block {} has been inactive for too long ({}ms)"
1074                                + " and layout power has not just been restored ({}ms ago)",
1075                                getDisplayName(), tn.toEpochMilli() - _timeLastInactive.toEpochMilli(),
1076                                bm.timeSinceLastLayoutPowerOn());
1077                        } else {
1078                            log.debug("not restoring previous value, block {} has been inactive since the "
1079                                + "start of this session and layout power has not just been restored ({}ms ago)",
1080                                getDisplayName(), bm.timeSinceLastLayoutPowerOn());
1081                        }
1082                    }
1083                } else {
1084                    if (infoMessageCount < MAXINFOMESSAGES) {
1085                        log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.",
1086                            getDisplayName());
1087                        infoMessageCount++;
1088                    }
1089                }
1090                break;
1091            case 1:
1092                // simple case
1093                if ((next != null) && (next.getBlock() != null)) {
1094                    // normal case, transfer value object
1095                    setValue(next.getBlock().getValue());
1096                    setDirection(next.getFromBlockDirection());
1097                    log.debug("Block {} gets new value '{}' from {}, direction={}",
1098                            getDisplayName(),
1099                            next.getBlock().getValue(),
1100                            next.getBlock().getDisplayName(),
1101                            Path.decodeDirection(getDirection()));
1102                } else if (next == null) {
1103                    log.error("unexpected next==null processing block {}", getDisplayName());
1104                } else {
1105                    log.error("unexpected next.getBlock()=null processing block {}", getDisplayName());
1106                }
1107                break;
1108            default:
1109                // count > 1, check for one with proper direction
1110                // this time, count ones with proper direction
1111                log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count);
1112                next = null;
1113                count = 0;
1114                // true until it's found that some neighbor blocks contain different contents (trains)
1115                boolean allNeighborsAgree = true;
1116
1117                // scan for neighbors without matching direction
1118                for (int i = 0; i < currPathCnt; i++) {
1119                    if (isSet[i] && isActive[i]) {  //only consider active reachable blocks
1120                        log.debug("comparing {} ({}) to {} ({})",
1121                                pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]),
1122                                getDisplayName(), Path.decodeDirection(pFromDir[i]));
1123                        //use bitwise comparison to support combination directions such as "North, West"
1124                        if ((pDir[i] & pFromDir[i]) > 0) {
1125                            if (next != null  && next.getBlock() != null ) {
1126                                Object value = next.getBlock().getValue();
1127                                if ( value != null && !value.equals(pList[i].getBlock().getValue())) {
1128                                    allNeighborsAgree = false;
1129                                }
1130                            }
1131                            count++;
1132                            next = pList[i];
1133                        }
1134                    }
1135                }
1136
1137                // If loop above didn't find neighbors with matching direction, scan w/out direction for neighbors
1138                // This is used when directions are not being used
1139                if (next == null) {
1140                    for (int i = 0; i < currPathCnt; i++) {
1141                        if (isSet[i] && isActive[i]) {
1142                            if (next != null && next.getBlock() != null ) {
1143                                Object value = next.getBlock().getValue();
1144                                if ( value != null && ! value.equals(pList[i].getBlock().getValue())) {
1145                                    allNeighborsAgree = false;
1146                                }
1147                            }
1148                            count++;
1149                            next = pList[i];
1150                        }
1151                    }
1152                }
1153
1154                if (next != null && count == 1) {
1155                    // found one block with proper direction, use it
1156                    setValue(next.getBlock().getValue());
1157                    setDirection(next.getFromBlockDirection());
1158                    log.debug("Block {} gets new value '{}' from {}, direction {}",
1159                            getDisplayName(), next.getBlock().getValue(),
1160                            next.getBlock().getDisplayName(), Path.decodeDirection(getDirection()));
1161                } else {
1162                    // handle merging trains: All neighbors with same content (train ID)
1163                    if (allNeighborsAgree && next != null) {
1164                        setValue(next.getBlock().getValue());
1165                        setDirection(next.getFromBlockDirection());
1166                    } else {
1167                    // don't all agree, so can't determine unique value
1168                        log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for"
1169                            + " block {} but maybe it can be determined when another block becomes free",
1170                            count, getDisplayName());
1171                        pListOfPossibleEntrancePaths = new Path[currPathCnt];
1172                        cntOfPossibleEntrancePaths = 0;
1173                        for (int i = 0; i < currPathCnt; i++) {
1174                            if (isSet[i] && isActive[i]) {
1175                                pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i];
1176                                cntOfPossibleEntrancePaths++;
1177                            }
1178                        }
1179                    }
1180                }
1181                break;
1182        }
1183        setState(OCCUPIED);
1184    }
1185
1186    /**
1187     * Find which path this Block became Active, without actually modifying the
1188     * state of this block.
1189     * <p>
1190     * (this is largely a copy of the 'Search' part of the logic from
1191     * goingActive())
1192     *
1193     * @return the next path
1194     */
1195    @CheckForNull
1196    public Path findFromPath() {
1197        // index through the paths, counting
1198        int count = 0;
1199        Path next = null;
1200        // get statuses of everything once
1201        int currPathCnt = paths.size();
1202        Path[] pList = new Path[currPathCnt];
1203        boolean[] isSet = new boolean[currPathCnt];
1204        boolean[] isActive = new boolean[currPathCnt];
1205        int[] pDir = new int[currPathCnt];
1206        int[] pFromDir = new int[currPathCnt];
1207        for (int i = 0; i < currPathCnt; i++) {
1208            pList[i] = paths.get(i);
1209            isSet[i] = pList[i].checkPathSet();
1210            Block b = pList[i].getBlock();
1211            if (b != null) {
1212                isActive[i] = b.getState() == OCCUPIED;
1213                pDir[i] = b.getDirection();
1214            } else {
1215                isActive[i] = false;
1216                pDir[i] = -1;
1217            }
1218            pFromDir[i] = pList[i].getFromBlockDirection();
1219            if (isSet[i] && isActive[i]) {
1220                count++;
1221                next = pList[i];
1222            }
1223        }
1224        // sort on number of neighbors
1225        if ((count == 0) || (count == 1)) {
1226            // do nothing.  OK to return null from this function.  "next" is already set.
1227        } else {
1228            // count > 1, check for one with proper direction
1229            // this time, count ones with proper direction
1230            log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count);
1231            next = null;
1232            count = 0;
1233            for (int i = 0; i < currPathCnt; i++) {
1234                if (isSet[i] && isActive[i]) {  //only consider active reachable blocks
1235                    log.debug("comparing {} ({}) to {} ({})",
1236                            pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]),
1237                            getDisplayName(), Path.decodeDirection(pFromDir[i]));
1238                    // Use bitwise comparison to support combination directions such as "North, West"
1239                    if ((pDir[i] & pFromDir[i]) > 0) {
1240                        count++;
1241                        next = pList[i];
1242                    }
1243                }
1244            }
1245            if (next == null) {
1246                log.debug("next is null!");
1247            }
1248            if (next != null && count == 1) {
1249                // found one block with proper direction, assume that
1250            } else {
1251                // no unique path with correct direction - this happens frequently from noise in block detectors!!
1252                log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}",
1253                    count, getDisplayName());
1254            }
1255        }
1256        // in any case, go OCCUPIED
1257        if (log.isDebugEnabled()) { // avoid potentially expensive non-logging
1258            log.debug("Block {} with direction {} gets new value from {} + (informational. No state change)",
1259                getDisplayName(), Path.decodeDirection(getDirection()),
1260                (next != null ? next.getBlock().getDisplayName() : "(no next block)"));
1261        }
1262        return next;
1263    }
1264
1265    /**
1266     * This allows the layout block to inform any listeners to the block
1267     * that the higher level layout block has been set to "useExtraColor" which is an
1268     * indication that it has been allocated to a section by the AutoDispatcher.
1269     * The value set is not retained in any form by the block,
1270     * it is purely to trigger a propertyChangeEvent.
1271     * @param boo Allocation status
1272     */
1273    public void setAllocated(Boolean boo) {
1274        firePropertyChange(PROPERTY_ALLOCATED, !boo, boo);
1275    }
1276
1277    // Methods to implmement PhysicalLocationReporter Interface
1278    //
1279    // If we have a Reporter that is also a PhysicalLocationReporter,
1280    // we will defer to that Reporter's methods.
1281    // Else we will assume a LocoNet style message to be parsed.
1282
1283    /**
1284     * Parse a given string and return the LocoAddress value that is presumed
1285     * stored within it based on this object's protocol. The Class Block
1286     * implementation defers to its associated Reporter, if it exists.
1287     *
1288     * @param rep String to be parsed
1289     * @return LocoAddress address parsed from string, or null if this Block
1290     *         isn't associated with a Reporter, or is associated with a
1291     *         Reporter that is not also a PhysicalLocationReporter
1292     */
1293    @Override
1294    public LocoAddress getLocoAddress(String rep) {
1295        // Defer parsing to our associated Reporter if we can.
1296        if (rep == null) {
1297            log.warn("String input is null!");
1298            return null;
1299        }
1300        Reporter testReporter = this.getReporter();
1301        if ( testReporter instanceof PhysicalLocationReporter ) {
1302            return ((PhysicalLocationReporter)testReporter).getLocoAddress(rep);
1303        } else {
1304            // Assume a LocoNet-style report.  This is (nascent) support for handling of Faller cars
1305            // for Dave Merrill's project.
1306            log.debug("report string: {}", rep);
1307            // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter
1308            // Match a number followed by the word "enter".  This is the LocoNet pattern.
1309            Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");
1310            Matcher m = lnp.matcher(rep);
1311            if (m.find()) {
1312                log.debug("Parsed address: {}", m.group(1));
1313                return new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC);
1314            } else {
1315                return null;
1316            }
1317        }
1318    }
1319
1320    /**
1321     * Parses out a (possibly old) LnReporter-generated report string to extract
1322     * the direction from within it based on this object's protocol. The Class
1323     * Block implementation defers to its associated Reporter, if it exists.
1324     *
1325     * @param rep String to be parsed
1326     * @return PhysicalLocationReporter.Direction direction parsed from string,
1327     *         or null if this Block isn't associated with a Reporter, or is
1328     *         associated with a Reporter that is not also a
1329     *         PhysicalLocationReporter
1330     */
1331    @Override
1332    public PhysicalLocationReporter.Direction getDirection(String rep) {
1333        if (rep == null) {
1334            log.warn("String input is null!");
1335            return (null);
1336        }
1337        // Defer parsing to our associated Reporter if we can.
1338        Reporter testReporter = this.getReporter();
1339        if ( testReporter instanceof PhysicalLocationReporter ) {
1340            return ((PhysicalLocationReporter)testReporter).getDirection(rep);
1341        } else {
1342            log.debug("report string: {}", rep);
1343            // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter
1344            // Match a number followed by the word "enter".  This is the LocoNet pattern.
1345            Pattern lnp = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");
1346            Matcher m = lnp.matcher(rep);
1347            if (m.find()) {
1348                log.debug("Parsed direction: {}", m.group(2));
1349                switch (m.group(2)) {
1350                    case "enter":
1351                        // LocoNet Enter message
1352                        return PhysicalLocationReporter.Direction.ENTER;
1353                    case "seen":
1354                        // Lissy message.  Treat them all as "entry" messages.
1355                        return PhysicalLocationReporter.Direction.ENTER;
1356                    default:
1357                        return PhysicalLocationReporter.Direction.EXIT;
1358                }
1359            } else {
1360                return PhysicalLocationReporter.Direction.UNKNOWN;
1361            }
1362        }
1363    }
1364
1365    /**
1366     * Return this Block's physical location, if it exists.
1367     * Defers actual work to the helper methods in class PhysicalLocation.
1368     *
1369     * @return PhysicalLocation : this Block's location.
1370     */
1371    @Override
1372    public PhysicalLocation getPhysicalLocation() {
1373        // We have our own PhysicalLocation. That's the point.  No need to defer to the Reporter.
1374        return PhysicalLocation.getBeanPhysicalLocation(this);
1375    }
1376
1377    /**
1378     * Return this Block's physical location, if it exists.
1379     * Does not use the parameter s.
1380     * Defers actual work to the helper methods in class PhysicalLocation
1381     *
1382     * @param s (this parameter is ignored)
1383     * @return PhysicalLocation : this Block's location.
1384     */
1385    @Override
1386    public PhysicalLocation getPhysicalLocation(String s) {
1387        // We have our own PhysicalLocation. That's the point.  No need to defer to the Reporter.
1388        // Intentionally ignore the String s
1389        return PhysicalLocation.getBeanPhysicalLocation(this);
1390    }
1391
1392    @Override
1393    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
1394        if (Manager.PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) {
1395            if (evt.getOldValue() instanceof Sensor
1396                && evt.getOldValue().equals(getSensor())) {
1397                throw new PropertyVetoException(getDisplayName(), evt);
1398            }
1399            if (evt.getOldValue() instanceof Reporter
1400                && evt.getOldValue().equals(getReporter())) {
1401                throw new PropertyVetoException(getDisplayName(), evt);
1402            }
1403        } else if (Manager.PROPERTY_DO_DELETE.equals(evt.getPropertyName())) {
1404            if (evt.getOldValue() instanceof Sensor
1405                && evt.getOldValue().equals(getSensor())) {
1406                setSensor(null);
1407            }
1408            if (evt.getOldValue() instanceof Reporter
1409                && evt.getOldValue().equals(getReporter())) {
1410                setReporter(null);
1411            }
1412        }
1413    }
1414
1415    @Override
1416    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
1417        List<NamedBeanUsageReport> report = new ArrayList<>();
1418        if (bean != null) {
1419            if (bean.equals(getSensor())) {
1420                report.add(new NamedBeanUsageReport("BlockSensor"));  // NOI18N
1421            }
1422            if (bean.equals(getReporter())) {
1423                report.add(new NamedBeanUsageReport("BlockReporter"));  // NOI18N
1424            }
1425            // Block paths
1426            getPaths().forEach( path -> {
1427                if (bean.equals(path.getBlock())) {
1428                    report.add(new NamedBeanUsageReport("BlockPathNeighbor"));  // NOI18N
1429                }
1430                path.getSettings().forEach( setting -> {
1431                    if (bean.equals(setting.getBean())) {
1432                        report.add(new NamedBeanUsageReport("BlockPathTurnout"));  // NOI18N
1433                    }
1434                });
1435            });
1436        }
1437        return report;
1438    }
1439
1440    @Override
1441    public String getBeanType() {
1442        return Bundle.getMessage("BeanNameBlock");
1443    }
1444
1445    /** {@inheritDoc} */
1446    @Override
1447    @Nonnull
1448    public String describeState(int state) {
1449        switch (state) {
1450            case Block.OCCUPIED:
1451                return Bundle.getMessage("BlockOccupied");
1452            case Block.UNOCCUPIED:
1453                return Bundle.getMessage("BlockUnOccupied");
1454            case Block.UNDETECTED:
1455                return Bundle.getMessage("BlockUndetected");
1456            default:  // state unknown, state inconsistent, state unexpected
1457                return super.describeState(state);
1458        }
1459    }
1460
1461    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Block.class);
1462}