001package jmri.jmrit.operations.routes; 002 003import java.awt.Color; 004import java.awt.Point; 005 006import org.jdom2.Attribute; 007import org.jdom2.Element; 008import org.slf4j.Logger; 009import org.slf4j.LoggerFactory; 010 011import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 012import jmri.InstanceManager; 013import jmri.beans.PropertyChangeSupport; 014import jmri.jmrit.operations.locations.Location; 015import jmri.jmrit.operations.locations.LocationManager; 016import jmri.jmrit.operations.setup.Control; 017import jmri.jmrit.operations.setup.Setup; 018import jmri.jmrit.operations.trains.TrainCommon; 019import jmri.util.ColorUtil; 020 021/** 022 * Represents a location in a route, a location can appear more than once in a 023 * route. 024 * 025 * @author Daniel Boudreau Copyright (C) 2008, 2013 026 */ 027public class RouteLocation extends PropertyChangeSupport implements java.beans.PropertyChangeListener { 028 029 public static final String NONE = ""; 030 031 protected String _id = NONE; 032 protected Location _location = null; // the location in the route 033 protected String _locationId = NONE; // the location's id 034 protected int _trainDir = (Setup.getTrainDirection() == Setup.EAST + Setup.WEST) ? EAST : NORTH; // train direction 035 protected int _maxTrainLength = Setup.getMaxTrainLength(); 036 protected int _maxCarMoves = Setup.getCarMoves(); 037 protected String _randomControl = DISABLED; 038 protected boolean _drops = true; // when true set outs allowed at this location 039 protected boolean _pickups = true; // when true pick ups allowed at this location 040 protected int _sequenceNum = 0; // used to determine location order in a route 041 protected double _grade = 0; // maximum grade between locations 042 protected int _wait = 0; // wait time at this location 043 protected String _departureTime = NONE; // departure time from this location 044 protected int _trainIconX = 0; // the x & y coordinates for the train icon 045 protected int _trainIconY = 0; 046 protected int _blockingOrder = 0; 047 protected String _comment = NONE; 048 protected Color _commentColor = Color.black; 049 050 protected int _carMoves = 0; // number of moves at this location 051 protected int _trainWeight = 0; // total car weight departing this location 052 protected int _trainLength = 0; // train length departing this location 053 054 public static final int EAST = 1; // train direction 055 public static final int WEST = 2; 056 public static final int NORTH = 4; 057 public static final int SOUTH = 8; 058 059 public static final String EAST_DIR = Setup.EAST_DIR; // train directions text 060 public static final String WEST_DIR = Setup.WEST_DIR; 061 public static final String NORTH_DIR = Setup.NORTH_DIR; 062 public static final String SOUTH_DIR = Setup.SOUTH_DIR; 063 064 public static final String DISPOSE = "routeLocationDispose"; // NOI18N 065 public static final String DELETED = Bundle.getMessage("locationDeleted"); 066 067 public static final String DROP_CHANGED_PROPERTY = "dropChange"; // NOI18N 068 public static final String PICKUP_CHANGED_PROPERTY = "pickupChange"; // NOI18N 069 public static final String MAX_MOVES_CHANGED_PROPERTY = "maxMovesChange"; // NOI18N 070 public static final String TRAIN_DIRECTION_CHANGED_PROPERTY = "trainDirectionChange"; // NOI18N 071 public static final String DEPARTURE_TIME_CHANGED_PROPERTY = "routeDepartureTimeChange"; // NOI18N 072 public static final String MAX_LENGTH_CHANGED_PROPERTY = "maxLengthChange"; // NOI18N 073 074 public static final String DISABLED = "Off"; 075 076 public RouteLocation(String id, Location location) { 077 log.debug("New route location ({}) id: {}", location.getName(), id); 078 _location = location; 079 _id = id; 080 // listen for name change or delete 081 location.addPropertyChangeListener(this); 082 } 083 084 // for combo boxes 085 @Override 086 public String toString() { 087 return getName(); 088 } 089 090 public String getId() { 091 return _id; 092 } 093 094 public String getName() { 095 if (getLocation() != null) { 096 return getLocation().getName(); 097 } 098 return DELETED; 099 } 100 101 public String getSplitName() { 102 if (getLocation() != null) { 103 return getLocation().getSplitName(); 104 } 105 return DELETED; 106 } 107 108 private String getNameId() { 109 if (_location != null) { 110 return _location.getId(); 111 } 112 return _locationId; 113 } 114 115 public Location getLocation() { 116 return _location; 117 } 118 119 public int getSequenceNumber() { 120 return _sequenceNum; 121 } 122 123 public void setSequenceNumber(int sequence) { 124 // property change not needed 125 _sequenceNum = sequence; 126 } 127 128 public int getBlockingOrder() { 129 return _blockingOrder; 130 } 131 132 public void setBlockingOrder(int order) { 133 _blockingOrder = order; 134 } 135 136 public void setComment(String comment) { 137 String old = _comment; 138 _comment = comment; 139 if (!old.equals(_comment)) { 140 setDirtyAndFirePropertyChange("RouteLocationComment", old, comment); // NOI18N 141 } 142 } 143 144 public String getComment() { 145 return _comment; 146 } 147 148 /** 149 * Sets the text color for the route comment 150 * @param color The color of the text 151 */ 152 public void setCommentColor(Color color) { 153 Color old = _commentColor; 154 _commentColor = color; 155 if (!old.equals(_commentColor)) { 156 setDirtyAndFirePropertyChange("RouteLocationCommentColor", old, color); // NOI18N 157 } 158 } 159 160 public Color getCommentColor() { 161 return _commentColor; 162 } 163 164 public String getCommentWithColor() { 165 return TrainCommon.formatColorString(getComment(), getCommentColor()); 166 } 167 168 public void setCommentTextColor(String color) { 169 setCommentColor(ColorUtil.stringToColor(color)); 170 } 171 172 public String getCommentTextColor() { 173 return ColorUtil.colorToColorName(getCommentColor()); 174 } 175 176 public void setTrainDirection(int direction) { 177 int old = _trainDir; 178 _trainDir = direction; 179 if (old != direction) { 180 setDirtyAndFirePropertyChange(TRAIN_DIRECTION_CHANGED_PROPERTY, Integer.toString(old), Integer 181 .toString(direction)); 182 } 183 } 184 185 /** 186 * Gets the binary representation of the train's direction at this location 187 * 188 * @return int representing train direction EAST WEST NORTH SOUTH 189 */ 190 public int getTrainDirection() { 191 return _trainDir; 192 } 193 194 /** 195 * Gets the String representation of the train's direction at this location 196 * 197 * @return String representing train direction at this location 198 */ 199 public String getTrainDirectionString() { 200 return Setup.getDirectionString(getTrainDirection()); 201 } 202 203 public void setMaxTrainLength(int length) { 204 int old = _maxTrainLength; 205 _maxTrainLength = length; 206 if (old != length) { 207 setDirtyAndFirePropertyChange(MAX_LENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length)); // NOI18N 208 } 209 } 210 211 public int getMaxTrainLength() { 212 return _maxTrainLength; 213 } 214 215 /** 216 * Set the train length departing this location when building a train 217 * @param length The train's current length. 218 * 219 */ 220 public void setTrainLength(int length) { 221 int old = _trainLength; 222 _trainLength = length; 223 if (old != length) { 224 firePropertyChange("trainLength", Integer.toString(old), Integer.toString(length)); // NOI18N 225 } 226 } 227 228 public int getTrainLength() { 229 return _trainLength; 230 } 231 232 /** 233 * Set the train weight departing this location when building a train 234 * @param weight The train's current weight. 235 * 236 */ 237 public void setTrainWeight(int weight) { 238 int old = _trainWeight; 239 _trainWeight = weight; 240 if (old != weight) { 241 firePropertyChange("trainWeight", Integer.toString(old), Integer.toString(weight)); // NOI18N 242 } 243 } 244 245 public int getTrainWeight() { 246 return _trainWeight; 247 } 248 249 public void setMaxCarMoves(int moves) { 250 int old = _maxCarMoves; 251 _maxCarMoves = moves; 252 if (old != moves) { 253 setDirtyAndFirePropertyChange(MAX_MOVES_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(moves)); 254 } 255 } 256 257 /** 258 * Get the maximum number of moves for this location 259 * 260 * @return maximum number of moves 261 */ 262 public int getMaxCarMoves() { 263 return _maxCarMoves; 264 } 265 266 public void setRandomControl(String value) { 267 String old = _randomControl; 268 _randomControl = value; 269 if (!old.equals(value)) { 270 setDirtyAndFirePropertyChange("randomControl", old, value); // NOI18N 271 } 272 } 273 274 public String getRandomControl() { 275 return _randomControl; 276 } 277 278 /** 279 * When true allow car drops at this location 280 * 281 * @param drops when true drops allowed at this location 282 */ 283 public void setDropAllowed(boolean drops) { 284 boolean old = _drops; 285 _drops = drops; 286 if (old != drops) { 287 setDirtyAndFirePropertyChange(DROP_CHANGED_PROPERTY, old ? "true" : "false", drops ? "true" : "false"); // NOI18N 288 } 289 } 290 291 public boolean isDropAllowed() { 292 return _drops; 293 } 294 295 /** 296 * When true allow car pick ups at this location 297 * 298 * @param pickups when true pick ups allowed at this location 299 */ 300 public void setPickUpAllowed(boolean pickups) { 301 boolean old = _pickups; 302 _pickups = pickups; 303 if (old != pickups) { 304 setDirtyAndFirePropertyChange(PICKUP_CHANGED_PROPERTY, old ? "true" : "false", pickups ? "true" : "false"); // NOI18N 305 } 306 } 307 308 public boolean isPickUpAllowed() { 309 return _pickups; 310 } 311 312 /** 313 * Set the number of moves completed when building a train 314 * @param moves An integer representing the amount of moves completed. 315 * 316 */ 317 public void setCarMoves(int moves) { 318 int old = _carMoves; 319 _carMoves = moves; 320 if (old != moves) { 321 firePropertyChange("carMoves", Integer.toString(old), Integer.toString(moves)); // NOI18N 322 } 323 } 324 325 public int getCarMoves() { 326 return _carMoves; 327 } 328 329 public void setWait(int time) { 330 int old = _wait; 331 _wait = time; 332 if (old != time) { 333 setDirtyAndFirePropertyChange("waitTime", Integer.toString(old), Integer.toString(time)); // NOI18N 334 } 335 } 336 337 public int getWait() { 338 return _wait; 339 } 340 341 /** 342 * Sets the formated departure time from this location 343 * @param time format hours:minutes 344 */ 345 public void setDepartureTime(String time) { 346 String old = _departureTime; 347 _departureTime = time; 348 if (!old.equals(time)) { 349 setDirtyAndFirePropertyChange(DEPARTURE_TIME_CHANGED_PROPERTY, old, time); 350 } 351 } 352 353 public void setDepartureTime(String hour, String minute) { 354 String old = _departureTime; 355 int h = Integer.parseInt(hour); 356 if (h < 10) { 357 hour = "0" + h; 358 } 359 int m = Integer.parseInt(minute); 360 if (m < 10) { 361 minute = "0" + m; 362 } 363 String time = hour + ":" + minute; 364 _departureTime = time; 365 if (!old.equals(time)) { 366 setDirtyAndFirePropertyChange(DEPARTURE_TIME_CHANGED_PROPERTY, old, time); 367 } 368 } 369 370 public String getDepartureTime() { 371 return _departureTime; 372 } 373 374 public String getDepartureTimeHour() { 375 String[] time = getDepartureTime().split(":"); 376 return time[0]; 377 } 378 379 public String getDepartureTimeMinute() { 380 String[] time = getDepartureTime().split(":"); 381 return time[1]; 382 } 383 384 public String getFormatedDepartureTime() { 385 if (getDepartureTime().equals(NONE) || !Setup.is12hrFormatEnabled()) { 386 return _departureTime; 387 } 388 String AM_PM = " " + Bundle.getMessage("AM"); 389 String[] time = getDepartureTime().split(":"); 390 int hour = Integer.parseInt(time[0]); 391 if (hour >= 12) { 392 AM_PM = " " + Bundle.getMessage("PM"); 393 hour = hour - 12; 394 } 395 if (hour == 0) { 396 hour = 12; 397 } 398 time[0] = Integer.toString(hour); 399 return time[0] + ":" + time[1] + AM_PM; 400 } 401 402 @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "firing property change doesn't matter") 403 public void setGrade(double grade) { 404 double old = _grade; 405 _grade = grade; 406 if (old != grade) { 407 setDirtyAndFirePropertyChange("grade", Double.toString(old), Double.toString(grade)); // NOI18N 408 } 409 } 410 411 public double getGrade() { 412 return _grade; 413 } 414 415 public void setTrainIconX(int x) { 416 int old = _trainIconX; 417 _trainIconX = x; 418 if (old != x) { 419 setDirtyAndFirePropertyChange("trainIconX", Integer.toString(old), Integer.toString(x)); // NOI18N 420 } 421 } 422 423 public int getTrainIconX() { 424 return _trainIconX; 425 } 426 427 public void setTrainIconY(int y) { 428 int old = _trainIconY; 429 _trainIconY = y; 430 if (old != y) { 431 setDirtyAndFirePropertyChange("trainIconY", Integer.toString(old), Integer.toString(y)); // NOI18N 432 } 433 } 434 435 public int getTrainIconY() { 436 return _trainIconY; 437 } 438 439 /** 440 * Gets the X range for detecting the manual movement of a train icon. 441 * @return the range for detection 442 */ 443 public int getTrainIconRangeX() { 444 return getLocation().getTrainIconRangeX(); 445 } 446 447 /** 448 * Gets the Y range for detecting the manual movement of a train icon. 449 * @return the range for detection 450 */ 451 public int getTrainIconRangeY() { 452 return getLocation().getTrainIconRangeY(); 453 } 454 455 /** 456 * Set the train icon panel coordinates to the location defaults. 457 * Coordinates are dependent on the train's departure direction. 458 */ 459 public void setTrainIconCoordinates() { 460 Location l = InstanceManager.getDefault(LocationManager.class).getLocationByName(getName()); 461 if ((getTrainDirection() & Location.EAST) == Location.EAST) { 462 setTrainIconX(l.getTrainIconEast().x); 463 setTrainIconY(l.getTrainIconEast().y); 464 } 465 if ((getTrainDirection() & Location.WEST) == Location.WEST) { 466 setTrainIconX(l.getTrainIconWest().x); 467 setTrainIconY(l.getTrainIconWest().y); 468 } 469 if ((getTrainDirection() & Location.NORTH) == Location.NORTH) { 470 setTrainIconX(l.getTrainIconNorth().x); 471 setTrainIconY(l.getTrainIconNorth().y); 472 } 473 if ((getTrainDirection() & Location.SOUTH) == Location.SOUTH) { 474 setTrainIconX(l.getTrainIconSouth().x); 475 setTrainIconY(l.getTrainIconSouth().y); 476 } 477 } 478 479 public Point getTrainIconCoordinates() { 480 return new Point(getTrainIconX(), getTrainIconY()); 481 } 482 483 public void dispose() { 484 if (_location != null) { 485 _location.removePropertyChangeListener(this); 486 } 487 firePropertyChange(DISPOSE, null, DISPOSE); 488 } 489 490 /** 491 * Construct this Entry from XML. This member has to remain synchronized 492 * with the detailed DTD in operations-config.xml 493 * 494 * @param e Consist XML element 495 */ 496 public RouteLocation(Element e) { 497 Attribute a; 498 if ((a = e.getAttribute(Xml.ID)) != null) { 499 _id = a.getValue(); 500 } else { 501 log.warn("no id attribute in route location element when reading operations"); 502 } 503 if ((a = e.getAttribute(Xml.LOCATION_ID)) != null) { 504 _locationId = a.getValue(); 505 _location = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue()); 506 if (_location != null) { 507 _location.addPropertyChangeListener(this); 508 } 509 } // old way of storing a route location 510 else if ((a = e.getAttribute(Xml.NAME)) != null) { 511 _location = InstanceManager.getDefault(LocationManager.class).getLocationByName(a.getValue()); 512 if (_location != null) { 513 _location.addPropertyChangeListener(this); 514 } 515 // force rewrite of route file 516 InstanceManager.getDefault(RouteManagerXml.class).setDirty(true); 517 } 518 if ((a = e.getAttribute(Xml.TRAIN_DIRECTION)) != null) { 519 // early releases had text for train direction 520 if (Setup.getTrainDirectionList().contains(a.getValue())) { 521 _trainDir = Setup.getDirectionInt(a.getValue()); 522 log.debug("found old train direction {} new direction {}", a.getValue(), _trainDir); 523 } else { 524 try { 525 _trainDir = Integer.parseInt(a.getValue()); 526 } catch (NumberFormatException ee) { 527 log.error("Route location ({}) direction ({}) is unknown", getName(), a.getValue()); 528 } 529 } 530 } 531 if ((a = e.getAttribute(Xml.MAX_TRAIN_LENGTH)) != null) { 532 try { 533 _maxTrainLength = Integer.parseInt(a.getValue()); 534 } catch (NumberFormatException ee) { 535 log.error("Route location ({}) maximum train length ({}) isn't a valid number", getName(), a.getValue()); 536 } 537 } 538 if ((a = e.getAttribute(Xml.GRADE)) != null) { 539 try { 540 _grade = Double.parseDouble(a.getValue()); 541 } catch (NumberFormatException ee) { 542 log.error("Route location ({}) grade ({}) isn't a valid number", getName(), a.getValue()); 543 } 544 } 545 if ((a = e.getAttribute(Xml.MAX_CAR_MOVES)) != null) { 546 try { 547 _maxCarMoves = Integer.parseInt(a.getValue()); 548 } catch (NumberFormatException ee) { 549 log.error("Route location ({}) maximum car moves ({}) isn't a valid number", getName(), a.getValue()); 550 } 551 } 552 if ((a = e.getAttribute(Xml.RANDOM_CONTROL)) != null) { 553 _randomControl = a.getValue(); 554 } 555 if ((a = e.getAttribute(Xml.PICKUPS)) != null) { 556 _pickups = a.getValue().equals(Xml.YES); 557 } 558 if ((a = e.getAttribute(Xml.DROPS)) != null) { 559 _drops = a.getValue().equals(Xml.YES); 560 } 561 if ((a = e.getAttribute(Xml.WAIT)) != null) { 562 try { 563 _wait = Integer.parseInt(a.getValue()); 564 } catch (NumberFormatException ee) { 565 log.error("Route location ({}) wait ({}) isn't a valid number", getName(), a.getValue()); 566 } 567 } 568 if ((a = e.getAttribute(Xml.DEPART_TIME)) != null) { 569 _departureTime = a.getValue(); 570 } 571 if ((a = e.getAttribute(Xml.BLOCKING_ORDER)) != null) { 572 try { 573 _blockingOrder = Integer.parseInt(a.getValue()); 574 } catch (NumberFormatException ee) { 575 log.error("Route location ({}) blocking order ({}) isn't a valid number", getName(), a.getValue()); 576 } 577 } 578 if ((a = e.getAttribute(Xml.TRAIN_ICON_X)) != null) { 579 try { 580 _trainIconX = Integer.parseInt(a.getValue()); 581 } catch (NumberFormatException ee) { 582 log.error("Route location ({}) icon x ({}) isn't a valid number", getName(), a.getValue()); 583 } 584 } 585 if ((a = e.getAttribute(Xml.TRAIN_ICON_Y)) != null) { 586 try { 587 _trainIconY = Integer.parseInt(a.getValue()); 588 } catch (NumberFormatException ee) { 589 log.error("Route location ({}) icon y ({}) isn't a valid number", getName(), a.getValue()); 590 } 591 } 592 if ((a = e.getAttribute(Xml.SEQUENCE_ID)) != null) { 593 try { 594 _sequenceNum = Integer.parseInt(a.getValue()); 595 } catch (NumberFormatException ee) { 596 log.error("Route location ({}) sequence id isn't a valid number {}", getName(), a.getValue()); 597 } 598 } 599 if ((a = e.getAttribute(Xml.COMMENT_COLOR)) != null) { 600 setCommentTextColor(a.getValue()); 601 } 602 603 if ((a = e.getAttribute(Xml.COMMENT)) != null) { 604 _comment = a.getValue(); 605 } 606 } 607 608 /** 609 * Create an XML element to represent this Entry. This member has to remain 610 * synchronized with the detailed DTD in operations-config.xml. 611 * 612 * @return Contents in a JDOM Element 613 */ 614 public Element store() { 615 Element e = new Element(Xml.LOCATION); 616 e.setAttribute(Xml.ID, getId()); 617 e.setAttribute(Xml.NAME, getName()); 618 e.setAttribute(Xml.LOCATION_ID, getNameId()); 619 e.setAttribute(Xml.SEQUENCE_ID, Integer.toString(getSequenceNumber())); 620 e.setAttribute(Xml.TRAIN_DIRECTION, Integer.toString(getTrainDirection())); 621 e.setAttribute(Xml.MAX_TRAIN_LENGTH, Integer.toString(getMaxTrainLength())); 622 e.setAttribute(Xml.GRADE, Double.toString(getGrade())); 623 e.setAttribute(Xml.MAX_CAR_MOVES, Integer.toString(getMaxCarMoves())); 624 e.setAttribute(Xml.RANDOM_CONTROL, getRandomControl()); 625 e.setAttribute(Xml.PICKUPS, isPickUpAllowed() ? Xml.YES : Xml.NO); 626 e.setAttribute(Xml.DROPS, isDropAllowed() ? Xml.YES : Xml.NO); 627 e.setAttribute(Xml.WAIT, Integer.toString(getWait())); 628 e.setAttribute(Xml.DEPART_TIME, getDepartureTime()); 629 e.setAttribute(Xml.BLOCKING_ORDER, Integer.toString(getBlockingOrder())); 630 e.setAttribute(Xml.TRAIN_ICON_X, Integer.toString(getTrainIconX())); 631 e.setAttribute(Xml.TRAIN_ICON_Y, Integer.toString(getTrainIconY())); 632 e.setAttribute(Xml.COMMENT_COLOR, getCommentTextColor()); 633 e.setAttribute(Xml.COMMENT, getComment()); 634 635 return e; 636 } 637 638 @Override 639 public void propertyChange(java.beans.PropertyChangeEvent e) { 640 if (Control.SHOW_PROPERTY) { 641 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e 642 .getNewValue()); 643 } 644 if (e.getPropertyName().equals(Location.DISPOSE_CHANGED_PROPERTY)) { 645 if (_location != null) { 646 _location.removePropertyChangeListener(this); 647 } 648 _location = null; 649 } 650 // forward property name change 651 if (e.getPropertyName().equals(Location.NAME_CHANGED_PROPERTY)) { 652 firePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue()); 653 } 654 } 655 656 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 657 InstanceManager.getDefault(RouteManagerXml.class).setDirty(true); 658 firePropertyChange(p, old, n); 659 } 660 661 private final static Logger log = LoggerFactory.getLogger(RouteLocation.class); 662 663}