java/src/jmri/jmrit/operations/trains/TrainSwitchLists.java

Summary

Maintainability
F
6 days
Test Coverage
A
94%
package jmri.jmrit.operations.trains;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jmri.InstanceManager;
import jmri.jmrit.operations.locations.Location;
import jmri.jmrit.operations.locations.Track;
import jmri.jmrit.operations.rollingstock.cars.*;
import jmri.jmrit.operations.rollingstock.engines.Engine;
import jmri.jmrit.operations.routes.Route;
import jmri.jmrit.operations.routes.RouteLocation;
import jmri.jmrit.operations.setup.Control;
import jmri.jmrit.operations.setup.Setup;
import jmri.jmrit.operations.trains.schedules.TrainSchedule;
import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
import jmri.util.FileUtil;

/**
 * Builds a switch list for a location on the railroad
 *
 * @author Daniel Boudreau (C) Copyright 2008, 2011, 2012, 2013, 2015, 2024
 */
public class TrainSwitchLists extends TrainCommon {

    TrainManager trainManager = InstanceManager.getDefault(TrainManager.class);
    private static final char FORM_FEED = '\f';
    private static final boolean IS_PRINT_HEADER = true;

    String messageFormatText = ""; // the text being formated in case there's an exception

    /**
     * Builds a switch list for a location showing the work by train arrival
     * time. If not running in real time, new train work is appended to the end
     * of the file. User has the ability to modify the text of the messages
     * which can cause an IllegalArgumentException. Some messages have more
     * arguments than the default message allowing the user to customize the
     * message to their liking. There also an option to list all of the car work
     * by track name. This option is only available in real time and is shown
     * after the switch list by train.
     *
     * @param location The Location needing a switch list
     */
    public void buildSwitchList(Location location) {

        boolean append = false; // add text to end of file when true
        boolean checkFormFeed = true; // used to determine if FF needed between trains

        // Append switch list data if not operating in real time
        if (!Setup.isSwitchListRealTime()) {
            if (!location.getStatus().equals(Location.MODIFIED) && !Setup.isSwitchListAllTrainsEnabled()) {
                return; // nothing to add
            }
            append = location.getSwitchListState() == Location.SW_APPEND;
            location.setSwitchListState(Location.SW_APPEND);
        }

        log.debug("Append: {} for location ({})", append, location.getName());

        // create switch list file
        File file = InstanceManager.getDefault(TrainManagerXml.class).createSwitchListFile(location.getName());

        PrintWriter fileOut = null;
        try {
            fileOut = new PrintWriter(new BufferedWriter(
                    new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8)), true);
        } catch (IOException e) {
            log.error("Can not open switchlist file: {}", file.getName());
            return;
        }
        try {
            // build header
            if (!append) {
                newLine(fileOut, Setup.getRailroadName());
                newLine(fileOut);
                newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListFor(),
                        new Object[]{location.getSplitName()}));
                if (!location.getSwitchListCommentWithColor().isEmpty()) {
                    newLine(fileOut, location.getSwitchListCommentWithColor());
                }
            } else {
                newLine(fileOut);
            }

            // get a list of built trains sorted by arrival time
            List<Train> trains = trainManager.getTrainsArrivingThisLocationList(location);
            for (Train train : trains) {
                if (!Setup.isSwitchListRealTime() && train.getSwitchListStatus().equals(Train.PRINTED)) {
                    continue; // already printed this train
                }
                Route route = train.getRoute();
                // TODO throw exception? only built trains should be in the list, so no route is
                // an error
                if (route == null) {
                    continue; // no route for this train
                } // determine if train works this location
                boolean works = isThereWorkAtLocation(train, location);
                if (!works && !Setup.isSwitchListAllTrainsEnabled()) {
                    log.debug("No work for train ({}) at location ({})", train.getName(), location.getName());
                    continue;
                }
                // we're now going to add to the switch list
                if (checkFormFeed) {
                    if (append && !Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
                        fileOut.write(FORM_FEED);
                    }
                    if (Setup.isPrintValidEnabled()) {
                        newLine(fileOut, getValid());
                    }
                } else if (!Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
                    fileOut.write(FORM_FEED);
                }
                checkFormFeed = false; // done with FF for this train
                // some cars booleans and the number of times this location get's serviced
                _pickupCars = false; // when true there was a car pick up
                _dropCars = false; // when true there was a car set out
                int stops = 1;
                boolean trainDone = false;
                // get engine and car lists
                List<Engine> engineList = engineManager.getByTrainBlockingList(train);
                List<Car> carList = carManager.getByTrainDestinationList(train);
                List<RouteLocation> routeList = route.getLocationsBySequenceList();
                RouteLocation rlPrevious = null;
                // does the train stop once or more at this location?
                for (RouteLocation rl : routeList) {
                    if (!rl.getSplitName().equals(location.getSplitName())) {
                        rlPrevious = rl;
                        continue;
                    }
                    if (train.getExpectedArrivalTime(rl).equals(Train.ALREADY_SERVICED)) {
                        trainDone = true;
                    }
                    // first time at this location?
                    if (stops == 1) {
                        firstTimeMessages(fileOut, train, rl);
                        stops++;
                    } else {
                        // multiple visits to this location
                        // Print visit number only if previous location isn't the same
                        if (rlPrevious == null ||
                                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
                            multipleVisitMessages(fileOut, train, rl, rlPrevious, stops);
                            stops++;
                        } else {
                            // don't bump stop count, same location
                            // Does the train reverse direction?
                            reverseDirectionMessage(fileOut, train, rl, rlPrevious);
                        }
                    }

                    // save current location in case there's back to back location with the same name
                    rlPrevious = rl;

                    // add route comment
                    if (Setup.isSwitchListRouteLocationCommentEnabled() && !rl.getComment().trim().isEmpty()) {
                        newLine(fileOut, rl.getCommentWithColor());
                    }

                    printTrackComments(fileOut, rl, carList, !IS_MANIFEST);

                    if (isThereWorkAtLocation(carList, engineList, rl)) {
                        // now print out the work for this location
                        if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
                            pickupEngines(fileOut, engineList, rl, !IS_MANIFEST);
                            // if switcher show loco drop at end of list
                            if (train.isLocalSwitcher()) {
                                blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
                                dropEngines(fileOut, engineList, rl, !IS_MANIFEST);
                            } else {
                                dropEngines(fileOut, engineList, rl, !IS_MANIFEST);
                                blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
                            }
                        } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) {
                            blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST);
                            blockCarsTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
                        } else {
                            blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST);
                            blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
                        }
                        // print horizontal line if there was work and enabled
                        if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
                            printHorizontalLine(fileOut, !IS_MANIFEST);
                        }
                    }

                    // done with work, now print summary for this location if we're done
                    if (rl != train.getTrainTerminatesRouteLocation()) {
                        RouteLocation nextRl = train.getRoute().getNextRouteLocation(rl);
                        if (rl.getSplitName().equals(nextRl.getSplitName())) {
                            continue; // the current location name is the "same" as the next
                        }
                        // print departure text if not a switcher
                        if (!train.isLocalSwitcher() && !trainDone) {
                            departureMessages(fileOut, train, rl);
                        }
                    }
                }
                // report if no pick ups or set outs or train has left
                trainSummaryMessages(fileOut, train, location, trainDone, stops);
            }

            // now report car movement by tracks at location
            reportByTrack(fileOut, location);

        } catch (IllegalArgumentException e) {
            newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument",
                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()));
            newLine(fileOut, messageFormatText);
            log.error("Illegal argument", e);
        }

        // Are there any cars that need to be found?
        addCarsLocationUnknown(fileOut, !IS_MANIFEST);
        fileOut.flush();
        fileOut.close();
        location.setStatus(Location.UPDATED);
    }

    private String getValid() {
        String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(),
                new Object[]{getDate(true)});
        if (Setup.isPrintTrainScheduleNameEnabled()) {
            TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule();
            if (sch != null) {
                valid = valid + " (" + sch.getName() + ")";
            }
        }
        return valid;
    }

    /*
     * Messages for the switch list when the train first arrives
     */
    private void firstTimeMessages(PrintWriter fileOut, Train train, RouteLocation rl) {
        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
        newLine(fileOut);
        newLine(fileOut,
                MessageFormat.format(messageFormatText = TrainSwitchListText.getStringScheduledWork(),
                        new Object[]{train.getName(), train.getDescription()}));
        if (train.isTrainEnRoute()) {
            if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
                // Departed {0}, expect to arrive in {1}, arrives {2}bound
                newLine(fileOut,
                        MessageFormat.format(
                                messageFormatText = TrainSwitchListText.getStringDepartedExpected(),
                                new Object[]{splitString(train.getTrainDepartsName()),
                                        expectedArrivalTime, rl.getTrainDirectionString(),
                                        train.getCurrentLocationName()}));
            }
        } else if (!train.isLocalSwitcher()) {
            // train hasn't departed
            if (rl == train.getTrainDepartsRouteLocation()) {
                // Departs {0} {1}bound at {2}
                newLine(fileOut, MessageFormat.format(
                        messageFormatText = TrainSwitchListText.getStringDepartsAt(),
                        new Object[]{splitString(train.getTrainDepartsName()),
                                rl.getTrainDirectionString(),
                                train.getFormatedDepartureTime()}));
            } else if (Setup.isUseSwitchListDepartureTimeEnabled() &&
                    rl != train.getTrainTerminatesRouteLocation()) {
                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
                newLine(fileOut, MessageFormat.format(
                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
                        new Object[]{splitString(rl.getName()),
                                train.getExpectedDepartureTime(rl), expectedArrivalTime,
                                rl.getTrainDirectionString()}));
            } else {
                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
                newLine(fileOut, MessageFormat.format(
                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
                        new Object[]{splitString(train.getTrainDepartsName()),
                                train.getFormatedDepartureTime(), expectedArrivalTime,
                                rl.getTrainDirectionString()}));
            }
        }
    }

    /*
     * Messages when a train services the location two or more times
     */
    private void multipleVisitMessages(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious,
            int stops) {
        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
        if (rlPrevious == null ||
                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_PER_VISIT)) {
                fileOut.write(FORM_FEED);
            }
            newLine(fileOut);
            if (train.isTrainEnRoute()) {
                if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
                    // Visit number {0} for train ({1})
                    newLine(fileOut,
                            MessageFormat.format(
                                    messageFormatText = TrainSwitchListText.getStringVisitNumberDone(),
                                    new Object[]{stops, train.getName(), train.getDescription()}));
                } else if (rl != train.getTrainTerminatesRouteLocation()) {
                    // Visit number {0} for train ({1}) expect to arrive in {2}, arrives {3}bound
                    newLine(fileOut, MessageFormat.format(
                            messageFormatText = TrainSwitchListText.getStringVisitNumberDeparted(),
                            new Object[]{stops, train.getName(), expectedArrivalTime,
                                    rl.getTrainDirectionString(), train.getDescription()}));
                } else {
                    // Visit number {0} for train ({1}) expect to arrive in {2}, terminates {3}
                    newLine(fileOut,
                            MessageFormat.format(
                                    messageFormatText = TrainSwitchListText
                                            .getStringVisitNumberTerminatesDeparted(),
                                    new Object[]{stops, train.getName(), expectedArrivalTime,
                                            rl.getSplitName(), train.getDescription()}));
                }
            } else {
                // train hasn't departed
                if (rl != train.getTrainTerminatesRouteLocation()) {
                    // Visit number {0} for train ({1}) expected arrival {2}, arrives {3}bound
                    newLine(fileOut,
                            MessageFormat.format(
                                    messageFormatText = TrainSwitchListText.getStringVisitNumber(),
                                    new Object[]{stops, train.getName(), expectedArrivalTime,
                                            rl.getTrainDirectionString(), train.getDescription()}));
                    if (Setup.isUseSwitchListDepartureTimeEnabled()) {
                        // Departs {0} {1}bound at {2}
                        newLine(fileOut, MessageFormat.format(
                                messageFormatText = TrainSwitchListText.getStringDepartsAt(),
                                new Object[]{splitString(rl.getName()),
                                        rl.getTrainDirectionString(),
                                        train.getExpectedDepartureTime(rl)}));
                    }
                } else {
                    // Visit number {0} for train ({1}) expected arrival {2}, terminates {3}
                    newLine(fileOut, MessageFormat.format(
                            messageFormatText = TrainSwitchListText.getStringVisitNumberTerminates(),
                            new Object[]{stops, train.getName(), expectedArrivalTime,
                                    rl.getSplitName(), train.getDescription()}));
                }
            }
        }
    }

    private void reverseDirectionMessage(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious) {
        // Does the train reverse direction?
        if (rl.getTrainDirection() != rlPrevious.getTrainDirection() &&
                !TrainSwitchListText.getStringTrainDirectionChange().isEmpty()) {
            // Train ({0}) direction change, departs {1}bound
            newLine(fileOut,
                    MessageFormat.format(
                            messageFormatText = TrainSwitchListText.getStringTrainDirectionChange(),
                            new Object[]{train.getName(), rl.getTrainDirectionString(),
                                    train.getDescription(), train.getTrainTerminatesName()}));
        }
    }

    /*
     * Train departure messages at the end of the switch list
     */
    private void departureMessages(PrintWriter fileOut, Train train, RouteLocation rl) {
        String trainDeparts = "";
        if (Setup.isPrintLoadsAndEmptiesEnabled()) {
            int emptyCars = train.getNumberEmptyCarsInTrain(rl);
            // Train departs {0} {1}bound with {2} loads, {3} empties, {4} {5}, {6} tons
            trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsLoads(),
                    new Object[]{rl.getSplitName(),
                            rl.getTrainDirectionString(),
                            train.getNumberCarsInTrain(rl) - emptyCars, emptyCars,
                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
                            train.getTrainWeight(rl), train.getTrainTerminatesName(),
                            train.getName()});
        } else {
            // Train departs {0} {1}bound with {2} cars, {3} {4}, {5} tons
            trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsCars(),
                    new Object[]{rl.getSplitName(),
                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl),
                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
                            train.getTrainWeight(rl), train.getTrainTerminatesName(),
                            train.getName()});
        }
        newLine(fileOut, trainDeparts);
    }

    private void trainSummaryMessages(PrintWriter fileOut, Train train, Location location, boolean trainDone,
            int stops) {
        if (trainDone && !_pickupCars && !_dropCars) {
            // Default message: Train ({0}) has serviced this location
            newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringTrainDone(),
                    new Object[]{train.getName(), train.getDescription(), location.getSplitName()}));
        } else {
            if (stops > 1 && !_pickupCars) {
                // Default message: No car pick ups for train ({0}) at this location
                newLine(fileOut,
                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(),
                                new Object[]{train.getName(), train.getDescription(),
                                        location.getSplitName()}));
            }
            if (stops > 1 && !_dropCars) {
                // Default message: No car set outs for train ({0}) at this location
                newLine(fileOut,
                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(),
                                new Object[]{train.getName(), train.getDescription(),
                                        location.getSplitName()}));
            }
        }
    }

    private void reportByTrack(PrintWriter fileOut, Location location) {
        if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) {
            clearUtilityCarTypes(); // list utility cars by quantity
            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
                newLine(fileOut);
                newLine(fileOut);
            } else {
                fileOut.write(FORM_FEED);
            }
            newLine(fileOut,
                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(),
                            new Object[]{location.getSplitName()}));

            // we only need the cars delivered to or at this location
            List<Car> rsList = carManager.getByTrainList();
            List<Car> carList = new ArrayList<>();
            for (Car rs : rsList) {
                if ((rs.getLocation() != null &&
                        rs.getLocation().getSplitName().equals(location.getSplitName())) ||
                        (rs.getDestination() != null &&
                                rs.getSplitDestinationName().equals(location.getSplitName())))
                    carList.add(rs);
            }

            List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list
                                                         // track names once
            for (Location loc : locationManager.getLocationsByNameList()) {
                if (!loc.getSplitName().equals(location.getSplitName()))
                    continue;
                for (Track track : loc.getTracksByNameList(null)) {
                    String trackName = track.getSplitName();
                    if (trackNames.contains(trackName))
                        continue;
                    trackNames.add(trackName);

                    String trainName = ""; // for printing train message once
                    newLine(fileOut);
                    newLine(fileOut, trackName); // print out just the track name
                    // now show the cars pickup and holds for this track
                    for (Car car : carList) {
                        if (!car.getSplitTrackName().equals(trackName)) {
                            continue;
                        }
                        // is the car scheduled for pickup?
                        if (car.getRouteLocation() != null) {
                            if (car.getRouteLocation().getLocation().getSplitName()
                                    .equals(location.getSplitName())) {
                                // cars are sorted by train name, print train message once
                                if (!trainName.equals(car.getTrainName())) {
                                    trainName = car.getTrainName();
                                    newLine(fileOut, MessageFormat.format(
                                            messageFormatText = TrainSwitchListText.getStringScheduledWork(),
                                            new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
                                    printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
                                }
                                if (car.isUtility()) {
                                    pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
                                } else {
                                    pickUpCar(fileOut, car, !IS_MANIFEST);
                                }
                            }
                            // car holds
                        } else if (car.isUtility()) {
                            String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
                            if (s != null) {
                                newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N
                            }
                        } else {
                            newLine(fileOut,
                                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(),
                                            new Object[]{
                                                    padAndTruncateIfNeeded(car.getRoadName(),
                                                            InstanceManager.getDefault(CarRoads.class)
                                                                    .getMaxNameLength()),
                                                    padAndTruncateIfNeeded(
                                                            TrainCommon.splitString(car.getNumber()),
                                                            Control.max_len_string_print_road_number),
                                                    padAndTruncateIfNeeded(
                                                            car.getTypeName().split(TrainCommon.HYPHEN)[0],
                                                            InstanceManager.getDefault(CarTypes.class)
                                                                    .getMaxNameLength()),
                                                    padAndTruncateIfNeeded(
                                                            car.getLength() + Setup.getLengthUnitAbv(),
                                                            Control.max_len_string_length_name),
                                                    padAndTruncateIfNeeded(car.getLoadName(),
                                                            InstanceManager.getDefault(CarLoads.class)
                                                                    .getMaxNameLength()),
                                                    padAndTruncateIfNeeded(trackName,
                                                            locationManager.getMaxTrackNameLength()),
                                                    padAndTruncateIfNeeded(car.getColor(), InstanceManager
                                                            .getDefault(CarColors.class).getMaxNameLength())}));
                        }
                    }
                    // now do set outs at this location
                    for (Car car : carList) {
                        if (!car.getSplitDestinationTrackName().equals(trackName)) {
                            continue;
                        }
                        if (car.getRouteDestination() != null &&
                                car.getRouteDestination().getLocation().getSplitName()
                                        .equals(location.getSplitName())) {
                            // cars are sorted by train name, print train message once
                            if (!trainName.equals(car.getTrainName())) {
                                trainName = car.getTrainName();
                                newLine(fileOut, MessageFormat.format(
                                        messageFormatText = TrainSwitchListText.getStringScheduledWork(),
                                        new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
                                printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
                            }
                            if (car.isUtility()) {
                                setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
                            } else {
                                dropCar(fileOut, car, !IS_MANIFEST);
                            }
                        }
                    }
                }
            }
        }
    }

    public void printSwitchList(Location location, boolean isPreview) {
        File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName());
        if (!switchListFile.exists()) {
            log.warn("Switch list file missing for location ({})", location.getName());
            return;
        }
        if (isPreview && Setup.isManifestEditorEnabled()) {
            TrainUtilities.openDesktop(switchListFile);
        } else {
            TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false,
                    FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(),
                    Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled());
        }
        if (!isPreview) {
            location.setStatus(Location.PRINTED);
            location.setSwitchListState(Location.SW_PRINTED);
        }
    }

    protected void newLine(PrintWriter file, String string) {
        if (!string.isEmpty()) {
            newLine(file, string, !IS_MANIFEST);
        }
    }

    private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class);
}