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