001package jmri.jmrit.operations.trains; 002 003import java.io.*; 004import java.nio.charset.StandardCharsets; 005import java.text.MessageFormat; 006import java.util.ArrayList; 007import java.util.List; 008 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012import jmri.InstanceManager; 013import jmri.jmrit.operations.locations.Location; 014import jmri.jmrit.operations.locations.Track; 015import jmri.jmrit.operations.rollingstock.cars.*; 016import jmri.jmrit.operations.rollingstock.engines.Engine; 017import jmri.jmrit.operations.routes.Route; 018import jmri.jmrit.operations.routes.RouteLocation; 019import jmri.jmrit.operations.setup.Control; 020import jmri.jmrit.operations.setup.Setup; 021import jmri.jmrit.operations.trains.schedules.TrainSchedule; 022import jmri.jmrit.operations.trains.schedules.TrainScheduleManager; 023import jmri.util.FileUtil; 024 025/** 026 * Builds a switch list for a location on the railroad 027 * 028 * @author Daniel Boudreau (C) Copyright 2008, 2011, 2012, 2013, 2015 029 * 030 * 031 */ 032public class TrainSwitchLists extends TrainCommon { 033 034 TrainManager trainManager = InstanceManager.getDefault(TrainManager.class); 035 private static final char FORM_FEED = '\f'; 036 private static final boolean IS_PRINT_HEADER = true; 037 038 String messageFormatText = ""; // the text being formated in case there's an exception 039 040 /** 041 * Builds a switch list for a location showing the work by train arrival time. 042 * If not running in real time, new train work is appended to the end of the 043 * file. User has the ability to modify the text of the messages which can cause 044 * an IllegalArgumentException. Some messages have more arguments than the 045 * default message allowing the user to customize the message to their liking. 046 * 047 * There also an option to list all of the car work by track name. This option 048 * is only available in real time and is shown after the switch list by train. 049 * 050 * @param location The Location needing a switch list 051 */ 052 public void buildSwitchList(Location location) { 053 054 boolean append = false; // add text to end of file when true 055 boolean checkFormFeed = true; // used to determine if FF needed between trains 056 057 // Append switch list data if not operating in real time 058 if (!Setup.isSwitchListRealTime()) { 059 if (!location.getStatus().equals(Location.MODIFIED) && !Setup.isSwitchListAllTrainsEnabled()) { 060 return; // nothing to add 061 } 062 append = location.getSwitchListState() == Location.SW_APPEND; 063 location.setSwitchListState(Location.SW_APPEND); 064 } 065 066 log.debug("Append: {} for location ({})", append, location.getName()); 067 068 // create switch list file 069 File file = InstanceManager.getDefault(TrainManagerXml.class).createSwitchListFile(location.getName()); 070 071 PrintWriter fileOut = null; 072 try { 073 fileOut = new PrintWriter(new BufferedWriter( 074 new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8)), true); 075 } catch (IOException e) { 076 log.error("Can not open switchlist file: {}", file.getName()); 077 return; 078 } 079 try { 080 // build header 081 if (!append) { 082 newLine(fileOut, Setup.getRailroadName()); 083 newLine(fileOut); 084 newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListFor(), 085 new Object[] { splitString(location.getName()) })); 086 if (!location.getSwitchListCommentWithColor().isEmpty()) { 087 newLine(fileOut, location.getSwitchListCommentWithColor()); 088 } 089 } else { 090 newLine(fileOut); 091 } 092 093 String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(), 094 new Object[] { getDate(true) }); 095 if (Setup.isPrintTrainScheduleNameEnabled()) { 096 TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule(); 097 if (sch != null) { 098 valid = valid + " (" + sch.getName() + ")"; 099 } 100 } 101 102 // get a list of built trains sorted by arrival time 103 List<Train> trains = trainManager.getTrainsArrivingThisLocationList(location); 104 for (Train train : trains) { 105 if (!Setup.isSwitchListRealTime() && train.getSwitchListStatus().equals(Train.PRINTED)) { 106 continue; // already printed this train 107 } 108 Route route = train.getRoute(); 109 // TODO throw exception? only built trains should be in the list, so no route is 110 // an error 111 if (route == null) { 112 continue; // no route for this train 113 } // determine if train works this location 114 boolean works = isThereWorkAtLocation(train, location); 115 if (!works && !Setup.isSwitchListAllTrainsEnabled()) { 116 log.debug("No work for train ({}) at location ({})", train.getName(), location.getName()); 117 continue; 118 } 119 // we're now going to add to the switch list 120 if (checkFormFeed) { 121 if (append && !Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 122 fileOut.write(FORM_FEED); 123 } 124 if (Setup.isPrintValidEnabled()) { 125 newLine(fileOut, valid); 126 } 127 } else if (!Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 128 fileOut.write(FORM_FEED); 129 } 130 checkFormFeed = false; // done with FF for this train 131 // some cars booleans and the number of times this location get's serviced 132 _pickupCars = false; // when true there was a car pick up 133 _dropCars = false; // when true there was a car set out 134 int stops = 1; 135 boolean trainDone = false; 136 // get engine and car lists 137 List<Engine> engineList = engineManager.getByTrainBlockingList(train); 138 List<Car> carList = carManager.getByTrainDestinationList(train); 139 List<RouteLocation> routeList = route.getLocationsBySequenceList(); 140 RouteLocation rlPrevious = null; 141 // does the train stop once or more at this location? 142 for (RouteLocation rl : routeList) { 143 if (!splitString(rl.getName()).equals(splitString(location.getName()))) { 144 rlPrevious = rl; 145 continue; 146 } 147 String expectedArrivalTime = train.getExpectedArrivalTime(rl); 148 if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) { 149 trainDone = true; 150 } 151 // first time at this location? 152 if (stops == 1) { 153 newLine(fileOut); 154 newLine(fileOut, 155 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringScheduledWork(), 156 new Object[] { train.getName(), train.getDescription() })); 157 if (train.isTrainEnRoute()) { 158 if (!trainDone) { 159 newLine(fileOut, 160 MessageFormat.format( 161 messageFormatText = TrainSwitchListText.getStringDepartedExpected(), 162 new Object[] { splitString(train.getTrainDepartsName()), 163 expectedArrivalTime, rl.getTrainDirectionString() })); 164 } 165 } else if (!train.isLocalSwitcher()) { 166 if (rl == train.getTrainDepartsRouteLocation()) { 167 newLine(fileOut, 168 MessageFormat.format( 169 messageFormatText = TrainSwitchListText.getStringDepartsAt(), 170 new Object[] { splitString(train.getTrainDepartsName()), 171 rl.getTrainDirectionString(), 172 train.getFormatedDepartureTime() })); 173 } else { 174 newLine(fileOut, MessageFormat.format( 175 messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(), 176 new Object[] { splitString(train.getTrainDepartsName()), 177 train.getFormatedDepartureTime(), expectedArrivalTime, 178 rl.getTrainDirectionString() })); 179 } 180 } 181 } else { 182 // multiple visits to this location 183 // Print visit number only if previous location wasn't the same 184 if (rlPrevious == null || 185 !splitString(rl.getName()).equals(splitString(rlPrevious.getName()))) { 186 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_PER_VISIT)) { 187 fileOut.write(FORM_FEED); 188 } 189 newLine(fileOut); 190 if (train.isTrainEnRoute()) { 191 if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) { 192 newLine(fileOut, 193 MessageFormat.format( 194 messageFormatText = TrainSwitchListText.getStringVisitNumberDone(), 195 new Object[] { stops, train.getName(), train.getDescription() })); 196 } else if (rl != train.getTrainTerminatesRouteLocation()) { 197 newLine(fileOut, MessageFormat.format( 198 messageFormatText = TrainSwitchListText.getStringVisitNumberDeparted(), 199 new Object[] { stops, train.getName(), expectedArrivalTime, 200 rl.getTrainDirectionString(), train.getDescription() })); 201 } else { 202 // message: Visit number {0} for train ({1}) expect to arrive in {2}, terminates 203 // {3} 204 newLine(fileOut, 205 MessageFormat.format( 206 messageFormatText = TrainSwitchListText 207 .getStringVisitNumberTerminatesDeparted(), 208 new Object[] { stops, train.getName(), expectedArrivalTime, 209 splitString(rl.getName()), train.getDescription() })); 210 } 211 } else { 212 // train hasn't departed 213 if (rl != train.getTrainTerminatesRouteLocation()) { 214 newLine(fileOut, 215 MessageFormat.format( 216 messageFormatText = TrainSwitchListText.getStringVisitNumber(), 217 new Object[] { stops, train.getName(), expectedArrivalTime, 218 rl.getTrainDirectionString(), train.getDescription() })); 219 } else { 220 newLine(fileOut, MessageFormat.format( 221 messageFormatText = TrainSwitchListText.getStringVisitNumberTerminates(), 222 new Object[] { stops, train.getName(), expectedArrivalTime, 223 splitString(rl.getName()), train.getDescription() })); 224 } 225 } 226 } else { 227 stops--; // don't bump stop count, same location 228 // Does the train reverse direction? 229 if (rl.getTrainDirection() != rlPrevious.getTrainDirection() && 230 !TrainSwitchListText.getStringTrainDirectionChange().isEmpty()) { 231 newLine(fileOut, 232 MessageFormat.format( 233 messageFormatText = TrainSwitchListText.getStringTrainDirectionChange(), 234 new Object[] { train.getName(), rl.getTrainDirectionString(), 235 train.getDescription(), train.getTrainTerminatesName() })); 236 } 237 } 238 } 239 240 rlPrevious = rl; // save current location in case there's back to back location with the same 241 // name 242 243 // add route comment 244 if (Setup.isSwitchListRouteLocationCommentEnabled() && !rl.getComment().trim().isEmpty()) { 245 newLine(fileOut, rl.getFormatedColorComment()); 246 } 247 248 printTrackComments(fileOut, rl, carList, !IS_MANIFEST); 249 250 // now print out the work for this location 251 if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 252 pickupEngines(fileOut, engineList, rl, !IS_MANIFEST); 253 // if switcher show loco drop at end of list 254 if (train.isLocalSwitcher()) { 255 blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 256 dropEngines(fileOut, engineList, rl, !IS_MANIFEST); 257 } else { 258 dropEngines(fileOut, engineList, rl, !IS_MANIFEST); 259 blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 260 } 261 } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) { 262 blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST); 263 blockCarsTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 264 } else { 265 blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST); 266 blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 267 } 268 if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 269 printHorizontalLine(fileOut, !IS_MANIFEST); 270 } 271 272 stops++; 273 274 // done with work, now print summary for this location if we're done 275 if (rl != train.getTrainTerminatesRouteLocation()) { 276 RouteLocation nextRl = train.getRoute().getNextRouteLocation(rl); 277 if (splitString(rl.getName()).equals(splitString(nextRl.getName()))) { 278 continue; // the current location name is the "same" as the next 279 } 280 // print departure text if not a switcher 281 if (!train.isLocalSwitcher() && !trainDone) { 282 String trainDeparts = ""; 283 if (Setup.isPrintLoadsAndEmptiesEnabled()) { 284 int emptyCars = train.getNumberEmptyCarsInTrain(rl); 285 // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 286 // feet, 3000 tons 287 trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsLoads(), 288 new Object[] { TrainCommon.splitString(rl.getName()), 289 rl.getTrainDirectionString(), 290 train.getNumberCarsInTrain(rl) - emptyCars, emptyCars, 291 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 292 train.getTrainWeight(rl), train.getTrainTerminatesName(), 293 train.getName() }); 294 } else { 295 // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 296 // tons 297 trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsCars(), 298 new Object[] { TrainCommon.splitString(rl.getName()), 299 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl), 300 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 301 train.getTrainWeight(rl), train.getTrainTerminatesName(), 302 train.getName() }); 303 } 304 newLine(fileOut, trainDeparts); 305 } 306 } 307 } 308 if (trainDone && !_pickupCars && !_dropCars) { 309 // Default message: Train ({0}) has serviced this location 310 newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringTrainDone(), 311 new Object[] { train.getName(), train.getDescription(), splitString(location.getName()) })); 312 } else { 313 if (stops > 1 && !_pickupCars) { 314 // Default message: No car pick ups for train ({0}) at this location 315 newLine(fileOut, 316 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(), 317 new Object[] { train.getName(), train.getDescription(), 318 splitString(location.getName()) })); 319 } 320 if (stops > 1 && !_dropCars) { 321 // Default message: No car set outs for train ({0}) at this location 322 newLine(fileOut, 323 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(), 324 new Object[] { train.getName(), train.getDescription(), 325 splitString(location.getName()) })); 326 } 327 } 328 } 329 330 // now report car movement by tracks at location 331 if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) { 332 clearUtilityCarTypes(); // list utility cars by quantity 333 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 334 newLine(fileOut); 335 newLine(fileOut); 336 } else { 337 fileOut.write(FORM_FEED); 338 } 339 newLine(fileOut, 340 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(), 341 new Object[] { splitString(location.getName()) })); 342 343 // we only need the cars delivered to or at this location 344 List<Car> rsList = carManager.getByTrainList(); 345 List<Car> carList = new ArrayList<>(); 346 for (Car rs : rsList) { 347 if ((rs.getLocation() != null && 348 splitString(rs.getLocation().getName()).equals(splitString(location.getName()))) || 349 (rs.getDestination() != null && 350 splitString(rs.getDestination().getName()).equals(splitString(location.getName())))) 351 carList.add(rs); 352 } 353 354 List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list 355 // track names once 356 for (Location loc : locationManager.getLocationsByNameList()) { 357 if (!splitString(loc.getName()).equals(splitString(location.getName()))) 358 continue; 359 for (Track track : loc.getTracksByNameList(null)) { 360 String trackName = splitString(track.getName()); 361 if (trackNames.contains(trackName)) 362 continue; 363 trackNames.add(trackName); 364 365 String trainName = ""; // for printing train message once 366 newLine(fileOut); 367 newLine(fileOut, trackName); // print out just the track name 368 // now show the cars pickup and holds for this track 369 for (Car car : carList) { 370 if (!splitString(car.getTrackName()).equals(trackName)) { 371 continue; 372 } 373 // is the car scheduled for pickup? 374 if (car.getRouteLocation() != null) { 375 if (splitString(car.getRouteLocation().getLocation().getName()) 376 .equals(splitString(location.getName()))) { 377 // cars are sorted by train name, print train message once 378 if (!trainName.equals(car.getTrainName())) { 379 trainName = car.getTrainName(); 380 newLine(fileOut, MessageFormat.format( 381 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 382 new Object[] { car.getTrainName(), car.getTrain().getDescription() })); 383 printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 384 } 385 if (car.isUtility()) { 386 pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 387 } else { 388 pickUpCar(fileOut, car, !IS_MANIFEST); 389 } 390 } 391 // car holds 392 } else if (car.isUtility()) { 393 String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 394 if (s != null) { 395 newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N 396 } 397 } else { 398 newLine(fileOut, 399 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(), 400 new Object[] { 401 padAndTruncateIfNeeded(car.getRoadName(), 402 InstanceManager.getDefault(CarRoads.class) 403 .getMaxNameLength()), 404 padAndTruncateIfNeeded( 405 TrainCommon.splitString(car.getNumber()), 406 Control.max_len_string_print_road_number), 407 padAndTruncateIfNeeded(car.getTypeName().split(TrainCommon.HYPHEN)[0], 408 InstanceManager.getDefault(CarTypes.class) 409 .getMaxNameLength()), 410 padAndTruncateIfNeeded( 411 car.getLength() + Setup.getLengthUnitAbv(), 412 Control.max_len_string_length_name), 413 padAndTruncateIfNeeded(car.getLoadName(), 414 InstanceManager.getDefault(CarLoads.class) 415 .getMaxNameLength()), 416 padAndTruncateIfNeeded(trackName, 417 locationManager.getMaxTrackNameLength()), 418 padAndTruncateIfNeeded(car.getColor(), InstanceManager 419 .getDefault(CarColors.class).getMaxNameLength()) })); 420 } 421 } 422 // now do set outs at this location 423 for (Car car : carList) { 424 if (!splitString(car.getDestinationTrackName()).equals(trackName)) { 425 continue; 426 } 427 if (car.getRouteDestination() != null && 428 splitString(car.getRouteDestination().getLocation().getName()) 429 .equals(splitString(location.getName()))) { 430 // cars are sorted by train name, print train message once 431 if (!trainName.equals(car.getTrainName())) { 432 trainName = car.getTrainName(); 433 newLine(fileOut, MessageFormat.format( 434 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 435 new Object[] { car.getTrainName(), car.getTrain().getDescription() })); 436 printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 437 } 438 if (car.isUtility()) { 439 setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 440 } else { 441 dropCar(fileOut, car, !IS_MANIFEST); 442 } 443 } 444 } 445 } 446 } 447 } 448 449 } catch (IllegalArgumentException e) { 450 newLine(fileOut, MessageFormat.format(Bundle.getMessage("ErrorIllegalArgument"), 451 new Object[] { Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage() })); 452 newLine(fileOut, messageFormatText); 453 log.error("Illegal argument", e); 454 } 455 456 // Are there any cars that need to be found? 457 addCarsLocationUnknown(fileOut, !IS_MANIFEST); 458 fileOut.flush(); 459 fileOut.close(); 460 location.setStatus(Location.UPDATED); 461 } 462 463 public void printSwitchList(Location location, boolean isPreview) { 464 File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName()); 465 if (!switchListFile.exists()) { 466 log.warn("Switch list file missing for location ({})", location.getName()); 467 return; 468 } 469 if (isPreview && Setup.isManifestEditorEnabled()) { 470 TrainUtilities.openDesktop(switchListFile); 471 } else { 472 TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false, 473 FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(), 474 Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled()); 475 } 476 if (!isPreview) { 477 location.setStatus(Location.PRINTED); 478 location.setSwitchListState(Location.SW_PRINTED); 479 } 480 } 481 482 protected void newLine(PrintWriter file, String string) { 483 if (!string.isEmpty()) { 484 newLine(file, string, !IS_MANIFEST); 485 } 486 } 487 488 private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class); 489}