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