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