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