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            if ( blockVal.getValue() != null ) {
111                Object val = blockVal.getValue();
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 (blockVal.getValue().equals(addr) ||
121                    blockVal.getValue().toString().equals(addr.toString()) || 
122                    blockVal.getValue().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    private Block nextBlockOnPath(Block current){
172        int fromdirection = current.getDirection();
173        List<Path> thispaths = current.getPaths();
174        for (final Path testpath : thispaths) {
175            if (testpath.checkPathSet()) {
176                Block blockTest = testpath.getBlock();
177                int dirftTest = testpath.getFromBlockDirection();
178                int dirtoTest = testpath.getToBlockDirection();
179                if (directionMatch(fromdirection, dirtoTest)) { // most reliable
180                    blockTest.setDirection(dirtoTest);
181                    return blockTest;
182                }
183                if ((fromdirection & dirftTest) == 0) { // less reliable
184                    blockTest.setDirection(dirtoTest);
185                    return blockTest;
186                }
187                if ((fromdirection != dirftTest)) { // least reliable but copes with 180 degrees 
188                    blockTest.setDirection(dirtoTest);
189                    return blockTest;
190                }
191            }
192        }
193      return null;
194    }
195
196    private boolean directionMatch(int fromDirection, int toDirection ) {
197        return
198            (((fromDirection & Path.NORTH) != 0) && ((toDirection & Path.NORTH) != 0)) ||
199            (((fromDirection & Path.SOUTH) != 0) && ((toDirection & Path.SOUTH) != 0)) ||
200            (((fromDirection & Path.EAST) != 0) && ((toDirection & Path.EAST) != 0)) ||
201            (((fromDirection & Path.WEST) != 0) && ((toDirection & Path.WEST) != 0)) ||
202            (((fromDirection & Path.CW) != 0) && ((toDirection & Path.CW) != 0)) ||
203            (((fromDirection & Path.CCW) != 0) && ((toDirection & Path.CCW) != 0)) ||
204            (((fromDirection & Path.LEFT) != 0) && ((toDirection & Path.LEFT) != 0)) ||
205            (((fromDirection & Path.RIGHT) != 0) && ((toDirection & Path.RIGHT) != 0)) ||
206            (((fromDirection & Path.UP) != 0) && ((toDirection & Path.UP) != 0)) ||
207            (((fromDirection & Path.DOWN) != 0) && ((toDirection & Path.DOWN) != 0));
208    }
209
210    /**
211     * Get the Next Signal Mast the locomotive is expected to pass.
212     * This value is calculated from the current block and direction 
213     * of travel.
214     *
215     * @return The next SignalMast position
216     */
217    @Override
218    public SignalMast getNextMast(){
219        SignalMast oldNextMast = _nextMast;
220        if (_nextMast != null) {
221            _nextMast.removePropertyChangeListener(_cconSignalMastListener);
222        }
223        _nextMast=null;
224        if( getBlock() != null ) {
225            LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
226        
227            Block b = getBlock();
228            Block nB = getNextBlock();
229            while(_nextMast == null && nB !=null ) {
230                _nextMast = lbm.getFacingSignalMast(b, nB);
231                b = nB;
232                nB = nextBlockOnPath(b); 
233            }
234            if ( _nextMast == null) {
235                // use block b which is the last non-null block in the path
236                _nextMast = lbm.getSignalMastAtEndBumper(b,null);
237            }
238           
239            if ( _nextMast != null) {
240                // add signal changelistener
241                _cconSignalMastListener = (PropertyChangeEvent e) -> {
242                    // aspect changed?, need to notify
243                    firePropertyChange("MastChanged",e.getNewValue(),e.getOldValue());
244                    forwardCabSignalToLayout();
245                };
246                _nextMast.addPropertyChangeListener(_cconSignalMastListener);
247            }
248        }
249        if( _nextMast != null ) {
250            if ( ! _nextMast.equals(oldNextMast)) {
251                firePropertyChange("NextMast",_nextMast,oldNextMast);
252            }
253        } else {
254            // currentNextMast is null, notify if old next mast was not.
255            if ( oldNextMast != null ) {
256               firePropertyChange("NextMast",_nextMast,oldNextMast);
257            }
258        }
259        return _nextMast;
260    }
261
262    /**
263     * Get Block List to the end of Path or Signal Mast Stop, whichever first.
264     * The first Block in the list ( if any ), will be the current Block.
265     * @return list of Blocks that the loco address is expected to traverse.
266     */
267    @Nonnull
268    @Override
269    public List<Block> getBlockList() {
270        java.util.ArrayList<Block> blockList = new java.util.ArrayList<>();
271        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class);
272        Block thisBlock = getBlock();
273        if ( thisBlock == null ) {
274            return blockList;
275        }
276        blockList.add(thisBlock);
277        Block nextBlock = nextBlockOnPath(thisBlock); 
278        SignalMast mast = ( nextBlock == null ? null : lbm.getFacingSignalMast(thisBlock, nextBlock));
279        while ( okToProceedAfterMast(mast) && nextBlock !=null ) {
280            blockList.add(nextBlock);
281            mast = lbm.getFacingSignalMast(thisBlock, nextBlock);
282            thisBlock = nextBlock;
283            nextBlock = nextBlockOnPath(thisBlock); 
284        }
285        return blockList;
286    }
287
288    private boolean okToProceedAfterMast( @CheckForNull SignalMast m ) {
289        if ( m == null ) {
290            return true;
291        }
292        return !m.isAtStop();
293    }
294
295    /**
296     * Forward the current cab signal value to the layout.
297     */
298    @Override
299    public void forwardCabSignalToLayout() {
300        if (!isCabSignalActive() ) {
301            return;
302        }
303        if (_masterPausedButtonActive) {
304            return;
305        }
306
307        LocoAddress locoaddr = getCabSignalAddress();
308        SignalMast mast = getNextMast();
309
310        if (mast != null) {
311            log.debug("cab {} aspect {}",locoaddr,mast.getAspect());
312        }
313        // and forward the message on to the layout.
314        forwardAspectToLayout();
315    }
316
317    /**
318     * Forward the command to the layout that sets the displayed signal
319     * aspect for this address
320     */
321    protected void forwardAspectToLayout(){
322        // this method is to be over-written by subclasses that actually
323        // talk to layout hardware.
324    }
325
326
327    /*
328     * get whether this cab signal is on or off
329     *
330     * @return true if on, false if off
331     */
332    @Override
333    public boolean isCabSignalActive(){
334        return _cabSignalActive;
335    }
336
337    /*
338     * set whether this cab signal is on or off
339     *
340     * @param active true if on, false if off
341     */
342    @Override
343    public void setCabSignalActive(boolean active){
344        _cabSignalActive = active;
345        if(_cabSignalActive) {
346           getNextMast(); // refreshes block, mast, and sends if master button not paused
347        }
348        else {
349            resetLayoutCabSignal(); // send data invalid to layout
350        }
351    }
352    
353    /*
354     * Set when initialised and when Master PAUSED button is toggled
355     *
356     * @param active true if paused, false if resumed
357     */
358    @Override
359    public void setMasterCabSigPauseActive (boolean active) {
360        _masterPausedButtonActive = active;
361        if ( !isCabSignalActive() ){
362            return; // if cabsig has already been disabled no action needed
363        }
364        if ( _masterPausedButtonActive ) {
365            log.debug("master paused");
366            resetLayoutCabSignal(); // send data invalid to layout
367        }
368        else {
369            log.debug("master not paused");
370            getNextMast(); // refreshes block, mast, and sends if single cabsig enabled
371        }
372    }
373
374    /**
375     * Forward the command to the layout that clears any displayed signal
376     * for this address
377     */
378    protected void resetLayoutCabSignal(){
379        // this method is to be over-written by subclasses that actually
380        // talk to layout hardware.
381    }
382
383    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
384
385    /**
386     * Add a listener for consist events
387     *
388     * @param l is a PropertyChangeListener object
389     */
390    @Override
391    public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
392        pcs.addPropertyChangeListener(l);
393    }
394
395    /**
396     * Remove a listener for cab signal events
397     *
398     * @param l is a PropertyChangeListener object
399     */
400    @Override
401    public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
402        pcs.removePropertyChangeListener(l);
403    }
404
405    protected void firePropertyChange(String p, Object old, Object n) {
406        log.debug("sending property {} new value {} old value {}",p,old,n);
407        pcs.firePropertyChange(p, old, n);
408    }
409
410    //PropertyChangeListener interface
411    @Override
412    public void propertyChange(PropertyChangeEvent event){
413        if(event.getSource() instanceof Block) {
414            if (event.getPropertyName().equals("value")){
415                setBlock(); // change the block.
416            }
417
418            // block value is changed before direction is set
419            if ((event.getPropertyName().equals("state")) || (event.getPropertyName().equals("direction"))) {
420                // update internal state to cascade changes.
421                getNextBlock();
422                forwardCabSignalToLayout();
423            }
424        } else if(event.getSource() instanceof SignalMast) {
425            // update internal state to cascade changes.
426            forwardCabSignalToLayout();
427        }
428    }
429
430    @Override
431    public String toString(){
432        return this.getClass().getSimpleName()+" "+this.getCabSignalAddress();
433    }
434
435    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultCabSignal.class);
436
437}