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}