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                setCarPickupTime(train, rl, carList);
157
158                if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
159                    pickupEngines(fileOut, engineList, rl, IS_MANIFEST);
160                    // if switcher show loco drop at end of list
161                    if (train.isLocalSwitcher() || Setup.isPrintLocoLastEnabled()) {
162                        blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
163                        dropEngines(fileOut, engineList, rl, IS_MANIFEST);
164                    } else {
165                        dropEngines(fileOut, engineList, rl, IS_MANIFEST);
166                        blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
167                    }
168                } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) {
169                    blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST);
170                    blockCarsTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
171                } else {
172                    blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST);
173                    blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
174                }
175                
176                if (rl != train.getTrainTerminatesRouteLocation()) {
177                    // Is the next location the same as the current?
178                    RouteLocation rlNext = train.getRoute().getNextRouteLocation(rl);
179                    if (routeLocationName.equals(rlNext.getSplitName())) {
180                        continue;
181                    }
182                    departureMessage(fileOut, train, rl, hadWork);
183                    hadWork = false;
184
185                } else {
186                    // last location in the train's route, print train terminates message
187                    if (!hadWork) {
188                        newLine(fileOut);
189                    } else if (Setup.isPrintHeadersEnabled() ||
190                            !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
191                        printHorizontalLine(fileOut, IS_MANIFEST);
192                    }
193                    newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
194                            .getStringTrainTerminates(),
195                            new Object[]{routeLocationName, train.getName(),
196                                    train.getDescription(), rl.getLocation().getDivisionName()}));
197                }
198            }
199            // Are there any cars that need to be found?
200            addCarsLocationUnknown(fileOut, IS_MANIFEST);
201
202        } catch (IllegalArgumentException e) {
203            newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument",
204                    Bundle.getMessage("TitleManifestText"), e.getLocalizedMessage()));
205            newLine(fileOut, messageFormatText);
206            log.error("Illegal argument", e);
207        }
208        fileOut.flush();
209        fileOut.close();
210        train.setModified(false);
211    }
212
213    private void arrivalMessage(PrintWriter fileOut, Train train, RouteLocation rl) {
214        newLine(fileOut, getTrainMessage(train, rl));
215    }
216
217    private void departureMessage(PrintWriter fileOut, Train train, RouteLocation rl, boolean hadWork) {
218        String routeLocationName = rl.getSplitName();
219        if (!hadWork) {
220            newLine(fileOut);
221            // No work at {0}
222            String s = MessageFormat.format(messageFormatText = TrainManifestText
223                    .getStringNoScheduledWork(),
224                    new Object[]{routeLocationName, train.getName(),
225                            train.getDescription(), rl.getLocation().getDivisionName()});
226            // if a route comment, then only use location name and route comment, useful for passenger
227            // trains
228            if (!rl.getComment().equals(RouteLocation.NONE)) {
229                s = routeLocationName;
230                if (!rl.getComment().isBlank()) {
231                    s = MessageFormat.format(messageFormatText = TrainManifestText
232                            .getStringNoScheduledWorkWithRouteComment(),
233                            new Object[]{routeLocationName, rl.getCommentWithColor(), train.getName(),
234                                    train.getDescription(), rl.getLocation().getDivisionName()});
235                }
236            }
237            // append arrival or departure time if enabled
238            if (train.isShowArrivalAndDepartureTimesEnabled()) {
239                if (rl == train.getTrainDepartsRouteLocation()) {
240                    s += MessageFormat.format(messageFormatText = TrainManifestText
241                            .getStringDepartTime(), new Object[]{train.getFormatedDepartureTime()});
242                } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
243                    s += MessageFormat.format(messageFormatText = TrainManifestText
244                            .getStringDepartTime(), new Object[]{train.getExpectedDepartureTime(rl)});
245                } else if (Setup.isUseDepartureTimeEnabled() &&
246                        !rl.getComment().equals(RouteLocation.NONE)) {
247                    s += MessageFormat
248                            .format(messageFormatText = TrainManifestText.getStringDepartTime(),
249                                    new Object[]{train.getExpectedDepartureTime(rl)});
250                }
251            }
252            newLine(fileOut, s);
253
254            // add location comment
255            if (Setup.isPrintLocationCommentsEnabled() &&
256                    !rl.getLocation().getCommentWithColor().equals(Location.NONE)) {
257                newLine(fileOut, rl.getLocation().getCommentWithColor());
258            }
259        } else if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
260            printHorizontalLine(fileOut, IS_MANIFEST);
261        }
262        if (Setup.isPrintLoadsAndEmptiesEnabled()) {
263            int emptyCars = train.getNumberEmptyCarsInTrain(rl);
264            // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons
265            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
266                    .getStringTrainDepartsLoads(),
267                    new Object[]{routeLocationName,
268                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl) - emptyCars,
269                            emptyCars,
270                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
271                            train.getTrainWeight(rl), train.getTrainTerminatesName(), train.getName()}));
272        } else {
273            // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
274            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
275                    .getStringTrainDepartsCars(),
276                    new Object[]{routeLocationName,
277                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl),
278                            train.getTrainLength(rl),
279                            Setup.getLengthUnit().toLowerCase(), train.getTrainWeight(rl),
280                            train.getTrainTerminatesName(), train.getName()}));
281        }
282    }
283
284    private void printChange(PrintWriter fileOut, RouteLocation rl, Train train, int legOptions)
285            throws IllegalArgumentException {
286        if ((legOptions & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
287            // assume 2nd leg for helper change
288            String numberEngines = train.getSecondLegNumberEngines();
289            String endLocationName = train.getSecondLegEndLocationName();
290            String engineModel = train.getSecondLegEngineModel();
291            String engineRoad = train.getSecondLegEngineRoad();
292            if (rl == train.getThirdLegStartRouteLocation()) {
293                numberEngines = train.getThirdLegNumberEngines();
294                endLocationName = train.getThirdLegEndLocationName();
295                engineModel = train.getThirdLegEngineModel();
296                engineRoad = train.getThirdLegEngineRoad();
297            }
298            newLine(fileOut,
299                    MessageFormat.format(messageFormatText = TrainManifestText.getStringAddHelpers(),
300                            new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
301                                    numberEngines, endLocationName, engineModel, engineRoad}));
302        } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
303                ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
304                        (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE)) {
305            newLine(fileOut, MessageFormat.format(
306                    messageFormatText = TrainManifestText.getStringLocoAndCabooseChange(), new Object[]{
307                            rl.getSplitName(), train.getName(), train.getDescription(),
308                            rl.getLocation().getDivisionName()}));
309        } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
310            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringLocoChange(),
311                    new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
312                            rl.getLocation().getDivisionName()}));
313        } else if ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
314                (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
315            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringCabooseChange(),
316                    new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
317                            rl.getLocation().getDivisionName()}));
318        }
319    }
320
321    private void newLine(PrintWriter file, String string) {
322        if (!string.isEmpty()) {
323            newLine(file, string, IS_MANIFEST);
324        }
325    }
326}