001package jmri.jmrit.operations.locations.tools;
002
003import java.io.BufferedReader;
004import java.io.File;
005import java.util.Locale;
006
007import jmri.InstanceManager;
008import jmri.jmrit.operations.locations.*;
009import jmri.jmrit.operations.locations.divisions.Division;
010import jmri.jmrit.operations.locations.divisions.DivisionManager;
011import jmri.jmrit.operations.locations.schedules.Schedule;
012import jmri.jmrit.operations.locations.schedules.ScheduleManager;
013import jmri.jmrit.operations.rollingstock.ImportCommon;
014import jmri.jmrit.operations.setup.Setup;
015import jmri.util.ThreadingUtil;
016import jmri.util.swing.JmriJOptionPane;
017
018/**
019 * This routine will import Locations from a CSV file into the operations
020 * database. The field order is: Location, Track, Type, Length, Used, Cars,
021 * Locos, Moves, Division, Serviced by Trains Traveling, Rolling Stock, Track
022 * Service Order, Road Option, Roads, Load Option, Loads, Ship Load Option,
023 * Ships, Set Out Restrictions, Restrictions, Pick up Restrictions,
024 * Restrictions, Schedule Name, Mode, Alternate Track, Pool name, Minimum, Track
025 * Blocking Order, Planned Pick Ups, Track Destinations, Destinations, Quick
026 * Service, Hold Cars, Disable Load Change, Swap default loads and empties,
027 * Empty cars with default loads, Generate custom loads for spurs serviced by
028 * this train, Generate custom loads for any spur (multiple trains), Generate
029 * custom loads for any staging track, Block cars by pick up location, Comment,
030 * Comment when there is only pick ups, Comment when there is only set outs
031 */
032public class ImportLocations extends ImportCommon {
033
034    LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
035    DivisionManager divisionManager = InstanceManager.getDefault(DivisionManager.class);
036
037    int tracksAdded = 0;
038
039    protected static final int FIELD_LOCATION = 0;
040    protected static final int FIELD_TRACK = 1;
041    protected static final int FIELD_TYPE = 2;
042    protected static final int FIELD_LENGTH = 3;
043    protected static final int FIELD_USED = 4; // not imported
044    protected static final int FIELD_CARS = 5; // not imported
045    protected static final int FIELD_LOCOS = 6; // not imported
046    protected static final int FIELD_MOVES = 7; // not imported
047    protected static final int FIELD_DIVISION = 8;
048    protected static final int FIELD_SERVICED_BY = 9;
049    protected static final int FIELD_ROLLING_STOCK = 10;
050    protected static final int FIELD_ORDER = 11;
051    protected static final int FIELD_ROAD_OPTION = 12;
052    protected static final int FIELD_ROADS = 13;
053    protected static final int FIELD_LOAD_OPTION = 14;
054    protected static final int FIELD_LOADS = 15;
055    protected static final int FIELD_SHIP_LOAD_OPTION = 16;
056    protected static final int FIELD_SHIPS = 17;
057    // 18 - 21 import not implemented
058    protected static final int FIELD_SET_OUT_RESTRICTIONS = 18;
059    protected static final int FIELD_RESTRICTIONS_1 = 19;
060    protected static final int FIELD_PICK_UP_RESTRICTIONS = 20;
061    protected static final int FIELD_RESTRICTIONS_2 = 21;
062
063    protected static final int FIELD_SCHEDULE_NAME = 22;
064    protected static final int FIELD_SCHEDULE_MODE = 23;
065    protected static final int FIELD_PERCENT_STAGING = 24;
066    protected static final int FIELD_ALTERNATE_TRACK = 25;
067    protected static final int FIELD_POOL_NAME = 26;
068    protected static final int FIELD_TRACK_MINIMUM_POOL = 27;
069    protected static final int FIELD_TRACK_MAXIMUM_POOL = 28;
070    protected static final int FIELD_TRACK_BLOCKING_ORDER = 29;
071    protected static final int FIELD_PLANNED_PICK_UPS = 30;
072    // 31 - 42 import not implemented
073    protected static final int FIELD_TRACK_DESTINATIONS = 31;
074    protected static final int FIELD_DESTINATIONS = 32;
075    protected static final int FIELD_QUICK_SERVICE = 33;
076    protected static final int FIELD_HOLD_CARS_CUSTOM_LOADS = 34;
077    protected static final int FIELD_DISABLE_LOAD_CHANGE = 35;
078    protected static final int FIELD_SWAP_DEFAULT = 36;
079    protected static final int FIELD_EMPTY_DEFAULT_LOADS = 37;
080    protected static final int FIELD_EMPTY_CUSTOM_LOADS = 38;
081    protected static final int FIELD_GENERATE_SPUR = 39;
082    protected static final int FIELD_GENERATE_ANY_SPUR = 40;
083    protected static final int FIELD_GENERATE_STAGING = 41;
084    protected static final int FIELD_BLOCK_CARS_BY_PICKUP = 42;
085
086    protected static final int FIELD_COMMENT = 43;
087    protected static final int FIELD_COMMENT_BOTH = 44;
088    protected static final int FIELD_COMMENT_PICKUPS = 45;
089    protected static final int FIELD_COMMENT_SETOUTS = 46;
090
091    @Override
092    public void run() {
093        File file = getFile();
094        if (file == null) {
095            return;
096        }
097        BufferedReader rdr = getBufferedReader(file);
098        if (rdr == null) {
099            return;
100        }
101        createStatusFrame(Bundle.getMessage("ImportLocations"));
102
103        // read the import (CSV) file
104        String[] inputLine;
105        boolean headerFound = false;
106
107        while (true) {
108            inputLine = readNextLine(rdr);
109            if (inputLine == BREAK) {
110                log.debug("Done");
111                break;
112            }
113            if (inputLine.length < 1) {
114                log.debug("Skipping blank line");
115                continue;
116            }
117            String fieldLocation = "";
118            String fieldTrack = "";
119            String fieldType = "";
120            String fieldLength = "";
121            // header?
122            if (!headerFound && inputLine[FIELD_LOCATION].equals(Bundle.getMessage("Location"))) {
123                headerFound = true;
124                int elementNum = 0;
125                for (String lineElement : inputLine) {
126                    log.debug("Header {} is: {}", elementNum++, lineElement);
127                }
128                continue; // skip header
129            }
130            if (inputLine.length < 4) {
131                log.info("Skipping row {} as we need at least 4 fields (Location, Track, Type and Length)",
132                        Integer.toString(lineNum));
133                continue;
134            }
135            fieldLocation = inputLine[FIELD_LOCATION];
136            Location location = locationManager.getLocationByName(fieldLocation);
137            if (location == null) {
138                log.debug("adding location - {}", fieldLocation);
139                location = locationManager.newLocation(fieldLocation);
140            }
141            fieldTrack = inputLine[FIELD_TRACK];
142            fieldLength = inputLine[FIELD_LENGTH].trim();
143            fieldType = inputLine[FIELD_TYPE].trim();
144            String typeValue = null;
145            if (fieldType.length() > 0) {
146                if (fieldType.equals(Bundle.getMessage("Spur").toLowerCase(Locale.ROOT))) {
147                    typeValue = Track.SPUR;
148                } else if (fieldType.equals(Bundle.getMessage("Yard").toLowerCase(Locale.ROOT))) {
149                    typeValue = Track.YARD;
150                } else if (fieldType.equals(Bundle.getMessage("Class/Interchange"))) {
151                    typeValue = Track.INTERCHANGE;
152                } else if (fieldType.equals(Bundle.getMessage("Staging").toLowerCase(Locale.ROOT))) {
153                    typeValue = Track.STAGING;
154                } else {
155                    typeValue = "unknown";
156                }
157            }
158            Track thisTrack = location.getTrackByName(fieldTrack, null);
159            Integer trackLength = null;
160            try {
161                trackLength = Integer.parseInt(fieldLength);
162            } catch (NumberFormatException exception) {
163                log.info(
164                        "Import caught an exception converting the length field of the new track - value was {} at line number {}",
165                        fieldLength, Integer.toString(lineNum));
166            }
167            if (thisTrack != null) {
168                if (!thisTrack.getTrackType().equals(typeValue)) {
169                    log.debug("Import is changing type of track for Location {} track {} to {}", location.getName(),
170                            thisTrack.getName(), typeValue);
171                    thisTrack.setTrackType(typeValue);
172                }
173            } else {
174                log.debug("Import is adding location {} new track {} of type {}", location.getName(), fieldTrack,
175                        typeValue);
176                thisTrack = location.addTrack(fieldTrack, typeValue);
177                ++tracksAdded;
178            }
179            if (trackLength != null) {
180                thisTrack.setLength(trackLength);
181            }
182
183            // ignore FIELD_USED
184            // ignore FIELD_CARS
185            // ignore FIELD_LOCOS
186            // ignore FIELD_MOVES
187
188            if (inputLine.length >= FIELD_DIVISION) {
189                // division was included in import
190                String fieldDivision = inputLine[FIELD_DIVISION].trim();
191                if (fieldDivision.length() > 0) {
192                    Division division = divisionManager.newDivision(fieldDivision);
193                    location.setDivision(division);
194                    log.debug("Setting this location to division {}", division);
195                }
196            }
197            if (inputLine.length >= FIELD_SERVICED_BY) {
198                // process direction string (a list of directions each ending with a semicolon)
199                String[] directions = inputLine[FIELD_SERVICED_BY].split("; ");
200                log.debug("this track is serviced by {} directions", directions.length);
201                int trackDir = 0; // no direction yet
202                for (String dir : directions) {
203                    trackDir += Setup.getDirectionInt(dir);
204                }
205                thisTrack.setTrainDirections(trackDir);
206                log.debug("setting this location to directions {}", trackDir);
207            }
208            if (inputLine.length >= FIELD_ROLLING_STOCK) {
209                // process rolling stock accepted
210                if (inputLine[FIELD_ROLLING_STOCK].length() > 0) {
211                    log.debug("Setting track to accepting the following rolling stock: {}",
212                            inputLine[FIELD_ROLLING_STOCK]);
213                    // first we need to remove all rolling stock types
214                    for (String typeName : thisTrack.getTypeNames()) {
215                        thisTrack.deleteTypeName(typeName);
216                    }
217                    String[] rollingStock = inputLine[FIELD_ROLLING_STOCK].split("; ");
218                    for (String typeName : rollingStock) {
219                        thisTrack.addTypeName(typeName);
220                    }
221                }
222            }
223            if (inputLine.length >= FIELD_ORDER) {
224                // process service order (Normal, FIFO or LIFO - Track handles the bundling
225                String fieldServiceOrder = inputLine[FIELD_ORDER].trim();
226                if (fieldServiceOrder.length() > 0) {
227                    thisTrack.setServiceOrder(fieldServiceOrder);
228                    log.debug("Setting the service order to {}", fieldServiceOrder);
229                }
230            }
231            if (inputLine.length >= FIELD_ROADS) {
232                log.debug("setting the road names to: {}", inputLine[FIELD_ROADS]);
233                // note -- don't trim so the final semi-colon space remains on the last field
234                if (inputLine[FIELD_ROADS].length() > 0) {
235                    String[] roads = inputLine[FIELD_ROADS].split("; ");
236                    for (String road : roads) {
237                        thisTrack.addRoadName(road);
238                    }
239                }
240            }
241            if (inputLine.length >= FIELD_ROAD_OPTION) {
242                // process road option - again use the words imported
243                String roadOptions = inputLine[FIELD_ROAD_OPTION].trim();
244                String optionValue = "";
245                if (roadOptions.length() > 0) {
246                    if (roadOptions.startsWith(Bundle.getMessage("AcceptsAllRoads"))) {
247                        optionValue = Track.ALL_ROADS;
248                    } else if (roadOptions.startsWith(Bundle.getMessage("AcceptOnly"))) {
249                        optionValue = Track.INCLUDE_ROADS;
250                    } else if (roadOptions.startsWith(Bundle.getMessage("Exclude"))) {
251                        optionValue = Track.EXCLUDE_ROADS;
252                    }
253                    thisTrack.setRoadOption(optionValue);
254                    log.debug("setting the road options to {}", optionValue);
255                }
256            }
257            if (inputLine.length >= FIELD_LOAD_OPTION) {
258                String loadOptions = inputLine[FIELD_LOAD_OPTION].trim();
259                String optionValue = "";
260                if (loadOptions.length() > 0) {
261                    if (loadOptions.startsWith(Bundle.getMessage("AcceptsAllLoads"))) {
262                        optionValue = Track.ALL_LOADS;
263                    } else if (loadOptions.startsWith(Bundle.getMessage("AcceptOnly"))) {
264                        optionValue = Track.INCLUDE_ROADS;
265                    } else if (loadOptions.startsWith(Bundle.getMessage("Exclude"))) {
266                        optionValue = Track.EXCLUDE_LOADS;
267                    } else {
268                        log.error("Locations Import load option was not recognized: {} ", loadOptions);
269                    }
270                    thisTrack.setLoadOption(optionValue);
271                }
272            }
273            if (inputLine.length >= FIELD_LOADS) {
274                // process names of loads, again, don't trim first
275                if (inputLine[FIELD_LOADS].length() > 0) {
276                    String[] loads = inputLine[FIELD_LOADS].split("; ");
277                    log.debug("This location is surviced by {} loads", loads.length);
278                    for (String load : loads) {
279                        thisTrack.addLoadName(load);
280                    }
281                }
282            }
283            if (inputLine.length >= FIELD_SHIP_LOAD_OPTION) {
284                String loadOptions = inputLine[FIELD_SHIP_LOAD_OPTION].trim();
285                String optionValue = "";
286                if (loadOptions.length() > 0) {
287                    if (loadOptions.startsWith(Bundle.getMessage("ShipsAllLoads"))) {
288                        optionValue = Track.ALL_LOADS;
289                    } else if (loadOptions.startsWith(Bundle.getMessage("ShipOnly"))) {
290                        optionValue = Track.INCLUDE_ROADS;
291                    } else if (loadOptions.startsWith(Bundle.getMessage("Exclude"))) {
292                        optionValue = Track.EXCLUDE_LOADS;
293                    } else {
294                        log.error("Locations Import ship load option was not recognized: {} ", loadOptions);
295                    }
296                    thisTrack.setShipLoadOption(optionValue);
297                }
298            }
299            if (inputLine.length >= FIELD_SHIPS) {
300                // process names of loads, again, don't trim first
301                if (inputLine[FIELD_SHIPS].length() > 0) {
302                    String[] loads = inputLine[FIELD_SHIPS].split("; ");
303                    log.debug("This location ships {} loads", loads.length);
304                    for (String load : loads) {
305                        thisTrack.addShipLoadName(load);
306                    }
307                }
308            }
309
310            // TODO import fields 18 through 21
311
312            if (inputLine.length >= FIELD_SCHEDULE_NAME) {
313                String scheduleName = inputLine[FIELD_SCHEDULE_NAME].trim();
314                Schedule schedule = InstanceManager.getDefault(ScheduleManager.class).newSchedule(scheduleName);
315                thisTrack.setSchedule(schedule);
316            }
317            if (inputLine.length >= FIELD_SCHEDULE_MODE) {
318                String scheduleMode = inputLine[FIELD_SCHEDULE_MODE].trim();
319                // default is match mode
320                if (scheduleMode.equals(Bundle.getMessage("Sequential"))) {
321                    thisTrack.setScheduleMode(Track.SEQUENTIAL);
322                }
323            }
324            if (inputLine.length >= FIELD_PERCENT_STAGING) {
325                String percentStaging = inputLine[FIELD_PERCENT_STAGING].trim();
326                try {
327                    thisTrack.setReservationFactor(Integer.parseInt(percentStaging));
328                } catch (NumberFormatException exception) {
329                    log.debug("Exception converting percentage from staging - value was {}", percentStaging);
330                }
331            }
332            if (inputLine.length >= FIELD_ALTERNATE_TRACK) {
333                String alternateTrackName = inputLine[FIELD_ALTERNATE_TRACK].trim();
334                if (!alternateTrackName.isBlank() && !alternateTrackName.equals(Bundle.getMessage("ButtonYes"))) {
335                    Track altTrack = location.getTrackByName(alternateTrackName, null);
336                    if (altTrack == null) {
337                        altTrack = location.addTrack(alternateTrackName, Track.YARD);
338                        ++tracksAdded;
339                    }
340                    thisTrack.setAlternateTrack(altTrack);
341                }
342            }
343            if (inputLine.length >= FIELD_POOL_NAME) {
344                String poolName = inputLine[FIELD_POOL_NAME].trim();
345                Pool pool = location.addPool(poolName);
346                thisTrack.setPool(pool);
347            }
348            if (inputLine.length >= FIELD_TRACK_MINIMUM_POOL) {
349                String minPool = inputLine[FIELD_TRACK_MINIMUM_POOL].trim();
350                if (minPool.length() > 0) {
351                    log.debug("setting track pool minimum: {}", minPool);
352                    try {
353                        thisTrack.setPoolMinimumLength(Integer.parseInt(minPool));
354                    } catch (NumberFormatException exception) {
355                        log.debug("Exception converting the ignore minimum to a number - value was {}", minPool);
356                    }
357                }
358            }
359            if (inputLine.length >= FIELD_TRACK_BLOCKING_ORDER) {
360                String fieldTrackBlockingOrder = inputLine[FIELD_TRACK_BLOCKING_ORDER].trim();
361                if (fieldTrackBlockingOrder.length() > 0) {
362                    log.debug("setting the blocking order to {}", fieldTrackBlockingOrder);
363                    Integer blockingOrder = null;
364                    try {
365                        blockingOrder = Integer.parseInt(fieldTrackBlockingOrder);
366                        thisTrack.setBlockingOrder(blockingOrder);
367                    } catch (NumberFormatException exception) {
368                        log.debug("Exception converting the track blocking order to a number - value was {}",
369                                fieldTrackBlockingOrder);
370                    }
371                }
372            }
373            if (inputLine.length >= FIELD_PLANNED_PICK_UPS) {
374                String ignoreUsedLength = inputLine[FIELD_PLANNED_PICK_UPS].trim();
375                if (ignoreUsedLength.length() > 0) {
376                    try {
377                        Integer ignorePercentage = Integer.parseInt(ignoreUsedLength);
378                        thisTrack.setIgnoreUsedLengthPercentage(ignorePercentage);
379                    } catch (NumberFormatException exception) {
380                        log.debug("Exception converting field Ignore Used track Percentage - value was {}",
381                                ignoreUsedLength);
382                    }
383                }
384            }
385
386            // TODO import fields 31 though 42
387
388            if (inputLine.length >= FIELD_COMMENT) {
389                String fieldComment = inputLine[FIELD_COMMENT].trim();
390                if (fieldComment.length() > 0) {
391                    log.debug("setting the location comment to: {}", fieldComment);
392                    thisTrack.setComment(fieldComment);
393                }
394            }
395            if (inputLine.length >= FIELD_COMMENT_BOTH) {
396                String commentBoth = inputLine[FIELD_COMMENT_BOTH].trim();
397                thisTrack.setCommentBoth(commentBoth);
398            }
399            if (inputLine.length >= FIELD_COMMENT_PICKUPS) {
400                String commentPickups = inputLine[FIELD_COMMENT_PICKUPS].trim();
401                thisTrack.setCommentPickup(commentPickups);
402            }
403            if (inputLine.length >= FIELD_COMMENT_SETOUTS) {
404                String commentSetouts = inputLine[FIELD_COMMENT_SETOUTS].trim();
405                thisTrack.setCommentSetout(commentSetouts);
406            }
407        }
408        ThreadingUtil.runOnGUI(() -> {
409            if (importOkay) {
410                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ImportTracksAdded", tracksAdded),
411                        Bundle.getMessage("SuccessfulImport"), JmriJOptionPane.INFORMATION_MESSAGE);
412            } else {
413                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ImportTracksAdded", tracksAdded),
414                        Bundle.getMessage("ImportFailed"), JmriJOptionPane.ERROR_MESSAGE);
415            }
416        });
417        fstatus.dispose();
418    }
419
420    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ImportLocations.class);
421
422}