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
073            if (!train.getCommentWithColor().equals(Train.NONE)) {
074                newLine(fileOut, train.getCommentWithColor());
075            }
076
077            List<Engine> engineList = engineManager.getByTrainBlockingList(train);
078
079            if (Setup.isPrintRouteCommentsEnabled() && !train.getRoute().getComment().equals(Route.NONE)) {
080                newLine(fileOut, train.getRoute().getComment());
081            }
082
083            List<Car> carList = carManager.getByTrainDestinationList(train);
084            log.debug("Train has {} cars assigned to it", carList.size());
085
086            boolean hadWork = false;
087            String previousRouteLocationName = null;
088            List<RouteLocation> routeList = train.getRoute().getLocationsBySequenceList();
089
090            /*
091             * Go through the train's route and print out the work for each
092             * location. Locations with "similar" names are combined to look
093             * like one location.
094             */
095            for (RouteLocation rl : routeList) {
096                boolean printHeader = false;
097                boolean hasWork = isThereWorkAtLocation(carList, engineList, rl);
098                // print info only if new location
099                String routeLocationName = rl.getSplitName();
100                if (!routeLocationName.equals(previousRouteLocationName) || (hasWork && !hadWork)) {
101                    if (hasWork) {
102                        newLine(fileOut);
103                        hadWork = true;
104                        printHeader = true;
105
106                        // add arrival message
107                        arrivalMessage(fileOut, train, rl);
108
109                        // add route location comment
110                        if (!rl.getComment().trim().equals(RouteLocation.NONE)) {
111                            newLine(fileOut, rl.getCommentWithColor());
112                        }
113
114                        // add location comment
115                        if (Setup.isPrintLocationCommentsEnabled() &&
116                                !rl.getLocation().getCommentWithColor().equals(Location.NONE)) {
117                            newLine(fileOut, rl.getLocation().getCommentWithColor());
118                        }
119                    }
120                }
121                // remember location name
122                previousRouteLocationName = routeLocationName;
123
124                // add track comments
125                printTrackComments(fileOut, rl, carList, IS_MANIFEST);
126
127                // engine change or helper service?
128                if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
129                    if (rl == train.getSecondLegStartRouteLocation()) {
130                        printChange(fileOut, rl, train, train.getSecondLegOptions());
131                    }
132                    if (rl == train.getSecondLegEndRouteLocation() &&
133                            train.getSecondLegOptions() == Train.HELPER_ENGINES) {
134                        newLine(fileOut,
135                                MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(),
136                                        new Object[]{rl.getSplitName(), train.getName(),
137                                                train.getDescription(), train.getSecondLegNumberEngines(),
138                                                train.getSecondLegEngineModel(), train.getSecondLegEngineRoad()}));
139                    }
140                }
141                if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
142                    if (rl == train.getThirdLegStartRouteLocation()) {
143                        printChange(fileOut, rl, train, train.getThirdLegOptions());
144                    }
145                    if (rl == train.getThirdLegEndRouteLocation() &&
146                            train.getThirdLegOptions() == Train.HELPER_ENGINES) {
147                        newLine(fileOut,
148                                MessageFormat.format(messageFormatText = TrainManifestText.getStringRemoveHelpers(),
149                                        new Object[]{rl.getSplitName(), train.getName(),
150                                                train.getDescription(), train.getThirdLegNumberEngines(),
151                                                train.getThirdLegEngineModel(), train.getThirdLegEngineRoad()}));
152                    }
153                }
154
155                if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
156                    pickupEngines(fileOut, engineList, rl, IS_MANIFEST);
157                    // if switcher show loco drop at end of list
158                    if (train.isLocalSwitcher()) {
159                        blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
160                        dropEngines(fileOut, engineList, rl, IS_MANIFEST);
161                    } else {
162                        dropEngines(fileOut, engineList, rl, IS_MANIFEST);
163                        blockCarsByTrack(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
164                    }
165                } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) {
166                    blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST);
167                    blockCarsTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
168                } else {
169                    blockLocosTwoColumn(fileOut, engineList, rl, IS_MANIFEST);
170                    blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, printHeader, IS_MANIFEST);
171                }
172
173                if (rl != train.getTrainTerminatesRouteLocation()) {
174                    // Is the next location the same as the current?
175                    RouteLocation rlNext = train.getRoute().getNextRouteLocation(rl);
176                    if (routeLocationName.equals(rlNext.getSplitName())) {
177                        continue;
178                    }
179                    departureMessage(fileOut, train, rl, hadWork);
180
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
207        fileOut.flush();
208        fileOut.close();
209
210        train.setModified(false);
211    }
212
213    private void arrivalMessage(PrintWriter fileOut, Train train, RouteLocation rl) {
214        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
215        String routeLocationName = rl.getSplitName();
216        // Scheduled work at {0}
217        String workAt = MessageFormat.format(messageFormatText = TrainManifestText
218                .getStringScheduledWork(),
219                new Object[]{routeLocationName, train.getName(),
220                        train.getDescription(), rl.getLocation().getDivisionName()});
221        if (!train.isShowArrivalAndDepartureTimesEnabled()) {
222            // Scheduled work at {0}
223            newLine(fileOut, workAt);
224        } else if (rl == train.getTrainDepartsRouteLocation()) {
225            // Scheduled work at {0}, departure time {1}
226            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
227                    .getStringWorkDepartureTime(),
228                    new Object[]{routeLocationName,
229                            train.getFormatedDepartureTime(), train.getName(),
230                            train.getDescription(), rl.getLocation().getDivisionName()}));
231        } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
232            // Scheduled work at {0}, departure time {1}
233            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
234                    .getStringWorkDepartureTime(),
235                    new Object[]{routeLocationName,
236                            rl.getFormatedDepartureTime(), train.getName(), train.getDescription(),
237                            rl.getLocation().getDivisionName()}));
238        } else if (Setup.isUseDepartureTimeEnabled() &&
239                rl != train.getTrainTerminatesRouteLocation()) {
240            // Scheduled work at {0}, departure time {1}
241            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
242                    .getStringWorkDepartureTime(),
243                    new Object[]{routeLocationName,
244                            train.getExpectedDepartureTime(rl), train.getName(),
245                            train.getDescription(), rl.getLocation().getDivisionName()}));
246        } else if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
247            // Scheduled work at {0}, arrival time {1}
248            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
249                    .getStringWorkArrivalTime(),
250                    new Object[]{routeLocationName, expectedArrivalTime,
251                            train.getName(), train.getDescription(),
252                            rl.getLocation().getDivisionName()}));
253        } else {
254            // Scheduled work at {0}
255            newLine(fileOut, workAt);
256        }
257    }
258
259    private void departureMessage(PrintWriter fileOut, Train train, RouteLocation rl, boolean hadWork) {
260        String routeLocationName = rl.getSplitName();
261        if (!hadWork) {
262            newLine(fileOut);
263            // No work at {0}
264            String s = MessageFormat.format(messageFormatText = TrainManifestText
265                    .getStringNoScheduledWork(),
266                    new Object[]{routeLocationName, train.getName(),
267                            train.getDescription(), rl.getLocation().getDivisionName()});
268            // if a route comment, then only use location name and route comment, useful for passenger
269            // trains
270            if (!rl.getComment().equals(RouteLocation.NONE)) {
271                s = routeLocationName;
272                if (!rl.getComment().isBlank()) {
273                    s = MessageFormat.format(messageFormatText = TrainManifestText
274                            .getStringNoScheduledWorkWithRouteComment(),
275                            new Object[]{routeLocationName, rl.getCommentWithColor(), train.getName(),
276                                    train.getDescription(), rl.getLocation().getDivisionName()});
277                }
278            }
279            // append arrival or departure time if enabled
280            if (train.isShowArrivalAndDepartureTimesEnabled()) {
281                if (rl == train.getTrainDepartsRouteLocation()) {
282                    s += MessageFormat.format(messageFormatText = TrainManifestText
283                            .getStringDepartTime(), new Object[]{train.getFormatedDepartureTime()});
284                } else if (!rl.getDepartureTime().equals(RouteLocation.NONE)) {
285                    s += MessageFormat.format(messageFormatText = TrainManifestText
286                            .getStringDepartTime(), new Object[]{rl.getFormatedDepartureTime()});
287                } else if (Setup.isUseDepartureTimeEnabled() &&
288                        !rl.getComment().equals(RouteLocation.NONE)) {
289                    s += MessageFormat
290                            .format(messageFormatText = TrainManifestText.getStringDepartTime(),
291                                    new Object[]{train.getExpectedDepartureTime(rl)});
292                }
293            }
294            newLine(fileOut, s);
295
296            // add location comment
297            if (Setup.isPrintLocationCommentsEnabled() &&
298                    !rl.getLocation().getCommentWithColor().equals(Location.NONE)) {
299                newLine(fileOut, rl.getLocation().getCommentWithColor());
300            }
301        } else if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
302            printHorizontalLine(fileOut, IS_MANIFEST);
303        }
304        if (Setup.isPrintLoadsAndEmptiesEnabled()) {
305            int emptyCars = train.getNumberEmptyCarsInTrain(rl);
306            // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons
307            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
308                    .getStringTrainDepartsLoads(),
309                    new Object[]{routeLocationName,
310                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl) - emptyCars,
311                            emptyCars,
312                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
313                            train.getTrainWeight(rl), train.getTrainTerminatesName(), train.getName()}));
314        } else {
315            // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
316            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText
317                    .getStringTrainDepartsCars(),
318                    new Object[]{routeLocationName,
319                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl),
320                            train.getTrainLength(rl),
321                            Setup.getLengthUnit().toLowerCase(), train.getTrainWeight(rl),
322                            train.getTrainTerminatesName(), train.getName()}));
323        }
324    }
325
326    private void printChange(PrintWriter fileOut, RouteLocation rl, Train train, int legOptions)
327            throws IllegalArgumentException {
328        if ((legOptions & Train.HELPER_ENGINES) == Train.HELPER_ENGINES) {
329            // assume 2nd leg for helper change
330            String numberEngines = train.getSecondLegNumberEngines();
331            String endLocationName = train.getSecondLegEndLocationName();
332            String engineModel = train.getSecondLegEngineModel();
333            String engineRoad = train.getSecondLegEngineRoad();
334            if (rl == train.getThirdLegStartRouteLocation()) {
335                numberEngines = train.getThirdLegNumberEngines();
336                endLocationName = train.getThirdLegEndLocationName();
337                engineModel = train.getThirdLegEngineModel();
338                engineRoad = train.getThirdLegEngineRoad();
339            }
340            newLine(fileOut,
341                    MessageFormat.format(messageFormatText = TrainManifestText.getStringAddHelpers(),
342                            new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
343                                    numberEngines, endLocationName, engineModel, engineRoad}));
344        } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES &&
345                ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
346                        (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE)) {
347            newLine(fileOut, MessageFormat.format(
348                    messageFormatText = TrainManifestText.getStringLocoAndCabooseChange(), new Object[]{
349                            rl.getSplitName(), train.getName(), train.getDescription(),
350                            rl.getLocation().getDivisionName()}));
351        } else if ((legOptions & Train.CHANGE_ENGINES) == Train.CHANGE_ENGINES) {
352            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringLocoChange(),
353                    new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
354                            rl.getLocation().getDivisionName()}));
355        } else if ((legOptions & Train.REMOVE_CABOOSE) == Train.REMOVE_CABOOSE ||
356                (legOptions & Train.ADD_CABOOSE) == Train.ADD_CABOOSE) {
357            newLine(fileOut, MessageFormat.format(messageFormatText = TrainManifestText.getStringCabooseChange(),
358                    new Object[]{rl.getSplitName(), train.getName(), train.getDescription(),
359                            rl.getLocation().getDivisionName()}));
360        }
361    }
362
363    private void newLine(PrintWriter file, String string) {
364        if (!string.isEmpty()) {
365            newLine(file, string, IS_MANIFEST);
366        }
367    }
368
369}