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                        printHorizontalLine3(fileOut, !IS_MANIFEST);
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.getSplitName(), 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.getSplitName(),
267                                            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.getSplitName(), 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.getSplitName(),
281                                            expectedArrivalTime, 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.getSplitName(),
291                                            expectedArrivalTime, rl.getTrainDirectionString(),
292                                            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.getSplitName(), 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.getSplitName(), 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.getSplitName()});
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.getSplitName()});
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.getSplitName(), train.getDescription(),
358                            location.getSplitName()}));
359        } else {
360            if (stops > 1 && !_pickupCars) {
361                // Default message: No car pick ups for train ({0}) at this location
362                newLine(fileOut,
363                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarPickUps(),
364                                new Object[]{train.getSplitName(), train.getDescription(),
365                                        location.getSplitName()}));
366            }
367            if (stops > 1 && !_dropCars) {
368                // Default message: No car set outs for train ({0}) at this location
369                newLine(fileOut,
370                        MessageFormat.format(messageFormatText = TrainSwitchListText.getStringNoCarDrops(),
371                                new Object[]{train.getSplitName(), train.getDescription(),
372                                        location.getSplitName()}));
373            }
374        }
375    }
376
377    private void reportByTrack(PrintWriter fileOut, Location location) {
378        if (Setup.isPrintTrackSummaryEnabled() && Setup.isSwitchListRealTime()) {
379            clearUtilityCarTypes(); // list utility cars by quantity
380            if (Setup.getSwitchListPageFormat().equals(Setup.PAGE_NORMAL)) {
381                newLine(fileOut);
382                newLine(fileOut);
383            } else {
384                fileOut.write(FORM_FEED);
385            }
386            newLine(fileOut,
387                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringSwitchListByTrack(),
388                            new Object[]{location.getSplitName()}));
389
390            // we only need the cars delivered to or at this location
391            List<Car> rsList = carManager.getByTrainList();
392            List<Car> carList = new ArrayList<>();
393            for (Car rs : rsList) {
394                if ((rs.getLocation() != null &&
395                        rs.getLocation().getSplitName().equals(location.getSplitName())) ||
396                        (rs.getDestination() != null &&
397                                rs.getSplitDestinationName().equals(location.getSplitName())))
398                    carList.add(rs);
399            }
400
401            List<String> trackNames = new ArrayList<>(); // locations and tracks can have "similar" names, only list
402                                                         // track names once
403            for (Location loc : locationManager.getLocationsByNameList()) {
404                if (!loc.getSplitName().equals(location.getSplitName()))
405                    continue;
406                for (Track track : loc.getTracksByBlockingOrderList(null)) {
407                    String trackName = track.getSplitName();
408                    if (trackNames.contains(trackName))
409                        continue;
410                    trackNames.add(trackName);
411
412                    String trainName = ""; // for printing train message once
413                    newLine(fileOut);
414                    newLine(fileOut, trackName); // print out just the track name
415                    // now show the cars pickup and holds for this track
416                    for (Car car : carList) {
417                        if (!car.getSplitTrackName().equals(trackName)) {
418                            continue;
419                        }
420                        // is the car scheduled for pickup?
421                        if (car.getRouteLocation() != null) {
422                            if (car.getRouteLocation().getLocation().getSplitName()
423                                    .equals(location.getSplitName())) {
424                                // cars are sorted by train name, print train message once
425                                if (!trainName.equals(car.getTrainName())) {
426                                    trainName = car.getTrainName();
427                                    newLine(fileOut, MessageFormat.format(
428                                            messageFormatText = TrainSwitchListText.getStringScheduledWork(),
429                                            new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
430                                    printPickupCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
431                                }
432                                if (car.isUtility()) {
433                                    pickupUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
434                                } else {
435                                    pickUpCar(fileOut, car, !IS_MANIFEST);
436                                }
437                            }
438                            // car holds
439                        } else if (car.isUtility()) {
440                            String s = pickupUtilityCars(carList, car, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
441                            if (s != null) {
442                                newLine(fileOut, TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim()); // NOI18N
443                            }
444                        } else {
445                            newLine(fileOut,
446                                    MessageFormat.format(messageFormatText = TrainSwitchListText.getStringHoldCar(),
447                                            new Object[]{
448                                                    padAndTruncateIfNeeded(car.getRoadName(),
449                                                            InstanceManager.getDefault(CarRoads.class)
450                                                                    .getMaxNameLength()),
451                                                    padAndTruncateIfNeeded(
452                                                            TrainCommon.splitString(car.getNumber()),
453                                                            Control.max_len_string_print_road_number),
454                                                    padAndTruncateIfNeeded(
455                                                            car.getTypeName().split(TrainCommon.HYPHEN)[0],
456                                                            InstanceManager.getDefault(CarTypes.class)
457                                                                    .getMaxNameLength()),
458                                                    padAndTruncateIfNeeded(
459                                                            car.getLength() + Setup.getLengthUnitAbv(),
460                                                            Control.max_len_string_length_name),
461                                                    padAndTruncateIfNeeded(car.getLoadName(),
462                                                            InstanceManager.getDefault(CarLoads.class)
463                                                                    .getMaxNameLength()),
464                                                    padAndTruncateIfNeeded(trackName,
465                                                            locationManager.getMaxTrackNameLength()),
466                                                    padAndTruncateIfNeeded(car.getColor(), InstanceManager
467                                                            .getDefault(CarColors.class).getMaxNameLength())}));
468                        }
469                    }
470                    // now do set outs at this location
471                    for (Car car : carList) {
472                        if (!car.getSplitDestinationTrackName().equals(trackName)) {
473                            continue;
474                        }
475                        if (car.getRouteDestination() != null &&
476                                car.getRouteDestination().getLocation().getSplitName()
477                                        .equals(location.getSplitName())) {
478                            // cars are sorted by train name, print train message once
479                            if (!trainName.equals(car.getTrainName())) {
480                                trainName = car.getTrainName();
481                                newLine(fileOut, MessageFormat.format(
482                                        messageFormatText = TrainSwitchListText.getStringScheduledWork(),
483                                        new Object[]{car.getTrainName(), car.getTrain().getDescription()}));
484                                printDropCarHeader(fileOut, !IS_MANIFEST, !IS_TWO_COLUMN_TRACK);
485                            }
486                            if (car.isUtility()) {
487                                setoutUtilityCars(fileOut, carList, car, false, !IS_MANIFEST);
488                            } else {
489                                dropCar(fileOut, car, !IS_MANIFEST);
490                            }
491                        }
492                    }
493                }
494            }
495        }
496    }
497
498    public void printSwitchList(Location location, boolean isPreview) {
499        File switchListFile = InstanceManager.getDefault(TrainManagerXml.class).getSwitchListFile(location.getName());
500        if (!switchListFile.exists()) {
501            log.warn("Switch list file missing for location ({})", location.getName());
502            return;
503        }
504        if (isPreview && Setup.isManifestEditorEnabled()) {
505            TrainUtilities.openDesktop(switchListFile);
506        } else {
507            TrainPrintManifest.printReport(switchListFile, location.getName(), isPreview, Setup.getFontName(),
508                    FileUtil.getExternalFilename(Setup.getManifestLogoURL()), location.getDefaultPrinterName(),
509                    Setup.getSwitchListOrientation(), Setup.getManifestFontSize(), Setup.isPrintPageHeaderEnabled(),
510                    Setup.getPrintDuplexSides());
511        }
512        if (!isPreview) {
513            location.setStatus(Location.PRINTED);
514            location.setSwitchListState(Location.SW_PRINTED);
515        }
516    }
517
518    protected void newLine(PrintWriter file, String string) {
519        if (!string.isEmpty()) {
520            newLine(file, string, !IS_MANIFEST);
521        }
522    }
523
524    private final static Logger log = LoggerFactory.getLogger(TrainSwitchLists.class);
525}