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