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