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