001package jmri.jmrit.symbolicprog;
002
003import java.awt.GridBagConstraints;
004import java.awt.GridBagLayout;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.List;
008import javax.swing.JComponent;
009import javax.swing.JLabel;
010import javax.swing.JPanel;
011import jmri.util.CvUtil;
012import jmri.util.jdom.LocaleSelector;
013
014import org.jdom2.*;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Provide a graphical representation of the NMRA Standard mapping between cab
021 * functions and physical outputs.
022 * <p>
023 * Uses data from the "model" element from the decoder definition file to
024 * configure the number of rows and columns and set up any custom column
025 * names:
026 * <dl>
027 * <dt>numOuts</dt>
028 * <dd>Number of physical outputs.</dd>
029 * <dd>&nbsp;</dd>
030 * <dt>numFns</dt>
031 * <dd>Maximum number of function rows to display.</dd>
032 * <dd>&nbsp;</dd>
033 * <dt>output</dt>
034 * <dd>name="n" label="yyy"</dd>
035 * <dd>&nbsp;-&nbsp;Set lower line of heading for column number "n" to
036 * "yyy".*</dd>
037 * <dd>&nbsp;</dd>
038 * <dd>name="n" label="xxx|yyy"</dd>
039 * <dd>&nbsp;-&nbsp;Set upper line of heading for column number "n" to "xxx" and
040 * lower line to "yyy".*</dd>
041 * <dd>&nbsp;</dd>
042 * <dd>name="n" label="|"</dd>
043 * <dd>&nbsp;-&nbsp;Sets both lines of heading for column number "n" to blank,
044 * causing the column to be suppressed from the table.*</dd>
045 * <dd>&nbsp;</dd>
046 * <dd>&nbsp;*&nbsp;The forms above increase the value of numOuts to n if
047 * numOuts &lt; n.</dd>
048 * <dd>&nbsp;</dd>
049 * <dd>name="text1" label="text2"</dd>
050 * <dd>&nbsp;-&nbsp;Set upper line of heading of column numOuts+1 to "xxx" and
051 * lower line to "yyy". numOuts is then incremented.</dd>
052 * <dd>&nbsp;(This is a legacy form, the other forms are preferred.)</dd>
053 * </dl>
054 * <dl>
055 * <dt>Default column headings:</dt>
056 * <dd>First row is the column number.</dd>
057 * <dd>Second row is defined in "SymbolicProgBundle.properties".</dd>
058 * <dd>Column headings can be overridden by the "output" elements documented
059 * above.</dd>
060 * <dd>&nbsp;</dd>
061 * <dt>Two rows are available for column headings:</dt>
062 * <dd>Use the "|" character to designate a row break.</dd>
063 * </dl>
064 * <dl>
065 * <dt>Columns will be suppressed if any of the following are true:</dt>
066 * <dd>No variables are found for that column.</dd>
067 * <dd>The column output name is of the form name="n" label="|".</dd>
068 * <dd>Column number is &gt; maxOut (an internal variable, currently 40).</dd>
069 * </dl>
070 * <dl>
071 * <dt>Searches the decoder file for variable definitions of the form:</dt>
072 * <dd>"Fd controls output n" (where d is a function number in the range 0-28
073 * and n is an output number in the range 0-maxOut)</dd>
074 * <dd>"FL controls output n" (L for light)</dd>
075 * <dd>"Sd controls output n" (where s is a sensor number in the range 0-28
076 * and n is an output number in the range 0-maxOut)</dd>
077 * <dd>"STOP controls output n" (where STOP designates a decoder state)</dd>
078 * <dd>"DRIVE controls output n" (where DRIVE designates a decoder state)</dd>
079 * <dd>"FWD controls output n" (where FWD designates a decoder state)</dd>
080 * <dd>"REV controls output n" (where REV designates a decoder state)</dd>
081 * <dd><br>Directional variants of all the above forms:</dd>
082 * <dd>"xxx(f) controls output n"</dd>
083 * <dd>"xxx(r) controls output n"</dd>
084 * <dd><br>Alternate variants of all the above forms:</dd>
085 * <dd>"xxx controls output n(alt)" (allows an alternate definition for the same
086 * variable, such as used by Tsunami decoders)</dd>
087 * <dd>"xxx(f) controls output n(alt)"</dd>
088 * <dd>"xxx(r) controls output n(alt)"</dd>
089 * <dd><br>
090 * The "tooltip" &amp; "label" attributes on a fnmapping variable are ignored.
091 * Expanded internationalized tooltips are generated in the code.
092 * </dd>
093 * </dl>
094 *
095 * @author Bob Jacobsen Copyright (C) 2001
096 * @author Dave Heap Copyright (C) 2016
097 */
098public class FnMapPanel extends JPanel {
099
100    // GridBayLayout column numbers
101    int fnNameCol = 0;
102    int firstOutCol = 1;
103
104    // GridBayLayout row numbers
105    int outputNameRow = 0;
106    int outputNumRow = 1;
107    int outputLabelRow = 2;
108    int firstFnRow = 3;
109
110    // Some limits and defaults
111    int highestFn = 28;
112    int highestSensor = 28;
113    int numFn;  // calculated later
114    int numOut = 20; // default number of physical outputs
115    int maxOut = 40; // maximum number of output columns
116
117    final String[] outName = new String[maxOut];
118    final String[] outLabel = new String[maxOut];
119    final boolean[] outIsUsed = new boolean[maxOut];
120
121    final String[] fnExtraList = new String[]{"STOP", "DRIVE", "FWD", "REV", "FL"};
122    final String[] fnVariantList = new String[]{"", "(f)", "(r)"};
123
124    List<String> fnList;
125    GridBagLayout gl = null;
126    GridBagConstraints cs = null;
127    VariableTableModel _varModel;
128
129    public FnMapPanel(VariableTableModel v, List<Integer> varsUsed, Element model) {
130        if (log.isDebugEnabled()) {
131            log.debug("Function map starts");
132        }
133        _varModel = v;
134
135        // Set up fnList array
136        this.fnList = new ArrayList<>();
137        fnList.addAll(Arrays.asList(fnExtraList));
138        for (int i = 0; i <= highestFn; i++) {
139            fnList.add("F" + i);
140        }
141        for (int i = 0; i <= highestSensor; i++) {
142            fnList.add("S" + i);
143        }
144
145        numFn = fnList.size() * fnVariantList.length;
146
147        // set up default names and labels
148        for (int iOut = 0; iOut < maxOut; iOut++) {
149            outName[iOut] = Integer.toString(iOut + 1);
150            outIsUsed[iOut] = false;
151            // get default labels, if any
152            try {
153                outLabel[iOut] = Bundle.getMessage("FnMapOutLabelDefault_" + (iOut + 1));
154            } catch (java.util.MissingResourceException e) {
155                outLabel[iOut] = "";  // no default label specified
156            }
157        }
158
159        // configure number of channels, arrays
160        configOutputs(model);
161
162        // initialize the layout
163        gl = new GridBagLayout();
164        cs = new GridBagConstraints();
165        setLayout(gl);
166
167        {
168            JLabel l = new JLabel(Bundle.getMessage("FnMapOutWireOr"));
169            cs.gridy = outputNameRow;
170            cs.gridx = firstOutCol;
171            cs.gridwidth = GridBagConstraints.REMAINDER;
172            gl.setConstraints(l, cs);
173            add(l);
174            cs.gridwidth = 1;
175        }
176
177        labelAt(0, fnNameCol, Bundle.getMessage("FnMapDesc"), GridBagConstraints.LINE_START);
178
179// Loop through function names and output names looking for variables
180        int row = firstFnRow;
181        for (String fnNameBase : fnList) {
182            if ((row - firstFnRow) >= numFn) {
183                break; // for compatibility with legacy defintions
184            }
185            for (String fnDirVariant : fnVariantList) {
186                String fnNameString = fnNameBase + fnDirVariant;
187//                log.info(fnNameString);
188                boolean rowIsUsed = false;
189                for (int iOut = 0; iOut < numOut; iOut++) {
190                    // if column is not suppressed by blank headers
191                    if (!outName[iOut].equals("") || !outLabel[iOut].equals("")) {
192                        // find the variable using the output number or label
193                        // include an (alt) variant to enable Tsunami function exchange definitions
194                        String searchNameBase = fnNameString + " controls output ";
195                        List<String> names = new ArrayList<>();
196                        if (!outName[iOut].equals(Integer.toString(iOut + 1))) {
197                            names.add(searchNameBase + (iOut + 1));
198                            names.add(searchNameBase + (iOut + 1) + "(alt)");
199                        }
200                        names.add(searchNameBase + outName[iOut]);
201                        names.add(searchNameBase + outName[iOut] + "(alt)");
202                        for (String name : names) {
203//                            log.info("Search name='" + name + "'");
204                            int iVar = _varModel.findVarIndex(name);
205                            if (iVar >= 0) {
206                                if (log.isDebugEnabled()) {
207                                    log.debug("Process var: {} as index {}", name, iVar);
208                                }
209                                varsUsed.add(Integer.valueOf(iVar));
210                                VariableValue var = _varModel.getVariable(iVar);
211                                // Only single-bit (exactly two options) variables should use checkbox
212                                // this really would be better fixed in EnumVariableValue
213                                // done here to avoid side effects elsewhere
214                                String displayFormat = "checkbox";
215                                if ((var.getMask() != null) && (((var.getMask().replace("X", "")).length()) != 1)) {
216                                    displayFormat = "";
217                                }
218                                JComponent j = (JComponent) (_varModel.getRep(iVar, displayFormat));
219                                j.setToolTipText(CvUtil.addCvDescription((fnNameString + " "
220                                        + Bundle.getMessage("FnMapControlsOutput") + " "
221                                        + outName[iOut] + " " + outLabel[iOut]), var.getCvDescription(), var.getMask()));
222                                int column = firstOutCol + iOut;
223                                saveAt(row, column, j);
224                                rowIsUsed = true;
225                                outIsUsed[iOut] = true;
226                            } else {
227                                if (log.isDebugEnabled()) {
228                                    log.debug("Did not find var: {}", name);
229                                }
230                            }
231                        }
232                    }
233                }
234                if (rowIsUsed) {
235                    if (fnNameBase.matches("F\\d+")) {
236                        fnNameString = Bundle.getMessage("FnMap_F") + " " + fnNameBase.substring(1);
237                        if (!fnDirVariant.equals("")) {
238                            fnNameString = fnNameString + Bundle.getMessage("FnMap_" + fnDirVariant);
239                        }
240                    } else if (fnNameBase.matches("S\\d+")) {
241                        fnNameString = Bundle.getMessage("FnMap_S") + " " + fnNameBase.substring(1);
242                        if (!fnDirVariant.equals("")) {
243                            fnNameString = fnNameString + Bundle.getMessage("FnMap_" + fnDirVariant);
244                        }
245                    } else {
246                        try {  // See if we have a match for whole fnNameString
247                            fnNameString = Bundle.getMessage("FnMap_" + fnNameString);
248                        } catch (java.util.MissingResourceException e) {
249                            try {  // Else see if we have a match for fnNameBase
250                                fnNameString = Bundle.getMessage("FnMap_" + fnNameBase);
251                                if (!fnDirVariant.equals("")) { // Add variant
252                                    fnNameString = fnNameString + Bundle.getMessage("FnMap_" + fnDirVariant);
253                                }
254                            } catch (java.util.MissingResourceException e1) {
255                                // No matches found
256                            }
257                        }
258                    }
259                    labelAt(row, fnNameCol, fnNameString, GridBagConstraints.LINE_START);
260                    row++;
261                }
262
263            }
264        }
265        if (log.isDebugEnabled()) {
266            log.debug("Function map complete");
267        }
268
269        // label used outputs only
270        for (int iOut = 0; iOut < numOut; iOut++) {
271            if (outIsUsed[iOut]) {
272                labelAt(outputNumRow, firstOutCol + iOut, outName[iOut]);
273                labelAt(outputLabelRow, firstOutCol + iOut, outLabel[iOut]);
274            }
275        }
276
277        // padding for the case of few outputs
278        cs.gridwidth = GridBagConstraints.REMAINDER;
279        labelAt(outputNumRow, firstOutCol + numOut, "");
280    }
281
282    void saveAt(int row, int column, JComponent j) {
283        this.saveAt(row, column, j, GridBagConstraints.CENTER);
284    }
285
286    void saveAt(int row, int column, JComponent j, int anchor) {
287        if (row < 0 || column < 0) {
288            return;
289        }
290        cs = new GridBagConstraints();
291        cs.gridy = row;
292        cs.gridx = column;
293        cs.anchor = anchor;
294        gl.setConstraints(j, cs);
295        add(j);
296    }
297
298    void labelAt(int row, int column, String name) {
299        this.labelAt(row, column, name, GridBagConstraints.CENTER);
300    }
301
302    void labelAt(int row, int column, String name, int anchor) {
303        if (row < 0 || column < 0) {
304            return;
305        }
306        JLabel t = new JLabel(" " + name + " ");
307        saveAt(row, column, t, anchor);
308    }
309
310    /**
311     * Use the "family" and "model" element from the decoder definition file to configure the
312     * number of outputs and set up any that are named instead of numbered.
313     * @param model ELement holding content to decode
314     */
315    protected void configOutputs(Element model) {
316        if (model == null) {
317            log.debug("configOutputs was given a null model");
318            return;
319        }
320        Element family = null;
321        Parent parent = model.getParent();
322        if (parent != null && parent instanceof Element) {
323            family = (Element) parent;
324        } else {
325            log.debug("configOutputs found an invalid parent family");
326            return;
327        }
328        
329        // get numOuts, numFns or leave the defaults
330        Attribute a = model.getAttribute("numOuts");
331        try {
332            if (a != null) {
333                numOut = Integer.parseInt(a.getValue());
334            }
335        } catch (NumberFormatException e) {
336            log.error("error handling decoder's numOuts value");
337        }
338        a = model.getAttribute("numFns");
339        try {
340            if (a != null) {
341                numFn = Integer.parseInt(a.getValue());
342            }
343        } catch (NumberFormatException e) {
344            log.error("error handling decoder's numFns value");
345        }
346        if (log.isDebugEnabled()) {
347            log.debug("numFns, numOuts {},{}", numFn, numOut);
348        }
349        
350        // take all "output" children
351        List<Element> elemList = new ArrayList<>();
352        addOutputElements(family.getChildren(), elemList);
353        addOutputElements(model.getChildren(), elemList);
354                
355        log.debug("output scan starting with {} elements", elemList.size());
356
357        for (int i = 0; i < elemList.size(); i++) {
358            Element e = elemList.get(i);
359            String name = e.getAttribute("name").getValue();
360            log.debug("output element name: {} value: {}", e.getAttributeValue("name"), e.getAttributeValue("label"));
361            // if this a number, or a character name?
362            try {
363                int outputNum = Integer.parseInt(name);
364                // yes, since it was converted.  All we do with
365                // these are store the label index (if it exists)
366                String at = LocaleSelector.getAttribute(e, "label");
367                if (at != null) {
368                    loadSplitLabel(outputNum - 1, at);
369                    numOut = Math.max(numOut, outputNum);
370                }
371            } catch (java.lang.NumberFormatException ex) {
372                // not a number, must be a name
373                if (numOut < maxOut) {
374                    outName[numOut] = name;
375                    String at;
376                    if ((at = LocaleSelector.getAttribute(e, "label")) != null) {
377                        outLabel[numOut] = at;
378                    } else {
379                        outLabel[numOut] = "";
380                    }
381                    numOut++;
382                }
383            }
384        }
385    }
386
387    void addOutputElements(List<Element> input, List<Element> accumulate) {
388      for (Element elem : input) {
389        if (elem.getName().equals("outputs")) {
390          log.debug(" found outputs element of size {}", elem.getChildren().size());
391          addOutputElements(elem.getChildren(), accumulate);
392        } else if (elem.getName().equals("output")) {
393          log.debug("adding output element name: {} value: {}", elem.getAttributeValue("name"), elem.getAttributeValue("label"));
394          accumulate.add(elem);
395        }
396      }
397    }
398    
399    // split and load two-line labels
400    void loadSplitLabel(int iOut, String theLabel) {
401        if (iOut < maxOut) {
402            String itemList[] = theLabel.split("\\|");
403//             log.info("theLabel=\""+theLabel+"\" itemList.length=\""+itemList.length+"\"");
404            if (theLabel.equals("|")) {
405                outName[iOut] = "";
406                outLabel[iOut] = "";
407            } else if (itemList.length == 1) {
408                outLabel[iOut] = itemList[0];
409            } else if (itemList.length > 1) {
410                outName[iOut] = itemList[0];
411                outLabel[iOut] = itemList[1];
412            }
413        }
414    }
415
416    /**
417     * clean up at end
418     */
419    public void dispose() {
420        removeAll();
421    }
422
423    // initialize logging
424    private final static Logger log = LoggerFactory.getLogger(FnMapPanel.class);
425}