001package jmri.implementation;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.HashMap;
006import java.util.List;
007import jmri.AddressedProgrammer;
008import jmri.AddressedProgrammerManager;
009import jmri.Consist;
010import jmri.ConsistListener;
011import jmri.jmrit.consisttool.ConsistPreferencesManager;
012import jmri.DccLocoAddress;
013import jmri.InstanceManager;
014import jmri.ProgListener;
015import jmri.ProgrammerException;
016import jmri.jmrit.decoderdefn.DecoderFile;
017import jmri.jmrit.decoderdefn.DecoderIndexFile;
018import jmri.jmrit.roster.Roster;
019import jmri.jmrit.roster.RosterEntry;
020import jmri.jmrit.symbolicprog.CvTableModel;
021import jmri.jmrit.symbolicprog.CvValue;
022import jmri.jmrit.symbolicprog.VariableTableModel;
023import org.jdom2.*;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * This is the Default DCC consist. It utilizes the fact that IF a Command
029 * Station supports OpsMode Programming, you can write the consist information
030 * to CV19, so ANY Command Station that supports Ops Mode Programming can write
031 * this address to a Command Station that supports it.
032 *
033 * @author Paul Bender Copyright (C) 2003-2008
034 */
035public class DccConsist implements Consist, ProgListener {
036
037    protected ArrayList<DccLocoAddress> consistList = null; // A List of Addresses in the consist
038    protected HashMap<DccLocoAddress, Boolean> consistDir = null; // A Hash table
039    // containing the directions of
040    // each locomotive in the consist,
041    // keyed by Loco Address.
042    protected HashMap<DccLocoAddress, Integer> consistPosition = null; // A Hash table
043    // containing the position of
044    // each locomotive in the consist,
045    // keyed by Loco Address.
046    protected HashMap<DccLocoAddress, String> consistRoster = null; // A Hash table
047    // containing the Roster Identifier of
048    // each locomotive in the consist,
049    // keyed by Loco Address.
050    protected int consistType = ADVANCED_CONSIST;
051    protected DccLocoAddress consistAddress = null;
052    protected String consistID = null;
053    // data member to hold the throttle listener objects
054    private final ArrayList<ConsistListener> listeners;
055
056
057    private AddressedProgrammerManager opsProgManager = null;
058
059    // Initialize a consist for the specific address.
060    // In this implementation, we can safely assume the address is a
061    // short address, since Advanced Consisting is only possible with
062    // a short address.
063    // The Default consist type is an advanced consist
064    public DccConsist(int address) {
065        this(new DccLocoAddress(address, false));
066    }
067
068    // Initialize a consist for a specific DccLocoAddress.
069    // The Default consist type is an advanced consist
070    public DccConsist(DccLocoAddress address) {
071        this(address,jmri.InstanceManager.getDefault(AddressedProgrammerManager.class));
072    }
073
074    // Initialize a consist for a specific DccLocoAddress.
075    // The Default consist type is an advanced consist
076    public DccConsist(DccLocoAddress address,AddressedProgrammerManager apm) {
077        opsProgManager = apm;
078        this.listeners = new ArrayList<>();
079        consistAddress = address;
080        consistDir = new HashMap<>();
081        consistList = new ArrayList<>();
082        consistPosition = new HashMap<>();
083        consistRoster = new HashMap<>();
084        consistID = consistAddress.toString();
085    }
086
087    // Clean Up local Storage.
088    @Override
089    public void dispose() {
090        if (consistList == null) {
091            return;
092        }
093        for (int i = (consistList.size() - 1); i >= 0; i--) {
094            DccLocoAddress loco = consistList.get(i);
095            if (log.isDebugEnabled()) {
096                log.debug("Deleting Locomotive: {}",loco);
097            }
098            try {
099                remove(loco);
100            } catch (Exception ex) {
101                log.error("Error removing loco: {} from consist: {}", loco, consistAddress);
102            }
103        }
104        consistList = null;
105        consistDir = null;
106        consistPosition = null;
107        consistRoster = null;
108    }
109
110    // Set the Consist Type
111    @Override
112    public void setConsistType(int consist_type) {
113        if (consist_type == ADVANCED_CONSIST) {
114            consistType = consist_type;
115        } else {
116            notifyUnsupportedConsistType();
117        }
118    }
119
120    private void notifyUnsupportedConsistType(){
121        log.error("Consist Type Not Supported");
122        notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
123    }
124
125    // get the Consist Type
126    @Override
127    public int getConsistType() {
128        return consistType;
129    }
130
131    // get the Consist Address
132    @Override
133    public DccLocoAddress getConsistAddress() {
134        return consistAddress;
135    }
136
137    /* is this address allowed?
138     * Since address 00 is an analog locomotive, we can't program CV19
139     * to include it in a consist, but all other addresses are ok.
140     */
141    @Override
142    public boolean isAddressAllowed(DccLocoAddress address) {
143        if (address.getNumber() != 0) {
144            return (true);
145        } else {
146            return (false);
147        }
148    }
149
150    /* is there a size limit for this consist?
151     * For Decoder Assisted Consists, returns -1 (no limit)
152     * return 0 for any other consist type.
153     */
154    @Override
155    public int sizeLimit() {
156        if (consistType == ADVANCED_CONSIST) {
157            return -1;
158        } else {
159            return 0;
160        }
161    }
162
163    // get a list of the locomotives in the consist
164    @Override
165    public ArrayList<DccLocoAddress> getConsistList() {
166        return consistList;
167    }
168
169    // does the consist contain the specified address?
170    @Override
171    public boolean contains(DccLocoAddress address) {
172        if (consistType == ADVANCED_CONSIST) {
173            return (consistList.contains(address));
174        } else {
175            notifyUnsupportedConsistType();
176        }
177        return false;
178    }
179
180    // get the relative direction setting for a specific
181    // locomotive in the consist
182    @Override
183    public boolean getLocoDirection(DccLocoAddress address) {
184        if (consistType == ADVANCED_CONSIST) {
185            Boolean Direction = consistDir.get(address);
186            return (Direction);
187        } else {
188            notifyUnsupportedConsistType();
189        }
190        return false;
191    }
192
193    /*
194     * Add a Locomotive to an Advanced Consist
195     *  @param address is the Locomotive address to add to the locomotive
196     *  @param directionNormal is True if the locomotive is traveling
197     *        the same direction as the consist, or false otherwise.
198     */
199    @Override
200    public void add(DccLocoAddress LocoAddress, boolean directionNormal) {
201        if (consistType == ADVANCED_CONSIST) {
202            Boolean Direction = directionNormal;
203            if (!(consistList.contains(LocoAddress))) {
204                consistList.add(LocoAddress);
205            }
206            consistDir.put(LocoAddress, Direction);
207            addToAdvancedConsist(LocoAddress, directionNormal);
208            //set the value in the roster entry for CV19
209            setRosterEntryCVValue(LocoAddress);
210        } else {
211            notifyUnsupportedConsistType();
212        }
213    }
214
215    /*
216     * Restore a Locomotive to an Advanced Consist, but don't write to
217     * the command station.  This is used for restoring the consist
218     * from a file or adding a consist read from the command station.
219     *  @param address is the Locomotive address to add to the locomotive
220     *  @param directionNormal is True if the locomotive is traveling
221     *        the same direction as the consist, or false otherwise.
222     */
223    @Override
224    public void restore(DccLocoAddress LocoAddress, boolean directionNormal) {
225        if (consistType == ADVANCED_CONSIST) {
226            Boolean Direction = directionNormal;
227            if (!(consistList.contains(LocoAddress))) {
228                consistList.add(LocoAddress);
229            }
230            consistDir.put(LocoAddress, Direction);
231        } else {
232            notifyUnsupportedConsistType();
233        }
234    }
235
236    /*
237     *  Remove a Locomotive from this Consist
238     *  @param address is the Locomotive address to add to the locomotive
239     */
240    @Override
241    public void remove(DccLocoAddress LocoAddress) {
242        if (consistType == ADVANCED_CONSIST) {
243            //reset the value in the roster entry for CV19
244            resetRosterEntryCVValue(LocoAddress);
245            consistDir.remove(LocoAddress);
246            consistList.remove(LocoAddress);
247            consistPosition.remove(LocoAddress);
248            consistRoster.remove(LocoAddress);
249            removeFromAdvancedConsist(LocoAddress);
250        } else {
251            notifyUnsupportedConsistType();
252        }
253    }
254
255
256    /*
257     *  Add a Locomotive to an Advanced Consist
258     *  @param address is the Locomotive address to add to the locomotive
259     *  @param directionNormal is True if the locomotive is traveling
260     *        the same direction as the consist, or false otherwise.
261     */
262    protected void addToAdvancedConsist(DccLocoAddress LocoAddress, boolean directionNormal) {
263        AddressedProgrammer opsProg = opsProgManager 
264                .getAddressedProgrammer(LocoAddress.isLongAddress(),
265                        LocoAddress.getNumber());
266        if (opsProg == null) {
267            log.error("Can't make consisting change because no programmer exists; this is probably a configuration error in the preferences");
268            return;
269        }
270
271        if (directionNormal) {
272            try {
273                opsProg.writeCV("19", consistAddress.getNumber(), this);
274            } catch (ProgrammerException e) {
275                // Don't do anything with this yet
276                log.warn("Exception writing CV19 while adding from consist", e);
277            }
278        } else {
279            try {
280                opsProg.writeCV("19", consistAddress.getNumber() + 128, this);
281            } catch (ProgrammerException e) {
282                // Don't do anything with this yet
283                log.warn("Exception writing CV19 while adding to consist", e);
284            }
285        }
286
287        InstanceManager.getDefault(jmri.AddressedProgrammerManager.class)
288                .releaseAddressedProgrammer(opsProg);
289    }
290
291    /*
292     *  Remove a Locomotive from an Advanced Consist
293     *  @param address is the Locomotive address to remove from the consist
294     */
295    protected void removeFromAdvancedConsist(DccLocoAddress LocoAddress) {
296        AddressedProgrammer opsProg = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class)
297                .getAddressedProgrammer(LocoAddress.isLongAddress(),
298                        LocoAddress.getNumber());
299        if (opsProg == null) {
300            log.error("Can't make consisting change because no programmer exists; this is probably a configuration error in the preferences");
301            return;
302        }
303
304        try {
305            opsProg.writeCV("19", 0, this);
306        } catch (ProgrammerException e) {
307            // Don't do anything with this yet
308            log.warn("Exception writing CV19 while removing from consist", e);
309        }
310
311        InstanceManager.getDefault(jmri.AddressedProgrammerManager.class)
312                .releaseAddressedProgrammer(opsProg);
313    }
314
315    /*
316     *  Set the position of a locomotive within the consist
317     *  @param address is the Locomotive address
318     *  @param position is a constant representing the position within
319     *         the consist.
320     */
321    @Override
322    public void setPosition(DccLocoAddress address, int position) {
323        consistPosition.put(address, position);
324    }
325
326    /*
327     * Get the position of a locomotive within the consist
328     * @param address is the Locomotive address of interest
329     */
330    @Override
331    public int getPosition(DccLocoAddress address) {
332        if (consistPosition.containsKey(address)) {
333            return (consistPosition.get(address));
334        }
335        // if the consist order hasn't been set, we'll use default
336        // positioning based on index in the arraylist.  Lead locomotive
337        // is position 0 in the list and the trail is the last locomtoive
338        // in the list.
339        int index = consistList.indexOf(address);
340        if (index == 0) {
341            return (Consist.POSITION_LEAD);
342        } else if (index == (consistList.size() - 1)) {
343            return (Consist.POSITION_TRAIL);
344        } else {
345            return index;
346        }
347    }
348
349    /**
350     * Set the roster entry of a locomotive within the consist
351     *
352     * @param address  is the Locomotive address
353     * @param rosterId is the roster Identifer of the associated roster entry.
354     */
355    @Override
356    public void setRosterId(DccLocoAddress address, String rosterId) {
357        consistRoster.put(address, rosterId);
358        if (consistType == ADVANCED_CONSIST) {
359            //set the value in the roster entry for CV19
360            setRosterEntryCVValue(address);
361        } 
362    }
363
364    /**
365     * Get the rosterId of a locomotive within the consist
366     *
367     * @param address is the Locomotive address of interest
368     * @return string roster Identifier associated with the given address in the
369     *         consist. Returns null if no roster entry is associated with this
370     *         entry.
371     */
372    @Override
373    public String getRosterId(DccLocoAddress address) {
374        if (consistRoster.containsKey(address)) {
375            return (consistRoster.get(address));
376        } else {
377            return null;
378        }
379    }
380            
381   /**
382    * Update the value in the roster entry for CV19 for the specified
383    * address
384    *
385    * @param address is the Locomotive address we are updating.
386    */
387   protected void setRosterEntryCVValue(DccLocoAddress address){
388      updateRosterCV(address,getLocoDirection(address),this.consistAddress.getNumber());
389   }
390
391   /**
392    * Set the value in the roster entry's value for for CV19 to 0
393    *
394    * @param address is the Locomotive address we are updating.
395    */
396   protected void resetRosterEntryCVValue(DccLocoAddress address){
397      updateRosterCV(address,getLocoDirection(address),0);
398   }
399
400   /**
401    * If allowed by the preferences, Update the CV19 value in the 
402    * specified address's roster entry, if the roster entry is known.
403    *
404    * @param address is the Locomotive address we are updating.
405    * @param direction the direction to set.
406    * @param value the numeric value of the consist address. 
407    */
408   protected void updateRosterCV(DccLocoAddress address,Boolean direction,int value){
409        if(!InstanceManager.getDefault(ConsistPreferencesManager.class).isUpdateCV19()){
410           log.trace("Consist Manager updates of CV19 are disabled in preferences");
411           return;
412        }
413        if(getRosterId(address)==null){
414           // roster entry unknown.
415           log.trace("No RosterID for address {} in consist {}.  Skipping CV19 update.",address,consistAddress);
416           return;
417        }
418        RosterEntry entry = Roster.getDefault().getEntryForId(getRosterId(address));
419
420        if(entry==null || entry.getFileName()==null || entry.getFileName().equals("")){
421           // roster entry unknown.
422           log.trace("No file name available for RosterID {},address {}, in consist {}.  Skipping CV19 update.",getRosterId(address),address,consistAddress);
423           return;
424        }
425        CvTableModel  cvTable = new CvTableModel(null, null);  // will hold CV objects
426        VariableTableModel varTable = new VariableTableModel(null, new String[]{"Name", "Value"}, cvTable); // NOI18N
427        entry.readFile();  // read, but don't yet process
428
429        // load from decoder file
430        loadDecoderFromLoco(entry,varTable);
431
432        entry.loadCvModel(varTable, cvTable);
433        CvValue cv19Value = cvTable.getCvByNumber("19");
434        cv19Value.setValue((value & 0xff) | (direction.booleanValue()?0x00:0x80 ));
435
436        entry.writeFile(cvTable,varTable);
437   }
438
439    // copied from PaneProgFrame
440    protected void loadDecoderFromLoco(RosterEntry r,VariableTableModel varTable) {
441        // get a DecoderFile from the locomotive xml
442        String decoderModel = r.getDecoderModel();
443        String decoderFamily = r.getDecoderFamily();
444        if (log.isDebugEnabled()) {
445            log.debug("selected loco uses decoder {} {}",decoderFamily,decoderModel);
446        }
447        // locate a decoder like that.
448        List<DecoderFile> l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, decoderFamily, null, null, null, decoderModel);
449        if (log.isDebugEnabled()) {
450            log.debug("found {} matches",l.size());
451        }
452        if (l.isEmpty()) {
453            log.debug("Loco uses {} {} decoder, but no such decoder defined",decoderFamily,decoderModel );
454            // fall back to use just the decoder name, not family
455            l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, decoderModel);
456            if (log.isDebugEnabled()) {
457                log.debug("found {} matches without family key",l.size());
458            }
459        }
460        if (!l.isEmpty()) {
461            DecoderFile d = l.get(0);
462            loadDecoderFile(d, r, varTable);
463        } else {
464            if (decoderModel.equals("")) {
465                log.debug("blank decoderModel requested, so nothing loaded");
466            } else {
467                log.warn("no matching \"{}\" decoder found for loco, no decoder info loaded",decoderModel );
468            }
469        }
470    }
471
472    protected void loadDecoderFile(DecoderFile df, RosterEntry re,VariableTableModel variableModel) {
473        if (df == null) {
474            log.warn("loadDecoder file invoked with null object");
475            return;
476        }
477        if (log.isDebugEnabled()) {
478            log.debug("loadDecoderFile from {} {}", DecoderFile.fileLocation, df.getFileName());
479        }
480
481        Element decoderRoot = null;
482
483        try {
484            decoderRoot = df.rootFromName(DecoderFile.fileLocation + df.getFileName());
485        } catch (JDOMException | IOException e) {
486            log.error("Exception while loading decoder XML file: {}", df.getFileName(), e);
487        }
488        // load variables from decoder tree
489        df.getProductID();
490        if(decoderRoot!=null) {
491           df.loadVariableModel(decoderRoot.getChild("decoder"), variableModel);
492           // load function names
493           re.loadFunctions(decoderRoot.getChild("decoder").getChild("family").getChild("functionlabels"));
494        }
495    }
496
497    /*
498     * Add a Listener for consist events
499     * @param Listener is a consistListener object
500     */
501    @Override
502    public void addConsistListener(ConsistListener Listener) {
503        if (!listeners.contains(Listener)) {
504            listeners.add(Listener);
505        }
506    }
507
508    /*
509     * Remove a Listener for consist events
510     * @param Listener is a consistListener object
511     */
512    @Override
513    public void removeConsistListener(ConsistListener Listener) {
514        if (listeners.contains(Listener)) {
515            listeners.remove(Listener);
516        }
517    }
518
519    // Get and set the
520    /*
521     * Set the text ID associated with the consist
522     * @param String is a string identifier for the consist
523     */
524    @Override
525    public void setConsistID(String ID) {
526        consistID = ID;
527    }
528
529    /*
530     * Get the text ID associated with the consist
531     * @return String identifier for the consist
532     *         default value is the string Identifier for the
533     *         consist address.
534     */
535    @Override
536    public String getConsistID() {
537        return consistID;
538    }
539
540    /*
541     * Reverse the order of locomotives in the consist and flip
542     * the direction bits of each locomotive.
543     */
544    @Override
545    public void reverse() {
546        // save the old lead locomotive direction.
547        Boolean oldDir = consistDir.get(consistList.get(0));
548        // reverse the direction of the list
549        java.util.Collections.reverse(consistList);
550        // and then save the new lead locomotive direction
551        Boolean newDir = consistDir.get(consistList.get(0));
552        // and itterate through the list to reverse the directions of the
553        // individual elements of the list.
554        java.util.Iterator<DccLocoAddress> i = consistList.iterator();
555        while (i.hasNext()) {
556            DccLocoAddress locoaddress = i.next();
557            if (oldDir.equals(newDir)) {
558                add(locoaddress, getLocoDirection(locoaddress));
559            } else {
560                add(locoaddress, !getLocoDirection(locoaddress));
561            }
562            if (consistPosition.containsKey(locoaddress)) {
563                switch (getPosition(locoaddress)) {
564                    case Consist.POSITION_LEAD:
565                        setPosition(locoaddress, Consist.POSITION_TRAIL);
566                        break;
567                    case Consist.POSITION_TRAIL:
568                        setPosition(locoaddress, Consist.POSITION_LEAD);
569                        break;
570                    default:
571                        setPosition(locoaddress, consistList.size() - getPosition(locoaddress));
572                        break;
573                }
574            }
575        }
576        // notify any listeners that the consist changed
577        this.notifyConsistListeners(consistAddress, ConsistListener.OK);
578    }
579
580    /*
581     * Restore the consist to the command station.
582     */
583    @Override
584    public void restore() {
585        // itterate through the list to re-add the addresses to the
586        // command station.
587        java.util.Iterator<DccLocoAddress> i = consistList.iterator();
588        while (i.hasNext()) {
589            DccLocoAddress locoaddress = i.next();
590            add(locoaddress, getLocoDirection(locoaddress));
591        }
592        // notify any listeners that the consist changed
593        this.notifyConsistListeners(consistAddress, ConsistListener.OK);
594    }
595
596    /*
597     * Notify all listener objects of a status change.
598     * @param LocoAddress is the address of any specific locomotive the
599     *       status refers to.
600     * @param ErrorCode is the status code to send to the
601     *       consistListener objects
602     */
603    @SuppressWarnings("unchecked")
604    protected void notifyConsistListeners(DccLocoAddress LocoAddress, int ErrorCode) {
605        // make a copy of the listener vector to notify.
606        ArrayList<ConsistListener> v;
607        synchronized (this) {
608            v = (ArrayList<ConsistListener>) listeners.clone();
609        }
610        log.debug("Sending Status code: {} to {} listeners for Address {}",
611                ErrorCode,
612                v.size(), LocoAddress);
613        // forward to all listeners
614        v.forEach(client -> {
615            client.consistReply(LocoAddress, ErrorCode);
616        });
617    }
618
619    // This class is to be registered as a programmer listener, so we
620    // include the programmingOpReply() function
621    @Override
622    public void programmingOpReply(int value, int status) {
623        log.debug("Programming Operation reply received, value is {}, status is {}", value, status);
624        notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.OPERATION_SUCCESS);
625    }
626
627    private static final  Logger log = LoggerFactory.getLogger(DccConsist.class);
628
629}