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