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}