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