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}