001package jmri.jmrit.roster; 002 003import com.fasterxml.jackson.databind.util.StdDateFormat; 004 005import java.awt.HeadlessException; 006import java.awt.Image; 007import java.io.File; 008import java.io.FileNotFoundException; 009import java.io.IOException; 010import java.io.Writer; 011import java.text.*; 012import java.util.*; 013 014import javax.annotation.CheckForNull; 015import javax.annotation.Nonnull; 016import javax.swing.ImageIcon; 017import javax.swing.JLabel; 018 019import jmri.BasicRosterEntry; 020import jmri.DccLocoAddress; 021import jmri.InstanceManager; 022import jmri.LocoAddress; 023import jmri.beans.ArbitraryBean; 024import jmri.jmrit.roster.rostergroup.RosterGroup; 025import jmri.jmrit.symbolicprog.CvTableModel; 026import jmri.jmrit.symbolicprog.VariableTableModel; 027import jmri.util.FileUtil; 028import jmri.util.StringUtil; 029import jmri.util.davidflanagan.HardcopyWriter; 030import jmri.util.jdom.LocaleSelector; 031import jmri.util.swing.JmriJOptionPane; 032 033import org.jdom2.Attribute; 034import org.jdom2.Element; 035import org.jdom2.JDOMException; 036 037/** 038 * RosterEntry represents a single element in a locomotive roster, including 039 * information on how to locate it from decoder information. 040 * <p> 041 * The RosterEntry is the central place to find information about a locomotive's 042 * configuration, including CV and "programming variable" information. 043 * RosterEntry handles persistence through the LocoFile class. Creating a 044 * RosterEntry does not necessarily read the corresponding file (which might not 045 * even exist), please see readFile(), writeFile() member functions. 046 * <p> 047 * All the data attributes have a content, not null. FileName, however, is 048 * special. A null value for it indicates that no physical file is (yet) 049 * associated with this entry. 050 * <p> 051 * When the filePath attribute is non-null, the user has decided to organize the 052 * roster into directories. 053 * <p> 054 * Each entry can have one or more "Attributes" associated with it. These are 055 * (key, value) pairs. The key has to be unique, and currently both objects have 056 * to be Strings. 057 * <p> 058 * All properties, including the "Attributes", are bound. 059 * 060 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2004, 2005, 2009 061 * @author Dennis Miller Copyright 2004 062 * @author Egbert Broerse Copyright (C) 2018 063 * @author Dave Heap Copyright (C) 2019 064 * @see jmri.jmrit.roster.LocoFile 065 */ 066public class RosterEntry extends ArbitraryBean implements RosterObject, BasicRosterEntry { 067 068 // identifiers for property change events and some XML elements 069 public static final String ID = "id"; // NOI18N 070 public static final String FILENAME = "filename"; // NOI18N 071 public static final String ROADNAME = "roadname"; // NOI18N 072 public static final String MFG = "mfg"; // NOI18N 073 public static final String MODEL = "model"; // NOI18N 074 public static final String OWNER = "owner"; // NOI18N 075 public static final String DCC_ADDRESS = "dccaddress"; // NOI18N 076 public static final String LONG_ADDRESS = "longaddress"; // NOI18N 077 public static final String PROTOCOL = "protocol"; // NOI18N 078 public static final String COMMENT = "comment"; // NOI18N 079 public static final String DECODER_MODEL = "decodermodel"; // NOI18N 080 public static final String DECODER_DEVELOPERID = "developerID"; // NOI18N 081 public static final String DECODER_MANUFACTURERID = "manufacturerID"; // NOI18N 082 public static final String DECODER_PRODUCTID = "productID"; // NOI18N 083 public static final String PROGRAMMING = "programming"; // NOI18N 084 public static final String DECODER_FAMILY = "decoderfamily"; // NOI18N 085 public static final String DECODER_MODES = "decoderModes"; // NOI18N 086 public static final String DECODER_COMMENT = "decodercomment"; // NOI18N 087 public static final String DECODER_MAXFNNUM = "decodermaxFnNum"; // NOI18N 088 public static final String DEFAULT_MAXFNNUM = "28"; // NOI18N 089 public static final String IMAGE_FILE_PATH = "imagefilepath"; // NOI18N 090 public static final String ICON_FILE_PATH = "iconfilepath"; // NOI18N 091 public static final String URL = "url"; // NOI18N 092 public static final String DATE_UPDATED = "dateupdated"; // NOI18N 093 public static final String FUNCTION_IMAGE = "functionImage"; // NOI18N 094 public static final String FUNCTION_LABEL = "functionlabel"; // NOI18N 095 public static final String FUNCTION_LOCKABLE = "functionLockable"; // NOI18N 096 public static final String FUNCTION_SELECTED_IMAGE = "functionSelectedImage"; // NOI18N 097 public static final String ATTRIBUTE_UPDATED = "attributeUpdated:"; // NOI18N 098 public static final String ATTRIBUTE_DELETED = "attributeDeleted"; // NOI18N 099 public static final String MAX_SPEED = "maxSpeed"; // NOI18N 100 public static final String SHUNTING_FUNCTION = "IsShuntingOn"; // NOI18N 101 public static final String SPEED_PROFILE = "speedprofile"; // NOI18N 102 public static final String SOUND_LABEL = "soundlabel"; // NOI18N 103 public static final String ATTRIBUTE_OPERATING_DURATION = "OperatingDuration"; // NOI18N 104 public static final String ATTRIBUTE_LAST_OPERATED = "LastOperated"; // NOI18N 105 106 // members to remember all the info 107 protected String _fileName = null; 108 109 protected String _id = ""; 110 protected String _roadName = ""; 111 protected String _roadNumber = ""; 112 protected String _mfg = ""; 113 protected String _owner = ""; 114 protected String _model = ""; 115 protected String _dccAddress = "3"; 116 protected LocoAddress.Protocol _protocol = LocoAddress.Protocol.DCC_SHORT; 117 protected String _comment = ""; 118 protected String _decoderModel = ""; 119 protected String _decoderFamily = ""; 120 protected String _decoderComment = ""; 121 protected String _maxFnNum = DEFAULT_MAXFNNUM; 122 protected String _dateUpdated = ""; 123 protected Date dateModified = null; 124 protected int _maxSpeedPCT = 100; 125 protected String _developerID = ""; 126 protected String _manufacturerID = ""; 127 protected String _productID = ""; 128 protected String _programmingModes = ""; 129 130 /** 131 * Get the highest valid Fn key number for this roster entry. 132 * <dl> 133 * <dt>The default value (28) can be overridden by a "maxFnNum" attribute in 134 * the "model" element of a decoder definition file</dt> 135 * <dd><ul> 136 * <li>A European standard (RCN-212) extends NMRA S9.2.1 up to F68.</li> 137 * <li>ESU LokSound 5 already uses up to F31.</li> 138 * </ul></dd> 139 * </dl> 140 * 141 * @return the highest function number (Fn) supported by this roster entry. 142 * 143 * @see "http://normen.railcommunity.de/RCN-212.pdf" 144 */ 145 public int getMaxFnNumAsInt() { 146 return Integer.parseInt(getMaxFnNum()); 147 } 148 149 protected Map<Integer, String> functionLabels; 150 protected Map<Integer, String> soundLabels; 151 protected Map<Integer, String> functionSelectedImages; 152 protected Map<Integer, String> functionImages; 153 protected Map<Integer, Boolean> functionLockables; 154 protected Map<Integer, Boolean> functionVisibles; 155 protected String _isShuntingOn = ""; 156 157 protected final TreeMap<String, String> attributePairs = new TreeMap<>(); 158 159 protected String _imageFilePath = null; 160 protected String _iconFilePath = null; 161 protected String _URL = ""; 162 163 protected RosterSpeedProfile _sp = null; 164 165 /** 166 * Construct a blank object. 167 */ 168 public RosterEntry() { 169 functionLabels = Collections.synchronizedMap(new HashMap<>()); 170 soundLabels = Collections.synchronizedMap(new HashMap<>()); 171 functionSelectedImages = Collections.synchronizedMap(new HashMap<>()); 172 functionImages = Collections.synchronizedMap(new HashMap<>()); 173 functionLockables = Collections.synchronizedMap(new HashMap<>()); 174 functionVisibles = Collections.synchronizedMap(new HashMap<>()); 175 } 176 177 /** 178 * Constructor based on a given file name. 179 * 180 * @param fileName xml file name for the user's Roster entry 181 */ 182 public RosterEntry(String fileName) { 183 this(); 184 _fileName = fileName; 185 } 186 187 /** 188 * Constructor based on a given RosterEntry object and name/ID. 189 * 190 * @param pEntry RosterEntry object 191 * @param pID unique name/ID for the roster entry 192 */ 193 public RosterEntry(RosterEntry pEntry, String pID) { 194 this(); 195 // The ID is different for this element 196 _id = pID; 197 198 // The filename is not set here, rather later 199 _fileName = null; 200 201 // All other items are copied 202 _roadName = pEntry._roadName; 203 _roadNumber = pEntry._roadNumber; 204 _mfg = pEntry._mfg; 205 _model = pEntry._model; 206 _dccAddress = pEntry._dccAddress; 207 _protocol = pEntry._protocol; 208 _comment = pEntry._comment; 209 _decoderModel = pEntry._decoderModel; 210 _decoderFamily = pEntry._decoderFamily; 211 _developerID = pEntry._developerID; 212 _manufacturerID = pEntry._manufacturerID; 213 _productID = pEntry._productID; 214 _programmingModes = pEntry._programmingModes; 215 _decoderComment = pEntry._decoderComment; 216 _owner = pEntry._owner; 217 _imageFilePath = pEntry._imageFilePath; 218 _iconFilePath = pEntry._iconFilePath; 219 _URL = pEntry._URL; 220 _maxSpeedPCT = pEntry._maxSpeedPCT; 221 _isShuntingOn = pEntry._isShuntingOn; 222 223 if (pEntry.functionLabels != null) { 224 pEntry.functionLabels.forEach((key, value) -> { 225 if (value != null) { 226 functionLabels.put(key, value); 227 } 228 }); 229 } 230 if (pEntry.soundLabels != null) { 231 pEntry.soundLabels.forEach((key, value) -> { 232 if (value != null) { 233 soundLabels.put(key, value); 234 } 235 }); 236 } 237 if (pEntry.functionSelectedImages != null) { 238 pEntry.functionSelectedImages.forEach((key, value) -> { 239 if (value != null) { 240 functionSelectedImages.put(key, value); 241 } 242 }); 243 } 244 if (pEntry.functionImages != null) { 245 pEntry.functionImages.forEach((key, value) -> { 246 if (value != null) { 247 functionImages.put(key, value); 248 } 249 }); 250 } 251 if (pEntry.functionLockables != null) { 252 pEntry.functionLockables.forEach((key, value) -> { 253 if (value != null) { 254 functionLockables.put(key, value); 255 } 256 }); 257 } 258 if (pEntry.functionVisibles != null) { 259 pEntry.functionVisibles.forEach((key, value) -> { 260 if (value != null) { 261 functionVisibles.put(key, value); 262 } 263 }); 264 } 265 } 266 267 /** 268 * Set the roster ID for this roster entry. 269 * 270 * @param s new ID 271 */ 272 public void setId(String s) { 273 String oldID = _id; 274 _id = s; 275 if (oldID == null || !oldID.equals(s)) { 276 firePropertyChange(RosterEntry.ID, oldID, s); 277 } 278 } 279 280 @Override 281 public String getId() { 282 return _id; 283 } 284 285 /** 286 * Set the file name for this roster entry. 287 * 288 * @param s the new roster entry file name 289 */ 290 public void setFileName(String s) { 291 String oldName = _fileName; 292 _fileName = s; 293 firePropertyChange(RosterEntry.FILENAME, oldName, s); 294 } 295 296 public String getFileName() { 297 return _fileName; 298 } 299 300 public String getPathName() { 301 return Roster.getDefault().getRosterFilesLocation() + _fileName; 302 } 303 304 /** 305 * Ensure the entry has a valid filename. 306 * <p> 307 * If none exists, create one based on the ID string. Does _not_ enforce any 308 * particular naming; you have to check separately for {@literal "<none>"} 309 * or whatever your convention is for indicating an invalid name. Does 310 * replace the space, period, colon, slash and backslash characters so that 311 * the filename will be generally usable. 312 */ 313 public void ensureFilenameExists() { 314 // if there isn't a filename, store using the id 315 if (getFileName() == null || getFileName().isEmpty()) { 316 317 String newFilename = Roster.makeValidFilename(getId()); 318 319 // we don't want to overwrite a file that exists, whether or not 320 // it's in the roster 321 File testFile = new File(Roster.getDefault().getRosterFilesLocation() + newFilename); 322 int count = 0; 323 String oldFilename = newFilename; 324 while (testFile.exists()) { 325 // oops - change filename and try again 326 newFilename = oldFilename.substring(0, oldFilename.length() - 4) + count + ".xml"; 327 count++; 328 log.debug("try to use {} as filename instead of {}", newFilename, oldFilename); 329 testFile = new File(Roster.getDefault().getRosterFilesLocation() + newFilename); 330 } 331 setFileName(newFilename); 332 log.debug("new filename: {}", getFileName()); 333 } 334 } 335 336 public void setRoadName(String s) { 337 String old = _roadName; 338 _roadName = s; 339 firePropertyChange(RosterEntry.ROADNAME, old, s); 340 } 341 342 public String getRoadName() { 343 return _roadName; 344 } 345 346 public void setRoadNumber(String s) { 347 String old = _roadNumber; 348 _roadNumber = s; 349 firePropertyChange(RosterEntry.ROADNAME, old, s); 350 } 351 352 public String getRoadNumber() { 353 return _roadNumber; 354 } 355 356 public void setMfg(String s) { 357 String old = _mfg; 358 _mfg = s; 359 firePropertyChange(RosterEntry.MFG, old, s); 360 } 361 362 public String getMfg() { 363 return _mfg; 364 } 365 366 public void setModel(String s) { 367 String old = _model; 368 _model = s; 369 firePropertyChange(RosterEntry.MODEL, old, s); 370 } 371 372 public String getModel() { 373 return _model; 374 } 375 376 public void setOwner(String s) { 377 String old = _owner; 378 _owner = s; 379 firePropertyChange(RosterEntry.OWNER, old, s); 380 } 381 382 public String getOwner() { 383 if (_owner.isEmpty()) { 384 RosterConfigManager manager = InstanceManager.getNullableDefault(RosterConfigManager.class); 385 if (manager != null) { 386 _owner = manager.getDefaultOwner(); 387 } 388 } 389 return _owner; 390 } 391 392 public void setDccAddress(String s) { 393 String old = _dccAddress; 394 _dccAddress = s; 395 firePropertyChange(RosterEntry.DCC_ADDRESS, old, s); 396 } 397 398 @Override 399 public String getDccAddress() { 400 return _dccAddress; 401 } 402 403 public void setLongAddress(boolean b) { 404 boolean old = false; 405 if (_protocol == LocoAddress.Protocol.DCC_LONG) { 406 old = true; 407 } 408 if (b) { 409 _protocol = LocoAddress.Protocol.DCC_LONG; 410 } else { 411 _protocol = LocoAddress.Protocol.DCC_SHORT; 412 } 413 firePropertyChange(RosterEntry.LONG_ADDRESS, old, b); 414 } 415 416 public RosterSpeedProfile getSpeedProfile() { 417 return _sp; 418 } 419 420 public void setSpeedProfile(RosterSpeedProfile sp) { 421 if (sp.getRosterEntry() != this) { 422 log.error("Attempting to set a speed profile against the wrong roster entry"); 423 return; 424 } 425 RosterSpeedProfile old = this._sp; 426 _sp = sp; 427 this.firePropertyChange(RosterEntry.SPEED_PROFILE, old, this._sp); 428 } 429 430 @Override 431 public boolean isLongAddress() { 432 return _protocol == LocoAddress.Protocol.DCC_LONG; 433 } 434 435 public void setProtocol(LocoAddress.Protocol protocol) { 436 LocoAddress.Protocol old = _protocol; 437 _protocol = protocol; 438 firePropertyChange(RosterEntry.PROTOCOL, old, _protocol); 439 } 440 441 public LocoAddress.Protocol getProtocol() { 442 return _protocol; 443 } 444 445 public String getProtocolAsString() { 446 return _protocol.getPeopleName(); 447 } 448 449 public void setComment(String s) { 450 String old = _comment; 451 _comment = s; 452 firePropertyChange(RosterEntry.COMMENT, old, s); 453 } 454 455 public String getComment() { 456 return _comment; 457 } 458 459 public void setDecoderModel(String s) { 460 String old = _decoderModel; 461 _decoderModel = s; 462 firePropertyChange(RosterEntry.DECODER_MODEL, old, s); 463 } 464 465 public String getDecoderModel() { 466 return _decoderModel; 467 } 468 469 public void setDeveloperID(String s) { 470 String old = _developerID; 471 _developerID = s; 472 firePropertyChange(DECODER_DEVELOPERID, old, s); 473 } 474 475 public String getDeveloperID() { 476 return _developerID; 477 } 478 479 public void setManufacturerID(String s) { 480 String old = _manufacturerID; 481 _manufacturerID = s; 482 firePropertyChange(DECODER_MANUFACTURERID, old, s); 483 } 484 485 public String getManufacturerID() { 486 return _manufacturerID; 487 } 488 489 public void setProductID(@CheckForNull String s) { 490 String old = _productID; 491 if (s == null) {s = "";} 492 _productID = s; 493 firePropertyChange(DECODER_PRODUCTID, old, s); 494 } 495 496 public String getProductID() { 497 return _productID; 498 } 499 500 /** 501 * Set programming modes as defined in a roster entry's decoder definition. 502 * @param s a comma separated string of predefined mode elements 503 */ 504 public void setProgrammingModes(@CheckForNull String s) { 505 String old = _programmingModes; 506 if (s == null) {s = "";} 507 _programmingModes = s; 508 firePropertyChange(DECODER_MODES, old, s); 509 } 510 511 /** 512 * Get the modes as defined in a roster entry's decoder definition. 513 * @return a comma separated string of predefined mode elements 514 */ 515 public String getProgrammingModes() { 516 return _programmingModes; 517 } 518 519 public void setDecoderFamily(String s) { 520 String old = _decoderFamily; 521 _decoderFamily = s; 522 firePropertyChange(RosterEntry.DECODER_FAMILY, old, s); 523 } 524 525 public String getDecoderFamily() { 526 return _decoderFamily; 527 } 528 529 public void setDecoderComment(String s) { 530 String old = _decoderComment; 531 _decoderComment = s; 532 firePropertyChange(RosterEntry.DECODER_COMMENT, old, s); 533 } 534 535 public String getDecoderComment() { 536 return _decoderComment; 537 } 538 539 public void setMaxFnNum(String s) { 540 String old = _maxFnNum; 541 _maxFnNum = s; 542 firePropertyChange(RosterEntry.DECODER_MAXFNNUM, old, s); 543 } 544 545 public String getMaxFnNum() { 546 return _maxFnNum; 547 } 548 549 @Override 550 public DccLocoAddress getDccLocoAddress() { 551 int n; 552 try { 553 n = Integer.parseInt(getDccAddress()); 554 } catch (NumberFormatException e) { 555 log.error("Illegal format for DCC address roster entry: \"{}\" value: \"{}\"", getId(), getDccAddress()); 556 n = 0; 557 } 558 return new DccLocoAddress(n, _protocol); 559 } 560 561 public void setImagePath(String s) { 562 String old = _imageFilePath; 563 _imageFilePath = s; 564 firePropertyChange(RosterEntry.IMAGE_FILE_PATH, old, s); 565 } 566 567 public String getImagePath() { 568 return _imageFilePath; 569 } 570 571 public void setIconPath(String s) { 572 String old = _iconFilePath; 573 _iconFilePath = s; 574 firePropertyChange(RosterEntry.ICON_FILE_PATH, old, s); 575 } 576 577 public String getIconPath() { 578 return _iconFilePath; 579 } 580 581 public void setShuntingFunction(String fn) { 582 String old = this._isShuntingOn; 583 _isShuntingOn = fn; 584 this.firePropertyChange(RosterEntry.SHUNTING_FUNCTION, old, this._isShuntingOn); 585 } 586 587 @Override 588 public String getShuntingFunction() { 589 return _isShuntingOn; 590 } 591 592 public void setURL(String s) { 593 String old = _URL; 594 _URL = s; 595 firePropertyChange(RosterEntry.URL, old, s); 596 } 597 598 public String getURL() { 599 return _URL; 600 } 601 602 public void setDateModified(@Nonnull Date date) { 603 Date old = this.dateModified; 604 this.dateModified = new Date(date.getTime()); 605 this.firePropertyChange(RosterEntry.DATE_UPDATED, old, date); 606 } 607 608 /** 609 * Set the date modified given a string representing a date. 610 * <p> 611 * Tries ISO 8601 and the current Java defaults as formats for parsing a 612 * date. 613 * 614 * @param date the string to parse into a date 615 * @throws ParseException if the date cannot be parsed 616 */ 617 public void setDateModified(@Nonnull String date) throws ParseException { 618 try { 619 // parse using ISO 8601 date format(s) 620 setDateModified(new StdDateFormat().parse(date)); 621 } catch (ParseException ex) { 622 log.debug("ParseException in setDateModified ISO attempt: \"{}\"", date); 623 // next, try parse using defaults since thats how it was saved if saved 624 // by earlier versions of JMRI 625 try { 626 setDateModified(DateFormat.getDateTimeInstance().parse(date)); 627 } catch (ParseException ex2) { 628 // then try with a specific format to handle e.g. "Apr 1, 2016 9:13:36 AM" 629 DateFormat customFmt = new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a"); 630 try { 631 setDateModified(customFmt.parse(date)); 632 } catch (ParseException ex3) { 633 // then try with a specific format to handle e.g. "01-Oct-2016 21:13:36" 634 customFmt = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss"); 635 setDateModified(customFmt.parse(date)); 636 } 637 } 638 } catch (IllegalArgumentException ex2) { 639 // warn that there's perhaps something wrong with the classpath 640 log.error( 641 "IllegalArgumentException in RosterEntry.setDateModified - this may indicate a problem with the classpath, specifically multiple copies of the 'jackson` library. See release notes"); 642 // parse using defaults since that is how it was saved if saved 643 // by earlier versions of JMRI 644 this.setDateModified(DateFormat.getDateTimeInstance().parse(date)); 645 } 646 } 647 648 @CheckForNull 649 public Date getDateModified() { 650 return this.dateModified; 651 } 652 653 /** 654 * Set the date last updated. 655 * 656 * @param s the string to parse into a date 657 */ 658 protected void setDateUpdated(String s) { 659 String old = _dateUpdated; 660 _dateUpdated = s; 661 try { 662 this.setDateModified(s); 663 } catch (ParseException ex) { 664 log.warn("Unable to parse \"{}\" as a date in roster entry \"{}\".", s, getId()); 665 // property change is fired by setDateModified if s parses as a date 666 firePropertyChange(RosterEntry.DATE_UPDATED, old, s); 667 } 668 } 669 670 /** 671 * Get the date this entry was last modified. Returns the value of 672 * {@link #getDateModified()} in ISO 8601 format if that is not null, 673 * otherwise returns the raw value for the last modified date from the XML 674 * file for the roster entry. 675 * <p> 676 * Use getDateModified() if control over formatting is required 677 * 678 * @return the string representation of the date last modified 679 */ 680 public String getDateUpdated() { 681 Date date = this.getDateModified(); 682 if (date == null) { 683 return _dateUpdated; 684 } else { 685 return new StdDateFormat().format(date); 686 } 687 } 688 689 //openCounter is used purely to indicate if the roster entry has been opened in an editing mode. 690 int openCounter = 0; 691 692 @Override 693 public void setOpen(boolean boo) { 694 if (boo) { 695 openCounter++; 696 } else { 697 openCounter--; 698 } 699 if (openCounter < 0) { 700 openCounter = 0; 701 } 702 } 703 704 @Override 705 public boolean isOpen() { 706 return openCounter != 0; 707 } 708 709 /** 710 * Construct this Entry from XML. 711 * <p> 712 * This member has to remain synchronized with the detailed schema in 713 * xml/schema/locomotive-config.xsd. 714 * 715 * @param e Locomotive XML element 716 */ 717 public RosterEntry(Element e) { 718 functionLabels = Collections.synchronizedMap(new HashMap<>()); 719 soundLabels = Collections.synchronizedMap(new HashMap<>()); 720 functionSelectedImages = Collections.synchronizedMap(new HashMap<>()); 721 functionImages = Collections.synchronizedMap(new HashMap<>()); 722 functionLockables = Collections.synchronizedMap(new HashMap<>()); 723 functionVisibles = Collections.synchronizedMap(new HashMap<>()); 724 log.debug("ctor from element {}", e); 725 Attribute a; 726 if ((a = e.getAttribute("id")) != null) { 727 _id = a.getValue(); 728 } else { 729 log.warn("no id attribute in locomotive element when reading roster"); 730 } 731 if ((a = e.getAttribute("fileName")) != null) { 732 _fileName = a.getValue(); 733 } 734 if ((a = e.getAttribute("roadName")) != null) { 735 _roadName = a.getValue(); 736 } 737 if ((a = e.getAttribute("roadNumber")) != null) { 738 _roadNumber = a.getValue(); 739 } 740 if ((a = e.getAttribute("owner")) != null) { 741 _owner = a.getValue(); 742 } 743 if ((a = e.getAttribute("mfg")) != null) { 744 _mfg = a.getValue(); 745 } 746 if ((a = e.getAttribute("model")) != null) { 747 _model = a.getValue(); 748 } 749 if ((a = e.getAttribute("dccAddress")) != null) { 750 _dccAddress = a.getValue(); 751 } 752 753 // file path was saved without default xml config path 754 if ((a = e.getAttribute("imageFilePath")) != null && !a.getValue().isEmpty()) { 755 try { 756 if (FileUtil.getFile(a.getValue()).isFile()) { 757 _imageFilePath = FileUtil.getAbsoluteFilename(a.getValue()); 758 } 759 } catch (FileNotFoundException ex) { 760 try { 761 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 762 _imageFilePath = FileUtil.getUserResourcePath() + a.getValue(); 763 } 764 } catch (FileNotFoundException ex1) { 765 _imageFilePath = null; 766 } 767 } 768 } 769 if ((a = e.getAttribute("iconFilePath")) != null && !a.getValue().isEmpty()) { 770 try { 771 if (FileUtil.getFile(a.getValue()).isFile()) { 772 _iconFilePath = FileUtil.getAbsoluteFilename(a.getValue()); 773 } 774 } catch (FileNotFoundException ex) { 775 try { 776 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 777 _iconFilePath = FileUtil.getUserResourcePath() + a.getValue(); 778 } 779 } catch (FileNotFoundException ex1) { 780 _iconFilePath = null; 781 } 782 } 783 } 784 if ((a = e.getAttribute("URL")) != null) { 785 _URL = a.getValue(); 786 } 787 if ((a = e.getAttribute(RosterEntry.SHUNTING_FUNCTION)) != null) { 788 _isShuntingOn = a.getValue(); 789 } 790 if ((a = e.getAttribute(RosterEntry.MAX_SPEED)) != null) { 791 try { 792 _maxSpeedPCT = Integer.parseInt(a.getValue()); 793 } catch ( NumberFormatException ex ) { 794 log.error("Could not set maxSpeedPCT from {} , {}", a.getValue(), ex.getMessage()); 795 } 796 } 797 798 if ((a = e.getAttribute(DECODER_DEVELOPERID)) != null) { 799 _developerID = a.getValue(); 800 } 801 802 if ((a = e.getAttribute(DECODER_MANUFACTURERID)) != null) { 803 _manufacturerID = a.getValue(); 804 } 805 806 if ((a = e.getAttribute(DECODER_PRODUCTID)) != null) { 807 _productID = a.getValue(); 808 } 809 810 if ((a = e.getAttribute(DECODER_MODES)) != null) { 811 _programmingModes = a.getValue(); 812 } 813 814 Element e3; 815 if ((e3 = e.getChild("dateUpdated")) != null) { 816 this.setDateUpdated(e3.getText()); 817 } 818 if ((e3 = e.getChild("locoaddress")) != null) { 819 DccLocoAddress la = (DccLocoAddress) ((new jmri.configurexml.LocoAddressXml()).getAddress(e3)); 820 if (la != null) { 821 _dccAddress = "" + la.getNumber(); 822 _protocol = la.getProtocol(); 823 } else { 824 _dccAddress = ""; 825 _protocol = LocoAddress.Protocol.DCC_SHORT; 826 } 827 } else { // Did not find "locoaddress" element carrying the short/long, probably 828 // because this is an older-format file, so try to use system default. 829 // This is generally the best we can do without parsing the decoder file now 830 // but may give the wrong answer in some cases (low value long addresses on NCE) 831 832 jmri.ThrottleManager tf = jmri.InstanceManager.getNullableDefault(jmri.ThrottleManager.class); 833 int address; 834 try { 835 address = Integer.parseInt(_dccAddress); 836 } catch (NumberFormatException e2) { 837 address = 3; 838 } // ignore, accepting the default value 839 if (tf != null && tf.canBeLongAddress(address) && !tf.canBeShortAddress(address)) { 840 // if it has to be long, handle that 841 _protocol = LocoAddress.Protocol.DCC_LONG; 842 } else if (tf != null && !tf.canBeLongAddress(address) && tf.canBeShortAddress(address)) { 843 // if it has to be short, handle that 844 _protocol = LocoAddress.Protocol.DCC_SHORT; 845 } else { 846 // else guess short address 847 // These people should resave their roster, so we'll warn them 848 warnShortLong(_id); 849 _protocol = LocoAddress.Protocol.DCC_SHORT; 850 851 } 852 } 853 if ((a = e.getAttribute("comment")) != null) { 854 _comment = a.getValue(); 855 } 856 Element d = e.getChild("decoder"); 857 if (d != null) { 858 if ((a = d.getAttribute("model")) != null) { 859 _decoderModel = a.getValue(); 860 } 861 if ((a = d.getAttribute("family")) != null) { 862 _decoderFamily = a.getValue(); 863 } 864 if ((a = d.getAttribute(DECODER_DEVELOPERID)) != null) { 865 _developerID = a.getValue(); 866 } 867 if ((a = d.getAttribute(DECODER_MANUFACTURERID)) != null) { 868 _manufacturerID = a.getValue(); 869 } 870 if ((a = d.getAttribute(DECODER_PRODUCTID)) != null) { 871 _productID = a.getValue(); 872 } 873 if ((a = d.getAttribute("comment")) != null) { 874 _decoderComment = a.getValue(); 875 } 876 if ((a = d.getAttribute("maxFnNum")) != null) { 877 _maxFnNum = a.getValue(); 878 } 879 } 880 881 loadFunctions(e.getChild("functionlabels"), "RosterEntry"); 882 loadSounds(e.getChild("soundlabels"), "RosterEntry"); 883 loadAttributes(e.getChild("attributepairs")); 884 885 if (e.getChild(RosterEntry.SPEED_PROFILE) != null) { 886 _sp = new RosterSpeedProfile(this); 887 _sp.load(e.getChild(RosterEntry.SPEED_PROFILE)); 888 } 889 } 890 891 boolean loadedOnce = false; 892 893 /** 894 * Load function names from a JDOM element. 895 * <p> 896 * Does not change values that are already present! 897 * 898 * @param e3 the XML element containing functions 899 */ 900 public void loadFunctions(Element e3) { 901 this.loadFunctions(e3, "family"); 902 } 903 904 /** 905 * Loads function names from a JDOM element. Does not change values that are 906 * already present! 907 * 908 * @param e3 the XML element containing the functions 909 * @param source "family" if source is the decoder definition, or "model" if 910 * source is the roster entry itself 911 */ 912 public void loadFunctions(Element e3, String source) { 913 /* 914 * Load flag once, means that when the roster entry is edited only the 915 * first set of function labels are displayed ie those saved in the 916 * roster file, rather than those being left blank rather than being 917 * over-written by the defaults linked to the decoder def 918 */ 919 if (loadedOnce) { 920 return; 921 } 922 if (e3 != null) { 923 // load function names 924 List<Element> l = e3.getChildren(RosterEntry.FUNCTION_LABEL); 925 for (Element fn : l) { 926 int num = Integer.parseInt(fn.getAttribute("num").getValue()); 927 String lock = fn.getAttribute("lockable").getValue(); 928 String visible = null; 929 if (fn.getAttribute("visible") != null) { 930 visible = fn.getAttribute("visible").getValue(); 931 } 932 String val = LocaleSelector.getAttribute(fn, "text"); 933 if (val == null) { 934 val = fn.getText(); 935 } 936 if ((this.getFunctionLabel(num) == null) || (source.equalsIgnoreCase("model"))) { 937 this.setFunctionLabel(num, val); 938 this.setFunctionLockable(num, "true".equals(lock)); 939 if (visible != null){ 940 this.setFunctionVisible(num, "true".equals(visible)); 941 } 942 Attribute a; 943 if ((a = fn.getAttribute("functionImage")) != null && !a.getValue().isEmpty()) { 944 try { 945 if (FileUtil.getFile(a.getValue()).isFile()) { 946 this.setFunctionImage(num, FileUtil.getAbsoluteFilename(a.getValue())); 947 } 948 } catch (FileNotFoundException ex) { 949 try { 950 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 951 this.setFunctionImage(num, FileUtil.getUserResourcePath() + a.getValue()); 952 } 953 } catch (FileNotFoundException ex1) { 954 this.setFunctionImage(num, null); 955 } 956 } 957 } 958 if ((a = fn.getAttribute("functionImageSelected")) != null && !a.getValue().isEmpty()) { 959 try { 960 if (FileUtil.getFile(a.getValue()).isFile()) { 961 this.setFunctionSelectedImage(num, FileUtil.getAbsoluteFilename(a.getValue())); 962 } 963 } catch (FileNotFoundException ex) { 964 try { 965 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 966 this.setFunctionSelectedImage(num, FileUtil.getUserResourcePath() + a.getValue()); 967 } 968 } catch (FileNotFoundException ex1) { 969 this.setFunctionSelectedImage(num, null); 970 } 971 } 972 } 973 } 974 } 975 } 976 if (source.equalsIgnoreCase("RosterEntry")) { 977 loadedOnce = true; 978 } 979 } 980 981 private boolean soundLoadedOnce = false; 982 983 /** 984 * Loads sound names from a JDOM element. Does not change values that are 985 * already present! 986 * 987 * @param e3 the XML element containing sound names 988 * @param source "family" if source is the decoder definition, or "model" if 989 * source is the roster entry itself 990 */ 991 public void loadSounds(Element e3, String source) { 992 /* 993 * Load flag once, means that when the roster entry is edited only the 994 * first set of sound labels are displayed ie those saved in the roster 995 * file, rather than those being left blank rather than being 996 * over-written by the defaults linked to the decoder def 997 */ 998 if (soundLoadedOnce) { 999 return; 1000 } 1001 if (e3 != null) { 1002 // load sound names 1003 List<Element> l = e3.getChildren(RosterEntry.SOUND_LABEL); 1004 for (Element fn : l) { 1005 int num = Integer.parseInt(fn.getAttribute("num").getValue()); 1006 String val = LocaleSelector.getAttribute(fn, "text"); 1007 if (val == null) { 1008 val = fn.getText(); 1009 } 1010 if ((this.getSoundLabel(num) == null) || (source.equalsIgnoreCase("model"))) { 1011 this.setSoundLabel(num, val); 1012 } 1013 } 1014 } 1015 if (source.equalsIgnoreCase("RosterEntry")) { 1016 soundLoadedOnce = true; 1017 } 1018 } 1019 1020 /** 1021 * Load attribute key/value pairs from a JDOM element. 1022 * 1023 * @param e3 XML element containing roster entry attributes 1024 */ 1025 public void loadAttributes(Element e3) { 1026 if (e3 != null) { 1027 List<Element> l = e3.getChildren("keyvaluepair"); 1028 for (Element fn : l) { 1029 String key = fn.getChild("key").getText(); 1030 String value = fn.getChild("value").getText(); 1031 this.putAttribute(key, value); 1032 } 1033 } 1034 } 1035 1036 /** 1037 * Set the label for a specific function. 1038 * 1039 * @param fn function number, starting with 0 1040 * @param label the label to use 1041 */ 1042 public void setFunctionLabel(int fn, String label) { 1043 if (functionLabels == null) { 1044 functionLabels = Collections.synchronizedMap(new HashMap<>()); 1045 } 1046 String old = functionLabels.get(fn); 1047 functionLabels.put(fn, label); 1048 this.firePropertyChange(RosterEntry.FUNCTION_LABEL + fn, old, label); 1049 } 1050 1051 /** 1052 * If a label has been defined for a specific function, return it, otherwise 1053 * return null. 1054 * 1055 * @param fn function number, starting with 0 1056 * @return function label or null if not defined 1057 */ 1058 public String getFunctionLabel(int fn) { 1059 if (functionLabels == null) { 1060 return null; 1061 } 1062 return functionLabels.get(fn); 1063 } 1064 1065 /** 1066 * Define label for a specific sound. 1067 * 1068 * @param fn sound number, starting with 0 1069 * @param label display label for the sound function 1070 */ 1071 public void setSoundLabel(int fn, String label) { 1072 if (soundLabels == null) { 1073 soundLabels = Collections.synchronizedMap(new HashMap<>()); 1074 } 1075 String old = soundLabels.get(fn); 1076 soundLabels.put(fn, label); 1077 this.firePropertyChange(RosterEntry.SOUND_LABEL + fn, old, label); 1078 } 1079 1080 /** 1081 * If a label has been defined for a specific sound, return it, otherwise 1082 * return null. 1083 * 1084 * @param fn sound number, starting with 0 1085 * @return sound label or null 1086 */ 1087 public String getSoundLabel(int fn) { 1088 if (soundLabels == null) { 1089 return null; 1090 } 1091 return soundLabels.get(fn); 1092 } 1093 1094 public void setFunctionImage(int fn, String s) { 1095 if (functionImages == null) { 1096 functionImages = Collections.synchronizedMap(new HashMap<>()); 1097 } 1098 String old = functionImages.get(fn); 1099 functionImages.put(fn, s); 1100 firePropertyChange(RosterEntry.FUNCTION_IMAGE + fn, old, s); 1101 } 1102 1103 public String getFunctionImage(int fn) { 1104 if (functionImages == null) { 1105 return null; 1106 } 1107 return functionImages.get(fn); 1108 } 1109 1110 public void setFunctionSelectedImage(int fn, String s) { 1111 if (functionSelectedImages == null) { 1112 functionSelectedImages = Collections.synchronizedMap(new HashMap<>()); 1113 } 1114 String old = functionSelectedImages.get(fn); 1115 functionSelectedImages.put(fn, s); 1116 firePropertyChange(RosterEntry.FUNCTION_SELECTED_IMAGE + fn, old, s); 1117 } 1118 1119 public String getFunctionSelectedImage(int fn) { 1120 if (functionSelectedImages == null) { 1121 return null; 1122 } 1123 return functionSelectedImages.get(fn); 1124 } 1125 1126 /** 1127 * Define whether a specific function is lockable. 1128 * 1129 * @param fn function number, starting with 0 1130 * @param lockable true if function is continuous; false if momentary 1131 */ 1132 public void setFunctionLockable(int fn, boolean lockable) { 1133 if (functionLockables == null) { 1134 functionLockables = Collections.synchronizedMap(new HashMap<>()); 1135 functionLockables.put(fn, true); 1136 } 1137 boolean old = ((functionLockables.get(fn) != null) ? functionLockables.get(fn) : true); 1138 functionLockables.put(fn, lockable); 1139 this.firePropertyChange(RosterEntry.FUNCTION_LOCKABLE + fn, old, lockable); 1140 } 1141 1142 /** 1143 * Return the lockable/latchable state of a specific function. Defaults to true. 1144 * 1145 * @param fn function number, starting with 0 1146 * @return true if function is lockable/latchable 1147 */ 1148 public boolean getFunctionLockable(int fn) { 1149 if (functionLockables == null) { 1150 return true; 1151 } 1152 return ((functionLockables.get(fn) != null) ? functionLockables.get(fn) : true); 1153 } 1154 1155 /** 1156 * Define whether a specific function button is visible. 1157 * 1158 * @param fn function number, starting with 0 1159 * @param visible true if function button is visible; false to hide 1160 */ 1161 public void setFunctionVisible(int fn, boolean visible) { 1162 if (functionVisibles == null) { 1163 functionVisibles = Collections.synchronizedMap(new HashMap<>()); 1164 functionVisibles.put(fn, true); 1165 } 1166 boolean old = ((functionVisibles.get(fn) != null) ? functionVisibles.get(fn) : true); 1167 functionVisibles.put(fn, visible); 1168 this.firePropertyChange(RosterEntry.FUNCTION_LOCKABLE + fn, old, visible); 1169 } 1170 1171 /** 1172 * Return the UI visibility of a specific function button. Defaults to true. 1173 * 1174 * @param fn function number, starting with 0 1175 * @return true if function button is visible 1176 */ 1177 public boolean getFunctionVisible(int fn) { 1178 if (functionVisibles == null) { 1179 return true; 1180 } 1181 return ((functionVisibles.get(fn) != null) ? functionVisibles.get(fn) : true); 1182 } 1183 1184 @Override 1185 public void putAttribute(String key, String value) { 1186 String oldValue = getAttribute(key); 1187 attributePairs.put(key, value); 1188 firePropertyChange(RosterEntry.ATTRIBUTE_UPDATED + key, oldValue, value); 1189 } 1190 1191 @Override 1192 public String getAttribute(String key) { 1193 return attributePairs.get(key); 1194 } 1195 1196 @Override 1197 public void deleteAttribute(String key) { 1198 if (attributePairs.containsKey(key)) { 1199 attributePairs.remove(key); 1200 firePropertyChange(RosterEntry.ATTRIBUTE_DELETED, key, null); 1201 } 1202 } 1203 1204 /** 1205 * Provide access to the set of attributes. 1206 * <p> 1207 * This is directly backed access, so e.g. removing an item from this Set 1208 * removes it from the RosterEntry too. 1209 * 1210 * @return a set of attribute keys 1211 */ 1212 public java.util.Set<String> getAttributes() { 1213 return attributePairs.keySet(); 1214 } 1215 1216 @Override 1217 public String[] getAttributeList() { 1218 return attributePairs.keySet().toArray(new String[0]); 1219 } 1220 1221 /** 1222 * List the roster groups this entry is a member of, returning existing 1223 * {@link jmri.jmrit.roster.rostergroup.RosterGroup}s from the default 1224 * {@link jmri.jmrit.roster.Roster} if they exist. 1225 * 1226 * @return list of roster groups 1227 */ 1228 public List<RosterGroup> getGroups() { 1229 return this.getGroups(Roster.getDefault()); 1230 } 1231 1232 /** 1233 * List the roster groups this entry is a member of, returning existing 1234 * {@link jmri.jmrit.roster.rostergroup.RosterGroup}s from the specified 1235 * {@link jmri.jmrit.roster.Roster} if they exist. 1236 * 1237 * @param roster the roster to get matching groups from 1238 * @return list of roster groups 1239 */ 1240 public List<RosterGroup> getGroups(Roster roster) { 1241 List<RosterGroup> groups = new ArrayList<>(); 1242 if (!this.getAttributes().isEmpty()) { 1243 for (String attribute : this.getAttributes()) { 1244 if (attribute.startsWith(Roster.ROSTER_GROUP_PREFIX)) { 1245 String name = attribute.substring(Roster.ROSTER_GROUP_PREFIX.length()); 1246 if (roster.getRosterGroups().containsKey(name)) { 1247 groups.add(roster.getRosterGroups().get(name)); 1248 } else { 1249 groups.add(new RosterGroup(name)); 1250 } 1251 } 1252 } 1253 } 1254 return groups; 1255 } 1256 1257 @Override 1258 public int getMaxSpeedPCT() { 1259 return _maxSpeedPCT; 1260 } 1261 1262 public void setMaxSpeedPCT(int maxSpeedPCT) { 1263 int old = this._maxSpeedPCT; 1264 _maxSpeedPCT = maxSpeedPCT; 1265 this.firePropertyChange(RosterEntry.MAX_SPEED, old, this._maxSpeedPCT); 1266 } 1267 1268 /** 1269 * Warn user that the roster entry needs to be resaved. 1270 * 1271 * @param id roster ID to warn about 1272 */ 1273 protected void warnShortLong(String id) { 1274 log.warn("Roster entry \"{}\" should be saved again to store the short/long address value", id); 1275 } 1276 1277 /** 1278 * Create an XML element to represent this Entry. 1279 * <p> 1280 * This member has to remain synchronized with the detailed schema in 1281 * xml/schema/locomotive-config.xsd. 1282 * 1283 * @return Contents in a JDOM Element 1284 */ 1285 @Override 1286 public Element store() { 1287 Element e = new Element("locomotive"); 1288 e.setAttribute("id", getId()); 1289 e.setAttribute("fileName", getFileName()); 1290 e.setAttribute("roadNumber", getRoadNumber()); 1291 e.setAttribute("roadName", getRoadName()); 1292 e.setAttribute("mfg", getMfg()); 1293 e.setAttribute("owner", getOwner()); 1294 e.setAttribute("model", getModel()); 1295 e.setAttribute("dccAddress", getDccAddress()); 1296 //e.setAttribute("protocol", "" + getProtocol()); 1297 e.setAttribute("comment", getComment()); 1298 e.setAttribute(DECODER_DEVELOPERID, getDeveloperID()); 1299 e.setAttribute(DECODER_MANUFACTURERID, getManufacturerID()); 1300 e.setAttribute(DECODER_PRODUCTID, getProductID()); 1301 e.setAttribute(DECODER_MODES, getProgrammingModes()); 1302 e.setAttribute(RosterEntry.MAX_SPEED, (Integer.toString(getMaxSpeedPCT()))); 1303 // file path are saved without default xml config path 1304 e.setAttribute("imageFilePath", 1305 (this.getImagePath() != null) ? FileUtil.getPortableFilename(this.getImagePath()) : ""); 1306 e.setAttribute("iconFilePath", 1307 (this.getIconPath() != null) ? FileUtil.getPortableFilename(this.getIconPath()) : ""); 1308 e.setAttribute("URL", getURL()); 1309 e.setAttribute(RosterEntry.SHUNTING_FUNCTION, getShuntingFunction()); 1310 if (_dateUpdated.isEmpty()) { 1311 // set date updated to now if never set previously 1312 this.changeDateUpdated(); 1313 } 1314 e.addContent(new Element("dateUpdated").addContent(this.getDateUpdated())); 1315 Element d = new Element("decoder"); 1316 d.setAttribute("model", getDecoderModel()); 1317 d.setAttribute("family", getDecoderFamily()); 1318 d.setAttribute("comment", getDecoderComment()); 1319 d.setAttribute("maxFnNum", getMaxFnNum()); 1320 1321 e.addContent(d); 1322 if (_dccAddress.isEmpty()) { 1323 e.addContent((new jmri.configurexml.LocoAddressXml()).store(null)); // store a null address 1324 } else { 1325 e.addContent((new jmri.configurexml.LocoAddressXml()) 1326 .store(new DccLocoAddress(Integer.parseInt(_dccAddress), _protocol))); 1327 } 1328 1329 if (functionLabels != null) { 1330 Element s = new Element("functionlabels"); 1331 1332 // loop to copy non-null elements 1333 functionLabels.forEach((key, value) -> { 1334 if (value != null && !value.isEmpty()) { 1335 Element fne = new Element(RosterEntry.FUNCTION_LABEL); 1336 fne.setAttribute("num", "" + key); 1337 fne.setAttribute("lockable", getFunctionLockable(key) ? "true" : "false"); 1338 fne.setAttribute("visible", getFunctionVisible(key) ? "true" : "false"); 1339 fne.setAttribute("functionImage", 1340 (getFunctionImage(key) != null) ? FileUtil.getPortableFilename(getFunctionImage(key)) : ""); 1341 fne.setAttribute("functionImageSelected", (getFunctionSelectedImage(key) != null) 1342 ? FileUtil.getPortableFilename(getFunctionSelectedImage(key)) : ""); 1343 fne.addContent(value); 1344 s.addContent(fne); 1345 } 1346 }); 1347 e.addContent(s); 1348 } 1349 1350 if (soundLabels != null) { 1351 Element s = new Element("soundlabels"); 1352 1353 // loop to copy non-null elements 1354 soundLabels.forEach((key, value) -> { 1355 if (value != null && !value.isEmpty()) { 1356 Element fne = new Element(RosterEntry.SOUND_LABEL); 1357 fne.setAttribute("num", "" + key); 1358 fne.addContent(value); 1359 s.addContent(fne); 1360 } 1361 }); 1362 e.addContent(s); 1363 } 1364 1365 if (!getAttributes().isEmpty()) { 1366 d = new Element("attributepairs"); 1367 for (String key : getAttributes()) { 1368 d.addContent(new Element("keyvaluepair") 1369 .addContent(new Element("key") 1370 .addContent(key)) 1371 .addContent(new Element("value") 1372 .addContent(getAttribute(key)))); 1373 } 1374 e.addContent(d); 1375 } 1376 if (_sp != null) { 1377 _sp.store(e); 1378 } 1379 return e; 1380 } 1381 1382 @Override 1383 public String titleString() { 1384 return getId(); 1385 } 1386 1387 @Override 1388 public String toString() { 1389 return new StringBuilder() 1390 .append("[RosterEntry: ") 1391 .append(_id) 1392 .append(" ") 1393 .append(_fileName != null ? _fileName : "<null>") 1394 .append(" ") 1395 .append(_roadName) 1396 .append(" ") 1397 .append(_roadNumber) 1398 .append(" ") 1399 .append(_mfg) 1400 .append(" ") 1401 .append(_owner) 1402 .append(" ") 1403 .append(_model) 1404 .append(" ") 1405 .append(_dccAddress) 1406 .append(" ") 1407 .append(_comment) 1408 .append(" ") 1409 .append(_decoderModel) 1410 .append(" ") 1411 .append(_decoderFamily) 1412 .append(" ") 1413 .append(_developerID) 1414 .append(" ") 1415 .append(_manufacturerID) 1416 .append(" ") 1417 .append(_productID) 1418 .append(" ") 1419 .append(_programmingModes) 1420 .append(" ") 1421 .append(_decoderComment) 1422 .append("]") 1423 .toString(); 1424 } 1425 1426 /** 1427 * Write the contents of this RosterEntry back to a file, preserving all 1428 * existing decoder CV content. 1429 * <p> 1430 * This writes the file back in place, with the same decoder-specific 1431 * content. 1432 */ 1433 public void updateFile() { 1434 LocoFile df = new LocoFile(); 1435 1436 String fullFilename = Roster.getDefault().getRosterFilesLocation() + getFileName(); 1437 1438 // read in the content 1439 try { 1440 mRootElement = df.rootFromName(fullFilename); 1441 } catch (JDOMException 1442 | IOException e) { 1443 log.error("Exception while loading loco XML file: {} exception", getFileName(), e); 1444 } 1445 1446 try { 1447 File f = new File(fullFilename); 1448 // do backup 1449 df.makeBackupFile(Roster.getDefault().getRosterFilesLocation() + getFileName()); 1450 1451 // and finally write the file 1452 df.writeFile(f, mRootElement, this.store()); 1453 1454 } catch (Exception e) { 1455 log.error("error during locomotive file output", e); 1456 try { 1457 JmriJOptionPane.showMessageDialog(null, 1458 Bundle.getMessage("ErrorSavingText") + "\n" 1459 + e.getMessage(), 1460 Bundle.getMessage("ErrorSavingTitle"), 1461 JmriJOptionPane.ERROR_MESSAGE); 1462 } catch (HeadlessException he) { 1463 // silently ignore inability to display dialog 1464 } 1465 } 1466 } 1467 1468 /** 1469 * Write the contents of this RosterEntry to a file. 1470 * <p> 1471 * Information on the contents is passed through the parameters, as the 1472 * actual XML creation is done in the LocoFile class. 1473 * 1474 * @param cvModel CV contents to include in file 1475 * @param variableModel Variable contents to include in file 1476 * 1477 */ 1478 public void writeFile(CvTableModel cvModel, VariableTableModel variableModel) { 1479 LocoFile df = new LocoFile(); 1480 1481 // do I/O 1482 FileUtil.createDirectory(Roster.getDefault().getRosterFilesLocation()); 1483 1484 try { 1485 String fullFilename = Roster.getDefault().getRosterFilesLocation() + getFileName(); 1486 File f = new File(fullFilename); 1487 // do backup 1488 df.makeBackupFile(Roster.getDefault().getRosterFilesLocation() + getFileName()); 1489 1490 // changed 1491 changeDateUpdated(); 1492 1493 // and finally write the file 1494 df.writeFile(f, cvModel, variableModel, this); 1495 1496 } catch (Exception e) { 1497 log.error("error during locomotive file output", e); 1498 try { 1499 JmriJOptionPane.showMessageDialog(null, 1500 Bundle.getMessage("ErrorSavingText") + "\n" 1501 + e.getMessage(), 1502 Bundle.getMessage("ErrorSavingTitle"), 1503 JmriJOptionPane.ERROR_MESSAGE); 1504 } catch (HeadlessException he) { 1505 // silently ignore inability to display dialog 1506 } 1507 } 1508 } 1509 1510 /** 1511 * Mark the date updated, e.g. from storing this roster entry. 1512 */ 1513 public void changeDateUpdated() { 1514 // used to create formatted string of now using defaults 1515 this.setDateModified(new Date()); 1516 } 1517 1518 /** 1519 * Store the root element of the JDOM tree representing this RosterEntry. 1520 */ 1521 private Element mRootElement = null; 1522 1523 /** 1524 * Load pre-existing Variable and CvTableModel object with the contents of 1525 * this entry. 1526 * 1527 * @param varModel the variable model to load 1528 * @param cvModel CV contents to load 1529 */ 1530 public void loadCvModel(VariableTableModel varModel, CvTableModel cvModel) { 1531 if (cvModel == null) { 1532 log.error("loadCvModel must be given a non-null argument"); 1533 return; 1534 } 1535 if (mRootElement == null) { 1536 log.error("loadCvModel called before readFile() succeeded"); 1537 return; 1538 } 1539 try { 1540 if (varModel != null) { 1541 LocoFile.loadVariableModel(mRootElement.getChild("locomotive"), varModel); 1542 } 1543 1544 LocoFile.loadCvModel(mRootElement.getChild("locomotive"), cvModel, getManufacturerID(), getDecoderFamily()); 1545 } catch (Exception ex) { 1546 log.error("Error reading roster entry", ex); 1547 try { 1548 JmriJOptionPane.showMessageDialog(null, 1549 Bundle.getMessage("ErrorReadingText") + "\n" + _fileName, 1550 Bundle.getMessage("ErrorReadingTitle"), 1551 JmriJOptionPane.ERROR_MESSAGE); 1552 } catch (HeadlessException he) { 1553 // silently ignore inability to display dialog 1554 } 1555 } 1556 } 1557 1558 /** 1559 * Ultra-compact list view of roster entries. Shows text from fields as 1560 * initially visible in the Roster frame table. 1561 * <p> 1562 * Header is created in 1563 * {@link PrintListAction#actionPerformed(java.awt.event.ActionEvent)} so 1564 * keep column widths identical with values of colWidth below. 1565 * 1566 * @param w writer providing output 1567 */ 1568 public void printEntryLine(HardcopyWriter w) { 1569 // no image 1570 // @see #printEntryDetails(w); 1571 1572 try { 1573 //int textSpace = w.getCharactersPerLine() - 1; // could be used to truncate line. 1574 // for now, text just flows to next line 1575 String thisText; 1576 String thisLine = ""; 1577 1578 // start each entry on a new line 1579 w.write(newLine, 0, 1); 1580 1581 int colWidth = 15; 1582 // roster entry ID (not the filname) 1583 if (_id != null) { 1584 thisText = String.format("%-" + colWidth + "s", _id.substring(0, Math.min(_id.length(), colWidth))); // %- = left align 1585 log.debug("thisText = |{}|, length = {}", thisText, thisText.length()); 1586 } else { 1587 thisText = String.format("%-" + colWidth + "s", "<null>"); 1588 } 1589 thisLine += thisText; 1590 colWidth = 6; 1591 // _dccAddress 1592 thisLine += StringUtil.padString(_dccAddress, colWidth); 1593 colWidth = 6; 1594 // _roadName 1595 thisLine += StringUtil.padString(_roadName, colWidth); 1596 colWidth = 6; 1597 // _roadNumber 1598 thisLine += StringUtil.padString(_roadNumber, colWidth); 1599 colWidth = 6; 1600 // _mfg 1601 thisLine += StringUtil.padString(_mfg, colWidth); 1602 colWidth = 10; 1603 // _model 1604 thisLine += StringUtil.padString(_model, colWidth); 1605 colWidth = 10; 1606 // _decoderModel 1607 thisLine += StringUtil.padString(_decoderModel, colWidth); 1608 colWidth = 12; 1609 // _protocol (type) 1610 thisLine += StringUtil.padString(_protocol.toString(), colWidth); 1611 colWidth = 6; 1612 // _owner 1613 thisLine += StringUtil.padString(_owner, colWidth); 1614 colWidth = 10; 1615 1616 // dateModified (type) 1617 if (dateModified != null) { 1618 DateFormat.getDateTimeInstance().format(dateModified); 1619 thisText = String.format("%-" + colWidth + "s", 1620 dateModified.toString().substring(0, Math.min(dateModified.toString().length(), colWidth))); 1621 thisLine += thisText; 1622 } 1623 // don't include comment and decoder family 1624 1625 w.write(thisLine); 1626 // extra whitespace line after each entry would miss goal of a compact listing 1627 // w.write(newLine, 0, 1); 1628 } catch (IOException e) { 1629 log.error("Error printing RosterEntry: ", e); 1630 } 1631 } 1632 1633 public void printEntry(HardcopyWriter w) { 1634 if (getIconPath() != null) { 1635 ImageIcon icon = new ImageIcon(getIconPath()); 1636 // We use an ImageIcon because it's guaranteed to have been loaded when ctor is complete. 1637 // We set the imagesize to 150x150 pixels 1638 int imagesize = 150; 1639 1640 Image img = icon.getImage(); 1641 int width = img.getWidth(null); 1642 int height = img.getHeight(null); 1643 double widthratio = (double) width / imagesize; 1644 double heightratio = (double) height / imagesize; 1645 double ratio = Math.max(widthratio, heightratio); 1646 width = (int) (width / ratio); 1647 height = (int) (height / ratio); 1648 Image newImg = img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH); 1649 1650 ImageIcon newIcon = new ImageIcon(newImg); 1651 w.writeNoScale(newIcon.getImage(), new JLabel(newIcon)); 1652 // Work out the number of line approx that the image takes up. 1653 // We might need to pad some areas of the roster out, so that things 1654 // look correct and text doesn't overflow into the image. 1655 blanks = (newImg.getHeight(null) - w.getLineAscent()) / w.getLineHeight(); 1656 textSpaceWithIcon 1657 = w.getCharactersPerLine() - ((newImg.getWidth(null) / w.getCharWidth())) - indentWidth - 1; 1658 1659 } 1660 printEntryDetails(w); 1661 } 1662 1663 private int blanks = 0; 1664 private int textSpaceWithIcon = 0; 1665 String indent = " "; 1666 int indentWidth = indent.length(); 1667 String newLine = "\n"; 1668 1669 /** 1670 * Print the roster entry information. 1671 * <p> 1672 * Updated to allow for multiline comment and decoder comment fields. 1673 * Separate write statements for text and line feeds to work around the 1674 * HardcopyWriter bug that misplaces borders. 1675 * 1676 * @param w the HardcopyWriter used to print 1677 */ 1678 public void printEntryDetails(Writer w) { 1679 if (!(w instanceof HardcopyWriter)) { 1680 throw new IllegalArgumentException("No HardcopyWriter instance passed"); 1681 } 1682 int linesAdded = -1; 1683 String title; 1684 String leftMargin = " "; // 3 spaces in front of legend labels 1685 int labelColumn = 19; // pad remaining spaces for legend using fixed width font, forms "%-19s" in line 1686 try { 1687 HardcopyWriter ww = (HardcopyWriter) w; 1688 int textSpace = ww.getCharactersPerLine() - indentWidth - 1; 1689 title = String.format("%-" + labelColumn + "s", 1690 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldID")))); // I18N ID: 1691 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1692 linesAdded = writeWrappedComment(w, _id, leftMargin + title, textSpaceWithIcon) + linesAdded; 1693 } else { 1694 linesAdded = writeWrappedComment(w, _id, leftMargin + title, textSpace) + linesAdded; 1695 } 1696 title = String.format("%-" + labelColumn + "s", 1697 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldFilename")))); // I18N Filename: 1698 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1699 linesAdded = writeWrappedComment(w, _fileName != null ? _fileName : "<null>", leftMargin + title, 1700 textSpaceWithIcon) + linesAdded; 1701 } else { 1702 linesAdded = writeWrappedComment(w, _fileName != null ? _fileName : "<null>", leftMargin + title, 1703 textSpace) + linesAdded; 1704 } 1705 1706 if (!(_roadName.isEmpty())) { 1707 title = String.format("%-" + labelColumn + "s", 1708 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldRoadName")))); // I18N Road name: 1709 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1710 linesAdded = writeWrappedComment(w, _roadName, leftMargin + title, textSpaceWithIcon) + linesAdded; 1711 } else { 1712 linesAdded = writeWrappedComment(w, _roadName, leftMargin + title, textSpace) + linesAdded; 1713 } 1714 } 1715 if (!(_roadNumber.isEmpty())) { 1716 title = String.format("%-" + labelColumn + "s", 1717 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldRoadNumber")))); // I18N Road number: 1718 1719 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1720 linesAdded 1721 = writeWrappedComment(w, _roadNumber, leftMargin + title, textSpaceWithIcon) + linesAdded; 1722 } else { 1723 linesAdded = writeWrappedComment(w, _roadNumber, leftMargin + title, textSpace) + linesAdded; 1724 } 1725 } 1726 if (!(_mfg.isEmpty())) { 1727 title = String.format("%-" + labelColumn + "s", 1728 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldManufacturer")))); // I18N Manufacturer: 1729 1730 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1731 linesAdded = writeWrappedComment(w, _mfg, leftMargin + title, textSpaceWithIcon) + linesAdded; 1732 } else { 1733 linesAdded = writeWrappedComment(w, _mfg, leftMargin + title, textSpace) + linesAdded; 1734 } 1735 } 1736 if (!(_owner.isEmpty())) { 1737 title = String.format("%-" + labelColumn + "s", 1738 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldOwner")))); // I18N Owner: 1739 1740 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1741 linesAdded = writeWrappedComment(w, _owner, leftMargin + title, textSpaceWithIcon) + linesAdded; 1742 } else { 1743 linesAdded = writeWrappedComment(w, _owner, leftMargin + title, textSpace) + linesAdded; 1744 } 1745 } 1746 if (!(_model.isEmpty())) { 1747 title = String.format("%-" + labelColumn + "s", 1748 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldModel")))); // I18N Model: 1749 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1750 linesAdded = writeWrappedComment(w, _model, leftMargin + title, textSpaceWithIcon) + linesAdded; 1751 } else { 1752 linesAdded = writeWrappedComment(w, _model, leftMargin + title, textSpace) + linesAdded; 1753 } 1754 } 1755 if (!(_dccAddress.isEmpty())) { 1756 w.write(newLine, 0, 1); 1757 title = String.format("%-" + labelColumn + "s", 1758 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDCCAddress")))); // I18N DCC Address: 1759 String s = leftMargin + title + _dccAddress; 1760 w.write(s, 0, s.length()); 1761 linesAdded++; 1762 } 1763 1764 // If there is a comment field, then wrap it using the new wrapCommment() 1765 // method and print it 1766 if (!(_comment.isEmpty())) { 1767 // Because the text will fill the width if the roster entry has an icon 1768 // then we need to add some blank lines to prevent the comment text going 1769 // through the picture. 1770 for (int i = 0; i < (blanks - linesAdded); i++) { 1771 w.write(newLine, 0, 1); 1772 } 1773 // As we have added the blank lines to pad out the comment we will 1774 // reset the number of blanks to 0. 1775 if (blanks != 0) { 1776 blanks = 0; 1777 } 1778 title = String.format("%-" + labelColumn + "s", 1779 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldComment")))); // I18N Comment: 1780 linesAdded = writeWrappedComment(w, _comment, leftMargin + title, textSpace) + linesAdded; 1781 } 1782 if (!(_decoderModel.isEmpty())) { 1783 title = String.format("%-" + labelColumn + "s", 1784 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderModel")))); // I18N Decoder Model: 1785 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1786 linesAdded 1787 = writeWrappedComment(w, _decoderModel, leftMargin + title, textSpaceWithIcon) + linesAdded; 1788 } else { 1789 linesAdded = writeWrappedComment(w, _decoderModel, leftMargin + title, textSpace) + linesAdded; 1790 } 1791 } 1792 if (!(_decoderFamily.isEmpty())) { 1793 title = String.format("%-" + labelColumn + "s", 1794 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderFamily")))); // I18N Decoder Family: 1795 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1796 linesAdded 1797 = writeWrappedComment(w, _decoderFamily, leftMargin + title, textSpaceWithIcon) + linesAdded; 1798 } else { 1799 linesAdded = writeWrappedComment(w, _decoderFamily, leftMargin + title, textSpace) + linesAdded; 1800 } 1801 } 1802 if (!(_programmingModes.isEmpty())) { 1803 title = String.format("%-" + labelColumn + "s", 1804 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderModes")))); // I18N Programming Mode(s): 1805 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1806 linesAdded 1807 = writeWrappedComment(w, _programmingModes, leftMargin + title, textSpaceWithIcon) + linesAdded; 1808 } else { 1809 linesAdded = writeWrappedComment(w, _programmingModes, leftMargin + title, textSpace) + linesAdded; 1810 } 1811 } 1812 1813 // If there is a decoderComment field, need to wrap it 1814 if (!(_decoderComment.isEmpty())) { 1815 // Because the text will fill the width if the roster entry has an icon 1816 // then we need to add some blank lines to prevent the comment text going 1817 // through the picture. 1818 for (int i = 0; i < (blanks - linesAdded); i++) { 1819 w.write(newLine, 0, 1); 1820 } 1821 // As we have added the blank lines to pad out the comment we will 1822 // reset the number of blanks to 0. 1823 if (blanks != 0) { 1824 blanks = 0; 1825 } 1826 title = String.format("%-" + labelColumn + "s", 1827 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderComment")))); // I18N Decoder Comment: 1828 linesAdded = writeWrappedComment(w, _decoderComment, leftMargin + title, textSpace) + linesAdded; 1829 } 1830 w.write(newLine, 0, 1); 1831 for (int i = -1; i < (blanks - linesAdded); i++) { 1832 w.write(newLine, 0, 1); 1833 } 1834 } catch (IOException e) { 1835 log.error("Error printing RosterEntry", e); 1836 } 1837 } 1838 1839 private int writeWrappedComment(Writer w, String text, String title, int textSpace) { 1840 Vector<String> commentVector = wrapComment(text, textSpace); 1841 1842 // Now have a vector of text pieces and line feeds that will all 1843 // fit in the allowed space. Print each piece, prefixing the first one 1844 // with the label and indenting any remaining. 1845 String s; 1846 int k = 0; 1847 try { 1848 w.write(newLine, 0, 1); 1849 s = title + commentVector.elementAt(k); 1850 w.write(s, 0, s.length()); 1851 k++; 1852 while (k < commentVector.size()) { 1853 String token = commentVector.elementAt(k); 1854 if (!token.equals("\n")) { 1855 s = indent + token; 1856 } else { 1857 s = token; 1858 } 1859 w.write(s, 0, s.length()); 1860 k++; 1861 } 1862 } catch (IOException e) { 1863 log.error("Error printing RosterEntry", e); 1864 } 1865 return k; 1866 } 1867 1868 /** 1869 * Line wrap a comment. 1870 * 1871 * @param comment the comment to wrap at word boundaries 1872 * @param textSpace the width of the space to print 1873 * 1874 * @return comment wrapped to fit given width 1875 */ 1876 public Vector<String> wrapComment(String comment, int textSpace) { 1877 //Tokenize the string using \n to separate the text on mulitple lines 1878 //and create a vector to hold the processed text pieces 1879 StringTokenizer commentTokens = new StringTokenizer(comment, "\n", true); 1880 Vector<String> textVector = new Vector<>(commentTokens.countTokens()); 1881 while (commentTokens.hasMoreTokens()) { 1882 String commentToken = commentTokens.nextToken(); 1883 int startIndex = 0; 1884 int endIndex; 1885 //Check each token to see if it needs to have a line wrap. 1886 //Get a piece of the token, either the size of the allowed space or 1887 //a shorter piece if there isn't enough text to fill the space 1888 if (commentToken.length() < startIndex + textSpace) { 1889 //the piece will fit so extract it and put it in the vector 1890 textVector.addElement(commentToken); 1891 } else { 1892 //Piece too long to fit. Extract a piece the size of the textSpace 1893 //and check for farthest right space for word wrapping. 1894 log.debug("token: /{}/", commentToken); 1895 1896 while (startIndex < commentToken.length()) { 1897 String tokenPiece = commentToken.substring(startIndex, startIndex + textSpace); 1898 if (log.isDebugEnabled()) { 1899 log.debug("loop: /{}/ {}", tokenPiece, tokenPiece.lastIndexOf(" ")); 1900 } 1901 if (tokenPiece.lastIndexOf(" ") == -1) { 1902 //If no spaces, put the whole piece in the vector and add a line feed, then 1903 //increment the startIndex to reposition for extracting next piece 1904 textVector.addElement(tokenPiece); 1905 textVector.addElement(newLine); 1906 startIndex += textSpace; 1907 } else { 1908 //If there is at least one space, extract up to and including the 1909 //last space and put in the vector as well as a line feed 1910 endIndex = tokenPiece.lastIndexOf(" ") + 1; 1911 log.debug("tokenPiece /{}/ {} {}", tokenPiece, startIndex, endIndex); 1912 1913 textVector.addElement(tokenPiece.substring(0, endIndex)); 1914 textVector.addElement(newLine); 1915 startIndex += endIndex; 1916 } 1917 //Check the remaining piece to see if it fits - startIndex now points 1918 //to the start of the next piece 1919 if (commentToken.substring(startIndex).length() < textSpace) { 1920 //It will fit so just insert it, otherwise will cycle through the 1921 //while loop and the checks above will take care of the remainder. 1922 //Line feed is not required as this is the last part of the token. 1923 textVector.addElement(commentToken.substring(startIndex)); 1924 startIndex += textSpace; 1925 } 1926 } 1927 } 1928 } 1929 return textVector; 1930 } 1931 1932 /** 1933 * Read a file containing the contents of this RosterEntry. 1934 * <p> 1935 * This has to be done before a call to loadCvModel, for example. 1936 */ 1937 public void readFile() { 1938 if (getFileName() == null) { 1939 log.warn("readFile invoked with null filename"); 1940 return; 1941 } else { 1942 log.debug("readFile invoked with filename {}", getFileName()); 1943 } 1944 1945 LocoFile lf = new LocoFile(); // used as a temporary 1946 String file = Roster.getDefault().getRosterFilesLocation() + getFileName(); 1947 if (!(new File(file).exists())) { 1948 // try without prefix 1949 file = getFileName(); 1950 } 1951 try { 1952 mRootElement = lf.rootFromName(file); 1953 } catch (JDOMException | IOException e) { 1954 log.error("Exception while loading loco XML file: {} from {}", getFileName(), file, e); 1955 } 1956 } 1957 1958 /** 1959 * Create a RosterEntry from a file. 1960 * 1961 * @param file The file containing the RosterEntry 1962 * @return a new RosterEntry 1963 * @throws JDOMException if unable to parse file 1964 * @throws IOException if unable to read file 1965 */ 1966 public static RosterEntry fromFile(@Nonnull File file) throws JDOMException, IOException { 1967 Element loco = (new LocoFile()).rootFromFile(file).getChild("locomotive"); 1968 if (loco == null) { 1969 throw new JDOMException("missing expected element"); 1970 } 1971 RosterEntry re = new RosterEntry(loco); 1972 re.setFileName(file.getName()); 1973 return re; 1974 } 1975 1976 @Override 1977 public String getDisplayName() { 1978 if (this.getRoadName() != null && !this.getRoadName().isEmpty()) { // NOI18N 1979 return Bundle.getMessage("RosterEntryDisplayName", this.getDccAddress(), this.getRoadName(), 1980 this.getRoadNumber()); // NOI18N 1981 } else { 1982 return Bundle.getMessage("RosterEntryDisplayName", this.getDccAddress(), this.getId(), ""); // NOI18N 1983 } 1984 } 1985 1986 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterEntry.class); 1987 1988}