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        // during testing listeners can be null
264        if (listeners != null) {
265            (new ArrayList<AddressListener>(listeners)).forEach((l) -> {
266                // log.debug("Notify address listener of address change {}", l.getClass());
267                l.notifyAddressThrottleFound(t);
268            });
269        }
270    }
271
272    @Override
273    public void notifyFailedThrottleRequest(LocoAddress address, String reason) {
274        JmriJOptionPane.showMessageDialog(null, reason, Bundle.getMessage("FailedSetupRequestTitle"), JmriJOptionPane.WARNING_MESSAGE);
275    }
276
277    /**
278    * A decision is required for Throttle creation to continue.
279    * <p>
280    * Steal / Cancel, Share / Cancel, or Steal / Share Cancel
281    */
282    @Override
283    public void notifyDecisionRequired(LocoAddress address, DecisionType question) {
284        if ( null != question )  {
285            switch (question) {
286                case STEAL:
287                    if (InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){
288                        throttleManager.responseThrottleDecision(address, this, DecisionType.STEAL );
289                        return;
290                    }
291                    jmri.util.ThreadingUtil.runOnGUI(() -> {
292                        if ( JmriJOptionPane.YES_OPTION == JmriJOptionPane.showConfirmDialog(
293                                this, Bundle.getMessage("StealQuestionText",address.toString()),
294                                Bundle.getMessage("StealRequestTitle"), JmriJOptionPane.YES_NO_OPTION)) {
295                            throttleManager.responseThrottleDecision(address, this, DecisionType.STEAL );
296                        } else {
297                            throttleManager.cancelThrottleRequest(address, this);
298                            requestedAddress = null;
299                        }
300                    });
301                    break;
302                case SHARE:
303                    if (InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare() ){
304                        throttleManager.responseThrottleDecision(address, this, DecisionType.SHARE );
305                        return;
306                    }
307                    jmri.util.ThreadingUtil.runOnGUI(() -> {
308                        if ( JmriJOptionPane.YES_OPTION == JmriJOptionPane.showConfirmDialog(
309                                this, Bundle.getMessage("ShareQuestionText",address.toString()),
310                                Bundle.getMessage("ShareRequestTitle"), JmriJOptionPane.YES_NO_OPTION)) {
311                            throttleManager.responseThrottleDecision(address, this, DecisionType.SHARE );
312                        } else {
313                            throttleManager.cancelThrottleRequest(address, this);
314                            requestedAddress = null;
315                        }
316                    });
317                    break;
318                case STEAL_OR_SHARE:
319                    if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentSteal() ){
320                        throttleManager.responseThrottleDecision(address, this, DecisionType.STEAL );
321                        return;
322                    }
323                    if ( InstanceManager.getDefault(ThrottlesPreferences.class).isSilentShare() ){
324                        throttleManager.responseThrottleDecision(address, this, DecisionType.SHARE );
325                        return;
326                    }
327                    String[] options = new String[] {Bundle.getMessage("StealButton"), Bundle.getMessage("ShareButton"), Bundle.getMessage("ButtonCancel")};
328                    jmri.util.ThreadingUtil.runOnGUI(() -> {
329                        int response = JmriJOptionPane.showOptionDialog(AddressPanel.this,
330                                Bundle.getMessage("StealShareQuestionText",address.toString()), Bundle.getMessage("StealShareRequestTitle"),
331                                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]);
332                        switch (response) {
333                            case 0:
334                                log.debug("steal clicked");
335                                throttleManager.responseThrottleDecision(address, AddressPanel.this, DecisionType.STEAL);
336                                break;
337                            case 1:
338                                log.debug("share clicked");
339                                throttleManager.responseThrottleDecision(address, AddressPanel.this, DecisionType.SHARE);
340                                break;
341                            default:
342                                log.debug("cancel clicked");
343                                throttleManager.cancelThrottleRequest(address, AddressPanel.this);
344                                requestedAddress = null;
345                                break;
346                        }
347                    });
348                    break;
349                default:
350                    break;
351            }
352        }
353    }
354
355    /**
356     * Get notification that a consist throttle has been found as we requested.
357     *
358     * @param t An instantiation of the DccThrottle with the address requested.
359     */
360    public void notifyConsistThrottleFound(DccThrottle t) {
361        if (consistThrottle != null) {
362            log.debug("notifyConsistThrottleFound() consistThrottle non null, called for loc {}",t.getLocoAddress());
363            return;
364        }        
365        requestedAddress = null;
366        consistThrottle = t;
367        currentAddress = (DccLocoAddress) t.getLocoAddress();
368        consistThrottle.addPropertyChangeListener(this);
369        
370        Consist consist = getConsistEntry();
371        if (consist != null && consist.getConsistType() == Consist.CS_CONSIST) {
372            // CS Consist, consist has the head locomotive id
373            // can we find a roster entry?
374            if ((rosterEntry == null)
375                    && (InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())
376                    && (InstanceManager.getDefault(ThrottlesPreferences.class).isEnablingRosterSearch())
377                    && currentAddress != null) {
378                List<RosterEntry> l = Roster.getDefault().matchingList(null, null, "" + currentAddress.getNumber(), null, null, null, null);
379                if (!l.isEmpty()) {
380                    rosterEntry = l.get(0);
381                }
382            }
383        }
384        
385        updateGUIOnThrottleFound(true);
386                
387        // send notification of new address
388        // work on a clone because some new listeners may be added while notifying the existing ones
389        (new ArrayList<AddressListener>(listeners)).forEach((l) -> {
390            l.notifyConsistAddressThrottleFound(t);
391        });
392        
393        if (consist != null && consist.getConsistType() == Consist.ADVANCED_CONSIST) {      
394            // request a throttle for head locomotive for functions
395            DccLocoAddress headLocoAddress = consist.getConsistList().get(0);
396            // only if consist address is not head locomotive address
397            if (! headLocoAddress.equals(currentAddress)) {
398                log.debug("Advanced consist throttle, requesting secondary throttle for head locomotive function control.");
399                changeOfAddress(headLocoAddress);
400            }
401        }
402    }
403    
404    private void updateGUIOnThrottleFound(boolean throttleActive) {
405        // update GUI
406        isUpdatingUI = true;
407        //addrSelector.setAddress(currentAddress);
408        setButton.setEnabled(!throttleActive);
409        addrSelector.setEnabled(!throttleActive);
410        releaseButton.setEnabled(throttleActive);
411        if (throttleActive && rosterEntry != null) {
412            getRosterEntrySelector().setSelectedRosterEntry(rosterEntry);
413        } else {
414            getRosterEntrySelector().getRosterEntryComboBox().setSelectedItem(Bundle.getMessage("NoLocoSelected"));
415        }
416        getRosterEntrySelector().setEnabled(!throttleActive);
417        if (conRosterBox != null) {
418            if (throttleActive && consistThrottle != null) {
419                conRosterBox.setSelectedItem(consistThrottle.getLocoAddress());
420            } else {
421                conRosterBox.setSelectedItem(Bundle.getMessage("NoConsistSelected"));
422            }
423            conRosterBox.setEnabled(!throttleActive);
424        }     
425        if (throttleManager.hasDispatchFunction()) {
426            dispatchButton.setEnabled(throttleActive);
427        }  
428        // enable program button if programmer available
429        // for ops-mode programming
430        if ((rosterEntry != null) && (ProgDefault.getDefaultProgFile() != null)
431                && (InstanceManager.getNullableDefault(jmri.AddressedProgrammerManager.class) != null)
432                && (InstanceManager.getDefault(jmri.AddressedProgrammerManager.class).isAddressedModePossible())) {
433            progButton.setEnabled(true);
434        } else {
435            progButton.setEnabled(false);
436        }
437        isUpdatingUI = false;
438    }
439
440    /**
441     * Receive notification that an address has been release or dispatched.
442     */
443    public void notifyThrottleDisposed() {
444        log.debug("notifyThrottleDisposed");
445        notifyListenersOfThrottleRelease();
446        updateGUIOnThrottleFound(false);
447        rosterEntry = null;
448        if (consistThrottle != null) {
449            consistThrottle.removePropertyChangeListener(this);
450        }
451        if (throttle != null) {
452            throttle.removePropertyChangeListener(this);
453        }
454    }
455
456    /**
457     * Get the RosterEntry if there's one for this throttle.
458     *
459     * @return RosterEntry or null
460     */
461    public RosterEntry getRosterEntry() {
462        return rosterEntry;
463    }
464    
465    /**
466     * Get the selected Consist if there's one for this throttle.
467     *
468     * @return Consist or null
469     */    
470    public Consist getConsistEntry() {
471        if (consistManager == null || consistAddress == null || !consistManager.isEnabled()) {
472            return null;
473        }
474        if (consistManager.getConsistList().contains(consistAddress)) {
475            return consistManager.getConsist(consistAddress);
476        }
477        return null;
478    }
479
480    /**
481     * Set the RosterEntry for this throttle and initiate a throttle request
482     * @param entry roster entry to set.
483     */
484    public void setRosterEntry(RosterEntry entry) {
485        isUpdatingUI = true;
486        getRosterEntrySelector().setSelectedRosterEntry(entry);
487        addrSelector.setAddress(entry.getDccLocoAddress());
488        isUpdatingUI = false;
489        rosterEntry = entry;
490        changeOfAddress(addrSelector.getAddress());
491    }
492
493    /**
494     * Create, initialize and place the GUI objects.
495     */
496    @SuppressWarnings("unchecked") //for the onRosterBox.insertItemAt(), to be a removed once NCE consists clarified
497    private void initGUI() {
498        this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
499        mainPanel = new JPanel();
500        mainPanel.setLayout(new BorderLayout());
501        this.setContentPane(mainPanel);
502
503        // center: address input
504        addrSelector.setVariableSize(true);
505        mainPanel.add(addrSelector.getCombinedJPanel(), BorderLayout.CENTER);
506        addrSelector.getTextField().addActionListener(e -> {
507            if (isUpdatingUI) {
508                return;
509            }
510            consistAddress = null;
511            changeOfAddress(addrSelector.getAddress());
512        });
513
514        // top : roster and consists selectors
515        JPanel topPanel = new JPanel();
516        topPanel.setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
517
518        rosterBox = new RosterEntrySelectorPanel();
519        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle()) && (InstanceManager.getDefault(ThrottlesPreferences.class).isAddressSelectorShowingAllRosterGroup())) {
520            rosterBox.getRosterGroupComboBox().setAllEntriesEnabled(true);
521        }
522        getRosterEntrySelector().setNonSelectedItem(Bundle.getMessage("NoLocoSelected"));
523        getRosterEntrySelector().setToolTipText(Bundle.getMessage("SelectLocoFromRosterTT"));
524        getRosterEntrySelector().addPropertyChangeListener("selectedRosterEntries", pce -> selectRosterEntry());
525        getRosterEntrySelector().setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
526        topPanel.add(getRosterEntrySelector());
527
528        if (InstanceManager.getDefault(NceConsistRoster.class).numEntries() > 0) { // NCE consists
529            // NCE implementation of consists is specific, TODO: refactor to use generic JMRI consists
530            conRosterBox = InstanceManager.getDefault(NceConsistRoster.class).fullRosterComboBox();
531            conRosterBox.insertItemAt(Bundle.getMessage("NoConsistSelected"), 0);  // empty entry
532            conRosterBox.setSelectedIndex(0);
533            conRosterBox.setToolTipText(Bundle.getMessage("SelectConsistFromRosterTT"));
534            conRosterBox.addActionListener(e -> nceConsistRosterSelected());
535            topPanel.add(conRosterBox);
536        } else {                      
537            if ((consistManager != null) && (consistManager.isEnabled())) {  // JMRI consists
538                JPanel consistPanel = new JPanel();
539                JButton consistToolButton = new JButton(new jmri.jmrit.consisttool.ConsistToolAction());
540                consistPanel.add(consistToolButton);
541                conRosterBox = new ConsistComboBox();
542                conRosterBox.addActionListener(e -> jmriConsistRosterSelected());
543                consistPanel.add(conRosterBox);
544                topPanel.add(consistPanel);
545            }
546        }
547
548        mainPanel.add(topPanel, BorderLayout.NORTH);
549
550        // bottom : buttons
551        JPanel buttonPanel = new JPanel();
552        buttonPanel.setLayout(new WrapLayout(FlowLayout.CENTER, 2, 2));
553
554        progButton = new JButton(Bundle.getMessage("ButtonProgram"));
555        buttonPanel.add(progButton);
556        progButton.setEnabled(false);
557        progButton.addActionListener(e -> openProgrammer());
558
559        dispatchButton = new JButton(Bundle.getMessage("ButtonDispatch"));
560        buttonPanel.add(dispatchButton);
561        dispatchButton.setEnabled(false);
562        dispatchButton.addActionListener(e -> dispatchAddress());
563
564        releaseButton = new JButton(Bundle.getMessage("ButtonRelease"));
565        buttonPanel.add(releaseButton);
566        releaseButton.setEnabled(false);
567        releaseButton.addActionListener(e -> releaseAddress());
568
569        setButton = new JButton(Bundle.getMessage("ButtonSet"));
570        setButton.addActionListener(e -> {
571            consistAddress = null;
572            changeOfAddress(addrSelector.getAddress());
573        });
574        buttonPanel.add(setButton);
575
576        mainPanel.add(buttonPanel, BorderLayout.SOUTH);
577
578        pack();
579    }
580
581    private void jmriConsistRosterSelected() {
582        if (isUpdatingUI) {
583            return;
584        }
585        if ((conRosterBox.getSelectedIndex() != 0) && (conRosterBox.getSelectedItem() instanceof DccLocoAddress)) {
586            consistAddress = (DccLocoAddress) conRosterBox.getSelectedItem() ;            
587            changeOfConsistAddress();
588        }
589    }
590
591    private void nceConsistRosterSelected() {
592        if (isUpdatingUI) {
593            return;
594        }
595        if (!(Objects.equals(conRosterBox.getSelectedItem(), Bundle.getMessage("NoConsistSelected")))) {
596            String rosterEntryTitle = Objects.requireNonNull(conRosterBox.getSelectedItem()).toString();
597            NceConsistRosterEntry nceConsistRosterEntry = InstanceManager.getDefault(NceConsistRoster.class)
598                    .entryFromTitle(rosterEntryTitle);
599
600            DccLocoAddress a = new DccLocoAddress(Integer.parseInt(nceConsistRosterEntry
601                    .getLoco1DccAddress()), nceConsistRosterEntry.isLoco1LongAddress());
602            addrSelector.setAddress(a);
603            consistAddress = null;
604            int cA = 0;
605            try {
606                cA = Integer.parseInt(nceConsistRosterEntry.getConsistNumber());
607            } catch (NumberFormatException ignored) {
608
609            }
610            if (0 < cA && cA < 128) {
611                consistAddress = new DccLocoAddress(cA, false);
612            } else {
613                log.warn("consist number missing {}", nceConsistRosterEntry.getLoco1DccAddress());
614                JmriJOptionPane.showMessageDialog(mainPanel,
615                        Bundle.getMessage("ConsistNumberHasNotBeenAssigned"),
616                        Bundle.getMessage("NeedsConsistNumber"),
617                        JmriJOptionPane.ERROR_MESSAGE);
618                return;
619            }
620            if (JmriJOptionPane.showConfirmDialog(mainPanel,
621                    Bundle.getMessage("SendFunctionToLead"), Bundle.getMessage("NCEconsistThrottle"),
622                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
623                addrSelector.setAddress(consistAddress);
624                consistAddress = null;
625            }
626            changeOfAddress(addrSelector.getAddress());
627        }
628    }
629
630    /**
631     * The user has selected a new address. Notify all listeners.
632     */
633    private void changeOfAddress(DccLocoAddress a) {
634        currentAddress = a;
635        if (currentAddress == null) {
636            return; // no address
637        }
638        // send notification of new address
639        // during testing listeners can be null
640        if (listeners != null) {
641            listeners.forEach((l) -> {
642                l.notifyAddressChosen(currentAddress);
643            });
644        }
645        log.debug("Requesting new slot for address {} rosterEntry {}",currentAddress,rosterEntry);
646        boolean requestOK;
647        if (rosterEntry == null) {
648            requestedAddress = currentAddress;
649            requestOK = throttleManager.requestThrottle(currentAddress, this, true);
650        }
651        else {
652            requestedAddress = rosterEntry.getDccLocoAddress();
653            requestOK = throttleManager.requestThrottle(rosterEntry, this, true);
654        }
655        if (!requestOK) {
656            requestedAddress = null;
657            JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("AddressInUse"));
658        }
659    }
660
661    private void changeOfConsistAddress() {
662        if (consistAddress == null) {
663            return; // no address
664        }  
665        addrSelector.setAddress(consistAddress);
666        // send notification of new address
667        listeners.forEach((l) -> {
668            l.notifyAddressChosen(currentAddress);
669        });
670        log.debug("Requesting new slot for consist address {}",consistAddress);        
671        requestedAddress = consistAddress;
672        boolean requestOK = throttleManager.requestThrottle(consistAddress, this, true);
673        if (!requestOK) {
674            requestedAddress = null;
675            JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("AddressInUse"));
676        }
677    }
678
679    /**
680     * Open a programmer for this address
681     */
682    protected void openProgrammer() {
683        if (rosterEntry == null) {
684            return;
685        }
686
687        java.util.ResourceBundle rbt = java.util.ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle");
688        String ptitle = java.text.MessageFormat.format(rbt.getString("FrameOpsProgrammerTitle"), rosterEntry.getId());
689        // find the ops-mode programmer
690        int address = Integer.parseInt(rosterEntry.getDccAddress());
691        boolean longAddr = true;
692        if (address < 100) {
693            longAddr = false;
694        }
695        Programmer programmer = InstanceManager.getDefault(jmri.AddressedProgrammerManager.class).getAddressedProgrammer(longAddr, address);
696        // and created the frame
697        JFrame p = new PaneOpsProgFrame(null, rosterEntry,
698                ptitle, "programmers" + File.separator + ProgDefault.getDefaultProgFile() + ".xml",
699                programmer);
700        p.pack();
701        p.setVisible(true);
702    }
703
704    /**
705     * Dispatch the current address for use by other throttles
706     */
707    public void dispatchAddress() {        
708        if (throttle != null) {
709            int usageCount  = throttleManager.getThrottleUsageCount(throttle.getLocoAddress()) - 1;
710            if ( usageCount != 0 ) {
711                JmriJOptionPane.showMessageDialog(mainPanel, Bundle.getMessage("CannotDispatch", usageCount));
712                return;
713            }
714            notifyThrottleDisposed();
715            throttleManager.dispatchThrottle(throttle, this);
716            throttle = null;
717        }
718    }
719
720    /**
721     * Release the current address.
722     */
723    public void releaseAddress() {
724        notifyThrottleDisposed();
725        if (throttle != null) {
726            throttleManager.releaseThrottle(throttle, this);
727            throttle = null;
728        }
729        if (consistThrottle != null) {
730            throttleManager.releaseThrottle(consistThrottle, this);
731            consistThrottle = null;
732        }
733    }
734
735    private void notifyListenersOfThrottleRelease() {
736        if (listeners != null) {
737            listeners.forEach((l) -> {
738                // log.debug("Notify address listener {} of release", l.getClass());
739                if (consistAddress != null) {
740                    l.notifyConsistAddressReleased(consistAddress);
741                }
742                l.notifyAddressReleased(currentAddress);
743            });
744        }
745    }
746
747    /**
748     * Create an Element of this object's preferences.
749     * <ul>
750     * <li> Window Preferences
751     * <li> Address value
752     * </ul>
753     *
754     * @return org.jdom2.Element for this objects preferences. Defined in
755     *         DTD/throttle-config
756     */
757    public Element getXml() {
758        Element me = new Element("AddressPanel");
759        //Element window = new Element("window");
760        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1);
761        children.add(WindowPreferences.getPreferences(this));
762        children.add((new jmri.configurexml.LocoAddressXml())
763                .store(addrSelector.getAddress()));
764        children.add((new jmri.configurexml.LocoAddressXml())
765                .store(consistAddress));
766        me.setContent(children);
767        return me;
768    }
769
770    /**
771     * Use the Element passed to initialize based on user prefs.
772     *
773     * @param e The Element containing prefs as defined in DTD/throttle-config
774     */
775    public void setXml(Element e) {
776        Element window = e.getChild("window");
777        WindowPreferences.setPreferences(this, window);
778
779        Element addressElement = e.getChild("address");
780        if ((addressElement != null) && (this.getRosterEntry() == null)) {
781            String address = addressElement.getAttribute("value").getValue();
782            addrSelector.setAddress(new DccLocoAddress(Integer
783                    .parseInt(address), false)); // guess at the short/long
784            consistAddress = null;
785            changeOfAddress(addrSelector.getAddress());
786        }
787
788        List<Element> elementList = e.getChildren("locoaddress");
789        if ((!elementList.isEmpty()) && (getThrottle() == null)) {
790            log.debug("found {} locoaddress(es)", elementList.size() );
791            currentAddress = (DccLocoAddress) (new jmri.configurexml.LocoAddressXml())
792                    .getAddress(elementList.get(0));
793            log.debug("Loaded address {} from xml",currentAddress);
794            addrSelector.setAddress(currentAddress);
795            consistAddress = null;
796            // if there are two locoaddress, the second is the consist address
797            if (elementList.size() > 1) {
798                DccLocoAddress tmpAdd = ((DccLocoAddress) (new jmri.configurexml.LocoAddressXml())
799                        .getAddress(elementList.get(1)));
800                if (tmpAdd !=null && ! currentAddress.equals(tmpAdd)) {
801                    log.debug("and consist with {}",tmpAdd);
802                    consistAddress = tmpAdd;
803                }
804            }
805            changeOfAddress(addrSelector.getAddress());
806        }
807    }
808
809    /**
810     * @return the RosterEntrySelectorPanel
811     */
812    public RosterEntrySelectorPanel getRosterEntrySelector() {
813        return rosterBox;
814    }
815
816    /**
817     * @return the curently assigned motor throttle for regular locomotives or consist
818     */
819    public DccThrottle getThrottle() {
820        if (consistThrottle != null) {
821            return consistThrottle;
822        }
823        return throttle;
824    }
825    
826    /**
827     * @return the curently assigned function throttle for regular locomotives or consist
828     */
829    public DccThrottle getFunctionThrottle() {        
830        if (throttle != null) {
831            return throttle;
832        }
833        return consistThrottle;
834    }
835        
836    
837    /**
838     * @return the currently used decoder address
839     */    
840    public DccLocoAddress getCurrentAddress() {
841        return currentAddress;
842    }
843
844    /**
845     * set the currently used decoder address and initiate a throttle request
846     * if a consist address is already set, this address will be used only for functions
847     * 
848     * @param currentAddress the address to use
849     * 
850     */ 
851    public void setCurrentAddress(DccLocoAddress currentAddress) {
852        if (log.isDebugEnabled()) {
853            log.debug("Setting CurrentAddress to {}", currentAddress);
854        }
855        addrSelector.setAddress(currentAddress);
856        changeOfAddress(addrSelector.getAddress());
857    }
858    
859    /**
860     * set the currently used decoder address and initiate a throttle request (same as setCurrentAddress)
861     * if a consist address is already set, this address will be used only for functions
862     * 
863     * @param number the address
864     * @param isLong long/short (true/false) address
865     * 
866     */ 
867    public void setAddress(int number, boolean isLong) {
868        setCurrentAddress(new DccLocoAddress(number, isLong));
869    }
870
871    /**
872     * @return the current consist address if any
873     */
874    @CheckForNull
875    public DccLocoAddress getConsistAddress() {
876        return consistAddress;
877    }
878
879    /**
880     * set the currently used consist address and initiate a throttle request
881     * 
882     * @param consistAddress the consist address to use
883     */ 
884    public void setConsistAddress(DccLocoAddress consistAddress) {
885        log.debug("Setting Consist Address to {}", consistAddress);
886        this.consistAddress = consistAddress;
887        changeOfConsistAddress();
888    }
889
890    @Override
891    public void propertyChange(PropertyChangeEvent evt) {
892        if (evt == null) {
893            return;
894        }
895        if ("ThrottleConnected".compareTo(evt.getPropertyName()) == 0) {
896            if (((Boolean) evt.getOldValue()) && (!((Boolean) evt.getNewValue()))) {
897                log.debug("propertyChange: ThrottleConnected to false");
898                notifyThrottleDisposed();
899                // remove all listeners and destroy the throttle
900                destroy();
901                throttle = null;
902                consistThrottle = null;
903            }
904        }
905
906        if ("DispatchEnabled".compareTo(evt.getPropertyName()) == 0) {
907            log.debug("propertyChange: Dispatch Button Enabled {}" , evt.getNewValue() );
908            dispatchButton.setEnabled( (Boolean) evt.getNewValue() );
909        }
910
911        if ("ReleaseEnabled".compareTo(evt.getPropertyName()) == 0) {
912            log.debug("propertyChange: release Button Enabled {}" , evt.getNewValue() );
913            releaseButton.setEnabled( (Boolean) evt.getNewValue() );
914        }
915    }
916
917    void applyPreferences() {
918        if ((InstanceManager.getDefault(ThrottlesPreferences.class).isUsingExThrottle())) {
919            rosterBox.getRosterGroupComboBox().setAllEntriesEnabled(InstanceManager.getDefault(ThrottlesPreferences.class).isAddressSelectorShowingAllRosterGroup());
920        }
921    }
922
923    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AddressPanel.class);
924
925}
926