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", e);
112        }
113    }
114
115    public void writeFile(String name) {
116        log.debug("writeFile {}", name);
117        // This is taken in large part from "Java and XML" page 368
118        File file = findFile(name);
119        if (file == null) {
120            file = new File(name);
121        }
122
123        try (CSVPrinter fileOut = new CSVPrinter(
124                new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) {
125
126            loadLayout(fileOut);
127            loadTrainTypes(fileOut);
128            loadSegment(fileOut);
129            loadStations(fileOut);
130            loadSchedule(fileOut);
131            loadTrains(fileOut);
132
133            JmriJOptionPane.showMessageDialog(null,
134                    Bundle.getMessage("ExportedTimetableToFile",
135                            defaultOperationsFilename()),
136                    Bundle.getMessage("ExportComplete"), JmriJOptionPane.INFORMATION_MESSAGE);
137
138            fileOut.flush();
139            fileOut.close();
140        } catch (IOException e) {
141            log.error("Can not open export timetable CSV file: {}", file.getName());
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() && rl.getLocation() != null && !rl.getLocation().isStaging())) {
270                    if (train.isBuilt()) {
271                        duration = train.getWorkTimeAtLocation(rl) + rl.getWait();
272                        if (!rl.getDepartureTime().isEmpty() && !train.getExpectedArrivalTime(rl).equals(Train.ALREADY_SERVICED)) {
273                            duration = 60 * Integer.parseInt(rl.getDepartureTimeHour())
274                                    + Integer.parseInt(rl.getDepartureTimeMinute()) - train.getExpectedTravelTimeInMinutes(rl);
275                        }
276                    } else {
277                        duration = rl.getMaxCarMoves() * Setup.getSwitchTime() + rl.getWait();
278                    }
279                }
280                fileOut.printRecord("Stop",
281                        station,
282                        duration,
283                        "0",
284                        "0",
285                        rl.getComment());
286            }
287        }
288    }
289
290    public File getExportFile() {
291        return findFile(defaultOperationsFilename());
292    }
293
294    // Operation files always use the same directory
295    public static String defaultOperationsFilename() {
296        return OperationsSetupXml.getFileLocation() +
297                OperationsSetupXml.getOperationsDirectoryName() +
298                File.separator +
299                getOperationsFileName();
300    }
301
302    public static void setOperationsFileName(String name) {
303        operationsFileName = name;
304    }
305
306    public static String getOperationsFileName() {
307        return operationsFileName;
308    }
309
310    private static String operationsFileName = "ExportOperationsTimetable.csv"; // NOI18N
311
312    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExportTimetable.class);
313
314}