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}