001package jmri.jmrit.operations.routes; 002 003import java.util.*; 004 005import javax.swing.JComboBox; 006 007import org.jdom2.Attribute; 008import org.jdom2.Element; 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012import jmri.InstanceManager; 013import jmri.beans.PropertyChangeSupport; 014import jmri.jmrit.operations.locations.Location; 015import jmri.jmrit.operations.setup.Control; 016import jmri.jmrit.operations.setup.Setup; 017import jmri.jmrit.operations.trains.Train; 018import jmri.jmrit.operations.trains.TrainManager; 019import jmri.jmrit.operations.trains.trainbuilder.TrainCommon; 020 021/** 022 * Represents a route on the layout 023 * 024 * @author Daniel Boudreau Copyright (C) 2008, 2010 025 */ 026public class Route extends PropertyChangeSupport implements java.beans.PropertyChangeListener { 027 028 public static final String NONE = ""; 029 030 protected String _id = NONE; 031 protected String _name = NONE; 032 protected String _comment = NONE; 033 034 // stores location names for this route 035 protected Hashtable<String, RouteLocation> _routeHashTable = new Hashtable<>(); 036 protected int _IdNumber = 0; // each location in a route gets its own id 037 protected int _sequenceNum = 0; // each location has a unique sequence number 038 039 public static final int EAST = 1; // train direction 040 public static final int WEST = 2; 041 public static final int NORTH = 4; 042 public static final int SOUTH = 8; 043 044 public static final String LISTCHANGE_CHANGED_PROPERTY = "routeListChange"; // NOI18N 045 public static final String ROUTE_STATUS_CHANGED_PROPERTY = "routeStatusChange"; // NOI18N 046 public static final String ROUTE_BLOCKING_CHANGED_PROPERTY = "routeBlockingChange"; // NOI18N 047 public static final String ROUTE_NAME_CHANGED_PROPERTY = "routeNameChange"; // NOI18N 048 public static final String DISPOSE = "routeDispose"; // NOI18N 049 050 public static final String OKAY = Bundle.getMessage("ButtonOK"); 051 public static final String TRAIN_BUILT = Bundle.getMessage("TrainBuilt"); 052 public static final String ORPHAN = Bundle.getMessage("Orphan"); 053 public static final String ERROR = Bundle.getMessage("ErrorTitle"); 054 055 public static final int START = 1; // add location at start of route 056 057 public Route(String id, String name) { 058 log.debug("New route ({}) id: {}", name, id); 059 _name = name; 060 _id = id; 061 } 062 063 public String getId() { 064 return _id; 065 } 066 067 public void setName(String name) { 068 String old = _name; 069 _name = name; 070 if (!old.equals(name)) { 071 setDirtyAndFirePropertyChange(ROUTE_NAME_CHANGED_PROPERTY, old, name); // NOI18N 072 } 073 } 074 075 // for combo boxes 076 @Override 077 public String toString() { 078 return _name; 079 } 080 081 public String getName() { 082 return _name; 083 } 084 085 public void setComment(String comment) { 086 String old = _comment; 087 _comment = comment; 088 if (!old.equals(comment)) { 089 setDirtyAndFirePropertyChange("commentChange", old, comment); // NOI18N 090 } 091 } 092 093 public String getComment() { 094 return _comment; 095 } 096 097 public void dispose() { 098 removeTrainListeners(); 099 setDirtyAndFirePropertyChange(DISPOSE, null, DISPOSE); 100 } 101 102 /** 103 * Adds a location to the end of this route 104 * 105 * @param location The Location. 106 * 107 * @return RouteLocation created for the location added 108 */ 109 public RouteLocation addLocation(Location location) { 110 _IdNumber++; 111 _sequenceNum++; 112 String id = _id + "r" + Integer.toString(_IdNumber); 113 log.debug("adding new location to ({}) id: {}", getName(), id); 114 RouteLocation rl = new RouteLocation(id, location); 115 rl.setSequenceNumber(_sequenceNum); 116 Integer old = Integer.valueOf(_routeHashTable.size()); 117 _routeHashTable.put(rl.getId(), rl); 118 119 resetBlockingOrder(); 120 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_routeHashTable.size())); 121 // listen for drop and pick up changes to forward 122 rl.addPropertyChangeListener(this); 123 return rl; 124 } 125 126 /** 127 * Add a location at a specific place (sequence) in the route Allowable sequence 128 * numbers are 1 to max size of route. 1 = start of route, or Route.START 129 * 130 * @param location The Location to add. 131 * @param sequence Where in the route to add the location. 132 * 133 * @return route location 134 */ 135 public RouteLocation addLocation(Location location, int sequence) { 136 RouteLocation rl = addLocation(location); 137 if (sequence < START || sequence > _routeHashTable.size()) { 138 return rl; 139 } 140 for (int i = 0; i < _routeHashTable.size() - sequence; i++) { 141 moveLocationUp(rl); 142 } 143 return rl; 144 } 145 146 /** 147 * Remember a NamedBean Object created outside the manager. 148 * 149 * @param rl The RouteLocation to add to this route. 150 */ 151 public void register(RouteLocation rl) { 152 Integer old = Integer.valueOf(_routeHashTable.size()); 153 _routeHashTable.put(rl.getId(), rl); 154 155 // find last id created 156 String[] getId = rl.getId().split("r"); 157 int id = Integer.parseInt(getId[1]); 158 if (id > _IdNumber) { 159 _IdNumber = id; 160 } 161 // find and save the highest sequence number 162 if (rl.getSequenceNumber() > _sequenceNum) { 163 _sequenceNum = rl.getSequenceNumber(); 164 } 165 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_routeHashTable.size())); 166 // listen for drop and pick up changes to forward 167 rl.addPropertyChangeListener(this); 168 } 169 170 /** 171 * Delete a RouteLocation 172 * 173 * @param rl The RouteLocation to remove from the route. 174 * 175 */ 176 public void deleteLocation(RouteLocation rl) { 177 if (rl != null) { 178 rl.removePropertyChangeListener(this); 179 String id = rl.getId(); 180 rl.dispose(); 181 Integer old = Integer.valueOf(_routeHashTable.size()); 182 _routeHashTable.remove(id); 183 resequence(); 184 resetBlockingOrder(); 185 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_routeHashTable.size())); 186 } 187 } 188 189 public int size() { 190 return _routeHashTable.size(); 191 } 192 193 /** 194 * Reorder the location sequence numbers for this route 195 */ 196 private void resequence() { 197 List<RouteLocation> routeList = getLocationsBySequenceList(); 198 for (int i = 0; i < routeList.size(); i++) { 199 _sequenceNum = i + START; // start sequence numbers at 1 200 routeList.get(i).setSequenceNumber(_sequenceNum); 201 } 202 } 203 204 /** 205 * Get the first location in a route 206 * 207 * @return the first route location 208 */ 209 public RouteLocation getDepartsRouteLocation() { 210 List<RouteLocation> list = getLocationsBySequenceList(); 211 if (list.size() > 0) { 212 return list.get(0); 213 } 214 return null; 215 } 216 217 public String getDepartureDirection() { 218 if (getDepartsRouteLocation() != null) { 219 return getDepartsRouteLocation().getTrainDirectionString(); 220 } 221 return NONE; 222 } 223 224 /** 225 * Get the last location in a route 226 * 227 * @return the last route location 228 */ 229 public RouteLocation getTerminatesRouteLocation() { 230 List<RouteLocation> list = getLocationsBySequenceList(); 231 if (list.size() > 0) { 232 return list.get(list.size() - 1); 233 } 234 return null; 235 } 236 237 /** 238 * Gets the next route location in a route 239 * 240 * @param rl the current route location 241 * @return the next route location, null if rl is the last location in a route. 242 */ 243 public RouteLocation getNextRouteLocation(RouteLocation rl) { 244 List<RouteLocation> list = getLocationsBySequenceList(); 245 for (int i = 0; i < list.size() - 1; i++) { 246 if (rl == list.get(i)) { 247 return list.get(i + 1); 248 } 249 } 250 return null; 251 } 252 253 /** 254 * Get location by name (gets last route location with name) 255 * 256 * @param name The string location name. 257 * 258 * @return route location 259 */ 260 public RouteLocation getLastLocationByName(String name) { 261 List<RouteLocation> routeList = getLocationsBySequenceList(); 262 RouteLocation rl; 263 264 for (int i = routeList.size() - 1; i >= 0; i--) { 265 rl = routeList.get(i); 266 if (rl.getName().equals(name)) { 267 return rl; 268 } 269 } 270 return null; 271 } 272 273 /** 274 * Used to determine if a "similar" location name is in the route. Note that 275 * a similar name might not actually be part of the route. 276 * 277 * @param name the name of the location 278 * @return true if a "similar" name was found 279 */ 280 public boolean isLocationNameInRoute(String name) { 281 for (RouteLocation rl : getLocationsBySequenceList()) { 282 if (rl.getSplitName().equals(TrainCommon.splitString(name))) { 283 return true; 284 } 285 } 286 return false; 287 } 288 289 /** 290 * Get a RouteLocation by id 291 * 292 * @param id The string id. 293 * 294 * @return route location 295 */ 296 public RouteLocation getRouteLocationById(String id) { 297 return _routeHashTable.get(id); 298 } 299 300 private List<RouteLocation> getLocationsByIdList() { 301 List<RouteLocation> out = new ArrayList<>(); 302 Enumeration<RouteLocation> en = _routeHashTable.elements(); 303 while (en.hasMoreElements()) { 304 out.add(en.nextElement()); 305 } 306 return out; 307 } 308 309 /** 310 * Get a list of RouteLocations sorted by route order 311 * 312 * @return list of RouteLocations ordered by sequence 313 */ 314 public List<RouteLocation> getLocationsBySequenceList() { 315 // now re-sort 316 List<RouteLocation> out = new ArrayList<>(); 317 for (RouteLocation rl : getLocationsByIdList()) { 318 for (int j = 0; j < out.size(); j++) { 319 if (rl.getSequenceNumber() < out.get(j).getSequenceNumber()) { 320 out.add(j, rl); 321 break; 322 } 323 } 324 if (!out.contains(rl)) { 325 out.add(rl); 326 } 327 } 328 return out; 329 } 330 331 public List<RouteLocation> getBlockingOrder() { 332 // now re-sort 333 List<RouteLocation> out = new ArrayList<>(); 334 for (RouteLocation rl : getLocationsBySequenceList()) { 335 if (rl.getBlockingOrder() == 0) { 336 rl.setBlockingOrder(out.size() + 1); 337 } 338 for (int j = 0; j < out.size(); j++) { 339 if (rl.getBlockingOrder() < out.get(j).getBlockingOrder()) { 340 out.add(j, rl); 341 break; 342 } 343 } 344 if (!out.contains(rl)) { 345 out.add(rl); 346 } 347 } 348 return out; 349 } 350 351 public RouteLocation getBlockingLocationFrontOfTrain() { 352 List<RouteLocation> list = getBlockingOrder(); 353 if (list.size() > 0) { 354 return list.get(0); 355 } 356 return null; 357 } 358 359 public RouteLocation getBlockingLocationRearOfTrain() { 360 List<RouteLocation> list = getBlockingOrder(); 361 if (list.size() > 0) { 362 return list.get(list.size() - 1); 363 } 364 return null; 365 } 366 367 public void setBlockingOrderUp(RouteLocation rl) { 368 List<RouteLocation> blockingOrder = getBlockingOrder(); 369 int order = rl.getBlockingOrder(); 370 if (--order < 1) { 371 order = size(); 372 for (RouteLocation rlx : blockingOrder) { 373 rlx.setBlockingOrder(rlx.getBlockingOrder() - 1); 374 } 375 } else { 376 RouteLocation rlx = blockingOrder.get(order - 1); 377 rlx.setBlockingOrder(order + 1); 378 } 379 rl.setBlockingOrder(order); 380 setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, order + 1, order); 381 } 382 383 public void setBlockingOrderDown(RouteLocation rl) { 384 List<RouteLocation> blockingOrder = getBlockingOrder(); 385 int order = rl.getBlockingOrder(); 386 if (++order > size()) { 387 order = 1; 388 for (RouteLocation rlx : blockingOrder) { 389 rlx.setBlockingOrder(rlx.getBlockingOrder() + 1); 390 } 391 } else { 392 RouteLocation rlx = blockingOrder.get(order - 1); 393 rlx.setBlockingOrder(order - 1); 394 } 395 rl.setBlockingOrder(order); 396 setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, order - 1, order); 397 } 398 399 public void resetBlockingOrder() { 400 for (RouteLocation rl : getLocationsByIdList()) { 401 rl.setBlockingOrder(0); 402 } 403 setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, "Order", "Reset"); 404 } 405 406 /** 407 * Places a RouteLocation earlier in the route. 408 * 409 * @param rl The RouteLocation to move. 410 * 411 */ 412 public void moveLocationUp(RouteLocation rl) { 413 int sequenceNum = rl.getSequenceNumber(); 414 if (sequenceNum - 1 <= 0) { 415 rl.setSequenceNumber(_sequenceNum + 1); // move to the end of the list 416 resequence(); 417 } else { 418 // adjust the other item taken by this one 419 RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum - 1); 420 if (replaceRl != null) { 421 replaceRl.setSequenceNumber(sequenceNum); 422 rl.setSequenceNumber(sequenceNum - 1); 423 } else { 424 resequence(); // error the sequence number is missing 425 } 426 } 427 resetBlockingOrder(); 428 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum)); 429 } 430 431 /** 432 * Moves a RouteLocation later in the route. 433 * 434 * @param rl The RouteLocation to move. 435 * 436 */ 437 public void moveLocationDown(RouteLocation rl) { 438 int sequenceNum = rl.getSequenceNumber(); 439 if (sequenceNum + 1 > _sequenceNum) { 440 rl.setSequenceNumber(0); // move to the start of the list 441 resequence(); 442 } else { 443 // adjust the other item taken by this one 444 RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum + 1); 445 if (replaceRl != null) { 446 replaceRl.setSequenceNumber(sequenceNum); 447 rl.setSequenceNumber(sequenceNum + 1); 448 } else { 449 resequence(); // error the sequence number is missing 450 } 451 } 452 resetBlockingOrder(); 453 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum)); 454 } 455 456 /** 457 * 1st RouteLocation in a route starts at 1. 458 * 459 * @param sequence selects which RouteLocation is to be returned 460 * @return RouteLocation selected 461 */ 462 public RouteLocation getRouteLocationBySequenceNumber(int sequence) { 463 for (RouteLocation rl : getLocationsByIdList()) { 464 if (rl.getSequenceNumber() == sequence) { 465 return rl; 466 } 467 } 468 return null; 469 } 470 471 /** 472 * Gets the status of the route: OKAY ORPHAN ERROR TRAIN_BUILT 473 * 474 * @return string with status of route. 475 */ 476 public String getStatus() { 477 removeTrainListeners(); 478 addTrainListeners(); // and add them right back in 479 List<RouteLocation> routeList = getLocationsByIdList(); 480 if (routeList.size() == 0) { 481 return ERROR; 482 } 483 List<String> directions = Setup.getTrainDirectionList(); 484 for (RouteLocation rl : routeList) { 485 if (rl.getName().equals(RouteLocation.DELETED)) { 486 return ERROR; 487 } 488 // did user eliminate the train direction for this route location? 489 if (!directions.contains(rl.getTrainDirectionString())) { 490 return ERROR; 491 } 492 } 493 // check to see if this route is used by a train that is built 494 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) { 495 if (train.getRoute() == this && train.isBuilt()) { 496 return TRAIN_BUILT; 497 } 498 } 499 // check to see if this route is used by a train 500 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) { 501 if (train.getRoute() == this) { 502 return OKAY; 503 } 504 } 505 return ORPHAN; 506 } 507 508 private void addTrainListeners() { 509 for (Train train : InstanceManager.getDefault(TrainManager.class).getList()) { 510 if (train.getRoute() == this) { 511 train.addPropertyChangeListener(this); 512 } 513 } 514 } 515 516 private void removeTrainListeners() { 517 for (Train train : InstanceManager.getDefault(TrainManager.class).getList()) { 518 train.removePropertyChangeListener(this); 519 } 520 } 521 522 /** 523 * Gets the shortest train length specified in the route. 524 * 525 * @return the minimum scale train length for this route. 526 */ 527 public int getRouteMinimumTrainLength() { 528 int min = getRouteMaximumTrainLength(); 529 for (RouteLocation rl : getLocationsByIdList()) { 530 if (rl.getMaxTrainLength() < min) 531 min = rl.getMaxTrainLength(); 532 } 533 return min; 534 } 535 536 /** 537 * Gets the longest train length specified in the route. 538 * 539 * @return the maximum scale train length for this route. 540 */ 541 public int getRouteMaximumTrainLength() { 542 int max = 0; 543 for (RouteLocation rl : getLocationsByIdList()) { 544 if (rl.getMaxTrainLength() > max) 545 max = rl.getMaxTrainLength(); 546 } 547 return max; 548 } 549 550 public JComboBox<RouteLocation> getComboBox() { 551 JComboBox<RouteLocation> box = new JComboBox<>(); 552 for (RouteLocation rl : getLocationsBySequenceList()) { 553 box.addItem(rl); 554 } 555 return box; 556 } 557 558 public void updateComboBox(JComboBox<RouteLocation> box) { 559 box.removeAllItems(); 560 box.addItem(null); 561 for (RouteLocation rl : getLocationsBySequenceList()) { 562 box.addItem(rl); 563 } 564 } 565 566 /** 567 * Construct this Entry from XML. This member has to remain synchronized with 568 * the detailed DTD in operations-config.xml 569 * 570 * @param e Consist XML element 571 */ 572 public Route(Element e) { 573 Attribute a; 574 if ((a = e.getAttribute(Xml.ID)) != null) { 575 _id = a.getValue(); 576 } else { 577 log.warn("no id attribute in route element when reading operations"); 578 } 579 if ((a = e.getAttribute(Xml.NAME)) != null) { 580 _name = a.getValue(); 581 } 582 if ((a = e.getAttribute(Xml.COMMENT)) != null) { 583 _comment = a.getValue(); 584 } 585 if (e.getChildren(Xml.LOCATION) != null) { 586 List<Element> eRouteLocations = e.getChildren(Xml.LOCATION); 587 log.debug("route: ({}) has {} locations", getName(), eRouteLocations.size()); 588 for (Element eRouteLocation : eRouteLocations) { 589 register(new RouteLocation(eRouteLocation)); 590 } 591 } 592 } 593 594 /** 595 * Create an XML element to represent this Entry. This member has to remain 596 * synchronized with the detailed DTD in operations-config.xml. 597 * 598 * @return Contents in a JDOM Element 599 */ 600 public Element store() { 601 Element e = new Element(Xml.ROUTE); 602 e.setAttribute(Xml.ID, getId()); 603 e.setAttribute(Xml.NAME, getName()); 604 e.setAttribute(Xml.COMMENT, getComment()); 605 for (RouteLocation rl : getLocationsBySequenceList()) { 606 e.addContent(rl.store()); 607 } 608 return e; 609 } 610 611 @Override 612 public void propertyChange(java.beans.PropertyChangeEvent e) { 613 if (Control.SHOW_PROPERTY) { 614 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), 615 e.getNewValue()); 616 } 617 // forward drops, pick ups, local moves, train direction, max moves, and max length as a list 618 // change 619 if (e.getPropertyName().equals(RouteLocation.DROP_CHANGED_PROPERTY) || 620 e.getPropertyName().equals(RouteLocation.PICKUP_CHANGED_PROPERTY) || 621 e.getPropertyName().equals(RouteLocation.LOCAL_MOVES_CHANGED_PROPERTY) || 622 e.getPropertyName().equals(RouteLocation.TRAIN_DIRECTION_CHANGED_PROPERTY) || 623 e.getPropertyName().equals(RouteLocation.MAX_MOVES_CHANGED_PROPERTY) || 624 e.getPropertyName().equals(RouteLocation.MAX_LENGTH_CHANGED_PROPERTY)) { 625 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, "RouteLocation"); // NOI18N 626 } 627 if (e.getPropertyName().equals(Train.BUILT_CHANGED_PROPERTY)) { 628 firePropertyChange(ROUTE_STATUS_CHANGED_PROPERTY, true, false); 629 } 630 } 631 632 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 633 InstanceManager.getDefault(RouteManagerXml.class).setDirty(true); 634 firePropertyChange(p, old, n); 635 } 636 637 private final static Logger log = LoggerFactory.getLogger(Route.class); 638 639}