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