001package jmri.implementation;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.beans.PropertyChangeSupport;
006import java.util.List;
007import java.util.Set;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.concurrent.GuardedBy;
011import javax.annotation.Nonnull;
012
013import jmri.Block;
014import jmri.BlockManager;
015import jmri.CabSignal;
016import jmri.InstanceManager;
017import jmri.LocoAddress;
018import jmri.SignalMast;
019import jmri.Path;
020import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
021
022/**
023 * Default implementation of a Cab Signal Object, describing the state of the 
024 * track ahead relative to a locomotive with a given address.  This is 
025 * effectively a mobile signal mast.
026 *
027 * @author Steve Young Copyright (C) 2018
028 * @author Paul Bender Copyright (C) 2019
029 */
030public class DefaultCabSignal implements CabSignal, PropertyChangeListener {
031
032    private LocoAddress _address = null;
033    @GuardedBy("this")
034    private Block _currentBlock = null;
035    private Block _nextBlock = null;
036    private SignalMast _nextMast = null;
037    private boolean _cabSignalActive = true;
038    private boolean _masterPausedButtonActive = false;
039    private PropertyChangeListener _cconSignalMastListener = null;
040
041    public DefaultCabSignal(LocoAddress address){
042       _address = address;
043    }
044
045    /**
046     * A method for cleaning up the cab signal 
047     */
048    @Override
049    @javax.annotation.OverridingMethodsMustInvokeSuper // to remove Signal Listener
050    public void dispose(){
051        if (_nextMast != null) {
052            _nextMast.removePropertyChangeListener(_cconSignalMastListener);
053        }
054       _address = null;
055       _currentBlock = null;
056       _nextBlock = null;
057       _nextMast = null;
058       _cabSignalActive = true;
059       _masterPausedButtonActive = false;
060    }
061
062    /**
063     * Get the LocoAddress associated with the consist
064     *
065     * @return the cab signal address
066     */
067    @Override
068    public LocoAddress getCabSignalAddress(){
069        return _address;
070    }
071
072    /**
073     * Set the Block of the locomotive
074     *
075     * @param position is a Block the locomotive is in.
076     */
077    @Override
078    public synchronized void setBlock(Block position){
079        log.debug("CabSignal for {} set block {}",getCabSignalAddress(),position);
080        Block oldCurrentBlock = _currentBlock;
081        if(_currentBlock!=null){
082           _currentBlock.removePropertyChangeListener(this);
083        }
084        _currentBlock = position;
085        if(_currentBlock!=null) {
086           _currentBlock.addPropertyChangeListener(this);
087           if(!_currentBlock.equals(oldCurrentBlock)) {
088              firePropertyChange("CurrentBlock",_currentBlock,oldCurrentBlock);
089           }
090       } else {
091           // currentblock is null, notify if old block was not.
092           if(oldCurrentBlock!=null){
093              firePropertyChange("CurrentBlock",_currentBlock,oldCurrentBlock);
094           }
095       }
096       getNextBlock(); // calculate the next block and fire an appropriate property change.
097       // calculate the next mast and fire an appropriate property change.
098       forwardCabSignalToLayout();
099    }
100
101    /**
102     * Set the Block of the locomotive by searching the block list.
103     */
104    @Override
105    public synchronized void setBlock(){
106        BlockManager bmgr = InstanceManager.getDefault(BlockManager.class);
107        Set<Block> blockSet = bmgr.getNamedBeanSet();
108        LocoAddress addr = getCabSignalAddress();
109        for (Block blockVal : blockSet) {
110            Object val = blockVal.getValue();
111            if ( val != null ) {
112                log.debug("CabSignal for {} searching block {} value {}",
113                           addr,blockVal,val);
114                if (val instanceof jmri.AddressedIdTag) {
115                    if( ((jmri.AddressedIdTag)val).getLocoAddress().toString().equals( 
116                         addr.toString())){
117                       setBlock(blockVal); 
118                       return;
119                    }
120                } else if ( val.equals(addr) ||
121                    val.toString().equals(addr.toString()) || 
122                    val.toString().equals("" + addr.getNumber())) {
123                    setBlock(blockVal);
124                    return;
125                }
126            }
127        }
128        // address not found in any block, set block to null
129        setBlock(null);
130    }
131
132    /**
133     * Get the Block position of the locomotive associated with the cab signal.
134     *
135     * @return The current Block position
136     */
137    @Override
138    public synchronized Block getBlock(){
139        return _currentBlock;
140    }
141
142    /**
143     * Get the Next Block the locomotive is expected to enter.
144     * This value is calculated from the current block and direction 
145     * of travel.
146     *
147     * @return The next Block position
148     */
149    @Override
150    public Block getNextBlock(){
151        Block oldNextBlock = _nextBlock;
152        if(getBlock()==null){
153           _nextBlock = null; // no current block, so can't have a next block.
154        } else {
155          _nextBlock = nextBlockOnPath(getBlock());
156        }
157    
158        if(_nextBlock!=null) {
159            if(!_nextBlock.equals(oldNextBlock)) {
160               firePropertyChange("NextBlock",_nextBlock,oldNextBlock);
161            }
162        } else {
163            // currentNextBlock is null, notify if old next block was not.
164            if(oldNextBlock!=null){
165               firePropertyChange("NextBlock",_nextBlock,oldNextBlock);
166            }
167        }
168        return _nextBlock;
169    }
170
171    @CheckForNull
172    private Block nextBlockOnPath(Block current){
173        int fromdirection = current.getDirection();
174        List<Path> thispaths = current.getPaths();
175        for (final Path testpath : thispaths) {
176            if (testpath.checkPathSet()) {
177                Block blockTest = testpath.getBlock();
178                int dirftTest = testpath.getFromBlockDirection();
179                int dirtoTest = testpath.getToBlockDirection();
180                if (directionMatch(fromdirection, dirtoTest)) { // most reliable
181                    blockTest.setDirection(dirtoTest);
182                    return blockTest;
183                }
184                if ((fromdirection & dirftTest) == 0) { // less reliable
185                    blockTest.setDirection(dirtoTest);
186                    return blockTest;
187                }
188                if ((fromdirection != dirftTest)) { // least reliable but copes with 180 degrees 
189                    blockTest.setDirection(dirtoTest);
190                    return blockTest;
191                }
192            }
193        }
194        return null;
195    }
196
197    private static boolean directionMatch(int fromDirection, int toDirection ) {
198        return (fromDirection & toDirection) != 0;
199    }
200
201    /**
202     * Get the Next Signal Mast the locomotive is expected to pass.
203     * This value is calculated from the current block and direction 
204     * of travel.
205     *
206     * @return The next SignalMast position
207     */
208    @Override
209    public SignalMast getNextMast(){
210        SignalMast oldNextMast = _nextMast;
211        if (_nextMast != null) {
212            _nextMast.removePropertyChangeListener(_cconSignalMastListener);
213        }
214        _nextMast=null;
215        if( getBlock() != null ) {
216            LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
217        
218            Block b = getBlock();
219            Block nB = getNextBlock();
220            while(_nextMast == null && nB !=null ) {
221                _nextMast = lbm.getFacingSignalMast(b, nB);
222                b = nB;
223                nB = nextBlockOnPath(b); 
224            }
225            if ( _nextMast == null) {
226                // use block b which is the last non-null block in the path
227                _nextMast = lbm.getSignalMastAtEndBumper(b,null);
228            }
229           
230            if ( _nextMast != null) {
231                // add signal changelistener
232                _cconSignalMastListener = (PropertyChangeEvent e) -> {
233                    // aspect changed?, need to notify
234                    firePropertyChange("MastChanged",e.getNewValue(),e.getOldValue());
235                    forwardCabSignalToLayout();
236                };
237                _nextMast.addPropertyChangeListener(_cconSignalMastListener);
238            }
239        }
240        if( _nextMast != null ) {
241            if ( ! _nextMast.equals(oldNextMast)) {
242                firePropertyChange("NextMast",_nextMast,oldNextMast);
243            }
244        } else {
245            // currentNextMast is null, notify if old next mast was not.
246            if ( oldNextMast != null ) {
247               firePropertyChange("NextMast",_nextMast,oldNextMast);
248            }
249        }
250        return _nextMast;
251    }
252
253    /**
254     * Get Block List to the end of Path or Signal Mast Stop, whichever first.
255     * The first Block in the list ( if any ), will be the current Block.
256     * @return list of Blocks that the loco address is expected to traverse.
257     */
258    @Nonnull
259    @Override
260    public List<Block> getBlockList() {
261        java.util.ArrayList<Block> blockList = new java.util.ArrayList<>();
262        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
263        Block thisBlock = getBlock();
264        if ( thisBlock == null ) {
265            return blockList;
266        }
267        blockList.add(thisBlock);
268        Block nextBlock = nextBlockOnPath(thisBlock); 
269        SignalMast mast = ( nextBlock == null ? null : lbm.getFacingSignalMast(thisBlock, nextBlock));
270        while ( okToProceedAfterMast(mast) && nextBlock !=null ) {
271            blockList.add(nextBlock);
272            mast = lbm.getFacingSignalMast(thisBlock, nextBlock);
273            thisBlock = nextBlock;
274            nextBlock = nextBlockOnPath(thisBlock); 
275        }
276        return blockList;
277    }
278
279    private boolean okToProceedAfterMast( @CheckForNull SignalMast m ) {
280        if ( m == null ) {
281            return true;
282        }
283        return !m.isAtStop();
284    }
285
286    /**
287     * Forward the current cab signal value to the layout.
288     */
289    @Override
290    public void forwardCabSignalToLayout() {
291        if (!isCabSignalActive() ) {
292            return;
293        }
294        if (_masterPausedButtonActive) {
295            return;
296        }
297
298        LocoAddress locoaddr = getCabSignalAddress();
299        SignalMast mast = getNextMast();
300
301        if (mast != null) {
302            log.debug("cab {} aspect {}",locoaddr,mast.getAspect());
303        }
304        // and forward the message on to the layout.
305        forwardAspectToLayout();
306    }
307
308    /**
309     * Forward the command to the layout that sets the displayed signal
310     * aspect for this address
311     */
312    protected void forwardAspectToLayout(){
313        // this method is to be over-written by subclasses that actually
314        // talk to layout hardware.
315    }
316
317
318    /*
319     * get whether this cab signal is on or off
320     *
321     * @return true if on, false if off
322     */
323    @Override
324    public boolean isCabSignalActive(){
325        return _cabSignalActive;
326    }
327
328    /*
329     * set whether this cab signal is on or off
330     *
331     * @param active true if on, false if off
332     */
333    @Override
334    public void setCabSignalActive(boolean active){
335        _cabSignalActive = active;
336        if(_cabSignalActive) {
337           getNextMast(); // refreshes block, mast, and sends if master button not paused
338        }
339        else {
340            resetLayoutCabSignal(); // send data invalid to layout
341        }
342    }
343    
344    /*
345     * Set when initialised and when Master PAUSED button is toggled
346     *
347     * @param active true if paused, false if resumed
348     */
349    @Override
350    public void setMasterCabSigPauseActive (boolean active) {
351        _masterPausedButtonActive = active;
352        if ( !isCabSignalActive() ){
353            return; // if cabsig has already been disabled no action needed
354        }
355        if ( _masterPausedButtonActive ) {
356            log.debug("master paused");
357            resetLayoutCabSignal(); // send data invalid to layout
358        }
359        else {
360            log.debug("master not paused");
361            getNextMast(); // refreshes block, mast, and sends if single cabsig enabled
362        }
363    }
364
365    /**
366     * Forward the command to the layout that clears any displayed signal
367     * for this address
368     */
369    protected void resetLayoutCabSignal(){
370        // this method is to be over-written by subclasses that actually
371        // talk to layout hardware.
372    }
373
374    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
375
376    /**
377     * Add a listener for consist events
378     *
379     * @param l is a PropertyChangeListener object
380     */
381    @Override
382    public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
383        pcs.addPropertyChangeListener(l);
384    }
385
386    /**
387     * Remove a listener for cab signal events
388     *
389     * @param l is a PropertyChangeListener object
390     */
391    @Override
392    public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
393        pcs.removePropertyChangeListener(l);
394    }
395
396    protected void firePropertyChange(String p, Object old, Object n) {
397        log.debug("sending property {} new value {} old value {}",p,old,n);
398        pcs.firePropertyChange(p, old, n);
399    }
400
401    //PropertyChangeListener interface
402    @Override
403    public void propertyChange(PropertyChangeEvent event){
404        if(event.getSource() instanceof Block ) {
405            String propName = event.getPropertyName();
406            if ( Block.PROPERTY_VALUE.equals(propName)){
407                setBlock(); // change the block.
408            }
409
410            // block value is changed before direction is set
411            if ( Block.PROPERTY_STATE.equals(propName) || Block.PROPERTY_DIRECTION.equals(propName)) {
412                // update internal state to cascade changes.
413                getNextBlock();
414                forwardCabSignalToLayout();
415            }
416        } else if(event.getSource() instanceof SignalMast) {
417            // update internal state to cascade changes.
418            forwardCabSignalToLayout();
419        }
420    }
421
422    @Override
423    public String toString(){
424        return this.getClass().getSimpleName()+" "+this.getCabSignalAddress();
425    }
426
427    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultCabSignal.class);
428
429}