001package jmri.jmrit.logix;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.Color;
005import java.awt.Font;
006import java.beans.PropertyChangeListener;
007import java.util.*;
008import java.util.stream.Collectors;
009
010import javax.annotation.CheckForNull;
011import javax.annotation.Nonnull;
012
013import jmri.InstanceManager;
014import jmri.NamedBean;
015import jmri.NamedBeanHandle;
016import jmri.NamedBeanUsageReport;
017import jmri.Path;
018import jmri.Sensor;
019import jmri.Turnout;
020import jmri.util.ThreadingUtil;
021
022/**
023 * OBlock extends jmri.Block to be used in Logix Conditionals and Warrants. It
024 * is the smallest piece of track that can have occupancy detection. A better
025 * name would be Detection Circuit. However, an OBlock can be defined without an
026 * occupancy sensor and used to calculate routes.
027 * <p>
028 * Additional states are defined to indicate status of the track and trains to
029 * control panels. A jmri.Block has a PropertyChangeListener on the occupancy
030 * sensor and the OBlock will pass state changes of the occ.sensor on to its
031 * Warrant.
032 * <p>
033 * Entrances (exits when train moves in opposite direction) to OBlocks have
034 * Portals. A Portal object is a pair of OBlocks. Each OBlock has a list of its
035 * Portals.
036 * <p>
037 * When an OBlock (Detection Circuit) has a Portal whose entrance to the OBlock
038 * has a signal, then the OBlock and its chains of adjacent OBlocks up to the
039 * next OBlock having an entrance Portal with a signal, can be considered a
040 * "Block" in the sense of a prototypical railroad. Preferably all entrances to
041 * the "Block" should have entrance Portals with a signal.
042 * <p>
043 * A Portal has a list of paths (OPath objects) for each OBlock it separates.
044 * The paths are determined by the turnout settings of the turnouts contained in
045 * the block. Paths are contained within the Block boundaries. Names of OPath
046 * objects only need be unique within an OBlock.
047 *
048 * @author Pete Cressman (C) 2009
049 * @author Egbert Broerse (C) 2020
050 */
051public class OBlock extends jmri.Block implements java.beans.PropertyChangeListener {
052
053    public enum OBlockStatus {
054        Unoccupied(UNOCCUPIED, "unoccupied", Bundle.getMessage("unoccupied")),
055        Occupied(OCCUPIED, "occupied", Bundle.getMessage("occupied")),
056        Allocated(ALLOCATED, "allocated", Bundle.getMessage("allocated")),
057        Running(RUNNING, "running", Bundle.getMessage("running")),
058        OutOfService(OUT_OF_SERVICE, "outOfService", Bundle.getMessage("outOfService")),
059        Dark(UNDETECTED, "dark", Bundle.getMessage("dark")),
060        TrackError(TRACK_ERROR, "powerError", Bundle.getMessage("powerError"));
061
062        private final int status;
063        private final String name;
064        private final String descr;
065
066        private static final Map<String, OBlockStatus> map = new HashMap<>();
067        private static final Map<String, OBlockStatus> reverseMap = new HashMap<>();
068
069        OBlockStatus(int status, String name, String descr) {
070            this.status = status;
071            this.name = name;
072            this.descr = descr;
073        }
074
075        public int getStatus() { return status; }
076
077        public String getName() { return name; }
078
079        public String getDescr() { return descr; }
080
081        public static OBlockStatus getByName(String name) { return map.get(name); }
082        public static OBlockStatus getByDescr(String descr) { return reverseMap.get(descr); }
083
084        static {
085            for (OBlockStatus oblockStatus : OBlockStatus.values()) {
086                map.put(oblockStatus.getName(), oblockStatus);
087                reverseMap.put(oblockStatus.getDescr(), oblockStatus);
088            }
089        }
090    }
091
092    /*
093     * OBlock states:
094     * NamedBean.UNKNOWN                 = 0x01
095     * Block.OCCUPIED =  Sensor.ACTIVE   = 0x02
096     * Block.UNOCCUPIED = Sensor.INACTIVE= 0x04
097     * NamedBean.INCONSISTENT            = 0x08
098     * Add the following to the 4 sensor states.
099     * States are OR'ed to show combination.  e.g. ALLOCATED | OCCUPIED = allocated block is occupied
100     */
101    public static final int ALLOCATED = 0x10;      // reserve the block for subsequent use by a train
102    public static final int RUNNING = 0x20;        // OBlock that running train has reached
103    public static final int OUT_OF_SERVICE = 0x40; // OBlock that should not be used
104    public static final int TRACK_ERROR = 0x80;    // OBlock has Error
105    // UNDETECTED state bit is used for DARK blocks
106    // public static final int DARK = 0x01;        // meaning: OBlock has no Sensor, same as UNKNOWN
107
108    private static final Color DEFAULT_FILL_COLOR = new Color(200, 0, 200);
109
110    /**
111     * String constant to represent path State.
112     */
113    public static final String PROPERTY_PATH_STATE = "pathState";
114
115    /**
116     * String constant to represent path Count.
117     */
118    public static final String PROPERTY_PATH_COUNT = "pathCount";
119
120    /**
121     * String constant to represent portal Count.
122     */
123    public static final String PROPERTY_PORTAL_COUNT = "portalCount";
124
125    /**
126     * String constant to represent deleted.
127     */
128    public static final String PROPERTY_DELETED = "deleted";
129
130    public static String getLocalStatusName(String str) {
131        return OBlockStatus.getByName(str).getDescr();
132    }
133
134    public static String getSystemStatusName(String str) {
135        return OBlockStatus.getByDescr(str).getName();
136    }
137    private List<Portal> _portals = new ArrayList<>();     // portals to this block
138
139    private Warrant _warrant;        // when not null, oblock is allocated to this warrant
140    private String _pathName;        // when not null, this is the allocated path or last path used by a warrant
141    protected long _entryTime;       // time when block became occupied
142    private boolean _metric = false; // desired display mode
143    private NamedBeanHandle<Sensor> _errNamedSensor;
144    private Color _markerForeground = Color.WHITE;
145    private Color _markerBackground = DEFAULT_FILL_COLOR;
146    private Font _markerFont;
147
148    public OBlock(@Nonnull String systemName) {
149        super(systemName);
150        OBlock.this.setState(UNDETECTED);
151    }
152
153    public OBlock(@Nonnull String systemName, String userName) {
154        super(systemName, userName);
155        OBlock.this.setState(UNDETECTED);
156    }
157
158    /* What super does currently is fine.
159     * FindBug wants us to duplicate and override anyway
160     */
161    @Override
162    public boolean equals(Object obj) {
163        if (obj == this) {
164            return true;
165        }
166        if (obj == null) {
167            return false;
168        }
169
170        if (!getClass().equals(obj.getClass())) {
171            return false;
172        } else {
173            OBlock b = (OBlock) obj;
174            return b.getSystemName().equals(this.getSystemName());
175        }
176    }
177
178    @Override
179    public int hashCode() {
180        return this.getSystemName().hashCode();
181    }
182
183    /**
184     * {@inheritDoc}
185     * <p>
186     * Override to only set an existing sensor and to amend state with not
187     * UNDETECTED return true if an existing Sensor is set or sensor is to be
188     * removed from block.
189     */
190    @Override
191    public boolean setSensor(String pName) {
192        Sensor oldSensor = getSensor();
193        Sensor newSensor = null;
194        if (pName != null && pName.trim().length() > 0) {
195            newSensor = InstanceManager.sensorManagerInstance().getByUserName(pName);
196            if (newSensor == null) {
197                newSensor = InstanceManager.sensorManagerInstance().getBySystemName(pName);
198            }
199            if (newSensor == null) {
200                log.error("No sensor named '{}' exists.", pName);
201                return false;
202            }
203        }
204        if (oldSensor != null && oldSensor.equals(newSensor)) {
205            return true;
206        }
207
208        // save the non-sensor states
209        int saveState = getState() & ~(UNKNOWN | OCCUPIED | UNOCCUPIED | INCONSISTENT | UNDETECTED);
210        if (newSensor == null || pName == null) {
211            setNamedSensor(null);
212        } else {
213            setNamedSensor(InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).
214                getNamedBeanHandle(pName, newSensor));
215        }
216        setState(getState() | saveState);   // add them back into new sensor
217        firePropertyChange(OCC_SENSOR_CHANGE, oldSensor, newSensor);
218        return true;
219    }
220
221    // override to determine if not UNDETECTED
222    @Override
223    public void setNamedSensor(@CheckForNull NamedBeanHandle<Sensor> namedSensor) {
224        super.setNamedSensor(namedSensor);
225        Sensor s = getSensor();
226        if ( s != null) {
227            setState( s.getState() & ~UNDETECTED);
228        }
229    }
230
231    /**
232     * @param pName name of error sensor
233     * @return true if successful
234     */
235    public boolean setErrorSensor(@CheckForNull String pName) {
236        NamedBeanHandle<Sensor> newErrSensorHdl = null;
237        Sensor newErrSensor = null;
238        if (pName != null && pName.trim().length() > 0) {
239            newErrSensor = InstanceManager.sensorManagerInstance().getByUserName(pName);
240            if (newErrSensor == null) {
241                newErrSensor = InstanceManager.sensorManagerInstance().getBySystemName(pName);
242            }
243           if (newErrSensor != null) {
244                newErrSensorHdl = InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).
245                    getNamedBeanHandle(pName, newErrSensor);
246           }
247           if (newErrSensor == null) {
248               log.error("No sensor named '{}' exists.", pName);
249               return false;
250           }
251        }
252        if (_errNamedSensor != null) {
253            if (_errNamedSensor.equals(newErrSensorHdl)) {
254                return true;
255            } else {
256                _errNamedSensor.getBean().removePropertyChangeListener(this);
257            }
258        }
259
260        _errNamedSensor = newErrSensorHdl;
261        setState(getState() & ~TRACK_ERROR);
262        if (newErrSensor  != null) {
263            newErrSensor.addPropertyChangeListener( this,
264                _errNamedSensor.getName(), "OBlock Error Sensor " + getDisplayName());
265            if (newErrSensor.getState() == Sensor.ACTIVE) {
266                setState(getState() | TRACK_ERROR);
267            } else {
268                setState(getState() & ~TRACK_ERROR);
269            }
270        }
271        return true;
272    }
273
274    @CheckForNull
275    public Sensor getErrorSensor() {
276        if (_errNamedSensor == null) {
277            return null;
278        }
279        return _errNamedSensor.getBean();
280    }
281
282    @CheckForNull
283    public NamedBeanHandle<Sensor> getNamedErrorSensor() {
284        return _errNamedSensor;
285    }
286
287    @Override
288    public void propertyChange(java.beans.PropertyChangeEvent evt) {
289        if (log.isDebugEnabled()) {
290            log.debug("property change: of \"{}\" property {} is now {} from {}",
291                    getDisplayName(), evt.getPropertyName(), evt.getNewValue(), evt.getSource().getClass().getName());
292        }
293        if ((getErrorSensor() != null) && (evt.getSource().equals(getErrorSensor()))
294            && Sensor.PROPERTY_KNOWN_STATE.equals(evt.getPropertyName())) {
295            int errState = ((Integer) evt.getNewValue());
296            int oldState = getState();
297            if (errState == Sensor.ACTIVE) {
298                setState(oldState | TRACK_ERROR);
299            } else {
300                setState(oldState & ~TRACK_ERROR);
301            }
302            firePropertyChange(PROPERTY_PATH_STATE, oldState, getState());
303        }
304    }
305
306    /**
307     * Another block sharing a turnout with this block queries whether turnout
308     * is in use.
309     *
310     * @param path that uses a common shared turnout
311     * @return If warrant exists and path==pathname, return warrant display
312     *         name, else null.
313     */
314    @CheckForNull
315    protected String isPathSet(@Nonnull String path) {
316        String msg = null;
317        if (_warrant != null && path.equals(_pathName)) {
318            msg = _warrant.getDisplayName();
319        }
320        log.trace("Path \"{}\" in oblock \"{}\" {}", path, getDisplayName(),
321            (msg == null ? "not set" : " set in warrant " + msg));
322        return msg;
323    }
324
325    @CheckForNull
326    public Warrant getWarrant() {
327        return _warrant;
328    }
329
330    public boolean isAllocatedTo( @CheckForNull Warrant warrant) {
331        if (warrant == null) {
332            return false;
333        }
334        return warrant.equals(_warrant);
335    }
336
337    public String getAllocatedPathName() {
338        return _pathName;
339    }
340
341    public void setMetricUnits(boolean type) {
342        _metric = type;
343    }
344
345    public boolean isMetric() {
346        return _metric;
347    }
348
349    public void setMarkerForeground(Color c) {
350        _markerForeground = c;
351    }
352
353    public Color getMarkerForeground() {
354        return _markerForeground;
355    }
356
357    public void setMarkerBackground(Color c) {
358        _markerBackground = c;
359    }
360
361    public Color getMarkerBackground() {
362        return _markerBackground;
363    }
364
365    public void setMarkerFont(Font f) {
366        _markerFont = f;
367    }
368
369    public Font getMarkerFont() {
370        return _markerFont;
371    }
372
373    /**
374     * Update the OBlock status.
375     * Block.setState fires the "state" property change with this OBlock as
376     * its source, as used by the Web Server and CPE indicator track icons.
377     *
378     * @param v the new state, from OBlock.ALLOCATED etc, named 'status' in JSON Servlet and Web Server
379     */
380    @Override
381    public void setState(int v) {
382        super.setState(v);
383        log.debug("\"{}\" setState({})", getDisplayName(), getState());
384    }
385
386    /**
387     * {@inheritDoc}
388     */
389    @Override
390    public void setValue(Object o) {
391        super.setValue(o);
392        if (o == null) {
393            _markerForeground = Color.WHITE;
394            _markerBackground = DEFAULT_FILL_COLOR;
395            _markerFont = null;
396        }
397    }
398
399    /*_
400     *  From the universal name for block status, check if it is the current status
401     */
402    public boolean statusIs(String statusName) {
403        OBlockStatus oblockStatus = OBlockStatus.getByName(statusName);
404        if (oblockStatus != null) {
405            return ((getState() & oblockStatus.getStatus()) != 0);
406        }
407        log.error("\"{}\" type not found.  Update Conditional State Variable testing OBlock \"{}\" status",
408                getDisplayName(), statusName);
409        return false;
410    }
411
412    public boolean isDark() {
413        return (getState() & OBlock.UNDETECTED) != 0;
414    }
415
416    public boolean isOccupied() {
417        return (getState() & OBlock.OCCUPIED) != 0;
418    }
419
420    @CheckForNull
421    public String occupiedBy() {
422        Warrant w = _warrant;
423        if (isOccupied()) {
424            if (w != null) {
425                return w.getTrainName();
426            } else {
427                return Bundle.getMessage("unknownTrain");
428            }
429        } else {
430            return null;
431        }
432    }
433
434    /**
435     * Test that block is not occupied and not allocated
436     *
437     * @return true if not occupied and not allocated
438     */
439    public boolean isFree() {
440        int state = getState();
441        return ((state & ALLOCATED) == 0 && (state & OCCUPIED) == 0);
442    }
443
444    /**
445     * Allocate (reserves) the block for the Warrant Note the block may be
446     * OCCUPIED by a non-warranted train, but the allocation is permitted.
447     *
448     * @param warrant the Warrant
449     * @return message with if block is already allocated to another warrant or
450     *         block is OUT_OF_SERVICE
451     */
452    @CheckForNull
453    public String allocate(Warrant warrant) {
454        if (warrant == null) {
455            log.error("allocate(warrant) called with null warrant in block \"{}\"!", getDisplayName());
456            return "ERROR! allocate called with null warrant in block \"" + getDisplayName() + "\"!";
457        }
458        if (_warrant != null) {
459            if (!warrant.equals(_warrant)) {
460                return Bundle.getMessage("AllocatedToWarrant",
461                        _warrant.getDisplayName(), getDisplayName(), _warrant.getTrainName());
462            } else {
463                return null;
464            }
465        }
466        /*
467        int state = getState();
468        if ((state & OUT_OF_SERVICE) != 0) {
469            return Bundle.getMessage("BlockOutOfService", getDisplayName());
470        }*/
471
472        _warrant = warrant;
473        if (log.isDebugEnabled()) {
474            log.debug("Allocate OBlock \"{}\" to warrant \"{}\".",
475                    getDisplayName(), warrant.getDisplayName());
476        }
477        super.setState(getState() | ALLOCATED);
478        return null;
479    }
480
481    // Highlights track icons to show that block is allocated.
482    protected void showAllocated(Warrant warrant, String pathName) {
483        if (_warrant != null && !_warrant.equals(warrant)) {
484            return;
485        }
486        if (_pathName == null) {
487            _pathName = pathName;
488        }
489        firePropertyChange(PROPERTY_PATH_STATE, 0, getState());
490//        super.setState(getState());
491    }
492
493    /**
494     * Note path name may be set if block is not allocated to a warrant. For use
495     * by CircuitBuilder Only. (test paths for editCircuitPaths)
496     *
497     * @param pathName name of a path
498     * @return error message, otherwise null
499     */
500    @CheckForNull
501    public String allocatePath(String pathName) {
502        log.debug("Allocate OBlock path \"{}\" in block \"{}\", state= {}",
503                pathName, getSystemName(), getState());
504        if (pathName == null) {
505            log.error("allocate called with null pathName in block \"{}\"!", getDisplayName());
506            return null;
507        } else if (_warrant != null) {
508            // allocated to another warrant
509            return Bundle.getMessage("AllocatedToWarrant",
510                    _warrant.getDisplayName(), getDisplayName(), _warrant.getTrainName());
511        }
512        _pathName = pathName;
513        //  DO NOT ALLOCATE block
514        return null;
515    }
516
517    public String getAllocatingWarrantName() {
518        if (_warrant == null) {
519            return ("no warrant");
520        } else {
521            return _warrant.getDisplayName();
522        }
523    }
524
525    /**
526     * Remove allocation state // maybe restore this? Remove listener regardless of ownership
527     *
528     * @param warrant warrant that has reserved this block. null is allowed for
529     *                Conditionals and CircuitBuilder to reset the block.
530     *                Otherwise, null should not be used.
531     * @return true if warrant deallocated.
532     */
533    public boolean deAllocate(Warrant warrant) {
534        if (warrant == null) {
535            return true;
536        }
537        if (_warrant != null) {
538            if (!_warrant.equals(warrant)) {
539                log.warn("{} cannot deallocate. {}", warrant.getDisplayName(), Bundle.getMessage("AllocatedToWarrant",
540                        _warrant.getDisplayName(), getDisplayName(), _warrant.getTrainName()));
541                return false;
542            }
543            Warrant curWarrant = _warrant;
544            _warrant = null;    // At times, removePropertyChangeListener may be run on a delayed thread.
545            try {
546                if (log.isDebugEnabled()) {
547                    log.debug("deAllocate block \"{}\" from warrant \"{}\"",
548                            getDisplayName(), warrant.getDisplayName());
549                }
550                removePropertyChangeListener(curWarrant);
551            } catch (Exception ex) {
552                // disposed warrant may throw null pointer - continue deallocation
553                log.trace("Warrant {} unregistered.", curWarrant.getDisplayName(), ex);
554            }
555        }
556        _warrant = null;
557        if (_pathName != null) {
558            OPath path = getPathByName(_pathName);
559            if (path != null) {
560                int lockState = Turnout.CABLOCKOUT & Turnout.PUSHBUTTONLOCKOUT;
561                path.setTurnouts(0, false, lockState, false);
562                Portal portal = path.getFromPortal();
563                if (portal != null) {
564                    portal.setState(Portal.UNKNOWN);
565                }
566                portal = path.getToPortal();
567                if (portal != null) {
568                    portal.setState(Portal.UNKNOWN);
569                }
570            }
571        }
572        super.setState(getState() & ~(ALLOCATED | RUNNING));  // unset allocated and running bits
573        return true;
574    }
575
576    public void setOutOfService(boolean set) {
577        if (set) {
578            setState(getState() | OUT_OF_SERVICE);  // set OoS bit
579        } else {
580            setState(getState() & ~OUT_OF_SERVICE);  // unset OoS bit
581        }
582    }
583
584    public void setError(boolean set) {
585        if (set) {
586            setState(getState() | TRACK_ERROR);  // set err bit
587        } else {
588            setState(getState() & ~TRACK_ERROR);  // unset err bit
589        }
590    }
591
592    /**
593     * Enforce unique portal names. Portals are now managed beans since 2014.
594     * This enforces unique names.
595     *
596     * @param portal the Portal to add
597     */
598    public void addPortal(Portal portal) {
599        String name = getDisplayName();
600        if (!name.equals(portal.getFromBlockName()) && !name.equals(portal.getToBlockName())) {
601            log.warn("{} not in block {}", portal.getDescription(), getDisplayName());
602            return;
603        }
604        String pName = portal.getName();
605        if (pName != null) {  // pName may be null if called from Portal ctor
606            for (Portal value : _portals) {
607                if (pName.equals(value.getName())) {
608                    return;
609                }
610            }
611        }
612        int oldSize = _portals.size();
613        _portals.add(portal);
614        log.trace("add portal \"{}\" to Block \"{}\"", portal.getName(), getDisplayName());
615        firePropertyChange(PROPERTY_PORTAL_COUNT, oldSize, _portals.size());
616    }
617
618    /**
619     * Remove portal from OBlock and stub all paths using this portal to be dead
620     * end spurs.
621     *
622     * @param portal the Portal to remove
623     */
624    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
625    protected void removePortal(@CheckForNull Portal portal) {
626        if (portal != null) {
627            Iterator<Path> iter = getPaths().iterator();
628            while (iter.hasNext()) {
629                OPath path = (OPath) iter.next();
630                if (portal.equals(path.getFromPortal())) {
631                    path.setFromPortal(null);
632                    log.trace("removed Portal {} from Path \"{}\" in oblock {}",
633                            portal.getName(), path.getName(), getDisplayName());
634                }
635                if (portal.equals(path.getToPortal())) {
636                    path.setToPortal(null);
637                    log.trace("removed Portal {} from Path \"{}\" in oblock {}",
638                            portal.getName(), path.getName(), getDisplayName());
639                }
640            }
641            iter = getPaths().iterator();
642            while (iter.hasNext()) {
643                OPath path = (OPath) iter.next();
644                if (path.getFromPortal() == null && path.getToPortal() == null) {
645                    removeOPath(path);
646                    log.trace("removed Path \"{}\" from oblock {}", path.getName(), getDisplayName());
647                }
648            }
649            int oldSize = _portals.size();
650            _portals = _portals.stream().filter(p -> !Objects.equals(p,portal)).collect(Collectors.toList());
651            firePropertyChange(PROPERTY_PORTAL_COUNT, oldSize, _portals.size());
652        }
653    }
654
655    public Portal getPortalByName(String name) {
656        for (Portal po : _portals) {
657            if (po.getName().equals(name)) {
658                return po;
659            }
660        }
661        return null;
662    }
663
664    @Nonnull
665    public List<Portal> getPortals() {
666        return new ArrayList<>(_portals);
667    }
668
669    public void setPortals(ArrayList<Portal> portals) {
670        _portals = portals;
671    }
672
673    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
674    public OPath getPathByName(String name) {
675        for (Path opa : getPaths()) {
676            OPath path = (OPath) opa;
677            if (path.getName().equals(name)) {
678                return path;
679            }
680        }
681        return null;
682    }
683
684    @Override
685    public void setLength(float len) {
686        // Only shorten paths longer than 'len'
687        getPaths().stream().forEach(p -> {
688            if (p.getLength() > len) {
689                p.setLength(len); // set to default
690            }
691        });
692        super.setLength(len);
693    }
694
695    /**
696     * Enforce unique path names within OBlock, but allow a duplicate name of an
697     * OPath from another OBlock to be checked if it is in one of the OBlock's
698     * Portals.
699     *
700     * @param path the OPath to add
701     * @return true if path was added to OBlock
702     */
703    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
704    public boolean addPath(OPath path) {
705        String pName = path.getName();
706        log.trace("addPath \"{}\" to OBlock {}", pName, getSystemName());
707        List<Path> list = getPaths();
708        for (Path p : list) {
709            if (((OPath) p).equals(path)) {
710                log.trace("Path \"{}\" duplicated in OBlock {}", pName, getSystemName());
711                return false;
712            }
713            if (pName.equals(((OPath) p).getName())) {
714                log.trace("Path named \"{}\" already exists in OBlock {}", pName, getSystemName());
715                return false;
716            }
717        }
718        OBlock pathBlock = (OBlock) path.getBlock();
719        if (pathBlock != null && !this.equals(pathBlock)) {
720            log.warn("Path \"{}\" already in block {}, cannot be added to block {}",
721                    pName, pathBlock.getDisplayName(), getDisplayName());
722            return false;
723        }
724        path.setBlock(this);
725        Portal portal = path.getFromPortal();
726        if (portal != null) {
727            if (!portal.addPath(path)) {
728                log.trace("Path \"{}\" rejected by portal  {}", pName, portal.getName());
729                return false;
730            }
731        }
732        portal = path.getToPortal();
733        if (portal != null) {
734            if (!portal.addPath(path)) {
735                log.debug("Path \"{}\" rejected by portal  {}", pName, portal.getName());
736                return false;
737            }
738        }
739        super.addPath(path);
740        firePropertyChange(PROPERTY_PATH_COUNT, null, getPaths().size());
741        return true;
742    }
743
744    public boolean removeOPath(OPath path) {
745        jmri.Block block = path.getBlock();
746        if (block != null && !getSystemName().equals(block.getSystemName())) {
747            return false;
748        }
749        if (!InstanceManager.getDefault(jmri.jmrit.logix.WarrantManager.class).okToRemoveBlockPath(this, path)) {
750            return false;
751        }
752        path.clearSettings();
753        super.removePath(path);
754        // remove path from its portals
755        Portal portal = path.getToPortal();
756        if (portal != null) {
757            portal.removePath(path);
758        }
759        portal = path.getFromPortal();
760        if (portal != null) {
761            portal.removePath(path);
762        }
763        path.dispose();
764        firePropertyChange(PROPERTY_PATH_COUNT, path, getPaths().size());
765        return true;
766    }
767
768    /**
769     * Set Turnouts for the path.
770     * <p>
771     * Called by warrants to set turnouts for a train it is able to run.
772     * The warrant parameter verifies that the block is
773     * indeed allocated to the warrant. If the block is unwarranted then the
774     * block is allocated to the calling warrant. A logix conditional may also
775     * call this method with a null warrant parameter for manual logix control.
776     * If the block is under a different warrant the call will be rejected.
777     *
778     * @param pathName name of the path
779     * @param warrant  warrant the block is allocated to
780     * @return error message if the call fails. null if the call succeeds
781     */
782    protected String setPath(String pathName, Warrant warrant) {
783        OPath path = getPathByName(pathName);
784        if (path == null) {
785            return Bundle.getMessage("PathNotFound", pathName, getDisplayName());
786        }
787        if (warrant == null || !warrant.equals(_warrant)) {
788            String name;
789            if (_warrant != null) {
790                name = _warrant.getDisplayName();
791            } else {
792                name = Bundle.getMessage("Warrant");
793            }
794            return Bundle.getMessage("PathNotSet", pathName, getDisplayName(), name);
795        }
796        _pathName = pathName;
797        int lockState = Turnout.CABLOCKOUT & Turnout.PUSHBUTTONLOCKOUT;
798        path.setTurnouts(0, true, lockState, true);
799        firePropertyChange(PROPERTY_PATH_STATE, 0, getState());
800        if (log.isTraceEnabled()) {
801            log.debug("setPath: Path \"{}\" in path \"{}\" {} set for warrant {}",
802                    pathName, getDisplayName(), _pathName, warrant.getDisplayName());
803        }
804        return null;
805    }
806
807    protected OPath getPath() {
808        if (_pathName == null) {
809            return null;
810        }
811        return getPathByName(_pathName);
812    }
813
814    /*
815     * Call for Circuit Builder to make icon color changes for its GUI
816     */
817    public void pseudoPropertyChange(String propName, Object old, Object n) {
818        log.trace("pseudoPropertyChange: Block \"{}\" property \"{}\" new value= {}",
819                getSystemName(), propName, n);
820        firePropertyChange(propName, old, n);
821    }
822
823    /**
824     * (Override) Handles Block sensor going INACTIVE: this block is empty.
825     * Called by handleSensorChange
826     */
827    @Override
828    public void goingInactive() {
829        //log.debug("OBlock \"{}\" going UNOCCUPIED from state= {}", getDisplayName(), getState());
830        // preserve the non-sensor states
831        // non-UNOCCUPIED sensor states are removed (also cannot be RUNNING there if being UNOCCUPIED)
832        setState((getState() & ~(UNKNOWN | OCCUPIED | INCONSISTENT | RUNNING)) | UNOCCUPIED);
833        setValue(null);
834        if (_warrant != null) {
835            ThreadingUtil.runOnLayout(() -> _warrant.goingInactive(this));
836        }
837    }
838
839    /**
840     * (Override) Handles Block sensor going ACTIVE: this block is now occupied,
841     * figure out from who and copy their value. Called by handleSensorChange
842     */
843    @Override
844    public void goingActive() {
845        // preserve the non-sensor states when being OCCUPIED and remove non-OCCUPIED sensor states
846        setState((getState() & ~(UNKNOWN | UNOCCUPIED | INCONSISTENT)) | OCCUPIED);
847        _entryTime = System.currentTimeMillis();
848        if (_warrant != null) {
849            ThreadingUtil.runOnLayout(() -> _warrant.goingActive(this));
850        }
851    }
852
853    @Override
854    public void goingUnknown() {
855        setState((getState() & ~(UNOCCUPIED | OCCUPIED | INCONSISTENT)) | UNKNOWN);
856    }
857
858    @Override
859    public void goingInconsistent() {
860        setState((getState() & ~(UNKNOWN | UNOCCUPIED | OCCUPIED)) | INCONSISTENT);
861    }
862
863    @Override
864    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
865    public void dispose() {
866        if (!InstanceManager.getDefault(WarrantManager.class).okToRemoveBlock(this)) {
867            return;
868        }
869        firePropertyChange(PROPERTY_DELETED, null, null);
870        // remove paths first
871        for (Path pa : getPaths()) {
872            removeOPath((OPath)pa);
873        }
874        for (Portal portal : getPortals()) {
875            if (log.isTraceEnabled()) {
876                log.debug("this = {}, toBlock = {}, fromblock= {}", getDisplayName(),
877                        portal.getToBlock().getDisplayName(), portal.getFromBlock().getDisplayName());
878            }
879            if (this.equals(portal.getToBlock())) {
880                portal.setToBlock(null, false);
881            }
882            if (this.equals(portal.getFromBlock())) {
883                portal.setFromBlock(null, false);
884            }
885        }
886        _portals.clear();
887        for (PropertyChangeListener listener : getPropertyChangeListeners()) {
888            removePropertyChangeListener(listener);
889        }
890        jmri.InstanceManager.getDefault(OBlockManager.class).deregister(this);
891        super.dispose();
892    }
893
894    public String getDescription() {
895        return java.text.MessageFormat.format(
896                Bundle.getMessage("BlockDescription"), getDisplayName());
897    }
898
899    @Override
900    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
901        List<NamedBeanUsageReport> report = new ArrayList<>();
902        List<NamedBean> duplicateCheck = new ArrayList<>();
903        if (bean != null) {
904            if (log.isDebugEnabled()) {
905                Sensor s = getSensor();
906                log.debug("oblock: {}, sensor = {}", getDisplayName(), (s==null?"Dark OBlock":s.getDisplayName()));  // NOI18N
907            }
908            if (bean.equals(getSensor())) {
909                report.add(new NamedBeanUsageReport("OBlockSensor"));  // NOI18N
910            }
911            if (bean.equals(getErrorSensor())) {
912                report.add(new NamedBeanUsageReport("OBlockSensorError"));  // NOI18N
913            }
914            if (bean.equals(getWarrant())) {
915                report.add(new NamedBeanUsageReport("OBlockWarant"));  // NOI18N
916            }
917
918            getPortals().forEach((portal) -> {
919                if (log.isDebugEnabled()) {
920                    log.debug("    portal: {}, fb = {}, tb = {}, fs = {}, ts = {}",  // NOI18N
921                            portal.getName(), portal.getFromBlockName(), portal.getToBlockName(),
922                            portal.getFromSignalName(), portal.getToSignalName());
923                }
924                if (bean.equals(portal.getFromBlock()) || bean.equals(portal.getToBlock())) {
925                    report.add(new NamedBeanUsageReport("OBlockPortalNeighborOBlock", portal.getName()));  // NOI18N
926                }
927                if (bean.equals(portal.getFromSignal()) || bean.equals(portal.getToSignal())) {
928                    report.add(new NamedBeanUsageReport("OBlockPortalSignal", portal.getName()));  // NOI18N
929                }
930
931                portal.getFromPaths().forEach((path) -> {
932                    log.debug("        from path = {}", path.getName());  // NOI18N
933                    path.getSettings().forEach((setting) -> {
934                        log.debug("            turnout = {}", setting.getBean().getDisplayName());  // NOI18N
935                        if (bean.equals(setting.getBean())) {
936                            if (!duplicateCheck.contains(bean)) {
937                                report.add(new NamedBeanUsageReport("OBlockPortalPathTurnout", portal.getName()));  // NOI18N
938                                duplicateCheck.add(bean);
939                            }
940                        }
941                    });
942                });
943                portal.getToPaths().forEach((path) -> {
944                    log.debug("        to path   = {}", path.getName());  // NOI18N
945                    path.getSettings().forEach((setting) -> {
946                        log.debug("            turnout = {}", setting.getBean().getDisplayName());  // NOI18N
947                        if (bean.equals(setting.getBean())) {
948                            if (!duplicateCheck.contains(bean)) {
949                                report.add(new NamedBeanUsageReport("OBlockPortalPathTurnout", portal.getName()));  // NOI18N
950                                duplicateCheck.add(bean);
951                            }
952                        }
953                    });
954                });
955            });
956        }
957        return report;
958    }
959
960    @Override
961    @Nonnull
962    public String getBeanType() {
963        return Bundle.getMessage("BeanNameOBlock");
964    }
965
966    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OBlock.class);
967
968}