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