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}