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