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        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.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    // 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    /**
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(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                getErrorSensor().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    public Sensor getErrorSensor() {
275        if (_errNamedSensor == null) {
276            return null;
277        }
278        return _errNamedSensor.getBean();
279    }
280
281    public NamedBeanHandle<Sensor> getNamedErrorSensor() {
282        return _errNamedSensor;
283    }
284
285    @Override
286    public void propertyChange(java.beans.PropertyChangeEvent evt) {
287        if (log.isDebugEnabled()) {
288            log.debug("property change: of \"{}\" property {} is now {} from {}",
289                    getDisplayName(), evt.getPropertyName(), evt.getNewValue(), evt.getSource().getClass().getName());
290        }
291        if ((getErrorSensor() != null) && (evt.getSource().equals(getErrorSensor()))
292            && Sensor.PROPERTY_KNOWN_STATE.equals(evt.getPropertyName())) {
293            int errState = ((Integer) evt.getNewValue());
294            int oldState = getState();
295            if (errState == Sensor.ACTIVE) {
296                setState(oldState | TRACK_ERROR);
297            } else {
298                setState(oldState & ~TRACK_ERROR);
299            }
300            firePropertyChange(PROPERTY_PATH_STATE, oldState, getState());
301        }
302    }
303
304    /**
305     * Another block sharing a turnout with this block queries whether turnout
306     * is in use.
307     *
308     * @param path that uses a common shared turnout
309     * @return If warrant exists and path==pathname, return warrant display
310     *         name, else null.
311     */
312    protected String isPathSet(@Nonnull String path) {
313        String msg = null;
314        if (_warrant != null && path.equals(_pathName)) {
315            msg = _warrant.getDisplayName();
316        }
317        log.trace("Path \"{}\" in oblock \"{}\" {}", path, getDisplayName(),
318            (msg == null ? "not set" : " set in warrant " + msg));
319        return msg;
320    }
321
322    public Warrant getWarrant() {
323        return _warrant;
324    }
325
326    public boolean isAllocatedTo(Warrant warrant) {
327        if (warrant == null) {
328            return false;
329        }
330        return warrant.equals(_warrant);
331    }
332
333    public String getAllocatedPathName() {
334        return _pathName;
335    }
336
337    public void setMetricUnits(boolean type) {
338        _metric = type;
339    }
340
341    public boolean isMetric() {
342        return _metric;
343    }
344
345    public void setMarkerForeground(Color c) {
346        _markerForeground = c;
347    }
348
349    public Color getMarkerForeground() {
350        return _markerForeground;
351    }
352
353    public void setMarkerBackground(Color c) {
354        _markerBackground = c;
355    }
356
357    public Color getMarkerBackground() {
358        return _markerBackground;
359    }
360
361    public void setMarkerFont(Font f) {
362        _markerFont = f;
363    }
364
365    public Font getMarkerFont() {
366        return _markerFont;
367    }
368
369    /**
370     * Update the OBlock status.
371     * Override Block because change must come from an OBlock for Web Server to receive it
372     *
373     * @param v the new state, from OBlock.ALLOCATED etc, named 'status' in JSON Servlet and Web Server
374     */
375    @Override
376    public void setState(int v) {
377        int old = getState();
378        super.setState(v);
379        // override Block to get proper source to be recognized by listener in Web Server
380        log.debug("\"{}\" setState({})", getDisplayName(), getState());
381        firePropertyChange(PROPERTY_STATE, old, getState()); // used by CPE indicator track icons
382    }
383
384    /**
385     * {@inheritDoc}
386     */
387    @Override
388    public void setValue(Object o) {
389        super.setValue(o);
390        if (o == null) {
391            _markerForeground = Color.WHITE;
392            _markerBackground = DEFAULT_FILL_COLOR;
393            _markerFont = null;
394        }
395    }
396
397    /*_
398     *  From the universal name for block status, check if it is the current status
399     */
400    public boolean statusIs(String statusName) {
401        OBlockStatus oblockStatus = OBlockStatus.getByName(statusName);
402        if (oblockStatus != null) {
403            return ((getState() & oblockStatus.getStatus()) != 0);
404        }
405        log.error("\"{}\" type not found.  Update Conditional State Variable testing OBlock \"{}\" status",
406                getDisplayName(), statusName);
407        return false;
408    }
409
410    public boolean isDark() {
411        return (getState() & OBlock.UNDETECTED) != 0;
412    }
413
414    public boolean isOccupied() {
415        return (getState() & OBlock.OCCUPIED) != 0;
416    }
417
418    public String occupiedBy() {
419        Warrant w = _warrant;
420        if (isOccupied()) {
421            if (w != null) {
422                return w.getTrainName();
423            } else {
424                return Bundle.getMessage("unknownTrain");
425            }
426        } else {
427            return null;
428        }
429    }
430
431    /**
432     * Test that block is not occupied and not allocated
433     *
434     * @return true if not occupied and not allocated
435     */
436    public boolean isFree() {
437        int state = getState();
438        return ((state & ALLOCATED) == 0 && (state & OCCUPIED) == 0);
439    }
440
441    /**
442     * Allocate (reserves) the block for the Warrant Note the block may be
443     * OCCUPIED by a non-warranted train, but the allocation is permitted.
444     *
445     * @param warrant the Warrant
446     * @return message with if block is already allocated to another warrant or
447     *         block is OUT_OF_SERVICE
448     */
449    @CheckForNull
450    public String allocate(Warrant warrant) {
451        if (warrant == null) {
452            log.error("allocate(warrant) called with null warrant in block \"{}\"!", getDisplayName());
453            return "ERROR! allocate called with null warrant in block \"" + getDisplayName() + "\"!";
454        }
455        if (_warrant != null) {
456            if (!warrant.equals(_warrant)) {
457                return Bundle.getMessage("AllocatedToWarrant",
458                        _warrant.getDisplayName(), getDisplayName(), _warrant.getTrainName());
459            } else {
460                return null;
461            }
462        }
463        /*
464        int state = getState();
465        if ((state & OUT_OF_SERVICE) != 0) {
466            return Bundle.getMessage("BlockOutOfService", getDisplayName());
467        }*/
468
469        _warrant = warrant;
470        if (log.isDebugEnabled()) {
471            log.debug("Allocate OBlock \"{}\" to warrant \"{}\".",
472                    getDisplayName(), warrant.getDisplayName());
473        }
474        int old = getState();
475        int newState = old | ALLOCATED;
476        super.setState(newState);
477        firePropertyChange(PROPERTY_STATE, old, newState);
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        int old = getState();
573        super.setState(old & ~(ALLOCATED | RUNNING));  // unset allocated and running bits
574        firePropertyChange(PROPERTY_STATE, old, getState());
575        return true;
576    }
577
578    public void setOutOfService(boolean set) {
579        if (set) {
580            setState(getState() | OUT_OF_SERVICE);  // set OoS bit
581        } else {
582            setState(getState() & ~OUT_OF_SERVICE);  // unset OoS bit
583        }
584    }
585
586    public void setError(boolean set) {
587        if (set) {
588            setState(getState() | TRACK_ERROR);  // set err bit
589        } else {
590            setState(getState() & ~TRACK_ERROR);  // unset err bit
591        }
592    }
593
594    /**
595     * Enforce unique portal names. Portals are now managed beans since 2014.
596     * This enforces unique names.
597     *
598     * @param portal the Portal to add
599     */
600    public void addPortal(Portal portal) {
601        String name = getDisplayName();
602        if (!name.equals(portal.getFromBlockName()) && !name.equals(portal.getToBlockName())) {
603            log.warn("{} not in block {}", portal.getDescription(), getDisplayName());
604            return;
605        }
606        String pName = portal.getName();
607        if (pName != null) {  // pName may be null if called from Portal ctor
608            for (Portal value : _portals) {
609                if (pName.equals(value.getName())) {
610                    return;
611                }
612            }
613        }
614        int oldSize = _portals.size();
615        _portals.add(portal);
616        log.trace("add portal \"{}\" to Block \"{}\"", portal.getName(), getDisplayName());
617        firePropertyChange(PROPERTY_PORTAL_COUNT, oldSize, _portals.size());
618    }
619
620    /**
621     * Remove portal from OBlock and stub all paths using this portal to be dead
622     * end spurs.
623     *
624     * @param portal the Portal to remove
625     */
626    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
627    protected void removePortal(@CheckForNull Portal portal) {
628        if (portal != null) {
629            Iterator<Path> iter = getPaths().iterator();
630            while (iter.hasNext()) {
631                OPath path = (OPath) iter.next();
632                if (portal.equals(path.getFromPortal())) {
633                    path.setFromPortal(null);
634                    log.trace("removed Portal {} from Path \"{}\" in oblock {}",
635                            portal.getName(), path.getName(), getDisplayName());
636                }
637                if (portal.equals(path.getToPortal())) {
638                    path.setToPortal(null);
639                    log.trace("removed Portal {} from Path \"{}\" in oblock {}",
640                            portal.getName(), path.getName(), getDisplayName());
641                }
642            }
643            iter = getPaths().iterator();
644            while (iter.hasNext()) {
645                OPath path = (OPath) iter.next();
646                if (path.getFromPortal() == null && path.getToPortal() == null) {
647                    removeOPath(path);
648                    log.trace("removed Path \"{}\" from oblock {}", path.getName(), getDisplayName());
649                }
650            }
651            int oldSize = _portals.size();
652            _portals = _portals.stream().filter(p -> !Objects.equals(p,portal)).collect(Collectors.toList());
653            firePropertyChange(PROPERTY_PORTAL_COUNT, oldSize, _portals.size());
654        }
655    }
656
657    public Portal getPortalByName(String name) {
658        for (Portal po : _portals) {
659            if (po.getName().equals(name)) {
660                return po;
661            }
662        }
663        return null;
664    }
665
666    @Nonnull
667    public List<Portal> getPortals() {
668        return new ArrayList<>(_portals);
669    }
670
671    public void setPortals(ArrayList<Portal> portals) {
672        _portals = portals;
673    }
674
675    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
676    public OPath getPathByName(String name) {
677        for (Path opa : getPaths()) {
678            OPath path = (OPath) opa;
679            if (path.getName().equals(name)) {
680                return path;
681            }
682        }
683        return null;
684    }
685
686    @Override
687    public void setLength(float len) {
688        // Only shorten paths longer than 'len'
689        getPaths().stream().forEach(p -> {
690            if (p.getLength() > len) {
691                p.setLength(len); // set to default
692            }
693        });
694        super.setLength(len);
695    }
696
697    /**
698     * Enforce unique path names within OBlock, but allow a duplicate name of an
699     * OPath from another OBlock to be checked if it is in one of the OBlock's
700     * Portals.
701     *
702     * @param path the OPath to add
703     * @return true if path was added to OBlock
704     */
705    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
706    public boolean addPath(OPath path) {
707        String pName = path.getName();
708        log.trace("addPath \"{}\" to OBlock {}", pName, getSystemName());
709        List<Path> list = getPaths();
710        for (Path p : list) {
711            if (((OPath) p).equals(path)) {
712                log.trace("Path \"{}\" duplicated in OBlock {}", pName, getSystemName());
713                return false;
714            }
715            if (pName.equals(((OPath) p).getName())) {
716                log.trace("Path named \"{}\" already exists in OBlock {}", pName, getSystemName());
717                return false;
718            }
719        }
720        OBlock pathBlock = (OBlock) path.getBlock();
721        if (pathBlock != null && !this.equals(pathBlock)) {
722            log.warn("Path \"{}\" already in block {}, cannot be added to block {}",
723                    pName, pathBlock.getDisplayName(), getDisplayName());
724            return false;
725        }
726        path.setBlock(this);
727        Portal portal = path.getFromPortal();
728        if (portal != null) {
729            if (!portal.addPath(path)) {
730                log.trace("Path \"{}\" rejected by portal  {}", pName, portal.getName());
731                return false;
732            }
733        }
734        portal = path.getToPortal();
735        if (portal != null) {
736            if (!portal.addPath(path)) {
737                log.debug("Path \"{}\" rejected by portal  {}", pName, portal.getName());
738                return false;
739            }
740        }
741        super.addPath(path);
742        firePropertyChange(PROPERTY_PATH_COUNT, null, getPaths().size());
743        return true;
744    }
745
746    public boolean removeOPath(OPath path) {
747        jmri.Block block = path.getBlock();
748        if (block != null && !getSystemName().equals(block.getSystemName())) {
749            return false;
750        }
751        if (!InstanceManager.getDefault(jmri.jmrit.logix.WarrantManager.class).okToRemoveBlockPath(this, path)) {
752            return false;
753        }
754        path.clearSettings();
755        super.removePath(path);
756        // remove path from its portals
757        Portal portal = path.getToPortal();
758        if (portal != null) {
759            portal.removePath(path);
760        }
761        portal = path.getFromPortal();
762        if (portal != null) {
763            portal.removePath(path);
764        }
765        path.dispose();
766        firePropertyChange(PROPERTY_PATH_COUNT, path, getPaths().size());
767        return true;
768    }
769
770    /**
771     * Set Turnouts for the path.
772     * <p>
773     * Called by warrants to set turnouts for a train it is able to run.
774     * The warrant parameter verifies that the block is
775     * indeed allocated to the warrant. If the block is unwarranted then the
776     * block is allocated to the calling warrant. A logix conditional may also
777     * call this method with a null warrant parameter for manual logix control.
778     * If the block is under a different warrant the call will be rejected.
779     *
780     * @param pathName name of the path
781     * @param warrant  warrant the block is allocated to
782     * @return error message if the call fails. null if the call succeeds
783     */
784    protected String setPath(String pathName, Warrant warrant) {
785        OPath path = getPathByName(pathName);
786        if (path == null) {
787            return Bundle.getMessage("PathNotFound", pathName, getDisplayName());
788        }
789        if (warrant == null || !warrant.equals(_warrant)) {
790            String name;
791            if (_warrant != null) {
792                name = _warrant.getDisplayName();
793            } else {
794                name = Bundle.getMessage("Warrant");
795            }
796            return Bundle.getMessage("PathNotSet", pathName, getDisplayName(), name);
797        }
798        _pathName = pathName;
799        int lockState = Turnout.CABLOCKOUT & Turnout.PUSHBUTTONLOCKOUT;
800        path.setTurnouts(0, true, lockState, true);
801        firePropertyChange(PROPERTY_PATH_STATE, 0, getState());
802        if (log.isTraceEnabled()) {
803            log.debug("setPath: Path \"{}\" in path \"{}\" {} set for warrant {}",
804                    pathName, getDisplayName(), _pathName, warrant.getDisplayName());
805        }
806        return null;
807    }
808
809    protected OPath getPath() {
810        if (_pathName == null) {
811            return null;
812        }
813        return getPathByName(_pathName);
814    }
815
816    /*
817     * Call for Circuit Builder to make icon color changes for its GUI
818     */
819    public void pseudoPropertyChange(String propName, Object old, Object n) {
820        log.trace("pseudoPropertyChange: Block \"{}\" property \"{}\" new value= {}",
821                getSystemName(), propName, n);
822        firePropertyChange(propName, old, n);
823    }
824
825    /**
826     * (Override) Handles Block sensor going INACTIVE: this block is empty.
827     * Called by handleSensorChange
828     */
829    @Override
830    public void goingInactive() {
831        //log.debug("OBlock \"{}\" going UNOCCUPIED from state= {}", getDisplayName(), getState());
832        // preserve the non-sensor states
833        // non-UNOCCUPIED sensor states are removed (also cannot be RUNNING there if being UNOCCUPIED)
834        setState((getState() & ~(UNKNOWN | OCCUPIED | INCONSISTENT | RUNNING)) | UNOCCUPIED);
835        setValue(null);
836        if (_warrant != null) {
837            ThreadingUtil.runOnLayout(() -> _warrant.goingInactive(this));
838        }
839    }
840
841    /**
842     * (Override) Handles Block sensor going ACTIVE: this block is now occupied,
843     * figure out from who and copy their value. Called by handleSensorChange
844     */
845    @Override
846    public void goingActive() {
847        // preserve the non-sensor states when being OCCUPIED and remove non-OCCUPIED sensor states
848        setState((getState() & ~(UNKNOWN | UNOCCUPIED | INCONSISTENT)) | OCCUPIED);
849        _entryTime = System.currentTimeMillis();
850        if (_warrant != null) {
851            ThreadingUtil.runOnLayout(() -> _warrant.goingActive(this));
852        }
853    }
854
855    @Override
856    public void goingUnknown() {
857        setState((getState() & ~(UNOCCUPIED | OCCUPIED | INCONSISTENT)) | UNKNOWN);
858    }
859
860    @Override
861    public void goingInconsistent() {
862        setState((getState() & ~(UNKNOWN | UNOCCUPIED | OCCUPIED)) | INCONSISTENT);
863    }
864
865    @Override
866    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
867    public void dispose() {
868        if (!InstanceManager.getDefault(WarrantManager.class).okToRemoveBlock(this)) {
869            return;
870        }
871        firePropertyChange(PROPERTY_DELETED, null, null);
872        // remove paths first
873        for (Path pa : getPaths()) {
874            removeOPath((OPath)pa);
875        }
876        for (Portal portal : getPortals()) {
877            if (log.isTraceEnabled()) {
878                log.debug("this = {}, toBlock = {}, fromblock= {}", getDisplayName(),
879                        portal.getToBlock().getDisplayName(), portal.getFromBlock().getDisplayName());
880            }
881            if (this.equals(portal.getToBlock())) {
882                portal.setToBlock(null, false);
883            }
884            if (this.equals(portal.getFromBlock())) {
885                portal.setFromBlock(null, false);
886            }
887        }
888        _portals.clear();
889        for (PropertyChangeListener listener : getPropertyChangeListeners()) {
890            removePropertyChangeListener(listener);
891        }
892        jmri.InstanceManager.getDefault(OBlockManager.class).deregister(this);
893        super.dispose();
894    }
895
896    public String getDescription() {
897        return java.text.MessageFormat.format(
898                Bundle.getMessage("BlockDescription"), getDisplayName());
899    }
900
901    @Override
902    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
903        List<NamedBeanUsageReport> report = new ArrayList<>();
904        List<NamedBean> duplicateCheck = new ArrayList<>();
905        if (bean != null) {
906            if (log.isDebugEnabled()) {
907                Sensor s = getSensor();
908                log.debug("oblock: {}, sensor = {}", getDisplayName(), (s==null?"Dark OBlock":s.getDisplayName()));  // NOI18N
909            }
910            if (bean.equals(getSensor())) {
911                report.add(new NamedBeanUsageReport("OBlockSensor"));  // NOI18N
912            }
913            if (bean.equals(getErrorSensor())) {
914                report.add(new NamedBeanUsageReport("OBlockSensorError"));  // NOI18N
915            }
916            if (bean.equals(getWarrant())) {
917                report.add(new NamedBeanUsageReport("OBlockWarant"));  // NOI18N
918            }
919
920            getPortals().forEach((portal) -> {
921                if (log.isDebugEnabled()) {
922                    log.debug("    portal: {}, fb = {}, tb = {}, fs = {}, ts = {}",  // NOI18N
923                            portal.getName(), portal.getFromBlockName(), portal.getToBlockName(),
924                            portal.getFromSignalName(), portal.getToSignalName());
925                }
926                if (bean.equals(portal.getFromBlock()) || bean.equals(portal.getToBlock())) {
927                    report.add(new NamedBeanUsageReport("OBlockPortalNeighborOBlock", portal.getName()));  // NOI18N
928                }
929                if (bean.equals(portal.getFromSignal()) || bean.equals(portal.getToSignal())) {
930                    report.add(new NamedBeanUsageReport("OBlockPortalSignal", portal.getName()));  // NOI18N
931                }
932
933                portal.getFromPaths().forEach((path) -> {
934                    log.debug("        from path = {}", path.getName());  // NOI18N
935                    path.getSettings().forEach((setting) -> {
936                        log.debug("            turnout = {}", setting.getBean().getDisplayName());  // NOI18N
937                        if (bean.equals(setting.getBean())) {
938                            if (!duplicateCheck.contains(bean)) {
939                                report.add(new NamedBeanUsageReport("OBlockPortalPathTurnout", portal.getName()));  // NOI18N
940                                duplicateCheck.add(bean);
941                            }
942                        }
943                    });
944                });
945                portal.getToPaths().forEach((path) -> {
946                    log.debug("        to path   = {}", path.getName());  // NOI18N
947                    path.getSettings().forEach((setting) -> {
948                        log.debug("            turnout = {}", setting.getBean().getDisplayName());  // NOI18N
949                        if (bean.equals(setting.getBean())) {
950                            if (!duplicateCheck.contains(bean)) {
951                                report.add(new NamedBeanUsageReport("OBlockPortalPathTurnout", portal.getName()));  // NOI18N
952                                duplicateCheck.add(bean);
953                            }
954                        }
955                    });
956                });
957            });
958        }
959        return report;
960    }
961
962    @Override
963    @Nonnull
964    public String getBeanType() {
965        return Bundle.getMessage("BeanNameOBlock");
966    }
967
968    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OBlock.class);
969
970}