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