001package jmri.jmrit.vsdecoder;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.io.File;
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Set;
011import jmri.Audio;
012import jmri.Block;
013import jmri.IdTag;
014import jmri.LocoAddress;
015import jmri.Manager;
016import jmri.NamedBean;
017import jmri.Path;
018import jmri.PhysicalLocationReporter;
019import jmri.Reporter;
020import jmri.implementation.DefaultIdTag;
021import jmri.jmrit.display.layoutEditor.*;
022import jmri.jmrit.roster.Roster;
023import jmri.jmrit.roster.RosterEntry;
024import jmri.jmrit.operations.trains.Train;
025import jmri.jmrit.operations.trains.TrainManager;
026import jmri.jmrit.vsdecoder.listener.ListeningSpot;
027import jmri.jmrit.vsdecoder.listener.VSDListener;
028import jmri.jmrit.vsdecoder.swing.VSDManagerFrame;
029import jmri.util.FileUtil;
030import jmri.util.JmriJFrame;
031import jmri.util.MathUtil;
032import jmri.util.PhysicalLocation;
033import java.awt.event.ActionEvent;
034import java.awt.event.ActionListener;
035import java.awt.geom.Point2D;
036import java.awt.GraphicsEnvironment;
037import javax.swing.Timer;
038import org.jdom2.Element;
039
040/**
041 * VSDecoderFactory, builds VSDecoders as needed, handles loading from XML if needed.
042 *
043 * <hr>
044 * This file is part of JMRI.
045 * <p>
046 * JMRI is free software; you can redistribute it and/or modify it under
047 * the terms of version 2 of the GNU General Public License as published
048 * by the Free Software Foundation. See the "COPYING" file for a copy
049 * of this license.
050 * <p>
051 * JMRI is distributed in the hope that it will be useful, but WITHOUT
052 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
053 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
054 * for more details.
055 *
056 * @author Mark Underwood Copyright (C) 2011
057 * @author Klaus Killinger Copyright (C) 2018-2025
058 */
059public class VSDecoderManager implements PropertyChangeListener {
060
061    //private static final ResourceBundle rb = VSDecoderBundle.bundle();
062    private static final String vsd_property_change_name = "VSDecoder Manager"; // NOI18N
063
064    // Array-pointer for blockParameter
065    private static final int RADIUS = 0;
066    private static final int SLOPE = 1;
067    private static final int ROTATE_XPOS_I = 2;
068    private static final int ROTATE_YPOS_I = 3;
069    private static final int LENGTH = 4;
070
071    // Array-pointer for locoInBlock
072    private static final int ADDRESS = 0;
073    private static final int BLOCK = 1;
074    private static final int DISTANCE_TO_GO = 2;
075    private static final int DIR_FN = 3;
076    private static final int DIRECTION = 4;
077
078    protected jmri.NamedBeanHandleManager nbhm = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class);
079
080    private HashMap<String, VSDListener> listenerTable; // list of listeners
081    private HashMap<String, VSDecoder> decodertable; // list of active decoders by System ID
082    private HashMap<String, VSDecoder> decoderAddressMap; // List of active decoders by address
083    private HashMap<Integer, VSDecoder> decoderInBlock; // list of active decoders by LocoAddress.getNumber()
084    private HashMap<String, String> profiletable; // list of loaded profiles key = profile name, value = path
085    HashMap<VSDecoder, Block> currentBlock; // list of active blocks by decoders
086    private HashMap<Block, LayoutEditor> possibleStartBlocks; // list of possible start blocks and their LE panel
087    private Timer timer;
088
089    private int locoInBlock[][]; // Block status for locos
090    private float blockParameter[][][];
091    private List<List<PhysicalLocation>> blockPositionlists;
092    private List<List<Integer>> reporterlists;
093    private List<Boolean> circlelist;
094    private PhysicalLocation newPosition;
095    private PhysicalLocation models_origin;
096    private ArrayList<Block> blockList;
097
098    // List of registered event listeners
099    protected javax.swing.event.EventListenerList listenerList = new javax.swing.event.EventListenerList();
100
101    //private static VSDecoderManager instance = null; // sole instance of this class
102    private volatile static VSDecoderManagerThread thread = null; // thread for running the manager
103
104    private VSDecoderPreferences vsdecoderPrefs; // local pointer to the preferences object
105
106    private JmriJFrame managerFrame = null;
107
108    private int vsdecoderID = 0;
109    private int locorow = -1; // Will be increased before first use
110
111    private int check_time; // Time interval in ms for track following updates
112    private float layout_scale;
113    private float distance_rest = 0.0f; // Block distance to go
114    private float distance_rest_old = 0.0f; // Block distance to go, copy
115    private float distance_rest_new = 0.0f; // Block distance to go, copy
116
117    private float xPosi;
118    public static final int max_decoder = 8;
119    boolean geofile_ok = false;
120    int num_setups;
121    private int lf_version;
122    int alf_version;
123
124    // constructor - for kicking off by the VSDecoderManagerThread...
125    // WARNING: Should only be called from static instance()
126    public VSDecoderManager() {
127        // Setup the decoder table
128        listenerTable = new HashMap<>();
129        decodertable = new HashMap<>();
130        decoderAddressMap = new HashMap<>();
131        decoderInBlock = new HashMap<>(); // Key = decoder number
132        profiletable = new HashMap<>(); // key = profile name, value = path
133        currentBlock = new HashMap<>(); // key = decoder, value = block
134        possibleStartBlocks = new HashMap<>();
135        locoInBlock = new int[max_decoder][5]; // Loco address number, current block, distance in cm to go in block, dirfn, direction
136        // Setup lists
137        reporterlists = new ArrayList<>();
138        blockPositionlists = new ArrayList<>();
139        circlelist = new ArrayList<>();
140        // Get preferences
141        String dirname = FileUtil.getUserFilesPath() + "vsdecoder" + File.separator; // NOI18N
142        FileUtil.createDirectory(dirname);
143        vsdecoderPrefs = new VSDecoderPreferences(dirname + VSDecoderPreferences.VSDPreferencesFileName);
144        // Listen to ReporterManager for Report List changes
145        setupReporterManagerListener();
146        // Get a Listener
147        VSDListener t = new VSDListener();
148        listenerTable.put(t.getSystemName(), t);
149        // Update JMRI "Default Audio Listener"
150        setListenerLocation(t.getSystemName(), vsdecoderPrefs.getListenerPosition());
151        // Look for additional layout geometry data
152        VSDGeoFile gf = new VSDGeoFile();
153        if (gf.geofile_ok) {
154            geofile_ok = true;
155            alf_version = gf.alf_version;
156            num_setups = gf.getNumberOfSetups();
157            reporterlists = gf.getReporterList();
158            blockParameter = gf.getBlockParameter();
159            blockPositionlists = gf.getBlockPosition();
160            circlelist = gf.getCirclingList();
161            check_time = gf.getCheckTime();
162            layout_scale = gf.layout_scale;
163            models_origin = gf.models_origin;
164            possibleStartBlocks = gf.possibleStartBlocks;
165            blockList = gf.blockList;
166        } else {
167            geofile_ok = false;
168            if (gf.lf_version > 0) {
169                lf_version = gf.lf_version;
170                log.debug("assume location following");
171            }
172        }
173    }
174
175    /**
176     * Provide the VSdecoderManager instance.
177     * @return the manager
178     */
179    public static VSDecoderManager instance() {
180        if (thread == null) {
181            thread = VSDecoderManagerThread.instance(true);
182        }
183        return VSDecoderManagerThread.manager();
184    }
185
186    /**
187     * Get a reference to the VSD Preferences.
188     * @return the preferences reference
189     */
190    public VSDecoderPreferences getVSDecoderPreferences() {
191        return vsdecoderPrefs;
192    }
193
194    /**
195     * Get the master volume of all VSDecoders.
196     * @return the master volume
197     */
198    public int getMasterVolume() {
199        return getVSDecoderPreferences().getMasterVolume();
200    }
201
202    /**
203     * Set the master volume for all VSDecoders.
204     * @param mv The new master volume
205     */
206    public void setMasterVolume(int mv) {
207        getVSDecoderPreferences().setMasterVolume(mv);
208    }
209
210    /**
211     * Check if Block is a possible startblock.
212     * @param blk Block to check
213     * @return true if possible, else false
214     */
215    public boolean checkForPossibleStartblock(Block blk) {
216        if (possibleStartBlocks.containsKey(blk)) {
217            return true;
218        } else {
219            if (geofile_ok) {
220                log.warn("Block {} is not a valid starting block", blk);
221            }
222            return false;
223        }
224    }
225
226    public void doResume() {
227        // prepare a re-open of the VSD manager window
228        if (geofile_ok && getVSDecoderList().size() > 0) {
229            initSoundPositionTimer();
230        }
231    }
232
233    /**
234     * Get the VSD GUI.
235     * @return the VSD frame
236     */
237    public JmriJFrame provideManagerFrame() {
238        if (managerFrame == null) {
239            if (GraphicsEnvironment.isHeadless()) {
240                String vsdRosterGroup = "VSD";
241                if (Roster.getDefault().getRosterGroupList().contains(vsdRosterGroup)) {
242                    List<RosterEntry> rosterList;
243                    rosterList = Roster.getDefault().getEntriesInGroup(vsdRosterGroup);
244                    // Allow <max_decoder> roster entries
245                    int entry_counter = 0;
246                    for (RosterEntry entry : rosterList) {
247                        if (entry_counter < max_decoder) {
248                            VSDConfig config = new VSDConfig();
249                            config.setLocoAddress(entry.getDccLocoAddress());
250                            log.info("Loading Roster Entry \"{}\", VSDecoder {} ...", entry.getId(), config.getLocoAddress());
251                            String path = entry.getAttribute("VSDecoder_Path");
252                            String profile = entry.getAttribute("VSDecoder_Profile");
253                            if (path != null && profile != null) {
254                                if (LoadVSDFileAction.loadVSDFile(path)) {
255                                    // config.xml OK
256                                    log.info(" VSD path: {}", FileUtil.getExternalFilename(path));
257                                    config.setProfileName(profile);
258                                    log.debug(" entry VSD profile: {}", profile);
259                                    if (entry.getAttribute("VSDecoder_Volume") != null) {
260                                        config.setVolume(Float.parseFloat(entry.getAttribute("VSDecoder_Volume")));
261                                    } else {
262                                        config.setVolume(0.8f);
263                                    }
264                                    VSDecoder newDecoder = getVSDecoder(config);
265                                    if (newDecoder != null) {
266                                        log.info("VSD {}, profile \"{}\" ready.", config.getLocoAddress(), config.getProfileName());
267                                        entry_counter++;
268                                    } else {
269                                        log.warn("VSD {} failed", config.getProfileName());
270                                    }
271                                }
272                            } else {
273                                log.error("Cannot load VSD File - path or profile missing - check your Roster Media");
274                            }
275                        } else {
276                            log.warn("Only {} roster entries allowed. Disgarded {}", max_decoder, rosterList.size() - max_decoder);
277                        }
278                    }
279                    if (entry_counter == 0) {
280                        log.warn("No Roster entry found in Roster Group {}", vsdRosterGroup);
281                    }
282                } else {
283                    log.warn("Roster group \"{}\" not found", vsdRosterGroup);
284                }
285            } else {
286                // Run VSDecoder with GUI
287                managerFrame = new VSDManagerFrame();
288            }
289        } else {
290            log.warn("Virtual Sound Decoder Manager is already running");
291        }
292        return managerFrame;
293    }
294
295    private String getNextVSDecoderID() {
296        // vsdecoderID initialized to zero, pre-incremented before return...
297        // first returned ID value is 1.
298        return "IAD:VSD:VSDecoderID" + (++vsdecoderID); // NOI18N
299    }
300
301    private Integer getNextlocorow() {
302        // locorow initialized to -1, pre-incremented before return...
303        // first returned value is 0.
304        return ++locorow;
305    }
306
307    /**
308     * Provide or build a VSDecoder based on a provided configuration.
309     *
310     * @param config previous configuration, not null.
311     * @return vsdecoder, or null on error.
312     */
313    public VSDecoder getVSDecoder(VSDConfig config) {
314        String path;
315        String profile_name = config.getProfileName();
316        // First, check to see if we already have a VSDecoder on this Address
317        if (decoderAddressMap.containsKey(config.getLocoAddress().toString())) {
318            return decoderAddressMap.get(config.getLocoAddress().toString());
319        }
320        if (profiletable.containsKey(profile_name)) {
321            path = profiletable.get(profile_name);
322            log.debug("Profile {} is in table.  Path: {}", profile_name, path);
323
324            config.setVSDPath(path);
325            config.setId(getNextVSDecoderID());
326            VSDecoder vsd = new VSDecoder(config);
327            decodertable.put(vsd.getId(), vsd);
328            decoderAddressMap.put(vsd.getAddress().toString(), vsd);
329            decoderInBlock.put(vsd.getAddress().getNumber(), vsd);
330            locoInBlock[getNextlocorow()][ADDRESS] = vsd.getAddress().getNumber();
331
332            if (vsd.isEnabled()) {
333                // set volume for this decoder
334                vsd.setDecoderVolume(vsd.getDecoderVolume());
335
336                if (geofile_ok) {
337                    if (vsd.topspeed == 0) {
338                        log.info("Top-speed not defined. No advanced location following possible.");
339                    } else {
340                        initSoundPositionTimer();
341                    }
342                }
343                return vsd;
344            } else {
345                deleteDecoder(vsd.getAddress().toString());
346                return null;
347            }
348        } else {
349            // Don't have enough info to try to load from file.
350            log.error("Requested profile not loaded: {}", profile_name);
351            return null;
352        }
353    }
354
355    /**
356     * Get a VSDecoder by its Id.
357     *
358     * @param id The Id of the VSDecoder
359     * @return vsdecoder, or null on error.
360     */
361    public VSDecoder getVSDecoderByID(String id) {
362        VSDecoder v = decodertable.get(id);
363        if (v == null) {
364            log.debug("No decoder in table! ID: {}", id);
365        }
366        return decodertable.get(id);
367    }
368
369    /**
370     * Get a VSDecoder by its address.
371     *
372     * @param sa The address of the VSDecoder
373     * @return vsdecoder, or null on error.
374     */
375    public VSDecoder getVSDecoderByAddress(String sa) {
376        if (sa == null) {
377            log.debug("Decoder Address is Null");
378            return null;
379        }
380        log.debug("Decoder Address: {}", sa);
381        VSDecoder rv = decoderAddressMap.get(sa);
382        if (rv == null) {
383            log.debug("Not found.");
384        } else {
385            log.debug("Found: {}", rv.getAddress());
386        }
387        return rv;
388    }
389
390    /**
391     * Get a list of all profiles.
392     *
393     * @return sl The profiles list.
394     */
395    public ArrayList<String> getVSDProfileNames() {
396        ArrayList<String> sl = new ArrayList<>();
397        for (String p : profiletable.keySet()) {
398            sl.add(p);
399        }
400        return sl;
401    }
402
403    /**
404     * Get a list of all VSDecoders.
405     *
406     * @return the VSDecoder list.
407     */
408    public Collection<VSDecoder> getVSDecoderList() {
409        return decodertable.values();
410    }
411
412    /**
413     * Get the VSD listener system name.
414     *
415     * @return the system name.
416     */
417    public String getDefaultListenerName() {
418        return VSDListener.ListenerSysName;
419    }
420
421    /**
422     * Get the VSD listener location.
423     *
424     * @return the location or null.
425     */
426    public ListeningSpot getDefaultListenerLocation() {
427        VSDListener l = listenerTable.get(getDefaultListenerName());
428        if (l != null) {
429            return l.getLocation();
430        } else {
431            return null;
432        }
433    }
434
435    public void setListenerLocation(String id, ListeningSpot sp) {
436        VSDListener l = listenerTable.get(id);
437        log.debug("Set listener location {} listener: {}", sp, l);
438        if (l != null) {
439            l.setLocation(sp);
440        }
441    }
442
443    public void setDecoderPositionByID(String id, PhysicalLocation p) {
444        VSDecoder d = decodertable.get(id);
445        if (d != null) {
446            d.setPosition(p);
447        }
448    }
449
450    public void setDecoderPositionByAddr(LocoAddress a, PhysicalLocation l) {
451        // Find the addressed decoder
452        // This is a bit hokey.  Need a better way to index decoder by address
453        // OK, this whole LocoAddress vs. DccLocoAddress thing has rendered this SUPER HOKEY.
454        if (a == null) {
455            log.warn("Decoder Address is Null");
456            return;
457        }
458        if (l == null) {
459            log.warn("PhysicalLocation is Null");
460            return;
461        }
462        if (l.equals(PhysicalLocation.Origin)) {
463            log.info("Location: {} ... ignoring", l);
464            // Physical location at origin means it hasn't been set.
465            return;
466        }
467        log.debug("Decoder Address: {}", a.getNumber());
468        for (VSDecoder d : decodertable.values()) {
469            // Get the Decoder's address protocol.  If it's a DCC_LONG or DCC_SHORT, convert to DCC
470            // since the LnReporter can't tell the difference and will always report "DCC".
471            if (d == null) {
472                log.debug("VSdecoder null pointer!");
473                return;
474            }
475            LocoAddress pa = d.getAddress();
476            if (pa == null) {
477                log.info("Vsdecoder {} address null!", d);
478                return;
479            }
480            LocoAddress.Protocol p = d.getAddress().getProtocol();
481            if (p == null) {
482                log.debug("Vsdecoder {} address = {} protocol null!", d, pa);
483                return;
484            }
485            if ((p == LocoAddress.Protocol.DCC_LONG) || (p == LocoAddress.Protocol.DCC_SHORT)) {
486                p = LocoAddress.Protocol.DCC;
487            }
488            if ((d.getAddress().getNumber() == a.getNumber()) && (p == a.getProtocol())) {
489                d.setPosition(l);
490                // Loop through all the decoders (assumes N will be "small"), in case
491                // there are multiple decoders with the same address.  This will be somewhat broken
492                // if there's a DCC_SHORT and a DCC_LONG decoder with the same address number.
493                //return;
494            }
495        }
496        // decoder not found.  Do nothing.
497        return;
498    }
499
500    // VSDecoderManager Events
501    public void addEventListener(VSDManagerListener listener) {
502        listenerList.add(VSDManagerListener.class, listener);
503    }
504
505    public void removeEventListener(VSDManagerListener listener) {
506        listenerList.remove(VSDManagerListener.class, listener);
507    }
508
509    void fireMyEvent(VSDManagerEvent evt) {
510        //Object[] listeners = listenerList.getListenerList();
511
512        for (VSDManagerListener l : listenerList.getListeners(VSDManagerListener.class)) {
513            l.eventAction(evt);
514        }
515    }
516
517    /**
518     * Retrieve the Path for a given Profile name.
519     *
520     * @param profile the profile to get the path for
521     * @return the path for the profile
522     */
523    public String getProfilePath(String profile) {
524        return profiletable.get(profile);
525    }
526
527    protected void registerReporterListener(String sysName) {
528        Reporter r = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getReporter(sysName);
529        if (r == null) {
530            return;
531        }
532        jmri.NamedBeanHandle<Reporter> h = nbhm.getNamedBeanHandle(sysName, r);
533
534        // Make sure we aren't already registered.
535        java.beans.PropertyChangeListener[] ll = r.getPropertyChangeListenersByReference(h.getName());
536        if (ll.length == 0) {
537            r.addPropertyChangeListener(this, h.getName(), vsd_property_change_name);
538        }
539    }
540
541    protected void registerBeanListener(Manager<Block> beanManager, String sysName) {
542        NamedBean b = beanManager.getBySystemName(sysName);
543        if (b == null) {
544            log.debug("No bean by name {}", sysName);
545            return;
546        }
547        jmri.NamedBeanHandle<NamedBean> h = nbhm.getNamedBeanHandle(sysName, b);
548
549        // Make sure we aren't already registered.
550        java.beans.PropertyChangeListener[] ll = b.getPropertyChangeListenersByReference(h.getName());
551        if (ll.length == 0) {
552            b.addPropertyChangeListener(this, h.getName(), vsd_property_change_name);
553            log.debug("Added listener to bean {} type {}", b.getDisplayName(), b.getClass().getName());
554        }
555    }
556
557    protected void registerReporterListeners() {
558        // Walk through the list of reporters
559        Set<Reporter> reporterSet = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet();
560        for (Reporter r : reporterSet) {
561            if (r != null) {
562                registerReporterListener(r.getSystemName());
563            }
564        }
565
566        Set<Block> blockSet = jmri.InstanceManager.getDefault(jmri.BlockManager.class).getNamedBeanSet();
567        for (Block b : blockSet) {
568            if (b != null) {
569                registerBeanListener(jmri.InstanceManager.getDefault(jmri.BlockManager.class), b.getSystemName());
570            }
571        }
572    }
573
574    // This listener listens to the ReporterManager for changes to the list of Reporters.
575    // Need to trap list length (name="length") changes and add listeners when new ones are added.
576    private void setupReporterManagerListener() {
577        // Register ourselves as a listener for changes to the Reporter list.  For now, we won't do this. Just force a
578        // save and reboot after reporters are added.  We'll fix this later.
579        // jmri.InstanceManager.getDefault(jmri.ReporterManager.class).addPropertyChangeListener(new PropertyChangeListener() {
580        // public void propertyChange(PropertyChangeEvent event) {
581        //      log.debug("property change name {}, old: {}, new: {}", event.getPropertyName(), event.getOldValue(), event.getNewValue());
582        //     reporterManagerPropertyChange(event);
583        // }
584        //   });
585        jmri.InstanceManager.getDefault(jmri.ReporterManager.class).addPropertyChangeListener(this);
586
587        // Now, the Reporter Table might already be loaded and filled out, so we need to get all the Reporters and list them.
588        // And add ourselves as a listener to them.
589        Set<Reporter> reporterSet = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet();
590        for (Reporter r : reporterSet) {
591            if (r != null) {
592                registerReporterListener(r.getSystemName());
593            }
594        }
595
596        Set<Block> blockSet = jmri.InstanceManager.getDefault(jmri.BlockManager.class).getNamedBeanSet();
597        for (Block b : blockSet) {
598            if (b != null) {
599                registerBeanListener(jmri.InstanceManager.getDefault(jmri.BlockManager.class), b.getSystemName());
600            }
601        }
602    }
603
604    /**
605     * Delete a VSDecoder
606     *
607     * @param address The DCC address of the VSDecoder
608     */
609    public void deleteDecoder(String address) {
610        log.debug("delete Decoder called, VSDecoder DCC address: {}", address);
611        if (this.getVSDecoderByAddress(address) == null) {
612            log.warn("VSDecoder not found");
613        } else {
614            removeVSDecoder(address);
615        }
616    }
617
618    private void removeVSDecoder(String sa) {
619        VSDecoder d = this.getVSDecoderByAddress(sa);
620        jmri.InstanceManager.getDefault(jmri.ThrottleManager.class).removeListener(d.getAddress(), d);
621        // sound position timer is based on GeoFile
622        if (geofile_ok && getVSDecoderList().size() == 1) {
623            // last VSDecoder
624            stopSoundPositionTimer();
625            timer = null;
626        }
627        d.shutdown();
628        d.disable();
629
630        decodertable.remove(d.getId());
631        decoderAddressMap.remove(sa);
632        currentBlock.remove(d);
633        decoderInBlock.remove(d.getAddress().getNumber());
634        locoInBlockRemove(d.getAddress().getNumber());
635
636        locorow--; // prepare array index for eventually adding a new decoder
637
638        d.sound_list.clear();
639        d.event_list.clear();
640
641        jmri.AudioManager am = jmri.InstanceManager.getDefault(jmri.AudioManager.class);
642        ArrayList<Audio> sources = new ArrayList<>(am.getNamedBeanSet(Audio.SOURCE));
643        ArrayList<Audio> buffers = new ArrayList<>(am.getNamedBeanSet(Audio.BUFFER));
644        for (Audio source: sources) {
645            if (source.getSystemName().contains(d.getId())) {
646                source.dispose();
647            }
648        }
649        for (Audio buffer: buffers) {
650            if (buffer.getSystemName().contains(d.getId())) {
651                buffer.dispose();
652            }
653        }
654        log.info("New number of buffers used after deletion: {}, max: {}",
655                am.getNamedBeanSet(jmri.Audio.BUFFER).size(), jmri.AudioManager.MAX_BUFFERS);
656    }
657
658    /**
659     * Prepare the start of a VSDecoder on the layout
660     *
661     * @param blk The current Block of the VSDecoder
662     */
663    public void atStart(Block blk) {
664        // blk could be the start block or a current block for an existing VSDecoder
665        int locoAddress = getLocoAddr(blk);
666        if (locoAddress != 0) {
667            // look for an existing and configured VSDecoder
668            if (decoderInBlock.containsKey(locoAddress)) {
669                VSDecoder d = decoderInBlock.get(locoAddress);
670                if (geofile_ok) {
671                    if (alf_version == 2 && blockList.contains(blk)) {
672                        handleAlf2(d, locoAddress, blk);
673                    } else {
674                        log.debug("Block {} not valid for panel {}", blk, d.getModels());
675                    }
676                } else {
677                    d.getEngineSound().setTunnel(blk.getPhysicalLocation().isTunnel());
678                    d.setPosition(blk.getPhysicalLocation());
679                }
680            } else {
681                log.warn("Block value \"{}\" is not a valid VSDecoder address", blk.getValue());
682            }
683        }
684    }
685
686    /**
687     * Get the loco address from a Block
688     *
689     * @param blk The current Block of the VSDecoder
690     * @return The number of the loco address
691     */
692    public int getLocoAddr(Block blk) {
693        if (blk == null || blk.getValue() == null) {
694            return 0;
695        }
696
697        var blkVal = blk.getValue();
698        int locoAddress = 0;
699
700        // handle different formats or objects to get the address
701        if (blkVal instanceof String) {
702            String val = blkVal.toString();
703            RosterEntry entry = Roster.getDefault().getEntryForId(val);
704            if (entry != null) {
705                locoAddress = Integer.parseInt(entry.getDccAddress()); // numeric RosterEntry Id
706            } else if (org.apache.commons.lang3.StringUtils.isNumeric(val)) {
707                locoAddress = Integer.parseInt(val);
708            } else if (jmri.InstanceManager.getDefault(TrainManager.class).getTrainByName(val) != null) {
709                // Operations Train
710                Train selected_train = jmri.InstanceManager.getDefault(TrainManager.class).getTrainByName(val);
711                if (selected_train.getLeadEngineDccAddress().isEmpty()) {
712                    locoAddress = 0;
713                } else {
714                    locoAddress = Integer.parseInt(selected_train.getLeadEngineDccAddress());
715                }
716            }
717        } else if (blkVal instanceof jmri.BasicRosterEntry) {
718            locoAddress = Integer.parseInt(((RosterEntry) blkVal).getDccAddress());
719        } else if (blkVal instanceof jmri.implementation.DefaultIdTag) {
720            // Covers TranspondingTag also
721            String val = ((DefaultIdTag) blkVal).getTagID();
722            if (org.apache.commons.lang3.StringUtils.isNumeric(val)) {
723                locoAddress = Integer.parseInt(val);
724            }
725        } else {
726            log.warn("Block Value \"{}\" found - unsupported object!", blkVal);
727        }
728        log.debug("loco address: {}", locoAddress);
729        return locoAddress;
730    }
731
732    @Override
733    public void propertyChange(PropertyChangeEvent evt) {
734        log.debug("property change type {} name {} old {} new {}",
735                evt.getSource().getClass().getName(), evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
736        if (evt.getSource() instanceof jmri.ReporterManager) {
737            reporterManagerPropertyChange(evt);
738        } else if (evt.getSource() instanceof jmri.Reporter) {
739            reporterPropertyChange(evt); // Location Following
740        } else if (evt.getSource() instanceof jmri.Block) {
741            log.debug("Block property change! name: {} old: {} new = {}", evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
742            blockPropertyChange(evt);
743        } else if (evt.getSource() instanceof VSDManagerFrame) {
744            if (evt.getPropertyName().equals(VSDManagerFrame.REMOVE_DECODER)) {
745                // Shut down the requested decoder and remove it from the manager's hash maps.
746                // Unless there are "illegal" handles, this should put the decoder on the garbage heap.  I think.
747                removeVSDecoder((String) evt.getOldValue());
748            } else if (evt.getPropertyName().equals(VSDManagerFrame.CLOSE_WINDOW)) {
749                // Note this assumes there is only one VSDManagerFrame open at a time.
750                stopSoundPositionTimer();
751                timer = null;
752                if (managerFrame != null) {
753                    managerFrame = null;
754                }
755            }
756        } else {
757            // Un-Handled source. Does nothing ... yet...
758        }
759        return;
760    }
761
762    public void blockPropertyChange(PropertyChangeEvent event) {
763        // Needs to check the ID on the event, look up the appropriate VSDecoder,
764        // get the location of the event source, and update the decoder's location.
765        String eventName = event.getPropertyName();
766        if (event.getSource() instanceof PhysicalLocationReporter) {
767            Block blk = (Block) event.getSource();
768            String repVal = null;
769            // Depending on the type of Block Event, extract the needed report info from
770            // the appropriate place...
771            // "state" => Get loco address from Block's Reporter if present
772            // "value" => Get loco address from event's newValue.
773            if ("state".equals(eventName)) { // NOI18N
774                // Need to decide which reporter it is, so we can use different methods
775                // to extract the address and the location.
776                if ((Integer) event.getNewValue() == Block.OCCUPIED) {
777                    // Is there a Block's Reporter?
778                    var blockReporter = blk.getReporter();
779                    if ( blockReporter == null) {
780                        log.debug("Block {} has no reporter!  Skipping state-type report", blk.getSystemName());
781                        return;
782                    }
783                    // Get this Block's Reporter's current/last report value
784                    if (blk.isReportingCurrent()) {
785                        Object currentReport = blockReporter.getCurrentReport();
786                        if ( currentReport != null) {
787                            if(currentReport instanceof jmri.Reportable) {
788                                repVal = ((jmri.Reportable)currentReport).toReportString();
789                            } else {
790                                repVal = currentReport.toString();
791                            }
792                        }
793                    } else {
794                        Object lastReport = blockReporter.getLastReport();
795                        if ( lastReport != null) {
796                            if(lastReport instanceof jmri.Reportable) {
797                                repVal = ((jmri.Reportable)lastReport).toReportString();
798                            } else {
799                                repVal = lastReport.toString();
800                            }
801                        }
802                    }
803                } else {
804                    log.debug("Ignoring report. not an OCCUPIED event.");
805                    return;
806                }
807                log.debug("block repVal: {}", repVal);
808            } else if ("value".equals(eventName)) { // NOI18N
809                if (event.getNewValue() == null ) {
810                    return; // block value was cleared, nothing to do
811                }
812                atStart(blk);
813            } else {
814                log.debug("Not a supported Block event type.  Ignoring.");
815                return;
816            }
817
818            // Set the decoder's position due to the report.
819            if (repVal == null) {
820                log.debug("Report from Block {} is null!", blk.getSystemName());
821            }
822            if (repVal != null && blk.getDirection(repVal) == PhysicalLocationReporter.Direction.ENTER) {
823                setDecoderPositionByAddr(blk.getLocoAddress(repVal), blk.getPhysicalLocation());
824            }
825
826        } else {
827            log.debug("Reporter doesn't support physical location reporting.");
828        }
829
830    }
831
832    public void reporterPropertyChange(PropertyChangeEvent event) {
833        // Needs to check the ID on the event, look up the appropriate VSDecoder,
834        // get the location of the event source, and update the decoder's location.
835        String eventName = event.getPropertyName();
836        if (lf_version == 1 || (geofile_ok && alf_version == 1)) {
837            if ((event.getSource() instanceof PhysicalLocationReporter) && (eventName.equals("currentReport"))) { // NOI18N
838                PhysicalLocationReporter arp = (PhysicalLocationReporter) event.getSource();
839                // Need to decide which reporter it is, so we can use different methods
840                // to extract the address and the location.
841                if (event.getNewValue() instanceof IdTag) {
842                    // RFID-tag, Digitrax Transponding tags, RailCom tags
843                    if (event.getNewValue() instanceof jmri.jmrix.loconet.TranspondingTag) {
844                        String repVal = ((jmri.Reportable) event.getNewValue()).toReportString();
845                        int locoAddress = arp.getLocoAddress(repVal).getNumber();
846                        log.debug("Reporter repVal: {}, number: {}", repVal, locoAddress);
847                        // Check: is loco address valid?
848                        if (decoderInBlock.containsKey(locoAddress)) {
849                            VSDecoder d = decoderInBlock.get(locoAddress);
850                            // look for additional geometric layout information
851                            if (geofile_ok) {
852                                Reporter rp = (Reporter) event.getSource();
853                                int new_rp = 0;
854                                try {
855                                    new_rp = Integer.parseInt(Manager.getSystemSuffix(rp.getSystemName()));
856                                } catch (java.lang.NumberFormatException e) {
857                                    log.warn("Invalid Reporter system name '{}'", rp.getSystemName());
858                                }
859                                // Check: Reporter must be valid for GeoData processing
860                                //    use the current Reporter list as a filter (changeable by a Train selection)
861                                if (reporterlists.get(d.setup_index).contains(new_rp)) {
862                                    if (arp.getDirection(repVal) == PhysicalLocationReporter.Direction.ENTER) {
863                                        handleAlf(d, locoAddress, new_rp); // Advanced Location Following version 1
864                                    }
865                                } else {
866                                    log.info("Reporter {} not valid for {} setup {}", new_rp, VSDGeoFile.VSDGeoDataFileName, d.setup_index + 1);
867                                }
868                            } else {
869                                if (arp.getDirection(repVal) == PhysicalLocationReporter.Direction.ENTER) {
870                                    d.getEngineSound().setTunnel(arp.getPhysicalLocation(repVal).isTunnel());
871                                    d.setPosition(arp.getPhysicalLocation(repVal));
872                                    log.debug("position set to: {}", arp.getPhysicalLocation(repVal));
873                                }
874                            }
875                        } else {
876                            log.info(" decoder address {} is not valid!", locoAddress);
877                        }
878                        return;
879                    } else {
880                        // newValue is of IdTag type.
881                        // Dcc4Pc, Ecos,
882                        // Assume Reporter "arp" is the most recent seen location
883                        IdTag newValue = (IdTag) event.getNewValue();
884                        decoderInBlock.get(arp.getLocoAddress(newValue.getTagID()).getNumber()).getEngineSound().setTunnel(arp.getPhysicalLocation(null).isTunnel());
885                        setDecoderPositionByAddr(arp.getLocoAddress(newValue.getTagID()), arp.getPhysicalLocation(null));
886                    }
887                } else {
888                    log.info("Reporter's return type is not supported.");
889                }
890            } else {
891                log.debug("Reporter doesn't support physical location reporting or isn't reporting new info.");
892            }
893        }
894        return;
895    }
896
897    public void reporterManagerPropertyChange(PropertyChangeEvent event) {
898        String eventName = event.getPropertyName();
899
900        log.debug("VSDecoder received Reporter Manager Property Change: {}", eventName);
901        if (eventName.equals("length")) { // NOI18N
902
903            // Re-register for all the reporters. The registerReporterListener() will skip
904            // any that we're already registered for.
905            for (Reporter r : jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet()) {
906                registerReporterListener(r.getSystemName());
907            }
908
909            // It could be that we lost a Reporter.  But since we aren't keeping a list anymore
910            // we don't care.
911        }
912    }
913
914    // handle Advanced Location Following version 1
915    private void handleAlf(VSDecoder d, int locoAddress, int new_rp) {
916        int new_rp_index = reporterlists.get(d.setup_index).indexOf(new_rp);
917        int old_rp = -1; // set to "undefined"
918        int old_rp_index = -1; // set to "undefined"
919        int ix = getArrayIndex(locoAddress);
920        if (ix < locoInBlock.length) {
921            old_rp = locoInBlock[ix][BLOCK];
922            if (old_rp == 0) old_rp = -1; // set to "undefined"
923            old_rp_index = reporterlists.get(d.setup_index).indexOf(old_rp); // -1 if not found (undefined)
924        } else {
925            log.warn(" Array locoInBlock INDEX {} IS NOT VALID! Set to 0.", ix);
926            ix = 0;
927        }
928        log.debug("new_rp: {}, old_rp: {}, new index: {}, old index: {}", new_rp, old_rp, new_rp_index, old_rp_index);
929        // Validation check: don't proceed when it's the same reporter
930        if (new_rp != old_rp) {
931            // Validation check: reporter must be a new or a neighbour reporter or must rotating in a circle
932            int lastrepix = reporterlists.get(d.setup_index).size() - 1; // Get the index of the last Reporter
933            if ((old_rp == -1) // Loco can be in any section, if it's the first reported section; old rp is "undefined"
934                    || (old_rp_index + d.dirfn == new_rp_index) // Loco is running forward or reverse
935                    || (circlelist.get(d.setup_index) && d.dirfn == -1 && old_rp_index == 0 && new_rp_index == lastrepix) // Loco is running reverse and circling
936                    || (circlelist.get(d.setup_index) && d.dirfn ==  1 && old_rp_index == lastrepix && new_rp_index == 0)) { // Loco is running forward and circling
937                // Validation check: OK
938                locoInBlock[ix][BLOCK] = new_rp; // Set new block number (int)
939                log.debug(" distance rest (old) to go in block {}: {} cm", old_rp, locoInBlock[ix][DISTANCE_TO_GO]);
940                locoInBlock[ix][DISTANCE_TO_GO] = Math.round(blockParameter[d.setup_index][new_rp_index][LENGTH] * 100.0f); // block distance init: block length in cm
941                log.debug(" distance rest (new) to go in block {}: {} cm", new_rp, locoInBlock[ix][DISTANCE_TO_GO]);
942                // get the new sound position point (depends on the loco traveling direction)
943                if (d.dirfn == 1) {
944                    d.posToSet = blockPositionlists.get(d.setup_index).get(new_rp_index); // Start position
945                } else {
946                    d.posToSet = blockPositionlists.get(d.setup_index).get(new_rp_index + 1); // End position
947                }
948                if (old_rp == -1 && d.startPos != null) { // Special case start position: first choice; if found, overwrite it.
949                    d.posToSet = d.startPos;
950                }
951                d.getEngineSound().setTunnel(blockPositionlists.get(d.setup_index).get(new_rp_index).isTunnel()); // set the tunnel status
952                log.debug("address {}: position to set: {}", d.getAddress(), d.posToSet);
953                d.setPosition(d.posToSet); // Sound set position
954                changeDirection(d, locoAddress, new_rp_index);
955            } else {
956                log.info(" Validation failed! Last reporter: {}, new reporter: {}, dirfn: {} for {}", old_rp, new_rp, d.dirfn, locoAddress);
957            }
958        } else {
959            log.info(" Same PhysicalLocationReporter, position not set!");
960        }
961    }
962
963    // handle Advanced Location Following version 2
964    private void handleAlf2(VSDecoder d, int locoAddress, Block newBlock) {
965        if (currentBlock.get(d) != newBlock) {
966            int ix = getArrayIndex(locoAddress); // ix = decoder number 0 - max_decoder-1
967            if (locoInBlock[ix][DIR_FN] == 0) { // at start
968                if (d.getLayoutTrack() == null) {
969                    if (possibleStartBlocks.get(newBlock) != null) {
970                        d.setModels(possibleStartBlocks.get(newBlock)); // get the models from the HashMap via block
971                        log.debug("Block: {}, models: {}", newBlock, d.getModels());
972                        TrackSegment ts = null;
973                        for (LayoutTrack lt : d.getModels().getLayoutTracks()) {
974                            if (lt instanceof TrackSegment) {
975                                ts = (TrackSegment) lt;
976                                if (ts.getLayoutBlock() != null && ts.getLayoutBlock().getBlock() == newBlock) {
977                                    break;
978                                }
979                            }
980                        }
981                        if (ts != null) {
982                            TrackSegmentView tsv = d.getModels().getTrackSegmentView(ts);
983                            d.setLayoutTrack(ts);
984                            d.setReturnTrack(d.getLayoutTrack());
985                            d.setReturnLastTrack(tsv.getConnect2());
986                            d.setLastTrack(tsv.getConnect1());
987                            d.setReturnDistance(MathUtil.distance(d.getModels().getCoords(tsv.getConnect1(), tsv.getType1()),
988                                    d.getModels().getCoords(tsv.getConnect2(), tsv.getType2())));
989                            d.setDistance(0);
990                            d.distanceOnTrack = 0.5d * d.getReturnDistance(); // halved to get starting position (mid or centre of the track)
991                            if (d.dirfn == -1) { // in case the loco is running in reverse direction
992                                d.setLayoutTrack(d.getReturnTrack());
993                                d.setLastTrack(d.getReturnLastTrack());
994                            }
995                            locoInBlock[ix][DIR_FN] = d.dirfn;
996                            currentBlock.put(d, newBlock);
997                            // prepare navigation
998                            d.posToSet = new PhysicalLocation(0.0f, 0.0f, 0.0f);
999                            log.info("at start - TS: {}, block: {}, loco: {}, panel: {}", ts.getName(), newBlock, locoAddress, d.getModels().getTitle());
1000                        }
1001                    } else {
1002                        log.warn("block {} is not a valid start block; valid start blocks are: {}", newBlock, possibleStartBlocks);
1003                    }
1004                }
1005
1006            } else {
1007
1008                currentBlock.put(d, newBlock);
1009                // new block; if end point is already reached, d.distanceOnTrack is zero
1010                if (d.distanceOnTrack > 0) {
1011                    // it's still on this track
1012                    // handle a block change, if the loco reaches the next block before the calculated end
1013                    boolean result = true; // new block, so go to the next track
1014                    d.distanceOnTrack = 0;
1015                    // go to next track
1016                    LayoutTrack last = d.getLayoutTrack();
1017                    if (d.getLayoutTrack() instanceof TrackSegment) {
1018                        TrackSegmentView tsv = d.getModels().getTrackSegmentView((TrackSegment) d.getLayoutTrack());
1019                        log.debug(" true - layout track: {}, last track: {}, connect1: {}, connect2: {}, last block: {}",
1020                                d.getLayoutTrack().getName(), d.getLastTrack().getName(), tsv.getConnect1(), tsv.getConnect2(), tsv.getBlockName());
1021                        if (tsv.getConnect1().equals(d.getLastTrack())) {
1022                            d.setLayoutTrack(tsv.getConnect2());
1023                        } else if (tsv.getConnect2().equals(d.getLastTrack())) {
1024                            d.setLayoutTrack(tsv.getConnect1());
1025                        } else { // OOPS! we're lost!
1026                            log.info(" TS lost, c1: {}, c2: {}, last track: {}", tsv.getConnect1(), tsv.getConnect2(), d.getLastTrack());
1027                            result = false;
1028                        }
1029                        if (result) {
1030                            d.setLastTrack(last);
1031                            d.setReturnTrack(d.getLayoutTrack());
1032                            d.setReturnLastTrack(d.getLayoutTrack());
1033                            log.debug(" next track (layout track): {}, last track: {}", d.getLayoutTrack(), d.getLastTrack());
1034                        }
1035                    } else if (d.getLayoutTrack() instanceof LayoutTurnout
1036                            || d.getLayoutTrack() instanceof LayoutSlip
1037                            || d.getLayoutTrack() instanceof LevelXing
1038                            || d.getLayoutTrack() instanceof LayoutTurntable) {
1039                        // go to next track
1040                        if (d.nextLayoutTrack != null) {
1041                            d.setLayoutTrack(d.nextLayoutTrack);
1042                        } else { // OOPS! we're lost!
1043                            result = false;
1044                        }
1045                        if (result) {
1046                            d.setLastTrack(last);
1047                            d.setReturnTrack(d.getLayoutTrack());
1048                            d.setReturnLastTrack(d.getLayoutTrack());
1049                        }
1050                    }
1051                }
1052            }
1053        } else {
1054           log.warn(" Same PhysicalLocationReporter, position not set!");
1055        }
1056    }
1057
1058    private void changeDirection(VSDecoder d, int locoAddress, int new_rp_index) {
1059        PhysicalLocation point1 = blockPositionlists.get(d.setup_index).get(new_rp_index);
1060        PhysicalLocation point2 = blockPositionlists.get(d.setup_index).get(new_rp_index + 1);
1061        Point2D coords1 = new Point2D.Double(point1.x, point1.y);
1062        Point2D coords2 = new Point2D.Double(point2.x, point2.y);
1063        int direct;
1064        if (d.dirfn == 1) {
1065            direct = Path.computeDirection(coords1, coords2);
1066        } else {
1067            direct = Path.computeDirection(coords2, coords1);
1068        }
1069        locoInBlock[getArrayIndex(locoAddress)][DIRECTION] = direct;
1070        log.debug("direction: {} ({})", Path.decodeDirection(direct), direct);
1071    }
1072
1073    /**
1074     * Get index of a decoder.
1075     * @param number The loco address number.
1076     * @return the index of a decoder's loco address number
1077     *         in the array or the length of the array.
1078     */
1079    public int getArrayIndex(int number) {
1080        for (int i = 0; i < locoInBlock.length; i++) {
1081            if (locoInBlock[i][ADDRESS] == number) {
1082                return i;
1083            }
1084        }
1085        return locoInBlock.length;
1086    }
1087
1088    public void locoInBlockRemove(int numb) {
1089        // Works only for <locoInBlock.length> rows
1090        //  find index first
1091        int remove_index = 0;
1092        for (int i = 0; i < locoInBlock.length; i++) {
1093            if (locoInBlock[i][ADDRESS] == numb) {
1094                remove_index = i;
1095            }
1096        }
1097        for (int i = remove_index; i < locoInBlock.length - 1; i++) {
1098            for (int k = 0; k < locoInBlock[i].length; k++) {
1099                locoInBlock[i][k] = locoInBlock[i + 1][k];
1100            }
1101        }
1102        // Delete last row
1103        int il = locoInBlock.length - 1;
1104        for (int k = 0; k < locoInBlock[il].length; k++) {
1105            locoInBlock[il][k] = 0;
1106        }
1107    }
1108
1109    public void loadProfiles(VSDFile vf) {
1110        Element root;
1111        String pname;
1112        root = vf.getRoot();
1113        if (root == null) {
1114            return;
1115        }
1116
1117        ArrayList<String> new_entries = new ArrayList<>();
1118
1119        java.util.Iterator<Element> i = root.getChildren("profile").iterator(); // NOI18N
1120        while (i.hasNext()) {
1121            Element e = i.next();
1122            pname = e.getAttributeValue("name");
1123            log.debug("Profile name: {}", pname);
1124            if ((pname != null) && !(pname.isEmpty())) { // NOI18N
1125                profiletable.put(pname, vf.getName());
1126                new_entries.add(pname);
1127            }
1128        }
1129
1130        if (!GraphicsEnvironment.isHeadless()) {
1131            fireMyEvent(new VSDManagerEvent(this, VSDManagerEvent.EventType.PROFILE_LIST_CHANGE, new_entries));
1132        }
1133    }
1134
1135    void initSoundPositionTimer() {
1136        if (timer == null) {
1137            timer = new Timer(check_time, new ActionListener() {
1138                @Override
1139                public void actionPerformed(ActionEvent e) {
1140                    for (VSDecoder d : getVSDecoderList()) {
1141                        if (alf_version == 1) {
1142                            calcNewPosition(d);
1143                        } else if (alf_version == 2 && d.getLayoutTrack() != null) {
1144                            int ix = getArrayIndex(d.getAddress().getNumber()); // ix = decoder number 0 - max_decoder-1
1145                            float actualspeed = d.getEngineSound().getActualSpeed();
1146                            if (locoInBlock[ix][DIR_FN] != d.dirfn) {
1147                                // traveling direction has changed
1148                                if (d.getEngineSound().isEngineStarted()) {
1149                                    locoInBlock[ix][DIR_FN] = d.dirfn; // save traveling direction info
1150                                    if (d.distanceOnTrack <= d.getReturnDistance()) {
1151                                        d.distanceOnTrack = d.getReturnDistance() - d.distanceOnTrack;
1152                                    } else {
1153                                        d.distanceOnTrack = d.getReturnDistance();
1154                                    }
1155                                    d.setLayoutTrack(d.getReturnTrack());
1156                                    d.setLastTrack(d.getReturnLastTrack());
1157                                    log.debug("direction changed to {}, layout: {}, last: {}, return: {}, d.getReturnDistance: {}, d.distanceOnTrack: {}, d.getDistance: {}",
1158                                            d.dirfn, d.getLayoutTrack(), d.getLastTrack(), d.getReturnTrack(), d.getReturnDistance(), d.distanceOnTrack, d.getDistance());
1159                                    d.setDistance(0);
1160                                }
1161                            }
1162                            if ((d.getEngineSound().isEngineStarted() && actualspeed > 0.0f) || d.getLayoutTrack() instanceof LayoutTurntable) {
1163                                float speed_ms = actualspeed * (d.dirfn == 1 ? d.topspeed : d.topspeed_rev) * 0.44704f / layout_scale; // calculate the speed
1164                                d.setDistance(d.getDistance() + speed_ms * check_time / 10.0); // d.getDistance() normally is 0, but can content an overflow
1165                                d.navigate();
1166                                if (d.getLocation() != null) {
1167                                    Point2D loc = d.getLocation();
1168                                    Point2D loc2 = new Point2D.Double(((float) loc.getX() - models_origin.x) * 0.01f, (models_origin.y - (float) loc.getY()) * 0.01f);
1169                                    d.posToSet.x = (float) loc2.getX();
1170                                    d.posToSet.y = (float) loc2.getY();
1171                                    d.posToSet.z = 0.0f;
1172                                    log.debug("address {} position to set: {}, location: {}", d.getAddress(), d.posToSet, loc);
1173                                    d.setPosition(d.posToSet);
1174                                }
1175                            }
1176                        }
1177                    }
1178                }
1179            });
1180            timer.setRepeats(true);
1181            timer.setInitialDelay(check_time);
1182            timer.start();
1183            log.debug("timer {} started, check time: {}", timer, check_time);
1184        }
1185    }
1186
1187    void stopSoundPositionTimer() {
1188        if (timer != null) {
1189            if (timer.isRunning()) {
1190                timer.stop();
1191            } else {
1192                log.debug("timer {} was not running", timer);
1193            }
1194        }
1195    }
1196
1197    // Simple way to calulate loco positions within a block
1198    //  train route is described by a combination of two types of geometric elements: line track or curve track
1199    //  the train route data is provided by a xml file and gathered by method getBlockValues
1200    public void calcNewPosition(VSDecoder d) {
1201        float actualspeed = d.getEngineSound().getActualSpeed();
1202        if (actualspeed > 0.0f && d.topspeed > 0) { // proceed only, if the loco is running and if a topspeed value is available
1203            int dadr = d.getAddress().getNumber();
1204            int dadr_index = getArrayIndex(dadr); // check, if the decoder is in "Block status for locos" - remove this check?
1205            if (dadr_index < locoInBlock.length) {
1206                // decoder is valid
1207                int dadr_block = locoInBlock[dadr_index][BLOCK]; // get block number for current decoder/loco
1208                if (reporterlists.get(d.setup_index).contains(dadr_block)) {
1209                    int dadr_block_index = reporterlists.get(d.setup_index).indexOf(dadr_block);
1210                    newPosition = new PhysicalLocation(0.0f, 0.0f, 0.0f, d.getEngineSound().getTunnel());
1211                    // calculate actual speed in meter/second; support topspeed forward or reverse
1212                    // JMRI speed is 0-1; actual speed is speed after speedCurve(float); in steam1 it is calculated from actual RPM; convert MPH to meter/second; regard layout scale
1213                    float speed_ms = actualspeed * (d.dirfn == 1 ? d.topspeed : d.topspeed_rev) * 0.44704f / layout_scale;
1214                    d.distanceMeter = speed_ms * check_time / 1000; // distance in Meter
1215                    if (locoInBlock[dadr_index][DIR_FN] == 0) { // at start
1216                        locoInBlock[dadr_index][DIR_FN] = d.dirfn;
1217                    }
1218                    distance_rest_old = locoInBlock[dadr_index][DISTANCE_TO_GO] / 100.0f; // Distance to go in meter
1219                    if (locoInBlock[dadr_index][DIR_FN] == d.dirfn) { // Last traveling direction
1220                        distance_rest = distance_rest_old;
1221                    } else {
1222                        // traveling direction has changed
1223                        distance_rest = blockParameter[d.setup_index][dadr_block_index][LENGTH] - distance_rest_old;
1224                        locoInBlock[dadr_index][DIR_FN] = d.dirfn;
1225                        changeDirection(d, dadr, dadr_block_index);
1226                        log.debug("direction changed to {}", locoInBlock[dadr_index][DIRECTION]);
1227                    }
1228                    distance_rest_new = distance_rest - d.distanceMeter; // Distance to go in Meter
1229                    log.debug(" distance_rest_old: {}, distance_rest: {}, distance_rest_new: {} (all in Meter)", distance_rest_old, distance_rest, distance_rest_new);
1230                    // Calculate and set sound position only, if loco would be still inside the block
1231                    if (distance_rest_new > 0.0f) {
1232                        // Which geometric element? RADIUS = 0 means "line"
1233                        if (blockParameter[d.setup_index][dadr_block_index][RADIUS] == 0.0f) {
1234                            // Line
1235                            if (locoInBlock[dadr_index][DIRECTION] == Path.SOUTH) {
1236                                newPosition.x = d.lastPos.x;
1237                                newPosition.y = d.lastPos.y - d.distanceMeter;
1238                            } else if (locoInBlock[dadr_index][DIRECTION] == Path.NORTH) {
1239                                newPosition.x = d.lastPos.x;
1240                                newPosition.y = d.lastPos.y + d.distanceMeter;
1241                            } else {
1242                                xPosi = d.distanceMeter * (float) Math.sqrt(1.0f / (1.0f +
1243                                        blockParameter[d.setup_index][dadr_block_index][SLOPE] * blockParameter[d.setup_index][dadr_block_index][SLOPE]));
1244                                if (locoInBlock[dadr_index][DIRECTION] == Path.SOUTH_WEST || locoInBlock[dadr_index][DIRECTION] == Path.WEST || locoInBlock[dadr_index][DIRECTION] == Path.NORTH_WEST) {
1245                                    newPosition.x = d.lastPos.x - xPosi;
1246                                    newPosition.y = d.lastPos.y - xPosi * blockParameter[d.setup_index][dadr_block_index][SLOPE];
1247                                } else {
1248                                    newPosition.x = d.lastPos.x + xPosi;
1249                                    newPosition.y = d.lastPos.y + xPosi * blockParameter[d.setup_index][dadr_block_index][SLOPE];
1250                                }
1251                            }
1252                            newPosition.z = 0.0f;
1253                        } else {
1254                            // Curve
1255                            float anglePos = d.distanceMeter / blockParameter[d.setup_index][dadr_block_index][RADIUS] * (-d.dirfn); // distanceMeter / RADIUS * (-loco direction)
1256                            float rotate_xpos = blockParameter[d.setup_index][dadr_block_index][ROTATE_XPOS_I];
1257                            float rotate_ypos = blockParameter[d.setup_index][dadr_block_index][ROTATE_YPOS_I]; // rotation center point y
1258                            newPosition.x =  rotate_xpos + (float) Math.cos(anglePos) * (d.lastPos.x - rotate_xpos) - (float) Math.sin(anglePos) * (d.lastPos.y - rotate_ypos);
1259                            newPosition.y =  rotate_ypos + (float) Math.sin(anglePos) * (d.lastPos.x - rotate_xpos) + (float) Math.cos(anglePos) * (d.lastPos.y - rotate_ypos);
1260                            newPosition.z = 0.0f;
1261                        }
1262                        log.debug("position to set: {}", newPosition);
1263                        d.setPosition(newPosition); // Sound set position
1264                        log.debug(" distance rest to go in block: {} of {} cm", Math.round(distance_rest_new * 100.0f),
1265                                Math.round(blockParameter[d.setup_index][dadr_block_index][LENGTH] * 100.0f));
1266                        locoInBlock[dadr_index][DISTANCE_TO_GO] = Math.round(distance_rest_new * 100.0f); // Save distance rest in cm
1267                        log.debug(" saved distance rest: {}", locoInBlock[dadr_index][DISTANCE_TO_GO]);
1268                    } else {
1269                        log.debug(" new position not set due to less distance");
1270                    }
1271                } else {
1272                    log.warn(" block for loco address {} not yet identified. May be there is another loco in the same block", dadr);
1273                }
1274            } else {
1275                log.warn(" decoder {} not found", dadr);
1276            }
1277        }
1278    }
1279
1280    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDecoderManager.class);
1281
1282}