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