001package jmri.jmrit.vsdecoder; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.util.ArrayList; 006import java.util.Collection; 007import java.util.HashMap; 008import java.util.LinkedHashMap; 009import java.util.Iterator; 010import java.awt.geom.Point2D; 011import jmri.Audio; 012import jmri.LocoAddress; 013import jmri.Throttle; 014import jmri.jmrit.display.layoutEditor.*; 015import jmri.jmrit.operations.locations.Location; 016import jmri.jmrit.operations.routes.RouteLocation; 017import jmri.jmrit.operations.routes.Route; 018import jmri.jmrit.operations.trains.Train; 019import jmri.jmrit.operations.trains.TrainManager; 020import jmri.jmrit.roster.RosterEntry; 021import jmri.jmrit.vsdecoder.swing.VSDControl; 022import jmri.jmrit.vsdecoder.swing.VSDManagerFrame; 023import jmri.util.PhysicalLocation; 024 025import org.jdom2.Element; 026 027/** 028 * Implements a software "decoder" that responds to throttle inputs and 029 * generates sounds in responds to them. 030 * <p> 031 * Each VSDecoder implements exactly one Sound Profile (describes a particular 032 * type of locomotive, say, an EMD GP7). 033 * <hr> 034 * This file is part of JMRI. 035 * <p> 036 * JMRI is free software; you can redistribute it and/or modify it under the 037 * terms of version 2 of the GNU General Public License as published by the Free 038 * Software Foundation. See the "COPYING" file for a copy of this license. 039 * <p> 040 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 041 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 042 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 043 * 044 * @author Mark Underwood Copyright (C) 2011 045 * @author Klaus Killinger Copyright (C) 2018-2023, 2025 046 */ 047public class VSDecoder implements PropertyChangeListener { 048 049 boolean initialized = false; // This decoder has been initialized 050 boolean enabled = false; // This decoder is enabled 051 private boolean create_xy_series = false; // Create xy coordinates in console 052 053 private VSDConfig config; 054 055 // For use in VSDecoderManager 056 int dirfn = 1; 057 PhysicalLocation posToSet; 058 PhysicalLocation lastPos; 059 PhysicalLocation startPos; 060 int topspeed; 061 int topspeed_rev; 062 float lastspeed; 063 float avgspeed; 064 int setup_index; // Can be set by a Route 065 boolean is_muted; 066 VSDSound savedSound; 067 068 double distanceOnTrack; 069 float distanceMeter; 070 double distance; // how far to travel this frame 071 private double returnDistance; // used by a direction change 072 private Point2D location; 073 private LayoutTrack lastTrack; // the layout track we were on previously 074 private LayoutTrack layoutTrack; // which layout track we're on 075 private LayoutTrack returnTrack; 076 private LayoutTrack returnLastTrack; 077 LayoutTrack nextLayoutTrack; 078 private double directionRAD; // directionRAD we're headed (in radians) 079 private LayoutEditor models; 080 private VSDNavigation navigation; 081 082 HashMap<String, VSDSound> sound_list; // list of sounds 083 LinkedHashMap<String, SoundEvent> event_list; // list of events 084 085 /** 086 * Construct a VSDecoder with the given system name (id) and configuration 087 * (config) 088 * 089 * @param cfg (VSDConfig) Configuration 090 */ 091 public VSDecoder(VSDConfig cfg) { 092 config = cfg; 093 094 sound_list = new HashMap<>(); 095 event_list = new LinkedHashMap<>(); 096 097 // Force re-initialization 098 initialized = _init(); 099 100 try { 101 VSDFile vsdfile = new VSDFile(config.getVSDPath()); 102 if (vsdfile.isInitialized()) { 103 log.debug("Constructor: vsdfile init OK, loading XML..."); 104 this.setXml(vsdfile, config.getProfileName()); 105 } else { 106 log.debug("Constructor: vsdfile init FAILED."); 107 initialized = false; 108 } 109 } catch (java.util.zip.ZipException e) { 110 log.error("ZipException loading VSDecoder from {}", config.getVSDPath()); 111 // would be nice to pop up a dialog here... 112 } catch (java.io.IOException ioe) { 113 log.error("IOException loading VSDecoder from {}", config.getVSDPath()); 114 // would be nice to pop up a dialog here... 115 } 116 117 if (this.getEngineSound().getBuffersFreeState()) { 118 // Since the Config already has the address set, we need to call 119 // our own setAddress() to register the throttle listener 120 this.setAddress(config.getLocoAddress()); 121 this.enable(); 122 123 // Handle Advanced Location Following (if the parameter file is OK) 124 if (VSDecoderManager.instance().geofile_ok) { 125 // ALF1 needs this 126 this.setup_index = 0; 127 // create a navigator for this VSDecoder 128 if (VSDecoderManager.instance().alf_version == 2) { 129 navigation = new VSDNavigation(this); 130 } 131 } 132 133 if (log.isDebugEnabled()) { 134 log.debug("VSDecoder Init Complete. Audio Objects Created:"); 135 jmri.InstanceManager.getDefault(jmri.AudioManager.class).getNamedBeanSet(Audio.SOURCE).forEach((s) -> { 136 log.debug("\tSource: {}", s); 137 }); 138 jmri.InstanceManager.getDefault(jmri.AudioManager.class).getNamedBeanSet(Audio.BUFFER).forEach((s) -> { 139 log.debug("\tBuffer: {}", s); 140 }); 141 } 142 143 log.info("Number of used buffers: {}, max: {}", 144 jmri.InstanceManager.getDefault(jmri.AudioManager.class).getNamedBeanSet(Audio.BUFFER).size(), 145 jmri.AudioManager.MAX_BUFFERS); 146 } else { 147 this.disable(); // not a valid VSDecoder 148 } 149 } 150 151 /** 152 * Construct a VSDecoder with the given system name (id), profile name and 153 * VSD file path 154 * 155 * @param id (String) System name for this VSDecoder 156 * @param name (String) Profile name 157 * @param path (String) Path to a VSD file to pull the given Profile from 158 */ 159 public VSDecoder(String id, String name, String path) { 160 161 config = new VSDConfig(); 162 config.setProfileName(name); 163 config.setId(id); 164 165 sound_list = new HashMap<>(); 166 event_list = new LinkedHashMap<>(); 167 168 // Force re-initialization 169 initialized = _init(); 170 171 config.setVSDPath(path); 172 173 try { 174 VSDFile vsdfile = new VSDFile(path); 175 if (vsdfile.isInitialized()) { 176 log.debug("Constructor: vsdfile init OK, loading XML..."); 177 this.setXml(vsdfile, name); 178 } else { 179 log.debug("Constructor: vsdfile init FAILED."); 180 initialized = false; 181 } 182 } catch (java.util.zip.ZipException e) { 183 log.error("ZipException loading VSDecoder from {}", path); 184 // would be nice to pop up a dialog here... 185 } catch (java.io.IOException ioe) { 186 log.error("IOException loading VSDecoder from {}", path); 187 // would be nice to pop up a dialog here... 188 } 189 } 190 191 private boolean _init() { 192 // Do nothing for now 193 this.enable(); 194 return true; 195 } 196 197 /** 198 * Get the ID (System Name) of this VSDecoder 199 * 200 * @return (String) system name of this VSDecoder 201 */ 202 public String getId() { 203 return config.getId(); 204 } 205 206 /** 207 * Check whether this VSDecoder has completed initialization 208 * 209 * @return (boolean) true if initialization is complete. 210 */ 211 public boolean isInitialized() { 212 return initialized; 213 } 214 215 /** 216 * Set the VSD File path for this VSDecoder to use 217 * 218 * @param p (String) path to VSD File 219 */ 220 public void setVSDFilePath(String p) { 221 config.setVSDPath(p); 222 } 223 224 /** 225 * Get the current VSD File path for this VSDecoder 226 * 227 * @return (String) path to VSD file 228 */ 229 public String getVSDFilePath() { 230 return config.getVSDPath(); 231 } 232 233 /** 234 * Shut down this VSDecoder and all of its associated sounds. 235 */ 236 public void shutdown() { 237 log.debug("Shutting down sounds..."); 238 for (VSDSound vs : sound_list.values()) { 239 log.debug("Stopping sound: {}", vs.getName()); 240 vs.shutdown(); 241 } 242 } 243 244 /** 245 * Handle the details of responding to a PropertyChangeEvent from a 246 * throttle. 247 * 248 * @param event (PropertyChangeEvent) Throttle event to respond to 249 */ 250 protected void throttlePropertyChange(PropertyChangeEvent event) { 251 // WARNING: FRAGILE CODE 252 // This will break if the return type of the event.getOld/NewValue() changes. 253 254 String eventName = event.getPropertyName(); 255 256 // Skip this if disabled 257 if (!enabled) { 258 log.debug("VSDecoder disabled. Take no action."); 259 return; 260 } 261 262 log.debug("VSDecoder throttle property change: {}", eventName); 263 264 if (eventName.equals("throttleAssigned")) { 265 Float s = (Float) jmri.InstanceManager.throttleManagerInstance().getThrottleInfo(config.getDccAddress(), Throttle.SPEEDSETTING); 266 if (s != null) { 267 this.getEngineSound().setFirstSpeed(true); // Auto-start needs this 268 // Mimic a throttlePropertyChange to propagate the current (init) speed setting of the throttle. 269 log.debug("Existing DCC Throttle found. Speed: {}", s); 270 this.throttlePropertyChange(new PropertyChangeEvent(this, Throttle.SPEEDSETTING, null, s)); 271 } 272 273 // Check for an existing throttle and get loco direction if it exists. 274 Boolean b = (Boolean) jmri.InstanceManager.throttleManagerInstance().getThrottleInfo(config.getDccAddress(), Throttle.ISFORWARD); 275 if (b != null) { 276 dirfn = b ? 1 : -1; 277 log.debug("Existing DCC Throttle found. IsForward is {}", b); 278 log.debug("Initial dirfn: {} for {}", dirfn, config.getDccAddress()); 279 this.throttlePropertyChange(new PropertyChangeEvent(this, Throttle.ISFORWARD, null, b)); 280 } else { 281 log.warn("No existing DCC throttle found."); 282 } 283 284 // Check for an existing throttle and get ENGINE throttle function key status if it exists. 285 // For all function keys used in config.xml (sound-event name="ENGINE") this will send an initial value! This could be ON or OFF. 286 if (event_list.get("ENGINE") != null) { 287 for (Trigger t : event_list.get("ENGINE").trigger_list.values()) { 288 log.debug("ENGINE trigger Name: {}, Event: {}, t: {}", t.getName(), t.getEventName(), t); 289 if (t.getEventName().startsWith("F")) { 290 log.debug("F-Key trigger found: {}, name: {}, event: {}", t, t.getName(), t.getEventName()); 291 // Don't send an initial value if trigger is ENGINE_STARTSTOP, because that would work against auto-start; BRAKE_KEY would play a sound 292 if (!t.getName().equals("ENGINE_STARTSTOP") && !t.getName().equals("BRAKE_KEY")) { 293 b = (Boolean) jmri.InstanceManager.throttleManagerInstance().getThrottleInfo(config.getDccAddress(), t.getEventName()); 294 if (b != null) { 295 this.throttlePropertyChange(new PropertyChangeEvent(this, t.getEventName(), null, b)); 296 } 297 } 298 } 299 } 300 } 301 } 302 303 // Iterate through the list of sound events, forwarding the propertyChange event. 304 for (SoundEvent t : event_list.values()) { 305 t.propertyChange(event); 306 } 307 308 if (eventName.equals(Throttle.ISFORWARD)) { 309 dirfn = (Boolean) event.getNewValue() ? 1 : -1; 310 } 311 } 312 313 /** 314 * Set this VSDecoder's LocoAddress, and register to follow events from the 315 * throttle with this address. 316 * 317 * @param l (LocoAddress) LocoAddress to be followed 318 */ 319 public void setAddress(LocoAddress l) { 320 // Hack for ThrottleManager Dcc dependency 321 config.setLocoAddress(l); 322 jmri.InstanceManager.throttleManagerInstance().attachListener(config.getDccAddress(), 323 new PropertyChangeListener() { 324 @Override 325 public void propertyChange(PropertyChangeEvent event) { 326 log.debug("property change name: {}, old: {}, new: {}", event.getPropertyName(), event.getOldValue(), event.getNewValue()); 327 throttlePropertyChange(event); 328 } 329 }); 330 log.debug("VSDecoder: Address set to {}", config.getLocoAddress()); 331 } 332 333 /** 334 * Get the currently assigned LocoAddress 335 * 336 * @return the currently assigned LocoAddress 337 */ 338 public LocoAddress getAddress() { 339 return config.getLocoAddress(); 340 } 341 342 public RosterEntry getRosterEntry() { 343 return config.getRosterEntry(); 344 } 345 346 /** 347 * Get the current decoder volume setting for this VSDecoder 348 * 349 * @return (float) volume level (0.0 - 1.0) 350 */ 351 public float getDecoderVolume() { 352 return config.getVolume(); 353 } 354 355 private void forwardMasterVolume(float volume) { 356 log.debug("VSD config id: {}, Master volume: {}, Decoder volume: {}", config.getId(), volume, config.getVolume()); 357 for (VSDSound vs : sound_list.values()) { 358 vs.setVolume(volume * config.getVolume()); 359 } 360 } 361 362 /** 363 * Set the decoder volume for this VSDecoder 364 * 365 * @param decoder_volume (float) volume level (0.0 - 1.0) 366 */ 367 public void setDecoderVolume(float decoder_volume) { 368 config.setVolume(decoder_volume); 369 float master_vol = 0.01f * VSDecoderManager.instance().getMasterVolume(); 370 log.debug("config set decoder volume to {}, master volume adjusted: {}", decoder_volume, master_vol); 371 for (VSDSound vs : sound_list.values()) { 372 vs.setVolume(master_vol * decoder_volume); 373 } 374 } 375 376 /** 377 * Is this VSDecoder muted? 378 * 379 * @return true if muted 380 */ 381 public boolean isMuted() { 382 return getMuteState(); 383 } 384 385 /** 386 * Mute or un-mute this VSDecoder 387 * 388 * @param m (boolean) true to mute, false to un-mute 389 */ 390 public void mute(boolean m) { 391 for (VSDSound vs : sound_list.values()) { 392 vs.mute(m); 393 } 394 } 395 396 private void setMuteState(boolean m) { 397 is_muted = m; 398 } 399 400 private boolean getMuteState() { 401 return is_muted; 402 } 403 404 /** 405 * set the x/y/z position in the soundspace of this VSDecoder Translates the 406 * given position to a position relative to the listener for the component 407 * VSDSounds. 408 * <p> 409 * The idea is that the user-preference Listener Position (relative to the 410 * USER's chosen origin) is always the OpenAL Context's origin. 411 * 412 * @param p (PhysicalLocation) location relative to the user's chosen 413 * Origin. 414 */ 415 public void setPosition(PhysicalLocation p) { 416 // Store the actual position relative to the user's Origin locally. 417 config.setPhysicalLocation(p); 418 if (create_xy_series) { 419 log.info("setPosition {}: {}\t{}", this.getAddress(), (float) Math.round(p.x*10000)/10000, p.y); 420 } 421 log.debug("address {} set Position: {}", this.getAddress(), p); 422 423 this.lastPos = p; // save this position 424 425 // Give all of the VSDSound objects the position translated relative to the listener position. 426 // This is a workaround for OpenAL requiring the listener position to always be at (0,0,0). 427 /* 428 * PhysicalLocation ref = VSDecoderManager.instance().getVSDecoderPreferences().getListenerPhysicalLocation(); 429 * if (ref == null) ref = PhysicalLocation.Origin; 430 */ 431 for (VSDSound s : sound_list.values()) { 432 // s.setPosition(PhysicalLocation.translate(p, ref)); 433 s.setPosition(p); 434 } 435 436 // Set (relative) volume for this location (in case we're in a tunnel) 437 float tv = 0.01f * VSDecoderManager.instance().getMasterVolume() * getDecoderVolume(); 438 log.debug("current master volume: {}, decoder volume: {}", VSDecoderManager.instance().getMasterVolume(), getDecoderVolume()); 439 if (savedSound.getTunnel()) { 440 tv *= VSDSound.tunnel_volume; 441 log.debug("VSD: In tunnel, volume: {}", tv); 442 } else { 443 log.debug("VSD: Not in tunnel, volume: {}", tv); 444 } 445 if (! getMuteState()) { 446 for (VSDSound vs : sound_list.values()) { 447 vs.setVolume(tv); 448 } 449 } 450 } 451 452 /** 453 * Get the current x/y/z position in the soundspace of this VSDecoder 454 * 455 * @return PhysicalLocation location of this VSDecoder 456 */ 457 public PhysicalLocation getPosition() { 458 return config.getPhysicalLocation(); 459 } 460 461 /** 462 * Respond to property change events from this VSDecoder's GUI 463 * 464 * @param evt (PropertyChangeEvent) event to respond to 465 */ 466 @Override 467 public void propertyChange(PropertyChangeEvent evt) { 468 String property = evt.getPropertyName(); 469 // Respond to events from the new GUI. 470 if (evt.getSource() instanceof VSDControl) { 471 if (property.equals(VSDControl.OPTION_CHANGE)) { 472 Train selected_train = jmri.InstanceManager.getDefault(TrainManager.class).getTrainByName((String) evt.getNewValue()); 473 if (selected_train != null) { 474 selected_train.addPropertyChangeListener(this); 475 // Handle Advanced Location Following (if the parameter file is OK) 476 if (VSDecoderManager.instance().geofile_ok) { 477 Route r = selected_train.getRoute(); 478 if (r != null) { 479 log.info("Train \"{}\" selected for {} - Route is now \"{}\"", selected_train, this.getAddress(), r.getName()); 480 if (r.getName().equals("VSDRoute1")) { 481 this.setup_index = 0; 482 } else if (r.getName().equals("VSDRoute2") && VSDecoderManager.instance().num_setups > 1) { 483 this.setup_index = 1; 484 } else if (r.getName().equals("VSDRoute3") && VSDecoderManager.instance().num_setups > 2) { 485 this.setup_index = 2; 486 } else if (r.getName().equals("VSDRoute4") && VSDecoderManager.instance().num_setups > 3) { 487 this.setup_index = 3; 488 } else { 489 log.warn("\"{}\" is not suitable for VSD Advanced Location Following", r.getName()); 490 } 491 } else { 492 log.warn("Train \"{}\" is without Route", selected_train); 493 } 494 } 495 } 496 } 497 return; 498 } 499 500 if (property.equals(VSDManagerFrame.MUTE)) { 501 // GUI Mute button 502 log.debug("VSD: Mute change. value: {}", evt.getNewValue()); 503 setMuteState((boolean) evt.getNewValue()); 504 this.mute(getMuteState()); 505 } else if (property.equals(VSDManagerFrame.VOLUME_CHANGE)) { 506 // GUI Volume slider (Master Volume) 507 log.debug("VSD: Volume change. value: {}", evt.getOldValue()); 508 // Slider gives integer 0-100. Need to change that to a float 0.0-1.0 509 this.forwardMasterVolume((0.01f * (Integer) evt.getOldValue())); 510 } else if (property.equals(Train.TRAIN_LOCATION_CHANGED_PROPERTY)) { 511 // Train Location Move 512 PhysicalLocation p = getTrainPosition((Train) evt.getSource()); 513 if (p != null) { 514 this.setPosition(getTrainPosition((Train) evt.getSource())); 515 } else { 516 log.debug("Train has null position"); 517 this.setPosition(new PhysicalLocation()); 518 } 519 } else if (property.equals(Train.STATUS_CHANGED_PROPERTY)) { 520 // Train Status change 521 String status = (String) evt.getOldValue(); 522 log.debug("Train status changed: {}", status); 523 log.debug("New Location: {}", getTrainPosition((Train) evt.getSource())); 524 if ((status.startsWith(Train.BUILT)) || (status.startsWith(Train.PARTIAL_BUILT))) { 525 log.debug("Train built. status: {}", status); 526 PhysicalLocation p = getTrainPosition((Train) evt.getSource()); 527 if (p != null) { 528 this.setPosition(getTrainPosition((Train) evt.getSource())); 529 } else { 530 log.debug("Train has null position"); 531 this.setPosition(new PhysicalLocation()); 532 } 533 } 534 } 535 } 536 537 // Methods for handling location tracking based on JMRI Operations 538 /** 539 * Get the physical location of the given Operations Train 540 * 541 * @param t (Train) the Train to interrogate 542 * @return PhysicalLocation location of the train 543 */ 544 protected PhysicalLocation getTrainPosition(Train t) { 545 if (t == null) { 546 log.debug("Train is null."); 547 return null; 548 } 549 RouteLocation rloc = t.getCurrentRouteLocation(); 550 if (rloc == null) { 551 log.debug("RouteLocation is null."); 552 return null; 553 } 554 Location loc = rloc.getLocation(); 555 if (loc == null) { 556 log.debug("Location is null."); 557 return null; 558 } 559 return loc.getPhysicalLocation(); 560 } 561 562 // Methods for handling the underlying sounds 563 /** 564 * Retrieve the VSDSound with the given system name 565 * 566 * @param name (String) System name of the requested VSDSound 567 * @return VSDSound the requested sound 568 */ 569 public VSDSound getSound(String name) { 570 return sound_list.get(name); 571 } 572 573 // Java Bean set/get Functions 574 /** 575 * Set the profile name to the given string 576 * 577 * @param pn (String) : name of the profile to set 578 */ 579 public void setProfileName(String pn) { 580 config.setProfileName(pn); 581 } 582 583 /** 584 * get the currently selected profile name 585 * 586 * @return (String) name of the currently selected profile 587 */ 588 public String getProfileName() { 589 return config.getProfileName(); 590 } 591 592 /** 593 * Enable this VSDecoder. 594 */ 595 void enable() { 596 enabled = true; 597 } 598 599 /** 600 * Disable this VSDecoder. 601 */ 602 void disable() { 603 enabled = false; 604 } 605 606 boolean isEnabled() { 607 return enabled; 608 } 609 610 /** 611 * Get a reference to the EngineSound associated with this VSDecoder 612 * 613 * @return EngineSound The EngineSound reference for this VSDecoder or null 614 */ 615 public EngineSound getEngineSound() { 616 return (EngineSound) sound_list.get("ENGINE"); 617 } 618 619 /** 620 * Get a Collection of SoundEvents associated with this VSDecoder 621 * 622 * @return {@literal Collection<SoundEvent>} collection of SoundEvents 623 */ 624 public Collection<SoundEvent> getEventList() { 625 return event_list.values(); 626 } 627 628 /** 629 * Get an XML representation of this VSDecoder Includes a subtree of 630 * Elements for all of the associated SoundEvents, Triggers, VSDSounds, etc. 631 * 632 * @return Element XML Element for this VSDecoder 633 */ 634 public Element getXml() { 635 Element me = new Element("vsdecoder"); 636 ArrayList<Element> le = new ArrayList<>(); 637 638 me.setAttribute("name", this.config.getProfileName()); 639 640 for (SoundEvent se : event_list.values()) { 641 le.add(se.getXml()); 642 } 643 644 for (VSDSound vs : sound_list.values()) { 645 le.add(vs.getXml()); 646 } 647 648 me.addContent(le); 649 650 // Need to add whatever else here. 651 return me; 652 } 653 654 /** 655 * Build this VSDecoder from an XML representation 656 * 657 * @param vf (VSDFile) : VSD File to pull the XML from 658 * @param pn (String) : Parameter Name to find within the VSD File. 659 */ 660 public void setXml(VSDFile vf, String pn) { 661 Iterator<Element> itr; 662 Element e = null; 663 Element el = null; 664 SoundEvent se; 665 String n; 666 667 if (vf == null) { 668 log.debug("Null VSD File Name"); 669 return; 670 } 671 672 log.debug("VSD File Name: {}, profile: {}", vf.getName(), pn); 673 // need to choose one. 674 this.setVSDFilePath(vf.getName()); 675 676 // Find the <profile/> element that matches the name pn 677 // List<Element> profiles = vf.getRoot().getChildren("profile"); 678 // java.util.Iterator i = profiles.iterator(); 679 java.util.Iterator<Element> i = vf.getRoot().getChildren("profile").iterator(); 680 while (i.hasNext()) { 681 e = i.next(); 682 if (e.getAttributeValue("name").equals(pn)) { 683 break; 684 } 685 } 686 // E is now the first <profile/> in vsdfile that matches pn. 687 688 if (e == null) { 689 // No matching profile name found. 690 return; 691 } 692 693 // Set this decoder's name. 694 this.setProfileName(e.getAttributeValue("name")); 695 log.debug("Decoder Name: {}", e.getAttributeValue("name")); 696 697 // Check for a flag element to create xy-position-coordinates. 698 n = e.getChildText("create-xy-series"); 699 if ((n != null) && (n.equals("yes"))) { 700 create_xy_series = true; 701 log.debug("Profile {}: xy-position-coordinates will be created in JMRI System Console", getProfileName()); 702 } else { 703 create_xy_series = false; 704 log.debug("Profile {}: xy-position-coordinates will NOT be created in JMRI System Console", getProfileName()); 705 } 706 707 // Check for an optional sound start-position. 708 n = e.getChildText("start-position"); 709 if (n != null) { 710 startPos = PhysicalLocation.parse(n); 711 } else { 712 startPos = null; 713 } 714 log.debug("Start position: {}", startPos); 715 716 // +++ DEBUG 717 // Log and print all of the child elements. 718 itr = (e.getChildren()).iterator(); 719 while (itr.hasNext()) { 720 // Pull each element from the XML file. 721 el = itr.next(); 722 log.debug("Element: {}", el); 723 if (el.getAttribute("name") != null) { 724 log.debug(" Name: {}", el.getAttributeValue("name")); 725 log.debug(" type: {}", el.getAttributeValue("type")); 726 } 727 } 728 // --- DEBUG 729 730 // First, the sounds. 731 String prefix = "" + this.getId() + ":"; 732 log.debug("VSDecoder {}, prefix: {}", this.getId(), prefix); 733 itr = (e.getChildren("sound")).iterator(); 734 while (itr.hasNext()) { 735 el = itr.next(); 736 if (el.getAttributeValue("type") == null) { 737 // Empty sound. Skip. 738 log.debug("Skipping empty Sound."); 739 continue; 740 } else if (el.getAttributeValue("type").equals("configurable")) { 741 // Handle configurable sounds. 742 ConfigurableSound cs = new ConfigurableSound(prefix + el.getAttributeValue("name")); 743 cs.setXml(el, vf); 744 sound_list.put(el.getAttributeValue("name"), cs); 745 } else if (el.getAttributeValue("type").equals("diesel")) { 746 // Handle a diesel Engine sound 747 DieselSound es = new DieselSound(prefix + el.getAttributeValue("name")); 748 es.setXml(el, vf); 749 sound_list.put(el.getAttributeValue("name"), es); 750 } else if (el.getAttributeValue("type").equals("diesel3")) { 751 // Handle a diesel3 Engine sound 752 Diesel3Sound es = new Diesel3Sound(prefix + el.getAttributeValue("name")); 753 savedSound = es; 754 es.setXml(el, vf); 755 sound_list.put(el.getAttributeValue("name"), es); 756 topspeed = es.top_speed; 757 topspeed_rev = topspeed; 758 } else if (el.getAttributeValue("type").equals("steam")) { 759 // Handle a steam Engine sound 760 SteamSound es = new SteamSound(prefix + el.getAttributeValue("name")); 761 savedSound = es; 762 es.setXml(el, vf); 763 sound_list.put(el.getAttributeValue("name"), es); 764 topspeed = es.top_speed; 765 topspeed_rev = topspeed; 766 } else if (el.getAttributeValue("type").equals("steam1")) { 767 // Handle a steam1 Engine sound 768 Steam1Sound es = new Steam1Sound(prefix + el.getAttributeValue("name")); 769 savedSound = es; 770 es.setXml(el, vf); 771 sound_list.put(el.getAttributeValue("name"), es); 772 topspeed = es.top_speed; 773 topspeed_rev = es.top_speed_reverse; 774 //} else { 775 // TODO: Some type other than configurable sound. Handle appropriately 776 } 777 } 778 779 // Next, grab all of the SoundEvents 780 // Have to do the sounds first because the SoundEvent's setXml() will 781 // expect to be able to look it up. 782 itr = (e.getChildren("sound-event")).iterator(); 783 while (itr.hasNext()) { 784 el = itr.next(); 785 switch (SoundEvent.ButtonType.valueOf(el.getAttributeValue("buttontype").toUpperCase())) { 786 case MOMENTARY: 787 se = new MomentarySoundEvent(el.getAttributeValue("name")); 788 break; 789 case TOGGLE: 790 se = new ToggleSoundEvent(el.getAttributeValue("name")); 791 break; 792 case ENGINE: 793 se = new EngineSoundEvent(el.getAttributeValue("name")); 794 break; 795 case NONE: 796 default: 797 se = new SoundEvent(el.getAttributeValue("name")); 798 } 799 se.setParent(this); 800 se.setXml(el, vf); 801 event_list.put(se.getName(), se); 802 } 803 // Handle other types of children similarly here. 804 } 805 806 // VSDNavigation accessors 807 // 808 // Code from George Warner's LENavigator 809 // 810 void setLocation(Point2D location) { 811 this.location = location; 812 } 813 814 Point2D getLocation() { 815 return location; 816 } 817 818 LayoutTrack getLastTrack() { 819 return lastTrack; 820 } 821 822 void setLastTrack(LayoutTrack lastTrack) { 823 this.lastTrack = lastTrack; 824 } 825 826 void setLayoutTrack(LayoutTrack layoutTrack) { 827 this.layoutTrack = layoutTrack; 828 } 829 830 LayoutTrack getLayoutTrack() { 831 return layoutTrack; 832 } 833 834 void setReturnTrack(LayoutTrack returnTrack) { 835 this.returnTrack = returnTrack; 836 } 837 838 LayoutTrack getReturnTrack() { 839 return returnTrack; 840 } 841 842 void setReturnLastTrack(LayoutTrack returnLastTrack) { 843 this.returnLastTrack = returnLastTrack; 844 } 845 846 LayoutTrack getReturnLastTrack() { 847 return returnLastTrack; 848 } 849 850 double getDistance() { 851 return distance; 852 } 853 854 void setDistance(double distance) { 855 this.distance = distance; 856 } 857 858 double getReturnDistance() { 859 return returnDistance; 860 } 861 862 void setReturnDistance(double returnDistance) { 863 this.returnDistance = returnDistance; 864 } 865 866 double getDirectionRAD() { 867 return directionRAD; 868 } 869 870 void setDirectionRAD(double directionRAD) { 871 this.directionRAD = directionRAD; 872 } 873 874 void setDirectionDEG(double directionDEG) { 875 this.directionRAD = Math.toRadians(directionDEG); 876 } 877 878 LayoutEditor getModels() { 879 return models; 880 } 881 882 void setModels(LayoutEditor models) { 883 this.models = models; 884 } 885 886 void navigate() { 887 boolean result = false; 888 do { 889 if (this.getLayoutTrack() instanceof TrackSegment) { 890 result = navigation.navigateTrackSegment(); 891 } else if (this.getLayoutTrack() instanceof LayoutSlip) { 892 result = navigation.navigateLayoutSlip(); 893 } else if (this.getLayoutTrack() instanceof LayoutTurnout) { 894 result = navigation.navigateLayoutTurnout(); 895 } else if (this.getLayoutTrack() instanceof PositionablePoint) { 896 result = navigation.navigatePositionalPoint(); 897 } else if (this.getLayoutTrack() instanceof LevelXing) { 898 result = navigation.navigateLevelXing(); 899 } else if (this.getLayoutTrack() instanceof LayoutTurntable) { 900 result = navigation.navigateLayoutTurntable(); 901 } else { 902 log.warn("Track type not supported"); 903 setReturnDistance(0); 904 setReturnTrack(getLastTrack()); 905 result = false; 906 } 907 } while (result); 908 } 909 910 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDecoder.class); 911 912}