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