001package jmri.jmrit.operations.trains.tools; 002 003import java.awt.Color; 004import java.io.*; 005import java.nio.charset.StandardCharsets; 006import java.text.SimpleDateFormat; 007import java.util.*; 008 009import org.apache.commons.csv.CSVFormat; 010import org.apache.commons.csv.CSVPrinter; 011 012import jmri.InstanceManager; 013import jmri.jmrit.XmlFile; 014import jmri.jmrit.operations.locations.Location; 015import jmri.jmrit.operations.locations.LocationManager; 016import jmri.jmrit.operations.routes.*; 017import jmri.jmrit.operations.setup.OperationsSetupXml; 018import jmri.jmrit.operations.setup.Setup; 019import jmri.jmrit.operations.trains.Train; 020import jmri.jmrit.operations.trains.TrainManager; 021import jmri.util.ColorUtil; 022import jmri.util.swing.JmriJOptionPane; 023 024/** 025 * Provides an export to the Timetable feature. 026 * 027 * @author Daniel Boudreau Copyright (C) 2019 028 * 029 * <pre> 030 * Copied from TimeTableCsvImport on 11/25/2019 031 * 032 * CSV Record Types. The first field is the record type keyword (not I18N). 033 * Most fields are optional. 034 * 035 * "Layout", "layout name", "scale", fastClock, throttles, "metric" 036 * Defaults: "New Layout", "HO", 4, 0, "No" 037 * Occurs: Must be first record, occurs once 038 * 039 * "TrainType", "type name", color number 040 * Defaults: "New Type", #000000 041 * Occurs: Follows Layout record, occurs 0 to n times. If none, a default train type is created which will be used for all trains. 042 * Notes: #000000 is black. 043 * If the type name is UseLayoutTypes, the train types for the current layout will be used. 044 * 045 * "Segment", "segment name" 046 * Default: "New Segment" 047 * Occurs: Follows last TrainType, if any. Occurs 1 to n times. 048 * 049 * "Station", "station name", distance, doubleTrack, sidings, staging 050 * Defaults: "New Station", 1.0, No, 0, 0 051 * Occurs: Follows parent segment, occurs 1 to n times. 052 * Note: If the station name is UseSegmentStations, the stations for the current segment will be used. 053 * 054 * "Schedule", "schedule name", "effective date", startHour, duration 055 * Defaults: "New Schedule", "Today", 0, 24 056 * Occurs: Follows last station, occurs 1 to n times. 057 * 058 * "Train", "train name", "train description", type, defaultSpeed, starttime, throttle, notes 059 * Defaults: "NT", "New Train", 0, 1, 0, 0, "" 060 * Occurs: Follows parent schedule, occurs 1 to n times. 061 * Note1: The type is the relative number of the train type listed above starting with 1 for the first train type. 062 * Note2: The start time is an integer between 0 and 1439, subject to the schedule start time and duration. 063 * 064 * "Stop", station, duration, nextSpeed, stagingTrack, notes 065 * Defaults: 0, 0, 0, 0, "" 066 * Required: station number. 067 * Occurs: Follows parent train in the proper sequence. Occurs 1 to n times. 068 * Notes: The station is the relative number of the station listed above starting with 1 for the first station. 069 * If more that one segment is used, the station number is cumulative. 070 * 071 * Except for Stops, each record can have one of three actions: 072 * 1) If no name is supplied, a default object will be created. 073 * 2) If the name matches an existing name, the existing object will be used. 074 * 3) A new object will be created with the supplied name. The remaining fields, if any, will replace the default values. 075 * 076 * Minimal file using defaults except for station names and distances: 077 * "Layout" 078 * "Segment" 079 * "Station", "Station 1", 0.0 080 * "Station", "Station 2", 25.0 081 * "Schedule" 082 * "Train" 083 * "Stop", 1 084 * "Stop", 2 085 * </pre> 086 */ 087public class ExportTimetable extends XmlFile { 088 089 public ExportTimetable() { 090 // nothing to do 091 } 092 093 public void writeOperationsTimetableFile() { 094 makeBackupFile(defaultOperationsFilename()); 095 try { 096 if (!checkFile(defaultOperationsFilename())) { 097 // The file does not exist, create it before writing 098 java.io.File file = new java.io.File(defaultOperationsFilename()); 099 java.io.File parentDir = file.getParentFile(); 100 if (!parentDir.exists()) { 101 if (!parentDir.mkdir()) { 102 log.error("Directory wasn't created"); 103 } 104 } 105 if (file.createNewFile()) { 106 log.debug("File created"); 107 } 108 } 109 writeFile(defaultOperationsFilename()); 110 } catch (IOException e) { 111 log.error("Exception while writing the new CSV operations file, may not be complete", e); 112 } 113 } 114 115 public void writeFile(String name) { 116 log.debug("writeFile {}", name); 117 // This is taken in large part from "Java and XML" page 368 118 File file = findFile(name); 119 if (file == null) { 120 file = new File(name); 121 } 122 123 try (CSVPrinter fileOut = new CSVPrinter( 124 new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) { 125 126 loadLayout(fileOut); 127 loadTrainTypes(fileOut); 128 loadSegment(fileOut); 129 loadStations(fileOut); 130 loadSchedule(fileOut); 131 loadTrains(fileOut); 132 133 JmriJOptionPane.showMessageDialog(null, 134 Bundle.getMessage("ExportedTimetableToFile", 135 defaultOperationsFilename()), 136 Bundle.getMessage("ExportComplete"), JmriJOptionPane.INFORMATION_MESSAGE); 137 138 fileOut.flush(); 139 fileOut.close(); 140 } catch (IOException e) { 141 log.error("Can not open export timetable CSV file: {}", file.getName()); 142 JmriJOptionPane.showMessageDialog(null, 143 Bundle.getMessage("ExportedTimetableToFile", 144 defaultOperationsFilename()), 145 Bundle.getMessage("ExportFailed"), JmriJOptionPane.ERROR_MESSAGE); 146 } 147 } 148 149 /* 150 * "Layout", "layout name", "scale", fastClock, throttles, "metric" 151 */ 152 private void loadLayout(CSVPrinter fileOut) throws IOException { 153 fileOut.printRecord("Layout", 154 Setup.getRailroadName(), 155 "HO", 156 "4", 157 "0", 158 "No"); 159 } 160 161 /* 162 * "TrainType", "type name", color number 163 */ 164 private void loadTrainTypes(CSVPrinter fileOut) throws IOException { 165 fileOut.printRecord("TrainType", 166 "Freight_Black", 167 ColorUtil.colorToHexString(Color.BLACK)); 168 fileOut.printRecord("TrainType", 169 "Freight_Red", 170 ColorUtil.colorToHexString(Color.RED)); 171 fileOut.printRecord("TrainType", 172 "Freight_Blue", 173 ColorUtil.colorToHexString(Color.BLUE)); 174 fileOut.printRecord("TrainType", 175 "Freight_Yellow", 176 ColorUtil.colorToHexString(Color.YELLOW)); 177 } 178 179 /* 180 * "Segment", "segment name" 181 */ 182 private void loadSegment(CSVPrinter fileOut) throws IOException { 183 fileOut.printRecord("Segment", "Locations"); 184 } 185 186 List<Location> locationList = new ArrayList<>(); 187 188 /* 189 * "Station", "station name", distance, doubleTrack, sidings, staging 190 */ 191 private void loadStations(CSVPrinter fileOut) throws IOException { 192 // provide a list of locations to use, use either a route called 193 // "Timetable" or alphabetically 194 195 Route route = InstanceManager.getDefault(RouteManager.class).getRouteByName("Timetable"); 196 if (route != null) { 197 route.getLocationsBySequenceList().forEach(rl -> locationList.add(rl.getLocation())); 198 } else { 199 InstanceManager.getDefault(LocationManager.class).getLocationsByNameList().forEach(location -> locationList.add(location)); 200 } 201 202 double distance = 0.0; 203 for (Location location : locationList) { 204 distance += 1.0; 205 fileOut.printRecord("Station", 206 location.getName(), 207 distance, 208 "No", 209 "0", 210 location.isStaging() ? location.getTracksList().size() : "0"); 211 } 212 } 213 214 /* 215 * "Schedule", "schedule name", "effective date", startHour, duration 216 */ 217 private void loadSchedule(CSVPrinter fileOut) throws IOException { 218 // create schedule name based on date and time 219 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd kk:mm"); 220 String scheduleName = simpleDateFormat.format(Calendar.getInstance().getTime()); 221 222 fileOut.printRecord("Schedule", scheduleName, "Today", "0", "24"); 223 } 224 225 /* 226 * "Train", "train name", "train description", type, defaultSpeed, 227 * starttime, throttle, notes 228 */ 229 private void loadTrains(CSVPrinter fileOut) throws IOException { 230 int type = 1; // cycle through the 4 train types (chart colors) 231 int defaultSpeed = 4; 232 233 // the following works pretty good for travel times between 1 and 4 minutes 234 if (Setup.getTravelTime() > 0) { 235 defaultSpeed = defaultSpeed/Setup.getTravelTime(); 236 } 237 238 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByTimeList()) { 239 if (!train.isBuildEnabled() || train.getRoute() == null) { 240 continue; 241 } 242 243 fileOut.printRecord("Train", 244 train.getName(), 245 train.getDescription(), 246 type++, 247 defaultSpeed, 248 train.getDepartTimeMinutes(), 249 "0", 250 train.getComment()); 251 252 // reset train types 253 if (type > 4) { 254 type = 1; 255 } 256 257 // Stop fields 258 // "Stop", station, duration, nextSpeed, stagingTrack, notes 259 for (RouteLocation rl : train.getRoute().getLocationsBySequenceList()) { 260 // calculate station stop 261 int station = 0; 262 for (Location location : locationList) { 263 station++; 264 if (rl.getLocation() == location) { 265 break; 266 } 267 } 268 int duration = 0; 269 if ((rl != train.getTrainDepartsRouteLocation() && rl.getLocation() != null && !rl.getLocation().isStaging())) { 270 if (train.isBuilt()) { 271 duration = train.getWorkTimeAtLocation(rl) + rl.getWait(); 272 if (!rl.getDepartureTime().isEmpty() && !train.getExpectedArrivalTime(rl).equals(Train.ALREADY_SERVICED)) { 273 duration = 60 * Integer.parseInt(rl.getDepartureTimeHour()) 274 + Integer.parseInt(rl.getDepartureTimeMinute()) - train.getExpectedTravelTimeInMinutes(rl); 275 } 276 } else { 277 duration = rl.getMaxCarMoves() * Setup.getSwitchTime() + rl.getWait(); 278 } 279 } 280 fileOut.printRecord("Stop", 281 station, 282 duration, 283 "0", 284 "0", 285 rl.getComment()); 286 } 287 } 288 } 289 290 public File getExportFile() { 291 return findFile(defaultOperationsFilename()); 292 } 293 294 // Operation files always use the same directory 295 public static String defaultOperationsFilename() { 296 return OperationsSetupXml.getFileLocation() + 297 OperationsSetupXml.getOperationsDirectoryName() + 298 File.separator + 299 getOperationsFileName(); 300 } 301 302 public static void setOperationsFileName(String name) { 303 operationsFileName = name; 304 } 305 306 public static String getOperationsFileName() { 307 return operationsFileName; 308 } 309 310 private static String operationsFileName = "ExportOperationsTimetable.csv"; // NOI18N 311 312 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExportTimetable.class); 313 314}