001package jmri.jmrit.throttle;
002
003import java.awt.*;
004import java.beans.PropertyChangeEvent;
005import java.beans.PropertyChangeListener;
006import java.io.File;
007import java.util.ArrayList;
008import java.util.List;
009import java.util.Objects;
010
011import javax.annotation.CheckForNull;
012
013import javax.swing.JButton;
014import javax.swing.JComboBox;
015import javax.swing.JFrame;
016import javax.swing.JInternalFrame;
017import javax.swing.JPanel;
018import javax.swing.WindowConstants;
019
020import jmri.Consist;
021import jmri.ConsistManager;
022import jmri.DccLocoAddress;
023import jmri.DccThrottle;
024import jmri.InstanceManager;
025import jmri.LocoAddress;
026import jmri.Programmer;
027import jmri.ThrottleListener;
028import jmri.ThrottleManager;
029import jmri.jmrit.DccLocoAddressSelector;
030import jmri.jmrit.consisttool.ConsistComboBox;
031import jmri.jmrit.roster.Roster;
032import jmri.jmrit.roster.RosterEntry;
033import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
034import jmri.jmrit.symbolicprog.ProgDefault;
035import jmri.jmrit.symbolicprog.tabbedframe.PaneOpsProgFrame;
036import jmri.jmrix.nce.consist.NceConsistRoster;
037import jmri.jmrix.nce.consist.NceConsistRosterEntry;
038import jmri.util.swing.JmriJOptionPane;
039import jmri.util.swing.WrapLayout;
040
041import org.jdom2.Element;
042
043/**
044 * A JInternalFrame that provides a way for the user to enter a decoder address.
045 * This class also store AddressListeners and notifies them when the user enters
046 * a new address.
047 *
048 * @author glen Copyright (C) 2002
049 * @author Daniel Boudreau Copyright (C) 2008 (add consist feature)
050 * @author Lionel Jeanson 2009-2021
051 */
052public class AddressPanel extends JInternalFrame implements ThrottleListener, PropertyChangeListener {
053
054    private final ThrottleManager throttleManager;
055    private final ConsistManager consistManager; 
056
057    private DccThrottle throttle;
058    private DccThrottle consistThrottle;
059
060    private final DccLocoAddressSelector addrSelector = new DccLocoAddressSelector();
061    private DccLocoAddress currentAddress;
062    private DccLocoAddress consistAddress;
063    private DccLocoAddress requestedAddress;
064    private ArrayList<AddressListener> listeners;
065
066    private JPanel mainPanel;
067
068    private JButton releaseButton;
069    private JButton dispatchButton;
070    private JButton progButton;
071    private JButton setButton;
072    private RosterEntrySelectorPanel rosterBox;
073    @SuppressWarnings("rawtypes") // TBD: once JMRI consists vs NCE consists resolved, can be removed
074    private JComboBox conRosterBox;
075    private boolean isUpdatingUI = false;
076    private boolean disableRosterBoxActions = false;
077    private RosterEntry rosterEntry;
078
079    /**
080     * Constructor
081     * @param throttleManager the throttle manager
082     */
083    public AddressPanel(ThrottleManager throttleManager) {
084        this.throttleManager = throttleManager;
085        consistManager = InstanceManager.getNullableDefault(jmri.ConsistManager.class);
086        initGUI();
087        applyPreferences();
088    }
089
090    public void destroy() { // Handle disposing of the throttle
091        if (conRosterBox != null && conRosterBox instanceof ConsistComboBox) {
092            ((ConsistComboBox)conRosterBox).dispose();
093        }
094        if ( requestedAddress != null ) {
095            throttleManager.cancelThrottleRequest(requestedAddress, this);
096            requestedAddress = null;
097        }
098        if (throttle != null) {
099            throttle.removePropertyChangeListener(this);
100            throttleManager.releaseThrottle(throttle, this);
101            notifyListenersOfThrottleRelease();
102            throttle = null;
103        }
104        if (consistThrottle != null) {
105            consistThrottle.removePropertyChangeListener(this);
106            throttleManager.releaseThrottle(consistThrottle, this);
107            notifyListenersOfThrottleRelease();
108            consistThrottle = null;
109        }
110    }
111
112    /**
113     * Add an AddressListener.
114     * AddressListeners are notified when the user
115     * selects a new address and when a Throttle is acquired for that address
116     * @param l listener to add.
117     *
118     */
119    public void addAddressListener(AddressListener l) {
120        if (listeners == null) {
121            listeners = new ArrayList<>();
122        }
123        if (!listeners.contains(l)) {
124            listeners.add(l);
125        }
126    }
127
128    /**
129     * Remove an AddressListener.
130     *
131     * @param l listener to remove.
132     */
133    public void removeAddressListener(AddressListener l) {
134        if (listeners == null) {
135            return;
136        }
137        listeners.remove(l);
138    }
139
140    /**
141     * Gets the selected index of the roster combo box. Implemented to support
142     * xboxThrottle.py
143     *
144     * @return the selected index of the roster combo box
145     */
146    public int getRosterSelectedIndex() {
147        return getRosterEntrySelector().getRosterEntryComboBox().getSelectedIndex();
148    }
149
150    /**
151     * Sets the selected index of the roster combo box.
152     * This method temporarily disables roster box actions so it
153     * can change the selected index without triggering a cascade of events. 
154     * selectRosterEntry() as to be called afterward to actually select the roster entry
155     *
156     * @param index the index to select in the combo box
157     */
158    public void setRosterSelectedIndex(int index) {
159        if (getRosterEntrySelector().isEnabled() && index >= 0 && index < getRosterEntrySelector().getRosterEntryComboBox().getItemCount()) {
160            disableRosterBoxActions = true; //Temporarily disable roster box actions
161            getRosterEntrySelector().getRosterEntryComboBox().setSelectedIndex(index);
162            disableRosterBoxActions = false;
163        }
164        if ((backgroundPanel != null) && (rosterBox.getSelectedRosterEntries().length > 0)) {
165            backgroundPanel.setImagePath(null);
166            String rosterEntryTitle = getRosterEntrySelector().getSelectedRosterEntries()[0].titleString();
167            RosterEntry re = Roster.getDefault().entryFromTitle(rosterEntryTitle);
168            if (re != null) {
169                backgroundPanel.setImagePath(re.getImagePath());
170            }
171        }
172    }
173
174    private BackgroundPanel backgroundPanel;
175
176    public void setBackgroundPanel(BackgroundPanel bp) {
177        backgroundPanel = bp;
178    }
179
180    /**
181     * "Sets" the current roster entry. Equivalent to the user pressing the
182     * "Set" button. Implemented to support xboxThrottle.py
183     */
184    public void selectRosterEntry() {
185        if (isUpdatingUI || disableRosterBoxActions) {
186            return;
187        }
188        if (getRosterEntrySelector().getSelectedRosterEntries().length != 0) {
189            setRosterEntry(getRosterEntrySelector().getSelectedRosterEntries()[0]);
190            consistAddress = null;
191        }
192    }
193
194    /**
195     * Get notification that a throttle has been found as we requested.
196     *
197     * @param t An instantiation of the DccThrottle with the address requested.
198     */
199    @Override
200    public void notifyThrottleFound(DccThrottle t) {
201        log.debug("Throttle found :  {} ", t.getLocoAddress());
202        // is this a consist address?
203        if (consistAddress == null && consistManager != null && consistManager.isEnabled() && consistManager.getConsistList().contains(t.getLocoAddress())) { 
204            // we found a consist with this address, this is a consist
205            consistAddress = (DccLocoAddress) t.getLocoAddress();
206        }
207        if (consistAddress != null && t.getLocoAddress().equals(consistAddress)) {
208            // notify the listeners that a throttle was found
209            // for the consist address.
210            log.debug("notifying that this is a consist");
211            notifyConsistThrottleFound(t);
212            return;
213        }
214        if (t.getLocoAddress().getNumber() != currentAddress.getNumber()) {
215            log.warn("Not correct address, asked for {} got {}, requesting again...", currentAddress.getNumber(), t.getLocoAddress());
216            boolean requestOK
217                    = throttleManager.requestThrottle(currentAddress, this, true);
218            if (!requestOK) {
219                JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("AddressInUse"));
220                requestedAddress = null;
221            }
222            return;
223        }
224
225        requestedAddress = null;
226        currentAddress = (DccLocoAddress) t.getLocoAddress();
227        // can we find a roster entry?
228        if ((rosterEntry == null)
229                && (InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())
230                && (InstanceManager.getDefault(ThrottlesPreferences.class).isEnablingRosterSearch())
231                && currentAddress != null) {
232            List<RosterEntry> l = Roster.getDefault().matchingList(null, null, "" + currentAddress.getNumber(), null, null, null, null);
233            if (!l.isEmpty()) {
234                rosterEntry = l.get(0);
235            }
236        }
237        
238        if (consistAddress != null) {
239            // if we get there, it means we got the throttle for the head locomotive of a consist
240            // only update the function panel
241            log.debug("Advanced consist throttle, got the throttle for the head locomotive functions control");
242            (new ArrayList<AddressListener>(listeners)).forEach((l) -> {
243                if (l instanceof FunctionPanel) {
244                    l.notifyAddressThrottleFound(t);
245                }
246            });
247            return;
248        }
249        
250        if (throttle != null) {
251            log.debug("notifyThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
252            return;
253        }  
254        
255        throttle = t;        
256        throttle.addPropertyChangeListener(this);
257
258        // update GUI
259        updateGUIOnThrottleFound(true);
260        
261        // send notification of new address
262        // work on a copy because some new listeners may be added while notifying the existing ones        
263        (new ArrayList<AddressListener>(listeners)).forEach((l) -> {
264            // log.debug("Notify address listener of address change {}", l.getClass());
265            l.notifyAddressThrottleFound(t);
266        });
267    }
268
269    @Override
270    public void notifyFailedThrottleRequest(LocoAddress address, String reason) {
271        JmriJOptionPane.showMessageDialog(null, reason, Bundle.getMessage("FailedSetupRequestTitle"), JmriJOptionPane.WARNING_MESSAGE);
272    }
273
274    /**
275    * A decision is required for Throttle creation to continue.
276    * <p>
277    * Steal / Cancel, Share / Cancel, or Steal / Share Cancel
278    */
279    @Override
280    public void notifyDecisionRequired(LocoAddress address, DecisionType question) {
281        if ( null != question )  {
282            switch (question) {
283                case STEAL:
284                    if (InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){
285                        throttleManager.responseThrottleDecision(address, this, DecisionType.STEAL );
286                        return;
287                    }
288                    jmri.util.ThreadingUtil.runOnGUI(() -> {
289                        if ( JmriJOptionPane.YES_OPTION == JmriJOptionPane.showConfirmDialog(
290                                this, Bundle.getMessage("StealQuestionText",address.toString()),
291                                Bundle.getMessage("StealRequestTitle"), JmriJOptionPane.YES_NO_OPTION)) {
292                            throttleManager.responseThrottleDecision(address, this, DecisionType.STEAL );
293                        } else {
294                            throttleManager.cancelThrottleRequest(address, this);
295                            requestedAddress = null;
296                        }
297                    });
298                    break;
299                case SHARE:
300                    if (InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare() ){
301                        throttleManager.responseThrottleDecision(address, this, DecisionType.SHARE );
302                        return;
303                    }
304                    jmri.util.ThreadingUtil.runOnGUI(() -> {
305                        if ( JmriJOptionPane.YES_OPTION == JmriJOptionPane.showConfirmDialog(
306                                this, Bundle.getMessage("ShareQuestionText",address.toString()),
307                                Bundle.getMessage("ShareRequestTitle"), JmriJOptionPane.YES_NO_OPTION)) {
308                            throttleManager.responseThrottleDecision(address, this, DecisionType.SHARE );
309                        } else {
310                            throttleManager.cancelThrottleRequest(address, this);
311                            requestedAddress = null;
312                        }
313                    });
314                    break;
315                case STEAL_OR_SHARE:
316                    if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){
317                        throttleManager.responseThrottleDecision(address, this, DecisionType.STEAL );
318                        return;
319                    }
320                    if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare() ){
321                        throttleManager.responseThrottleDecision(address, this, DecisionType.SHARE );
322                        return;
323                    }
324                    String[] options = new String[] {Bundle.getMessage("StealButton"), Bundle.getMessage("ShareButton"), Bundle.getMessage("ButtonCancel")};
325                    jmri.util.ThreadingUtil.runOnGUI(() -> {
326                        int response = JmriJOptionPane.showOptionDialog(AddressPanel.this,
327                                Bundle.getMessage("StealShareQuestionText",address.toString()), Bundle.getMessage("StealShareRequestTitle"),
328                                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]);
329                        switch (response) {
330                            case 0:
331                                log.debug("steal clicked");
332                                throttleManager.responseThrottleDecision(address, AddressPanel.this, DecisionType.STEAL);
333                                break;
334                            case 1:
335                                log.debug("share clicked");
336                                throttleManager.responseThrottleDecision(address, AddressPanel.this, DecisionType.SHARE);
337                                break;
338                            default:
339                                log.debug("cancel clicked");
340                                throttleManager.cancelThrottleRequest(address, AddressPanel.this);
341                                requestedAddress = null;
342                                break;
343                        }
344                    });
345                    break;
346                default:
347                    break;
348            }
349        }
350    }
351
352    /**
353     * Get notification that a consist throttle has been found as we requested.
354     *
355     * @param t An instantiation of the DccThrottle with the address requested.
356     */
357    public void notifyConsistThrottleFound(DccThrottle t) {
358        if (consistThrottle != null) {
359            log.debug("notifyConsistThrottleFound() consistThrottle non null, called for loc {}",t.getLocoAddress());
360            return;
361        }        
362        requestedAddress = null;
363        consistThrottle = t;
364        currentAddress = (DccLocoAddress) t.getLocoAddress();
365        consistThrottle.addPropertyChangeListener(this);
366        
367        Consist consist = getConsistEntry();
368        if (consist != null && consist.getConsistType() == Consist.CS_CONSIST) {
369            // CS Consist, consist has the head locomotive id
370            // can we find a roster entry?
371            if ((rosterEntry == null)
372                    && (InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())
373                    && (InstanceManager.getDefault(ThrottlesPreferences.class).isEnablingRosterSearch())
374                    && currentAddress != null) {
375                List<RosterEntry> l = Roster.getDefault().matchingList(null, null, "" + currentAddress.getNumber(), null, null, null, null);
376                if (!l.isEmpty()) {
377                    rosterEntry = l.get(0);
378                }
379            }
380        }
381        
382        updateGUIOnThrottleFound(true);
383                
384        // send notification of new address
385        // work on a clone because some new listeners may be added while notifying the existing ones
386        (new ArrayList<AddressListener>(listeners)).forEach((l) -> {
387            l.notifyConsistAddressThrottleFound(t);
388        });
389        
390        if (consist != null && consist.getConsistType() == Consist.ADVANCED_CONSIST) {      
391            // request a throttle for head locomotive for functions
392            DccLocoAddress headLocoAddress = consist.getConsistList().get(0);
393            // only if consist address is not head locomotive address
394            if (! headLocoAddress.equals(currentAddress)) {
395                log.debug("Advanced consist throttle, requesting secondary throttle for head locomotive function control.");
396                changeOfAddress(headLocoAddress);
397            }
398        }
399    }
400    
401    private void updateGUIOnThrottleFound(boolean throttleActive) {
402        // update GUI
403        isUpdatingUI = true;
404        //addrSelector.setAddress(currentAddress);
405        setButton.setEnabled(!throttleActive);
406        addrSelector.setEnabled(!throttleActive);
407        releaseButton.setEnabled(throttleActive);
408        if (throttleActive && rosterEntry != null) {
409            getRosterEntrySelector().setSelectedRosterEntry(rosterEntry);
410        } else {
411            getRosterEntrySelector().getRosterEntryComboBox().setSelectedItem(Bundle.getMessage("NoLocoSelected"));
412        }
413        getRosterEntrySelector().setEnabled(!throttleActive);
414        if (conRosterBox != null) {
415            if (throttleActive && consistThrottle != null) {
416                conRosterBox.setSelectedItem(consistThrottle.getLocoAddress());
417            } else {
418                conRosterBox.setSelectedItem(Bundle.getMessage("NoConsistSelected"));
419            }
420            conRosterBox.setEnabled(!throttleActive);
421        }     
422        if (throttleManager.hasDispatchFunction()) {
423            dispatchButton.setEnabled(throttleActive);
424        }  
425        // enable program button if programmer available
426        // for ops-mode programming
427        if ((rosterEntry != null) && (ProgDefault.getDefaultProgFile() != null)
428                && (InstanceManager.getNullableDefault(jmri.AddressedProgrammerManager.class) != null)
429                && (InstanceManager.getDefault(jmri.AddressedProgrammerManager.class).isAddressedModePossible())) {
430            progButton.setEnabled(true);
431        } else {
432            progButton.setEnabled(false);
433        }
434        isUpdatingUI = false;
435    }
436
437    /**
438     * Receive notification that an address has been release or dispatched.
439     */
440    public void notifyThrottleDisposed() {
441        log.debug("notifyThrottleDisposed");
442        notifyListenersOfThrottleRelease();
443        updateGUIOnThrottleFound(false);
444        rosterEntry = null;
445        if (consistThrottle != null) {
446            consistThrottle.removePropertyChangeListener(this);
447        }
448        if (throttle != null) {
449            throttle.removePropertyChangeListener(this);
450        }
451    }
452
453    /**
454     * Get the RosterEntry if there's one for this throttle.
455     *
456     * @return RosterEntry or null
457     */
458    public RosterEntry getRosterEntry() {
459        return rosterEntry;
460    }
461    
462    /**
463     * Get the selected Consist if there's one for this throttle.
464     *
465     * @return Consist or null
466     */    
467    public Consist getConsistEntry() {
468        if (consistManager == null || consistAddress == null || !consistManager.isEnabled()) {
469            return null;
470        }
471        if (consistManager.getConsistList().contains(consistAddress)) {
472            return consistManager.getConsist(consistAddress);
473        }
474        return null;
475    }
476
477    /**
478     * Set the RosterEntry for this throttle and initiate a throttle request
479     * @param entry roster entry to set.
480     */
481    public void setRosterEntry(RosterEntry entry) {
482        isUpdatingUI = true;
483        getRosterEntrySelector().setSelectedRosterEntry(entry);
484        addrSelector.setAddress(entry.getDccLocoAddress());
485        isUpdatingUI = false;
486        rosterEntry = entry;
487        changeOfAddress(addrSelector.getAddress());
488    }
489
490    /**
491     * Create, initialize and place the GUI objects.
492     */
493    @SuppressWarnings("unchecked") //for the onRosterBox.insertItemAt(), to be a removed once NCE consists clarified
494    private void initGUI() {
495        this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
496        mainPanel = new JPanel();
497        mainPanel.setLayout(new BorderLayout());
498        this.setContentPane(mainPanel);
499
500        // center: address input
501        addrSelector.setVariableSize(true);
502        mainPanel.add(addrSelector.getCombinedJPanel(), BorderLayout.CENTER);
503        addrSelector.getTextField().addActionListener(e -> {
504            if (isUpdatingUI) {
505                return;
506            }
507            consistAddress = null;
508            changeOfAddress(addrSelector.getAddress());
509        });
510
511        // top : roster and consists selectors
512        JPanel topPanel = new JPanel();
513        topPanel.setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
514
515        rosterBox = new RosterEntrySelectorPanel();
516        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle()) && (InstanceManager.getDefault(ThrottlesPreferences.class).isAddressSelectorShowingAllRosterGroup())) {
517            rosterBox.getRosterGroupComboBox().setAllEntriesEnabled(true);
518        }
519        getRosterEntrySelector().setNonSelectedItem(Bundle.getMessage("NoLocoSelected"));
520        getRosterEntrySelector().setToolTipText(Bundle.getMessage("SelectLocoFromRosterTT"));
521        getRosterEntrySelector().addPropertyChangeListener("selectedRosterEntries", pce -> selectRosterEntry());
522        getRosterEntrySelector().setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
523        topPanel.add(getRosterEntrySelector());
524
525        if (InstanceManager.getDefault(NceConsistRoster.class).numEntries() > 0) { // NCE consists
526            // NCE implementation of consists is specific, TODO: refactor to use generic JMRI consists
527            conRosterBox = InstanceManager.getDefault(NceConsistRoster.class).fullRosterComboBox();
528            conRosterBox.insertItemAt(Bundle.getMessage("NoConsistSelected"), 0);  // empty entry
529            conRosterBox.setSelectedIndex(0);
530            conRosterBox.setToolTipText(Bundle.getMessage("SelectConsistFromRosterTT"));
531            conRosterBox.addActionListener(e -> nceConsistRosterSelected());
532            topPanel.add(conRosterBox);
533        } else {                      
534            if ((consistManager != null) && (consistManager.isEnabled())) {  // JMRI consists
535                JPanel consistPanel = new JPanel();
536                JButton consistToolButton = new JButton(new jmri.jmrit.consisttool.ConsistToolAction());
537                consistPanel.add(consistToolButton);
538                conRosterBox = new ConsistComboBox();
539                conRosterBox.addActionListener(e -> jmriConsistRosterSelected());
540                consistPanel.add(conRosterBox);
541                topPanel.add(consistPanel);
542            }
543        }
544
545        mainPanel.add(topPanel, BorderLayout.NORTH);
546
547        // bottom : buttons
548        JPanel buttonPanel = new JPanel();
549        buttonPanel.setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
550
551        progButton = new JButton(Bundle.getMessage("ButtonProgram"));
552        buttonPanel.add(progButton);
553        progButton.setEnabled(false);
554        progButton.addActionListener(e -> openProgrammer());
555
556        dispatchButton = new JButton(Bundle.getMessage("ButtonDispatch"));
557        buttonPanel.add(dispatchButton);
558        dispatchButton.setEnabled(false);
559        dispatchButton.addActionListener(e -> dispatchAddress());
560
561        releaseButton = new JButton(Bundle.getMessage("ButtonRelease"));
562        buttonPanel.add(releaseButton);
563        releaseButton.setEnabled(false);
564        releaseButton.addActionListener(e -> releaseAddress());
565
566        setButton = new JButton(Bundle.getMessage("ButtonSet"));
567        setButton.addActionListener(e -> {
568            consistAddress = null;
569            changeOfAddress(addrSelector.getAddress());
570        });
571        buttonPanel.add(setButton);
572
573        mainPanel.add(buttonPanel, BorderLayout.SOUTH);
574
575        pack();
576    }
577
578    private void jmriConsistRosterSelected() {
579        if (isUpdatingUI) {
580            return;
581        }
582        if ((conRosterBox.getSelectedIndex() != 0) && (conRosterBox.getSelectedItem() instanceof DccLocoAddress)) {
583            consistAddress = (DccLocoAddress) conRosterBox.getSelectedItem() ;            
584            changeOfConsistAddress();
585        }
586    }
587
588    private void nceConsistRosterSelected() {
589        if (isUpdatingUI) {
590            return;
591        }
592        if (!(Objects.equals(conRosterBox.getSelectedItem(), Bundle.getMessage("NoConsistSelected")))) {
593            String rosterEntryTitle = Objects.requireNonNull(conRosterBox.getSelectedItem()).toString();
594            NceConsistRosterEntry nceConsistRosterEntry = InstanceManager.getDefault(NceConsistRoster.class)
595                    .entryFromTitle(rosterEntryTitle);
596
597            DccLocoAddress a = new DccLocoAddress(Integer.parseInt(nceConsistRosterEntry
598                    .getLoco1DccAddress()), nceConsistRosterEntry.isLoco1LongAddress());
599            addrSelector.setAddress(a);
600            consistAddress = null;
601            int cA = 0;
602            try {
603                cA = Integer.parseInt(nceConsistRosterEntry.getConsistNumber());
604            } catch (NumberFormatException ignored) {
605
606            }
607            if (0 < cA && cA < 128) {
608                consistAddress = new DccLocoAddress(cA, false);
609            } else {
610                log.warn("consist number missing {}", nceConsistRosterEntry.getLoco1DccAddress());
611                JmriJOptionPane.showMessageDialog(mainPanel,
612                        Bundle.getMessage("ConsistNumberHasNotBeenAssigned"),
613                        Bundle.getMessage("NeedsConsistNumber"),
614                        JmriJOptionPane.ERROR_MESSAGE);
615                return;
616            }
617            if (JmriJOptionPane.showConfirmDialog(mainPanel,
618                    Bundle.getMessage("SendFunctionToLead"), Bundle.getMessage("NCEconsistThrottle"),
619                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
620                addrSelector.setAddress(consistAddress);
621                consistAddress = null;
622            }
623            changeOfAddress(addrSelector.getAddress());
624        }
625    }
626
627    /**
628     * The user has selected a new address. Notify all listeners.
629     */
630    private void changeOfAddress(DccLocoAddress a) {
631        currentAddress = a;
632        if (currentAddress == null) {
633            return; // no address
634        }
635        // send notification of new address
636        listeners.forEach((l) -> {
637            l.notifyAddressChosen(currentAddress);
638        });
639        log.debug("Requesting new slot for address {} rosterEntry {}",currentAddress,rosterEntry);
640        boolean requestOK;
641        if (rosterEntry == null) {
642            requestedAddress = currentAddress;
643            requestOK = throttleManager.requestThrottle(currentAddress, this, true);
644        }
645        else {
646            requestedAddress = rosterEntry.getDccLocoAddress();
647            requestOK = throttleManager.requestThrottle(rosterEntry, this, true);
648        }
649        if (!requestOK) {
650            requestedAddress = null;
651            JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("AddressInUse"));
652        }
653    }
654
655    private void changeOfConsistAddress() {
656        if (consistAddress == null) {
657            return; // no address
658        }  
659        addrSelector.setAddress(consistAddress);
660        // send notification of new address
661        listeners.forEach((l) -> {
662            l.notifyAddressChosen(currentAddress);
663        });
664        log.debug("Requesting new slot for consist address {}",consistAddress);        
665        requestedAddress = consistAddress;
666        boolean requestOK = throttleManager.requestThrottle(consistAddress, this, true);
667        if (!requestOK) {
668            requestedAddress = null;
669            JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("AddressInUse"));
670        }
671    }
672
673    /**
674     * Open a programmer for this address
675     */
676    protected void openProgrammer() {
677        if (rosterEntry == null) {
678            return;
679        }
680
681        java.util.ResourceBundle rbt = java.util.ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle");
682        String ptitle = java.text.MessageFormat.format(rbt.getString("FrameOpsProgrammerTitle"), rosterEntry.getId());
683        // find the ops-mode programmer
684        int address = Integer.parseInt(rosterEntry.getDccAddress());
685        boolean longAddr = true;
686        if (address < 100) {
687            longAddr = false;
688        }
689        Programmer programmer = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class).getAddressedProgrammer(longAddr, address);
690        // and created the frame
691        JFrame p = new PaneOpsProgFrame(null, rosterEntry,
692                ptitle, "programmers" + File.separator + ProgDefault.getDefaultProgFile() + ".xml",
693                programmer);
694        p.pack();
695        p.setVisible(true);
696    }
697
698    /**
699     * Dispatch the current address for use by other throttles
700     */
701    public void dispatchAddress() {        
702        if (throttle != null) {
703            int usageCount  = throttleManager.getThrottleUsageCount(throttle.getLocoAddress()) - 1;
704            if ( usageCount != 0 ) {
705                JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("CannotDispatch", usageCount));
706                return;
707            }
708            notifyThrottleDisposed();
709            throttleManager.dispatchThrottle(throttle, this);
710            throttle = null;
711        }
712    }
713
714    /**
715     * Release the current address.
716     */
717    public void releaseAddress() {
718        notifyThrottleDisposed();
719        if (throttle != null) {
720            throttleManager.releaseThrottle(throttle, this);
721            throttle = null;
722        }
723        if (consistThrottle != null) {
724            throttleManager.releaseThrottle(consistThrottle, this);
725            consistThrottle = null;
726        }
727    }
728
729    private void notifyListenersOfThrottleRelease() {
730        if (listeners != null) {
731            listeners.forEach((l) -> {
732                // log.debug("Notify address listener {} of release", l.getClass());
733                if (consistAddress != null) {
734                    l.notifyConsistAddressReleased(consistAddress);
735                }
736                l.notifyAddressReleased(currentAddress);
737            });
738        }
739    }
740
741    /**
742     * Create an Element of this object's preferences.
743     * <ul>
744     * <li> Window Preferences
745     * <li> Address value
746     * </ul>
747     *
748     * @return org.jdom2.Element for this objects preferences. Defined in
749     *         DTD/throttle-config
750     */
751    public Element getXml() {
752        Element me = new Element("AddressPanel");
753        //Element window = new Element("window");
754        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1);
755        children.add(WindowPreferences.getPreferences(this));
756        children.add((new jmri.configurexml.LocoAddressXml())
757                .store(addrSelector.getAddress()));
758        children.add((new jmri.configurexml.LocoAddressXml())
759                .store(consistAddress));
760        me.setContent(children);
761        return me;
762    }
763
764    /**
765     * Use the Element passed to initialize based on user prefs.
766     *
767     * @param e The Element containing prefs as defined in DTD/throttle-config
768     */
769    public void setXml(Element e) {
770        Element window = e.getChild("window");
771        WindowPreferences.setPreferences(this, window);
772
773        Element addressElement = e.getChild("address");
774        if ((addressElement != null) && (this.getRosterEntry() == null)) {
775            String address = addressElement.getAttribute("value").getValue();
776            addrSelector.setAddress(new DccLocoAddress(Integer
777                    .parseInt(address), false)); // guess at the short/long
778            consistAddress = null;
779            changeOfAddress(addrSelector.getAddress());
780        }
781
782        List<Element> elementList = e.getChildren("locoaddress");
783        if ((!elementList.isEmpty()) && (getThrottle() == null)) {
784            log.debug("found {} locoaddress(es)", elementList.size() );
785            currentAddress = (DccLocoAddress) (new jmri.configurexml.LocoAddressXml())
786                    .getAddress(elementList.get(0));
787            log.debug("Loaded address {} from xml",currentAddress);
788            addrSelector.setAddress(currentAddress);
789            consistAddress = null;
790            // if there are two locoaddress, the second is the consist address
791            if (elementList.size() > 1) {
792                DccLocoAddress tmpAdd = ((DccLocoAddress) (new jmri.configurexml.LocoAddressXml())
793                        .getAddress(elementList.get(1)));
794                if (tmpAdd !=null && ! currentAddress.equals(tmpAdd)) {
795                    log.debug("and consist with {}",tmpAdd);
796                    consistAddress = tmpAdd;
797                }
798            }
799            changeOfAddress(addrSelector.getAddress());
800        }
801    }
802
803    /**
804     * @return the RosterEntrySelectorPanel
805     */
806    public RosterEntrySelectorPanel getRosterEntrySelector() {
807        return rosterBox;
808    }
809
810    /**
811     * @return the curently assigned motor throttle for regular locomotives or consist
812     */
813    public DccThrottle getThrottle() {
814        if (consistThrottle != null) {
815            return consistThrottle;
816        }
817        return throttle;
818    }
819    
820    /**
821     * @return the curently assigned function throttle for regular locomotives or consist
822     */
823    public DccThrottle getFunctionThrottle() {        
824        if (throttle != null) {
825            return throttle;
826        }
827        return consistThrottle;
828    }
829        
830    
831    /**
832     * @return the currently used decoder address
833     */    
834    public DccLocoAddress getCurrentAddress() {
835        return currentAddress;
836    }
837
838    /**
839     * set the currently used decoder address and initiate a throttle request
840     * if a consist address is already set, this address will be used only for functions
841     * 
842     * @param currentAddress the address to use
843     * 
844     */ 
845    public void setCurrentAddress(DccLocoAddress currentAddress) {
846        if (log.isDebugEnabled()) {
847            log.debug("Setting CurrentAddress to {}", currentAddress);
848        }
849        addrSelector.setAddress(currentAddress);
850        changeOfAddress(addrSelector.getAddress());
851    }
852    
853    /**
854     * set the currently used decoder address and initiate a throttle request (same as setCurrentAddress)
855     * if a consist address is already set, this address will be used only for functions
856     * 
857     * @param number the address
858     * @param isLong long/short (true/false) address
859     * 
860     */ 
861    public void setAddress(int number, boolean isLong) {
862        setCurrentAddress(new DccLocoAddress(number, isLong));
863    }
864
865    /**
866     * @return the current consist address if any
867     */
868    @CheckForNull
869    public DccLocoAddress getConsistAddress() {
870        return consistAddress;
871    }
872
873    /**
874     * set the currently used consist address and initiate a throttle request
875     * 
876     * @param consistAddress the consist address to use
877     */ 
878    public void setConsistAddress(DccLocoAddress consistAddress) {
879        log.debug("Setting Consist Address to {}", consistAddress);
880        this.consistAddress = consistAddress;
881        changeOfConsistAddress();
882    }
883
884    @Override
885    public void propertyChange(PropertyChangeEvent evt) {
886        if (evt == null) {
887            return;
888        }
889        if ("ThrottleConnected".compareTo(evt.getPropertyName()) == 0) {
890            if (((Boolean) evt.getOldValue()) && (!((Boolean) evt.getNewValue()))) {
891                log.debug("propertyChange: ThrottleConnected to false");
892                notifyThrottleDisposed();
893                throttle = null;
894                consistThrottle = null;
895            }
896        }
897
898        if ("DispatchEnabled".compareTo(evt.getPropertyName()) == 0) {
899            log.debug("propertyChange: Dispatch Button Enabled {}" , evt.getNewValue() );
900            dispatchButton.setEnabled( (Boolean) evt.getNewValue() );
901        }
902
903        if ("ReleaseEnabled".compareTo(evt.getPropertyName()) == 0) {
904            log.debug("propertyChange: release Button Enabled {}" , evt.getNewValue() );
905            releaseButton.setEnabled( (Boolean) evt.getNewValue() );
906        }
907    }
908
909    void applyPreferences() {
910        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())) {
911            rosterBox.getRosterGroupComboBox().setAllEntriesEnabled(InstanceManager.getDefault(ThrottlesPreferences.class).isAddressSelectorShowingAllRosterGroup());
912        }
913    }
914
915    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AddressPanel.class);
916
917}
918