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> </dd> 030 * <dt>numFns</dt> 031 * <dd>Maximum number of function rows to display.</dd> 032 * <dd> </dd> 033 * <dt>output</dt> 034 * <dd>name="n" label="yyy"</dd> 035 * <dd> - Set lower line of heading for column number "n" to 036 * "yyy".*</dd> 037 * <dd> </dd> 038 * <dd>name="n" label="xxx|yyy"</dd> 039 * <dd> - Set upper line of heading for column number "n" to "xxx" and 040 * lower line to "yyy".*</dd> 041 * <dd> </dd> 042 * <dd>name="n" label="|"</dd> 043 * <dd> - Sets both lines of heading for column number "n" to blank, 044 * causing the column to be suppressed from the table.*</dd> 045 * <dd> </dd> 046 * <dd> * The forms above increase the value of numOuts to n if 047 * numOuts < n.</dd> 048 * <dd> </dd> 049 * <dd>name="text1" label="text2"</dd> 050 * <dd> - Set upper line of heading of column numOuts+1 to "xxx" and 051 * lower line to "yyy". numOuts is then incremented.</dd> 052 * <dd> (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> </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 > 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" & "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}