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