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 train.getCurrentRouteLocation() != rl) { 138 trainDone = true; 139 } 140 // first time at this location? 141 if (stops == 1) { 142 firstTimeMessages(fileOut, train, rl); 143 stops++; 144 } else { 145 // multiple visits to this location 146 // Print visit number only if previous location isn't the same 147 if (rlPrevious == null || 148 !rl.getSplitName().equals(rlPrevious.getSplitName())) { 149 multipleVisitMessages(fileOut, train, rl, rlPrevious, stops); 150 stops++; 151 } else { 152 // don't bump stop count, same location 153 // Does the train reverse direction? 154 reverseDirectionMessage(fileOut, train, rl, rlPrevious); 155 } 156 } 157 158 // save current location in case there's back to back location with the same name 159 rlPrevious = rl; 160 161 // add route location comment 162 if (Setup.isSwitchListRouteLocationCommentEnabled() && !rl.getComment().trim().isEmpty()) { 163 newLine(fileOut, rl.getCommentWithColor()); 164 } 165 166 printTrackComments(fileOut, rl, carList, !IS_MANIFEST); 167 168 if (isThereWorkAtLocation(carList, engineList, rl)) { 169 // now print out the work for this location 170 if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 171 pickupEngines(fileOut, engineList, rl, !IS_MANIFEST); 172 // if switcher show loco drop at end of list 173 if (train.isLocalSwitcher()) { 174 blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 175 dropEngines(fileOut, engineList, rl, !IS_MANIFEST); 176 } else { 177 dropEngines(fileOut, engineList, rl, !IS_MANIFEST); 178 blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 179 } 180 } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) { 181 blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST); 182 blockCarsTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 183 } else { 184 blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST); 185 blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST); 186 } 187 // print horizontal line if there was work and enabled 188 if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 189 printHorizontalLine(fileOut, !IS_MANIFEST); 190 } 191 } 192 193 // done with work, now print summary for this location if we're done 194 if (rl != train.getTrainTerminatesRouteLocation()) { 195 RouteLocation nextRl = train.getRoute().getNextRouteLocation(rl); 196 if (rl.getSplitName().equals(nextRl.getSplitName())) { 197 continue; // the current location name is the "same" as the next 198 } 199 // print departure text if not a switcher 200 if (!train.isLocalSwitcher() && !trainDone) { 201 departureMessages(fileOut, train, rl); 202 } 203 } 204 } 205 // report if no pick ups or set outs or train has left 206 trainSummaryMessages(fileOut, train, location, trainDone, stops); 207 } 208 209 // now report car movement by tracks at location 210 reportByTrack(fileOut, location); 211 212 } catch (IllegalArgumentException e) { 213 newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument", 214 Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage())); 215 newLine(fileOut, messageFormatText); 216 log.error("Illegal argument", e); 217 } 218 219 // Are there any cars that need to be found? 220 addCarsLocationUnknown(fileOut, !IS_MANIFEST); 221 fileOut.flush(); 222 fileOut.close(); 223 location.setStatus(Location.UPDATED); 224 } 225 226 private String getValid() { 227 String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(), 228 new Object[]{getDate(true)}); 229 if (Setup.isPrintTrainScheduleNameEnabled()) { 230 TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule(); 231 if (sch != null) { 232 valid = valid + " (" + sch.getName() + ")"; 233 } 234 } 235 return valid; 236 } 237 238 /* 239 * Messages for the switch list when the train first arrives 240 */ 241 private void firstTimeMessages(PrintWriter fileOut, Train train, RouteLocation rl) { 242 newLine(fileOut); 243 newLine(fileOut, 244 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringScheduledWork(), 245 new Object[]{train.getName(), train.getDescription()})); 246 newLine(fileOut, getSwitchListTrainStatus(train, rl)); 247 } 248 249 /* 250 * Messages when a train services the location two or more times 251 */ 252 private void multipleVisitMessages(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious, 253 int stops) { 254 String expectedArrivalTime = train.getExpectedArrivalTime(rl); 255 if (rlPrevious == null || 256 !rl.getSplitName().equals(rlPrevious.getSplitName())) { 257 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_PER_VISIT)) { 258 fileOut.write(FORM_FEED); 259 } 260 newLine(fileOut); 261 if (train.isTrainEnRoute()) { 262 if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) { 263 // Visit number {0} for train ({1}) 264 newLine(fileOut, 265 MessageFormat.format( 266 messageFormatText = TrainSwitchListText.getStringVisitNumberDone(), 267 new Object[]{stops, train.getName(), 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.getName(), 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.getName(), expectedArrivalTime, 281 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.getName(), expectedArrivalTime, 291 rl.getTrainDirectionString(), train.getDescription()})); 292 if (Setup.isUseSwitchListDepartureTimeEnabled()) { 293 // Departs {0} {1}bound at {2} 294 newLine(fileOut, MessageFormat.format( 295 messageFormatText = TrainSwitchListText.getStringDepartsAt(), 296 new Object[]{splitString(rl.getName()), 297 rl.getTrainDirectionString(), 298 train.getExpectedDepartureTime(rl)})); 299 } 300 } else { 301 // Visit number {0} for train ({1}) expected arrival {2}, terminates {3} 302 newLine(fileOut, MessageFormat.format( 303 messageFormatText = TrainSwitchListText.getStringVisitNumberTerminates(), 304 new Object[]{stops, train.getName(), expectedArrivalTime, 305 rl.getSplitName(), train.getDescription()})); 306 } 307 } 308 } 309 } 310 311 private void reverseDirectionMessage(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious) { 312 // Does the train reverse direction? 313 if (rl.getTrainDirection() != rlPrevious.getTrainDirection() && 314 !TrainSwitchListText.getStringTrainDirectionChange().isEmpty()) { 315 // Train ({0}) direction change, departs {1}bound 316 newLine(fileOut, 317 MessageFormat.format( 318 messageFormatText = TrainSwitchListText.getStringTrainDirectionChange(), 319 new Object[]{train.getName(), rl.getTrainDirectionString(), 320 train.getDescription(), train.getTrainTerminatesName()})); 321 } 322 } 323 324 /* 325 * Train departure messages at the end of the switch list 326 */ 327 private void departureMessages(PrintWriter fileOut, Train train, RouteLocation rl) { 328 String trainDeparts = ""; 329 if (Setup.isPrintLoadsAndEmptiesEnabled()) { 330 int emptyCars = train.getNumberEmptyCarsInTrain(rl); 331 // Train departs {0} {1}bound with {2} loads, {3} empties, {4} {5}, {6} tons 332 trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsLoads(), 333 new Object[]{rl.getSplitName(), 334 rl.getTrainDirectionString(), 335 train.getNumberCarsInTrain(rl) - emptyCars, emptyCars, 336 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 337 train.getTrainWeight(rl), train.getTrainTerminatesName(), 338 train.getName()}); 339 } else { 340 // Train departs {0} {1}bound with {2} cars, {3} {4}, {5} tons 341 trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsCars(), 342 new Object[]{rl.getSplitName(), 343 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl), 344 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 345 train.getTrainWeight(rl), train.getTrainTerminatesName(), 346 train.getName()}); 347 } 348 newLine(fileOut, trainDeparts); 349 } 350 351 private void trainSummaryMessages(PrintWriter fileOut, Train train, Location location, boolean trainDone, 352 int stops) { 353 if (trainDone && !_pickupCars && !_dropCars) { 354 // Default message: Train ({0}) has serviced this location 355 newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringTrainDone(), 356 new Object[]{train.getName(), train.getDescription(), location.getSplitName()})); 357 } else { 358 if (stops > 1 && !_pickupCars) { 359 // Default message: No car pick ups for train ({0}) at this location 360 newLine(fileOut, 361 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(), 362 new Object[]{train.getName(), train.getDescription(), 363 location.getSplitName()})); 364 } 365 if (stops > 1 && !_dropCars) { 366 // Default message: No car set outs for train ({0}) at this location 367 newLine(fileOut, 368 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(), 369 new Object[]{train.getName(), train.getDescription(), 370 location.getSplitName()})); 371 } 372 } 373 } 374 375 private void reportByTrack(PrintWriter fileOut, Location location) { 376 if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) { 377 clearUtilityCarTypes(); // list utility cars by quantity 378 if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) { 379 newLine(fileOut); 380 newLine(fileOut); 381 } else { 382 fileOut.write(FORM_FEED); 383 } 384 newLine(fileOut, 385 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(), 386 new Object[]{location.getSplitName()})); 387 388 // we only need the cars delivered to or at this location 389 List<Car> rsList = carManager.getByTrainList(); 390 List<Car> carList = new ArrayList<>(); 391 for (Car rs : rsList) { 392 if ((rs.getLocation() != null && 393 rs.getLocation().getSplitName().equals(location.getSplitName())) || 394 (rs.getDestination() != null && 395 rs.getSplitDestinationName().equals(location.getSplitName()))) 396 carList.add(rs); 397 } 398 399 List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list 400 // track names once 401 for (Location loc : locationManager.getLocationsByNameList()) { 402 if (!loc.getSplitName().equals(location.getSplitName())) 403 continue; 404 for (Track track : loc.getTracksByBlockingOrderList(null)) { 405 String trackName = track.getSplitName(); 406 if (trackNames.contains(trackName)) 407 continue; 408 trackNames.add(trackName); 409 410 String trainName = ""; // for printing train message once 411 newLine(fileOut); 412 newLine(fileOut, trackName); // print out just the track name 413 // now show the cars pickup and holds for this track 414 for (Car car : carList) { 415 if (!car.getSplitTrackName().equals(trackName)) { 416 continue; 417 } 418 // is the car scheduled for pickup? 419 if (car.getRouteLocation() != null) { 420 if (car.getRouteLocation().getLocation().getSplitName() 421 .equals(location.getSplitName())) { 422 // cars are sorted by train name, print train message once 423 if (!trainName.equals(car.getTrainName())) { 424 trainName = car.getTrainName(); 425 newLine(fileOut, MessageFormat.format( 426 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 427 new Object[]{car.getTrainName(), car.getTrain().getDescription()})); 428 printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 429 } 430 if (car.isUtility()) { 431 pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 432 } else { 433 pickUpCar(fileOut, car, !IS_MANIFEST); 434 } 435 } 436 // car holds 437 } else if (car.isUtility()) { 438 String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 439 if (s != null) { 440 newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N 441 } 442 } else { 443 newLine(fileOut, 444 MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(), 445 new Object[]{ 446 padAndTruncateIfNeeded(car.getRoadName(), 447 InstanceManager.getDefault(CarRoads.class) 448 .getMaxNameLength()), 449 padAndTruncateIfNeeded( 450 TrainCommon.splitString(car.getNumber()), 451 Control.max_len_string_print_road_number), 452 padAndTruncateIfNeeded( 453 car.getTypeName().split(TrainCommon.HYPHEN)[0], 454 InstanceManager.getDefault(CarTypes.class) 455 .getMaxNameLength()), 456 padAndTruncateIfNeeded( 457 car.getLength() + Setup.getLengthUnitAbv(), 458 Control.max_len_string_length_name), 459 padAndTruncateIfNeeded(car.getLoadName(), 460 InstanceManager.getDefault(CarLoads.class) 461 .getMaxNameLength()), 462 padAndTruncateIfNeeded(trackName, 463 locationManager.getMaxTrackNameLength()), 464 padAndTruncateIfNeeded(car.getColor(), InstanceManager 465 .getDefault(CarColors.class).getMaxNameLength())})); 466 } 467 } 468 // now do set outs at this location 469 for (Car car : carList) { 470 if (!car.getSplitDestinationTrackName().equals(trackName)) { 471 continue; 472 } 473 if (car.getRouteDestination() != null && 474 car.getRouteDestination().getLocation().getSplitName() 475 .equals(location.getSplitName())) { 476 // cars are sorted by train name, print train message once 477 if (!trainName.equals(car.getTrainName())) { 478 trainName = car.getTrainName(); 479 newLine(fileOut, MessageFormat.format( 480 messageFormatText = TrainSwitchListText.getStringScheduledWork(), 481 new Object[]{car.getTrainName(), car.getTrain().getDescription()})); 482 printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK); 483 } 484 if (car.isUtility()) { 485 setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST); 486 } else { 487 dropCar(fileOut, car, !IS_MANIFEST); 488 } 489 } 490 } 491 } 492 } 493 } 494 } 495 496 public void printSwitchList(Location location, boolean isPreview) { 497 File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName()); 498 if (!switchListFile.exists()) { 499 log.warn("Switch list file missing for location ({})", location.getName()); 500 return; 501 } 502 if (isPreview && Setup.isManifestEditorEnabled()) { 503 TrainUtilities.openDesktop(switchListFile); 504 } else { 505 TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false, 506 FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(), 507 Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(), 508 Setup.getPrintDuplexSides()); 509 } 510 if (!isPreview) { 511 location.setStatus(Location.PRINTED); 512 location.setSwitchListState(Location.SW_PRINTED); 513 } 514 } 515 516 protected void newLine(PrintWriter file, String string) { 517 if (!string.isEmpty()) { 518 newLine(file, string, !IS_MANIFEST); 519 } 520 } 521 522 private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class); 523}