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