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 void setBlockingOrderUp(RouteLocation rl) { 352 List<RouteLocation> blockingOrder = getBlockingOrder(); 353 int order = rl.getBlockingOrder(); 354 if (--order < 1) { 355 order = size(); 356 for (RouteLocation rlx : blockingOrder) { 357 rlx.setBlockingOrder(rlx.getBlockingOrder() - 1); 358 } 359 } else { 360 RouteLocation rlx = blockingOrder.get(order - 1); 361 rlx.setBlockingOrder(order + 1); 362 } 363 rl.setBlockingOrder(order); 364 setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, order + 1, order); 365 } 366 367 public void setBlockingOrderDown(RouteLocation rl) { 368 List<RouteLocation> blockingOrder = getBlockingOrder(); 369 int order = rl.getBlockingOrder(); 370 if (++order > size()) { 371 order = 1; 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 resetBlockingOrder() { 384 for (RouteLocation rl : getLocationsByIdList()) { 385 rl.setBlockingOrder(0); 386 } 387 setDirtyAndFirePropertyChange(ROUTE_BLOCKING_CHANGED_PROPERTY, "Order", "Reset"); 388 } 389 390 /** 391 * Places a RouteLocation earlier in the route. 392 * 393 * @param rl The RouteLocation to move. 394 * 395 */ 396 public void moveLocationUp(RouteLocation rl) { 397 int sequenceNum = rl.getSequenceNumber(); 398 if (sequenceNum - 1 <= 0) { 399 rl.setSequenceNumber(_sequenceNum + 1); // move to the end of the list 400 resequence(); 401 } else { 402 // adjust the other item taken by this one 403 RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum - 1); 404 if (replaceRl != null) { 405 replaceRl.setSequenceNumber(sequenceNum); 406 rl.setSequenceNumber(sequenceNum - 1); 407 } else { 408 resequence(); // error the sequence number is missing 409 } 410 } 411 resetBlockingOrder(); 412 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum)); 413 } 414 415 /** 416 * Moves a RouteLocation later in the route. 417 * 418 * @param rl The RouteLocation to move. 419 * 420 */ 421 public void moveLocationDown(RouteLocation rl) { 422 int sequenceNum = rl.getSequenceNumber(); 423 if (sequenceNum + 1 > _sequenceNum) { 424 rl.setSequenceNumber(0); // move to the start of the list 425 resequence(); 426 } else { 427 // adjust the other item taken by this one 428 RouteLocation replaceRl = getRouteLocationBySequenceNumber(sequenceNum + 1); 429 if (replaceRl != null) { 430 replaceRl.setSequenceNumber(sequenceNum); 431 rl.setSequenceNumber(sequenceNum + 1); 432 } else { 433 resequence(); // error the sequence number is missing 434 } 435 } 436 resetBlockingOrder(); 437 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceNum)); 438 } 439 440 /** 441 * 1st RouteLocation in a route starts at 1. 442 * 443 * @param sequence selects which RouteLocation is to be returned 444 * @return RouteLocation selected 445 */ 446 public RouteLocation getRouteLocationBySequenceNumber(int sequence) { 447 for (RouteLocation rl : getLocationsByIdList()) { 448 if (rl.getSequenceNumber() == sequence) { 449 return rl; 450 } 451 } 452 return null; 453 } 454 455 /** 456 * Gets the status of the route: OKAY ORPHAN ERROR TRAIN_BUILT 457 * 458 * @return string with status of route. 459 */ 460 public String getStatus() { 461 removeTrainListeners(); 462 addTrainListeners(); // and add them right back in 463 List<RouteLocation> routeList = getLocationsByIdList(); 464 if (routeList.size() == 0) { 465 return ERROR; 466 } 467 List<String> directions = Setup.getTrainDirectionList(); 468 for (RouteLocation rl : routeList) { 469 if (rl.getName().equals(RouteLocation.DELETED)) { 470 return ERROR; 471 } 472 // did user eliminate the train direction for this route location? 473 if (!directions.contains(rl.getTrainDirectionString())) { 474 return ERROR; 475 } 476 } 477 // check to see if this route is used by a train that is built 478 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) { 479 if (train.getRoute() == this && train.isBuilt()) { 480 return TRAIN_BUILT; 481 } 482 } 483 // check to see if this route is used by a train 484 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) { 485 if (train.getRoute() == this) { 486 return OKAY; 487 } 488 } 489 return ORPHAN; 490 } 491 492 private void addTrainListeners() { 493 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) { 494 if (train.getRoute() == this) { 495 train.addPropertyChangeListener(this); 496 } 497 } 498 } 499 500 private void removeTrainListeners() { 501 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByIdList()) { 502 train.removePropertyChangeListener(this); 503 } 504 } 505 506 /** 507 * Gets the shortest train length specified in the route. 508 * 509 * @return the minimum scale train length for this route. 510 */ 511 public int getRouteMinimumTrainLength() { 512 int min = getRouteMaximumTrainLength(); 513 for (RouteLocation rl : getLocationsByIdList()) { 514 if (rl.getMaxTrainLength() < min) 515 min = rl.getMaxTrainLength(); 516 } 517 return min; 518 } 519 520 /** 521 * Gets the longest train length specified in the route. 522 * 523 * @return the maximum scale train length for this route. 524 */ 525 public int getRouteMaximumTrainLength() { 526 int max = 0; 527 for (RouteLocation rl : getLocationsByIdList()) { 528 if (rl.getMaxTrainLength() > max) 529 max = rl.getMaxTrainLength(); 530 } 531 return max; 532 } 533 534 public JComboBox<RouteLocation> getComboBox() { 535 JComboBox<RouteLocation> box = new JComboBox<>(); 536 for (RouteLocation rl : getLocationsBySequenceList()) { 537 box.addItem(rl); 538 } 539 return box; 540 } 541 542 public void updateComboBox(JComboBox<RouteLocation> box) { 543 box.removeAllItems(); 544 box.addItem(null); 545 for (RouteLocation rl : getLocationsBySequenceList()) { 546 box.addItem(rl); 547 } 548 } 549 550 /** 551 * Construct this Entry from XML. This member has to remain synchronized with 552 * the detailed DTD in operations-config.xml 553 * 554 * @param e Consist XML element 555 */ 556 public Route(Element e) { 557 Attribute a; 558 if ((a = e.getAttribute(Xml.ID)) != null) { 559 _id = a.getValue(); 560 } else { 561 log.warn("no id attribute in route element when reading operations"); 562 } 563 if ((a = e.getAttribute(Xml.NAME)) != null) { 564 _name = a.getValue(); 565 } 566 if ((a = e.getAttribute(Xml.COMMENT)) != null) { 567 _comment = a.getValue(); 568 } 569 if (e.getChildren(Xml.LOCATION) != null) { 570 List<Element> eRouteLocations = e.getChildren(Xml.LOCATION); 571 log.debug("route: ({}) has {} locations", getName(), eRouteLocations.size()); 572 for (Element eRouteLocation : eRouteLocations) { 573 register(new RouteLocation(eRouteLocation)); 574 } 575 } 576 } 577 578 /** 579 * Create an XML element to represent this Entry. This member has to remain 580 * synchronized with the detailed DTD in operations-config.xml. 581 * 582 * @return Contents in a JDOM Element 583 */ 584 public Element store() { 585 Element e = new Element(Xml.ROUTE); 586 e.setAttribute(Xml.ID, getId()); 587 e.setAttribute(Xml.NAME, getName()); 588 e.setAttribute(Xml.COMMENT, getComment()); 589 for (RouteLocation rl : getLocationsBySequenceList()) { 590 e.addContent(rl.store()); 591 } 592 return e; 593 } 594 595 @Override 596 public void propertyChange(java.beans.PropertyChangeEvent e) { 597 if (Control.SHOW_PROPERTY) { 598 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), 599 e.getNewValue()); 600 } 601 // forward drops, pick ups, local moves, train direction, max moves, and max length as a list 602 // change 603 if (e.getPropertyName().equals(RouteLocation.DROP_CHANGED_PROPERTY) || 604 e.getPropertyName().equals(RouteLocation.PICKUP_CHANGED_PROPERTY) || 605 e.getPropertyName().equals(RouteLocation.LOCAL_MOVES_CHANGED_PROPERTY) || 606 e.getPropertyName().equals(RouteLocation.TRAIN_DIRECTION_CHANGED_PROPERTY) || 607 e.getPropertyName().equals(RouteLocation.MAX_MOVES_CHANGED_PROPERTY) || 608 e.getPropertyName().equals(RouteLocation.MAX_LENGTH_CHANGED_PROPERTY)) { 609 setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, "RouteLocation"); // NOI18N 610 } 611 if (e.getPropertyName().equals(Train.BUILT_CHANGED_PROPERTY)) { 612 firePropertyChange(ROUTE_STATUS_CHANGED_PROPERTY, true, false); 613 } 614 } 615 616 protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) { 617 InstanceManager.getDefault(RouteManagerXml.class).setDirty(true); 618 firePropertyChange(p, old, n); 619 } 620 621 private final static Logger log = LoggerFactory.getLogger(Route.class); 622 623}