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