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