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