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