001package jmri.implementation;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.net.URL;
006import java.util.Collections;
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;
013import java.util.Vector;
014import javax.annotation.Nonnull;
015import jmri.InstanceManager;
016import jmri.InstanceManagerAutoDefault;
017import jmri.InstanceManagerAutoInitialize;
018import jmri.beans.Bean;
019import jmri.jmrit.logix.WarrantPreferences;
020import jmri.util.FileUtil;
021import org.jdom2.Element;
022import org.jdom2.JDOMException;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026/**
027 * Default implementation to map Signal aspects or appearances to speed
028 * requirements.
029 * <p>
030 * The singleton instance is referenced from the InstanceManager by SignalHeads
031 * and SignalMasts
032 *
033 * @author Pete Cressman Copyright (C) 2010
034 */
035public class SignalSpeedMap extends Bean implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize // auto-initialize in InstanceManager
036{
037
038    private final HashMap<String, Float> _table = new LinkedHashMap<>();
039    private final HashMap<String, String> _headTable = new LinkedHashMap<>();
040    private int _interpretation;
041    private int _sStepDelay;     // ramp step time interval
042    private int _numSteps = 4;   // num throttle steps per ramp step - deprecated
043    private float _stepIncrement = 0.04f;       // ramp step throttle increment
044    private float _throttleFactor = 0.75f;
045    private float _scale = 87.1f;
046
047    static public final int PERCENT_NORMAL = 1;
048    static public final int PERCENT_THROTTLE = 2;
049    static public final int SPEED_MPH = 3;
050    static public final int SPEED_KMPH = 4;
051    private PropertyChangeListener warrantPreferencesListener = null;
052
053    public SignalSpeedMap() {
054        loadMap();
055        this.warrantPreferencesListener = (PropertyChangeEvent evt) -> {
056            WarrantPreferences preferences = WarrantPreferences.getDefault();
057            SignalSpeedMap map = SignalSpeedMap.this;
058            switch (evt.getPropertyName()) {
059                case WarrantPreferences.APPEARANCES:
060                    map.setAppearances(preferences.getAppearances());
061                    break;
062                case WarrantPreferences.LAYOUT_SCALE:
063                    map.setLayoutScale(preferences.getLayoutScale());
064                    break;
065                case WarrantPreferences.SPEED_NAMES:
066                case WarrantPreferences.INTERPRETATION:
067                    map.setAspects(preferences.getSpeedNames(), preferences.getInterpretation());
068                    break;
069                case WarrantPreferences.THROTTLE_SCALE:
070                    map.setDefaultThrottleFactor(preferences.getThrottleScale());
071                    break;
072                case WarrantPreferences.TIME_INCREMENT:
073                case WarrantPreferences.RAMP_INCREMENT:
074                    map.setRampParams(preferences.getThrottleIncrement(), preferences.getTimeIncrement());
075                    break;
076                default:
077                // ignore other properties
078            }
079        };
080    }
081
082    @Override
083    public void initialize() {
084        InstanceManager.getOptionalDefault(WarrantPreferences.class).ifPresent((wp) -> {
085            wp.addPropertyChangeListener(this.warrantPreferencesListener);
086        });
087        InstanceManager.addPropertyChangeListener((PropertyChangeEvent evt) -> {
088            if (evt.getPropertyName().equals(InstanceManager.getDefaultsPropertyName(WarrantPreferences.class))) {
089                InstanceManager.getDefault(WarrantPreferences.class).addPropertyChangeListener(this.warrantPreferencesListener);
090            }
091        });
092    }
093
094    void loadMap() {
095        URL path = FileUtil.findURL("signalSpeeds.xml", new String[]{"", "xml/signals"});
096        jmri.jmrit.XmlFile xf = new jmri.jmrit.XmlFile() {
097        };
098        try {
099            loadRoot(xf.rootFromURL(path));
100        } catch (java.io.FileNotFoundException e) {
101            log.warn("signalSpeeds file ({}) doesn't exist in XmlFile search path.", path);
102            throw new IllegalArgumentException("signalSpeeds file (" + path + ") doesn't exist in XmlFile search path.");
103        } catch (org.jdom2.JDOMException | java.io.IOException e) {
104            log.error("error reading file \"{}\" due to: {}", path, e);
105        }
106    }
107
108    public void loadRoot(@Nonnull Element root) {
109        try {
110            Element e = root.getChild("interpretation");
111            String sval = e.getText().toUpperCase();
112            switch (sval) {
113                case "PERCENTNORMAL":
114                    _interpretation = PERCENT_NORMAL;
115                    break;
116                case "PERCENTTHROTTLE":
117                    _interpretation = PERCENT_THROTTLE;
118                    break;
119                default:
120                    throw new JDOMException("invalid content for interpretation: " + sval);
121            }
122            log.debug("_interpretation= {}", _interpretation);
123
124            e = root.getChild("msPerIncrement");
125            _sStepDelay = 1000;
126            try {
127                _sStepDelay = Integer.parseInt(e.getText());
128            } catch (NumberFormatException nfe) {
129                throw new JDOMException("invalid content for msPerIncrement: " + e.getText());
130            }
131            if (_sStepDelay < 200) {
132                _sStepDelay = 200;
133                log.warn("\"msPerIncrement\" must be at least 200 milliseconds.");
134            }
135            log.debug("_sStepDelay = {}", _sStepDelay);
136
137            e = root.getChild("stepsPerIncrement");
138            try {
139                _numSteps = Integer.parseInt(e.getText());
140            } catch (NumberFormatException nfe) {
141                throw new JDOMException("invalid content for stepsPerIncrement: " + e.getText());
142            }
143            if (_numSteps < 1) {
144                _numSteps = 1;
145            }
146
147            List<Element> list = root.getChild("aspectSpeeds").getChildren();
148            _table.clear();
149            for (int i = 0; i < list.size(); i++) {
150                String name = list.get(i).getName();
151                Float speed;
152                try {
153                    speed = Float.valueOf(list.get(i).getText());
154                } catch (NumberFormatException nfe) {
155                    log.error("invalid content for {} = {}", name, list.get(i).getText());
156                    throw new JDOMException("invalid content for " + name + " = " + list.get(i).getText());
157                }
158                log.debug("Add {}, {} to AspectSpeed Table", name, speed);
159                _table.put(name, speed);
160            }
161
162            synchronized (this._headTable) {
163                _headTable.clear();
164                List<Element> l = root.getChild("appearanceSpeeds").getChildren();
165                for (int i = 0; i < l.size(); i++) {
166                    String name = l.get(i).getName();
167                    String speed = l.get(i).getText();
168                    _headTable.put(Bundle.getMessage(name), speed);
169                    log.debug("Add {}={}, {} to AppearanceSpeed Table", name, Bundle.getMessage(name), speed);
170                }
171            }
172        } catch (org.jdom2.JDOMException e) {
173            log.error("error reading speed map elements due to: {}", e);
174        }
175    }
176
177    public boolean checkSpeed(String name) {
178        if (name == null) {
179            return false;
180        }
181        return _table.get(name) != null;
182    }
183
184    /**
185     * @param aspect appearance (not called head in US) to check
186     * @param system system name of head
187     * @return speed from SignalMast Aspect name
188     */
189    public String getAspectSpeed(@Nonnull String aspect, @Nonnull jmri.SignalSystem system) {
190        String property = (String) system.getProperty(aspect, "speed");
191        log.debug("getAspectSpeed: aspect={}, speed={}", aspect, property);
192        return property;
193    }
194
195    /**
196     * @param aspect appearance (not called head in US) to check
197     * @param system system name of head
198     * @return speed2 from SignalMast Aspect name
199     */
200    public String getAspectExitSpeed(@Nonnull String aspect, @Nonnull jmri.SignalSystem system) {
201        String property = (String) system.getProperty(aspect, "speed2");
202        log.debug("getAspectSpeed: aspect={}, speed2={}", aspect, property);
203        return property;
204    }
205
206    /**
207     * Get speed for a given signal head appearance.
208     *
209     * @param name appearance default name
210     * @return speed from SignalHead Appearance name
211     */
212    public String getAppearanceSpeed(@Nonnull String name) throws NumberFormatException {
213        String speed = _headTable.get(name);
214        log.debug("getAppearanceSpeed Appearance={}, speed={}", name, speed);
215        return speed;
216    }
217
218    public Enumeration<String> getAppearanceIterator() {
219        return Collections.enumeration(_headTable.keySet());
220    }
221
222    public Enumeration<String> getSpeedIterator() {
223        return Collections.enumeration(_table.keySet());
224    }
225
226    public Vector<String> getValidSpeedNames() {
227        return new Vector<>(this._table.keySet());
228    }
229
230    public float getSpeed(@Nonnull String name) throws IllegalArgumentException {
231        if (!checkSpeed(name)) {
232            // not a valid aspect
233            log.warn("attempting to get speed for invalid name: '{}'", name);
234            //java.util.Enumeration<String> e = _table.keys();
235            throw new IllegalArgumentException("attempting to get speed from invalid name: \"" + name + "\"");
236        }
237        Float speed = _table.get(name);
238        if (speed == null) {
239            return 0.0f;
240        }
241        return speed;
242    }
243
244    public String getNamedSpeed(float speed) {
245        Enumeration<String> e = this.getSpeedIterator();
246        while (e.hasMoreElements()) {
247            String key = e.nextElement();
248            if (_table.get(key).equals(speed)) {
249                return key;
250            }
251        }
252        return null;
253    }
254
255    public int getInterpretation() {
256        return _interpretation;
257    }
258
259    public int getStepDelay() {
260        return _sStepDelay;
261    }
262
263    public float getStepIncrement() {
264        return _stepIncrement;
265    }
266
267    public void setAspects(@Nonnull HashMap<String, Float> map, int interpretation) {
268        HashMap<String, Float> oldMap = new HashMap<>(this._table);
269        int oldInterpretation = this._interpretation;
270        this._table.clear();
271        this._table.putAll(map);
272        this._interpretation = interpretation;
273        if (interpretation != oldInterpretation) {
274            this.firePropertyChange("interpretation", oldInterpretation, interpretation);
275        }
276        if (!map.equals(oldMap)) {
277            this.firePropertyChange("aspects", oldMap, new HashMap<>(map));
278        }
279    }
280
281    public void setAspectTable(@Nonnull Iterator<Entry<String, Float>> iter, int interpretation) {
282        _table.clear();
283        while (iter.hasNext()) {
284            Entry<String, Float> ent = iter.next();
285            _table.put(ent.getKey(), ent.getValue());
286        }
287        _interpretation = interpretation;
288    }
289
290    public void setAppearances(@Nonnull HashMap<String, String> map) {
291        synchronized (this._headTable) {
292            HashMap<String, String> old = new HashMap<>(_headTable);
293            _headTable.clear();
294            _headTable.putAll(map);
295            if (!map.equals(old)) {
296                this.firePropertyChange("Appearances", old, new HashMap<>(_headTable));
297            }
298        }
299    }
300
301    public void setAppearanceTable(@Nonnull Iterator<Entry<String, String>> iter) {
302        synchronized (this._headTable) {
303            _headTable.clear();
304            while (iter.hasNext()) {
305                Entry<String, String> ent = iter.next();
306                _headTable.put(ent.getKey(), ent.getValue());
307            }
308        }
309    }
310
311    public void setRampParams(float throttleIncr, int msIncrTime) {
312        _sStepDelay = msIncrTime;
313        _stepIncrement = throttleIncr;
314    }
315
316    public void setDefaultThrottleFactor(float f) {
317        _throttleFactor = f;
318    }
319
320    public float getDefaultThrottleFactor() {
321        return _throttleFactor;
322    }
323
324    public void setLayoutScale(float s) {
325        _scale = s;
326    }
327
328    public float getLayoutScale() {
329        return _scale;
330    }
331
332    static private final Logger log = LoggerFactory.getLogger(SignalSpeedMap.class);
333}