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