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