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