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}