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}