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 if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 190 printHorizontalLine(fileOut, !IS_MANIFEST); 191 } 192 } 193 194 // done with work, now print summary for this location if we're done 195 if (rl != train.getTrainTerminatesRouteLocation()) { 196 RouteLocation nextRl = train.getRoute().getNextRouteLocation(rl); 197 if (rl.getSplitName().equals(nextRl.getSplitName())) { 198 continue; // the current location name is the "same" as the next 199 } 200 // print departure text if not a switcher 201 if (!train.isLocalSwitcher() && !trainDone) { 202 departureMessages(fileOut, train, rl); 203 } 204 } 205 } 206 // report if no pick ups or set outs or train has left 207 trainSummaryMessages(fileOut, train, location, trainDone, stops); 208 } 209 210 // now report car movement by tracks at location 211 reportByTrack(fileOut, location); 212 213 } catch (IllegalArgumentException e) { 214 newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument", 215 Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage())); 216 newLine(fileOut, messageFormatText); 217 log.error("Illegal argument", e); 218 } 219 220 // Are there any cars that need to be found? 221 addCarsLocationUnknown(fileOut, !IS_MANIFEST); 222 fileOut.flush(); 223 fileOut.close(); 224 location.setStatus(Location.UPDATED); 225 } 226 227 private String getValid() { 228 String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(), 229 new Object[]{getDate(true)}); 230 if (Setup.isPrintTrainScheduleNameEnabled()) { 231 TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule(); 232 if (sch != null) { 233 valid = valid + " (" + sch.getName() + ")"; 234 } 235 } 236 return valid; 237 } 238 239 /* 240 * Messages for the switch list when the train first arrives 241 */ 242 private void firstTimeMessages(PrintWriter fileOut, Train train, RouteLocation rl) { 243 newLine(fileOut); 244 newLine(fileOut, 245 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringScheduledWork(), 246 new Object[]{train.getName(), train.getDescription()})); 247 newLine(fileOut, getSwitchListTrainStatus(train, rl)); 248 } 249 250 /* 251 * Messages when a train services the location two or more times 252 */ 253 private void multipleVisitMessages(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious, 254 int stops) { 255 String expectedArrivalTime = train.getExpectedArrivalTime(rl); 256 if (rlPrevious == null || 257 !rl.getSplitName().equals(rlPrevious.getSplitName())) { 258 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_PER_VISIT)) { 259 fileOut.write(FORM_FEED); 260 } 261 newLine(fileOut); 262 if (train.isTrainEnRoute()) { 263 if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) { 264 // Visit number {0} for train ({1}) 265 newLine(fileOut, 266 MessageFormat.format( 267 messageFormatText = TrainSwitchListText.getStringVisitNumberDone(), 268 new Object[]{stops, train.getName(), train.getDescription()})); 269 } else if (rl != train.getTrainTerminatesRouteLocation()) { 270 // Visit number {0} for train ({1}) expect to arrive in {2}, arrives {3}bound 271 newLine(fileOut, MessageFormat.format( 272 messageFormatText = TrainSwitchListText.getStringVisitNumberDeparted(), 273 new Object[]{stops, train.getName(), expectedArrivalTime, 274 rl.getTrainDirectionString(), train.getDescription()})); 275 } else { 276 // Visit number {0} for train ({1}) expect to arrive in {2}, terminates {3} 277 newLine(fileOut, 278 MessageFormat.format( 279 messageFormatText = TrainSwitchListText 280 .getStringVisitNumberTerminatesDeparted(), 281 new Object[]{stops, train.getName(), expectedArrivalTime, 282 rl.getSplitName(), train.getDescription()})); 283 } 284 } else { 285 // train hasn't departed 286 if (rl != train.getTrainTerminatesRouteLocation()) { 287 // Visit number {0} for train ({1}) expected arrival {2}, arrives {3}bound 288 newLine(fileOut, 289 MessageFormat.format( 290 messageFormatText = TrainSwitchListText.getStringVisitNumber(), 291 new Object[]{stops, train.getName(), expectedArrivalTime, 292 rl.getTrainDirectionString(), 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.getName(), 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.getName(), 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.getName()}); 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.getName()}); 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.getName(), train.getDescription(), location.getSplitName()})); 358 } else { 359 if (stops > 1 && !_pickupCars) { 360 // Default message: No car pick ups for train ({0}) at this location 361 newLine(fileOut, 362 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(), 363 new Object[]{train.getName(), train.getDescription(), 364 location.getSplitName()})); 365 } 366 if (stops > 1 && !_dropCars) { 367 // Default message: No car set outs for train ({0}) at this location 368 newLine(fileOut, 369 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(), 370 new Object[]{train.getName(), train.getDescription(), 371 location.getSplitName()})); 372 } 373 } 374 } 375 376 private void reportByTrack(PrintWriter fileOut, Location location) { 377 if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) { 378 clearUtilityCarTypes(); // list utility cars by quantity 379 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 380 newLine(fileOut); 381 newLine(fileOut); 382 } else { 383 fileOut.write(FORM_FEED); 384 } 385 newLine(fileOut, 386 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(), 387 new Object[]{location.getSplitName()})); 388 389 // we only need the cars delivered to or at this location 390 List<Car> rsList = carManager.getByTrainList(); 391 List<Car> carList = new ArrayList<>(); 392 for (Car rs : rsList) { 393 if ((rs.getLocation() != null && 394 rs.getLocation().getSplitName().equals(location.getSplitName())) || 395 (rs.getDestination() != null && 396 rs.getSplitDestinationName().equals(location.getSplitName()))) 397 carList.add(rs); 398 } 399 400 List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list 401 // track names once 402 for (Location loc : locationManager.getLocationsByNameList()) { 403 if (!loc.getSplitName().equals(location.getSplitName())) 404 continue; 405 for (Track track : loc.getTracksByBlockingOrderList(null)) { 406 String trackName = track.getSplitName(); 407 if (trackNames.contains(trackName)) 408 continue; 409 trackNames.add(trackName); 410 411 String trainName = ""; // for printing train message once 412 newLine(fileOut); 413 newLine(fileOut, trackName); // print out just the track name 414 // now show the cars pickup and holds for this track 415 for (Car car : carList) { 416 if (!car.getSplitTrackName().equals(trackName)) { 417 continue; 418 } 419 // is the car scheduled for pickup? 420 if (car.getRouteLocation() != null) { 421 if (car.getRouteLocation().getLocation().getSplitName() 422 .equals(location.getSplitName())) { 423 // cars are sorted by train name, print train message once 424 if (!trainName.equals(car.getTrainName())) { 425 trainName = car.getTrainName(); 426 newLine(fileOut, MessageFormat.format( 427 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 428 new Object[]{car.getTrainName(), car.getTrain().getDescription()})); 429 printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 430 } 431 if (car.isUtility()) { 432 pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 433 } else { 434 pickUpCar(fileOut, car, !IS_MANIFEST); 435 } 436 } 437 // car holds 438 } else if (car.isUtility()) { 439 String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 440 if (s != null) { 441 newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N 442 } 443 } else { 444 newLine(fileOut, 445 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(), 446 new Object[]{ 447 padAndTruncateIfNeeded(car.getRoadName(), 448 InstanceManager.getDefault(CarRoads.class) 449 .getMaxNameLength()), 450 padAndTruncateIfNeeded( 451 TrainCommon.splitString(car.getNumber()), 452 Control.max_len_string_print_road_number), 453 padAndTruncateIfNeeded( 454 car.getTypeName().split(TrainCommon.HYPHEN)[0], 455 InstanceManager.getDefault(CarTypes.class) 456 .getMaxNameLength()), 457 padAndTruncateIfNeeded( 458 car.getLength() + Setup.getLengthUnitAbv(), 459 Control.max_len_string_length_name), 460 padAndTruncateIfNeeded(car.getLoadName(), 461 InstanceManager.getDefault(CarLoads.class) 462 .getMaxNameLength()), 463 padAndTruncateIfNeeded(trackName, 464 locationManager.getMaxTrackNameLength()), 465 padAndTruncateIfNeeded(car.getColor(), InstanceManager 466 .getDefault(CarColors.class).getMaxNameLength())})); 467 } 468 } 469 // now do set outs at this location 470 for (Car car : carList) { 471 if (!car.getSplitDestinationTrackName().equals(trackName)) { 472 continue; 473 } 474 if (car.getRouteDestination() != null && 475 car.getRouteDestination().getLocation().getSplitName() 476 .equals(location.getSplitName())) { 477 // cars are sorted by train name, print train message once 478 if (!trainName.equals(car.getTrainName())) { 479 trainName = car.getTrainName(); 480 newLine(fileOut, MessageFormat.format( 481 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 482 new Object[]{car.getTrainName(), car.getTrain().getDescription()})); 483 printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 484 } 485 if (car.isUtility()) { 486 setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 487 } else { 488 dropCar(fileOut, car, !IS_MANIFEST); 489 } 490 } 491 } 492 } 493 } 494 } 495 } 496 497 public void printSwitchList(Location location, boolean isPreview) { 498 File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName()); 499 if (!switchListFile.exists()) { 500 log.warn("Switch list file missing for location ({})", location.getName()); 501 return; 502 } 503 if (isPreview && Setup.isManifestEditorEnabled()) { 504 TrainUtilities.openDesktop(switchListFile); 505 } else { 506 TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false, 507 FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(), 508 Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(), 509 Setup.getPrintDuplexSides()); 510 } 511 if (!isPreview) { 512 location.setStatus(Location.PRINTED); 513 location.setSwitchListState(Location.SW_PRINTED); 514 } 515 } 516 517 protected void newLine(PrintWriter file, String string) { 518 if (!string.isEmpty()) { 519 newLine(file, string, !IS_MANIFEST); 520 } 521 } 522 523 private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class); 524}