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