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, 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); 093 for (Train train : trains) { 094 if (!Setup.isSwitchListRealTime() && train.getSwitchListStatus().equals(Train.PRINTED)) { 095 continue; // already printed this train 096 } 097 Route route = train.getRoute(); 098 // TODO throw exception? only built trains should be in the list, so no route is 099 // an error 100 if (route == null) { 101 continue; // no route for this train 102 } // determine if train works this location 103 boolean works = isThereWorkAtLocation(train, location); 104 if (!works && !Setup.isSwitchListAllTrainsEnabled()) { 105 log.debug("No work for train ({}) at location ({})", train.getName(), location.getName()); 106 continue; 107 } 108 // we're now going to add to the switch list 109 if (checkFormFeed) { 110 if (append && !Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 111 fileOut.write(FORM_FEED); 112 } 113 if (Setup.isPrintValidEnabled()) { 114 newLine(fileOut, getValid()); 115 } 116 } else if (!Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 117 fileOut.write(FORM_FEED); 118 } 119 checkFormFeed = false; // done with FF for this train 120 // some cars booleans and the number of times this location get's serviced 121 _pickupCars = false; // when true there was a car pick up 122 _dropCars = false; // when true there was a car set out 123 int stops = 1; 124 boolean trainDone = false; 125 // get engine and car lists 126 List<Engine> engineList = engineManager.getByTrainBlockingList(train); 127 List<Car> carList = carManager.getByTrainDestinationList(train); 128 List<RouteLocation> routeList = route.getLocationsBySequenceList(); 129 RouteLocation rlPrevious = null; 130 // does the train stop once or more at this location? 131 for (RouteLocation rl : routeList) { 132 if (!rl.getSplitName().equals(location.getSplitName())) { 133 rlPrevious = rl; 134 continue; 135 } 136 if (train.getExpectedArrivalTime(rl).equals(Train.ALREADY_SERVICED)) { 137 trainDone = true; 138 } 139 // first time at this location? 140 if (stops == 1) { 141 firstTimeMessages(fileOut, train, rl); 142 stops++; 143 } else { 144 // multiple visits to this location 145 // Print visit number only if previous location isn't the same 146 if (rlPrevious == null || 147 !rl.getSplitName().equals(rlPrevious.getSplitName())) { 148 multipleVisitMessages(fileOut, train, rl, rlPrevious, stops); 149 stops++; 150 } else { 151 // don't bump stop count, same location 152 // Does the train reverse direction? 153 reverseDirectionMessage(fileOut, train, rl, rlPrevious); 154 } 155 } 156 157 // save current location in case there's back to back location with the same name 158 rlPrevious = rl; 159 160 // add route location comment 161 if (Setup.isSwitchListRouteLocationCommentEnabled() && !rl.getComment().trim().isEmpty()) { 162 newLine(fileOut, rl.getCommentWithColor()); 163 } 164 165 printTrackComments(fileOut, rl, carList, !IS_MANIFEST); 166 167 if (isThereWorkAtLocation(carList, engineList, rl)) { 168 // now print out the work for this location 169 if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 170 pickupEngines(fileOut, engineList, rl, !IS_MANIFEST); 171 // if switcher show loco drop at end of list 172 if (train.isLocalSwitcher()) { 173 blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 174 dropEngines(fileOut, engineList, rl, !IS_MANIFEST); 175 } else { 176 dropEngines(fileOut, engineList, rl, !IS_MANIFEST); 177 blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 178 } 179 } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) { 180 blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST); 181 blockCarsTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 182 } else { 183 blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST); 184 blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 185 } 186 // print horizontal line if there was work and enabled 187 if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 188 printHorizontalLine(fileOut, !IS_MANIFEST); 189 } 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.getName(), 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.getName(), train.getDescription()})); 267 } else if (rl != train.getTrainTerminatesRouteLocation()) { 268 // Visit number {0} for train ({1}) expect to arrive in {2}, arrives {3}bound 269 newLine(fileOut, MessageFormat.format( 270 messageFormatText = TrainSwitchListText.getStringVisitNumberDeparted(), 271 new Object[]{stops, train.getName(), expectedArrivalTime, 272 rl.getTrainDirectionString(), train.getDescription()})); 273 } else { 274 // Visit number {0} for train ({1}) expect to arrive in {2}, terminates {3} 275 newLine(fileOut, 276 MessageFormat.format( 277 messageFormatText = TrainSwitchListText 278 .getStringVisitNumberTerminatesDeparted(), 279 new Object[]{stops, train.getName(), expectedArrivalTime, 280 rl.getSplitName(), train.getDescription()})); 281 } 282 } else { 283 // train hasn't departed 284 if (rl != train.getTrainTerminatesRouteLocation()) { 285 // Visit number {0} for train ({1}) expected arrival {2}, arrives {3}bound 286 newLine(fileOut, 287 MessageFormat.format( 288 messageFormatText = TrainSwitchListText.getStringVisitNumber(), 289 new Object[]{stops, train.getName(), expectedArrivalTime, 290 rl.getTrainDirectionString(), train.getDescription()})); 291 if (Setup.isUseSwitchListDepartureTimeEnabled()) { 292 // Departs {0} {1}bound at {2} 293 newLine(fileOut, MessageFormat.format( 294 messageFormatText = TrainSwitchListText.getStringDepartsAt(), 295 new Object[]{splitString(rl.getName()), 296 rl.getTrainDirectionString(), 297 train.getExpectedDepartureTime(rl)})); 298 } 299 } else { 300 // Visit number {0} for train ({1}) expected arrival {2}, terminates {3} 301 newLine(fileOut, MessageFormat.format( 302 messageFormatText = TrainSwitchListText.getStringVisitNumberTerminates(), 303 new Object[]{stops, train.getName(), expectedArrivalTime, 304 rl.getSplitName(), train.getDescription()})); 305 } 306 } 307 } 308 } 309 310 private void reverseDirectionMessage(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious) { 311 // Does the train reverse direction? 312 if (rl.getTrainDirection() != rlPrevious.getTrainDirection() && 313 !TrainSwitchListText.getStringTrainDirectionChange().isEmpty()) { 314 // Train ({0}) direction change, departs {1}bound 315 newLine(fileOut, 316 MessageFormat.format( 317 messageFormatText = TrainSwitchListText.getStringTrainDirectionChange(), 318 new Object[]{train.getName(), rl.getTrainDirectionString(), 319 train.getDescription(), train.getTrainTerminatesName()})); 320 } 321 } 322 323 /* 324 * Train departure messages at the end of the switch list 325 */ 326 private void departureMessages(PrintWriter fileOut, Train train, RouteLocation rl) { 327 String trainDeparts = ""; 328 if (Setup.isPrintLoadsAndEmptiesEnabled()) { 329 int emptyCars = train.getNumberEmptyCarsInTrain(rl); 330 // Train departs {0} {1}bound with {2} loads, {3} empties, {4} {5}, {6} tons 331 trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsLoads(), 332 new Object[]{rl.getSplitName(), 333 rl.getTrainDirectionString(), 334 train.getNumberCarsInTrain(rl) - emptyCars, emptyCars, 335 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 336 train.getTrainWeight(rl), train.getTrainTerminatesName(), 337 train.getName()}); 338 } else { 339 // Train departs {0} {1}bound with {2} cars, {3} {4}, {5} tons 340 trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsCars(), 341 new Object[]{rl.getSplitName(), 342 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl), 343 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 344 train.getTrainWeight(rl), train.getTrainTerminatesName(), 345 train.getName()}); 346 } 347 newLine(fileOut, trainDeparts); 348 } 349 350 private void trainSummaryMessages(PrintWriter fileOut, Train train, Location location, boolean trainDone, 351 int stops) { 352 if (trainDone && !_pickupCars && !_dropCars) { 353 // Default message: Train ({0}) has serviced this location 354 newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringTrainDone(), 355 new Object[]{train.getName(), train.getDescription(), location.getSplitName()})); 356 } else { 357 if (stops > 1 && !_pickupCars) { 358 // Default message: No car pick ups for train ({0}) at this location 359 newLine(fileOut, 360 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(), 361 new Object[]{train.getName(), train.getDescription(), 362 location.getSplitName()})); 363 } 364 if (stops > 1 && !_dropCars) { 365 // Default message: No car set outs for train ({0}) at this location 366 newLine(fileOut, 367 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(), 368 new Object[]{train.getName(), train.getDescription(), 369 location.getSplitName()})); 370 } 371 } 372 } 373 374 private void reportByTrack(PrintWriter fileOut, Location location) { 375 if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) { 376 clearUtilityCarTypes(); // list utility cars by quantity 377 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 378 newLine(fileOut); 379 newLine(fileOut); 380 } else { 381 fileOut.write(FORM_FEED); 382 } 383 newLine(fileOut, 384 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(), 385 new Object[]{location.getSplitName()})); 386 387 // we only need the cars delivered to or at this location 388 List<Car> rsList = carManager.getByTrainList(); 389 List<Car> carList = new ArrayList<>(); 390 for (Car rs : rsList) { 391 if ((rs.getLocation() != null && 392 rs.getLocation().getSplitName().equals(location.getSplitName())) || 393 (rs.getDestination() != null && 394 rs.getSplitDestinationName().equals(location.getSplitName()))) 395 carList.add(rs); 396 } 397 398 List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list 399 // track names once 400 for (Location loc : locationManager.getLocationsByNameList()) { 401 if (!loc.getSplitName().equals(location.getSplitName())) 402 continue; 403 for (Track track : loc.getTracksByBlockingOrderList(null)) { 404 String trackName = track.getSplitName(); 405 if (trackNames.contains(trackName)) 406 continue; 407 trackNames.add(trackName); 408 409 String trainName = ""; // for printing train message once 410 newLine(fileOut); 411 newLine(fileOut, trackName); // print out just the track name 412 // now show the cars pickup and holds for this track 413 for (Car car : carList) { 414 if (!car.getSplitTrackName().equals(trackName)) { 415 continue; 416 } 417 // is the car scheduled for pickup? 418 if (car.getRouteLocation() != null) { 419 if (car.getRouteLocation().getLocation().getSplitName() 420 .equals(location.getSplitName())) { 421 // cars are sorted by train name, print train message once 422 if (!trainName.equals(car.getTrainName())) { 423 trainName = car.getTrainName(); 424 newLine(fileOut, MessageFormat.format( 425 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 426 new Object[]{car.getTrainName(), car.getTrain().getDescription()})); 427 printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 428 } 429 if (car.isUtility()) { 430 pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 431 } else { 432 pickUpCar(fileOut, car, !IS_MANIFEST); 433 } 434 } 435 // car holds 436 } else if (car.isUtility()) { 437 String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 438 if (s != null) { 439 newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N 440 } 441 } else { 442 newLine(fileOut, 443 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(), 444 new Object[]{ 445 padAndTruncateIfNeeded(car.getRoadName(), 446 InstanceManager.getDefault(CarRoads.class) 447 .getMaxNameLength()), 448 padAndTruncateIfNeeded( 449 TrainCommon.splitString(car.getNumber()), 450 Control.max_len_string_print_road_number), 451 padAndTruncateIfNeeded( 452 car.getTypeName().split(TrainCommon.HYPHEN)[0], 453 InstanceManager.getDefault(CarTypes.class) 454 .getMaxNameLength()), 455 padAndTruncateIfNeeded( 456 car.getLength() + Setup.getLengthUnitAbv(), 457 Control.max_len_string_length_name), 458 padAndTruncateIfNeeded(car.getLoadName(), 459 InstanceManager.getDefault(CarLoads.class) 460 .getMaxNameLength()), 461 padAndTruncateIfNeeded(trackName, 462 locationManager.getMaxTrackNameLength()), 463 padAndTruncateIfNeeded(car.getColor(), InstanceManager 464 .getDefault(CarColors.class).getMaxNameLength())})); 465 } 466 } 467 // now do set outs at this location 468 for (Car car : carList) { 469 if (!car.getSplitDestinationTrackName().equals(trackName)) { 470 continue; 471 } 472 if (car.getRouteDestination() != null && 473 car.getRouteDestination().getLocation().getSplitName() 474 .equals(location.getSplitName())) { 475 // cars are sorted by train name, print train message once 476 if (!trainName.equals(car.getTrainName())) { 477 trainName = car.getTrainName(); 478 newLine(fileOut, MessageFormat.format( 479 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 480 new Object[]{car.getTrainName(), car.getTrain().getDescription()})); 481 printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 482 } 483 if (car.isUtility()) { 484 setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 485 } else { 486 dropCar(fileOut, car, !IS_MANIFEST); 487 } 488 } 489 } 490 } 491 } 492 } 493 } 494 495 public void printSwitchList(Location location, boolean isPreview) { 496 File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName()); 497 if (!switchListFile.exists()) { 498 log.warn("Switch list file missing for location ({})", location.getName()); 499 return; 500 } 501 if (isPreview && Setup.isManifestEditorEnabled()) { 502 TrainUtilities.openDesktop(switchListFile); 503 } else { 504 TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false, 505 FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(), 506 Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(), 507 Setup.getPrintDuplexSides()); 508 } 509 if (!isPreview) { 510 location.setStatus(Location.PRINTED); 511 location.setSwitchListState(Location.SW_PRINTED); 512 } 513 } 514 515 protected void newLine(PrintWriter file, String string) { 516 if (!string.isEmpty()) { 517 newLine(file, string, !IS_MANIFEST); 518 } 519 } 520 521 private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class); 522}