001package jmri.jmrit.logix;
002
003import java.beans.PropertyChangeListener;
004import java.beans.PropertyChangeSupport;
005import java.util.ArrayList;
006import java.util.List;
007import jmri.Block;
008import jmri.InstanceManager;
009import jmri.NamedBean;
010import jmri.SignalHead;
011import jmri.SignalMast;
012import jmri.implementation.SignalSpeedMap;
013
014import javax.annotation.Nonnull;
015import javax.annotation.CheckForNull;
016import javax.annotation.OverridingMethodsMustInvokeSuper;
017
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021/**
022 * A Portal is a boundary between two Blocks.
023 * <p>
024 * A Portal has Lists of the {@link OPath}s that connect through it.
025 * The direction of trains passing through the portal is managed from the
026 * BlockOrders of the Warrant the train is running under.
027 * The Portal fires a PropertyChangeEvent that a
028 * {@link jmri.jmrit.display.controlPanelEditor.PortalIcon} can listen
029 * for to set direction arrows for a given route.
030 *
031 * The Portal also supplies speed information from any signals set at its
032 * location that the Warrant passes on the Engineer.
033 *
034 * @author  Pete Cressman Copyright (C) 2009
035 */
036public class Portal {
037
038    private static final String NAME_CHANGE = "NameChange";
039    private static final String SIGNAL_CHANGE = "signalChange";
040    private static final String ENTRANCE = "entrance";
041    private final ArrayList<OPath> _fromPaths = new ArrayList<>();
042    private OBlock _fromBlock;
043    private NamedBean _fromSignal;          // may be either SignalHead or SignalMast
044    private float _fromSignalOffset;        // adjustment distance for speed change
045    private final ArrayList<OPath> _toPaths = new ArrayList<>();
046    private OBlock _toBlock;
047    private NamedBean _toSignal;            // may be either SignalHead or SignalMast
048    private float _toSignalOffset;          // adjustment distance for speed change
049    private String _name;
050    private int _state = UNKNOWN;
051    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
052
053    public static final int UNKNOWN = 0x01;
054    public static final int ENTER_TO_BLOCK = 0x02;
055    public static final int ENTER_FROM_BLOCK = 0x04;
056
057    public Portal(String uName) {
058        _name = uName;
059    }
060
061    /**
062     * Determine which list the Path belongs to and add it to that list.
063     *
064     * @param path OPath to add
065     * @return false if Path does not have a matching block for this Portal
066     */
067    public boolean addPath(@Nonnull OPath path) {
068        Block block = path.getBlock();
069        if (block == null) {
070            log.error("Path \"{}\" has no block.", path.getName());
071            return false;
072        }
073        if (!this.equals(path.getFromPortal())
074                && !this.equals(path.getToPortal())) {
075            return false;
076        }
077        if ((_fromBlock != null) && _fromBlock.equals(block)) {
078            return addPath(_fromPaths, path);
079        } else if ((_toBlock != null) && _toBlock.equals(block)) {
080            return addPath(_toPaths, path);
081        }
082        // portal is incomplete or path block not in this portal
083        return false;
084    }
085
086    /**
087     * Utility for both path lists.
088     * Checks for duplicate name.
089     */
090    private boolean addPath(@Nonnull List<OPath> list, @Nonnull OPath path) {
091        String pName = path.getName();
092        for (OPath p : list) {
093            if (p.equals(path)) {
094                if (pName.equals(p.getName())) {
095                    return true;    // OK, everything equal
096                } else {
097                    log.warn("Path \"{}\" is duplicate of path \"{}\" in Portal \"{}\" from block {}.", path.getName(), p.getName(), _name, path.getBlock().getDisplayName());
098                    return false;
099                }
100            } else if (pName.equals(p.getName())) {
101                log.warn("Path \"{}\" is duplicate name for another path in Portal \"{}\" from block {}.", path.getName(), _name, path.getBlock().getDisplayName());
102                return false;
103            }
104        }
105        list.add(path);
106        return true;
107    }
108
109    /**
110     * Remove an OPath from this Portal.
111     * Checks both the _fromBlock list as the _toBlock list.
112     *
113     * @param path the OPath to remove
114     */
115    public void removePath(@Nonnull OPath path) {
116        Block block = path.getBlock();
117        if (block == null) {
118            log.error("Path \"{}\" has no block.", path.getName());
119            return;
120        }
121        log.debug("removePath: {}", this);
122        if (!this.equals(path.getFromPortal())
123                && !this.equals(path.getToPortal())) {
124            return;
125        }
126        if (_fromBlock != null && _fromBlock.equals(block)) {
127            _fromPaths.remove(path);
128        } else if (_toBlock != null && _toBlock.equals(block)) {
129            _toPaths.remove(path);
130        }
131//        pcs.firePropertyChange("RemovePath", block, path); not needed
132    }
133
134    /**
135     * Set userName of this Portal. Checks if name is available.
136     *
137     * @param newName name for path
138     * @return return error message, null if name change is OK
139     */
140    public String setName(String newName) {
141        if (newName == null || newName.length() == 0) {
142            return null;
143        }
144        String oldName = _name;
145        if (newName.equals(oldName)) {
146            return null;
147        }
148        Portal p = InstanceManager.getDefault(PortalManager.class).getPortal(newName);
149        if (p != null) {
150            return Bundle.getMessage("DuplicatePortalName", newName, p.getDescription());
151        }
152        _name = newName;
153        InstanceManager.getDefault(WarrantManager.class).portalNameChange(oldName, newName);
154
155        // for some unknown reason, PortalManager firePropertyChange is not read by PortalTableModel
156        // so let OBlock do it
157        if (_toBlock != null) {
158            _toBlock.pseudoPropertyChange(NAME_CHANGE, oldName, this);
159        } else if (_fromBlock != null) {
160            _fromBlock.pseudoPropertyChange(NAME_CHANGE, oldName, this);
161        }
162        // CircuitBuilder PortalList needs this property change
163        pcs.firePropertyChange(NAME_CHANGE, oldName, newName);
164        return null;
165    }
166
167    public String getName() {
168        return _name;
169    }
170
171    /**
172     * Set this portal's toBlock. Remove this portal from old toBlock, if any.
173     * Add this portal in the new toBlock's list of portals.
174     *
175     * @param block to be the new toBlock
176     * @param changePaths if true, set block in paths. If false,
177     *                    verify that all toPaths are contained in the block.
178     * @return false if paths are not in the block
179     */
180    public boolean setToBlock(OBlock block, boolean changePaths) {
181        if (((block != null) && block.equals(_toBlock)) || ((block == null) && (_toBlock == null))) {
182            return true;
183        }
184        if (changePaths) {
185            // Switch paths to new block. User will need to verify connections
186            for (OPath opa : _toPaths) {
187                opa.setBlock(block);
188            }
189        } else if (!verify(_toPaths, block)) {
190            return false;
191        }
192        log.debug("setToBlock: oldBlock= \"{}\" newBlock \"{}\".", getToBlockName(),
193              (block != null ? block.getDisplayName() : null));
194        OBlock oldBlock = _toBlock;
195        if (_toBlock != null) {
196            _toBlock.removePortal(this);    // may should not
197        }
198        _toBlock = block;
199        if (_toBlock != null) {
200            _toBlock.addPortal(this);
201        }
202        pcs.firePropertyChange("BlockChanged", oldBlock, _toBlock);
203        return true;
204    }
205
206    public OBlock getToBlock() {
207        return _toBlock;
208    }
209
210    public String getToBlockName() {
211        return (_toBlock != null ? _toBlock.getDisplayName() : null);
212    }
213
214    public List<OPath> getToPaths() {
215        return _toPaths;
216    }
217
218    /**
219     * Set this portal's fromBlock. Remove this portal from old fromBlock, if any.
220     * Add this portal in the new toBlock's list of portals.
221     *
222     * @param block to be the new fromBlock
223     * @param changePaths if true, set block in paths. If false,
224     *                    verify that all toPaths are contained in the block.
225     * @return false if paths are not in the block
226     */
227    public boolean setFromBlock(OBlock block, boolean changePaths) {
228        if ((block != null && block.equals(_fromBlock)) || (block == null && _fromBlock == null)) {
229            return true;
230        }
231        if (changePaths) {
232            //Switch paths to new block.  User will need to verify connections
233            for (OPath fromPath : _fromPaths) {
234                fromPath.setBlock(block);
235            }
236        } else if (!verify(_fromPaths, block)) {
237            return false;
238        }
239        log.debug("setFromBlock: oldBlock= \"{}\" newBlock \"{}\".", getFromBlockName(),
240             (block != null ? block.getDisplayName() : null));
241        OBlock oldBlock = _fromBlock;
242        if (_fromBlock != null) {
243            _fromBlock.removePortal(this);
244        }
245        _fromBlock = block;
246        if (_fromBlock != null) {
247            _fromBlock.addPortal(this);
248        }
249        pcs.firePropertyChange("BlockChanged", oldBlock, _fromBlock);
250        return true;
251    }
252
253    public OBlock getFromBlock() {
254        return _fromBlock;
255    }
256
257    public String getFromBlockName() {
258        return (_fromBlock != null ? _fromBlock.getDisplayName() : null);
259    }
260
261    public List<OPath> getFromPaths() {
262        return _fromPaths;
263    }
264
265    /**
266     * Set a signal to protect an OBlock. Warrants look ahead for speed changes
267     * and change the train speed accordingly.
268     *
269     * @param signal either a SignalMast or a SignalHead. Set to null to remove (previous) signal from Portal
270     * @param length offset length in millimeters. This is additional
271     *               entrance space for the block. This distance added to or subtracted
272     *               from the calculation of the ramp distance when a warrant must slow
273     *               the train in response to the aspect or appearance of the signal.
274     * @param protectedBlock OBlock the signal protects
275     * @return true if signal is set
276     */
277    public boolean setProtectSignal(@CheckForNull NamedBean signal, float length, OBlock protectedBlock) {
278        if (protectedBlock == null) {
279            return false;
280        }
281        boolean ret = false;
282        if ((_fromBlock != null) && _fromBlock.equals(protectedBlock)) {
283            _toSignal = signal;
284            _toSignalOffset = length;
285            log.debug("OPortal FromBlock Offset set to {} on signal {}", _toSignalOffset,
286                    (_toSignal != null ? _toSignal.getDisplayName() : "<removed>"));
287            ret = true;
288        }
289        if ((_toBlock != null) && _toBlock.equals(protectedBlock)) {
290            _fromSignal = signal;
291            _fromSignalOffset = length;
292            log.debug("OPortal ToBlock Offset set to {} on signal {}", _fromSignalOffset,
293                    (_fromSignal != null ? _fromSignal.getDisplayName() : "<removed>"));
294            ret = true;
295        }
296        if (ret) {
297            protectedBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, true);
298            pcs.firePropertyChange(SIGNAL_CHANGE, false, true);
299            log.debug("setProtectSignal: \"{}\" for Block= {} at Portal {}",
300                    (signal != null ? signal.getDisplayName() : "null"), protectedBlock.getDisplayName(), _name);
301        }
302        return ret;
303    }
304
305    /**
306     * Get the signal (either a SignalMast or a SignalHead) protecting an OBlock.
307     *
308     * @param block is the direction of entry, i.e. the protected block
309     * @return signal protecting block, if block is protected, otherwise null.
310     */
311    public NamedBean getSignalProtectingBlock(@Nonnull OBlock block) {
312        if (block.equals(_toBlock)) {
313            return _fromSignal;
314        } else if (block.equals(_fromBlock)) {
315            return _toSignal;
316        }
317        return null;
318    }
319
320    /**
321     * Get the OBlock protected by a signal.
322     *
323     * @param signal is the signal, either a SignalMast or a SignalHead
324     * @return Protected OBlock, if it is protected, otherwise null.
325     */
326    public OBlock getProtectedBlock(NamedBean signal) {
327        if (signal == null) {
328            return null;
329        }
330        if (signal.equals(_fromSignal)) {
331            return _toBlock;
332        } else if (signal.equals(_toSignal)) {
333            return _fromBlock;
334        }
335        return null;
336    }
337
338    public NamedBean getFromSignal() {
339        return _fromSignal;
340    }
341
342    public String getFromSignalName() {
343        return (_fromSignal != null ? _fromSignal.getDisplayName() : null);
344    }
345
346    public float getFromSignalOffset() {
347        return _fromSignalOffset; // it seems clear that this method should return what is asks
348    }
349
350    public NamedBean getToSignal() {
351        return _toSignal;
352    }
353
354    public String getToSignalName() {
355        return (_toSignal != null ? _toSignal.getDisplayName() : null);
356    }
357
358    public float getToSignalOffset() {
359        return _toSignalOffset;
360    }
361
362    public void deleteSignal(@Nonnull NamedBean signal) {
363        if (signal.equals(_toSignal)) {
364            _toSignal = null; // set the 2 _tos
365            _toSignalOffset = 0;
366            if (_fromBlock != null) {
367                _fromBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, false);
368                pcs.firePropertyChange(SIGNAL_CHANGE, false, false);
369            }
370        } else if (signal.equals(_fromSignal)) {
371            _fromSignal = null; // set the 2 _froms
372            _fromSignalOffset = 0;
373            if (_toBlock != null) {
374                _toBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, false);
375                pcs.firePropertyChange(SIGNAL_CHANGE, false, false);
376            }
377        }
378    }
379
380    public static NamedBean getSignal(String name) {
381        NamedBean signal = InstanceManager.getDefault(jmri.SignalMastManager.class).getSignalMast(name);
382        if (signal == null) {
383            signal = InstanceManager.getDefault(jmri.SignalHeadManager.class).getSignalHead(name);
384        }
385        return signal;
386    }
387
388    /**
389     * Get the paths to the portal within the connected OBlock i.e. the paths in
390     * this (the param) block through the Portal.
391     *
392     * @param block OBlock
393     * @return null if portal does not connect to block
394     */
395    public List<OPath> getPathsWithinBlock(OBlock block) {
396        if (block == null) {
397            return null;
398        }
399        if (block.equals(_fromBlock)) {
400            return _fromPaths;
401        } else if (block.equals(_toBlock)) {
402            return _toPaths;
403        }
404        return null;
405    }
406
407    /**
408     * Get the OBlock on the other side of the Portal from the given
409     * OBlock.
410     *
411     * @param block starting OBlock
412     * @return the opposite block
413     */
414    public OBlock getOpposingBlock(@Nonnull OBlock block) {
415        if (block.equals(_fromBlock)) {
416            return _toBlock;
417        } else if (block.equals(_toBlock)) {
418            return _fromBlock;
419        }
420        return null;
421    }
422
423    /**
424     * Get the paths from the portal in the next connected OBlock i.e. paths in
425     * the block on the other side of the portal from this (the param) block.
426     *
427     * @param block OBlock
428     * @return null if portal does not connect to block
429     */
430    public List<OPath> getPathsFromOpposingBlock(@Nonnull OBlock block) {
431        if (block.equals(_fromBlock)) {
432            return _toPaths;
433        } else if (block.equals(_toBlock)) {
434            return _fromPaths;
435        }
436        return null;
437    }
438
439    /**
440     * Call is from BlockOrder when setting the path.
441     *
442     * @param block OBlock
443     */
444    protected void setEntryState(OBlock block) {
445        if (block == null) {
446            _state = UNKNOWN;
447        } else if (block.equals(_fromBlock)) {
448            setState(ENTER_FROM_BLOCK);
449        } else if (block.equals(_toBlock)) {
450            setState(ENTER_TO_BLOCK);
451        }
452    }
453
454    public void setState(int s) {
455        int old = _state;
456        _state = s;
457        pcs.firePropertyChange("Direction", old, _state);
458    }
459
460    public int getState() {
461        return _state;
462    }
463
464    @OverridingMethodsMustInvokeSuper
465    public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
466        pcs.addPropertyChangeListener(listener);
467    }
468
469    @OverridingMethodsMustInvokeSuper
470    public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
471        pcs.removePropertyChangeListener(listener);
472    }
473
474    /**
475     * Set the distance (plus or minus) in millimeters from the portal gap
476     * where the speed change indicated by the signal should be completed.
477     *
478     * @param block a protected OBlock
479     * @param distance length in millimeters, called Offset in the OBlock Signal Table
480     */
481    public void setEntranceSpaceForBlock(@Nonnull OBlock block, float distance) {
482        if (block.equals(_toBlock)) {
483            if (_fromSignal != null) {
484                _fromSignalOffset = distance;
485            }
486        } else if (block.equals(_fromBlock)) {
487            if (_toSignal != null) {
488                _toSignalOffset = distance;
489            }
490        }
491    }
492
493    /**
494     * Get the distance (plus or minus) in millimeters from the portal gap
495     * where the speed change indicated by the signal should be completed.
496     * Property is called Offset in the OBlock Signal Table.
497     *
498     * @param block a protected OBlock
499     * @return distance
500     */
501    public float getEntranceSpaceForBlock(@Nonnull OBlock block) {
502        if (block.equals(_toBlock)) {
503            if (_fromSignal != null) {
504                return _fromSignalOffset;
505            }
506        } else if (block.equals(_fromBlock)) {
507            if (_toSignal != null) {
508                return _toSignalOffset;
509            }
510        }
511        return 0;
512    }
513
514    /**
515     * Check signals, if any, for speed into/out of a given block. The signal that protects
516     * the "to" block is the signal facing the "from" Block, i.e. the "from"
517     * signal. (and vice-versa)
518     *
519     * @param block is the direction of entry, "from" block
520     * @param entrance true for EntranceSpeed, false for ExitSpeed
521     * @return permissible speed, null if no signal
522     */
523    public String getPermissibleSpeed(@Nonnull OBlock block, boolean entrance) {
524        String speed = null;
525        String blockName = block.getDisplayName();
526        if (block.equals(_toBlock)) {
527            if (_fromSignal != null) {
528                if (_fromSignal instanceof SignalHead) {
529                    speed = getPermissibleSignalSpeed((SignalHead) _fromSignal, entrance);
530                } else {
531                    speed = getPermissibleSignalSpeed((SignalMast) _fromSignal, entrance);
532                }
533            }
534        } else if (block.equals(_fromBlock)) {
535            if (_toSignal != null) {
536                if (_toSignal instanceof SignalHead) {
537                    speed = getPermissibleSignalSpeed((SignalHead) _toSignal, entrance);
538                } else {
539                    speed = getPermissibleSignalSpeed((SignalMast) _toSignal, entrance);
540                }
541            }
542        } else {
543            log.error("Block \"{}\" is not in Portal \"{}\".", blockName, _name);
544        }
545        if (log.isDebugEnabled()) {
546            if (speed != null) {
547                log.debug("Portal \"{}\" has {} speed= {} into \"{}\" from signal.",
548                        _name, (entrance ? "ENTRANCE" : "EXIT"), speed, blockName);
549            }
550        }
551        // no signals, proceed at recorded speed
552        return speed;
553    }
554
555    /**
556     * Get entrance or exit speed set on signal head.
557     *
558     * @param signal signal head to query
559     * @param entrance true for EntranceSpeed, false for ExitSpeed
560     * @return permissible speed, Restricted if no speed set on signal
561     */
562    private static @Nonnull String getPermissibleSignalSpeed(@Nonnull SignalHead signal, boolean entrance) {
563        int appearance = signal.getAppearance();
564        String speed = InstanceManager.getDefault(SignalSpeedMap.class).getAppearanceSpeed(signal.getAppearanceName(appearance));
565        // on head, speed is the same for entry and exit
566        if (speed == null) {
567            log.error("SignalHead \"{}\" has no {} speed specified for appearance \"{}\"! - Restricting Movement!",
568                    signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), signal.getAppearanceName(appearance));
569            speed = "Restricted";
570        }
571        log.debug("SignalHead \"{}\" has {} speed notch= {} from appearance \"{}\"",
572                signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), speed, signal.getAppearanceName(appearance));
573        return speed;
574    }
575
576    /**
577     * Get entrance or exit speed set on signal mast.
578     *
579     * @param signal signal mast to query
580     * @param entrance true for EntranceSpeed, false for ExitSpeed
581     * @return permissible speed, Restricted if no speed set on signal
582     */
583    private static @Nonnull String getPermissibleSignalSpeed(@Nonnull SignalMast signal, boolean entrance) {
584        String aspect = signal.getAspect(); 
585        String signalAspect = ( aspect == null ? "" : aspect );
586        String speed;
587        if (entrance) {
588            speed = InstanceManager.getDefault(SignalSpeedMap.class).getAspectSpeed(signalAspect, signal.getSignalSystem());
589        } else {
590            speed = InstanceManager.getDefault(SignalSpeedMap.class).getAspectExitSpeed(signalAspect, signal.getSignalSystem());
591        }
592        if (speed == null) {
593            log.error("SignalMast \"{}\" has no {} speed specified for aspect \"{}\"! - Restricting Movement!",
594                    signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), aspect);
595            speed = "Restricted";
596        }
597        log.debug("SignalMast \"{}\" has {} speed notch= {} from aspect \"{}\"",
598                signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), speed, aspect);
599        return speed;
600    }
601
602    /**
603     * Verify that each path has this potential block as its owning block.
604     * Block is a potential _toBlock and Paths are the current _toPaths 
605     * or
606     * Block is a potential _fromBlock and Paths are the current _fromPaths
607     */
608    private static boolean verify(List<OPath> paths, OBlock block) {
609        if (block == null) {
610            return (paths.isEmpty());
611        }
612        String name = block.getSystemName();
613        for (OPath path : paths) {
614            Block blk = path.getBlock();
615            if (blk == null) {
616                log.error("Path \"{}\" belongs to null block. Cannot verify set block to \"{}\"",
617                        path.getName(), name);
618                return false;
619            }
620            String pathName = blk.getSystemName();
621            if (!pathName.equals(name)) {
622                log.warn("Path \"{}\" belongs to block \"{}\". Cannot verify set block to \"{}\"",
623                        path.getName(), pathName, name);
624                return false;
625            }
626        }
627        return true;
628    }
629
630    /**
631     * Check if path connects to Portal.
632     *
633     * @param path OPath to test
634     * @return true if valid
635     */
636    public boolean isValidPath(OPath path) {
637        String name = path.getName();
638        for (OPath toPath : _toPaths) {
639            if (toPath.getName().equals(name)) {
640                return true;
641            }
642        }
643        for (OPath fromPath : _fromPaths) {
644            if (fromPath.getName().equals(name)) {
645                return true;
646            }
647        }
648        return false;
649    }
650
651    /**
652     * Check portal has both blocks and they are different blocks.
653     *
654     * @return true if valid
655     */
656    public boolean isValid() {
657        if (_toBlock == null || _fromBlock==null) {
658            return false;
659        }
660        return (!_toBlock.equals(_fromBlock));
661    }
662
663    @OverridingMethodsMustInvokeSuper
664    public boolean dispose() {
665        if (!InstanceManager.getDefault(jmri.jmrit.logix.WarrantManager.class).okToRemovePortal(this)) {
666            return false;
667        }
668        if (_toBlock != null) {
669            _toBlock.removePortal(this);
670        }
671        if (_fromBlock != null) {
672            _fromBlock.removePortal(this);
673        }
674        pcs.firePropertyChange("portalDelete", true, false);
675        PropertyChangeListener[] listeners = pcs.getPropertyChangeListeners();
676        for (PropertyChangeListener l : listeners) {
677            pcs.removePropertyChangeListener(l);
678        }
679        return true;
680    }
681
682    public String getDescription() {
683        return Bundle.getMessage("PortalDescription",
684                _name, getFromBlockName(), getToBlockName());
685    }
686
687    @Override
688    @Nonnull
689    public String toString() {
690        StringBuilder sb = new StringBuilder("Portal \"");
691        sb.append(_name);
692        sb.append("\" from block \"");
693        sb.append(getFromBlockName());
694        sb.append("\" to block \"");
695        sb.append(getToBlockName());
696        sb.append("\"");
697        return sb.toString();
698    }
699
700    private static final Logger log = LoggerFactory.getLogger(Portal.class);
701
702}