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}