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