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