001package jmri.jmrit.timetable; 002 003import java.io.File; 004import java.io.BufferedReader; 005import java.io.FileReader; 006import java.io.IOException; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.List; 010import org.apache.commons.csv.CSVFormat; 011import org.apache.commons.csv.CSVParser; 012import org.apache.commons.csv.CSVRecord; 013 014/** 015 * CSV Record Types. The first field is the record type keyword (not I18N). 016 * Most fields are optional. 017 * <pre> 018 * "Layout", "layout name", "scale", fastClock, throttles, "metric" 019 * Defaults: "New Layout", "HO", 4, 0, "No" 020 * Occurs: Must be first record, occurs once 021 * 022 * "TrainType", "type name", color number 023 * Defaults: "New Type", #000000 024 * Occurs: Follows Layout record, occurs 0 to n times. If none, a default train type is created which will be used for all trains. 025 * Notes: #000000 is black. 026 * If the type name is UseLayoutTypes, the train types for the current layout will be used. 027 * 028 * "Segment", "segment name" 029 * Default: "New Segment" 030 * Occurs: Follows last TrainType, if any. Occurs 1 to n times. 031 * 032 * "Station", "station name", distance, doubleTrack, sidings, staging 033 * Defaults: "New Station", 1.0, No, 0, 0 034 * Occurs: Follows parent segment, occurs 1 to n times. 035 * Note: If the station name is UseSegmentStations, the stations for the current segment will be used. 036 * 037 * "Schedule", "schedule name", "effective date", startHour, duration 038 * Defaults: "New Schedule", "Today", 0, 24 039 * Occurs: Follows last station, occurs 1 to n times. 040 * 041 * "Train", "train name", "train description", type, defaultSpeed, starttime, throttle, notes 042 * Defaults: "NT", "New Train", 0, 1, 0, 0, "" 043 * Occurs: Follows parent schedule, occurs 1 to n times. 044 * Note1: The type is the relative number of the train type listed above starting with 1 for the first train type. 045 * Note2: The start time is an integer between 0 and 1439, subject to the schedule start time and duration. 046 * 047 * "Stop", station, duration, nextSpeed, stagingTrack, notes 048 * Defaults: 0, 0, 0, 0, "" 049 * Required: station number. 050 * Occurs: Follows parent train in the proper sequence. Occurs 1 to n times. 051 * Notes: The station is the relative number of the station listed above starting with 1 for the first station. 052 * If more that one segment is used, the station number is cumulative. 053 * 054 * Except for Stops, each record can have one of three actions: 055 * 1) If no name is supplied, a default object will be created. 056 * 2) If the name matches an existing name, the existing object will be used. 057 * 3) A new object will be created with the supplied name. The remaining fields, if any, will replace the default values. 058 * 059 * Minimal file using defaults except for station names and distances: 060 * "Layout" 061 * "Segment" 062 * "Station", "Station 1", 0.0 063 * "Station", "Station 2", 25.0 064 * "Schedule" 065 * "Train" 066 * "Stop", 1 067 * "Stop", 2 068 * </pre> 069 * The import applies the changes to the data in memory. At the end of the import 070 * a dialog is displayed with the option to save the changes to the timetable data file. 071 * @author Dave Sand Copyright (C) 2019 072 * @since 4.15.3 073 */ 074public class TimeTableCsvImport { 075 076 TimeTableDataManager tdm = TimeTableDataManager.getDataManager(); 077 boolean errorOccurred; 078 List<String> importFeedback = new ArrayList<>(); 079 FileReader fileReader; 080 BufferedReader bufferedReader; 081 CSVParser csvFile; 082 083 int recordNumber = 0; 084 int layoutId = 0; //Current layout object id 085 int segmentId = 0; //Current segment object id 086 int scheduleId = 0; //Current schedule object id 087 int trainId = 0; //Current train object id 088 List<Integer> trainTypes = new ArrayList<>(); //List of train type ids, translates the relative type occurrence to a type id 089 List<Integer> stations = new ArrayList<>(); //List of stations ids, translates the relative station occurence to a station id 090 091 public List<String> importCsv(File file) throws IOException { 092 tdm.setLockCalculate(true); 093 errorOccurred = false; 094 try { 095 fileReader = new FileReader(file); 096 bufferedReader = new BufferedReader(fileReader); 097 csvFile = new CSVParser(bufferedReader, CSVFormat.DEFAULT); 098 for (CSVRecord record : csvFile.getRecords()) { 099 if (errorOccurred) { 100 break; 101 } 102 recordNumber += 1; 103 if (record.size() > 0) { 104 List<String> list = new ArrayList<>(); 105 record.forEach(list::add); 106 String[] values = list.toArray(new String[record.size()]); 107 String recd = values[0]; 108 109 if (recd.equals("Layout") && layoutId == 0) { 110 doLayout(values); 111 } else if (recd.equals("TrainType") && layoutId != 0) { 112 doTrainType(values); 113 } else if (recd.equals("Segment") && layoutId != 0) { 114 doSegment(values); 115 } else if (recd.equals("Station") && segmentId != 0) { 116 doStation(values); 117 } else if (recd.equals("Schedule") && layoutId != 0) { 118 doSchedule(values); 119 } else if (recd.equals("Train") && scheduleId != 0) { 120 doTrain(values); 121 } else if (recd.equals("Stop") && trainId != 0) { 122 doStop(values); 123 } else { 124 log.warn("Unable to process record {}, content = {}", recordNumber, values); 125 importFeedback.add(String.format("Unable to process record %d, content = %s", 126 recordNumber, record.toString())); 127 errorOccurred = true; 128 } 129 } 130 } 131 csvFile.close(); 132 } catch (IOException ex) { 133 log.error("CSV Import failed: ", ex); 134 importFeedback.add(String.format("CSV Import failed: %s", ex.getMessage())); 135 errorOccurred = true; 136 } finally { 137 if(bufferedReader != null) { 138 bufferedReader.close(); 139 } 140 if(fileReader != null) { 141 fileReader.close(); 142 } 143 } 144 tdm.setLockCalculate(false); 145 if (!errorOccurred) { 146 // Force arrive/depart calculations 147 Layout layout = tdm.getLayout(layoutId); 148 if (layout != null) { 149 int fastClock = layout.getFastClock(); 150 try { 151 layout.setFastClock(fastClock + 1); 152 layout.setFastClock(fastClock); 153 } catch (IllegalArgumentException ex) { 154 log.error("Calculation error occured: ", ex); 155 importFeedback.add(String.format("Calculation error occured: %s", ex.getMessage())); 156 } 157 } 158 } 159 return importFeedback; 160 } 161 162 void doLayout(String[] values) { 163 if (recordNumber != 1) { 164 log.error("Invalid file structure"); 165 importFeedback.add("Invalid file structure, the first record must be a layout record."); 166 errorOccurred = true; 167 return; 168 } 169 log.debug("Layout values: {}", Arrays.toString(values)); 170 if (values.length == 1) { 171 // Create default layout 172 Layout defaultLayout = new Layout(); 173 layoutId = defaultLayout.getLayoutId(); 174 return; 175 } 176 177 String layoutName = values[1]; 178 for (Layout layout : tdm.getLayouts(false)) { 179 if (layout.getLayoutName().equals(layoutName)) { 180 // Use existing layout 181 layoutId = layout.getLayoutId(); 182 return; 183 } 184 } 185 186 // Create a new layout and set the name 187 Layout newLayout = new Layout(); 188 layoutId = newLayout.getLayoutId(); 189 newLayout.setLayoutName(layoutName); 190 191 // Change the defaults to the supplied values if available 192 String scaleName = (values.length > 2) ? values[2] : "HO"; 193 jmri.Scale scale = jmri.ScaleManager.getScale(scaleName); 194 if (scale != null) { 195 newLayout.setScale(scale); 196 } 197 198 String clockString = (values.length > 3) ? values[3] : "4"; 199 int clock = convertToInteger(clockString); 200 if (clock > 0) { 201 newLayout.setFastClock(clock); 202 } 203 204 String throttlesString = (values.length > 4) ? values[4] : "0"; 205 int throttles = convertToInteger(throttlesString); 206 if (throttles >= 0) { 207 newLayout.setThrottles(throttles); 208 } 209 210 String metric = (values.length > 5) ? values[5] : "No"; 211 if (metric.equals("Yes") || metric.equals("No")) { 212 newLayout.setMetric((metric.equals("Yes"))); 213 } 214 } 215 216 void doTrainType(String[] values) { 217 log.debug("TrainType values: {}", Arrays.toString(values)); 218 if (values.length == 1) { 219 // Create default train type 220 TrainType defaultType = new TrainType(layoutId); 221 trainTypes.add(defaultType.getTypeId()); 222 return; 223 } 224 225 String typeName = values[1]; 226 if (typeName.equals("UseLayoutTypes")) { 227 tdm.getTrainTypes(layoutId, true).forEach((currType) -> { 228 trainTypes.add(currType.getTypeId()); 229 }); 230 return; 231 } 232 for (TrainType trainType : tdm.getTrainTypes(layoutId, false)) { 233 if (trainType.getTypeName().equals(typeName)) { 234 // Use existing train type 235 trainTypes.add(trainType.getTypeId()); 236 return; 237 } 238 } 239 240 // Create a new train type and set the name and color if available 241 TrainType newType = new TrainType(layoutId); 242 trainTypes.add(newType.getTypeId()); 243 newType.setTypeName(typeName); 244 245 String typeColor = (values.length > 2) ? values[2] : "#000000"; 246 try { 247 java.awt.Color checkColor = java.awt.Color.decode(typeColor); 248 log.debug("Color = {}", checkColor); 249 newType.setTypeColor(typeColor); 250 } catch (java.lang.NumberFormatException ex) { 251 log.error("Invalid color value"); 252 } 253 } 254 255 void doSegment(String[] values) { 256 if (recordNumber == 2) { 257 // No train type, create one 258 TrainType trainType = new TrainType(layoutId); 259 trainTypes.add(trainType.getTypeId()); 260 } 261 262 log.debug("Segment values: {}", Arrays.toString(values)); 263 if (values.length == 1) { 264 // Create default segment 265 Segment defaultSegment = new Segment(layoutId); 266 segmentId = defaultSegment.getSegmentId(); 267 return; 268 } 269 270 String segmentName = values[1]; 271 for (Segment segment : tdm.getSegments(layoutId, false)) { 272 if (segment.getSegmentName().equals(segmentName)) { 273 // Use existing segment 274 segmentId = segment.getSegmentId(); 275 return; 276 } 277 } 278 279 // Create a new segment 280 Segment newSegment = new Segment(layoutId); 281 newSegment.setSegmentName(segmentName); 282 segmentId = newSegment.getSegmentId(); 283 } 284 285 void doStation(String[] values) { 286 log.debug("Station values: {}", Arrays.toString(values)); 287 if (values.length == 1) { 288 // Create default station 289 Station defaultStation = new Station(segmentId); 290 stations.add(defaultStation.getStationId()); 291 return; 292 } 293 294 String stationName = values[1]; 295 if (stationName.equals("UseSegmentStations")) { 296 tdm.getStations(segmentId, true).forEach((currStation) -> { 297 stations.add(currStation.getStationId()); 298 }); 299 return; 300 } 301 for (Station station : tdm.getStations(segmentId, false)) { 302 if (station.getStationName().equals(stationName)) { 303 // Use existing station 304 stations.add(station.getStationId()); 305 return; 306 } 307 } 308 309 // Create a new station 310 Station newStation = new Station(segmentId); 311 newStation.setStationName(stationName); 312 stations.add(newStation.getStationId()); 313 314 // Change the defaults to the supplied values if available 315 String distanceString = (values.length > 2) ? values[2] : "1.0"; 316 double distance = convertToDouble(distanceString); 317 if (distance >= 0.0) { 318 newStation.setDistance(distance); 319 } 320 321 String doubleTrack = (values.length > 3) ? values[3] : "No"; 322 if (doubleTrack.equals("Yes") || doubleTrack.equals("No")) { 323 newStation.setDoubleTrack((doubleTrack.equals("Yes"))); 324 } 325 326 String sidingsString = (values.length > 4) ? values[4] : "0"; 327 int sidings = convertToInteger(sidingsString); 328 if (sidings >= 0) { 329 newStation.setSidings(sidings); 330 } 331 332 String stagingString = (values.length > 5) ? values[5] : "0"; 333 int staging = convertToInteger(stagingString); 334 if (staging >= 0) { 335 newStation.setStaging(staging); 336 } 337 } 338 339 void doSchedule(String[] values) { 340 log.debug("Schedule values: {}", Arrays.toString(values)); 341 if (values.length == 1) { 342 // Create default schedule 343 Schedule defaultSchedule = new Schedule(layoutId); 344 scheduleId = defaultSchedule.getScheduleId(); 345 return; 346 } 347 348 String scheduleName = values[1]; 349 for (Schedule schedule : tdm.getSchedules(layoutId, false)) { 350 if (schedule.getScheduleName().equals(scheduleName)) { 351 // Use existing schedule 352 scheduleId = schedule.getScheduleId(); 353 return; 354 } 355 } 356 357 // Create a new schedule 358 Schedule newSchedule = new Schedule(layoutId); 359 newSchedule.setScheduleName(scheduleName); 360 scheduleId = newSchedule.getScheduleId(); 361 362 // Change the defaults to the supplied values if available 363 String effectiveDate = (values.length > 2) ? values[2] : "Today"; 364 if (!effectiveDate.isEmpty()) { 365 newSchedule.setEffDate(effectiveDate); 366 } 367 368 String startString = (values.length > 3) ? values[3] : "0"; 369 int startHour = convertToInteger(startString); 370 if (startHour >= 0 && startHour < 24) { 371 newSchedule.setStartHour(startHour); 372 } 373 374 String durationString = (values.length > 4) ? values[4] : "24"; 375 int duration = convertToInteger(durationString); 376 if (duration > 0 && duration < 25) { 377 newSchedule.setDuration(duration); 378 } 379 } 380 381 void doTrain(String[] values) { 382 log.debug("Train values: {}", Arrays.toString(values)); 383 if (values.length == 1) { 384 // Create default train 385 Train defaultTrain = new Train(scheduleId); 386 defaultTrain.setTypeId(trainTypes.get(0)); // Set default train type using first type 387 trainId = defaultTrain.getTrainId(); 388 return; 389 } 390 391 String trainName = values[1]; 392 for (Train train : tdm.getTrains(scheduleId, 0, false)) { 393 if (train.getTrainName().equals(trainName)) { 394 // Use existing train 395 trainId = train.getTrainId(); 396 return; 397 } 398 } 399 400 // Create a new train 401 Train newTrain = new Train(scheduleId); 402 newTrain.setTrainName(trainName); 403 newTrain.setTypeId(trainTypes.get(0)); // Set default train type using first type 404 trainId = newTrain.getTrainId(); 405 406 // Change the defaults to the supplied values if available 407 String description = (values.length > 2) ? values[2] : ""; 408 if (!description.isEmpty()) { 409 newTrain.setTrainDesc(description); 410 } 411 412 String typeIndexString = (values.length > 3) ? values[3] : "1"; 413 int typeIndex = convertToInteger(typeIndexString); 414 typeIndex -= 1; // trainTypes list is 0 to n-1 415 if (typeIndex >= 0 && typeIndex < trainTypes.size()) { 416 newTrain.setTypeId(trainTypes.get(typeIndex)); 417 } 418 419 String speedString = (values.length > 4) ? values[4] : "1"; 420 int defaultSpeed = convertToInteger(speedString); 421 if (defaultSpeed >= 0) { 422 newTrain.setDefaultSpeed(defaultSpeed); 423 } 424 425 String startTimeString = (values.length > 5) ? values[5] : "0"; 426 int startTime = convertToInteger(startTimeString); 427 if (startTime >= 0 && startTime < 1440) { 428 // Validate time 429 Schedule schedule = tdm.getSchedule(scheduleId); 430 if (tdm.validateTime(schedule.getStartHour(), schedule.getDuration(), startTime)) { 431 newTrain.setStartTime(startTime); 432 } else { 433 errorOccurred = true; 434 log.error("Train start time outside of schedule time: {}", startTime); 435 importFeedback.add(String.format("Train start time outside of schedule time: %d", startTime)); 436 } 437 } 438 439 String throttleString = (values.length > 6) ? values[6] : "0"; 440 int throttle = convertToInteger(throttleString); 441 int throttles = tdm.getLayout(layoutId).getThrottles(); 442 if (throttle >= 0 && throttle <= throttles) { 443 newTrain.setThrottle(throttle); 444 } 445 446 String trainNotes = (values.length > 7) ? values[7] : ""; 447 if (!trainNotes.isEmpty()) { 448 newTrain.setTrainNotes(trainNotes); 449 } 450 } 451 452 void doStop(String[] values) { 453 // The stop sequence number is one higher than the last sequence number. 454 // Each stop record creates a new stop. 455 // Stops don't reuse any existing entries. 456 log.debug("Stop values: {}", Arrays.toString(values)); 457 String stopStationString = (values.length > 1) ? values[1] : "-1"; 458 int stopStationIndex = convertToInteger(stopStationString); 459 stopStationIndex -= 1; // stations list is 0 to n-1 460 if (stopStationIndex >= 0 && stopStationIndex < stations.size()) { 461 List<Stop> stops = tdm.getStops(trainId, 0, false); 462 Stop newStop = new Stop(trainId, stops.size() + 1); 463 newStop.setStationId(stations.get(stopStationIndex)); 464 465 // Change the defaults to the supplied values if available 466 String durationString = (values.length > 2) ? values[2] : "0"; 467 int stopDuration = convertToInteger(durationString); 468 if (stopDuration > 0) { 469 newStop.setDuration(stopDuration); 470 } 471 472 String nextSpeedString = (values.length > 3) ? values[3] : "0"; 473 int nextSpeed = convertToInteger(nextSpeedString); 474 if (nextSpeed > 0) { 475 newStop.setNextSpeed(nextSpeed); 476 } 477 478 String stagingString = (values.length > 4) ? values[4] : "0"; 479 int stagingTrack = convertToInteger(stagingString); 480 Station station = tdm.getStation(stations.get(stopStationIndex)); 481 if (stagingTrack >= 0 && stagingTrack <= station.getStaging()) { 482 newStop.setStagingTrack(stagingTrack); 483 } 484 485 String stopNotes = (values.length > 5) ? values[5] : ""; 486 if (!stopNotes.isEmpty()) { 487 newStop.setStopNotes(stopNotes); 488 } 489 } 490 } 491 492 int convertToInteger(String number) { 493 try { 494 return Integer.parseInt(number); 495 } catch (NumberFormatException ex) { 496 return -1; 497 } 498 } 499 500 double convertToDouble(String number) { 501 try { 502 return Double.parseDouble(number); 503 } catch (NumberFormatException ex) { 504 return -1.0; 505 } 506 } 507 508 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TimeTableCsvImport.class); 509}