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                        trainDone = true;
138                    }
139                    // first time at this location?
140                    if (stops == 1) {
141                        firstTimeMessages(fileOut, train, rl);
142                        stops++;
143                    } else {
144                        // multiple visits to this location
145                        // Print visit number only if previous location isn't the same
146                        if (rlPrevious == null ||
147                                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
148                            multipleVisitMessages(fileOut, train, rl, rlPrevious, stops);
149                            stops++;
150                        } else {
151                            // don't bump stop count, same location
152                            // Does the train reverse direction?
153                            reverseDirectionMessage(fileOut, train, rl, rlPrevious);
154                        }
155                    }
156
157                    // save current location in case there's back to back location with the same name
158                    rlPrevious = rl;
159
160                    // add route location comment
161                    if (Setup.isSwitchListRouteLocationCommentEnabled() && !rl.getComment().trim().isEmpty()) {
162                        newLine(fileOut, rl.getCommentWithColor());
163                    }
164
165                    printTrackComments(fileOut, rl, carList, !IS_MANIFEST);
166
167                    if (isThereWorkAtLocation(carList, engineList, rl)) {
168                        // now print out the work for this location
169                        if (Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
170                            pickupEngines(fileOut, engineList, rl, !IS_MANIFEST);
171                            // if switcher show loco drop at end of list
172                            if (train.isLocalSwitcher()) {
173                                blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
174                                dropEngines(fileOut, engineList, rl, !IS_MANIFEST);
175                            } else {
176                                dropEngines(fileOut, engineList, rl, !IS_MANIFEST);
177                                blockCarsByTrack(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
178                            }
179                        } else if (Setup.getManifestFormat().equals(Setup.TWO_COLUMN_FORMAT)) {
180                            blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST);
181                            blockCarsTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
182                        } else {
183                            blockLocosTwoColumn(fileOut, engineList, rl, !IS_MANIFEST);
184                            blockCarsByTrackNameTwoColumn(fileOut, train, carList, rl, IS_PRINT_HEADER, !IS_MANIFEST);
185                        }
186                        // print horizontal line if there was work and enabled
187                        if (Setup.isPrintHeadersEnabled() || !Setup.getManifestFormat().equals(Setup.STANDARD_FORMAT)) {
188                            printHorizontalLine(fileOut, !IS_MANIFEST);
189                        }
190                    }
191
192                    // done with work, now print summary for this location if we're done
193                    if (rl != train.getTrainTerminatesRouteLocation()) {
194                        RouteLocation nextRl = train.getRoute().getNextRouteLocation(rl);
195                        if (rl.getSplitName().equals(nextRl.getSplitName())) {
196                            continue; // the current location name is the "same" as the next
197                        }
198                        // print departure text if not a switcher
199                        if (!train.isLocalSwitcher() && !trainDone) {
200                            departureMessages(fileOut, train, rl);
201                        }
202                    }
203                }
204                // report if no pick ups or set outs or train has left
205                trainSummaryMessages(fileOut, train, location, trainDone, stops);
206            }
207
208            // now report car movement by tracks at location
209            reportByTrack(fileOut, location);
210
211        } catch (IllegalArgumentException e) {
212            newLine(fileOut, Bundle.getMessage("ErrorIllegalArgument",
213                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()));
214            newLine(fileOut, messageFormatText);
215            log.error("Illegal argument", e);
216        }
217
218        // Are there any cars that need to be found?
219        addCarsLocationUnknown(fileOut, !IS_MANIFEST);
220        fileOut.flush();
221        fileOut.close();
222        location.setStatus(Location.UPDATED);
223    }
224
225    private String getValid() {
226        String valid = MessageFormat.format(messageFormatText = TrainManifestText.getStringValid(),
227                new Object[]{getDate(true)});
228        if (Setup.isPrintTrainScheduleNameEnabled()) {
229            TrainSchedule sch = InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule();
230            if (sch != null) {
231                valid = valid + " (" + sch.getName() + ")";
232            }
233        }
234        return valid;
235    }
236
237    /*
238     * Messages for the switch list when the train first arrives
239     */
240    private void firstTimeMessages(PrintWriter fileOut, Train train, RouteLocation rl) {
241        newLine(fileOut);
242        newLine(fileOut,
243                MessageFormat.format(messageFormatText = TrainSwitchListText.getStringScheduledWork(),
244                        new Object[]{train.getName(), train.getDescription()}));
245        newLine(fileOut, getSwitchListTrainStatus(train, rl));
246    }
247
248    /*
249     * Messages when a train services the location two or more times
250     */
251    private void multipleVisitMessages(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious,
252            int stops) {
253        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
254        if (rlPrevious == null ||
255                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
256            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_PER_VISIT)) {
257                fileOut.write(FORM_FEED);
258            }
259            newLine(fileOut);
260            if (train.isTrainEnRoute()) {
261                if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
262                    // Visit number {0} for train ({1})
263                    newLine(fileOut,
264                            MessageFormat.format(
265                                    messageFormatText = TrainSwitchListText.getStringVisitNumberDone(),
266                                    new Object[]{stops, train.getName(), train.getDescription()}));
267                } else if (rl != train.getTrainTerminatesRouteLocation()) {
268                    // Visit number {0} for train ({1}) expect to arrive in {2}, arrives {3}bound
269                    newLine(fileOut, MessageFormat.format(
270                            messageFormatText = TrainSwitchListText.getStringVisitNumberDeparted(),
271                            new Object[]{stops, train.getName(), expectedArrivalTime,
272                                    rl.getTrainDirectionString(), train.getDescription()}));
273                } else {
274                    // Visit number {0} for train ({1}) expect to arrive in {2}, terminates {3}
275                    newLine(fileOut,
276                            MessageFormat.format(
277                                    messageFormatText = TrainSwitchListText
278                                            .getStringVisitNumberTerminatesDeparted(),
279                                    new Object[]{stops, train.getName(), expectedArrivalTime,
280                                            rl.getSplitName(), train.getDescription()}));
281                }
282            } else {
283                // train hasn't departed
284                if (rl != train.getTrainTerminatesRouteLocation()) {
285                    // Visit number {0} for train ({1}) expected arrival {2}, arrives {3}bound
286                    newLine(fileOut,
287                            MessageFormat.format(
288                                    messageFormatText = TrainSwitchListText.getStringVisitNumber(),
289                                    new Object[]{stops, train.getName(), expectedArrivalTime,
290                                            rl.getTrainDirectionString(), train.getDescription()}));
291                    if (Setup.isUseSwitchListDepartureTimeEnabled()) {
292                        // Departs {0} {1}bound at {2}
293                        newLine(fileOut, MessageFormat.format(
294                                messageFormatText = TrainSwitchListText.getStringDepartsAt(),
295                                new Object[]{splitString(rl.getName()),
296                                        rl.getTrainDirectionString(),
297                                        train.getExpectedDepartureTime(rl)}));
298                    }
299                } else {
300                    // Visit number {0} for train ({1}) expected arrival {2}, terminates {3}
301                    newLine(fileOut, MessageFormat.format(
302                            messageFormatText = TrainSwitchListText.getStringVisitNumberTerminates(),
303                            new Object[]{stops, train.getName(), expectedArrivalTime,
304                                    rl.getSplitName(), train.getDescription()}));
305                }
306            }
307        }
308    }
309
310    private void reverseDirectionMessage(PrintWriter fileOut, Train train, RouteLocation rl, RouteLocation rlPrevious) {
311        // Does the train reverse direction?
312        if (rl.getTrainDirection() != rlPrevious.getTrainDirection() &&
313                !TrainSwitchListText.getStringTrainDirectionChange().isEmpty()) {
314            // Train ({0}) direction change, departs {1}bound
315            newLine(fileOut,
316                    MessageFormat.format(
317                            messageFormatText = TrainSwitchListText.getStringTrainDirectionChange(),
318                            new Object[]{train.getName(), rl.getTrainDirectionString(),
319                                    train.getDescription(), train.getTrainTerminatesName()}));
320        }
321    }
322
323    /*
324     * Train departure messages at the end of the switch list
325     */
326    private void departureMessages(PrintWriter fileOut, Train train, RouteLocation rl) {
327        String trainDeparts = "";
328        if (Setup.isPrintLoadsAndEmptiesEnabled()) {
329            int emptyCars = train.getNumberEmptyCarsInTrain(rl);
330            // Train departs {0} {1}bound with {2} loads, {3} empties, {4} {5}, {6} tons
331            trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsLoads(),
332                    new Object[]{rl.getSplitName(),
333                            rl.getTrainDirectionString(),
334                            train.getNumberCarsInTrain(rl) - emptyCars, emptyCars,
335                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
336                            train.getTrainWeight(rl), train.getTrainTerminatesName(),
337                            train.getName()});
338        } else {
339            // Train departs {0} {1}bound with {2} cars, {3} {4}, {5} tons
340            trainDeparts = MessageFormat.format(TrainSwitchListText.getStringTrainDepartsCars(),
341                    new Object[]{rl.getSplitName(),
342                            rl.getTrainDirectionString(), train.getNumberCarsInTrain(rl),
343                            train.getTrainLength(rl), Setup.getLengthUnit().toLowerCase(),
344                            train.getTrainWeight(rl), train.getTrainTerminatesName(),
345                            train.getName()});
346        }
347        newLine(fileOut, trainDeparts);
348    }
349
350    private void trainSummaryMessages(PrintWriter fileOut, Train train, Location location, boolean trainDone,
351            int stops) {
352        if (trainDone && !_pickupCars && !_dropCars) {
353            // Default message: Train ({0}) has serviced this location
354            newLine(fileOut, MessageFormat.format(messageFormatText = TrainSwitchListText.getStringTrainDone(),
355                    new Object[]{train.getName(), train.getDescription(), location.getSplitName()}));
356        } else {
357            if (stops > 1 && !_pickupCars) {
358                // Default message: No car pick ups for train ({0}) at this location
359                newLine(fileOut,
360                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(),
361                                new Object[]{train.getName(), train.getDescription(),
362                                        location.getSplitName()}));
363            }
364            if (stops > 1 && !_dropCars) {
365                // Default message: No car set outs for train ({0}) at this location
366                newLine(fileOut,
367                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(),
368                                new Object[]{train.getName(), train.getDescription(),
369                                        location.getSplitName()}));
370            }
371        }
372    }
373
374    private void reportByTrack(PrintWriter fileOut, Location location) {
375        if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) {
376            clearUtilityCarTypes(); // list utility cars by quantity
377            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
378                newLine(fileOut);
379                newLine(fileOut);
380            } else {
381                fileOut.write(FORM_FEED);
382            }
383            newLine(fileOut,
384                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(),
385                            new Object[]{location.getSplitName()}));
386
387            // we only need the cars delivered to or at this location
388            List<Car> rsList = carManager.getByTrainList();
389            List<Car> carList = new ArrayList<>();
390            for (Car rs : rsList) {
391                if ((rs.getLocation() != null &&
392                        rs.getLocation().getSplitName().equals(location.getSplitName())) ||
393                        (rs.getDestination() != null &&
394                                rs.getSplitDestinationName().equals(location.getSplitName())))
395                    carList.add(rs);
396            }
397
398            List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list
399                                                         // track names once
400            for (Location loc : locationManager.getLocationsByNameList()) {
401                if (!loc.getSplitName().equals(location.getSplitName()))
402                    continue;
403                for (Track track : loc.getTracksByBlockingOrderList(null)) {
404                    String trackName = track.getSplitName();
405                    if (trackNames.contains(trackName))
406                        continue;
407                    trackNames.add(trackName);
408
409                    String trainName = ""; // for printing train message once
410                    newLine(fileOut);
411                    newLine(fileOut, trackName); // print out just the track name
412                    // now show the cars pickup and holds for this track
413                    for (Car car : carList) {
414                        if (!car.getSplitTrackName().equals(trackName)) {
415                            continue;
416                        }
417                        // is the car scheduled for pickup?
418                        if (car.getRouteLocation() != null) {
419                            if (car.getRouteLocation().getLocation().getSplitName()
420                                    .equals(location.getSplitName())) {
421                                // cars are sorted by train name, print train message once
422                                if (!trainName.equals(car.getTrainName())) {
423                                    trainName = car.getTrainName();
424                                    newLine(fileOut, MessageFormat.format(
425                                            messageFormatText = TrainSwitchListText.getStringScheduledWork(),
426                                            new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
427                                    printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
428                                }
429                                if (car.isUtility()) {
430                                    pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
431                                } else {
432                                    pickUpCar(fileOut, car, !IS_MANIFEST);
433                                }
434                            }
435                            // car holds
436                        } else if (car.isUtility()) {
437                            String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
438                            if (s != null) {
439                                newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N
440                            }
441                        } else {
442                            newLine(fileOut,
443                                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(),
444                                            new Object[]{
445                                                    padAndTruncateIfNeeded(car.getRoadName(),
446                                                            InstanceManager.getDefault(CarRoads.class)
447                                                                    .getMaxNameLength()),
448                                                    padAndTruncateIfNeeded(
449                                                            TrainCommon.splitString(car.getNumber()),
450                                                            Control.max_len_string_print_road_number),
451                                                    padAndTruncateIfNeeded(
452                                                            car.getTypeName().split(TrainCommon.HYPHEN)[0],
453                                                            InstanceManager.getDefault(CarTypes.class)
454                                                                    .getMaxNameLength()),
455                                                    padAndTruncateIfNeeded(
456                                                            car.getLength() + Setup.getLengthUnitAbv(),
457                                                            Control.max_len_string_length_name),
458                                                    padAndTruncateIfNeeded(car.getLoadName(),
459                                                            InstanceManager.getDefault(CarLoads.class)
460                                                                    .getMaxNameLength()),
461                                                    padAndTruncateIfNeeded(trackName,
462                                                            locationManager.getMaxTrackNameLength()),
463                                                    padAndTruncateIfNeeded(car.getColor(), InstanceManager
464                                                            .getDefault(CarColors.class).getMaxNameLength())}));
465                        }
466                    }
467                    // now do set outs at this location
468                    for (Car car : carList) {
469                        if (!car.getSplitDestinationTrackName().equals(trackName)) {
470                            continue;
471                        }
472                        if (car.getRouteDestination() != null &&
473                                car.getRouteDestination().getLocation().getSplitName()
474                                        .equals(location.getSplitName())) {
475                            // cars are sorted by train name, print train message once
476                            if (!trainName.equals(car.getTrainName())) {
477                                trainName = car.getTrainName();
478                                newLine(fileOut, MessageFormat.format(
479                                        messageFormatText = TrainSwitchListText.getStringScheduledWork(),
480                                        new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
481                                printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
482                            }
483                            if (car.isUtility()) {
484                                setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
485                            } else {
486                                dropCar(fileOut, car, !IS_MANIFEST);
487                            }
488                        }
489                    }
490                }
491            }
492        }
493    }
494
495    public void printSwitchList(Location location, boolean isPreview) {
496        File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName());
497        if (!switchListFile.exists()) {
498            log.warn("Switch list file missing for location ({})", location.getName());
499            return;
500        }
501        if (isPreview && Setup.isManifestEditorEnabled()) {
502            TrainUtilities.openDesktop(switchListFile);
503        } else {
504            TrainPrintUtilities.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(), false,
505                    FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(),
506                    Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(),
507                    Setup.getPrintDuplexSides());
508        }
509        if (!isPreview) {
510            location.setStatus(Location.PRINTED);
511            location.setSwitchListState(Location.SW_PRINTED);
512        }
513    }
514
515    protected void newLine(PrintWriter file, String string) {
516        if (!string.isEmpty()) {
517            newLine(file, string, !IS_MANIFEST);
518        }
519    }
520
521    private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class);
522}