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}