001package jmri.jmrit.operations.trains; 002 003import java.io.*; 004import java.nio.charset.StandardCharsets; 005import java.text.MessageFormat; 006import java.util.List; 007 008import org.slf4j.Logger; 009import org.slf4j.LoggerFactory; 010 011import jmri.InstanceManager; 012import jmri.jmrit.operations.locations.Location; 013import jmri.jmrit.operations.rollingstock.cars.Car; 014import jmri.jmrit.operations.rollingstock.engines.Engine; 015import jmri.jmrit.operations.routes.Route; 016import jmri.jmrit.operations.routes.RouteLocation; 017import jmri.jmrit.operations.setup.Setup; 018import jmri.jmrit.operations.trains.schedules.TrainSchedule; 019import jmri.jmrit.operations.trains.schedules.TrainScheduleManager; 020 021/** 022 * Builds a train's manifest. User has the ability to modify the text of the 023 * messages which can cause an IllegalArgumentException. Some messages have more 024 * arguments than the default message allowing the user to customize the message 025 * to their liking. 026 * 027 * @author Daniel Boudreau Copyright (C) 2011, 2012, 2013, 2015, 2024 028 */ 029public class TrainManifest extends TrainCommon { 030 031 private static final Logger log = LoggerFactory.getLogger(TrainManifest.class); 032 033 String messageFormatText = ""; // the text being formated in case there's an exception 034 035 public TrainManifest(Train train) throws BuildFailedException { 036 // create manifest file 037 File file = InstanceManager.getDefault(TrainManagerXml.class).createTrainManifestFile(train.getName()); 038 PrintWriter fileOut; 039 040 try { 041 fileOut = new PrintWriter( 042 new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)), 043 true); 044 } catch (IOException e) { 045 log.error("Can not open train manifest file: {}", e.getLocalizedMessage()); 046 throw new BuildFailedException(e); 047 } 048 049 try { 050 // build header 051 if (!train.getRailroadName().equals(Train.NONE)) { 052 newLine(fileOut, train.getRailroadName()); 053 } else { 054 newLine(fileOut, Setup.getRailroadName()); 055 } 056 newLine(fileOut); // empty line 057 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringManifestForTrain(), 058 new Object[]{train.getName(), train.getDescription()})); 059 060 String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(), 061 new Object[]{getDate(true)}); 062 063 if (Setup.isPrintTrainScheduleNameEnabled()) { 064 TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule(); 065 if (sch != null) { 066 valid = valid + " (" + sch.getName() + ")"; 067 } 068 } 069 if (Setup.isPrintValidEnabled()) { 070 newLine(fileOut, valid); 071 } 072 073 if (!train.getCommentWithColor().equals(Train.NONE)) { 074 newLine(fileOut, train.getCommentWithColor()); 075 } 076 077 List<Engine> engineList = engineManager.getByTrainBlockingList(train); 078 079 if (Setup.isPrintRouteCommentsEnabled() && !train.getRoute().getComment().equals(Route.NONE)) { 080 newLine(fileOut, train.getRoute().getComment()); 081 } 082 083 List<Car> carList = carManager.getByTrainDestinationList(train); 084 log.debug("Train has {} cars assigned to it", carList.size()); 085 086 boolean hadWork = false; 087 String previousRouteLocationName = null; 088 List<RouteLocation> routeList = train.getRoute().getLocationsBySequenceList(); 089 090 /* 091 * Go through the train's route and print out the work for each 092 * location. Locations with "similar" names are combined to look 093 * like one location. 094 */ 095 for (RouteLocation rl : routeList) { 096 boolean printHeader = false; 097 boolean hasWork = isThereWorkAtLocation(carList, engineList, rl); 098 // print info only if new location 099 String routeLocationName = rl.getSplitName(); 100 if (!routeLocationName.equals(previousRouteLocationName) || (hasWork && !hadWork)) { 101 if (hasWork) { 102 newLine(fileOut); 103 hadWork = true; 104 printHeader = true; 105 106 // add arrival message 107 arrivalMessage(fileOut, train, rl); 108 109 // add route location comment 110 if (!rl.getComment().trim().equals(RouteLocation.NONE)) { 111 newLine(fileOut, rl.getCommentWithColor()); 112 } 113 114 // add location comment 115 if (Setup.isPrintLocationCommentsEnabled() && 116 !rl.getLocation().getCommentWithColor().equals(Location.NONE)) { 117 newLine(fileOut, rl.getLocation().getCommentWithColor()); 118 } 119 } 120 } 121 // remember location name 122 previousRouteLocationName = routeLocationName; 123 124 // add track comments 125 printTrackComments(fileOut, rl, carList, IS_MANIFEST); 126 127 // engine change or helper service? 128 if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) { 129 if (rl == train.getSecondLegStartRouteLocation()) { 130 printChange(fileOut, rl, train, train.getSecondLegOptions()); 131 } 132 if (rl == train.getSecondLegEndRouteLocation() && 133 train.getSecondLegOptions() == Train.HELPER_ENGINES) { 134 newLine(fileOut, 135 MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(), 136 new Object[]{rl.getSplitName(), train.getName(), 137 train.getDescription(), train.getSecondLegNumberEngines(), 138 train.getSecondLegEngineModel(), train.getSecondLegEngineRoad()})); 139 } 140 } 141 if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) { 142 if (rl == train.getThirdLegStartRouteLocation()) { 143 printChange(fileOut, rl, train, train.getThirdLegOptions()); 144 } 145 if (rl == train.getThirdLegEndRouteLocation() && 146 train.getThirdLegOptions() == Train.HELPER_ENGINES) { 147 newLine(fileOut, 148 MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(), 149 new Object[]{rl.getSplitName(), train.getName(), 150 train.getDescription(), train.getThirdLegNumberEngines(), 151 train.getThirdLegEngineModel(), train.getThirdLegEngineRoad()})); 152 } 153 } 154 155 if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 156 pickupEngines(fileOut, engineList, rl, IS_MANIFEST); 157 // if switcher show loco drop at end of list 158 if (train.isLocalSwitcher()) { 159 blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 160 dropEngines(fileOut, engineList, rl, IS_MANIFEST); 161 } else { 162 dropEngines(fileOut, engineList, rl, IS_MANIFEST); 163 blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 164 } 165 } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) { 166 blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST); 167 blockCarsTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 168 } else { 169 blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST); 170 blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 171 } 172 173 if (rl != train.getTrainTerminatesRouteLocation()) { 174 // Is the next location the same as the current? 175 RouteLocation rlNext = train.getRoute().getNextRouteLocation(rl); 176 if (routeLocationName.equals(rlNext.getSplitName())) { 177 continue; 178 } 179 departureMessage(fileOut, train, rl, hadWork); 180 181 hadWork = false; 182 183 } else { 184 // last location in the train's route, print train terminates message 185 if (!hadWork) { 186 newLine(fileOut); 187 } else if (Setup.isPrintHeadersEnabled() || 188 !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 189 printHorizontalLine(fileOut, IS_MANIFEST); 190 } 191 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 192 .getStringTrainTerminates(), 193 new Object[]{routeLocationName, train.getName(), 194 train.getDescription(), rl.getLocation().getDivisionName()})); 195 } 196 } 197 // Are there any cars that need to be found? 198 addCarsLocationUnknown(fileOut, IS_MANIFEST); 199 200 } catch (IllegalArgumentException e) { 201 newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument", 202 Bundle.getMessage("TitleManifestText"), e.getLocalizedMessage())); 203 newLine(fileOut, messageFormatText); 204 log.error("Illegal argument", e); 205 } 206 207 fileOut.flush(); 208 fileOut.close(); 209 210 train.setModified(false); 211 } 212 213 private void arrivalMessage(PrintWriter fileOut, Train train, RouteLocation rl) { 214 String expectedArrivalTime = train.getExpectedArrivalTime(rl); 215 String routeLocationName = rl.getSplitName(); 216 // Scheduled work at {0} 217 String workAt = MessageFormat.format(messageFormatText = TrainManifestText 218 .getStringScheduledWork(), 219 new Object[]{routeLocationName, train.getName(), 220 train.getDescription(), rl.getLocation().getDivisionName()}); 221 if (!train.isShowArrivalAndDepartureTimesEnabled()) { 222 // Scheduled work at {0} 223 newLine(fileOut, workAt); 224 } else if (rl == train.getTrainDepartsRouteLocation()) { 225 // Scheduled work at {0}, departure time {1} 226 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 227 .getStringWorkDepartureTime(), 228 new Object[]{routeLocationName, 229 train.getFormatedDepartureTime(), train.getName(), 230 train.getDescription(), rl.getLocation().getDivisionName()})); 231 } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) { 232 // Scheduled work at {0}, departure time {1} 233 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 234 .getStringWorkDepartureTime(), 235 new Object[]{routeLocationName, 236 rl.getFormatedDepartureTime(), train.getName(), train.getDescription(), 237 rl.getLocation().getDivisionName()})); 238 } else if (Setup.isUseDepartureTimeEnabled() && 239 rl != train.getTrainTerminatesRouteLocation()) { 240 // Scheduled work at {0}, departure time {1} 241 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 242 .getStringWorkDepartureTime(), 243 new Object[]{routeLocationName, 244 train.getExpectedDepartureTime(rl), train.getName(), 245 train.getDescription(), rl.getLocation().getDivisionName()})); 246 } else if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) { 247 // Scheduled work at {0}, arrival time {1} 248 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 249 .getStringWorkArrivalTime(), 250 new Object[]{routeLocationName, expectedArrivalTime, 251 train.getName(), train.getDescription(), 252 rl.getLocation().getDivisionName()})); 253 } else { 254 // Scheduled work at {0} 255 newLine(fileOut, workAt); 256 } 257 } 258 259 private void departureMessage(PrintWriter fileOut, Train train, RouteLocation rl, boolean hadWork) { 260 String routeLocationName = rl.getSplitName(); 261 if (!hadWork) { 262 newLine(fileOut); 263 // No work at {0} 264 String s = MessageFormat.format(messageFormatText = TrainManifestText 265 .getStringNoScheduledWork(), 266 new Object[]{routeLocationName, train.getName(), 267 train.getDescription(), rl.getLocation().getDivisionName()}); 268 // if a route comment, then only use location name and route comment, useful for passenger 269 // trains 270 if (!rl.getComment().equals(RouteLocation.NONE)) { 271 s = routeLocationName; 272 if (!rl.getComment().isBlank()) { 273 s = MessageFormat.format(messageFormatText = TrainManifestText 274 .getStringNoScheduledWorkWithRouteComment(), 275 new Object[]{routeLocationName, rl.getCommentWithColor(), train.getName(), 276 train.getDescription(), rl.getLocation().getDivisionName()}); 277 } 278 } 279 // append arrival or departure time if enabled 280 if (train.isShowArrivalAndDepartureTimesEnabled()) { 281 if (rl == train.getTrainDepartsRouteLocation()) { 282 s += MessageFormat.format(messageFormatText = TrainManifestText 283 .getStringDepartTime(), new Object[]{train.getFormatedDepartureTime()}); 284 } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) { 285 s += MessageFormat.format(messageFormatText = TrainManifestText 286 .getStringDepartTime(), new Object[]{rl.getFormatedDepartureTime()}); 287 } else if (Setup.isUseDepartureTimeEnabled() && 288 !rl.getComment().equals(RouteLocation.NONE)) { 289 s += MessageFormat 290 .format(messageFormatText = TrainManifestText.getStringDepartTime(), 291 new Object[]{train.getExpectedDepartureTime(rl)}); 292 } 293 } 294 newLine(fileOut, s); 295 296 // add location comment 297 if (Setup.isPrintLocationCommentsEnabled() && 298 !rl.getLocation().getCommentWithColor().equals(Location.NONE)) { 299 newLine(fileOut, rl.getLocation().getCommentWithColor()); 300 } 301 } else if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 302 printHorizontalLine(fileOut, IS_MANIFEST); 303 } 304 if (Setup.isPrintLoadsAndEmptiesEnabled()) { 305 int emptyCars = train.getNumberEmptyCarsInTrain(rl); 306 // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons 307 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 308 .getStringTrainDepartsLoads(), 309 new Object[]{routeLocationName, 310 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl) - emptyCars, 311 emptyCars, 312 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 313 train.getTrainWeight(rl), train.getTrainTerminatesName(), train.getName()})); 314 } else { 315 // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons 316 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 317 .getStringTrainDepartsCars(), 318 new Object[]{routeLocationName, 319 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl), 320 train.getTrainLength(rl), 321 Setup.getLengthUnit().toLowerCase(), train.getTrainWeight(rl), 322 train.getTrainTerminatesName(), train.getName()})); 323 } 324 } 325 326 private void printChange(PrintWriter fileOut, RouteLocation rl, Train train, int legOptions) 327 throws IllegalArgumentException { 328 if ((legOptions & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) { 329 // assume 2nd leg for helper change 330 String numberEngines = train.getSecondLegNumberEngines(); 331 String endLocationName = train.getSecondLegEndLocationName(); 332 String engineModel = train.getSecondLegEngineModel(); 333 String engineRoad = train.getSecondLegEngineRoad(); 334 if (rl == train.getThirdLegStartRouteLocation()) { 335 numberEngines = train.getThirdLegNumberEngines(); 336 endLocationName = train.getThirdLegEndLocationName(); 337 engineModel = train.getThirdLegEngineModel(); 338 engineRoad = train.getThirdLegEngineRoad(); 339 } 340 newLine(fileOut, 341 MessageFormat.format(messageFormatText = TrainManifestText.getStringAddHelpers(), 342 new Object[]{rl.getSplitName(), train.getName(), train.getDescription(), 343 numberEngines, endLocationName, engineModel, engineRoad})); 344 } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES && 345 ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE || 346 (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE)) { 347 newLine(fileOut, MessageFormat.format( 348 messageFormatText = TrainManifestText.getStringLocoAndCabooseChange(), new Object[]{ 349 rl.getSplitName(), train.getName(), train.getDescription(), 350 rl.getLocation().getDivisionName()})); 351 } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) { 352 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringLocoChange(), 353 new Object[]{rl.getSplitName(), train.getName(), train.getDescription(), 354 rl.getLocation().getDivisionName()})); 355 } else if ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE || 356 (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) { 357 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringCabooseChange(), 358 new Object[]{rl.getSplitName(), train.getName(), train.getDescription(), 359 rl.getLocation().getDivisionName()})); 360 } 361 } 362 363 private void newLine(PrintWriter file, String string) { 364 if (!string.isEmpty()) { 365 newLine(file, string, IS_MANIFEST); 366 } 367 } 368 369}