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 String schName = ""; 064 065 if (Setup.isPrintTrainScheduleNameEnabled()) { 066 TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule(); 067 if (sch != null) { 068 schName = "(" + sch.getName() + ")"; 069 } 070 } 071 if (Setup.isPrintValidEnabled()) { 072 newLine(fileOut, valid + " " + schName); 073 } else { 074 newLine(fileOut, schName); 075 } 076 if (!train.getCommentWithColor().equals(Train.NONE)) { 077 newLine(fileOut, train.getCommentWithColor()); 078 } 079 if (Setup.isPrintRouteCommentsEnabled() && !train.getRoute().getComment().equals(Route.NONE)) { 080 newLine(fileOut, train.getRoute().getComment()); 081 } 082 083 List<Engine> engineList = engineManager.getByTrainBlockingList(train); 084 List<Car> carList = carManager.getByTrainDestinationList(train); 085 log.debug("Train has {} cars assigned to it", carList.size()); 086 087 boolean hadWork = false; 088 String previousRouteLocationName = null; 089 List<RouteLocation> routeList = train.getRoute().getLocationsBySequenceList(); 090 091 /* 092 * Go through the train's route and print out the work for each 093 * location. Locations with "similar" names are combined to look 094 * like one location. 095 */ 096 for (RouteLocation rl : routeList) { 097 boolean printHeader = false; 098 boolean hasWork = isThereWorkAtLocation(carList, engineList, rl); 099 // print info only if new location 100 String routeLocationName = rl.getSplitName(); 101 if (!routeLocationName.equals(previousRouteLocationName) || (hasWork && !hadWork)) { 102 if (hasWork) { 103 newLine(fileOut); 104 hadWork = true; 105 printHeader = true; 106 107 // add arrival message 108 arrivalMessage(fileOut, train, rl); 109 110 // add route location comment 111 if (!rl.getComment().trim().equals(RouteLocation.NONE)) { 112 newLine(fileOut, rl.getCommentWithColor()); 113 } 114 115 // add location comment 116 if (Setup.isPrintLocationCommentsEnabled() && 117 !rl.getLocation().getCommentWithColor().equals(Location.NONE)) { 118 newLine(fileOut, rl.getLocation().getCommentWithColor()); 119 } 120 } 121 } 122 // remember location name 123 previousRouteLocationName = routeLocationName; 124 125 // add track comments 126 printTrackComments(fileOut, rl, carList, IS_MANIFEST); 127 128 // engine change or helper service? 129 if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) { 130 if (rl == train.getSecondLegStartRouteLocation()) { 131 printChange(fileOut, rl, train, train.getSecondLegOptions()); 132 } 133 if (rl == train.getSecondLegEndRouteLocation() && 134 train.getSecondLegOptions() == Train.HELPER_ENGINES) { 135 newLine(fileOut, 136 MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(), 137 new Object[]{rl.getSplitName(), train.getName(), 138 train.getDescription(), train.getSecondLegNumberEngines(), 139 train.getSecondLegEngineModel(), train.getSecondLegEngineRoad()})); 140 } 141 } 142 if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) { 143 if (rl == train.getThirdLegStartRouteLocation()) { 144 printChange(fileOut, rl, train, train.getThirdLegOptions()); 145 } 146 if (rl == train.getThirdLegEndRouteLocation() && 147 train.getThirdLegOptions() == Train.HELPER_ENGINES) { 148 newLine(fileOut, 149 MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(), 150 new Object[]{rl.getSplitName(), train.getName(), 151 train.getDescription(), train.getThirdLegNumberEngines(), 152 train.getThirdLegEngineModel(), train.getThirdLegEngineRoad()})); 153 } 154 } 155 156 if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 157 pickupEngines(fileOut, engineList, rl, IS_MANIFEST); 158 // if switcher show loco drop at end of list 159 if (train.isLocalSwitcher() || Setup.isPrintLocoLastEnabled()) { 160 blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 161 dropEngines(fileOut, engineList, rl, IS_MANIFEST); 162 } else { 163 dropEngines(fileOut, engineList, rl, IS_MANIFEST); 164 blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 165 } 166 } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) { 167 blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST); 168 blockCarsTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 169 } else { 170 blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST); 171 blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST); 172 } 173 174 if (rl != train.getTrainTerminatesRouteLocation()) { 175 // Is the next location the same as the current? 176 RouteLocation rlNext = train.getRoute().getNextRouteLocation(rl); 177 if (routeLocationName.equals(rlNext.getSplitName())) { 178 continue; 179 } 180 departureMessage(fileOut, train, rl, hadWork); 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 fileOut.flush(); 207 fileOut.close(); 208 train.setModified(false); 209 } 210 211 private void arrivalMessage(PrintWriter fileOut, Train train, RouteLocation rl) { 212 newLine(fileOut, getTrainMessage(train, rl)); 213 } 214 215 private void departureMessage(PrintWriter fileOut, Train train, RouteLocation rl, boolean hadWork) { 216 String routeLocationName = rl.getSplitName(); 217 if (!hadWork) { 218 newLine(fileOut); 219 // No work at {0} 220 String s = MessageFormat.format(messageFormatText = TrainManifestText 221 .getStringNoScheduledWork(), 222 new Object[]{routeLocationName, train.getName(), 223 train.getDescription(), rl.getLocation().getDivisionName()}); 224 // if a route comment, then only use location name and route comment, useful for passenger 225 // trains 226 if (!rl.getComment().equals(RouteLocation.NONE)) { 227 s = routeLocationName; 228 if (!rl.getComment().isBlank()) { 229 s = MessageFormat.format(messageFormatText = TrainManifestText 230 .getStringNoScheduledWorkWithRouteComment(), 231 new Object[]{routeLocationName, rl.getCommentWithColor(), train.getName(), 232 train.getDescription(), rl.getLocation().getDivisionName()}); 233 } 234 } 235 // append arrival or departure time if enabled 236 if (train.isShowArrivalAndDepartureTimesEnabled()) { 237 if (rl == train.getTrainDepartsRouteLocation()) { 238 s += MessageFormat.format(messageFormatText = TrainManifestText 239 .getStringDepartTime(), new Object[]{train.getFormatedDepartureTime()}); 240 } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) { 241 s += MessageFormat.format(messageFormatText = TrainManifestText 242 .getStringDepartTime(), new Object[]{rl.getFormatedDepartureTime()}); 243 } else if (Setup.isUseDepartureTimeEnabled() && 244 !rl.getComment().equals(RouteLocation.NONE)) { 245 s += MessageFormat 246 .format(messageFormatText = TrainManifestText.getStringDepartTime(), 247 new Object[]{train.getExpectedDepartureTime(rl)}); 248 } 249 } 250 newLine(fileOut, s); 251 252 // add location comment 253 if (Setup.isPrintLocationCommentsEnabled() && 254 !rl.getLocation().getCommentWithColor().equals(Location.NONE)) { 255 newLine(fileOut, rl.getLocation().getCommentWithColor()); 256 } 257 } else if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) { 258 printHorizontalLine(fileOut, IS_MANIFEST); 259 } 260 if (Setup.isPrintLoadsAndEmptiesEnabled()) { 261 int emptyCars = train.getNumberEmptyCarsInTrain(rl); 262 // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons 263 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 264 .getStringTrainDepartsLoads(), 265 new Object[]{routeLocationName, 266 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl) - emptyCars, 267 emptyCars, 268 train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(), 269 train.getTrainWeight(rl), train.getTrainTerminatesName(), train.getName()})); 270 } else { 271 // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons 272 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText 273 .getStringTrainDepartsCars(), 274 new Object[]{routeLocationName, 275 rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl), 276 train.getTrainLength(rl), 277 Setup.getLengthUnit().toLowerCase(), train.getTrainWeight(rl), 278 train.getTrainTerminatesName(), train.getName()})); 279 } 280 } 281 282 private void printChange(PrintWriter fileOut, RouteLocation rl, Train train, int legOptions) 283 throws IllegalArgumentException { 284 if ((legOptions & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) { 285 // assume 2nd leg for helper change 286 String numberEngines = train.getSecondLegNumberEngines(); 287 String endLocationName = train.getSecondLegEndLocationName(); 288 String engineModel = train.getSecondLegEngineModel(); 289 String engineRoad = train.getSecondLegEngineRoad(); 290 if (rl == train.getThirdLegStartRouteLocation()) { 291 numberEngines = train.getThirdLegNumberEngines(); 292 endLocationName = train.getThirdLegEndLocationName(); 293 engineModel = train.getThirdLegEngineModel(); 294 engineRoad = train.getThirdLegEngineRoad(); 295 } 296 newLine(fileOut, 297 MessageFormat.format(messageFormatText = TrainManifestText.getStringAddHelpers(), 298 new Object[]{rl.getSplitName(), train.getName(), train.getDescription(), 299 numberEngines, endLocationName, engineModel, engineRoad})); 300 } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES && 301 ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE || 302 (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE)) { 303 newLine(fileOut, MessageFormat.format( 304 messageFormatText = TrainManifestText.getStringLocoAndCabooseChange(), new Object[]{ 305 rl.getSplitName(), train.getName(), train.getDescription(), 306 rl.getLocation().getDivisionName()})); 307 } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) { 308 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringLocoChange(), 309 new Object[]{rl.getSplitName(), train.getName(), train.getDescription(), 310 rl.getLocation().getDivisionName()})); 311 } else if ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE || 312 (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) { 313 newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringCabooseChange(), 314 new Object[]{rl.getSplitName(), train.getName(), train.getDescription(), 315 rl.getLocation().getDivisionName()})); 316 } 317 } 318 319 private void newLine(PrintWriter file, String string) { 320 if (!string.isEmpty()) { 321 newLine(file, string, IS_MANIFEST); 322 } 323 } 324}