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.regex.Matcher;
010import java.util.regex.Pattern;
011import javax.annotation.Nonnull;
012import jmri.implementation.AbstractNamedBean;
013import jmri.implementation.SignalSpeedMap;
014import jmri.util.PhysicalLocation;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Represents a particular piece of track, more informally a "Block".
020 * <p>
021 * A Block (at least in this implementation) corresponds exactly to the track
022 * covered by at most one sensor. That could be generalized in the future.
023 * <p>
024 * As trains move around the layout, a set of Block objects that are attached to
025 * sensors can interact to keep track of which train is where, going in which
026 * direction. As a result of this, the set of Block objects pass around "token"
027 * (value) Objects representing the trains. This could be e.g. a Throttle to
028 * control the train, or something else.
029 * <p>
030 * A block maintains a "direction" flag that is set from the direction of the
031 * incoming train. When an arriving train is detected via the connected sensor
032 * and the Block's status information is sufficient to determine that it is
033 * arriving via a particular Path, that Path's getFromBlockDirection becomes the
034 * direction of the train in this Block. This only works
035 * <p>
036 * Optionally, a Block can be associated with a Reporter. In this case, the
037 * Reporter will provide the Block with the "token" (value). This could be e.g
038 * an RFID reader reading an ID tag attached to a locomotive. Depending on the
039 * specific Reporter implementation, either the current reported value or the
040 * last reported value will be relevant - this can be configured
041 * <p>
042 * Objects of this class are Named Beans, so can be manipulated through tables,
043 * have listeners, etc.
044 * <p>
045 * There is no functional requirement for a type letter in the System Name, but
046 * by convention we use 'B' for 'Block'. The default implementation is not
047 * system-specific, so a system letter of 'I' is appropriate. This leads to
048 * system names like "IB201".
049 * <p>
050 * Issues:
051 * <ul>
052 * <li>The tracking doesn't handle a train pulling in behind another well:
053 * <ul>
054 * <li>When the 2nd train arrives, the Sensor is already active, so the value is
055 * unchanged (but the value can only be a single object anyway)
056 * <li>When the 1st train leaves, the Sensor stays active, so the value remains
057 * that of the 1st train
058 * </ul>
059 * <li> The assumption is that a train will only go through a set turnout. For
060 * example, a train could come into the turnout block from the main even if the
061 * turnout is set to the siding. (Ignoring those layouts where this would cause
062 * a short; it doesn't do so on all layouts)
063 * <li> Does not handle closely-following trains where there is only one
064 * electrical block per signal. To do this, it probably needs some type of
065 * "assume a train doesn't back up" logic. A better solution is to have multiple
066 * sensors and Block objects between each signal head.
067 * <li> If a train reverses in a block and goes back the way it came (e.g. b1 to
068 * b2 to b1), the block that's re-entered will get an updated direction, but the
069 * direction of this block (b2 in the example) is not updated. In other words,
070 * we're not noticing that the train must have reversed to go back out.
071 * </ul>
072 * <p>
073 * Do not assume that a Block object uniquely represents a piece of track. To
074 * allow independent development, it must be possible for multiple Block objects
075 * to take care of a particular section of track.
076 * <p>
077 * Possible state values:
078 * <ul>
079 * <li>UNKNOWN - The sensor shows UNKNOWN, so this block doesn't know if it's
080 * occupied or not.
081 * <li>INCONSISTENT - The sensor shows INCONSISTENT, so this block doesn't know
082 * if it's occupied or not.
083 * <li>OCCUPIED - This sensor went active. Note that OCCUPIED will be set even
084 * if the logic is unable to figure out which value to take.
085 * <li>UNOCCUPIED - No content, because the sensor has determined this block is
086 * unoccupied.
087 * <li>UNDETECTED - No sensor configured.
088 * </ul>
089 * <p>
090 * Possible Curvature attributes (optional) User can set the curvature if
091 * desired. For use in automatic running of trains, to indicate where slow down
092 * is required.
093 * <ul>
094 * <li>NONE - No curvature in Block track, or Not entered.
095 * <li>GRADUAL - Gradual curve - no action by engineer is warranted - full speed
096 * OK
097 * <li>TIGHT - Tight curve in Block track - Train should slow down some
098 * <li>SEVERE - Severe curve in Block track - Train should slow down a lot
099 * </ul>
100 * <p>
101 * The length of the block may also optionally be entered if desired. This
102 * attribute is for use in automatic running of trains. Length should be the
103 * actual length of model railroad track in the block. It is always stored here
104 * in millimeter units. A length of 0.0 indicates no entry of length by the
105 * user.
106 *
107 * @author Bob Jacobsen Copyright (C) 2006, 2008, 2014
108 * @author Dave Duchamp Copywright (C) 2009
109 */
110public class Block extends AbstractNamedBean implements PhysicalLocationReporter {
111
112    public Block(String systemName) {
113        super(systemName);
114    }
115
116    public Block(String systemName, String userName) {
117        super(systemName, userName);
118    }
119
120    static final public int OCCUPIED = Sensor.ACTIVE;
121    static final public int UNOCCUPIED = Sensor.INACTIVE;
122    // why isn't UNDETECTED == NamedBean.UNKNOWN?
123    static final public int UNDETECTED = 0x100;  // bit coded, just in case; really should be enum
124
125    // Curvature attributes
126    static final public int NONE = 0x00;
127    static final public int GRADUAL = 0x01;
128    static final public int TIGHT = 0x02;
129    static final public int SEVERE = 0x04;
130
131    // this should only be used for debugging...
132    public String toDebugString() {
133        String result = getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME) + " ";
134        switch (getState()) {
135            case UNDETECTED: {
136                result += "UNDETECTED";
137                break;
138            }
139            case UNOCCUPIED: {
140                result += "UNOCCUPIED";
141                break;
142            }
143            case OCCUPIED: {
144                result += "OCCUPIED";
145                break;
146            }
147            default: {
148                result += "unknown " + getState();
149                break;
150            }
151        }
152        return result;
153    }
154
155    /**
156     * Set the sensor by name.
157     *
158     * @param pName the name of the Sensor to set
159     * @return true if a Sensor is set and is not null; false otherwise
160     */
161    public boolean setSensor(String pName) {
162        if (pName == null || pName.equals("")) {
163            setNamedSensor(null);
164            return false;
165        }
166        if (InstanceManager.getNullableDefault(SensorManager.class) != null) {
167            try {
168                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
169                setNamedSensor(InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
170                return true;
171            } catch (IllegalArgumentException ex) {
172                setNamedSensor(null);
173                log.error("Sensor '{}' not available", pName);
174            }
175        } else {
176            log.error("No SensorManager for this protocol");
177        }
178        return false;
179    }
180
181    public void setNamedSensor(NamedBeanHandle<Sensor> s) {
182        if (_namedSensor != null) {
183            if (_sensorListener != null) {
184                getSensor().removePropertyChangeListener(_sensorListener);
185                _sensorListener = null;
186            }
187        }
188        _namedSensor = s;
189
190        if (_namedSensor != null) {
191            getSensor().addPropertyChangeListener(_sensorListener = (PropertyChangeEvent e) -> {
192                handleSensorChange(e);
193            }, s.getName(), "Block Sensor " + getDisplayName());
194            _current = getSensor().getState();
195        } else {
196            _current = UNDETECTED;
197        }
198    }
199
200    public Sensor getSensor() {
201        if (_namedSensor != null) {
202            return _namedSensor.getBean();
203        }
204        return null;
205    }
206
207    public NamedBeanHandle<Sensor> getNamedSensor() {
208        return _namedSensor;
209    }
210
211    /**
212     * Set the Reporter that should provide the data value for this block.
213     *
214     * @see Reporter
215     * @param reporter Reporter object to link, or null to clear
216     */
217    public void setReporter(Reporter reporter) {
218        if (_reporter != null) {
219            // remove reporter listener
220            if (_reporterListener != null) {
221                _reporter.removePropertyChangeListener(_reporterListener);
222                _reporterListener = null;
223            }
224        }
225        _reporter = reporter;
226        if (_reporter != null) {
227            // attach listener
228            _reporter.addPropertyChangeListener(_reporterListener = (PropertyChangeEvent e) -> {
229                handleReporterChange(e);
230            });
231        }
232    }
233
234    /**
235     * Retrieve the Reporter that is linked to this Block
236     *
237     * @see Reporter
238     * @return linked Reporter object, or null if not linked
239     */
240    public Reporter getReporter() {
241        return _reporter;
242    }
243
244    /**
245     * Define if the Block's value should be populated from the
246     * {@link Reporter#getCurrentReport() current report} or from the
247     * {@link Reporter#getLastReport() last report}.
248     *
249     * @see Reporter
250     * @param reportingCurrent true if to use current report; false if to use
251     *                         last report
252     */
253    public void setReportingCurrent(boolean reportingCurrent) {
254        _reportingCurrent = reportingCurrent;
255    }
256
257    /**
258     * Determine if the Block's value is being populated from the
259     * {@link Reporter#getCurrentReport() current report} or from the
260     * {@link Reporter#getLastReport() last report}.
261     *
262     * @see Reporter
263     * @return true if populated by
264     *         {@link Reporter#getCurrentReport() current report}; false if from
265     *         {@link Reporter#getLastReport() last report}.
266     */
267    public boolean isReportingCurrent() {
268        return _reportingCurrent;
269    }
270
271    @Override
272    public int getState() {
273        return _current;
274    }
275
276    ArrayList<Path> paths = new ArrayList<>();
277
278    public void addPath(Path p) {
279        if (p == null) {
280            throw new IllegalArgumentException("Can't add null path");
281        }
282        paths.add(p);
283    }
284
285    public void removePath(Path p) {
286        int j = -1;
287        for (int i = 0; i < paths.size(); i++) {
288            if (p == paths.get(i)) {
289                j = i;
290            }
291        }
292        if (j > -1) {
293            paths.remove(j);
294        }
295    }
296
297    public boolean hasPath(Path p) {
298        return paths.stream().anyMatch((t) -> (t.equals(p)));
299    }
300
301    /**
302     * Get a copy of the list of Paths.
303     *
304     * @return the paths or an empty list
305     */
306    public List<Path> getPaths() {
307        return new ArrayList<>(paths);
308    }
309
310    /**
311     * Provide a general method for updating the report.
312     *
313     * @param v the new state
314     */
315    @Override
316    public void setState(int v) {
317        int old = _current;
318        _current = v;
319        // notify
320
321        // It is rather unpleasant that the following needs to be done in a try-catch, but exceptions have been observed
322        try {
323            firePropertyChange("state", old, _current);
324        } catch (Exception e) {
325            log.error("{} got exception during firePropertyChange({},{}) in thread {} {}: {}", getDisplayName(), old, _current,
326                    Thread.currentThread().getName(), Thread.currentThread().getId(), e);
327        }
328    }
329
330    /**
331     * Set the value retained by this Block. Also used when the Block itself
332     * gathers a value from an adjacent Block. This can be overridden in a
333     * subclass if e.g. you want to keep track of Blocks elsewhere, but make
334     * sure you also eventually invoke the super.setValue() here.
335     *
336     * @param value The new Object resident in this block, or null if none
337     */
338    public void setValue(Object value) {
339        //ignore if unchanged
340        if (value != _value) {
341            log.debug("Block {} value changed from '{}' to '{}'", getDisplayName(), _value, value);
342            _previousValue = _value;
343            _value = value;
344            firePropertyChange("value", _previousValue, _value);
345        }
346    }
347
348    public Object getValue() {
349        return _value;
350    }
351
352    public void setDirection(int direction) {
353        //ignore if unchanged
354        if (direction != _direction) {
355            log.debug("Block {} direction changed from {} to {}", getDisplayName(), Path.decodeDirection(_direction), Path.decodeDirection(direction));
356            int oldDirection = _direction;
357            _direction = direction;
358            // this is a bound parameter
359            firePropertyChange("direction", oldDirection, direction);
360        }
361    }
362
363    public int getDirection() {
364        return _direction;
365    }
366
367    //Deny traffic entering from this block
368    ArrayList<NamedBeanHandle<Block>> blockDenyList = new ArrayList<>(1);
369
370    /**
371     * The block deny list, is used by higher level code, to determine if
372     * traffic/trains should be allowed to enter from an attached block, the
373     * list only deals with blocks that access should be denied from. If we want
374     * to prevent traffic from following from this block to another then this
375     * block must be added to the deny list of the other block. By default no
376     * block is barred, so traffic flow is bi-directional.
377     *
378     * @param pName name of the block to add, which must exist
379     */
380    public void addBlockDenyList(@Nonnull String pName) {
381        Block blk = InstanceManager.getDefault(BlockManager.class).getBlock(pName);
382        if (blk == null) {
383            throw new IllegalArgumentException("addBlockDenyList requests block \"" + pName + "\" exists");
384        }
385        NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(pName, blk);
386        if (!blockDenyList.contains(namedBlock)) {
387            blockDenyList.add(namedBlock);
388        }
389    }
390
391    public void addBlockDenyList(Block blk) {
392        NamedBeanHandle<Block> namedBlock = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(blk.getDisplayName(), blk);
393        if (!blockDenyList.contains(namedBlock)) {
394            blockDenyList.add(namedBlock);
395        }
396    }
397
398    public void removeBlockDenyList(String blk) {
399        NamedBeanHandle<Block> toremove = null;
400        for (NamedBeanHandle<Block> bean : blockDenyList) {
401            if (bean.getName().equals(blk)) {
402                toremove = bean;
403            }
404        }
405        if (toremove != null) {
406            blockDenyList.remove(toremove);
407        }
408    }
409
410    public void removeBlockDenyList(Block blk) {
411        NamedBeanHandle<Block> toremove = null;
412        for (NamedBeanHandle<Block> bean : blockDenyList) {
413            if (bean.getBean() == blk) {
414                toremove = bean;
415            }
416        }
417        if (toremove != null) {
418            blockDenyList.remove(toremove);
419        }
420    }
421
422    public List<String> getDeniedBlocks() {
423        List<String> list = new ArrayList<>(blockDenyList.size());
424        blockDenyList.forEach((bean) -> {
425            list.add(bean.getName());
426        });
427        return list;
428    }
429
430    public boolean isBlockDenied(String deny) {
431        return blockDenyList.stream().anyMatch((bean) -> (bean.getName().equals(deny)));
432    }
433
434    public boolean isBlockDenied(Block deny) {
435        return blockDenyList.stream().anyMatch((bean) -> (bean.getBean() == deny));
436    }
437
438    public boolean getPermissiveWorking() {
439        return _permissiveWorking;
440    }
441
442    public void setPermissiveWorking(boolean w) {
443        _permissiveWorking = w;
444    }
445    private boolean _permissiveWorking = false;
446
447    public float getSpeedLimit() {
448        if ((_blockSpeed == null) || (_blockSpeed.equals(""))) {
449            return -1;
450        }
451        String speed = _blockSpeed;
452        if (_blockSpeed.equals("Global")) {
453            speed = InstanceManager.getDefault(BlockManager.class).getDefaultSpeed();
454        }
455
456        try {
457            return Float.valueOf(speed);
458        } catch (NumberFormatException nx) {
459            //considered normal if the speed is not a number.
460        }
461        try {
462            return InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
463        } catch (IllegalArgumentException ex) {
464            return -1;
465        }
466    }
467
468    private String _blockSpeed = "";
469
470    public String getBlockSpeed() {
471        if (_blockSpeed.equals("Global")) {
472            return (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed());
473            // Ensure the word "Global" is always in the speed name for later comparison
474        }
475        return _blockSpeed;
476    }
477
478    public void setBlockSpeedName(String s) {
479        if (s == null) {
480            _blockSpeed = "";
481        } else {
482            _blockSpeed = s;
483        }
484    }
485
486    public void setBlockSpeed(String s) throws JmriException {
487        if ((s == null) || (_blockSpeed.equals(s))) {
488            return;
489        }
490        if (s.contains("Global")) {
491            s = "Global";
492        } else {
493            try {
494                Float.parseFloat(s);
495            } catch (NumberFormatException nx) {
496                try {
497                    InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(s);
498                } catch (IllegalArgumentException ex) {
499                    throw new JmriException("Value of requested block speed is not valid");
500                }
501            }
502        }
503        String oldSpeed = _blockSpeed;
504        _blockSpeed = s;
505        firePropertyChange("BlockSpeedChange", oldSpeed, s);
506    }
507
508    public void setCurvature(int c) {
509        _curvature = c;
510    }
511
512    public int getCurvature() {
513        return _curvature;
514    }
515
516    /**
517     * Set length in millimeters.
518     * <p>
519     * Paths will inherit this length, if their length is not specifically set.
520     * This length is the maximum length of any Path in the block. Path lengths
521     * exceeding this will be set to the default length.
522     *
523     * @param l length in millimeters
524     */
525    public void setLength(float l) {
526        _length = l;
527        getPaths().stream().forEach(p -> {
528            if (p.getLength() > l) {
529                p.setLength(0); // set to default
530            }
531        });
532    }
533
534    public float getLengthMm() {
535        return _length;
536    } // return length in millimeters
537
538    public float getLengthCm() {
539        return (_length / 10.0f);
540    }  // return length in centimeters
541
542    public float getLengthIn() {
543        return (_length / 25.4f);
544    }  // return length in inches
545
546    /**
547     * Note: this has to make choices about identity values (always the same)
548     * and operation values (can change as the block works). Might be missing
549     * some identity values.
550     */
551    @Override
552    public boolean equals(Object obj) {
553        if (obj == this) {
554            return true;
555        }
556        if (obj == null) {
557            return false;
558        }
559
560        if (!(getClass() == obj.getClass())) {
561            return false;
562        } else {
563            Block b = (Block) obj;
564            return b.getSystemName().equals(this.getSystemName());
565        }
566    }
567
568    @Override
569    // This can't change, so can't include mutable values
570    public int hashCode() {
571        return this.getSystemName().hashCode();
572    }
573
574    // internal data members
575    private int _current = UNDETECTED; // state until sensor is set
576    //private Sensor _sensor = null;
577    private NamedBeanHandle<Sensor> _namedSensor = null;
578    private PropertyChangeListener _sensorListener = null;
579    private Object _value;
580    private Object _previousValue;
581    private int _direction;
582    private int _curvature = NONE;
583    private float _length = 0.0f;  // always stored in millimeters
584    private Reporter _reporter = null;
585    private PropertyChangeListener _reporterListener = null;
586    private boolean _reportingCurrent = false;
587
588    private Path[] pListOfPossibleEntrancePaths = null;
589    private int cntOfPossibleEntrancePaths = 0;
590
591    void resetCandidateEntrancePaths() {
592        pListOfPossibleEntrancePaths = null;
593        cntOfPossibleEntrancePaths = 0;
594    }
595
596    boolean setAsEntryBlockIfPossible(Block b) {
597        for (int i = 0; i < cntOfPossibleEntrancePaths; i++) {
598            Block CandidateBlock = pListOfPossibleEntrancePaths[i].getBlock();
599            if (CandidateBlock == b) {
600                setValue(CandidateBlock.getValue());
601                setDirection(pListOfPossibleEntrancePaths[i].getFromBlockDirection());
602                log.info("Block {} gets LATE new value from {}, direction= {}", getDisplayName(), CandidateBlock.getDisplayName(), Path.decodeDirection(getDirection()));
603                resetCandidateEntrancePaths();
604                return true;
605            }
606        }
607        return false;
608    }
609
610    /**
611     * Handle change in sensor state.
612     * <p>
613     * Defers real work to goingActive, goingInactive methods.
614     *
615     * @param e the event
616     */
617    void handleSensorChange(PropertyChangeEvent e) {
618        if (e.getPropertyName().equals("KnownState")) {
619            int state = getSensor().getState();
620            switch (state) {
621                case Sensor.ACTIVE:
622                    goingActive();
623                    break;
624                case Sensor.INACTIVE:
625                    goingInactive();
626                    break;
627                case Sensor.UNKNOWN:
628                    goingUnknown();
629                    break;
630                default:
631                    goingInconsistent();
632                    break;
633            }
634        }
635    }
636
637    public void goingUnknown() {
638        setValue(null);
639        setState(UNKNOWN);
640    }
641
642    public void goingInconsistent() {
643        setValue(null);
644        setState(INCONSISTENT);
645    }
646
647    /**
648     * Handle change in Reporter value.
649     *
650     * @param e PropertyChangeEvent
651     */
652    void handleReporterChange(PropertyChangeEvent e) {
653        if ((_reportingCurrent && e.getPropertyName().equals("currentReport"))
654                || (!_reportingCurrent && e.getPropertyName().equals("lastReport"))) {
655            setValue(e.getNewValue());
656        }
657    }
658
659    private Instant _timeLastInactive;
660    /**
661     * Handles Block sensor going INACTIVE: this block is empty
662     */
663    public void goingInactive() {
664        log.debug("Block {} goes UNOCCUPIED", getDisplayName());
665        for (Path path : paths) {
666            Block b = path.getBlock();
667            if (b != null) {
668                b.setAsEntryBlockIfPossible(this);
669            }
670        }
671        setValue(null);
672        setDirection(Path.NONE);
673        setState(UNOCCUPIED);
674        _timeLastInactive = Instant.now();
675    }
676
677    private final int maxInfoMessages = 5;
678    private int infoMessageCount = 0;
679
680    /**
681     * Handles Block sensor going ACTIVE: this block is now occupied, figure out
682     * from who and copy their value.
683     */
684    public void goingActive() {
685        if (getState() == OCCUPIED) {
686            return;
687        }
688        log.debug("Block {} goes OCCUPIED", getDisplayName());
689        resetCandidateEntrancePaths();
690        // index through the paths, counting
691        int count = 0;
692        Path next = null;
693        // get statuses of everything once
694        int currPathCnt = paths.size();
695        Path[] pList = new Path[currPathCnt];
696        boolean[] isSet = new boolean[currPathCnt];
697        boolean[] isActive = new boolean[currPathCnt];
698        int[] pDir = new int[currPathCnt];
699        int[] pFromDir = new int[currPathCnt];
700        for (int i = 0; i < currPathCnt; i++) {
701            pList[i] = paths.get(i);
702            isSet[i] = pList[i].checkPathSet();
703            Block b = pList[i].getBlock();
704            if (b != null) {
705                isActive[i] = b.getState() == OCCUPIED;
706                pDir[i] = b.getDirection();
707            } else {
708                isActive[i] = false;
709                pDir[i] = -1;
710            }
711            pFromDir[i] = pList[i].getFromBlockDirection();
712            if (isSet[i] && isActive[i]) {
713                count++;
714                next = pList[i];
715            }
716        }
717        // sort on number of neighbors
718        switch (count) {
719            case 0:
720                if (null != _previousValue) {
721                    // restore the previous value under either of these circumstances:
722                    // 1. the block has been 'unoccupied' only very briefly
723                    // 2. power has just come back on
724                    Instant tn = Instant.now();
725                    BlockManager bm = jmri.InstanceManager.getDefault(jmri.BlockManager.class);
726                    if (bm.timeSinceLastLayoutPowerOn() < 5000 || (_timeLastInactive != null && tn.toEpochMilli() - _timeLastInactive.toEpochMilli() < 2000)) {
727                        setValue(_previousValue);
728                        if (infoMessageCount < maxInfoMessages) {
729                            log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Restoring previous value.", getDisplayName());
730                            infoMessageCount++;
731                        }
732                    } else if (log.isDebugEnabled()) {
733                        if (null != _timeLastInactive) {
734                            log.debug("not restoring previous value, block {} has been inactive for too long ({}ms) and layout power has not just been restored ({}ms ago)", getDisplayName(), tn.toEpochMilli() - _timeLastInactive.toEpochMilli(), bm.timeSinceLastLayoutPowerOn());
735                        } else {
736                            log.debug("not restoring previous value, block {} has been inactive since the start of this session and layout power has not just been restored ({}ms ago)", getDisplayName(), bm.timeSinceLastLayoutPowerOn());
737                        }
738                    }
739                } else {
740                    if (infoMessageCount < maxInfoMessages) {
741                        log.debug("Sensor ACTIVE came out of nowhere, no neighbors active for block {}. Value not set.", getDisplayName());
742                        infoMessageCount++;
743                    }
744                }
745                break;
746            case 1:
747                // simple case
748                if ((next != null) && (next.getBlock() != null)) {
749                    // normal case, transfer value object
750                    setValue(next.getBlock().getValue());
751                    setDirection(next.getFromBlockDirection());
752                    log.debug("Block {} gets new value '{}' from {}, direction={}",
753                            getDisplayName(),
754                            next.getBlock().getValue(),
755                            next.getBlock().getDisplayName(),
756                            Path.decodeDirection(getDirection()));
757                } else if (next == null) {
758                    log.error("unexpected next==null processing block {}", getDisplayName());
759                } else {
760                    log.error("unexpected next.getBlock()=null processing block {}", getDisplayName());
761                }
762                break;
763            default:
764                // count > 1, check for one with proper direction
765                // this time, count ones with proper direction
766                log.debug("Block {} has {} active linked blocks, comparing directions", getDisplayName(), count);
767                next = null;
768                count = 0;
769                for (int i = 0; i < currPathCnt; i++) {
770                    if (isSet[i] && isActive[i]) {  //only consider active reachable blocks
771                        log.debug("comparing {} ({}) to {} ({})",
772                                pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]),
773                                getDisplayName(), Path.decodeDirection(pFromDir[i]));
774                        if ((pDir[i] & pFromDir[i]) > 0) { //use bitwise comparison to support combination directions such as "North, West"
775                            count++;
776                            next = pList[i];
777                        }
778                    }
779                }
780                if (next == null) {
781                    for (int i = 0; i < currPathCnt; i++) {
782                        if (isSet[i] && isActive[i]) {
783                            count++;
784                            next = pList[i];
785                        }
786                    }
787                }
788                if (next != null && count == 1) {
789                    // found one block with proper direction, use it
790                    setValue(next.getBlock().getValue());
791                    setDirection(next.getFromBlockDirection());
792                    log.debug("Block {} gets new value '{}' from {}, direction {}",
793                            getDisplayName(), next.getBlock().getValue(),
794                            next.getBlock().getDisplayName(), Path.decodeDirection(getDirection()));
795                } else {
796                    // no unique path with correct direction - this happens frequently from noise in block detectors!!
797                    log.warn("count of {} ACTIVE neightbors with proper direction can't be handled for block {} but maybe it can be determined when another block becomes free", count, getDisplayName());
798                    pListOfPossibleEntrancePaths = new Path[currPathCnt];
799                    cntOfPossibleEntrancePaths = 0;
800                    for (int i = 0; i < currPathCnt; i++) {
801                        if (isSet[i] && isActive[i]) {
802                            pListOfPossibleEntrancePaths[cntOfPossibleEntrancePaths] = pList[i];
803                            cntOfPossibleEntrancePaths++;
804                        }
805                    }
806                }
807                break;
808        }
809        setState(OCCUPIED);
810    }
811
812    /**
813     * Find which path this Block became Active, without actually modifying the
814     * state of this block.
815     * <p>
816     * (this is largely a copy of the 'Search' part of the logic from
817     * goingActive())
818     *
819     * @return the next path
820     */
821    public Path findFromPath() {
822        // index through the paths, counting
823        int count = 0;
824        Path next = null;
825        // get statuses of everything once
826        int currPathCnt = paths.size();
827        Path[] pList = new Path[currPathCnt];
828        boolean[] isSet = new boolean[currPathCnt];
829        boolean[] isActive = new boolean[currPathCnt];
830        int[] pDir = new int[currPathCnt];
831        int[] pFromDir = new int[currPathCnt];
832        for (int i = 0; i < currPathCnt; i++) {
833            pList[i] = paths.get(i);
834            isSet[i] = pList[i].checkPathSet();
835            Block b = pList[i].getBlock();
836            if (b != null) {
837                isActive[i] = b.getState() == OCCUPIED;
838                pDir[i] = b.getDirection();
839            } else {
840                isActive[i] = false;
841                pDir[i] = -1;
842            }
843            pFromDir[i] = pList[i].getFromBlockDirection();
844            if (isSet[i] && isActive[i]) {
845                count++;
846                next = pList[i];
847            }
848        }
849        // sort on number of neighbors
850        if ((count == 0) || (count == 1)) {
851            // do nothing.  OK to return null from this function.  "next" is already set.
852        } else {
853            // count > 1, check for one with proper direction
854            // this time, count ones with proper direction
855            log.debug("Block {} - count of active linked blocks = {}", getDisplayName(), count);
856            next = null;
857            count = 0;
858            for (int i = 0; i < currPathCnt; i++) {
859                if (isSet[i] && isActive[i]) {  //only consider active reachable blocks
860                    log.debug("comparing {} ({}) to {} ({})",
861                            pList[i].getBlock().getDisplayName(), Path.decodeDirection(pDir[i]),
862                            getDisplayName(), Path.decodeDirection(pFromDir[i]));
863                    if ((pDir[i] & pFromDir[i]) > 0) { //use bitwise comparison to support combination directions such as "North, West"
864                        count++;
865                        next = pList[i];
866                    }
867                }
868            }
869            if (next == null) {
870                log.debug("next is null!");
871            }
872            if (next != null && count == 1) {
873                // found one block with proper direction, assume that
874            } else {
875                // no unique path with correct direction - this happens frequently from noise in block detectors!!
876                log.warn("count of {} ACTIVE neighbors with proper direction can't be handled for block {}", count, getDisplayName());
877            }
878        }
879        // in any case, go OCCUPIED
880        if (log.isDebugEnabled()) { // avoid potentially expensive non-logging
881            log.debug("Block {} with direction {} gets new value from {} + (informational. No state change)", getDisplayName(), Path.decodeDirection(getDirection()), (next != null ? next.getBlock().getDisplayName() : "(no next block)"));
882        }
883        return (next);
884    }
885
886    /*
887     * This allows the layout block to inform any listeners to the block that the higher level layout block has been set to "useExtraColor" which is an
888     * indication that it has been allocated to a section by the AutoDispatcher.  The value set is not retained in any form by the block, it is purely to
889     * trigger a propertyChangeEvent.
890     */
891    public void setAllocated(Boolean boo) {
892        firePropertyChange("allocated", !boo, boo);
893    }
894
895    // Methods to implmement PhysicalLocationReporter Interface
896    //
897    // If we have a Reporter that is also a PhysicalLocationReporter,
898    // we will defer to that Reporter's methods.
899    // Else we will assume a LocoNet style message to be parsed.
900    /**
901     * Parse a given string and return the LocoAddress value that is presumed
902     * stored within it based on this object's protocol. The Class Block
903     * implementation defers to its associated Reporter, if it exists.
904     *
905     * @param rep String to be parsed
906     * @return LocoAddress address parsed from string, or null if this Block
907     *         isn't associated with a Reporter, or is associated with a
908     *         Reporter that is not also a PhysicalLocationReporter
909     */
910    @Override
911    public LocoAddress getLocoAddress(String rep) {
912        // Defer parsing to our associated Reporter if we can.
913        if (rep == null) {
914            log.warn("String input is null!");
915            return (null);
916        }
917        if ((this.getReporter() != null) && (this.getReporter() instanceof PhysicalLocationReporter)) {
918            return (((PhysicalLocationReporter) this.getReporter()).getLocoAddress(rep));
919        } else {
920            // Assume a LocoNet-style report.  This is (nascent) support for handling of Faller cars
921            // for Dave Merrill's project.
922            log.debug("report string: {}", rep);
923            // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter
924            Pattern ln_p = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");  // Match a number followed by the word "enter".  This is the LocoNet pattern.
925            Matcher m = ln_p.matcher(rep);
926            if (m.find()) {
927                log.debug("Parsed address: {}", m.group(1));
928                return (new DccLocoAddress(Integer.parseInt(m.group(1)), LocoAddress.Protocol.DCC));
929            } else {
930                return (null);
931            }
932        }
933    }
934
935    /**
936     * Parses out a (possibly old) LnReporter-generated report string to extract
937     * the direction from within it based on this object's protocol. The Class
938     * Block implementationd defers to its associated Reporter, if it exists.
939     *
940     * @param rep String to be parsed
941     * @return PhysicalLocationReporter.Direction direction parsed from string,
942     *         or null if this Block isn't associated with a Reporter, or is
943     *         associated with a Reporter that is not also a
944     *         PhysicalLocationReporter
945     */
946    @Override
947    public PhysicalLocationReporter.Direction getDirection(String rep) {
948        if (rep == null) {
949            log.warn("String input is null!");
950            return (null);
951        }
952        // Defer parsing to our associated Reporter if we can.
953        if ((this.getReporter() != null) && (this.getReporter() instanceof PhysicalLocationReporter)) {
954            return (((PhysicalLocationReporter) this.getReporter()).getDirection(rep));
955        } else {
956            log.debug("report string: {}", rep);
957            // NOTE: This pattern is based on the one defined in LocoNet-specific LnReporter
958            Pattern ln_p = Pattern.compile("(\\d+) (enter|exits|seen)\\s*(northbound|southbound)?");  // Match a number followed by the word "enter".  This is the LocoNet pattern.
959            Matcher m = ln_p.matcher(rep);
960            if (m.find()) {
961                log.debug("Parsed direction: {}", m.group(2));
962                switch (m.group(2)) {
963                    case "enter":
964                        // LocoNet Enter message
965                        return (PhysicalLocationReporter.Direction.ENTER);
966                    case "seen":
967                        // Lissy message.  Treat them all as "entry" messages.
968                        return (PhysicalLocationReporter.Direction.ENTER);
969                    default:
970                        return (PhysicalLocationReporter.Direction.EXIT);
971                }
972            } else {
973                return (PhysicalLocationReporter.Direction.UNKNOWN);
974            }
975        }
976    }
977
978    /**
979     * Return this Block's physical location, if it exists. Defers actual work
980     * to the helper methods in class PhysicalLocation
981     *
982     * @return PhysicalLocation : this Block's location.
983     */
984    @Override
985    public PhysicalLocation getPhysicalLocation() {
986        // We have our won PhysicalLocation. That's the point.  No need to defer to the Reporter.
987        return (PhysicalLocation.getBeanPhysicalLocation(this));
988    }
989
990    /**
991     * Return this Block's physical location, if it exists. Does not use the
992     * parameter s Defers actual work to the helper methods in class
993     * PhysicalLocation
994     *
995     * @param s (this parameter is ignored)
996     * @return PhysicalLocation : this Block's location.
997     */
998    @Override
999    public PhysicalLocation getPhysicalLocation(String s) {
1000        // We have our won PhysicalLocation. That's the point.  No need to defer to the Reporter.
1001        // Intentionally ignore the String s
1002        return (PhysicalLocation.getBeanPhysicalLocation(this));
1003    }
1004
1005    @Override
1006    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
1007        if ("CanDelete".equals(evt.getPropertyName())) { // No I18N
1008            if (evt.getOldValue() instanceof Sensor) {
1009                if (evt.getOldValue().equals(getSensor())) {
1010                    throw new PropertyVetoException(getDisplayName(), evt);
1011                }
1012            }
1013            if (evt.getOldValue() instanceof Reporter) {
1014                if (evt.getOldValue().equals(getReporter())) {
1015                    throw new PropertyVetoException(getDisplayName(), evt);
1016                }
1017            }
1018        } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
1019            if (evt.getOldValue() instanceof Sensor) {
1020                if (evt.getOldValue().equals(getSensor())) {
1021                    setSensor(null);
1022                }
1023            }
1024            if (evt.getOldValue() instanceof Reporter) {
1025                if (evt.getOldValue().equals(getReporter())) {
1026                    setReporter(null);
1027                }
1028            }
1029        }
1030    }
1031
1032    @Override
1033    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
1034        List<NamedBeanUsageReport> report = new ArrayList<>();
1035        if (bean != null) {
1036            if (bean.equals(getSensor())) {
1037                report.add(new NamedBeanUsageReport("BlockSensor"));  // NOI18N
1038            }
1039            if (bean.equals(getReporter())) {
1040                report.add(new NamedBeanUsageReport("BlockReporter"));  // NOI18N
1041            }
1042            // Block paths
1043            getPaths().forEach((path) -> {
1044                if (bean.equals(path.getBlock())) {
1045                    report.add(new NamedBeanUsageReport("BlockPathNeighbor"));  // NOI18N
1046                }
1047                path.getSettings().forEach((setting) -> {
1048                    if (bean.equals(setting.getBean())) {
1049                        report.add(new NamedBeanUsageReport("BlockPathTurnout"));  // NOI18N
1050                    }
1051                });
1052            });
1053        }
1054        return report;
1055    }
1056
1057    @Override
1058    public String getBeanType() {
1059        return Bundle.getMessage("BeanNameBlock");
1060    }
1061
1062    private final static Logger log = LoggerFactory.getLogger(Block.class);
1063}