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