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