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;
020import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
021
022/**
023 * Builds a train's manifest. User has the ability to modify the text of the
024 * messages which can cause an IllegalArgumentException. Some messages have more
025 * arguments than the default message allowing the user to customize the message
026 * to their liking.
027 *
028 * @author Daniel Boudreau Copyright (C) 2011, 2012, 2013, 2015, 2024
029 */
030public class TrainManifest extends TrainCommon {
031
032    private static final Logger log = LoggerFactory.getLogger(TrainManifest.class);
033
034    String messageFormatText = ""; // the text being formated in case there's an exception
035
036    public TrainManifest(Train train) throws BuildFailedException {
037        // create manifest file
038        File file = InstanceManager.getDefault(TrainManagerXml.class).createTrainManifestFile(train.getName());
039        PrintWriter fileOut;
040
041        try {
042            fileOut = new PrintWriter(
043                    new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)),
044                    true);
045        } catch (IOException e) {
046            log.error("Can not open train manifest file: {}", e.getLocalizedMessage());
047            throw new BuildFailedException(e);
048        }
049
050        try {
051            // build header
052            if (!train.getRailroadName().equals(Train.NONE)) {
053                newLine(fileOut, train.getRailroadName());
054            } else {
055                newLine(fileOut, Setup.getRailroadName());
056            }
057            newLine(fileOut); // empty line
058            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringManifestForTrain(),
059                    new Object[]{train.getName(), train.getDescription()}));
060
061            String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(),
062                    new Object[]{getDate(true)});
063
064            String schName = "";
065
066            if (Setup.isPrintTrainScheduleNameEnabled()) {
067                TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule();
068                if (sch != null) {
069                    schName = "(" + sch.getName() + ")";
070                }
071            }
072            if (Setup.isPrintValidEnabled()) {
073                newLine(fileOut, valid + " " + schName);
074            } else {
075                newLine(fileOut, schName);
076            }
077            if (!train.getCommentWithColor().equals(Train.NONE)) {
078                newLine(fileOut, train.getCommentWithColor());
079            }
080            if (Setup.isPrintRouteCommentsEnabled() && !train.getRoute().getComment().equals(Route.NONE)) {
081                newLine(fileOut, train.getRoute().getComment());
082            }
083
084            List<Engine> engineList = engineManager.getByTrainBlockingList(train);
085            List<Car> carList = carManager.getByTrainDestinationList(train);
086            log.debug("Train has {} cars assigned to it", carList.size());
087
088            boolean hadWork = false;
089            String previousRouteLocationName = null;
090            List<RouteLocation> routeList = train.getRoute().getLocationsBySequenceList();
091
092            /*
093             * Go through the train's route and print out the work for each
094             * location. Locations with "similar" names are combined to look
095             * like one location.
096             */
097            for (RouteLocation rl : routeList) {
098                boolean printHeader = false;
099                boolean hasWork = isThereWorkAtLocation(carList, engineList, rl);
100                // print info only if new location
101                String routeLocationName = rl.getSplitName();
102                if (!routeLocationName.equals(previousRouteLocationName) || (hasWork && !hadWork)) {
103                    if (hasWork) {
104                        newLine(fileOut);
105                        hadWork = true;
106                        printHeader = true;
107
108                        // add arrival message
109                        arrivalMessage(fileOut, train, rl);
110
111                        // add route location comment
112                        if (!rl.getComment().trim().equals(RouteLocation.NONE)) {
113                            newLine(fileOut, rl.getCommentWithColor());
114                        }
115
116                        // add location comment
117                        if (Setup.isPrintLocationCommentsEnabled() &&
118                                !rl.getLocation().getCommentWithColor().equals(Location.NONE)) {
119                            newLine(fileOut, rl.getLocation().getCommentWithColor());
120                        }
121                    }
122                }
123                // remember location name
124                previousRouteLocationName = routeLocationName;
125
126                // add track comments
127                printTrackComments(fileOut, rl, carList, IS_MANIFEST);
128
129                // engine change or helper service?
130                if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
131                    if (rl == train.getSecondLegStartRouteLocation()) {
132                        printChange(fileOut, rl, train, train.getSecondLegOptions());
133                    }
134                    if (rl == train.getSecondLegEndRouteLocation() &&
135                            train.getSecondLegOptions() == Train.HELPER_ENGINES) {
136                        newLine(fileOut,
137                                MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(),
138                                        new Object[]{rl.getSplitName(), train.getName(),
139                                                train.getDescription(), train.getSecondLegNumberEngines(),
140                                                train.getSecondLegEngineModel(), train.getSecondLegEngineRoad()}));
141                    }
142                }
143                if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
144                    if (rl == train.getThirdLegStartRouteLocation()) {
145                        printChange(fileOut, rl, train, train.getThirdLegOptions());
146                    }
147                    if (rl == train.getThirdLegEndRouteLocation() &&
148                            train.getThirdLegOptions() == Train.HELPER_ENGINES) {
149                        newLine(fileOut,
150                                MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(),
151                                        new Object[]{rl.getSplitName(), train.getName(),
152                                                train.getDescription(), train.getThirdLegNumberEngines(),
153                                                train.getThirdLegEngineModel(), train.getThirdLegEngineRoad()}));
154                    }
155                }
156
157                setCarPickupTime(train, rl, carList);
158
159                if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
160                    pickupEngines(fileOut, engineList, rl, IS_MANIFEST);
161                    // if switcher show loco drop at end of list
162                    if (train.isLocalSwitcher() || Setup.isPrintLocoLastEnabled()) {
163                        blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
164                        dropEngines(fileOut, engineList, rl, IS_MANIFEST);
165                    } else {
166                        dropEngines(fileOut, engineList, rl, IS_MANIFEST);
167                        blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
168                    }
169                } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) {
170                    blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST);
171                    blockCarsTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
172                } else {
173                    blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST);
174                    blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
175                }
176                
177                if (rl != train.getTrainTerminatesRouteLocation()) {
178                    // Is the next location the same as the current?
179                    RouteLocation rlNext = train.getRoute().getNextRouteLocation(rl);
180                    if (routeLocationName.equals(rlNext.getSplitName())) {
181                        continue;
182                    }
183                    departureMessage(fileOut, train, rl, hadWork);
184                    hadWork = false;
185
186                } else {
187                    // last location in the train's route, print train terminates message
188                    if (!hadWork) {
189                        newLine(fileOut);
190                    } else if (Setup.isPrintHeadersEnabled() ||
191                            !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
192                        printHorizontalLine(fileOut, IS_MANIFEST);
193                    }
194                    newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
195                            .getStringTrainTerminates(),
196                            new Object[]{routeLocationName, train.getName(),
197                                    train.getDescription(), rl.getLocation().getDivisionName()}));
198                }
199            }
200            // Are there any cars that need to be found?
201            addCarsLocationUnknown(fileOut, IS_MANIFEST);
202
203        } catch (IllegalArgumentException e) {
204            newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument",
205                    Bundle.getMessage("TitleManifestText"), e.getLocalizedMessage()));
206            newLine(fileOut, messageFormatText);
207            log.error("Illegal argument", e);
208        }
209        fileOut.flush();
210        fileOut.close();
211        train.setModified(false);
212    }
213
214    private void arrivalMessage(PrintWriter fileOut, Train train, RouteLocation rl) {
215        newLine(fileOut, getTrainMessage(train, rl));
216    }
217
218    private void departureMessage(PrintWriter fileOut, Train train, RouteLocation rl, boolean hadWork) {
219        String routeLocationName = rl.getSplitName();
220        if (!hadWork) {
221            newLine(fileOut);
222            // No work at {0}
223            String s = MessageFormat.format(messageFormatText = TrainManifestText
224                    .getStringNoScheduledWork(),
225                    new Object[]{routeLocationName, train.getName(),
226                            train.getDescription(), rl.getLocation().getDivisionName()});
227            // if a route comment, then only use location name and route comment, useful for passenger
228            // trains
229            if (!rl.getComment().equals(RouteLocation.NONE)) {
230                s = routeLocationName;
231                if (!rl.getComment().isBlank()) {
232                    s = MessageFormat.format(messageFormatText = TrainManifestText
233                            .getStringNoScheduledWorkWithRouteComment(),
234                            new Object[]{routeLocationName, rl.getCommentWithColor(), train.getName(),
235                                    train.getDescription(), rl.getLocation().getDivisionName()});
236                }
237            }
238            // append arrival or departure time if enabled
239            if (train.isShowArrivalAndDepartureTimesEnabled()) {
240                if (rl == train.getTrainDepartsRouteLocation()) {
241                    s += MessageFormat.format(messageFormatText = TrainManifestText
242                            .getStringDepartTime(), new Object[]{train.getFormatedDepartureTime()});
243                } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
244                    s += MessageFormat.format(messageFormatText = TrainManifestText
245                            .getStringDepartTime(), new Object[]{train.getExpectedDepartureTime(rl)});
246                } else if (Setup.isUseDepartureTimeEnabled() &&
247                        !rl.getComment().equals(RouteLocation.NONE)) {
248                    s += MessageFormat
249                            .format(messageFormatText = TrainManifestText.getStringDepartTime(),
250                                    new Object[]{train.getExpectedDepartureTime(rl)});
251                }
252            }
253            newLine(fileOut, s);
254
255            // add location comment
256            if (Setup.isPrintLocationCommentsEnabled() &&
257                    !rl.getLocation().getCommentWithColor().equals(Location.NONE)) {
258                newLine(fileOut, rl.getLocation().getCommentWithColor());
259            }
260        } else if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
261            printHorizontalLine(fileOut, IS_MANIFEST);
262        }
263        if (Setup.isPrintLoadsAndEmptiesEnabled()) {
264            int emptyCars = train.getNumberEmptyCarsInTrain(rl);
265            // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons
266            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
267                    .getStringTrainDepartsLoads(),
268                    new Object[]{routeLocationName,
269                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl) - emptyCars,
270                            emptyCars,
271                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
272                            train.getTrainWeight(rl), train.getTrainTerminatesName(), train.getName()}));
273        } else {
274            // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
275            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
276                    .getStringTrainDepartsCars(),
277                    new Object[]{routeLocationName,
278                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl),
279                            train.getTrainLength(rl),
280                            Setup.getLengthUnit().toLowerCase(), train.getTrainWeight(rl),
281                            train.getTrainTerminatesName(), train.getName()}));
282        }
283    }
284
285    private void printChange(PrintWriter fileOut, RouteLocation rl, Train train, int legOptions)
286            throws IllegalArgumentException {
287        if ((legOptions & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
288            // assume 2nd leg for helper change
289            String numberEngines = train.getSecondLegNumberEngines();
290            String endLocationName = train.getSecondLegEndLocationName();
291            String engineModel = train.getSecondLegEngineModel();
292            String engineRoad = train.getSecondLegEngineRoad();
293            if (rl == train.getThirdLegStartRouteLocation()) {
294                numberEngines = train.getThirdLegNumberEngines();
295                endLocationName = train.getThirdLegEndLocationName();
296                engineModel = train.getThirdLegEngineModel();
297                engineRoad = train.getThirdLegEngineRoad();
298            }
299            newLine(fileOut,
300                    MessageFormat.format(messageFormatText = TrainManifestText.getStringAddHelpers(),
301                            new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
302                                    numberEngines, endLocationName, engineModel, engineRoad}));
303        } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
304                ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
305                        (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE)) {
306            newLine(fileOut, MessageFormat.format(
307                    messageFormatText = TrainManifestText.getStringLocoAndCabooseChange(), new Object[]{
308                            rl.getSplitName(), train.getName(), train.getDescription(),
309                            rl.getLocation().getDivisionName()}));
310        } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
311            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringLocoChange(),
312                    new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
313                            rl.getLocation().getDivisionName()}));
314        } else if ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
315                (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
316            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringCabooseChange(),
317                    new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
318                            rl.getLocation().getDivisionName()}));
319        }
320    }
321
322    private void newLine(PrintWriter file, String string) {
323        if (!string.isEmpty()) {
324            newLine(file, string, IS_MANIFEST);
325        }
326    }
327}