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) { 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}