001package jmri.jmrit.beantable.signalmast;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.util.*;
006import java.util.List;
007
008import javax.annotation.Nonnull;
009import javax.swing.*;
010import javax.swing.border.TitledBorder;
011
012import jmri.*;
013import jmri.implementation.DccSignalMast;
014import jmri.util.*;
015
016import org.openide.util.lookup.ServiceProvider;
017
018/**
019 * A pane for configuring DCC SignalMast objects.
020 *
021 * @see jmri.jmrit.beantable.signalmast.SignalMastAddPane
022 * @author Bob Jacobsen Copyright (C) 2018
023 * @since 4.11.2
024 */
025public class DccSignalMastAddPane extends SignalMastAddPane {
026
027    public DccSignalMastAddPane() {
028        init();
029    }
030
031    final void init() {
032        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
033
034        add(unLitOption());
035        add(connectionData());
036
037        dccMastScroll = new JScrollPane(dccMastPanel);
038        dccMastScroll.setBorder(BorderFactory.createEmptyBorder());
039        add(dccMastScroll);
040    }
041
042    JPanel connectionData() {
043        JPanel p = new JPanel();
044
045        TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black));
046        border.setTitle(Bundle.getMessage("DCCMastConnection"));
047        p.setBorder(border);
048
049        p.setLayout(new jmri.util.javaworld.GridLayout2(3, 3));
050
051        p.add(systemPrefixBoxLabel);
052        p.add(systemPrefixBox);
053        p.add(new JLabel());    // Empty 1,3 cell
054
055        p.add(dccAspectAddressLabel);
056        dccAspectAddressField.setText("");
057        dccOffSetAddress.setToolTipText(Bundle.getMessage("DccOffsetTooltip"));
058        p.add(dccAspectAddressField);
059        p.add(dccOffSetAddress);
060
061        p.add(new JLabel(Bundle.getMessage("DCCMastPacketSendCount")));
062        packetSendCountSpinner.setModel(new SpinnerNumberModel(3, 1, 4, 1));
063        p.add(packetSendCountSpinner);
064        packetSendCountSpinner.setToolTipText(Bundle.getMessage("DCCMastPacketSendCountToolTip"));
065
066        return p;
067    }
068
069    JPanel unLitOption() {
070        JPanel p = new JPanel();
071
072        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
073        p.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("AllowUnLitLabel"))));
074        p.add(allowUnLit);
075
076        p.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("DCCUnlitAspectId"))));
077        unlitIdSpinner.setModel(new SpinnerNumberModel(31, 0, 31, 1));
078        p.add(unlitIdSpinner);
079
080        return p;
081    }
082
083    /** {@inheritDoc} */
084    @Override
085    @Nonnull public String getPaneName() {
086        return Bundle.getMessage("DCCMast");
087    }
088
089    JScrollPane dccMastScroll;
090    JPanel dccMastPanel = new JPanel();
091
092    JLabel systemPrefixBoxLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("DCCSystem")));
093    JComboBox<String> systemPrefixBox = new JComboBox<>();
094
095    JLabel dccAspectAddressLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("DCCMastAddress")));
096    JTextField dccAspectAddressField = new JTextField(5);
097
098    JCheckBox dccOffSetAddress = new JCheckBox(Bundle.getMessage("DccAccessoryAddressOffSet"));
099
100    JCheckBox allowUnLit = new JCheckBox();
101//     JTextField unLitAspectField = new JTextField(5);
102
103    LinkedHashMap<String, DCCAspectPanel> dccAspect = new LinkedHashMap<>(NOTIONAL_ASPECT_COUNT);
104
105    DccSignalMast currentMast = null;
106    SignalSystem sigsys;
107    /* IMM Send Count */
108    JSpinner packetSendCountSpinner = new JSpinner();
109    JSpinner unlitIdSpinner = new JSpinner();
110
111    /**
112     * Check if a command station will work for this subtype.
113     * @param cs The current command station.
114     * @return true if cs supports IMM packets.
115     */
116    protected boolean usableCommandStation(CommandStation cs) {
117        return true;
118    }
119
120    /** {@inheritDoc} */
121    @Override
122    public void setAspectNames(@Nonnull SignalAppearanceMap map,
123                               @Nonnull SignalSystem sigSystem) {
124        log.trace("setAspectNames(...) start");
125
126        dccAspect.clear();
127
128        Enumeration<String> aspects = map.getAspects();
129        sigsys = map.getSignalSystem();
130
131        while (aspects.hasMoreElements()) {
132            String aspect = aspects.nextElement();
133            DCCAspectPanel aPanel = new DCCAspectPanel(aspect);
134            dccAspect.put(aspect, aPanel);
135            log.trace(" in loop, dccAspect: {} ", map.getProperty(aspect, "dccAspect"));
136            aPanel.setAspectId((String) sigSystem.getProperty(aspect, "dccAspect"));
137        }
138
139        systemPrefixBox.removeAllItems();
140        List<CommandStation> connList = InstanceManager.getList(CommandStation.class);
141        if (!connList.isEmpty()) {
142            for (int x = 0; x < connList.size(); x++) {
143                CommandStation station = connList.get(x);
144                if (usableCommandStation(station)) {
145                    systemPrefixBox.addItem(station.getUserName());
146                }
147            }
148        } else {
149            systemPrefixBox.addItem("None");
150        }
151
152        dccMastPanel.removeAll();
153
154        for (Map.Entry<String, DCCAspectPanel> entry : dccAspect.entrySet()) {
155            log.trace("   aspect: {}", entry.getKey());
156            dccMastPanel.add(entry.getValue().getPanel());
157        }
158
159        if (dccAspect.size() % 2 > 0) {
160            dccMastPanel.add(new JLabel());     // finish odd number aspect list
161        }
162
163        dccMastPanel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("DCCMastCopyAspectId"))));
164        dccMastPanel.add(copyFromMastSelection());
165
166        dccMastPanel.setLayout(new jmri.util.javaworld.GridLayout2(0, 2)); // 0 means enough
167        dccMastPanel.revalidate();
168        dccMastScroll.revalidate();
169
170        log.trace("setAspectNames(...) end");
171    }
172
173    /** {@inheritDoc} */
174    @Override
175    public boolean canHandleMast(@Nonnull SignalMast mast) {
176        // because that mast can be subtyped by something
177        // completely different, we text for exact here.
178        return mast.getClass().getCanonicalName().equals(DccSignalMast.class.getCanonicalName());
179    }
180
181    /** {@inheritDoc} */
182    @Override
183    public void setMast(SignalMast mast) {
184        log.debug("setMast({}) start", mast);
185        if (mast == null) {
186            currentMast = null;
187            log.debug("setMast() end early with null");
188            return;
189        }
190
191        if (! (mast instanceof DccSignalMast) ) {
192            log.error("mast was wrong type: {} {}", mast.getSystemName(), mast.getClass().getName());
193            log.debug("setMast({}) end early: wrong type", mast);
194            return;
195        }
196
197        currentMast = (DccSignalMast) mast;
198        SignalAppearanceMap appMap = mast.getAppearanceMap();
199
200        if (appMap != null) {
201            Enumeration<String> aspects = appMap.getAspects();
202            while (aspects.hasMoreElements()) {
203                String key = aspects.nextElement();
204                DCCAspectPanel dccPanel = dccAspect.get(key);
205                dccPanel.setAspectDisabled(currentMast.isAspectDisabled(key));
206                if (!currentMast.isAspectDisabled(key)) {
207                    dccPanel.setAspectId(currentMast.getOutputForAppearance(key));
208                }
209            }
210        }
211        List<CommandStation> connList = InstanceManager.getList(CommandStation.class);
212        if (!connList.isEmpty()) {
213            for (int x = 0; x < connList.size(); x++) {
214                CommandStation station = connList.get(x);
215                if (usableCommandStation(station)) {
216                    systemPrefixBox.addItem(station.getUserName());
217                }
218            }
219        } else {
220            systemPrefixBox.addItem("None");
221        }
222        dccAspectAddressField.setText("" + currentMast.getDccSignalMastAddress());
223        dccOffSetAddress.setSelected(currentMast.useAddressOffSet());
224        systemPrefixBox.setSelectedItem(currentMast.getCommandStation().getUserName());
225
226        systemPrefixBoxLabel.setEnabled(false);
227        systemPrefixBox.setEnabled(false);
228        dccAspectAddressLabel.setEnabled(false);
229        dccAspectAddressField.setEnabled(false);
230
231        allowUnLit.setSelected(currentMast.allowUnLit());
232        if (currentMast.allowUnLit()) {
233            unlitIdSpinner.setValue(currentMast.getUnlitId());
234        }
235
236        // set up DCC IMM send count
237        packetSendCountSpinner.setValue(currentMast.getDccSignalMastPacketSendCount());
238        log.debug("setMast({}) end", mast);
239    }
240
241    static boolean validateAspectId(@Nonnull String strAspect) {
242        int aspect;
243        try {
244            aspect = Integer.parseInt(strAspect.trim());
245        } catch (java.lang.NumberFormatException e) {
246            JOptionPane.showMessageDialog(null, Bundle.getMessage("DCCMastAspectNumber"));
247            return false;
248        }
249        if (aspect < 0 || aspect > 31) {
250            JOptionPane.showMessageDialog(null, Bundle.getMessage("DCCMastAspectOutOfRange"));
251            log.error("invalid aspect {}", aspect);
252            return false;
253        }
254        return true;
255    }
256
257    /**
258     * Get the first part of the system name
259     * for the specific mast type.
260     * @return For this specific class, "F$dsm:"
261     */
262    protected @Nonnull String getNamePrefix() {
263        return "F$dsm:";
264    }
265
266    /**
267     * Create a mast of the specific subtype.
268     * @param name A valid subtype name
269     * @return A SignalMast of that subtype
270     */
271    protected DccSignalMast constructMast(@Nonnull String name) {
272        return new DccSignalMast(name);
273    }
274
275    /** {@inheritDoc} */
276    @Override
277    public boolean createMast(@Nonnull
278            String sigsysname, @Nonnull
279                    String mastname, @Nonnull
280                            String username) {
281        log.debug("createMast({},{} start)", sigsysname, mastname);
282
283        // are we already editing?  If no, create a new one.
284        if (currentMast == null) {
285            log.trace("Creating new mast");
286            if (!validateDCCAddress()) {
287                log.trace("validateDCCAddress failed, return from createMast");
288                return false;
289            }
290            String systemNameText = ConnectionNameFromSystemName.getPrefixFromName((String) systemPrefixBox.getSelectedItem());
291            // if we return a null string then we will set it to use internal, thus picking up the default command station at a later date.
292            if (systemNameText == null || systemNameText.isEmpty()) {
293                systemNameText = "I";
294            }
295            systemNameText = systemNameText + getNamePrefix();
296
297            String name = systemNameText
298                    + sigsysname
299                    + ":" + mastname.substring(11, mastname.length() - 4);
300            name += "(" + dccAspectAddressField.getText() + ")";
301            currentMast = constructMast(name);
302            InstanceManager.getDefault(SignalMastManager.class).register(currentMast);
303        }
304
305        for (Map.Entry<String, DCCAspectPanel> entry : dccAspect.entrySet()) {
306            dccMastPanel.add(entry.getValue().getPanel()); // update mast from aspect subpanel panel
307            currentMast.setOutputForAppearance(entry.getKey(), entry.getValue().getAspectId());
308            if (entry.getValue().isAspectDisabled()) {
309                currentMast.setAspectDisabled(entry.getKey());
310            } else {
311                currentMast.setAspectEnabled(entry.getKey());
312            }
313        }
314        if (!username.isEmpty()) {
315            currentMast.setUserName(username);
316        }
317
318        currentMast.useAddressOffSet(dccOffSetAddress.isSelected());
319        currentMast.setAllowUnLit(allowUnLit.isSelected());
320        if (allowUnLit.isSelected()) {
321            currentMast.setUnlitId((Integer) unlitIdSpinner.getValue());
322        }
323
324        int sendCount = (Integer) packetSendCountSpinner.getValue(); // from a JSpinner with 1 set as minimum 4 max
325        currentMast.setDccSignalMastPacketSendCount(sendCount);
326
327        log.debug("createMast({},{} end)", sigsysname, mastname);
328        return true;
329   }
330
331
332    @ServiceProvider(service = SignalMastAddPane.SignalMastAddPaneProvider.class)
333    static public class SignalMastAddPaneProvider extends SignalMastAddPane.SignalMastAddPaneProvider {
334        /** {@inheritDoc} */
335        @Override
336        @Nonnull public String getPaneName() {
337            return Bundle.getMessage("DCCMast");
338        }
339        /** {@inheritDoc} */
340        @Override
341        @Nonnull public SignalMastAddPane getNewPane() {
342            return new DccSignalMastAddPane();
343        }
344    }
345
346    private boolean validateDCCAddress() {
347        if (dccAspectAddressField.getText().isEmpty()) {
348            JOptionPane.showMessageDialog(null, Bundle.getMessage("DCCMastAddressBlank"));
349            return false;
350        }
351        int address;
352        try {
353            address = Integer.parseInt(dccAspectAddressField.getText().trim());
354        } catch (java.lang.NumberFormatException e) {
355            JOptionPane.showMessageDialog(null, Bundle.getMessage("DCCMastAddressNumber"));
356            return false;
357        }
358
359        if (address < NmraPacket.accIdLowLimit || address > NmraPacket.accIdAltHighLimit) {
360            JOptionPane.showMessageDialog(null, Bundle.getMessage("DCCMastAddressOutOfRange"));
361            log.error("invalid address {}", address);
362            return false;
363        }
364        if (DccSignalMast.isDCCAddressUsed(address) != null) {
365            String msg = Bundle.getMessage("DCCMastAddressAssigned", new Object[]{dccAspectAddressField.getText(), DccSignalMast.isDCCAddressUsed(address)});
366            JOptionPane.showMessageDialog(null, msg);
367            return false;
368        }
369        return true;
370    }
371
372    @Nonnull JComboBox<String> copyFromMastSelection() {
373        JComboBox<String> mastSelect = new JComboBox<>();
374        for (SignalMast mast : InstanceManager.getDefault(SignalMastManager.class).getNamedBeanSet()) {
375            if (mast instanceof DccSignalMast){
376                mastSelect.addItem(mast.getDisplayName());
377            }
378        }
379        if (mastSelect.getItemCount() == 0) {
380            mastSelect.setEnabled(false);
381        } else {
382            mastSelect.insertItemAt("", 0);
383            mastSelect.setSelectedIndex(0);
384            mastSelect.addActionListener((ActionEvent e) -> {
385                @SuppressWarnings("unchecked") // e.getSource() cast from mastSelect source
386                JComboBox<String> eb = (JComboBox<String>) e.getSource();
387                String sourceMast = (String) eb.getSelectedItem();
388                if (sourceMast != null && !sourceMast.isEmpty()) {
389                    copyFromAnotherDCCMastAspect(sourceMast);
390                }
391            });
392        }
393        return mastSelect;
394    }
395
396    /**
397     * Copy aspects by name from another DccSignalMast.
398     * @param strMast User or system name of mast to copy from
399     */
400    void copyFromAnotherDCCMastAspect(@Nonnull String strMast) {
401        DccSignalMast mast = (DccSignalMast) InstanceManager.getDefault(SignalMastManager.class).getNamedBean(strMast);
402        if (mast == null) {
403            log.error("can't copy from another mast because {} doesn't exist", strMast);
404            return;
405        }
406        Vector<String> validAspects = mast.getValidAspects();
407        for (Map.Entry<String, DCCAspectPanel> entry : dccAspect.entrySet()) {
408            if (validAspects.contains(entry.getKey()) || mast.isAspectDisabled(entry.getKey())) { // valid doesn't include disabled
409                // present, copy
410                entry.getValue().setAspectId(mast.getOutputForAppearance(entry.getKey()));
411                entry.getValue().setAspectDisabled(mast.isAspectDisabled(entry.getKey()));
412            } else {
413                // not present, log
414                log.info("Can't get aspect \"{}\" from head \"{}\", leaving unchanged", entry.getKey(), mast);
415            }
416        }
417    }
418
419    /**
420     * JPanel to define properties of an Aspect for a DCC Signal Mast.
421     * <p>
422     * Invoked from the {@link AddSignalMastPanel} class when a DCC Signal Mast is
423     * selected.
424     */
425    static class DCCAspectPanel {
426
427        String aspect = "";
428        JCheckBox disabledCheck = new JCheckBox(Bundle.getMessage("DisableAspect"));
429        JLabel aspectLabel = new JLabel(Bundle.getMessage("DCCMastSetAspectId") + ":");
430        JTextField aspectId = new JTextField(5);
431
432        DCCAspectPanel(String aspect) {
433            this.aspect = aspect;
434        }
435
436        void setAspectDisabled(boolean boo) {
437            disabledCheck.setSelected(boo);
438            if (boo) {
439                aspectLabel.setEnabled(false);
440                aspectId.setEnabled(false);
441            } else {
442                aspectLabel.setEnabled(true);
443                aspectId.setEnabled(true);
444            }
445        }
446
447        boolean isAspectDisabled() {
448            return disabledCheck.isSelected();
449        }
450
451        int getAspectId() {
452            try {
453                String value = aspectId.getText();
454                return Integer.parseInt(value);
455
456            } catch (NumberFormatException ex) {
457                log.error("failed to convert DCC number");
458            }
459            return -1;
460        }
461
462        void setAspectId(int i) {
463            aspectId.setText("" + i);
464        }
465
466        void setAspectId(String s) {
467            aspectId.setText(s);
468        }
469
470        JPanel panel;
471
472        JPanel getPanel() {
473            if (panel == null) {
474                panel = new JPanel();
475                panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
476                JPanel dccDetails = new JPanel();
477                dccDetails.add(aspectLabel);
478                dccDetails.add(aspectId);
479                panel.add(dccDetails);
480                panel.add(disabledCheck);
481                TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black));
482                border.setTitle(aspect);
483                panel.setBorder(border);
484                aspectId.addFocusListener(new FocusListener() {
485                    @Override
486                    public void focusLost(FocusEvent e) {
487                        if (aspectId.getText().isEmpty()) {
488                            return;
489                        }
490                        if (!validateAspectId(aspectId.getText())) {
491                            aspectId.requestFocusInWindow();
492                        }
493                    }
494
495                    @Override
496                    public void focusGained(FocusEvent e) {
497                    }
498
499                });
500                disabledCheck.addActionListener((ActionEvent e) -> {
501                    setAspectDisabled(disabledCheck.isSelected());
502                });
503
504            }
505            return panel;
506        }
507    }
508
509    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DccSignalMastAddPane.class);
510
511}