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