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