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