001package jmri.jmrit.operations.trains;
002
003import java.io.*;
004import java.nio.charset.StandardCharsets;
005import java.util.ArrayList;
006import java.util.List;
007
008import org.apache.commons.csv.CSVFormat;
009import org.apache.commons.csv.CSVPrinter;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013import jmri.InstanceManager;
014import jmri.jmrit.operations.locations.Location;
015import jmri.jmrit.operations.rollingstock.cars.Car;
016import jmri.jmrit.operations.rollingstock.cars.CarManager;
017import jmri.jmrit.operations.rollingstock.engines.Engine;
018import jmri.jmrit.operations.rollingstock.engines.EngineManager;
019import jmri.jmrit.operations.routes.Route;
020import jmri.jmrit.operations.routes.RouteLocation;
021import jmri.jmrit.operations.setup.Setup;
022
023/**
024 * Builds a comma separated value (csv) switch list for a location on the
025 * railroad.
026 *
027 * @author Daniel Boudreau (C) Copyright 2011, 2013, 2014, 2015, 2021
028 *
029 *
030 */
031public class TrainCsvSwitchLists extends TrainCsvCommon {
032
033    /**
034     * builds a csv file containing the switch list for a location
035     *
036     * @param location The Location requesting a switch list.
037     *
038     * @return File
039     */
040    public File buildSwitchList(Location location) {      
041        if (!Setup.isGenerateCsvSwitchListEnabled()) {
042            return null; // done, not enabled
043        }
044        
045        // Append switch list data if not operating in real time
046        boolean append = false; // add text to end of file when true
047
048        if (!Setup.isSwitchListRealTime()) {
049            if (location.getStatus().equals(Location.UPDATED)) {
050                return null; // nothing to add
051            }
052            append = location.getSwitchListState() == Location.SW_APPEND;
053        }
054        // create CSV switch list file
055        File file = InstanceManager.getDefault(TrainManagerXml.class).createCsvSwitchListFile(location.getName());
056
057        log.debug("Append CSV file: {} for location ({})", append, location.getName());
058
059        // need to delete CSV data from file from tags "END" which is car hold list for
060        // this location
061        if (append) {
062            trimCvsFile(file, location);
063        }
064
065        try (CSVPrinter fileOut = new CSVPrinter(
066                new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8),
067                CSVFormat.DEFAULT)) {
068            if (!append) {
069                // build header
070                printHeader(fileOut);
071                fileOut.printRecord("SWL", Bundle.getMessage("csvSwitchList")); // NOI18N
072                printRailroadName(fileOut, Setup.getRailroadName());
073                printLocationName(fileOut, location.getSplitName());
074                printPrinterName(fileOut, location.getDefaultPrinterName());
075                printLocationSwitchListComment(fileOut, location);
076                printLocationComment(fileOut, location);
077            }
078            printValidity(fileOut, getDate(true));
079
080            // get a list of trains sorted by arrival time
081            List<Train> trains = InstanceManager.getDefault(TrainManager.class)
082                    .getTrainsArrivingThisLocationList(location);
083            for (Train train : trains) {
084                if (!train.isBuilt()) {
085                    continue; // train wasn't built so skip
086                }
087                if (!Setup.isSwitchListRealTime() && train.getSwitchListStatus().equals(Train.PRINTED)) {
088                    continue; // already loaded this train
089                }
090                int pickupCars = 0;
091                int dropCars = 0;
092                int stops = 1;
093                boolean trainDone = false;
094                List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
095                List<Engine> enginesList = InstanceManager.getDefault(EngineManager.class)
096                        .getByTrainBlockingList(train);
097                // does the train stop once or more at this location?
098                Route route = train.getRoute();
099                if (route == null) {
100                    continue; // no route for this train
101                }
102                List<RouteLocation> routeList = route.getLocationsBySequenceList();
103                RouteLocation rlPrevious = null;
104                // need to know where in the route we are for the various comments
105                for (RouteLocation rl : routeList) {
106                    if (!rl.getSplitName().equals(location.getSplitName())) {
107                        rlPrevious = rl;
108                        continue;
109                    }
110                    String expectedArrivalTime = train.getExpectedArrivalTime(rl);
111                    if (expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
112                        trainDone = true;
113                    }
114                    // First time a train stops at a location provide:
115                    // train name
116                    // train description
117                    // if the train has started its route
118                    // the arrival time or relative time if the train has started its route
119                    // the departure location
120                    // the departure time
121                    // the train's direction when it arrives
122                    // if it terminates at this location
123                    if (stops == 1) {
124                        // newLine(fileOut);
125                        printTrainName(fileOut, train.getName());
126                        printTrainDescription(fileOut, train.getDescription());
127                        printTrainComment(fileOut, train);
128                        printRouteComment(fileOut, train);
129
130                        if (train.isTrainEnRoute()) {
131                            fileOut.printRecord("TIR", Bundle.getMessage("csvTrainEnRoute")); // NOI18N
132                            printEstimatedTimeEnRoute(fileOut, expectedArrivalTime);
133                        } else {
134                            fileOut.printRecord("DL", Bundle.getMessage("csvDepartureLocationName"),
135                                    splitString(train.getTrainDepartsName())); // NOI18N
136                            printDepartureTime(fileOut, train.getFormatedDepartureTime());
137                            if (rl == train.getTrainDepartsRouteLocation() && !train.isLocalSwitcher()) {
138                                printTrainDeparts(fileOut, rl.getSplitName(), rl.getTrainDirectionString());
139                            }
140                            if (rl != train.getTrainDepartsRouteLocation()) {
141                                printExpectedTimeArrival(fileOut, expectedArrivalTime);
142                                printTrainArrives(fileOut, rl.getSplitName(), rl.getTrainDirectionString());
143                            }
144                        }
145                        if (rl == train.getTrainTerminatesRouteLocation()) {
146                            printTrainTerminates(fileOut, rl.getSplitName());
147                        }
148                    }
149                    if (stops > 1) {
150                        // Print visit number, etc. only if previous location wasn't the same
151                        if (rlPrevious == null ||
152                                !rl.getSplitName().equals(rlPrevious.getSplitName())) {
153                            // After the first time a train stops at a location provide:
154                            // if the train has started its route
155                            // the arrival time or relative time if the train has started its route
156                            // the train's direction when it arrives
157                            // if it terminate at this location
158
159                            fileOut.printRecord("VN", Bundle.getMessage("csvVisitNumber"), stops);
160                            if (train.isTrainEnRoute()) {
161                                printEstimatedTimeEnRoute(fileOut, expectedArrivalTime);
162                            } else {
163                                printExpectedTimeArrival(fileOut, expectedArrivalTime);
164                            }
165                            printTrainArrives(fileOut, rl.getSplitName(), rl.getTrainDirectionString());
166                            if (rl == train.getTrainTerminatesRouteLocation()) {
167                                printTrainTerminates(fileOut, rl.getSplitName());
168                            }
169                        } else {
170                            stops--; // don't bump stop count, same location
171                            // Does the train change direction?
172                            if (rl.getTrainDirection() != rlPrevious.getTrainDirection()) {
173                                fileOut.printRecord("TDC", Bundle.getMessage("csvTrainChangesDirection"),
174                                        rl.getTrainDirectionString()); // NOI18N
175                            }
176                        }
177                    }
178
179                    rlPrevious = rl;
180                    printRouteLocationComment(fileOut, rl);
181                    printTrackComments(fileOut, rl, carList);
182
183                    // engine change or helper service?
184                    checkForEngineOrCabooseChange(fileOut, train, rl);
185
186                    // go through the list of engines and determine if the engine departs here
187                    for (Engine engine : enginesList) {
188                        if (engine.getRouteLocation() == rl && engine.getTrack() != null) {
189                            printEngine(fileOut, engine, "PL", Bundle.getMessage("csvPickUpLoco"));
190                        }
191                    }
192                    // now block out train
193                    // caboose or FRED is placed at end of the train
194                    // passenger cars are already blocked in the car list
195                    // passenger cars with negative block numbers are placed at
196                    // the front of the train, positive numbers at the end of
197                    // the train.
198                    for (RouteLocation rld : train.getTrainBlockingOrder()) {
199                        for (Car car : carList) {
200                            if (isNextCar(car, rl, rld)) {
201                                pickupCars++;
202                                int count = 0;
203                                if (car.isUtility()) {
204                                    count = countPickupUtilityCars(carList, car, !IS_MANIFEST);
205                                    if (count == 0) {
206                                        continue; // already done this set of
207                                        // utility cars
208                                    }
209                                }
210                                printCar(fileOut, car, "PC", Bundle.getMessage("csvPickUpCar"), count);
211                            }
212                        }
213                    }
214
215                    for (Engine engine : enginesList) {
216                        if (engine.getRouteDestination() == rl) {
217                            printEngine(fileOut, engine, "SL", Bundle.getMessage("csvSetOutLoco"));
218                        }
219                    }
220                    // now do car set outs
221                    for (Car car : carList) {
222                        if (car.getRouteDestination() == rl) {
223                            dropCars++;
224                            int count = 0;
225                            if (car.isUtility()) {
226                                count = countSetoutUtilityCars(carList, car, !LOCAL, !IS_MANIFEST);
227                                if (count == 0) {
228                                    continue; // already done this set of utility cars
229                                }
230                            }
231                            printCar(fileOut, car, "SC", Bundle.getMessage("csvSetOutCar"), count);
232                        }
233                    }
234                    stops++;
235                    if (rl != train.getTrainTerminatesRouteLocation()) {
236                        printTrainLength(fileOut, train.getTrainLength(rl), train.getNumberEmptyCarsInTrain(rl),
237                                train.getNumberCarsInTrain(rl));
238                        printTrainWeight(fileOut, train.getTrainWeight(rl));
239                    }
240                }
241                if (trainDone && pickupCars == 0 && dropCars == 0) {
242                    fileOut.printRecord("TDONE", Bundle.getMessage("csvTrainHasAlreadyServiced"));
243                } else if (stops > 1) {
244                    if (pickupCars == 0) {
245                        fileOut.printRecord("NCPU", Bundle.getMessage("csvNoCarPickUp"));
246                    }
247                    if (dropCars == 0) {
248                        fileOut.printRecord("NCSO", Bundle.getMessage("csvNoCarSetOut"));
249                    }
250                    fileOut.printRecord("TEND", Bundle.getMessage("csvTrainEnd"), train.getName()); // done with this
251                                                                                                    // train // NOI18N
252                }
253            }
254            printEnd(fileOut); // done with switch list
255
256            if (Setup.isSwitchListRealTime() && Setup.isPrintTrackSummaryEnabled()) {
257                // now list hold cars
258                List<Car> rsByLocation = InstanceManager.getDefault(CarManager.class).getByLocationList();
259                List<Car> carList = new ArrayList<>();
260                for (Car rs : rsByLocation) {
261                    if (rs.getLocation() != null &&
262                            rs.getLocation().getSplitName().equals(location.getSplitName()) &&
263                            rs.getRouteLocation() == null) {
264                        carList.add(rs);
265                    }
266                }
267                clearUtilityCarTypes(); // list utility cars by quantity
268                for (Car car : carList) {
269                    int count = 0;
270                    if (car.isUtility()) {
271                        count = countPickupUtilityCars(carList, car, !IS_MANIFEST);
272                        if (count == 0) {
273                            continue; // already done this set of utility cars
274                        }
275                    }
276                    printCar(fileOut, car, "HOLD", Bundle.getMessage("csvHoldCar"), count);
277                }
278            }
279            printEnd(fileOut); // done with hold cars
280
281            // Are there any cars that need to be found?
282            listCarsLocationUnknown(fileOut);
283            fileOut.flush();
284            fileOut.close();
285        } catch (IOException e) {
286            log.error("Can not open CSV switch list file: {}", file.getName());
287            return null;
288        }
289        return file;
290    }
291
292    protected final void printEnd(CSVPrinter printer) throws IOException {
293        printer.printRecord("END", Bundle.getMessage("csvEnd")); // NOI18N
294    }
295
296    protected final void printExpectedTimeArrival(CSVPrinter printer, String time) throws IOException {
297        printer.printRecord("ETA", Bundle.getMessage("csvExpectedTimeArrival"), time); // NOI18N
298    }
299
300    protected final void printEstimatedTimeEnRoute(CSVPrinter printer, String time) throws IOException {
301        printer.printRecord("ETE", Bundle.getMessage("csvEstimatedTimeEnRoute"), time); // NOI18N
302    }
303
304    protected final void printTrainArrives(CSVPrinter printer, String name, String direction) throws IOException {
305        printer.printRecord("TA", Bundle.getMessage("csvTrainArrives"), name, direction); // NOI18N
306    }
307
308    /*
309     * Used to delete CSV data from file from tags "END" which is car hold list for
310     * this location. Creates a backup file and then copies the needed lines back
311     * into the original file.
312     */
313    private void trimCvsFile(File file, Location location) {
314        // need to delete CSV data from file from tags "END" which is car hold list for
315        // this location
316        try (PrintWriter fileOut = new PrintWriter(
317                new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)), true)) {
318            File backupFile = new File(
319                    InstanceManager.getDefault(TrainManagerXml.class).backupFileName(file.getAbsolutePath()));
320            try (BufferedReader in = new BufferedReader(
321                    new InputStreamReader(new FileInputStream(backupFile), StandardCharsets.UTF_8))) {
322                while (true) {
323                    String line = in.readLine();
324                    if (line == null) {
325                        break; // done
326                    }
327                    if (!line.startsWith("END")) {
328                        fileOut.println(line);
329                    } else {
330                        break; // done
331                    }
332                }
333                in.close();
334            } catch (FileNotFoundException e) {
335                log.error("Can not open CSV switch list file: {}", file.getName());
336            }
337            fileOut.flush();
338            fileOut.close();
339        } catch (IOException e) {
340            log.error("Can not open CSV switch list file: {}", file.getName());
341        }
342    }
343
344    private final static Logger log = LoggerFactory.getLogger(TrainCsvSwitchLists.class);
345}