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