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