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