001package jmri.jmrit.logix;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Enumeration;
007import java.util.HashMap;
008import java.util.Iterator;
009import java.util.LinkedHashMap;
010import java.util.List;
011import java.util.Map.Entry;
012import javax.annotation.CheckReturnValue;
013import javax.annotation.Nonnull;
014import jmri.InstanceManager;
015import jmri.implementation.SignalSpeedMap;
016import jmri.jmrit.XmlFile;
017import jmri.jmrit.logix.WarrantPreferencesPanel.DataPair;
018import jmri.profile.Profile;
019import jmri.profile.ProfileManager;
020import jmri.spi.PreferencesManager;
021import jmri.util.FileUtil;
022import jmri.util.prefs.AbstractPreferencesManager;
023import jmri.util.prefs.InitializationException;
024import org.jdom2.Attribute;
025import org.jdom2.DataConversionException;
026import org.jdom2.Document;
027import org.jdom2.Element;
028import org.jdom2.JDOMException;
029import org.openide.util.lookup.ServiceProvider;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * Hold configuration data for Warrants, includes Speed Map
035 *
036 * @author Pete Cressman Copyright (C) 2015
037 */
038@ServiceProvider(service = PreferencesManager.class)
039public class WarrantPreferences extends AbstractPreferencesManager {
040
041    public static final String LAYOUT_PARAMS = "layoutParams"; // NOI18N
042    public static final String LAYOUT_SCALE = "layoutScale"; // NOI18N
043    public static final String SEARCH_DEPTH = "searchDepth"; // NOI18N
044    public static final String SPEED_MAP_PARAMS = "speedMapParams"; // NOI18N
045    public static final String RAMP_PREFS = "rampPrefs";         // NOI18N
046    public static final String TIME_INCREMENT = "timeIncrement"; // NOI18N
047    public static final String THROTTLE_SCALE = "throttleScale"; // NOI18N
048    public static final String RAMP_INCREMENT = "rampIncrement"; // NOI18N
049    public static final String STEP_INCREMENTS = "stepIncrements"; // NOI18N
050    public static final String SPEED_NAME_PREFS = "speedNames";   // NOI18N
051    public static final String SPEED_NAMES = SPEED_NAME_PREFS;
052    public static final String INTERPRETATION = "interpretation"; // NOI18N
053    public static final String APPEARANCE_PREFS = "appearancePrefs"; // NOI18N
054    public static final String APPEARANCES = "appearances"; // NOI18N
055    public static final String SHUT_DOWN = "shutdown"; // NOI18N
056    public static final String NO_MERGE = "NO_MERGE";
057    public static final String PROMPT   = "PROMPT";
058    public static final String MERGE_ALL = "MERGE_ALL";
059    public static final String TRACE = "Trace";
060    public static final String SPEED_ASSISTANCE = "SpeedAssistance";
061
062    private String _fileName;
063    private float _scale = 87.1f;
064    private int _searchDepth = 20;      // How many tree nodes (blocks) to walk in finding routes
065    private float _throttleScale = 0.90f;  // factor to approximate throttle setting to track speed
066
067    private final LinkedHashMap<String, Float> _speedNames = new LinkedHashMap<>();
068    private final LinkedHashMap<String, String> _headAppearances = new LinkedHashMap<>();
069    private int _interpretation = SignalSpeedMap.PERCENT_NORMAL;    // Interpretation of values in speed name table
070
071    private int _msIncrTime = 1000;          // time in milliseconds between speed changes ramping up or down
072    private float _throttleIncr = 0.0238f;  // throttle increment for each ramp speed change - 3 steps
073
074    public enum Shutdown {NO_MERGE, PROMPT, MERGE_ALL}
075    private Shutdown _shutdown = Shutdown.PROMPT;     // choice for handling session RosterSpeedProfiles
076
077    private boolean _trace = false;         // trace warrant activity to log.info on the console
078    private float _slowSpeedAssistance = 0.02f;
079    /**
080     * Get the default instance.
081     *
082     * @return the default instance, creating it if necessary
083     */
084    public static WarrantPreferences getDefault() {
085        return InstanceManager.getOptionalDefault(WarrantPreferences.class).orElseGet(() -> {
086            WarrantPreferences preferences = InstanceManager.setDefault(WarrantPreferences.class, new WarrantPreferences());
087            try {
088                preferences.initialize(ProfileManager.getDefault().getActiveProfile());
089            } catch (InitializationException ex) {
090                log.error("Error initializing default WarrantPreferences", ex);
091            }
092            return preferences;
093        });
094    }
095
096    public void openFile(String name) {
097        _fileName = name;
098        WarrantPreferencesXml prefsXml = new WarrantPreferencesXml();
099        File file = new File(_fileName);
100        Element root;
101        try {
102            root = prefsXml.rootFromFile(file);
103        } catch (java.io.FileNotFoundException ea) {
104            log.debug("Could not find Warrant preferences file.  Normal if preferences have not been saved before.");
105            root = null;
106        } catch (IOException | JDOMException eb) {
107            log.error("Exception while loading warrant preferences", eb);
108            root = null;
109        }
110        if (root != null) {
111//            log.info("Found Warrant preferences file: {}", _fileName);
112            loadLayoutParams(root.getChild(LAYOUT_PARAMS));
113            if (!loadSpeedMap(root.getChild(SPEED_MAP_PARAMS))) {
114                loadSpeedMapFromOldXml();
115                log.error("Unable to read ramp parameters. Setting to default values.");
116            }
117        } else {
118            loadSpeedMapFromOldXml();
119        }
120    }
121
122    public void loadLayoutParams(Element layoutParm) {
123        if (layoutParm == null) {
124            return;
125        }
126        Attribute a;
127        if ((a = layoutParm.getAttribute(LAYOUT_SCALE)) != null) {
128            try {
129                setLayoutScale(a.getFloatValue());
130            } catch (DataConversionException ex) {
131                setLayoutScale(87.1f);
132                log.error("Unable to read layout scale. Setting to default value.", ex);
133            }
134        }
135        if ((a = layoutParm.getAttribute(SEARCH_DEPTH)) != null) {
136            try {
137                _searchDepth = a.getIntValue();
138            } catch (DataConversionException ex) {
139                _searchDepth = 20;
140                log.error("Unable to read route search depth. Setting to default value (20).", ex);
141            }
142        }
143        Element shutdown = layoutParm.getChild(SHUT_DOWN);
144        if (shutdown != null) {
145            String choice = shutdown.getText();
146            if (MERGE_ALL.equals(choice)) {
147                _shutdown = Shutdown.MERGE_ALL;
148            } else if (NO_MERGE.equals(choice)) {
149                _shutdown = Shutdown.NO_MERGE;
150            } else {
151                _shutdown = Shutdown.PROMPT;
152            }
153        }
154        Element trace = layoutParm.getChild(TRACE);
155        if (trace != null) {
156            _trace = "true".equals(trace.getText());
157        }
158        Element speedAssistance = layoutParm.getChild(SPEED_ASSISTANCE);
159        if (speedAssistance != null) {
160            _slowSpeedAssistance = Float.parseFloat(speedAssistance.getText());
161        }
162    }
163
164    // Avoid firePropertyChange until SignalSpeedMap is completely loaded
165    private void loadSpeedMapFromOldXml() {
166        SignalSpeedMap map = jmri.InstanceManager.getNullableDefault(SignalSpeedMap.class);
167        if (map == null) {
168            log.error("Cannot find signalSpeeds.xml file.");
169            return;
170        }
171        Iterator<String> it = map.getValidSpeedNames().iterator();
172        LinkedHashMap<String, Float> names = new LinkedHashMap<>();
173        while (it.hasNext()) {
174            String name = it.next();
175            names.put(name, map.getSpeed(name));
176        }
177        this.setSpeedNames(names);  // OK, no firePropertyChange
178
179        Enumeration<String> en = map.getAppearanceIterator();
180        LinkedHashMap<String, String> heads = new LinkedHashMap<>();
181        while (en.hasMoreElements()) {
182            String name = en.nextElement();
183            heads.put(name, map.getAppearanceSpeed(name));
184        }
185        this.setAppearances(heads);  // no firePropertyChange
186        this._msIncrTime = map.getStepDelay();
187        this._throttleIncr = map.getStepIncrement();
188    }
189
190    // Avoid firePropertyChange until SignalSpeedMap is completely loaded
191    private boolean loadSpeedMap(Element child) {
192        if (child == null) {
193            return false;
194        }
195        Element rampParms = child.getChild(STEP_INCREMENTS);
196        if (rampParms == null) {
197            return false;
198        }
199        Attribute a;
200        if ((a = rampParms.getAttribute(TIME_INCREMENT)) != null) {
201            try {
202                this._msIncrTime = a.getIntValue();
203            } catch (DataConversionException ex) {
204                this._msIncrTime = 500;
205                log.error("Unable to read ramp time increment. Setting to default value (500ms).", ex);
206            }
207        }
208        if ((a = rampParms.getAttribute(RAMP_INCREMENT)) != null) {
209            try {
210                this._throttleIncr = a.getFloatValue();
211            } catch (DataConversionException ex) {
212                this._throttleIncr = 0.03f;
213                log.error("Unable to read ramp throttle increment. Setting to default value (0.03).", ex);
214            }
215        }
216        if ((a = rampParms.getAttribute(THROTTLE_SCALE)) != null) {
217            try {
218                _throttleScale = a.getFloatValue();
219            } catch (DataConversionException ex) {
220                _throttleScale = .90f;
221                log.error("Unable to read throttle scale. Setting to default value (0.90f).", ex);
222            }
223        }
224
225        rampParms = child.getChild(SPEED_NAME_PREFS);
226        if (rampParms == null) {
227            return false;
228        }
229        if ((a = rampParms.getAttribute("percentNormal")) != null) {
230            if (a.getValue().equals("yes")) {
231                _interpretation = 1;
232            } else {
233                _interpretation = 2;
234            }
235        }
236        if ((a = rampParms.getAttribute(INTERPRETATION)) != null) {
237            try {
238                _interpretation = a.getIntValue();
239            } catch (DataConversionException ex) {
240                _interpretation = 1;
241                log.error("Unable to read interpetation of Speed Map. Setting to default value % normal.", ex);
242            }
243        }
244        HashMap<String, Float> map = new LinkedHashMap<>();
245        List<Element> list = rampParms.getChildren();
246        for (int i = 0; i < list.size(); i++) {
247            String name = list.get(i).getName();
248            Float speed = 0f;
249            try {
250                speed = Float.valueOf(list.get(i).getText());
251            } catch (NumberFormatException nfe) {
252                log.error("Speed names has invalid content for {} = {}", name, list.get(i).getText());
253            }
254            log.debug("Add {}, {} to AspectSpeed Table", name, speed);
255            map.put(name, speed);
256        }
257        this.setSpeedNames(map);    // no firePropertyChange
258
259        rampParms = child.getChild(APPEARANCE_PREFS);
260        if (rampParms == null) {
261            return false;
262        }
263        LinkedHashMap<String, String> heads = new LinkedHashMap<>();
264        list = rampParms.getChildren();
265        for (int i = 0; i < list.size(); i++) {
266            String name = Bundle.getMessage(list.get(i).getName());
267            String speed = list.get(i).getText();
268            heads.put(name, speed);
269        }
270        this.setAppearances(heads); // no firePropertyChange
271
272        // Now set SignalSpeedMap members.
273        SignalSpeedMap speedMap = jmri.InstanceManager.getDefault(SignalSpeedMap.class);
274        speedMap.setRampParams(_throttleIncr, _msIncrTime);
275        speedMap.setDefaultThrottleFactor(_throttleScale);
276        speedMap.setLayoutScale(_scale);
277        speedMap.setAspects(new HashMap<>(this._speedNames), _interpretation);
278        speedMap.setAppearances(new HashMap<>(this._headAppearances));
279        return true;
280    }
281
282    public void save() {
283        if (_fileName == null) {
284            log.error("_fileName null. Could not create warrant preferences file.");
285            return;
286        }
287
288        XmlFile xmlFile = new XmlFile() {
289        };
290        xmlFile.makeBackupFile(_fileName);
291        File file = new File(_fileName);
292        try {
293            File parentDir = file.getParentFile();
294            if (!parentDir.exists()) {
295                if (!parentDir.mkdir()) {
296                    log.warn("Could not create parent directory for prefs file :{}", _fileName);
297                    return;
298                }
299            }
300            if (file.createNewFile()) {
301                log.debug("Creating new warrant prefs file: {}", _fileName);
302            }
303        } catch (IOException ea) {
304            log.error("Could not create warrant preferences file at {}.", _fileName, ea);
305        }
306
307        try {
308            Element root = new Element("warrantPreferences");
309            Document doc = XmlFile.newDocument(root);
310            if (store(root)) {
311                xmlFile.writeXML(file, doc);
312            }
313        } catch (IOException eb) {
314            log.warn("Exception in storing warrant xml", eb);
315        }
316    }
317
318    public boolean store(Element root) {
319        Element prefs = new Element(LAYOUT_PARAMS);
320        try {
321            prefs.setAttribute(LAYOUT_SCALE, Float.toString(getLayoutScale()));
322            prefs.setAttribute(SEARCH_DEPTH, Integer.toString(getSearchDepth()));
323            Element shutdownPref = new Element(SHUT_DOWN);
324            shutdownPref.setText(_shutdown.toString());
325            prefs.addContent(shutdownPref);
326
327            Element tracePref = new Element(TRACE);
328            tracePref.setText(_trace ? "true" : "false");
329            prefs.addContent(tracePref);
330
331            Element speedAssistancePref = new Element(SPEED_ASSISTANCE);
332            speedAssistancePref.setText(String.valueOf(_slowSpeedAssistance));
333            prefs.addContent(speedAssistancePref);
334            root.addContent(prefs);
335
336            prefs = new Element(SPEED_MAP_PARAMS);
337            Element rampPrefs = new Element(STEP_INCREMENTS);
338            rampPrefs.setAttribute(TIME_INCREMENT, Integer.toString(getTimeIncrement()));
339            rampPrefs.setAttribute(RAMP_INCREMENT, Float.toString(getThrottleIncrement()));
340            rampPrefs.setAttribute(THROTTLE_SCALE, Float.toString(getThrottleScale()));
341            prefs.addContent(rampPrefs);
342
343            rampPrefs = new Element(SPEED_NAME_PREFS);
344            rampPrefs.setAttribute(INTERPRETATION, Integer.toString(getInterpretation()));
345
346            Iterator<Entry<String, Float>> it = getSpeedNameEntryIterator();
347            while (it.hasNext()) {
348                Entry<String, Float> ent = it.next();
349                Element step = new Element(ent.getKey());
350                step.setText(ent.getValue().toString());
351                rampPrefs.addContent(step);
352            }
353            prefs.addContent(rampPrefs);
354
355            rampPrefs = new Element(APPEARANCE_PREFS);
356            Element step = new Element("SignalHeadStateRed");
357            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateRed")));
358            rampPrefs.addContent(step);
359            step = new Element("SignalHeadStateFlashingRed");
360            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateFlashingRed")));
361            rampPrefs.addContent(step);
362            step = new Element("SignalHeadStateGreen");
363            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateGreen")));
364            rampPrefs.addContent(step);
365            step = new Element("SignalHeadStateFlashingGreen");
366            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateFlashingGreen")));
367            rampPrefs.addContent(step);
368            step = new Element("SignalHeadStateYellow");
369            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateYellow")));
370            rampPrefs.addContent(step);
371            step = new Element("SignalHeadStateFlashingYellow");
372            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateFlashingYellow")));
373            rampPrefs.addContent(step);
374            step = new Element("SignalHeadStateLunar");
375            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateLunar")));
376            rampPrefs.addContent(step);
377            step = new Element("SignalHeadStateFlashingLunar");
378            step.setText(_headAppearances.get(Bundle.getMessage("SignalHeadStateFlashingLunar")));
379            rampPrefs.addContent(step);
380            prefs.addContent(rampPrefs);
381        } catch (RuntimeException ex) {
382            log.warn("Exception in storing warrant xml.", ex);
383            return false;
384        }
385        root.addContent(prefs);
386        return true;
387    }
388
389    /**
390     * Get the layout scale.
391     *
392     * @return the scale
393     */
394    public final float getLayoutScale() {
395        return _scale;
396    }
397
398    /**
399     * Set the layout scale.
400     *
401     * @param scale the scale
402     */
403    public void setLayoutScale(float scale) {
404        float oldScale = this._scale;
405        _scale = scale;
406        this.firePropertyChange(LAYOUT_SCALE, oldScale, scale);
407    }
408
409    public float getThrottleScale() {
410        return _throttleScale;
411    }
412
413    public void setThrottleScale(float scale) {
414        float oldScale = this._throttleScale;
415        _throttleScale = scale;
416        this.firePropertyChange(THROTTLE_SCALE, oldScale, scale);
417    }
418
419    int getSearchDepth() {
420        return _searchDepth;
421    }
422
423    void setSearchDepth(int depth) {
424        int oldDepth = this._searchDepth;
425        _searchDepth = depth;
426        this.firePropertyChange(SEARCH_DEPTH, oldDepth, depth);
427    }
428
429    boolean getTrace() {
430        return _trace;
431    }
432
433    void setTrace(boolean t) {
434        _trace = t;
435    }
436
437    float getSpeedAssistance() {
438        return _slowSpeedAssistance;
439    }
440
441    void setSpeedAssistance(float f) {
442        _slowSpeedAssistance = f;
443    }
444
445    Iterator<Entry<String, Float>> getSpeedNameEntryIterator() {
446        List<Entry<String, Float>> vec = new java.util.ArrayList<>();
447        _speedNames.entrySet().forEach((entry) -> {
448            vec.add(new DataPair<>(entry.getKey(), entry.getValue()));
449        });
450        return vec.iterator();
451    }
452
453    Float getSpeedNameValue(String key) {
454        return _speedNames.get(key);
455    }
456
457    @Nonnull
458    @CheckReturnValue
459    public HashMap<String, Float> getSpeedNames() {
460        return new HashMap<>(this._speedNames);
461    }
462
463    // Only called directly at load time
464    private void setSpeedNames(@Nonnull HashMap<String, Float> map) {
465        _speedNames.clear();
466        _speedNames.putAll(map);
467    }
468
469    // Called when preferences is updated from panel
470    protected void setSpeedNames(ArrayList<DataPair<String, Float>> speedNameMap) {
471        LinkedHashMap<String, Float> map = new LinkedHashMap<>();
472        for (int i = 0; i < speedNameMap.size(); i++) {
473            DataPair<String, Float> dp = speedNameMap.get(i);
474            map.put(dp.getKey(), dp.getValue());
475        }
476        LinkedHashMap<String, Float> old = new LinkedHashMap<>(_speedNames);
477        this.setSpeedNames(map);
478        this.firePropertyChange(SPEED_NAMES, old, new LinkedHashMap<>(_speedNames));
479    }
480
481    Iterator<Entry<String, String>> getAppearanceEntryIterator() {
482        List<Entry<String, String>> vec = new ArrayList<>();
483        _headAppearances.entrySet().stream().forEach((entry) -> {
484            vec.add(new DataPair<>(entry.getKey(), entry.getValue()));
485        });
486        return vec.iterator();
487    }
488
489    String getAppearanceValue(String key) {
490        return _headAppearances.get(key);
491    }
492
493    /**
494     * Get a map of signal head appearances.
495     *
496     * @return a map of appearances or an empty map if none are defined
497     */
498    @Nonnull
499    @CheckReturnValue
500    public HashMap<String, String> getAppearances() {
501        return new HashMap<>(this._headAppearances);
502    }
503
504    // Only called directly at load time
505    private void setAppearances(HashMap<String, String> map) {
506        this._headAppearances.clear();
507        this._headAppearances.putAll(map);
508     }
509
510    // Called when preferences are updated
511    protected void setAppearances(ArrayList<DataPair<String, String>> appearanceMap) {
512        LinkedHashMap<String, String> map = new LinkedHashMap<>();
513        for (int i = 0; i < appearanceMap.size(); i++) {
514            DataPair<String, String> dp = appearanceMap.get(i);
515            map.put(dp.getKey(), dp.getValue());
516        }
517        LinkedHashMap<String, String> old = new LinkedHashMap<>(this._headAppearances);
518        this.setAppearances(map);
519        this.firePropertyChange(APPEARANCES, old, new LinkedHashMap<>(this._headAppearances));
520    }
521
522    public int getInterpretation() {
523        return _interpretation;
524    }
525
526    void setInterpretation(int interp) {
527        int oldInterpretation = this._interpretation;
528        _interpretation = interp;
529        this.firePropertyChange(INTERPRETATION, oldInterpretation, interp);
530    }
531
532    /**
533     * Get the time increment.
534     *
535     * @return the time increment in milliseconds
536     */
537    public final int getTimeIncrement() {
538        return _msIncrTime;
539    }
540
541    /**
542     * Set the time increment.
543     *
544     * @param increment the time increment in milliseconds
545     */
546    public void setTimeIncrement(int increment) {
547        int oldIncrement = this._msIncrTime;
548        this._msIncrTime = increment;
549        this.firePropertyChange(TIME_INCREMENT, oldIncrement, increment);
550    }
551
552    /**
553     * Get the throttle increment.
554     *
555     * @return the throttle increment
556     */
557    public final float getThrottleIncrement() {
558        return _throttleIncr;
559    }
560
561    /**
562     * Set the throttle increment.
563     *
564     * @param increment the throttle increment
565     */
566    public void setThrottleIncrement(float increment) {
567        float oldIncrement = this._throttleIncr;
568        this._throttleIncr = increment;
569        this.firePropertyChange(RAMP_INCREMENT, oldIncrement, increment);
570
571    }
572
573    @Override
574    public void initialize(Profile profile) throws InitializationException {
575        if (!this.isInitialized(profile) && !this.isInitializing(profile)) {
576            this.setInitializing(profile, true);
577            this.openFile(FileUtil.getUserFilesPath() + "signal" + File.separator + "WarrantPreferences.xml");
578            this.setInitialized(profile, true);
579        }
580    }
581
582    public void setShutdown(Shutdown set) {
583        _shutdown = set;
584    }
585    public Shutdown getShutdown() {
586        return _shutdown;
587    }
588
589    @Override
590    public void savePreferences(Profile profile) {
591        this.save();
592    }
593
594    public static class WarrantPreferencesXml extends XmlFile {
595    }
596
597    private final static Logger log = LoggerFactory.getLogger(WarrantPreferences.class);
598}