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.jmrit.operations.trains.trainbuilder.TrainCommon;
022import jmri.util.ColorUtil;
023import jmri.util.swing.JmriJOptionPane;
024
025/**
026 * Provides an export to the Timetable feature.
027 *
028 * @author Daniel Boudreau Copyright (C) 2019
029 *
030 * <pre>
031 * Copied from TimeTableCsvImport on 11/25/2019
032 *
033 * CSV Record Types. The first field is the record type keyword (not I18N).
034 * Most fields are optional.
035 *
036 * "Layout", "layout name", "scale", fastClock, throttles, "metric"
037 *            Defaults:  "New Layout", "HO", 4, 0, "No"
038 *            Occurs:  Must be first record, occurs once
039 *
040 * "TrainType", "type name", color number
041 *            Defaults: "New Type", #000000
042 *            Occurs:  Follows Layout record, occurs 0 to n times.  If none, a default train type is created which will be used for all trains.
043 *            Notes:  #000000 is black.
044 *                    If the type name is UseLayoutTypes, the train types for the current layout will be used.
045 *
046 * "Segment", "segment name"
047 *            Default: "New Segment"
048 *            Occurs: Follows last TrainType, if any.  Occurs 1 to n times.
049 *
050 * "Station", "station name", distance, doubleTrack, sidings, staging
051 *            Defaults: "New Station", 1.0, No, 0, 0
052 *            Occurs:  Follows parent segment, occurs 1 to n times.
053 *            Note:  If the station name is UseSegmentStations, the stations for the current segment will be used.
054 *
055 * "Schedule", "schedule name", "effective date", startHour, duration
056 *            Defaults:  "New Schedule", "Today", 0, 24
057 *            Occurs: Follows last station, occurs 1 to n times.
058 *
059 * "Train", "train name", "train description", type, defaultSpeed, starttime, throttle, notes
060 *            Defaults:  "NT", "New Train", 0, 1, 0, 0, ""
061 *            Occurs:  Follows parent schedule, occurs 1 to n times.
062 *            Note1:  The type is the relative number of the train type listed above starting with 1 for the first train type.
063 *            Note2:  The start time is an integer between 0 and 1439, subject to the schedule start time and duration.
064 *
065 * "Stop", station, duration, nextSpeed, stagingTrack, notes
066 *            Defaults:  0, 0, 0, 0, ""
067 *            Required: station number.
068 *            Occurs:  Follows parent train in the proper sequence.  Occurs 1 to n times.
069 *            Notes:  The station is the relative number of the station listed above starting with 1 for the first station.
070 *                    If more that one segment is used, the station number is cumulative.
071 *
072 * Except for Stops, each record can have one of three actions:
073 *    1) If no name is supplied, a default object will be created.
074 *    2) If the name matches an existing name, the existing object will be used.
075 *    3) A new object will be created with the supplied name.  The remaining fields, if any, will replace the default values.
076 *
077 * Minimal file using defaults except for station names and distances:
078 * "Layout"
079 * "Segment"
080 * "Station", "Station 1", 0.0
081 * "Station", "Station 2", 25.0
082 * "Schedule"
083 * "Train"
084 * "Stop", 1
085 * "Stop", 2
086 * </pre>
087 */
088public class ExportTimetable extends XmlFile {
089
090    public ExportTimetable() {
091        // nothing to do
092    }
093
094    public void writeOperationsTimetableFile() {
095        makeBackupFile(defaultOperationsFilename());
096        try {
097            if (!checkFile(defaultOperationsFilename())) {
098                // The file does not exist, create it before writing
099                java.io.File file = new java.io.File(defaultOperationsFilename());
100                java.io.File parentDir = file.getParentFile();
101                if (!parentDir.exists()) {
102                    if (!parentDir.mkdir()) {
103                        log.error("Directory wasn't created");
104                    }
105                }
106                if (file.createNewFile()) {
107                    log.debug("File created");
108                }
109            }
110            writeFile(defaultOperationsFilename());
111        } catch (IOException e) {
112            log.error("Exception while writing the new CSV operations file, may not be complete: {}",
113                    e.getLocalizedMessage());
114        }
115    }
116
117    public void writeFile(String name) {
118        log.debug("writeFile {}", name);
119        // This is taken in large part from "Java and XML" page 368
120        File file = findFile(name);
121        if (file == null) {
122            file = new File(name);
123        }
124
125        try (CSVPrinter fileOut = new CSVPrinter(
126                new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) {
127
128            loadLayout(fileOut);
129            loadTrainTypes(fileOut);
130            loadSegment(fileOut);
131            loadStations(fileOut);
132            loadSchedule(fileOut);
133            loadTrains(fileOut);
134
135            JmriJOptionPane.showMessageDialog(null,
136                    Bundle.getMessage("ExportedTimetableToFile",
137                            defaultOperationsFilename()),
138                    Bundle.getMessage("ExportComplete"), JmriJOptionPane.INFORMATION_MESSAGE);
139
140        } catch (IOException e) {
141            log.error("Can not open export timetable CSV file: {}", e.getLocalizedMessage());
142            JmriJOptionPane.showMessageDialog(null,
143                    Bundle.getMessage("ExportedTimetableToFile",
144                            defaultOperationsFilename()),
145                    Bundle.getMessage("ExportFailed"), JmriJOptionPane.ERROR_MESSAGE);
146        }
147    }
148
149    /*
150     * "Layout", "layout name", "scale", fastClock, throttles, "metric"
151     */
152    private void loadLayout(CSVPrinter fileOut) throws IOException {
153        fileOut.printRecord("Layout",
154                Setup.getRailroadName(),
155                "HO",
156                "4",
157                "0",
158                "No");
159    }
160
161    /*
162     * "TrainType", "type name", color number
163     */
164    private void loadTrainTypes(CSVPrinter fileOut) throws IOException {
165        fileOut.printRecord("TrainType",
166                "Freight_Black",
167                ColorUtil.colorToHexString(Color.BLACK));
168        fileOut.printRecord("TrainType",
169                "Freight_Red",
170                ColorUtil.colorToHexString(Color.RED));
171        fileOut.printRecord("TrainType",
172                "Freight_Blue",
173                ColorUtil.colorToHexString(Color.BLUE));
174        fileOut.printRecord("TrainType",
175                "Freight_Yellow",
176                ColorUtil.colorToHexString(Color.YELLOW));
177    }
178
179    /*
180     * "Segment", "segment name"
181     */
182    private void loadSegment(CSVPrinter fileOut) throws IOException {
183        fileOut.printRecord("Segment", "Locations");
184    }
185
186    List<Location> locationList = new ArrayList<>();
187
188    /*
189     * "Station", "station name", distance, doubleTrack, sidings, staging
190     */
191    private void loadStations(CSVPrinter fileOut) throws IOException {
192        // provide a list of locations to use, use either a route called
193        // "Timetable" or alphabetically
194
195        Route route = InstanceManager.getDefault(RouteManager.class).getRouteByName("Timetable");
196        if (route != null) {
197            route.getLocationsBySequenceList().forEach(rl -> locationList.add(rl.getLocation()));
198        } else {
199            InstanceManager.getDefault(LocationManager.class).getLocationsByNameList().forEach(location -> locationList.add(location));
200        }
201
202        double distance = 0.0;
203        for (Location location : locationList) {
204            distance += 1.0;
205            fileOut.printRecord("Station",
206                    location.getName(),
207                    distance,
208                    "No",
209                    "0",
210                    location.isStaging() ? location.getTracksList().size() : "0");
211        }
212    }
213
214    /*
215     * "Schedule", "schedule name", "effective date", startHour, duration
216     */
217    private void loadSchedule(CSVPrinter fileOut) throws IOException {
218        // create schedule name based on date and time
219        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd kk:mm");
220        String scheduleName = simpleDateFormat.format(Calendar.getInstance().getTime());
221
222        fileOut.printRecord("Schedule", scheduleName, "Today", "0", "24");
223    }
224
225    /*
226     * "Train", "train name", "train description", type, defaultSpeed,
227     * starttime, throttle, notes
228     */
229    private void loadTrains(CSVPrinter fileOut) throws IOException {
230        int type = 1; // cycle through the 4 train types (chart colors)
231        int defaultSpeed = 4;
232
233        // the following works pretty good for travel times between 1 and 4 minutes
234        if (Setup.getTravelTime() > 0) {
235            defaultSpeed = defaultSpeed/Setup.getTravelTime();
236        }
237
238        for (Train train : InstanceManager.getDefault(TrainManager.class).getTrainsByTimeList()) {
239            if (!train.isBuildEnabled() || train.getRoute() == null) {
240                continue;
241            }
242
243            fileOut.printRecord("Train",
244                    train.getName(),
245                    train.getDescription(),
246                    type++,
247                    defaultSpeed,
248                    train.getDepartTimeMinutes(),
249                    "0",
250                    train.getComment());
251
252            // reset train types
253            if (type > 4) {
254                type = 1;
255            }
256
257            // Stop fields
258            // "Stop", station, duration, nextSpeed, stagingTrack, notes
259            for (RouteLocation rl : train.getRoute().getLocationsBySequenceList()) {
260                // calculate station stop
261                int station = 0;
262                for (Location location : locationList) {
263                    station++;
264                    if (rl.getLocation() == location) {
265                        break;
266                    }
267                }
268                int duration = 0;
269                if ((rl != train.getTrainDepartsRouteLocation() &&
270                        rl.getLocation() != null &&
271                        !rl.getLocation().isStaging())) {
272                    // if there's a departure time, use that
273                    if (!rl.getDepartureTimeHourMinutes().isEmpty()) {
274                        duration = TrainCommon.convertStringTime(rl.getDepartureTime()) -
275                                train.getExpectedTravelTimeInMinutes(rl);
276                    } else if (train.isBuilt()) {
277                        duration = train.getWorkTimeAtLocation(rl) + rl.getWait();
278                    } else {
279                        duration = rl.getMaxCarMoves() * Setup.getSwitchTime() + rl.getWait();
280                    }
281                }
282                fileOut.printRecord("Stop",
283                        station,
284                        duration,
285                        "0",
286                        "0",
287                        rl.getComment());
288            }
289        }
290    }
291
292    public File getExportFile() {
293        return findFile(defaultOperationsFilename());
294    }
295
296    // Operation files always use the same directory
297    public static String defaultOperationsFilename() {
298        return OperationsSetupXml.getFileLocation() +
299                OperationsSetupXml.getOperationsDirectoryName() +
300                File.separator +
301                getOperationsFileName();
302    }
303
304    public static void setOperationsFileName(String name) {
305        operationsFileName = name;
306    }
307
308    public static String getOperationsFileName() {
309        return operationsFileName;
310    }
311
312    private static String operationsFileName = "ExportOperationsTimetable.csv"; // NOI18N
313
314    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExportTimetable.class);
315
316}