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}