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