001package jmri.jmrit.dispatcher;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.List;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010import jmri.util.FileUtil;
011import jmri.util.XmlFilenameFilter;
012import org.jdom2.Document;
013import org.jdom2.Element;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017import jmri.InstanceManager;
018import jmri.configurexml.AbstractXmlAdapter.EnumIO;
019import jmri.configurexml.AbstractXmlAdapter.EnumIoNamesNumbers;
020import jmri.jmrit.dispatcher.ActiveTrain.TrainDetection;
021
022/**
023 * Handles reading and writing of TrainInfo files to disk as an XML file to/from
024 * the dispatcher/traininfo/ directory in the user's preferences area
025 * <p>
026 * This class manipulates the files conforming to the dispatcher-traininfo DTD
027 * <p>
028 * The file is written when the user requests that train information be saved. A
029 * TrainInfo file is read when the user request it in the Activate New Train
030 * window
031 *
032 * <p>
033 * This file is part of JMRI.
034 * <p>
035 * JMRI is open source software; you can redistribute it and/or modify it under
036 * the terms of version 2 of the GNU General Public License as published by the
037 * Free Software Foundation. See the "COPYING" file for a copy of this license.
038 * <p>
039 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
040 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
041 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
042 *
043 * @author Dave Duchamp Copyright (C) 2009
044 */
045public class TrainInfoFile extends jmri.jmrit.XmlFile {
046
047    public TrainInfoFile() {
048        super();
049    }
050    // operational variables
051    private String fileLocation = FileUtil.getUserFilesPath()
052            + "dispatcher" + File.separator + "traininfo" + File.separator;
053
054    public void setFileLocation(String testLocation) {
055        fileLocation = testLocation;
056    }
057    private Document doc = null;
058    private Element root = null;
059
060    static final EnumIO<ActiveTrain.TrainDetection> trainsdectionFromEnumMap = new EnumIoNamesNumbers<>(ActiveTrain.TrainDetection.class);
061    static final EnumIO<ActiveTrain.TrainLengthUnits> trainlengthFromEnumMap = new EnumIoNamesNumbers<>(ActiveTrain.TrainLengthUnits.class);
062
063    /*
064     *  Reads Dispatcher TrainInfo from a file in the user's preferences directory
065     *  If the file containing Dispatcher TrainInfo does not exist this routine returns quietly.
066     *  "name" is assumed to have the .xml or .XML extension already included
067     */
068    public TrainInfo readTrainInfo(String name) throws org.jdom2.JDOMException, java.io.IOException {
069        log.debug("entered readTrainInfo for {}", name);
070        TrainInfo tInfo = null;
071        int version  = 1;
072        // check if file exists
073        if (checkFile(fileLocation + name)) {
074            // file is present.
075            tInfo = new TrainInfo();
076            root = rootFromName(fileLocation + name);
077            if (root != null) {
078                // there is a file
079                Element traininfo = root.getChild("traininfo");
080                if (traininfo != null) {
081                    // get version so we dont look for missing fields
082                    if (traininfo.getAttribute("version") != null ) {
083                        try {
084                            version = traininfo.getAttribute("version").getIntValue();
085                        } catch (org.jdom2.DataConversionException ex) {
086                            log.error("Traininfo file version number not an integer: assuming version 1");
087                            version = 1;
088                        }
089                    } else {
090                        version = 1;
091                    }
092                    tInfo.setVersion(version);
093                    // there are train info options defined, read them
094                    if (traininfo.getAttribute("transitname") != null) {
095                        // there is a transit name selected
096                        tInfo.setTransitName(traininfo.getAttribute("transitname").getValue());
097                    } else {
098                        log.error("Transit name missing when reading TrainInfoFile {}", name);
099                    }
100                    if (traininfo.getAttribute("dynamictransit") != null ) {
101                        tInfo.setDynamicTransit(traininfo.getAttribute("dynamictransit").getValue().equals("yes"));
102                    }
103                    if (traininfo.getAttribute("dynamictransitcloseloop") != null ) {
104                        tInfo.setDynamicTransitCloseLoopIfPossible(traininfo.getAttribute("dynamictransitcloseloop").getValue().equals("yes"));
105                    }
106                    if (version < 6) {
107                        if (traininfo.getAttribute("trainname") != null) {
108                            tInfo.setTrainName(traininfo.getAttribute("trainname").getValue());
109                            tInfo.setRosterId(traininfo.getAttribute("trainname").getValue());
110                            tInfo.setTrainUserName(traininfo.getAttribute("trainname").getValue());
111                        } else {
112                            log.error("Train name missing when reading TrainInfoFile {}", name);
113                        }
114                    } else {
115                        if (traininfo.getAttribute("trainname") != null) {
116                            tInfo.setTrainName(traininfo.getAttribute("trainname").getValue());
117                        }
118                        if (traininfo.getAttribute("rosterid") != null) {
119                            tInfo.setRosterId(traininfo.getAttribute("rosterid").getValue());
120                        }
121                        if (traininfo.getAttribute("trainusername") != null) {
122                            tInfo.setTrainUserName(traininfo.getAttribute("trainusername").getValue());
123                        }
124                    }
125                    if (traininfo.getAttribute("dccaddress") != null) {
126                        tInfo.setDccAddress(traininfo.getAttribute("dccaddress").getValue());
127                    } else {
128                        log.error("DCC Address missing when reading TrainInfoFile {}", name);
129                    }
130                    if (traininfo.getAttribute("trainintransit") != null) {
131                        tInfo.setTrainInTransit(true);
132                        if (traininfo.getAttribute("trainintransit").getValue().equals("no")) {
133                            tInfo.setTrainInTransit(false);
134                        }
135                    } else {
136                        log.error("Train in Transit check box missing  when reading TrainInfoFile {}", name);
137                    }
138                    if (traininfo.getAttribute("startblockname") != null) {
139                        // there is a transit name selected
140                        tInfo.setStartBlockName(traininfo.getAttribute("startblockname").getValue());
141                    } else {
142                        log.error("Start block name missing when reading TrainInfoFile {}", name);
143                    }
144                    if (traininfo.getAttribute("endblockname") != null) {
145                        // there is a transit name selected
146                        tInfo.setDestinationBlockName(traininfo.getAttribute("endblockname").getValue());
147                    } else {
148                        log.error("Destination block name missing when reading TrainInfoFile {}", name);
149                    }
150                    if (tInfo.getDynamicTransit()) {
151                        if (traininfo.getAttribute("viablockname") != null) {
152                            // there is a transit name selected
153                            tInfo.setViaBlockName(traininfo.getAttribute("viablockname").getValue());
154                        } else {
155                            log.error("Via block name missing for dynamic trainsit when reading TrainInfoFile {}", name);
156                        }
157                    }
158                    if (traininfo.getAttribute("trainfromroster") != null) {
159                        tInfo.setTrainFromRoster(true);
160                        if (traininfo.getAttribute("trainfromroster").getValue().equals("no")) {
161                            tInfo.setTrainFromRoster(false);
162                        }
163                    }
164                    if (traininfo.getAttribute("trainfromtrains") != null) {
165                        tInfo.setTrainFromTrains(true);
166                        if (traininfo.getAttribute("trainfromtrains").getValue().equals("no")) {
167                            tInfo.setTrainFromTrains(false);
168                        }
169                    }
170                    if (traininfo.getAttribute("trainfromuser") != null) {
171                        tInfo.setTrainFromUser(true);
172                        if (traininfo.getAttribute("trainfromuser").getValue().equals("no")) {
173                            tInfo.setTrainFromUser(false);
174                        }
175                    }
176                    if (traininfo.getAttribute("trainfromsetlater") != null) {
177                        tInfo.setTrainFromSetLater(true);
178                        if (traininfo.getAttribute("trainfromsetlater").getValue().equals("no")) {
179                            tInfo.setTrainFromSetLater(false);
180                        }
181                    }
182                    if (traininfo.getAttribute("priority") != null) {
183                        tInfo.setPriority(Integer.parseInt(traininfo.getAttribute("priority").getValue()));
184                    } else {
185                        log.error("Priority missing when reading TrainInfoFile {}", name);
186                    }
187                    if (traininfo.getAttribute("allocatealltheway") != null) {
188                        if (traininfo.getAttribute("allocatealltheway").getValue().equals("yes")) {
189                            tInfo.setAllocateAllTheWay(true);
190                        }
191                    }
192                    if (traininfo.getAttribute("allocationmethod") != null) {
193                        tInfo.setAllocationMethod(traininfo.getAttribute("allocationmethod").getIntValue());
194                    }
195                    if (traininfo.getAttribute("nexttrain") != null) {
196                        tInfo.setNextTrain(traininfo.getAttribute("nexttrain").getValue());
197                    }
198                    if (traininfo.getAttribute("resetwhendone") != null) {
199                        if (traininfo.getAttribute("resetwhendone").getValue().equals("yes")) {
200                            tInfo.setResetWhenDone(true);
201                        }
202                        if (traininfo.getAttribute("delayedrestart") != null) {
203                            // for older files that didnot have seperate restart details for to and fro
204                            // we default that data to this data.
205                            switch (traininfo.getAttribute("delayedrestart").getValue()) {
206                                case "no":
207                                    tInfo.setDelayedRestart(ActiveTrain.NODELAY);
208                                    tInfo.setReverseDelayedRestart(ActiveTrain.NODELAY);
209                                    break;
210                                case "sensor":
211                                    tInfo.setDelayedRestart(ActiveTrain.SENSORDELAY);
212                                    tInfo.setReverseDelayedRestart(ActiveTrain.SENSORDELAY);
213                                    if (traininfo.getAttribute("delayedrestartsensor") != null) {
214                                        tInfo.setRestartSensorName(traininfo.getAttribute("delayedrestartsensor").getValue());
215                                        tInfo.setReverseRestartSensorName(traininfo.getAttribute("delayedrestartsensor").getValue());
216                                    }
217                                    if (traininfo.getAttribute("resetrestartsensor") != null) {
218                                        tInfo.setResetRestartSensor(traininfo.getAttribute("resetrestartsensor").getValue().equals("yes"));
219                                        tInfo.setReverseResetRestartSensor(traininfo.getAttribute("resetrestartsensor").getValue().equals("yes"));
220                                    }
221                                    break;
222                                case "timed":
223                                    tInfo.setDelayedRestart(ActiveTrain.TIMEDDELAY);
224                                    tInfo.setReverseDelayedRestart(ActiveTrain.TIMEDDELAY);
225                                    if (traininfo.getAttribute("delayedrestarttime") != null) {
226                                        tInfo.setRestartDelayMin((int) traininfo.getAttribute("delayedrestarttime").getLongValue());
227                                        tInfo.setReverseRestartDelayMin((int) traininfo.getAttribute("delayedrestarttime").getLongValue());
228                                    }   break;
229                                default:
230                                    break;
231                            }
232                        }
233                    }
234                    if (traininfo.getAttribute("reverseatend") != null) {
235                        tInfo.setReverseAtEnd(true);
236                        if (traininfo.getAttribute("reverseatend").getValue().equals("no")) {
237                            tInfo.setReverseAtEnd(false);
238                        }
239                        if (version > 3) {
240                            // fro delays are independent from to delays
241                            if (traininfo.getAttribute("reversedelayedrestart") != null) {
242                                switch (traininfo.getAttribute("reversedelayedrestart").getValue()) {
243                                    case "no":
244                                        tInfo.setReverseDelayedRestart(ActiveTrain.NODELAY);
245                                        break;
246                                    case "sensor":
247                                        tInfo.setReverseDelayedRestart(ActiveTrain.SENSORDELAY);
248                                        if (traininfo.getAttribute("reversedelayedrestartsensor") != null) {
249                                            tInfo.setReverseRestartSensorName(
250                                                    traininfo.getAttribute("reversedelayedrestartsensor").getValue());
251                                        }
252                                        if (traininfo.getAttribute("reverseresetrestartsensor") != null) {
253                                            tInfo.setReverseResetRestartSensor(
254                                                    traininfo.getAttribute("reverseresetrestartsensor").getValue()
255                                                            .equals("yes"));
256                                        }
257                                        break;
258                                    case "timed":
259                                        tInfo.setReverseDelayedRestart(ActiveTrain.TIMEDDELAY);
260                                        if (traininfo.getAttribute("reversedelayedrestarttime") != null) {
261                                            tInfo.setReverseRestartDelayMin((int) traininfo
262                                                    .getAttribute("reversedelayedrestarttime").getLongValue());
263                                        }
264                                        break;
265                                    default:
266                                        break;
267                                }
268                            }
269                        }
270                    }
271                    if (traininfo.getAttribute("delayedstart") != null) {
272                        switch (traininfo.getAttribute("delayedstart").getValue()) {
273                            case "no":
274                                tInfo.setDelayedStart(ActiveTrain.NODELAY);
275                                break;
276                            case "sensor":
277                                tInfo.setDelayedStart(ActiveTrain.SENSORDELAY);
278                                break;
279                            default:
280                                //This covers the old versions of the file with "yes"
281                                tInfo.setDelayedStart(ActiveTrain.TIMEDDELAY);
282                                break;
283                        }
284                    }
285                    if (traininfo.getAttribute("departuretimehr") != null) {
286                        tInfo.setDepartureTimeHr(Integer.parseInt(traininfo.getAttribute("departuretimehr").getValue()));
287                    }
288                    if (traininfo.getAttribute("departuretimemin") != null) {
289                        tInfo.setDepartureTimeMin(Integer.parseInt(traininfo.getAttribute("departuretimemin").getValue()));
290                    }
291                    if (traininfo.getAttribute("delayedSensor") != null) {
292                        tInfo.setDelaySensorName(traininfo.getAttribute("delayedSensor").getValue());
293                    }
294                    if (traininfo.getAttribute("resetstartsensor") != null) {
295                        tInfo.setResetStartSensor(traininfo.getAttribute("resetstartsensor").getValue().equals("yes"));
296                    }
297                    if (traininfo.getAttribute("traintype") != null) {
298                        tInfo.setTrainType(traininfo.getAttribute("traintype").getValue());
299                    }
300                    if (traininfo.getAttribute("autorun") != null) {
301                        tInfo.setAutoRun(true);
302                        if (traininfo.getAttribute("autorun").getValue().equals("no")) {
303                            tInfo.setAutoRun(false);
304                        }
305                    }
306                    if (traininfo.getAttribute("loadatstartup") != null) {
307                        tInfo.setLoadAtStartup(true);
308                        if (traininfo.getAttribute("loadatstartup").getValue().equals("no")) {
309                            tInfo.setLoadAtStartup(false);
310                        }
311                    }
312                    // here retrieve items related only to automatically run trains if present
313                    if (traininfo.getAttribute("speedfactor") != null) {
314                        tInfo.setSpeedFactor(Float.parseFloat(traininfo.getAttribute("speedfactor").getValue()));
315                    }
316                    if (traininfo.getAttribute("maxspeed") != null) {
317                        tInfo.setMaxSpeed(Float.parseFloat(traininfo.getAttribute("maxspeed").getValue()));
318                    }
319                    // Optional scale km/h cap (absent in older files).
320                    if (traininfo.getAttribute("maxspeedscalekmh") != null) {
321                        try {
322                            tInfo.setMaxSpeedScaleKmh(traininfo.getAttribute("maxspeedscalekmh").getFloatValue());
323                        } catch (org.jdom2.DataConversionException ex) {
324                            // Malformed value: leave unset (0.0f) for backward compatibility.
325                        }
326                    }
327                    if (traininfo.getAttribute("minreliableoperatingspeed") != null) {
328                        tInfo.setMinReliableOperatingSpeed(Float.parseFloat(traininfo.getAttribute("minreliableoperatingspeed").getValue()));
329                    }
330                    if (traininfo.getAttribute("ramprate") != null) {
331                        tInfo.setRampRate(traininfo.getAttribute("ramprate").getValue());
332                    }
333                    tInfo.setTrainDetection(TrainDetection.TRAINDETECTION_WHOLETRAIN);
334                    if (version > 4) {
335                        if (traininfo.getAttribute("traindetection") != null) {
336                            tInfo.setTrainDetection(trainsdectionFromEnumMap.inputFromAttribute(traininfo.getAttribute("traindetection")));
337                        }
338                    }
339                    else {
340                        if (traininfo.getAttribute("resistancewheels").getValue().equals("no")) {
341                            tInfo.setTrainDetection(TrainDetection.TRAINDETECTION_HEADONLY);
342                        }
343                    }
344                    if (traininfo.getAttribute("runinreverse") != null) {
345                        tInfo.setRunInReverse(true);
346                        if (traininfo.getAttribute("runinreverse").getValue().equals("no")) {
347                            tInfo.setRunInReverse(false);
348                        }
349                    }
350                    if (traininfo.getAttribute("sounddecoder") != null) {
351                        tInfo.setSoundDecoder(true);
352                        if (traininfo.getAttribute("sounddecoder").getValue().equals("no")) {
353                            tInfo.setSoundDecoder(false);
354                        }
355                    }
356                    if (version > 5) {
357                        if (traininfo.getAttribute("trainlengthunits") != null) {
358                            tInfo.setTrainLengthUnits(trainlengthFromEnumMap.inputFromAttribute(traininfo.getAttribute("trainlengthunits")));
359                        }
360                    }
361                    if (traininfo.getAttribute("maxtrainlengthscalemeters") != null) {
362                        tInfo.setMaxTrainLengthScaleMeters(Float.parseFloat(traininfo.getAttribute("maxtrainlengthscalemeters").getValue()));
363                    } else {
364                        if (traininfo.getAttribute("maxtrainlength") != null) {
365                            if (InstanceManager.getDefault(DispatcherFrame.class).getUseScaleMeters()) {
366                                tInfo.setMaxTrainLengthScaleMeters(Float.parseFloat(traininfo.getAttribute("maxtrainlength").getValue()));
367                            } else {
368                                tInfo.setMaxTrainLengthScaleFeet(Float.parseFloat(traininfo.getAttribute("maxtrainlength").getValue()));
369                            }
370                        }
371                    }
372                    if (traininfo.getAttribute("terminatewhendone") != null) {
373                        tInfo.setTerminateWhenDone(false);
374                        if (traininfo.getAttribute("terminatewhendone").getValue().equals("yes")) {
375                            tInfo.setTerminateWhenDone(true);
376                        }
377                    }
378                    if (traininfo.getAttribute("usespeedprofile") != null) {
379                        tInfo.setUseSpeedProfile(false);
380                        if (traininfo.getAttribute("usespeedprofile").getValue().equals("yes")) {
381                            tInfo.setUseSpeedProfile(true);
382                        }
383                    }
384                    if (traininfo.getAttribute("stopbyspeedprofile") != null) {
385                        tInfo.setStopBySpeedProfile(false);
386                        if (traininfo.getAttribute("stopbyspeedprofile").getValue().equals("yes")) {
387                            tInfo.setStopBySpeedProfile(true);
388                        }
389                    }
390
391                 // Read the per-train stop-by-speed-profile adjust factor (fraction of block length)
392                 // Preferred attribute name since v5+: "stopbyspeedprofileadjust"
393                 // Backward-compatibility: accept a couple of historical synonyms if present.
394                 org.jdom2.Attribute adjAttr = traininfo.getAttribute("stopbyspeedprofileadjust");
395                 if (adjAttr == null) {
396                     // Legacy fallbacks seen in older files/tests
397                     adjAttr = traininfo.getAttribute("speedprofileadjust");
398                 }
399                 if (adjAttr == null) {
400                     adjAttr = traininfo.getAttribute("usespeedprofileadjust");
401                 }
402                 if (adjAttr != null) {
403                     try {
404                         float v = adjAttr.getFloatValue();
405                         // Sanity: bounds to [0.0, 1.0] if needed; older content is a fraction (not percent)
406                         if (v < 0.0f) v = 0.0f;
407                         if (v > 1.0f) v = 1.0f;
408                         tInfo.setStopBySpeedProfileAdjust(v);
409                     } catch (org.jdom2.DataConversionException ex) {
410                         // Malformed value -> leave adjust at default for backward compatibility
411                     }
412                 }
413
414                 // Preferred: "overridestopsensor" (yes/no or true/false or 1/0)
415                 // Fallback:  legacy "usestopsensor" (yes/no or true/false or 1/0)
416                 // Default:   use sensors (existing behavior)
417                 boolean useSensors = true; // default preserves existing behavior
418
419                 if (traininfo.getAttribute("overridestopsensor") != null) {
420                     String v = traininfo.getAttribute("overridestopsensor").getValue();
421                     boolean override = "yes".equalsIgnoreCase(v) || "true".equalsIgnoreCase(v) || "1".equals(v);
422                     useSensors = !override;
423                 } else if (traininfo.getAttribute("usestopsensor") != null) {
424                     String v = traininfo.getAttribute("usestopsensor").getValue();
425                     boolean legacyUse = "yes".equalsIgnoreCase(v) || "true".equalsIgnoreCase(v) || "1".equals(v);
426                     useSensors = legacyUse;
427                 }
428                 tInfo.setUseStopSensor(useSensors);
429
430                 if (traininfo.getAttribute("stopbydistance_mm") != null) {
431                     try {
432                         float mm = traininfo.getAttribute("stopbydistance_mm").getFloatValue();
433                         if (mm > 0.0f) {
434                             tInfo.setStopByDistanceMm(mm);
435                             // HEAD by default; override if attribute present and equals "TAIL"
436                             if (traininfo.getAttribute("stopbydistance_ref") != null) {
437                                 String ref = traininfo.getAttribute("stopbydistance_ref").getValue();
438                                 if ("TAIL".equalsIgnoreCase(ref)) {
439                                     tInfo.setStopByDistanceRef(TrainInfo.StopReference.TAIL);
440                                 } else {
441                                     tInfo.setStopByDistanceRef(TrainInfo.StopReference.HEAD);
442                                 }
443                             }
444                         }
445                     } catch (org.jdom2.DataConversionException ex) {
446                         // Malformed value => leave unset for backward compatibility
447                     }
448                 }
449                    
450                    if (traininfo.getAttribute("waittime") != null) {
451                        tInfo.setWaitTime(traininfo.getAttribute("waittime").getFloatValue());
452                    }
453                    if (traininfo.getAttribute("blockname") != null) {
454                        tInfo.setBlockName(traininfo.getAttribute("blockname").getValue());
455                    }
456                    if (traininfo.getAttribute("fnumberlight") != null) {
457                        tInfo.setFNumberLight(Integer.parseInt(traininfo.getAttribute("fnumberlight").getValue()));
458                    }
459                    if (traininfo.getAttribute("fnumberbell") != null) {
460                        tInfo.setFNumberBell(Integer.parseInt(traininfo.getAttribute("fnumberbell").getValue()));
461                    }
462                    if (traininfo.getAttribute("fnumberhorn") != null) {
463                        tInfo.setFNumberHorn(Integer.parseInt(traininfo.getAttribute("fnumberhorn").getValue()));
464                    }
465                    
466                    // Physics: read additional train weight (metric tonnes)
467                    if (traininfo.getAttribute("additionaltrainweight_tonnes") != null) {
468                        try {
469                            tInfo.setAdditionalTrainWeightMetricTonnes(traininfo.getAttribute("additionaltrainweight_tonnes").getFloatValue());
470                        } catch (org.jdom2.DataConversionException ex) {
471                            // Malformed value -> leave default (0.0f) for backward compatibility
472                        }
473                    }
474                     
475                    // Physics: read rolling resistance coefficient (dimensionless)
476                    if (traininfo.getAttribute("rollingresistancecoeff") != null) {
477                        try {
478                            tInfo.setRollingResistanceCoeff(traininfo.getAttribute("rollingresistancecoeff").getFloatValue());
479                        } catch (org.jdom2.DataConversionException ex) {
480                            // Malformed value -> leave default (0.002f) for backward compatibility
481                        }
482                    }
483                     
484                      // Physics: read driver power percent (stored as fraction 0..1; default 1.0 if absent)
485                     if (traininfo.getAttribute("driverpowerpercent") != null) {
486                         try {
487                             float dp = traininfo.getAttribute("driverpowerpercent").getFloatValue();
488                             if (dp < 0.0f) dp = 0.0f;
489                             if (dp > 1.0f) dp = 1.0f;
490                             tInfo.setDriverPowerPercent(dp);
491                         } catch (org.jdom2.DataConversionException ex) {
492                             // Malformed: ignore and leave TrainInfo default (typically 1.0f)
493                         }
494                     }
495            
496                    if (version == 1) {
497                        String parseArray[];
498                        // If you only have a systemname then its everything before the dash
499                        tInfo.setStartBlockId(tInfo.getStartBlockName().split("-")[0]);
500                        // If you have a systemname and username you want everything before the open bracket
501                        tInfo.setStartBlockId(tInfo.getStartBlockId().split("\\(")[0]);
502                        // to guard against a dash in the names, we need the last part, not just [1]
503                        parseArray = tInfo.getStartBlockName().split("-");
504                        tInfo.setStartBlockSeq(-1); // default value
505                        if (parseArray.length > 0) {
506                            try {
507                                tInfo.setStartBlockSeq(Integer.parseInt(parseArray[parseArray.length -1]));
508                            }
509                            catch (NumberFormatException Ex) {
510                                log.error("Invalid StartBlockSequence{}",parseArray[parseArray.length -1]);
511                            }
512                        }
513                        // repeat for destination
514                        tInfo.setDestinationBlockId(tInfo.getDestinationBlockName().split("-")[0]);
515                        tInfo.setDestinationBlockId(tInfo.getDestinationBlockId().split("\\(")[0]);
516                        parseArray = tInfo.getDestinationBlockName().split("-");
517                        tInfo.setDestinationBlockSeq(-1);
518                        if (parseArray.length > 0) {
519                            try {
520                                tInfo.setDestinationBlockSeq(Integer.parseInt(parseArray[parseArray.length -1]));
521                            }
522                            catch (NumberFormatException Ex) {
523                                log.error("Invalid StartBlockSequence{}",parseArray[parseArray.length -1]);
524                            }
525                        }
526                        // Transit we need the whole thing or the bit before the first open bracket
527                        tInfo.setTransitId(tInfo.getTransitName().split("\\(")[0]);
528                        log.debug("v1: t = {}, bs = {}, be = {}", tInfo.getTransitName(), tInfo.getStartBlockName(), tInfo.getDestinationBlockName());
529                    }
530                    if ( version > 1 ) {
531                        if (traininfo.getAttribute("transitid") != null) {
532                            // there is a transit name selected
533                            tInfo.setTransitId(traininfo.getAttribute("transitid").getValue());
534                        } else {
535                            log.error("Transit id missing when reading TrainInfoFile {}", name);
536                        }
537                        if (traininfo.getAttribute("startblockid") != null) {
538                            // there is a transit name selected
539                            tInfo.setStartBlockId(traininfo.getAttribute("startblockid").getValue());
540                        } else {
541                            log.error("Start block Id missing when reading TrainInfoFile {}", name);
542                        }
543                        if (traininfo.getAttribute("endblockid") != null) {
544                            // there is a transit name selected
545                            tInfo.setDestinationBlockId(traininfo.getAttribute("endblockid").getValue());
546                        } else {
547                            log.error("Destination block Id missing when reading TrainInfoFile {}", name);
548                        }
549                        if (traininfo.getAttribute("startblockseq") != null) {
550                            // there is a transit name selected
551                            try {
552                                tInfo.setStartBlockSeq(traininfo.getAttribute("startblockseq").getIntValue());
553                            }
554                            catch (org.jdom2.DataConversionException ex) {
555                                log.error("Start block sequence invalid when reading TrainInfoFile");
556                            }
557                        } else {
558                            log.error("Start block sequence missing when reading TrainInfoFile {}", name);
559                        }
560                        if (traininfo.getAttribute("endblockseq") != null) {
561                            // there is a transit name selected
562                            try {
563                                tInfo.setDestinationBlockSeq(traininfo.getAttribute("endblockseq").getIntValue());
564                            }
565                            catch (org.jdom2.DataConversionException ex) {
566                                log.error("Destination block sequence invalid when reading TrainInfoFile {}", name);
567                            }
568                        } else {
569                            log.error("Destination block sequence missing when reading TrainInfoFile {}", name);
570                        }
571                    }
572                    if ( version == 1 || version == 2) {
573                        // Change transit and block names from sysName(userName) to displayName
574                        tInfo.setTransitName(convertName(tInfo.getTransitName()));
575                        tInfo.setStartBlockName(convertName(tInfo.getStartBlockName()));
576                        tInfo.setDestinationBlockName(convertName(tInfo.getDestinationBlockName()));
577                    }
578               }
579            }
580        }
581        return tInfo;
582    }
583
584    public String convertName(String name) {
585        // transit: sys(user), block: sys(user)-n
586        String newName = name;
587
588        Pattern p = Pattern.compile(".+\\((.+)\\)(-\\d+)*");
589        Matcher m = p.matcher(name);
590        if (m.matches()) {
591            log.debug("regex: name = '{}', group 1 = '{}', group 2 = '{}'", name, m.group(1), m.group(2));
592            if (m.group(1) != null) {
593                newName = m.group(1).trim();
594                if (m.group(2) != null) {
595                    newName = newName + m.group(2).trim();
596                }
597            }
598        }
599
600        log.debug("convertName: old = '{}', new = '{}'", name, newName);
601        return newName;
602    }
603
604    /*
605     *  Writes out Dispatcher options to a file in the user's preferences directory
606     */
607    public void writeTrainInfo(TrainInfo tf, String name) throws java.io.IOException {
608        log.debug("entered writeTrainInfo");
609        root = new Element("traininfofile");
610        doc = newDocument(root, dtdLocation + "dispatcher-traininfo.dtd");
611        // add XSLT processing instruction
612        // <?xml-stylesheet type="text/xsl" href="XSLT/block-values.xsl"?>
613        java.util.Map<String, String> m = new java.util.HashMap<>();
614        m.put("type", "text/xsl");
615        m.put("href", xsltLocation + "dispatcher-traininfo.xsl");
616        org.jdom2.ProcessingInstruction p = new org.jdom2.ProcessingInstruction("xml-stylesheet", m);
617        doc.addContent(0, p);
618
619        // save Dispatcher TrainInfo in xml format
620        Element traininfo = new Element("traininfo");
621        // write version number
622        traininfo.setAttribute("version", "8");
623        traininfo.setAttribute("transitname", tf.getTransitName());
624        traininfo.setAttribute("transitid", tf.getTransitId());
625        traininfo.setAttribute("dynamictransit", (tf.getDynamicTransit() ? "yes" : "no"));
626        traininfo.setAttribute("trainname", tf.getTrainName());
627        traininfo.setAttribute("trainusername", tf.getTrainUserName());
628        traininfo.setAttribute("rosterid", tf.getRosterId());
629        traininfo.setAttribute("dccaddress", tf.getDccAddress());
630        traininfo.setAttribute("trainintransit", "" + (tf.getTrainInTransit() ? "yes" : "no"));
631        traininfo.setAttribute("startblockname", tf.getStartBlockName());
632        traininfo.setAttribute("startblockid", tf.getStartBlockId());
633        traininfo.setAttribute("startblockseq", Integer.toString(tf.getStartBlockSeq()));
634        traininfo.setAttribute("endblockname", tf.getDestinationBlockName());
635        traininfo.setAttribute("endblockid", tf.getDestinationBlockId());
636        traininfo.setAttribute("endblockseq", Integer.toString(tf.getDestinationBlockSeq()));
637        traininfo.setAttribute("viablockname", tf.getViaBlockName());
638        traininfo.setAttribute("trainfromroster", "" + (tf.getTrainFromRoster() ? "yes" : "no"));
639        traininfo.setAttribute("trainfromtrains", "" + (tf.getTrainFromTrains() ? "yes" : "no"));
640        traininfo.setAttribute("trainfromuser", "" + (tf.getTrainFromUser() ? "yes" : "no"));
641        traininfo.setAttribute("trainfromsetlater", "" + (tf.getTrainFromSetLater() ? "yes" : "no"));
642        traininfo.setAttribute("priority", Integer.toString(tf.getPriority()));
643        traininfo.setAttribute("traindetection", trainsdectionFromEnumMap.outputFromEnum(tf.getTrainDetection()));
644        traininfo.setAttribute("resetwhendone", "" + (tf.getResetWhenDone() ? "yes" : "no"));
645        switch (tf.getDelayedRestart()) {
646            case ActiveTrain.SENSORDELAY:
647                traininfo.setAttribute("delayedrestart", "sensor");
648                traininfo.setAttribute("delayedrestartsensor", tf.getRestartSensorName());
649                traininfo.setAttribute("resetrestartsensor", "" + (tf.getResetRestartSensor() ? "yes" : "no"));
650                break;
651            case ActiveTrain.TIMEDDELAY:
652                traininfo.setAttribute("delayedrestart", "timed");
653                traininfo.setAttribute("delayedrestarttime", Integer.toString(tf.getRestartDelayMin()));
654                break;
655            default:
656                traininfo.setAttribute("delayedrestart", "no");
657                break;
658        }
659
660        traininfo.setAttribute("reverseatend", "" + (tf.getReverseAtEnd() ? "yes" : "no"));
661        switch (tf.getReverseDelayedRestart()) {
662            case ActiveTrain.SENSORDELAY:
663                traininfo.setAttribute("reversedelayedrestart", "sensor");
664                traininfo.setAttribute("reversedelayedrestartsensor", tf.getReverseRestartSensorName());
665                traininfo.setAttribute("reverseresetrestartsensor", "" + (tf.getReverseResetRestartSensor() ? "yes" : "no"));
666                break;
667            case ActiveTrain.TIMEDDELAY:
668                traininfo.setAttribute("reversedelayedrestart", "timed");
669                traininfo.setAttribute("reversedelayedrestarttime", Integer.toString(tf.getReverseRestartDelayMin()));
670                break;
671            default:
672                traininfo.setAttribute("reversedelayedrestart", "no");
673                break;
674        }
675        if (tf.getDelayedStart() == ActiveTrain.TIMEDDELAY) {
676            traininfo.setAttribute("delayedstart", "timed");
677        } else if (tf.getDelayedStart() == ActiveTrain.SENSORDELAY) {
678            traininfo.setAttribute("delayedstart", "sensor");
679            if (tf.getDelaySensorName() != null) {
680                traininfo.setAttribute("delayedSensor", tf.getDelaySensorName());
681                traininfo.setAttribute("resetstartsensor", "" + (tf.getResetStartSensor() ? "yes" : "no"));
682            }
683        }
684
685        traininfo.setAttribute("terminatewhendone", (tf.getTerminateWhenDone() ? "yes" : "no"));
686        traininfo.setAttribute("departuretimehr", Integer.toString(tf.getDepartureTimeHr()));
687        traininfo.setAttribute("departuretimemin", Integer.toString(tf.getDepartureTimeMin()));
688        traininfo.setAttribute("traintype", tf.getTrainType());
689        traininfo.setAttribute("autorun", "" + (tf.getAutoRun() ? "yes" : "no"));
690        traininfo.setAttribute("loadatstartup", "" + (tf.getLoadAtStartup() ? "yes" : "no"));
691        traininfo.setAttribute("allocatealltheway", "" + (tf.getAllocateAllTheWay() ? "yes" : "no"));
692        traininfo.setAttribute("allocationmethod", Integer.toString(tf.getAllocationMethod()));
693        traininfo.setAttribute("nexttrain", tf.getNextTrain());
694        // here save items related to automatically running active trains
695        traininfo.setAttribute("speedfactor", Float.toString(tf.getSpeedFactor()));
696        traininfo.setAttribute("maxspeed", Float.toString(tf.getMaxSpeed()));
697        // Persist maximum speed in scale km/h when the user selected a speed cap.
698        // Omit when 0.0f (means "use throttle cap" for backward compatibility).
699        if (tf.getMaxSpeedScaleKmh() > 0.0f) {
700            traininfo.setAttribute("maxspeedscalekmh", Float.toString(tf.getMaxSpeedScaleKmh()));
701        }
702        traininfo.setAttribute("minreliableoperatingspeed", Float.toString(tf.getMinReliableOperatingSpeed()));
703        traininfo.setAttribute("ramprate", tf.getRampRate());
704        traininfo.setAttribute("runinreverse", "" + (tf.getRunInReverse() ? "yes" : "no"));
705        traininfo.setAttribute("sounddecoder", "" + (tf.getSoundDecoder() ? "yes" : "no"));
706        traininfo.setAttribute("maxtrainlengthscalemeters", Float.toString(tf.getMaxTrainLengthScaleMeters()));
707        traininfo.setAttribute("trainlengthunits", trainlengthFromEnumMap.outputFromEnum(tf.getTrainLengthUnits()));
708        traininfo.setAttribute("usespeedprofile", "" + (tf.getUseSpeedProfile() ? "yes" : "no"));
709        traininfo.setAttribute("stopbyspeedprofile", "" + (tf.getStopBySpeedProfile() ? "yes" : "no"));
710        traininfo.setAttribute("stopbyspeedprofileadjust", Float.toString(tf.getStopBySpeedProfileAdjust()));
711        traininfo.setAttribute("usestopsensor", (tf.getUseStopSensor() ? "yes" : "no"));
712        traininfo.setAttribute("overridestopsensor", (tf.getUseStopSensor() ? "no" : "yes"));
713        traininfo.setAttribute("waittime", Float.toString(tf.getWaitTime()));
714        traininfo.setAttribute("blockname", tf.getBlockName());
715        traininfo.setAttribute("fnumberlight", Integer.toString(tf.getFNumberLight()));
716        traininfo.setAttribute("fnumberbell", Integer.toString(tf.getFNumberBell()));
717        traininfo.setAttribute("fnumberhorn", Integer.toString(tf.getFNumberHorn()));
718        // Physics: persist additional train weight (metric tonnes, float)
719        traininfo.setAttribute("additionaltrainweight_tonnes", Float.toString(tf.getAdditionalTrainWeightMetricTonnes()));
720        // Physics: persist rolling resistance coefficient (dimensionless)
721        traininfo.setAttribute("rollingresistancecoeff", Float.toString(tf.getRollingResistanceCoeff()));
722        traininfo.setAttribute("driverpowerpercent", Float.toString(tf.getDriverPowerPercent()));
723            
724         // Only write these when the user has set a positive distance in mm; otherwise omit.
725         if (tf.getStopByDistanceMm() > 0.0f) {
726             // Persist the explicit distance (mm) into the stop block
727             traininfo.setAttribute("stopbydistance_mm", Float.toString(tf.getStopByDistanceMm()));
728             // Persist whether the distance applies to the HEAD or TAIL of the train
729             traininfo.setAttribute("stopbydistance_ref", tf.getStopByDistanceRef().name()); // HEAD | TAIL
730         }
731
732        root.addContent(traininfo);
733
734        // write out the file
735        try {
736            if (!checkFile(fileLocation + name)) {
737                // file does not exist, create it
738                File file = new File(fileLocation + name);
739                if (!file.createNewFile()) // create file and check result
740                {
741                    log.error("createNewFile failed");
742                }
743            }
744            // write content to file
745            writeXML(findFile(fileLocation + name), doc);
746        } catch (java.io.IOException ioe) {
747            log.error("IO Exception writing", ioe);
748            throw (ioe);
749        }
750    }
751
752    /**
753     * Get the names of all current TrainInfo files. Returns names as an array
754     * of Strings. Returns an empty array if no files are present. Note: Fill
755     * names still end with .xml or .XML. (Modeled after a method in
756     * RecreateRosterAction.java by Bob Jacobsen)
757     *
758     * @return names as an array or an empty array if none present
759     */
760    public String[] getTrainInfoFileNames() {
761        // ensure preferences will be found for read
762        FileUtil.createDirectory(fileLocation);
763        // create an array of file names from roster dir in preferences, count entries
764        List<String> names = new ArrayList<>();
765        log.debug("directory of TrainInfoFiles is {}", fileLocation);
766        File fp = new File(fileLocation);
767        if (fp.exists()) {
768            String[] xmlList = fp.list(new XmlFilenameFilter());
769            if (xmlList!=null) {
770                names.addAll(Arrays.asList(xmlList));
771            }
772        }
773        // Sort the resulting array
774        names.sort((s1, s2) -> {
775            return s1.compareTo(s2);
776        });
777        return names.toArray(new String[names.size()]);
778    }
779
780    /**
781     * Get the names of all current TrainInfo files. Returns list
782     * of files and some basic details for each file
783     *.
784     * @return names as an array or an empty array if none present
785     */
786    public List<TrainInfoFileSummary> getTrainInfoFileSummaries() {
787        List<TrainInfoFileSummary> summaries = new ArrayList<>();
788        for (String fileName : getTrainInfoFileNames()) {
789            try {
790                TrainInfo ti = readTrainInfo(fileName);
791                summaries.add(new TrainInfoFileSummary(fileName, ti.getTrainName(), ti.getTransitName(),
792                        ti.getStartBlockName(), ti.getDestinationBlockName(), ti.getDccAddress()));
793            } catch (org.jdom2.JDOMException ex) {
794                summaries.add(new TrainInfoFileSummary(fileName));
795            } catch (IOException ex) {
796                summaries.add(new TrainInfoFileSummary(fileName));
797            } catch (RuntimeException ex) {
798                log.error("Traininfo File [{}] unexplained error, currupted?",fileName);
799            }
800        }
801        return summaries;
802    }
803
804    /**
805     * Delete a specified TrainInfo file.
806     *
807     * @param name the file to delete
808     */
809    public void deleteTrainInfoFile(String name) {
810        // locate the file and delete it if it exists
811        File f = new File(fileLocation + name);
812        if (!f.delete()) { // delete file and check success
813            log.error("failed to delete TrainInfo file - {}", name);
814        }
815    }
816
817    private final static Logger log = LoggerFactory.getLogger(TrainInfoFile.class);
818}