001package jmri.jmrit.symbolicprog; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.awt.Dimension; 005import java.awt.GridBagConstraints; 006import java.awt.GridBagLayout; 007import java.awt.Insets; 008import java.util.*; 009import java.util.stream.IntStream; 010import javax.swing.ButtonGroup; 011import javax.swing.ImageIcon; 012import javax.swing.JButton; 013import javax.swing.JComponent; 014import javax.swing.JLabel; 015import javax.swing.JPanel; 016import javax.swing.JRadioButton; 017import javax.swing.JScrollPane; 018import javax.swing.JTextField; 019 020import jmri.jmrit.roster.RosterEntry; 021import jmri.util.CvUtil; 022import jmri.util.FileUtil; 023import jmri.util.jdom.LocaleSelector; 024import jmri.util.swing.JmriJOptionPane; 025 026import org.jdom2.*; 027 028/** 029 * Provide a graphical representation of the ESU mapping table. Each row 030 * represents a possible mapping between input conditions (function keys, etc.) 031 * and logical, physical or sound outputs. 032 * <p> 033 * Uses data from the "model" and "family" elements from the decoder definition 034 * file to configure the number of rows and set up any custom item names: 035 * <dl> 036 * <dt>extFnsESU</dt> 037 * <dd>Uses the ESU-style function map rather than the NMRA style.</dd> 038 * <dd> extFnsESU="V4" for generation 4 decoders.</dd> 039 * <dd> extFnsESU="V5" for generation 5 decoders.</dd> 040 * <dd> </dd> 041 * <dt>numOuts</dt> 042 * <dd>Number of physical outputs (information only, not used by the code).</dd> 043 * <dd> </dd> 044 * <dt>numOutsFromDefinition</dt> 045 * <dd>Number of physical outputs read from decoder definition.</dd> 046 * <dd> </dd> 047 * <dt>numFns</dt> 048 * <dd>Number of mapping rows to display.</dd> 049 * <dd><em>Only use this parameter if the specific decoder definition implements 050 * less rows than the default for that decoder generation (V4/V5), for example 051 * the LokPilot V4.</em></dd> 052 * <dd> </dd> 053 * <dt>output (in "family" or "model")</dt> 054 * <dd>name="blockNo,itemNo" label="theName"</dd> 055 * <dd> - Set name of block "blockNo", item "itemNo" to 056 * "theName".</dd> 057 * <dd> </dd> 058 * <dd>name="blockNo,itemNo" label="theName|OnChoice|OffChoice"</dd> 059 * <dd> - Set name of block "blockNo", item "itemNo" to "theName" and 060 * replace the default "On and "Off" choices for enumChoice items.</dd> 061 * <dd> </dd> 062 * <dd>name="blockNo,itemNo" label="|"</dd> 063 * <dd> - Cause item block "blockNo", item "itemNo" to be suppressed 064 * from the table.</dd> 065 * <dd> </dd> 066 * <dd>name="itemNo" label="..."</dd> 067 * <dd> - As above, but using an absolute "itemNo" (not 068 * recommended).</dd> 069 * <dd> </dd> 070 * <dd>name="theName" label="OnChoice|OffChoice"</dd> 071 * <dd> - Set name of the nth item to "theName" and replace the 072 * default "On and "Off" choices for enumChoice items, where this line is the 073 * nth "output" element of the "model" element in the decoder definition file 074 * (not recommended).</dd> 075 * </dl> 076 * <dl> 077 * <dt>Default item headings:</dt> 078 * <dd>Coded in String array itemDescESU[] of this class.</dd> 079 * <dd>Item headings can be overridden by the "output" elements documented 080 * above.</dd> 081 * </dl> 082 * <dl> 083 * <dt>Items will be suppressed if any of the following are true:</dt> 084 * <dd>No variables are found for that item.</dd> 085 * <dd>The item output name is of the form name="n" label="|".</dd> 086 * <dd>Item number is > numOuts.</dd> 087 * </dl> 088 * <dl> 089 * <dt>Variable definitions:</dt> 090 * <dd>Are of the form "ESU Function Row xx Item yy" and are created "on the 091 * fly" by this class. Many thousands of variables are needed to populate the 092 * function map. It is more efficient to create these in code than to use XML in 093 * the decoder file. <strong>DO NOT</strong> specify them in the decoder 094 * file.</dd> 095 * <dd><br> 096 * The "tooltip" & "label" attributes on a fnmapping variable are ignored. 097 * Expanded internationalized tooltips are generated in the code. 098 * </dd> 099 * </dl> 100 * 101 * @author Bob Jacobsen Copyright (C) 2001 102 * @author Dave Heap Copyright (C) 2016, 2019 103 */ 104public final class FnMapPanelESU extends JPanel { 105 106 // columns 107 int firstCol = 0; 108 int firstOut = 2; 109 110 int currentCol = firstCol; 111 112 // rows 113 static final int HINTS_ROW = 0; 114 static final int MOVE_ARROWS_TOP_ROW = 1; 115 static final int BLOCK_NAME_ROW = 1; 116 static final int FIRST_ROW = BLOCK_NAME_ROW + 2; 117 static final int ROW_LABEL_ROW = FIRST_ROW - 1; 118 119 int currentRow = FIRST_ROW; 120 121 static final int PI_CV = 16; 122 static final int SI_START_CV = 2; 123 static final int SI_CV_MODULUS = 16; 124 static final int START_CV = 257; 125 static final int CV_PAGE_MODULUS = 256; 126 static final int BIT_MODULUS = 8; 127 128 GridBagLayout gl = null; 129 GridBagConstraints cs = null; 130 VariableTableModel varModel; 131 // Titles for blocks of items. 132 String[] outBlockName; 133 // Number of items per block. 134 int[] outBlockLength; 135 int[] outBlockSiStartCv; 136 int[] outBlockSiCvModulus; 137 int[] outBlockStartCv; 138 int[] outBlockCvModulus; 139 // Number of bits per block item. 140 int[] outBlockItemBits; 141 // Starting column column of block. 142 int[] outBlockStartCol; 143 // Number of used items per block. 144 int[] outBlockUsed; 145 JTextField[][] summaryLine; 146 int maxItems; 147 // Default item labels. 148 String[] itemDescESU; 149 /** 150 * Default item labels. 151 * <dl> 152 * <dt>Two rows are available for item labels</dt> 153 * <dd>Use the '|' character to designate a row break</dd> 154 * </dl> 155 * <p> 156 * Item labels can be overridden by the "output" element of the "model" or 157 * "family" element from the decoder definition file. 158 */ 159 String[] itemLabel; 160 String[][] itemName; 161 boolean[] itemIsUsed; 162 int[][] iVarIndex; 163 164 // default values 165 String extFnsESU = "no"; 166 int numItems = -1; 167 int numFns = 1; 168 int numRows = 5; 169 int numOuts = 2; 170 // numOuts above is used for calculating correct offsets 171 // the following is actually read from the definition 172 int numOutsFromDefinition = numOuts; 173 int numStates = 2; 174 int numWheelSensors = 1; 175 int numReserved = 1; 176 int numSensors = 4; 177 int numConfig2 = 4; 178 int numLogic = 1; 179 int numSounds = 1; 180 181 // for row moves 182 int selectedRow = -1; 183 JRadioButton[] rowButton; 184 185 public FnMapPanelESU(VariableTableModel v, List<Integer> varsUsed, 186 Element model, RosterEntry rosterEntry, CvTableModel cvModel) { 187 188 // retrieve function map version 189 Attribute a = model.getAttribute("extFnsESU"); 190 try { 191 if (a != null) { 192 extFnsESU = (a.getValue()); 193 } 194 } catch (Exception ex) { 195 log.error("error handling decoder's extFnsESU value"); 196 } 197 198 switch (extFnsESU) { 199 case "V4": 200 case "yes": 201 numFns = 29; 202 numReserved = 0; 203 numOuts = 12; 204 numLogic = 16; 205 numSounds = 24; 206 numRows = 40; 207 outBlockSiStartCv = new int[]{2, 2, 2, 2}; 208 outBlockSiCvModulus = new int[]{16, 16, 16, 16}; 209 outBlockStartCv = new int[]{257, 266, 268, 270}; 210 outBlockCvModulus = new int[]{16, 16, 16, 16}; 211 212 break; 213 case "V5": 214 numFns = 32; 215 numReserved = 1; 216 numOuts = 20; 217 numLogic = 24; 218 numSounds = 32; 219 numRows = 72; 220 outBlockSiStartCv = new int[]{3, 8, 8, 8}; 221 outBlockSiCvModulus = new int[]{16, 16, 16, 16}; 222 outBlockStartCv = new int[]{257, 257, 260, 263}; 223 outBlockCvModulus = new int[]{16, 16, 16, 16}; 224 break; 225 default: 226 throw new IllegalArgumentException("Invalid extFnsESU value '" + extFnsESU + "'"); 227 } 228 229 // get relevant model attributes 230 loadModelAttributes(model); 231 232 // build outBlock*** arrays 233 outBlockLength = new int[]{numStates + numFns + numWheelSensors + numReserved + numSensors, 234 numOuts + numConfig2, numLogic, numSounds 235 }; 236 outBlockItemBits = new int[]{2, 1, 1, 1}; 237 238 log.debug( 239 "Constructor outBlockLength[0]={}, outBlockLength[1]={}, outBlockLength[2]={}, outBlockLength[3]={}", 240 outBlockLength[0], outBlockLength[1], outBlockLength[2], outBlockLength[3]); 241 242 maxItems = IntStream.of(outBlockLength).sum(); 243 if (numItems <= 0) { 244 numItems = maxItems; 245 } 246 if (numItems > maxItems) { 247 log.error("numItems={} exceeds the maximum number of items ({}) defined in the code", numItems, maxItems); 248 numItems = Math.min(numItems, maxItems); 249 } 250 251 log.debug( 252 "Constructor numFns={}, numRows={}, numOuts={}, numItems={}, maxItems={}", 253 numFns, numRows, numOuts, numItems, maxItems); 254 255 itemName = new String[maxItems][3]; 256 iVarIndex = new int[maxItems][numRows]; 257 itemIsUsed = new boolean[maxItems]; 258 itemLabel = new String[maxItems]; 259 itemDescESU = new String[maxItems]; 260 summaryLine = new JTextField[numRows][outBlockLength.length]; 261 outBlockUsed = new int[outBlockLength.length]; 262 outBlockStartCol = new int[outBlockLength.length]; 263 outBlockName = new String[outBlockLength.length]; 264 265 log.debug( 266 "ESU Function map starts"); 267 varModel = v; 268 269 setupDefaultNamesLabels(); 270 271 // configure numRows(from numFns), numItems(from numOuts) & any custom labels from decoder file 272 configOutputs(model); 273 274 // initialize the layout 275 gl = new GridBagLayout(); 276 cs = new GridBagConstraints(); 277 278 setLayout(gl); 279 280 cs.anchor = GridBagConstraints.LINE_START; 281 cs.gridwidth = GridBagConstraints.REMAINDER; 282 283 saveAt(HINTS_ROW, 284 0, new JLabel("<html><em>(For hints and instructions for using this pane, see the </em><strong>"Function Map"</strong><em> section of the </em><strong>"Read Me - IMPORTANT"</strong><em> pane.)</em><br /> </html>")); 285 cs.gridwidth = 1; 286 287 // for row moves 288 ButtonGroup group = new ButtonGroup(); 289 rowButton = new JRadioButton[numRows]; 290 291 // add row move buttons 292 addRowMoveButtons(); 293 294 cs.anchor = GridBagConstraints.LINE_END; 295 296 saveAt(ROW_LABEL_ROW, firstOut 297 - 1, new JLabel("Row")); 298 299 cs.anchor = GridBagConstraints.LINE_START; 300 301 int siCV = 0; 302 303 // loop through rows 304 for (int iRow = 0; 305 iRow < numRows; 306 iRow++) { 307 currentCol = firstCol; 308 int outBlockNum = -1; 309 int nextOutBlockStart = 0; 310 int thisOutBlockStart = 0; 311 int nextFreeBit = 0; 312 // add row shift buttons 313 { 314 rowButton[iRow] = new JRadioButton(); 315 rowButton[iRow].setActionCommand(String.valueOf(iRow)); 316 rowButton[iRow].setToolTipText(Bundle.getMessage("FnMapESURowSelect")); 317 rowButton[iRow].addActionListener(new java.awt.event.ActionListener() { 318 @Override 319 public void actionPerformed(java.awt.event.ActionEvent e) { 320 selectedRow = Integer.parseInt(e.getActionCommand()); 321 } 322 }); 323 group.add(rowButton[iRow]); 324 cs.anchor = GridBagConstraints.CENTER; 325 saveAt(currentRow, currentCol++, rowButton[iRow]); 326 } 327 cs.anchor = GridBagConstraints.LINE_END; 328 saveAt(currentRow, currentCol++, new JLabel(Integer.toString(iRow + 1))); 329 cs.anchor = GridBagConstraints.LINE_START; 330 331 // loop through outputs (columns) 332 int item = 0; 333 do { 334 JPanel blockPanel = new JPanel(); 335 GridBagLayout blockPanelLay; 336 GridBagConstraints blockPanelCs = new GridBagConstraints(); 337 338 JPanel blockItemsSelectorPanel = new JPanel(); 339 GridBagLayout bIsPlay; 340 GridBagConstraints bIsPcs = new GridBagConstraints(); 341 342 // check for block separators 343 if (item == nextOutBlockStart) { 344 outBlockNum++; 345 outBlockStartCol[outBlockNum] = item; 346 thisOutBlockStart = item; 347 nextOutBlockStart = item + outBlockLength[outBlockNum]; 348 blockItemsSelectorPanel = new JPanel(); 349 siCV = outBlockSiStartCv[outBlockNum] + (iRow / outBlockSiCvModulus[outBlockNum]); 350 nextFreeBit = 0; 351 352 bIsPlay = new GridBagLayout(); 353 bIsPcs = new GridBagConstraints(); 354 blockItemsSelectorPanel.setLayout(bIsPlay); 355 bIsPcs.gridx = 0; 356 bIsPcs.gridy = 0; 357 358 blockPanelLay = new GridBagLayout(); 359 blockPanelCs = new GridBagConstraints(); 360 blockPanel.setLayout(blockPanelLay); 361 blockPanelCs.gridx = 0; 362 blockPanelCs.gridy = 0; 363 } 364 365 // block loop 366 do { 367 // if column is not suppressed by blank headers 368 if (!itemName[item][0].equals("")) { 369 // set up the variable using the output label 370 String name = "ESU Function Row " + Integer.toString(iRow + 1) + " Item " + Integer.toString(item + 1); 371 int iCV = outBlockStartCv[outBlockNum] + (((outBlockCvModulus[outBlockNum] * iRow) + (nextFreeBit / BIT_MODULUS)) % CV_PAGE_MODULUS); 372 String thisCV = PI_CV + "." + siCV + "." + iCV; 373 int bitValue = (int) (Math.pow(2, outBlockItemBits[outBlockNum]) - 1) << (nextFreeBit % BIT_MODULUS); 374 String bitMask = "00000000" + Integer.toBinaryString(bitValue); 375 bitMask = (bitMask.substring(bitMask.length() - 8)); 376 String bitPattern = bitMask.replace("0", "X").replace("1", "V"); 377 log.debug("Block {} CV{} mask'{}' {}", outBlockNum, thisCV, bitPattern, name); 378 379 // Get Cv value from file. We need to do this to get the GUI synchronised with cvModel initially 380 int savedValue = 0; 381 CvValue cvObject = cvModel.allCvMap().get(thisCV); 382 if (cvObject != null) { 383 savedValue = cvObject.getValue(); 384 } 385 String defaultValue = Integer.toString((savedValue & bitValue) >>> (nextFreeBit % BIT_MODULUS)); 386 387 // skip function settings for nonexistant function outputs 388 if (outBlockNum == 1 && item >= thisOutBlockStart + numOutsFromDefinition && item < thisOutBlockStart + numOuts) { 389 log.debug("Skipping previous item because function output AUX {} does not exist on this decoder", item - thisOutBlockStart - 2 + 1); 390 } 391 else { 392 // create a JDOM tree with some elements to add to varModel 393 Element root = new Element("decoder-config"); 394 Document doc = new Document(root); 395 doc.setDocType(new DocType("decoder-config")); 396 397 // set up choices 398 String defChoice1 = "On"; 399 String defChoice2 = "Off"; 400 if (!itemName[item][1].equals("")) { 401 defChoice1 = itemName[item][1]; 402 } 403 if (!itemName[item][2].equals("")) { 404 defChoice2 = itemName[item][2]; 405 } 406 407 // add some elements 408 Element thisVar; 409 if (outBlockItemBits[outBlockNum] == 2) { 410 root.addContent(new Element("decoder") // the sites information here lists all relevant 411 .addContent(new Element("variables") 412 .addContent(thisVar = new Element("variable") 413 .setAttribute("CV", thisCV) 414 .setAttribute("default", defaultValue) 415 .setAttribute("mask", bitPattern) 416 .setAttribute("item", name) 417 .setAttribute("readOnly", "no") 418 .addContent(new Element("enumVal") 419 .addContent(new Element("enumChoice") 420 .setAttribute("choice", "-") 421 ) 422 .addContent(new Element("enumChoice") 423 .setAttribute("choice", defChoice1) 424 ) 425 .addContent(new Element("enumChoice") 426 .setAttribute("choice", defChoice2) 427 ) 428 ) 429 ) 430 ) // variables element 431 ); // decoder element 432 } else { 433 root.addContent(new Element("decoder") // the sites information here lists all relevant 434 .addContent(new Element("variables") 435 .addContent(thisVar = new Element("variable") 436 .setAttribute("CV", thisCV) 437 .setAttribute("default", defaultValue) 438 .setAttribute("mask", bitPattern) 439 .setAttribute("item", name) 440 .setAttribute("readOnly", "no") 441 .addContent(new Element("enumVal") 442 .addContent(new Element("enumChoice") 443 .setAttribute("choice", "Off") 444 ) 445 .addContent(new Element("enumChoice") 446 .setAttribute("choice", "On") 447 ) 448 ) 449 ) 450 ) // variables element 451 ); // decoder element 452 } 453 // end of adding content 454 455 varModel.setRow(0, thisVar); 456 457 // cleanup 458 root = null; 459 doc = null; 460 thisVar = null; 461 } 462 463 int iVar = varModel.findVarIndex(name, true); // now pick up the varModel entry we just created 464 465 // hopefully we found it! 466 if (iVar >= 0) { 467 // try to find item labels for itemLabel[item] 468 if (itemName[item][0].startsWith(Bundle.getMessage("FnMapSndSlot"))) { 469 try { 470 itemLabel[item] = rosterEntry.getSoundLabel(Integer.parseInt(itemName[item][0].substring((Bundle.getMessage("FnMapSndSlot") + " ").length()))); 471 } catch (NumberFormatException e) { 472 log.warn("Error for sound slot label \"{}\" in \"{}\"", itemName[item][0], item); 473 } 474 } else if (itemName[item][0].matches("F\\d+")) { 475 try { 476 int fn = Integer.parseInt(itemName[item][0].substring(1)); 477 if (fn <= rosterEntry.getMaxFnNumAsInt()) { 478 itemLabel[item] = rosterEntry.getFunctionLabel(fn); 479 } 480 } catch (NumberFormatException e) { 481 log.warn("Error for function label \"{}\" in \"{}\"", itemName[item][0], item); 482 } 483 } 484 if (itemLabel[item] == null) { 485 itemLabel[item] = ""; 486 } 487 488 // generate a fullItemName 489 String fullItemName = itemName[item][0]; 490 if (!itemLabel[item].equals("")) { 491 fullItemName = fullItemName + (": " + itemLabel[item]); 492 } 493 494 log.debug("Process var: {} as index {}", name, iVar); 495 varsUsed.add(iVar); 496 JComponent varComp; 497 if (outBlockItemBits[outBlockNum] == 1) { 498 varComp = (JComponent) (varModel.getRep(iVar, "checkbox")); 499 } else { 500 varComp = (JComponent) (varModel.getRep(iVar, "")); 501 } 502 VariableValue var = varModel.getVariable(iVar); 503 varComp.setToolTipText(CvUtil.addCvDescription((Bundle.getMessage("FnMapESURow") + " " + Integer.toString(iRow + 1) + ", " + fullItemName), var.getCvDescription(), var.getMask())); 504 if (cvObject == null) { 505 cvObject = cvModel.allCvMap().get(thisCV); // case of new loco 506 } 507 if (cvObject != null) { 508 cvObject.addPropertyChangeListener(new java.beans.PropertyChangeListener() { 509 private int row; 510 private int block; 511 512 @Override 513 public void propertyChange(java.beans.PropertyChangeEvent e) { 514 log.debug("Updating Summary Line for row {} block {}", row, block); 515 updateSummaryLine(row, block); 516 } 517 518 private java.beans.PropertyChangeListener init(int i, int j) { 519 row = i; 520 block = j; 521 return this; 522 } 523 }.init(iRow, outBlockNum)); 524 } else { 525 log.error("cvObject still null after attempt to allocate"); 526 } 527 528 // add line to scroll pane 529 String label = itemName[item][0]; 530 if (outBlockItemBits[outBlockNum] == 1) { 531 label = fullItemName; 532 } 533 bIsPcs.anchor = GridBagConstraints.LINE_START; 534 bIsPcs.gridx = outBlockItemBits[outBlockNum] % 2; 535 blockItemsSelectorPanel.add(new JLabel(label), bIsPcs); 536 bIsPcs.gridx = outBlockItemBits[outBlockNum] - 1; 537 blockItemsSelectorPanel.add(varComp, bIsPcs); 538 bIsPcs.gridy++; 539 540 itemIsUsed[item] = true; 541 iVarIndex[item][iRow] = iVar; 542 } else { 543 log.debug("Did not find var: {}", name); 544 } 545 } 546 nextFreeBit = nextFreeBit + outBlockItemBits[outBlockNum]; 547 548 item++; 549 } while ((item < nextOutBlockStart) && (item < numItems)); // end block loop 550 551 // display block 552 JScrollPane blockItemsScrollPane = new JScrollPane(blockItemsSelectorPanel); 553 blockItemsScrollPane.setPreferredSize(new Dimension(400, 400)); 554 555 blockPanelCs.anchor = GridBagConstraints.LINE_START; 556 blockPanelCs.gridx = 0; 557 blockPanelCs.gridy = 0; 558 blockPanelCs.insets = new Insets(0, 20, 0, 0); 559 blockPanel.add(summaryLine[iRow][outBlockNum], blockPanelCs); 560 updateSummaryLine(iRow, outBlockNum); 561 562 JButton button = new JButton("Change"); 563 button.setActionCommand(iRow + "," + outBlockNum); 564 button.addActionListener(new java.awt.event.ActionListener() { 565 @Override 566 public void actionPerformed(java.awt.event.ActionEvent e) { 567 String params[] = e.getActionCommand().split(","); 568 JmriJOptionPane.showMessageDialog( 569 blockPanel, blockItemsScrollPane, "Row " + (Integer.parseInt(params[0]) + 1) + ", " 570 + outBlockName[Integer.parseInt(params[1])], JmriJOptionPane.PLAIN_MESSAGE); 571 } 572 }); 573 blockPanelCs.anchor = GridBagConstraints.LINE_START; 574 blockPanelCs.gridx = 1; 575 blockPanelCs.gridy = 0; 576 blockPanelCs.insets = new Insets(0, 0, 0, 0); 577 blockPanel.add(button, blockPanelCs); 578 579 saveAt(currentRow, currentCol, blockPanel); 580 currentCol++; 581 582 } while (item < numItems); // end outputs (columns) loop 583 584 saveAt(currentRow++, currentCol, new JLabel(Integer.toString(iRow + 1))); 585 } // end row loop 586 587 saveAt(ROW_LABEL_ROW, currentCol, 588 new JLabel(Bundle.getMessage("FnMapESURow"))); 589 // tally used columns 590 int currentBlock = -1; 591 int blockStart = 0; 592 for (int item = 0; 593 item < maxItems; 594 item++) { 595 if (item == blockStart) { 596 currentBlock++; 597 blockStart = blockStart + outBlockLength[currentBlock]; 598 outBlockUsed[currentBlock] = 0; 599 } 600 if (itemIsUsed[item]) { 601 outBlockUsed[currentBlock]++; 602 } 603 } 604 605 // Create formatted block labels 606 for (int iBlock = 0; 607 iBlock < outBlockLength.length; 608 iBlock++) { 609 if (outBlockUsed[iBlock] > 0) { 610 StringBuilder label = new StringBuilder("<html><strong>" + outBlockName[iBlock]); 611 try { 612 String s = Bundle.getMessage("FnMapESUBlockDesc_" + (iBlock + 1)); 613 label.append("</strong><br>"); 614 label.append(s); 615 label.append("</html>"); 616 } catch (MissingResourceException e) { 617 label.append("</strong></html>"); 618 } 619 JLabel lx = new JLabel(label.toString()); 620 GridBagConstraints csx = new GridBagConstraints(); 621 csx.gridy = BLOCK_NAME_ROW; 622 csx.gridx = firstOut + iBlock; 623 csx.insets = new Insets(0, 40, 0, 0); 624 csx.gridwidth = 1; 625 csx.anchor = GridBagConstraints.LINE_START; 626 gl.setConstraints(lx, csx); 627 add(lx); 628 } 629 } 630 631 log.debug( 632 "Function map complete"); 633 } 634 635 /** 636 * Set up the default names and labels. 637 */ 638 void setupDefaultNamesLabels() { 639 // get block names 640 for (int i = 0; 641 i < outBlockName.length; 642 i++) { 643 outBlockName[i] = Bundle.getMessage("FnMapESUBlockName_" + (i + 1)); 644 } 645 646 // make item names 647 int item = 0; 648 itemDescESU[item++] = Bundle.getMessage("FnMap_STATE") + "|" + Bundle.getMessage("FnMap_DRIVE") + "|" + Bundle.getMessage("FnMap_STOP"); 649 itemDescESU[item++] = Bundle.getMessage("FnMap_DIR") + "|" + Bundle.getMessage("FnMap_FWD") + "|" + Bundle.getMessage("FnMap_REV"); 650 for (int i = 0; i < numFns; i++) { 651 itemDescESU[item++] = "F" + i; 652 } 653 itemDescESU[item++] = Bundle.getMessage("FnMap_WS"); 654 if (extFnsESU.equalsIgnoreCase("V5")) { 655 itemDescESU[item++] = Bundle.getMessage("FnMap_RS"); 656 } 657 for (int i = 1; i <= numSensors; i++) { 658 itemDescESU[item++] = Bundle.getMessage("FnMap_S") + " " + i; 659 } 660 itemDescESU[item++] = Bundle.getMessage("FnMap_HL") + "[1]"; 661 itemDescESU[item++] = Bundle.getMessage("FnMap_RL") + "[1]"; 662 for (int i = 1; i <= numOuts - 2; i++) { 663 itemDescESU[item++] = Bundle.getMessage("FnMap_A") + " " + i + (i <= 2 ? "[1]" : ""); 664 } 665 itemDescESU[item++] = Bundle.getMessage("FnMap_HL") + "[2]"; 666 itemDescESU[item++] = Bundle.getMessage("FnMap_RL") + "[2]"; 667 for (int i = 1; i <= 2; i++) { 668 itemDescESU[item++] = Bundle.getMessage("FnMap_A") + " " + i + "[2]"; 669 } 670 for (int i = 1; i <= outBlockLength[2]; i++) { 671 itemDescESU[item] = "(output 3," + Integer.toString(i) + ")"; // dummy, use output labels 672 item++; 673 } 674 for (int i = 1; i <= outBlockLength[3]; i++) { 675 itemDescESU[item++] = Bundle.getMessage("FnMapSndSlot") + " " + i; 676 } 677 678 // set up default names and labels 679 for (int itemNum = 0; 680 itemNum < maxItems; 681 itemNum++) { 682 itemLabel[itemNum] = ""; 683 itemName[itemNum][0] = ""; 684 itemName[itemNum][1] = ""; 685 itemName[itemNum][2] = ""; 686 itemIsUsed[itemNum] = false; 687 for (int iRow = 0; iRow < numRows; iRow++) { 688 iVarIndex[itemNum][iRow] = 0; 689 for (int outBlockNum = 0; outBlockNum < outBlockLength.length; outBlockNum++) { 690 summaryLine[iRow][outBlockNum] = new JTextField(20); 691 summaryLine[iRow][outBlockNum].setHorizontalAlignment(JTextField.LEFT); 692 summaryLine[iRow][outBlockNum].setEditable(false); 693 } 694 } 695 } 696 } 697 698 /** 699 * Updates all summary lines, including setting appropriate states. 700 */ 701 void updateAllSummaryLines() { 702 for (int row = 0; row < numRows; row++) { 703 for (int block = 0; block < outBlockLength.length; block++) { 704 updateSummaryLine(row, block); 705 } 706 } 707 } 708 709 /** 710 * Updates a summary line at the specified location, including setting 711 * appropriate state. 712 * 713 * @param row the row to update 714 * @param block the block to update 715 */ 716 void updateSummaryLine(int row, int block) { 717 StringBuilder retString = new StringBuilder(""); 718 AbstractValue.ValueState retState = AbstractValue.ValueState.SAME; 719 720 for (int item = outBlockStartCol[block]; item < (outBlockStartCol[block] + outBlockLength[block]); item++) { 721 if (itemIsUsed[item]) { 722 int value = Integer.parseInt(varModel.getValString(iVarIndex[item][row])); 723 var state = varModel.getState(iVarIndex[item][row]); 724 if ((item == outBlockStartCol[block]) || (priorityValue(state) > priorityValue(retState))) { 725 retState = state; 726 } 727 if (value > 0) { 728 if (outBlockItemBits[block] == 1) { 729 if (itemLabel[item].equals("")) { 730 retString.append(",").append(itemName[item][0]); 731 } else { 732 retString.append(",").append(itemLabel[item]); 733 } 734 } else if (outBlockItemBits[block] == 2) { 735 if (value > 2) { 736 retString.append(",").append("reserved value ").append(value); 737 } else if (itemName[item][value].equals("")) { 738 if (value == 1) { 739 retString.append(",").append(itemName[item][0]); 740 } else { 741 retString.append(",not ").append(itemName[item][0]); 742 } 743 } else { 744 retString.append(",").append(itemName[item][value]); 745 } 746 } 747 } 748 } 749 } 750 751 if (retString.length() == 0) { 752 retString.append("-"); 753 } else if (retString.charAt(0) == ',') { 754 retString.deleteCharAt(0); 755 } 756 757 summaryLine[row][block].setBackground(retState.getColor()); 758 summaryLine[row][block].setText(retString.toString()); 759 summaryLine[row][block].setToolTipText(retString.toString()); 760 } 761 762 /** 763 * Assigns a priority value to a specified state. 764 * 765 * @param state the state 766 * @return the assigned priority value 767 */ 768 @SuppressFBWarnings({"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}) 769 int priorityValue(AbstractValue.ValueState state) { 770 int value = 0; 771 switch (state) { 772 case UNKNOWN: 773 value++; 774 //$FALL-THROUGH$ 775 case DIFFERENT: 776 value++; 777 //$FALL-THROUGH$ 778 case EDITED: 779 value++; 780 //$FALL-THROUGH$ 781 case FROMFILE: 782 value++; 783 //$FALL-THROUGH$ 784 default: 785 return value; 786 } 787 } 788 789 /** 790 * Saves an item at the specified row and column. 791 * 792 * @param row the row 793 * @param column the column 794 * @param j the item 795 */ 796 void saveAt(int row, int column, JComponent j) { 797 if (row < 0 || column < 0) { 798 return; 799 } 800 cs.gridy = row; 801 cs.gridx = column; 802 gl.setConstraints(j, cs); 803 add(j); 804 } 805 806 /** 807 * Moves rows up or down. 808 * <p> 809 * Row moves are for convenience purposes only. Decoder functioning is 810 * unaffected by row position in mapping table. 811 * 812 * @param increment number of rows to move by 813 */ 814 void moveRow(int increment) { 815 if (selectedRow == -1) { 816 return; 817 } 818 if ((selectedRow + increment) < 0) { 819 return; 820 } 821 if ((selectedRow + increment) >= numRows) { 822 return; 823 } 824 int newRow = selectedRow + increment; 825 // now to swap the data 826 for (int item = 0; item < maxItems; item++) { 827 if (itemIsUsed[item]) { 828 int selectedRowValue = Integer.parseInt(varModel.getValString(iVarIndex[item][selectedRow])); 829 int newRowValue = Integer.parseInt(varModel.getValString(iVarIndex[item][newRow])); 830 varModel.setIntValue(iVarIndex[item][selectedRow], newRowValue); 831 varModel.setIntValue(iVarIndex[item][newRow], selectedRowValue); 832 } 833 } 834 835 selectedRow = newRow; 836 rowButton[selectedRow].setSelected(true); 837 838 } 839 840 /** 841 * Adds the Row Move buttons at top and bottom. 842 */ 843 void addRowMoveButtons() { 844 { 845 JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowUp-16.png"))); 846 button.setActionCommand(String.valueOf(-1)); 847 button.setToolTipText(Bundle.getMessage("FnMapESUMoveUp")); 848// button.setBorderPainted(false); 849 button.addActionListener(new java.awt.event.ActionListener() { 850 @Override 851 public void actionPerformed(java.awt.event.ActionEvent e) { 852 moveRow(Integer.parseInt(e.getActionCommand())); 853 } 854 }); 855 cs.anchor = GridBagConstraints.CENTER; 856 saveAt(MOVE_ARROWS_TOP_ROW, 0, button); 857 } 858 { 859 JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowDown-16.png"))); 860 button.setActionCommand(String.valueOf(1)); 861 button.setToolTipText(Bundle.getMessage("FnMapESUMoveDown")); 862// button.setBorderPainted(false); 863 button.addActionListener(new java.awt.event.ActionListener() { 864 @Override 865 public void actionPerformed(java.awt.event.ActionEvent e) { 866 moveRow(Integer.parseInt(e.getActionCommand())); 867 } 868 }); 869 cs.anchor = GridBagConstraints.CENTER; 870 saveAt(MOVE_ARROWS_TOP_ROW, 1, button); 871 } 872 { 873 JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowUp-16.png"))); 874 button.setActionCommand(String.valueOf(-1)); 875 button.setToolTipText(Bundle.getMessage("FnMapESUMoveUp")); 876// button.setBorderPainted(false); 877 button.addActionListener(new java.awt.event.ActionListener() { 878 @Override 879 public void actionPerformed(java.awt.event.ActionEvent e) { 880 moveRow(Integer.parseInt(e.getActionCommand())); 881 } 882 }); 883 cs.anchor = GridBagConstraints.CENTER; 884 saveAt(FIRST_ROW + numRows, 0, button); 885 } 886 { 887 JButton button = new JButton(new ImageIcon(FileUtil.findURL("resources/icons/misc/ArrowDown-16.png"))); 888 button.setActionCommand(String.valueOf(1)); 889 button.setToolTipText(Bundle.getMessage("FnMapESUMoveDown")); 890// button.setBorderPainted(false); 891 button.addActionListener(new java.awt.event.ActionListener() { 892 @Override 893 public void actionPerformed(java.awt.event.ActionEvent e) { 894 moveRow(Integer.parseInt(e.getActionCommand())); 895 } 896 }); 897 cs.anchor = GridBagConstraints.CENTER; 898 saveAt(FIRST_ROW + numRows, 1, button); 899 } 900 } 901 902 /** 903 * Use the "model" and "family" elements from the decoder definition file to 904 * configure the number of rows and columns and set up any custom column 905 * names. 906 * 907 * @param model the "model" element from the decoder definition file 908 */ 909 void configOutputs(Element model) { 910 if (model == null) { 911 log.debug("configOutputs was given a null model"); 912 return; 913 } 914 Element family; 915 Parent parent = model.getParent(); 916 if (parent != null && parent instanceof Element) { 917 family = (Element) parent; 918 } else { 919 log.debug("configOutputs found an invalid parent family"); 920 return; 921 } 922 923 // add ESU default split labels before reading custom ones 924 for (int item = 0; item < maxItems; item++) { 925 loadSplitLabel(item, itemDescESU[item]); 926 } 927 928 // take all "output" children 929 List<Element> elemList = new ArrayList<>(); 930 addOutputElements(family.getChildren(), elemList); 931 addOutputElements(model.getChildren(), elemList); 932 933 log.debug("output scan starting with {} elements", elemList.size()); 934 935 for (int i = 0; i < elemList.size(); i++) { 936 Element e = elemList.get(i); 937 String name = e.getAttribute("name").getValue(); 938 log.debug("output element name: {} value: {}", e.getAttributeValue("name"), e.getAttributeValue("label")); 939 // does this element have a label? 940 String label = LocaleSelector.getAttribute(e, "label"); 941 if (label != null) { 942 parseLoadLabel(i, name, label); 943 } 944 } 945 } 946 947 /** 948 * Use the "model" element from the decoder definition file to fetch 949 * attributes relevant to building this function map. 950 * 951 * @param model the "model" element from the decoder definition file 952 */ 953 void loadModelAttributes(Element model) { 954 955 Attribute a; 956 957 if (model == null) { 958 log.debug("loadModelAttributes was given a null model"); 959 return; 960 } 961 962 // get numOuts, numFns or leave the defaults 963 a = model.getAttribute("numFns"); 964 try { 965 if (a != null) { 966 numRows = Integer.parseInt(a.getValue()); 967 } 968 } catch (NumberFormatException e) { 969 log.error("error handling decoder's numFns value"); 970 } 971 972 a = model.getAttribute("numOuts"); 973 try { 974 if (a != null) { 975 numOutsFromDefinition = Integer.parseInt(a.getValue()); 976 } 977 } catch (NumberFormatException e) { 978 log.error("error handling decoder's numOuts value"); 979 } 980 981 log.debug("loadModelAttributes numFns={}, numRows={}, numOuts={}, numOutsFromDefinition={}, numItems={}", 982 numFns, numRows, numOuts, numOutsFromDefinition, numItems); 983 } 984 985 /** 986 * Adds a list of "output" or "outputs" elements to an existing list. 987 * 988 * @param input the list to add from 989 * @param accumulate the list to add to 990 */ 991 void addOutputElements(List<Element> input, List<Element> accumulate) { 992 for (Element elem : input) { 993 if (elem.getName().equals("outputs")) { 994 log.debug(" found outputs element of size {}", elem.getChildren().size()); 995 addOutputElements(elem.getChildren(), accumulate); 996 } else if (elem.getName().equals("output")) { 997 log.debug("adding output element name: {} value: {}", elem.getAttributeValue("name"), elem.getAttributeValue("label")); 998 accumulate.add(elem); 999 } 1000 } 1001 } 1002 1003 /** 1004 * Loads labels as per documentation at {@link FnMapPanelESU}. 1005 * 1006 * @param item the item number to load 1007 * @param name the "name" attribute from the "output" element 1008 * @param label the "label" attribute from the "output" element 1009 */ 1010 void parseLoadLabel(int item, String name, String label) { 1011 // is the name a number? 1012 try { 1013 int outputNum = Integer.parseInt(name); 1014 // yes, since it was converted 1015 // store the label at the appropriate index 1016 log.debug("Output name='{}', label='{}' has an item number.", name, label); 1017 loadSplitLabel(outputNum - 1, label); 1018 return; 1019 } catch (java.lang.NumberFormatException ex) { 1020 log.debug("Output name='{}', label='{}' is not a simple item number.", name, label); 1021 } 1022 1023 // see if it is a "block,item" construct 1024 String[] itemList = name.split(","); 1025 if (itemList.length == 2) { 1026 try { 1027 int blockNum = Integer.parseInt(itemList[0]); 1028 int blockItemNum = Integer.parseInt(itemList[1]); 1029 int itemNum = Arrays.stream(outBlockLength, 0, blockNum - 1).sum() + blockItemNum - 1; 1030 log.debug("Output name='{}', label='{}', blockNum='{}', blockItemNum='{}', itemNum='{}'.", 1031 name, label, blockNum, blockItemNum, itemNum); 1032 loadSplitLabel(itemNum, label); 1033 return; 1034 } catch (java.lang.NumberFormatException ex1) { 1035 log.debug("Output name='{}', label='{}' is not a \"block,item\" construct.", 1036 name, label); 1037 } 1038 } 1039 1040 // must be a name 1041 if (item < maxItems) { 1042 itemName[item][0] = name; 1043 itemName[item][1] = ""; 1044 itemName[item][2] = ""; 1045 log.debug("Output name='{}', label='{}' has no item number.", name, label); 1046 loadSplitLabel(item, name + "|" + label); 1047 } 1048 } 1049 1050 /** 1051 * Splits a label as per documentation at {@link FnMapPanelESU}. 1052 * 1053 * @param item the item number to load 1054 * @param theLabel the label attribute from the "output" element 1055 */ 1056 void loadSplitLabel(int item, String theLabel) { 1057 if (item < maxItems) { 1058 String[] itemList = theLabel.split("\\|"); 1059 if (theLabel.equals("|")) { 1060 itemName[item][0] = ""; 1061 itemName[item][1] = ""; 1062 itemName[item][2] = ""; 1063 } else if (itemList.length == 1) { 1064 itemName[item][0] = itemList[0]; 1065 itemName[item][1] = ""; 1066 } else if (itemList.length == 2) { 1067 itemName[item][0] = itemList[0]; 1068 itemName[item][1] = itemList[1]; 1069 itemName[item][2] = ""; 1070 } else if (itemList.length > 2) { 1071 itemName[item][0] = itemList[0]; 1072 itemName[item][1] = itemList[1]; 1073 itemName[item][2] = itemList[2]; 1074 } 1075 } 1076 } 1077 1078 /** 1079 * Clean up at end. 1080 */ 1081 public void dispose() { 1082 varModel = null; // kills GC cycles during test 1083 for (int i = 0; i < rowButton.length; i++) { 1084 rowButton[i] = null; 1085 } 1086 for (JTextField[] summaryLine1 : summaryLine) { 1087 for (int j = 0; j < summaryLine1.length; j++) { 1088 summaryLine1[j] = null; 1089 } 1090 } 1091 removeAll(); // JPanel call 1092 } 1093 1094 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FnMapPanelESU.class); 1095 1096}