001package jmri.jmrit.timetable;
002
003import java.io.File;
004import java.io.BufferedWriter;
005import java.io.FileWriter;
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.List;
012import org.apache.commons.csv.CSVFormat;
013import org.apache.commons.csv.CSVPrinter;
014
015/**
016 * Export a timetable in CSV format for import into a speadsheet.
017 * <pre>
018 * CSV Content:
019 *   Line 1 - Layout name, segment name and schedule name.
020 *   Line 2 - Train names sorted by name and grouped by down or up direction.
021 *   Line 3-n - Station row with the arrive and depart times for each train.
022 * </pre>
023 *
024 * @author Dave Sand Copyright (C) 2019
025 * @since 4.15.3
026 */
027public class TimeTableCsvExport {
028
029    TimeTableDataManager tdm = TimeTableDataManager.getDataManager();
030    boolean errorOccurred;
031    FileWriter fileWriter;
032    BufferedWriter bufferedWriter;
033    CSVPrinter csvFile;
034
035    HashMap<Integer, TrainEntry> trainMap = new HashMap<>();
036    int trainIndex = 0;
037    List<TrainEntry> downTrains = new ArrayList<>();
038    List<TrainEntry> upTrains = new ArrayList<>();
039    String[] stopRow;
040
041    /**
042     * Create a CSV file that can be imported into a spreadsheet to create a
043     * timetable.
044     *
045     * @param file       The file to be created.
046     * @param layoutId   The selected layout.
047     * @param segmentId  The selected segment.
048     * @param scheduleId The selected schedule.
049     * @return true if an error occured.
050     * @throws java.io.IOException if unable to export the CSV file.
051     */
052    public boolean exportCsv(File file, int layoutId, int segmentId, int scheduleId) throws IOException {
053        // Create CSV file
054        errorOccurred = false;
055        fileWriter = new FileWriter(file);
056        bufferedWriter = new BufferedWriter(fileWriter);
057        csvFile = new CSVPrinter(bufferedWriter, CSVFormat.DEFAULT);
058
059        // write basic data of what has been processed to file
060        Layout layout = tdm.getLayout(layoutId);
061        Segment segment = tdm.getSegment(segmentId);
062        Schedule schedule = tdm.getSchedule(scheduleId);
063
064        csvFile.printRecord(layout.toString(), segment.toString(), schedule.toString());
065
066        List<String> record = new ArrayList<>();
067        // this is important - it places a blank cell on the first line of the output
068        record.add("");
069
070        // get all Trains in Schedule
071        List<Train> trains = tdm.getTrains(schedule.getScheduleId(), 0, true);
072
073        // sort trains into time order by start time
074        Collections.sort(trains, (o1, o2) -> Integer.compare(o1.getStartTime(), o2.getStartTime()));
075
076        // Check direction of train (Up or Down) and add to the appropriate list
077        trains.forEach((train) -> {
078            // Determine train direction by checking distance variations from one stop to another
079            List<Stop> trainStops = tdm.getStops(train.getTrainId(), 0, true);
080            int lastStop = trainStops.size();
081            if (lastStop > 1) {
082                double firstDistance = tdm.getStation(trainStops.get(0).getStationId()).getDistance();
083                double secondDistance = tdm.getStation(trainStops.get(1).getStationId()).getDistance();
084                if (firstDistance < secondDistance) {
085                    downTrains.add(new TrainEntry(train, "Down", lastStop));
086                } else {
087                    upTrains.add(new TrainEntry(train, "Up", lastStop));
088                }
089            } else {
090                // One stop trains, such as yard switches are arbitrarily assigned to down
091                downTrains.add(new TrainEntry(train, "Down", lastStop));
092            }
093        });
094
095        // # Write reference data to trainMap for use in next processing stops
096        for (TrainEntry downTrain : downTrains) {
097            Train train = downTrain.getTrain();
098            downTrain.setTrainIndex(trainIndex);
099            trainMap.put(train.getTrainId(), downTrain);
100            record.add(train.toString());
101            trainIndex += 1;
102        }
103        for (TrainEntry upTrain : upTrains) {
104            Train train = upTrain.getTrain();
105            upTrain.setTrainIndex(trainIndex);
106            trainMap.put(train.getTrainId(), upTrain);
107            record.add(train.toString());
108            trainIndex += 1;
109        }
110        // This is the end of the top line of the grid containing all of the train names
111        csvFile.printRecord(record);
112
113        // We have the trains - now find where they stop and record times for output
114        for (Station station : tdm.getStations(segment.getSegmentId(), true)) {
115            // pre-fill output values
116            stopRow = new String[trainMap.size()];
117            Arrays.fill(stopRow, "_");
118
119            // Get list of all stops for this station
120            tdm.getStops(0, station.getStationId(), false).forEach((stop) -> {
121                Train chkTrain = tdm.getTrain(stop.getTrainId());
122                // Ignore stops in other schedules
123                if (!(chkTrain.getScheduleId() != schedule.getScheduleId())) {
124                    // Get stored data for this train
125                    TrainEntry trainEntry = trainMap.get(stop.getTrainId());
126                    int idx = trainEntry.getTrainIndex();
127                    int lastStop = trainEntry.getLastStation();
128                    String direction = trainEntry.getDirection();
129                    
130                    // Collect required stop data
131                    int thisStop = stop.getSeq();
132                    int arrive = stop.getArriveTime();
133                    int depart = stop.getDepartTime();
134                    
135                    if (thisStop != 1 && thisStop != lastStop) {
136                        // neither first nor last stop
137                        if (arrive == depart) {
138                            stopRow[idx] = String.format("%s (d)", formatTime(depart));
139                        } else if (direction.equals("Down")) {
140                            stopRow[idx] = String.format("%s (a)%n%s (d)", formatTime(arrive), formatTime(depart));
141                        } else {
142                            stopRow[idx] = String.format("%s (d)%n%s (a)", formatTime(depart), formatTime(arrive));
143                        }
144                    } else if (thisStop == 1) {
145                        // first stop (aka start)
146                        stopRow[idx] = String.format("%s (d)", formatTime(depart));
147                    } else if (thisStop == lastStop) {
148                        // last stop
149                        stopRow[idx] = String.format("%s (a)", formatTime(arrive));
150                    }
151                }
152            });
153
154            // end of stops, output station line
155            record = new ArrayList<>();
156            record.add(station.toString());
157            record.addAll(Arrays.asList(stopRow));
158            csvFile.printRecord(record);
159        }
160
161        // Flush the write buffer and close the file
162        csvFile.flush();
163        csvFile.close();
164
165        return errorOccurred;
166    }
167
168    String formatTime(int t) {
169        // Convert minutes to hh:mm
170        return String.format("%02d:%02d", t / 60, t % 60);
171    }
172
173    static class TrainEntry {
174
175        private final Train _train;
176        private final String _direction;
177        private final int _lastStation;
178        private int _trainIndex;
179
180        public TrainEntry(Train train, String direction, int lastStation) {
181            _train = train;
182            _direction = direction;
183            _lastStation = lastStation;
184            _trainIndex = -1;
185        }
186
187        public Train getTrain() {
188            return _train;
189        }
190
191        public String getDirection() {
192            return _direction;
193        }
194
195        public int getLastStation() {
196            return _lastStation;
197        }
198
199        public int getTrainIndex() {
200            return _trainIndex;
201        }
202
203        public void setTrainIndex(int trainIndex) {
204            _trainIndex = trainIndex;
205        }
206
207        @Override
208        public String toString() {
209            return String.format("%s : %s : %d : %d",
210                    _train.getTrainName(), _direction, _lastStation, _trainIndex);
211        }
212    }
213}