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.locations.Track;
015import jmri.jmrit.operations.rollingstock.cars.*;
016import jmri.jmrit.operations.rollingstock.engines.Engine;
017import jmri.jmrit.operations.routes.Route;
018import jmri.jmrit.operations.routes.RouteLocation;
019import jmri.jmrit.operations.setup.Control;
020import jmri.jmrit.operations.setup.Setup;
021import jmri.jmrit.operations.trains.schedules.TrainSchedule;
022import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
023import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
024import jmri.util.FileUtil;
025
026/**
027 * Builds a switch list for a location on the railroad
028 *
029 * @author Daniel Boudreau (C) Copyright 2008, 2011, 2012, 2013, 2015, 2024
030 */
031public class TrainSwitchLists extends TrainCommon {
032
033    TrainManager trainManager = InstanceManager.getDefault(TrainManager.class);
034    private static final char FORM_FEED = '\f';
035    private static final boolean IS_PRINT_HEADER = true;
036
037    String messageFormatText = ""; // the text being formated in case there's an exception
038
039    /**
040     * Builds a switch list for a location showing the work by train arrival
041     * time. If not running in real time, new train work is appended to the end
042     * of the file. User has the ability to modify the text of the messages
043     * which can cause an IllegalArgumentException. Some messages have more
044     * arguments than the default message allowing the user to customize the
045     * message to their liking. There also an option to list all of the car work
046     * by track name. This option is only available in real time and is shown
047     * after the switch list by train.
048     *
049     * @param location The Location needing a switch list
050     */
051    public void buildSwitchList(Location location) {
052
053        boolean append = false; // add text to end of file when true
054        boolean checkFormFeed = true; // used to determine if FF needed between trains
055
056        // Append switch list data if not operating in real time
057        if (!Setup.isSwitchListRealTime()) {
058            if (!location.getStatus().equals(Location.MODIFIED) && !Setup.isSwitchListAllTrainsEnabled()) {
059                return; // nothing to add
060            }
061            append = location.getSwitchListState() == Location.SW_APPEND;
062            location.setSwitchListState(Location.SW_APPEND);
063        }
064
065        log.debug("Append: {} for location ({})", append, location.getName());
066
067        // create switch list file
068        File file = InstanceManager.getDefault(TrainManagerXml.class).createSwitchListFile(location.getName());
069
070        PrintWriter fileOut = null;
071        try {
072            fileOut = new PrintWriter(new BufferedWriter(
073                    new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8)), true);
074        } catch (IOException e) {
075            log.error("Can not open switchlist file: {}", e.getLocalizedMessage());
076            return;
077        }
078        try {
079            // build header
080            if (!append) {
081                newLine(fileOut, Setup.getRailroadName());
082                newLine(fileOut);
083                newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListFor(),
084                        new Object[]{location.getSplitName()}));
085                if (!location.getSwitchListCommentWithColor().isEmpty()) {
086                    newLine(fileOut, location.getSwitchListCommentWithColor());
087                }
088            } else {
089                newLine(fileOut);
090            }
091
092            // get a list of built trains sorted by arrival time
093            List<Train> trains = trainManager.getTrainsArrivingThisLocationList(location);
094            for (Train train : trains) {
095                if (!Setup.isSwitchListRealTime() && train.getSwitchListStatus().equals(Train.PRINTED)) {
096                    continue; // already printed this train
097                }
098                Route route = train.getRoute();
099                // TODO throw exception? only built trains should be in the list, so no route is
100                // an error
101                if (route == null) {
102                    continue; // no route for this train
103                } // determine if train works this location
104                boolean works = isThereWorkAtLocation(train, location);
105                if (!works && !Setup.isSwitchListAllTrainsEnabled()) {
106                    log.debug("No work for train ({}) at location ({})", train.getName(), location.getName());
107                    continue;
108                }
109                // we're now going to add to the switch list
110                if (checkFormFeed) {
111                    if (append && !Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
112                        fileOut.write(FORM_FEED);
113                    }
114                    if (Setup.isPrintValidEnabled()) {
115                        newLine(fileOut, getValid());
116                    }
117                } else if (!Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
118                    fileOut.write(FORM_FEED);
119                }
120                checkFormFeed = false; // done with FF for this train
121                // some cars booleans and the number of times this location get's serviced
122                _pickupCars = false; // when true there was a car pick up
123                _dropCars = false; // when true there was a car set out
124                int stops = 1;
125                boolean trainDone = false;
126                // get engine and car lists
127                List<Engine> engineList = engineManager.getByTrainBlockingList(train);
128                List<Car> carList = carManager.getByTrainDestinationList(train);
129                List<RouteLocation> routeList = route.getLocationsBySequenceList();
130                RouteLocation rlPrevious = null;
131                // does the train stop once or more at this location?
132                for (RouteLocation rl : routeList) {
133                    if (!rl.getSplitName().equals(location.getSplitName())) {
134                        rlPrevious = rl;
135                        continue;
136                    }
137                    if (train.getExpectedArrivalTime(rl).equals(Train.ALREADY_SERVICED) &&
138                            train.getCurrentRouteLocation() != rl) {
139                        trainDone = true;
140                    }
141                    // first time at this location?
142                    if (stops == 1) {
143                        firstTimeMessages(fileOut, train, rl);
144                        stops++;
145                    } else {
146                        // multiple visits to this location
147                        // Print visit number only if previous location isn't the same
148                        if (rlPrevious == null ||
149                                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
150                            multipleVisitMessages(fileOut, train, rl, rlPrevious, stops);
151                            stops++;
152                        } else {
153                            // don't bump stop count, same location
154                            // Does the train reverse direction?
155                            reverseDirectionMessage(fileOut, train, rl, rlPrevious);
156                        }
157                    }
158
159                    // save current location in case there's back to back location with the same name
160                    rlPrevious = rl;
161
162                    // add route location comment
163                    if (Setup.isSwitchListRouteLocationCommentEnabled() && !rl.getComment().trim().isEmpty()) {
164                        newLine(fileOut, rl.getCommentWithColor());
165                    }
166
167                    printTrackComments(fileOut, rl, carList, !IS_MANIFEST);
168
169                    if (isThereWorkAtLocation(carList, engineList, rl)) {
170                        // now print out the work for this location
171                        if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
172                            pickupEngines(fileOut, engineList, rl, !IS_MANIFEST);
173                            // if switcher show loco drop at end of list
174                            if (train.isLocalSwitcher() || Setup.isPrintLocoLastEnabled()) {
175                                blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
176                                dropEngines(fileOut, engineList, rl, !IS_MANIFEST);
177                            } else {
178                                dropEngines(fileOut, engineList, rl, !IS_MANIFEST);
179                                blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
180                            }
181                        } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) {
182                            blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST);
183                            blockCarsTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
184                        } else {
185                            blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST);
186                            blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
187                        }
188                        // print horizontal line if there was work and enabled
189                        if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
190                            printHorizontalLine(fileOut, !IS_MANIFEST);
191                        }
192                    }
193
194                    // done with work, now print summary for this location if we're done
195                    if (rl != train.getTrainTerminatesRouteLocation()) {
196                        RouteLocation nextRl = train.getRoute().getNextRouteLocation(rl);
197                        if (rl.getSplitName().equals(nextRl.getSplitName())) {
198                            continue; // the current location name is the "same" as the next
199                        }
200                        // print departure text if not a switcher
201                        if (!train.isLocalSwitcher() && !trainDone) {
202                            departureMessages(fileOut, train, rl);
203                        }
204                    }
205                }
206                // report if no pick ups or set outs or train has left
207                trainSummaryMessages(fileOut, train, location, trainDone, stops);
208            }
209
210            // now report car movement by tracks at location
211            reportByTrack(fileOut, location);
212
213        } catch (IllegalArgumentException e) {
214            newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument",
215                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()));
216            newLine(fileOut, messageFormatText);
217            log.error("Illegal argument", e);
218        }
219
220        // Are there any cars that need to be found?
221        addCarsLocationUnknown(fileOut, !IS_MANIFEST);
222        fileOut.flush();
223        fileOut.close();
224        location.setStatus(Location.UPDATED);
225    }
226
227    private String getValid() {
228        String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(),
229                new Object[]{getDate(true)});
230        if (Setup.isPrintTrainScheduleNameEnabled()) {
231            TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule();
232            if (sch != null) {
233                valid = valid + " (" + sch.getName() + ")";
234            }
235        }
236        return valid;
237    }
238
239    /*
240     * Messages for the switch list when the train first arrives
241     */
242    private void firstTimeMessages(PrintWriter fileOut, Train train, RouteLocation rl) {
243        newLine(fileOut);
244        newLine(fileOut,
245                MessageFormat.format(messageFormatText = TrainSwitchListText.getStringScheduledWork(),
246                        new Object[]{train.getName(), train.getDescription()}));
247        newLine(fileOut, getSwitchListTrainStatus(train, rl));
248    }
249
250    /*
251     * Messages when a train services the location two or more times
252     */
253    private void multipleVisitMessages(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious,
254            int stops) {
255        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
256        if (rlPrevious == null ||
257                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
258            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_PER_VISIT)) {
259                fileOut.write(FORM_FEED);
260            }
261            newLine(fileOut);
262            if (train.isTrainEnRoute()) {
263                if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
264                    // Visit number {0} for train ({1})
265                    newLine(fileOut,
266                            MessageFormat.format(
267                                    messageFormatText = TrainSwitchListText.getStringVisitNumberDone(),
268                                    new Object[]{stops, train.getName(), train.getDescription()}));
269                } else if (rl != train.getTrainTerminatesRouteLocation()) {
270                    // Visit number {0} for train ({1}) expect to arrive in {2}, arrives {3}bound
271                    newLine(fileOut, MessageFormat.format(
272                            messageFormatText = TrainSwitchListText.getStringVisitNumberDeparted(),
273                            new Object[]{stops, train.getName(), expectedArrivalTime,
274                                    rl.getTrainDirectionString(), train.getDescription()}));
275                } else {
276                    // Visit number {0} for train ({1}) expect to arrive in {2}, terminates {3}
277                    newLine(fileOut,
278                            MessageFormat.format(
279                                    messageFormatText = TrainSwitchListText
280                                            .getStringVisitNumberTerminatesDeparted(),
281                                    new Object[]{stops, train.getName(), expectedArrivalTime,
282                                            rl.getSplitName(), train.getDescription()}));
283                }
284            } else {
285                // train hasn't departed
286                if (rl != train.getTrainTerminatesRouteLocation()) {
287                    // Visit number {0} for train ({1}) expected arrival {2}, arrives {3}bound
288                    newLine(fileOut,
289                            MessageFormat.format(
290                                    messageFormatText = TrainSwitchListText.getStringVisitNumber(),
291                                    new Object[]{stops, train.getName(), expectedArrivalTime,
292                                            rl.getTrainDirectionString(), train.getDescription()}));
293                    if (Setup.isUseSwitchListDepartureTimeEnabled()) {
294                        // Departs {0} {1}bound at {2}
295                        newLine(fileOut, MessageFormat.format(
296                                messageFormatText = TrainSwitchListText.getStringDepartsAt(),
297                                new Object[]{splitString(rl.getName()),
298                                        rl.getTrainDirectionString(),
299                                        train.getExpectedDepartureTime(rl)}));
300                    }
301                } else {
302                    // Visit number {0} for train ({1}) expected arrival {2}, terminates {3}
303                    newLine(fileOut, MessageFormat.format(
304                            messageFormatText = TrainSwitchListText.getStringVisitNumberTerminates(),
305                            new Object[]{stops, train.getName(), expectedArrivalTime,
306                                    rl.getSplitName(), train.getDescription()}));
307                }
308            }
309        }
310    }
311
312    private void reverseDirectionMessage(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious) {
313        // Does the train reverse direction?
314        if (rl.getTrainDirection() != rlPrevious.getTrainDirection() &&
315                !TrainSwitchListText.getStringTrainDirectionChange().isEmpty()) {
316            // Train ({0}) direction change, departs {1}bound
317            newLine(fileOut,
318                    MessageFormat.format(
319                            messageFormatText = TrainSwitchListText.getStringTrainDirectionChange(),
320                            new Object[]{train.getName(), rl.getTrainDirectionString(),
321                                    train.getDescription(), train.getTrainTerminatesName()}));
322        }
323    }
324
325    /*
326     * Train departure messages at the end of the switch list
327     */
328    private void departureMessages(PrintWriter fileOut, Train train, RouteLocation rl) {
329        String trainDeparts = "";
330        if (Setup.isPrintLoadsAndEmptiesEnabled()) {
331            int emptyCars = train.getNumberEmptyCarsInTrain(rl);
332            // Train departs {0} {1}bound with {2} loads, {3} empties, {4} {5}, {6} tons
333            trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsLoads(),
334                    new Object[]{rl.getSplitName(),
335                            rl.getTrainDirectionString(),
336                            train.getNumberCarsInTrain(rl) - emptyCars, emptyCars,
337                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
338                            train.getTrainWeight(rl), train.getTrainTerminatesName(),
339                            train.getName()});
340        } else {
341            // Train departs {0} {1}bound with {2} cars, {3} {4}, {5} tons
342            trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsCars(),
343                    new Object[]{rl.getSplitName(),
344                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl),
345                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
346                            train.getTrainWeight(rl), train.getTrainTerminatesName(),
347                            train.getName()});
348        }
349        newLine(fileOut, trainDeparts);
350    }
351
352    private void trainSummaryMessages(PrintWriter fileOut, Train train, Location location, boolean trainDone,
353            int stops) {
354        if (trainDone && !_pickupCars && !_dropCars) {
355            // Default message: Train ({0}) has serviced this location
356            newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringTrainDone(),
357                    new Object[]{train.getName(), train.getDescription(), location.getSplitName()}));
358        } else {
359            if (stops > 1 && !_pickupCars) {
360                // Default message: No car pick ups for train ({0}) at this location
361                newLine(fileOut,
362                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(),
363                                new Object[]{train.getName(), train.getDescription(),
364                                        location.getSplitName()}));
365            }
366            if (stops > 1 && !_dropCars) {
367                // Default message: No car set outs for train ({0}) at this location
368                newLine(fileOut,
369                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(),
370                                new Object[]{train.getName(), train.getDescription(),
371                                        location.getSplitName()}));
372            }
373        }
374    }
375
376    private void reportByTrack(PrintWriter fileOut, Location location) {
377        if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) {
378            clearUtilityCarTypes(); // list utility cars by quantity
379            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
380                newLine(fileOut);
381                newLine(fileOut);
382            } else {
383                fileOut.write(FORM_FEED);
384            }
385            newLine(fileOut,
386                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(),
387                            new Object[]{location.getSplitName()}));
388
389            // we only need the cars delivered to or at this location
390            List<Car> rsList = carManager.getByTrainList();
391            List<Car> carList = new ArrayList<>();
392            for (Car rs : rsList) {
393                if ((rs.getLocation() != null &&
394                        rs.getLocation().getSplitName().equals(location.getSplitName())) ||
395                        (rs.getDestination() != null &&
396                                rs.getSplitDestinationName().equals(location.getSplitName())))
397                    carList.add(rs);
398            }
399
400            List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list
401                                                         // track names once
402            for (Location loc : locationManager.getLocationsByNameList()) {
403                if (!loc.getSplitName().equals(location.getSplitName()))
404                    continue;
405                for (Track track : loc.getTracksByBlockingOrderList(null)) {
406                    String trackName = track.getSplitName();
407                    if (trackNames.contains(trackName))
408                        continue;
409                    trackNames.add(trackName);
410
411                    String trainName = ""; // for printing train message once
412                    newLine(fileOut);
413                    newLine(fileOut, trackName); // print out just the track name
414                    // now show the cars pickup and holds for this track
415                    for (Car car : carList) {
416                        if (!car.getSplitTrackName().equals(trackName)) {
417                            continue;
418                        }
419                        // is the car scheduled for pickup?
420                        if (car.getRouteLocation() != null) {
421                            if (car.getRouteLocation().getLocation().getSplitName()
422                                    .equals(location.getSplitName())) {
423                                // cars are sorted by train name, print train message once
424                                if (!trainName.equals(car.getTrainName())) {
425                                    trainName = car.getTrainName();
426                                    newLine(fileOut, MessageFormat.format(
427                                            messageFormatText = TrainSwitchListText.getStringScheduledWork(),
428                                            new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
429                                    printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
430                                }
431                                if (car.isUtility()) {
432                                    pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
433                                } else {
434                                    pickUpCar(fileOut, car, !IS_MANIFEST);
435                                }
436                            }
437                            // car holds
438                        } else if (car.isUtility()) {
439                            String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
440                            if (s != null) {
441                                newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N
442                            }
443                        } else {
444                            newLine(fileOut,
445                                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(),
446                                            new Object[]{
447                                                    padAndTruncateIfNeeded(car.getRoadName(),
448                                                            InstanceManager.getDefault(CarRoads.class)
449                                                                    .getMaxNameLength()),
450                                                    padAndTruncateIfNeeded(
451                                                            TrainCommon.splitString(car.getNumber()),
452                                                            Control.max_len_string_print_road_number),
453                                                    padAndTruncateIfNeeded(
454                                                            car.getTypeName().split(TrainCommon.HYPHEN)[0],
455                                                            InstanceManager.getDefault(CarTypes.class)
456                                                                    .getMaxNameLength()),
457                                                    padAndTruncateIfNeeded(
458                                                            car.getLength() + Setup.getLengthUnitAbv(),
459                                                            Control.max_len_string_length_name),
460                                                    padAndTruncateIfNeeded(car.getLoadName(),
461                                                            InstanceManager.getDefault(CarLoads.class)
462                                                                    .getMaxNameLength()),
463                                                    padAndTruncateIfNeeded(trackName,
464                                                            locationManager.getMaxTrackNameLength()),
465                                                    padAndTruncateIfNeeded(car.getColor(), InstanceManager
466                                                            .getDefault(CarColors.class).getMaxNameLength())}));
467                        }
468                    }
469                    // now do set outs at this location
470                    for (Car car : carList) {
471                        if (!car.getSplitDestinationTrackName().equals(trackName)) {
472                            continue;
473                        }
474                        if (car.getRouteDestination() != null &&
475                                car.getRouteDestination().getLocation().getSplitName()
476                                        .equals(location.getSplitName())) {
477                            // cars are sorted by train name, print train message once
478                            if (!trainName.equals(car.getTrainName())) {
479                                trainName = car.getTrainName();
480                                newLine(fileOut, MessageFormat.format(
481                                        messageFormatText = TrainSwitchListText.getStringScheduledWork(),
482                                        new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
483                                printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
484                            }
485                            if (car.isUtility()) {
486                                setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
487                            } else {
488                                dropCar(fileOut, car, !IS_MANIFEST);
489                            }
490                        }
491                    }
492                }
493            }
494        }
495    }
496
497    public void printSwitchList(Location location, boolean isPreview) {
498        File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName());
499        if (!switchListFile.exists()) {
500            log.warn("Switch list file missing for location ({})", location.getName());
501            return;
502        }
503        if (isPreview && Setup.isManifestEditorEnabled()) {
504            TrainUtilities.openDesktop(switchListFile);
505        } else {
506            TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false,
507                    FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(),
508                    Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(),
509                    Setup.getPrintDuplexSides());
510        }
511        if (!isPreview) {
512            location.setStatus(Location.PRINTED);
513            location.setSwitchListState(Location.SW_PRINTED);
514        }
515    }
516
517    protected void newLine(PrintWriter file, String string) {
518        if (!string.isEmpty()) {
519            newLine(file, string, !IS_MANIFEST);
520        }
521    }
522
523    private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class);
524}