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: {}", 112 e.getLocalizedMessage()); 113 } 114 } 115 116 public void writeFile(String name) { 117 log.debug("writeFile {}", name); 118 // This is taken in large part from "Java and XML" page 368 119 File file = findFile(name); 120 if (file == null) { 121 file = new File(name); 122 } 123 124 try (CSVPrinter fileOut = new CSVPrinter( 125 new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) { 126 127 loadLayout(fileOut); 128 loadTrainTypes(fileOut); 129 loadSegment(fileOut); 130 loadStations(fileOut); 131 loadSchedule(fileOut); 132 loadTrains(fileOut); 133 134 JmriJOptionPane.showMessageDialog(null, 135 Bundle.getMessage("ExportedTimetableToFile", 136 defaultOperationsFilename()), 137 Bundle.getMessage("ExportComplete"), JmriJOptionPane.INFORMATION_MESSAGE); 138 139 fileOut.flush(); 140 fileOut.close(); 141 } catch (IOException e) { 142 log.error("Can not open export timetable CSV file: {}", e.getLocalizedMessage()); 143 JmriJOptionPane.showMessageDialog(null, 144 Bundle.getMessage("ExportedTimetableToFile", 145 defaultOperationsFilename()), 146 Bundle.getMessage("ExportFailed"), JmriJOptionPane.ERROR_MESSAGE); 147 } 148 } 149 150 /* 151 * "Layout", "layout name", "scale", fastClock, throttles, "metric" 152 */ 153 private void loadLayout(CSVPrinter fileOut) throws IOException { 154 fileOut.printRecord("Layout", 155 Setup.getRailroadName(), 156 "HO", 157 "4", 158 "0", 159 "No"); 160 } 161 162 /* 163 * "TrainType", "type name", color number 164 */ 165 private void loadTrainTypes(CSVPrinter fileOut) throws IOException { 166 fileOut.printRecord("TrainType", 167 "Freight_Black", 168 ColorUtil.colorToHexString(Color.BLACK)); 169 fileOut.printRecord("TrainType", 170 "Freight_Red", 171 ColorUtil.colorToHexString(Color.RED)); 172 fileOut.printRecord("TrainType", 173 "Freight_Blue", 174 ColorUtil.colorToHexString(Color.BLUE)); 175 fileOut.printRecord("TrainType", 176 "Freight_Yellow", 177 ColorUtil.colorToHexString(Color.YELLOW)); 178 } 179 180 /* 181 * "Segment", "segment name" 182 */ 183 private void loadSegment(CSVPrinter fileOut) throws IOException { 184 fileOut.printRecord("Segment", "Locations"); 185 } 186 187 List<Location> locationList = new ArrayList<>(); 188 189 /* 190 * "Station", "station name", distance, doubleTrack, sidings, staging 191 */ 192 private void loadStations(CSVPrinter fileOut) throws IOException { 193 // provide a list of locations to use, use either a route called 194 // "Timetable" or alphabetically 195 196 Route route = InstanceManager.getDefault(RouteManager.class).getRouteByName("Timetable"); 197 if (route != null) { 198 route.getLocationsBySequenceList().forEach(rl -> locationList.add(rl.getLocation())); 199 } else { 200 InstanceManager.getDefault(LocationManager.class).getLocationsByNameList().forEach(location -> locationList.add(location)); 201 } 202 203 double distance = 0.0; 204 for (Location location : locationList) { 205 distance += 1.0; 206 fileOut.printRecord("Station", 207 location.getName(), 208 distance, 209 "No", 210 "0", 211 location.isStaging() ? location.getTracksList().size() : "0"); 212 } 213 } 214 215 /* 216 * "Schedule", "schedule name", "effective date", startHour, duration 217 */ 218 private void loadSchedule(CSVPrinter fileOut) throws IOException { 219 // create schedule name based on date and time 220 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd kk:mm"); 221 String scheduleName = simpleDateFormat.format(Calendar.getInstance().getTime()); 222 223 fileOut.printRecord("Schedule", scheduleName, "Today", "0", "24"); 224 } 225 226 /* 227 * "Train", "train name", "train description", type, defaultSpeed, 228 * starttime, throttle, notes 229 */ 230 private void loadTrains(CSVPrinter fileOut) throws IOException { 231 int type = 1; // cycle through the 4 train types (chart colors) 232 int defaultSpeed = 4; 233 234 // the following works pretty good for travel times between 1 and 4 minutes 235 if (Setup.getTravelTime() > 0) { 236 defaultSpeed = defaultSpeed/Setup.getTravelTime(); 237 } 238 239 for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByTimeList()) { 240 if (!train.isBuildEnabled() || train.getRoute() == null) { 241 continue; 242 } 243 244 fileOut.printRecord("Train", 245 train.getName(), 246 train.getDescription(), 247 type++, 248 defaultSpeed, 249 train.getDepartTimeMinutes(), 250 "0", 251 train.getComment()); 252 253 // reset train types 254 if (type > 4) { 255 type = 1; 256 } 257 258 // Stop fields 259 // "Stop", station, duration, nextSpeed, stagingTrack, notes 260 for (RouteLocation rl : train.getRoute().getLocationsBySequenceList()) { 261 // calculate station stop 262 int station = 0; 263 for (Location location : locationList) { 264 station++; 265 if (rl.getLocation() == location) { 266 break; 267 } 268 } 269 int duration = 0; 270 if ((rl != train.getTrainDepartsRouteLocation() && rl.getLocation() != null && !rl.getLocation().isStaging())) { 271 if (train.isBuilt()) { 272 duration = train.getWorkTimeAtLocation(rl) + rl.getWait(); 273 if (!rl.getDepartureTime().isEmpty() && !train.getExpectedArrivalTime(rl).equals(Train.ALREADY_SERVICED)) { 274 duration = 60 * Integer.parseInt(rl.getDepartureTimeHour()) 275 + Integer.parseInt(rl.getDepartureTimeMinute()) - train.getExpectedTravelTimeInMinutes(rl); 276 } 277 } else { 278 duration = rl.getMaxCarMoves() * Setup.getSwitchTime() + rl.getWait(); 279 } 280 } 281 fileOut.printRecord("Stop", 282 station, 283 duration, 284 "0", 285 "0", 286 rl.getComment()); 287 } 288 } 289 } 290 291 public File getExportFile() { 292 return findFile(defaultOperationsFilename()); 293 } 294 295 // Operation files always use the same directory 296 public static String defaultOperationsFilename() { 297 return OperationsSetupXml.getFileLocation() + 298 OperationsSetupXml.getOperationsDirectoryName() + 299 File.separator + 300 getOperationsFileName(); 301 } 302 303 public static void setOperationsFileName(String name) { 304 operationsFileName = name; 305 } 306 307 public static String getOperationsFileName() { 308 return operationsFileName; 309 } 310 311 private static String operationsFileName = "ExportOperationsTimetable.csv"; // NOI18N 312 313 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExportTimetable.class); 314 315}