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