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            RosterEntry entry = Roster.getDefault().getEntryForId(repVal);
680            if (entry != null) {
681                locoAddress = Integer.parseInt(entry.getDccAddress()); // numeric RosterEntry Id
682            } else if (org.apache.commons.lang3.StringUtils.isNumeric(repVal)) {
683                locoAddress = Integer.parseInt(repVal);
684            } else if (jmri.InstanceManager.getDefault(TrainManager.class).getTrainByName(repVal) != null) {
685                // Operations Train
686                Train selected_train = jmri.InstanceManager.getDefault(TrainManager.class).getTrainByName(repVal);
687                if (selected_train.getLeadEngineDccAddress().isEmpty()) {
688                    locoAddress = 0;
689                } else {
690                    locoAddress = Integer.parseInt(selected_train.getLeadEngineDccAddress());
691                }
692            }
693        } else if (blk.getValue() instanceof jmri.BasicRosterEntry) {
694            locoAddress = Integer.parseInt(((RosterEntry) blk.getValue()).getDccAddress());
695        } else if (blk.getValue() instanceof jmri.implementation.DefaultIdTag) {
696            // Covers TranspondingTag also
697            repVal = ((DefaultIdTag) blk.getValue()).getTagID();
698            if (org.apache.commons.lang3.StringUtils.isNumeric(repVal)) {
699                locoAddress = Integer.parseInt(repVal);
700            }
701        } else {
702            log.warn("Block Value \"{}\" found - unsupported object!", blk.getValue());
703        }
704        log.debug("loco address: {}", locoAddress);
705        return locoAddress;
706    }
707
708    @Override
709    public void propertyChange(PropertyChangeEvent evt) {
710        log.debug("property change type {} name {} old {} new {}",
711                evt.getSource().getClass().getName(), evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
712        if (evt.getSource() instanceof jmri.ReporterManager) {
713            reporterManagerPropertyChange(evt);
714        } else if (evt.getSource() instanceof jmri.Reporter) {
715            reporterPropertyChange(evt); // Location Following
716        } else if (evt.getSource() instanceof jmri.Block) {
717            log.debug("Block property change! name: {} old: {} new = {}", evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
718            blockPropertyChange(evt);
719        } else if (evt.getSource() instanceof VSDManagerFrame) {
720            if (evt.getPropertyName().equals(VSDManagerFrame.REMOVE_DECODER)) {
721                // Shut down the requested decoder and remove it from the manager's hash maps.
722                // Unless there are "illegal" handles, this should put the decoder on the garbage heap.  I think.
723                removeVSDecoder((String) evt.getOldValue());
724            } else if (evt.getPropertyName().equals(VSDManagerFrame.CLOSE_WINDOW)) {
725                // Note this assumes there is only one VSDManagerFrame open at a time.
726                if (managerFrame != null) {
727                    managerFrame = null;
728                }
729            }
730        } else {
731            // Un-Handled source. Does nothing ... yet...
732        }
733        return;
734    }
735
736    public void blockPropertyChange(PropertyChangeEvent event) {
737        // Needs to check the ID on the event, look up the appropriate VSDecoder,
738        // get the location of the event source, and update the decoder's location.
739        String eventName = event.getPropertyName();
740        if (event.getSource() instanceof PhysicalLocationReporter) {
741            Block blk = (Block) event.getSource();
742            String repVal = null;
743            // Depending on the type of Block Event, extract the needed report info from
744            // the appropriate place...
745            // "state" => Get loco address from Block's Reporter if present
746            // "value" => Get loco address from event's newValue.
747            if (eventName.equals("state")) { // NOI18N
748                // Need to decide which reporter it is, so we can use different methods
749                // to extract the address and the location.
750                if ((Integer) event.getNewValue() == Block.OCCUPIED) {
751                    // Is there a Block's Reporter?
752                    if (blk.getReporter() == null) {
753                        log.debug("Block {} has no reporter!  Skipping state-type report", blk.getSystemName());
754                        return;
755                    }
756                    // Get this Block's Reporter's current/last report value
757                    if (blk.isReportingCurrent()) {
758                        Object currentReport = blk.getReporter().getCurrentReport();
759                        if ( currentReport != null) {
760                            if(currentReport instanceof jmri.Reportable) {
761                                repVal = ((jmri.Reportable)currentReport).toReportString();
762                            } else {
763                                repVal = currentReport.toString();
764                            }
765                        }
766                    } else {
767                        Object lastReport = blk.getReporter().getLastReport();
768                        if ( lastReport != null) {
769                            if(lastReport instanceof jmri.Reportable) {
770                                repVal = ((jmri.Reportable)lastReport).toReportString();
771                            } else {
772                                repVal = lastReport.toString();
773                            }
774                        }
775                    }
776                } else {
777                    log.debug("Ignoring report. not an OCCUPIED event.");
778                    return;
779                }
780                log.debug("block repVal: {}", repVal);
781            } else if (eventName.equals("value")) { // NOI18N
782                if (event.getNewValue() == null ) {
783                    return; // block value was cleared, nothing to do
784                }
785                atStart(blk);
786            } else {
787                log.debug("Not a supported Block event type.  Ignoring.");
788                return;
789            }
790
791            // Set the decoder's position due to the report.
792            if (repVal == null) {
793                log.debug("Report from Block {} is null!", blk.getSystemName());
794            }
795            if (repVal != null && blk.getDirection(repVal) == PhysicalLocationReporter.Direction.ENTER) {
796                setDecoderPositionByAddr(blk.getLocoAddress(repVal), blk.getPhysicalLocation());
797            }
798            return;
799        } else {
800            log.debug("Reporter doesn't support physical location reporting.");
801        }
802        return;
803    }
804
805    public void reporterPropertyChange(PropertyChangeEvent event) {
806        // Needs to check the ID on the event, look up the appropriate VSDecoder,
807        // get the location of the event source, and update the decoder's location.
808        String eventName = event.getPropertyName();
809        if (lf_version == 1 || (geofile_ok && alf_version == 1)) {
810            if ((event.getSource() instanceof PhysicalLocationReporter) && (eventName.equals("currentReport"))) { // NOI18N
811                PhysicalLocationReporter arp = (PhysicalLocationReporter) event.getSource();
812                // Need to decide which reporter it is, so we can use different methods
813                // to extract the address and the location.
814                if (event.getNewValue() instanceof IdTag) {
815                    // RFID-tag, Digitrax Transponding tags, RailCom tags
816                    if (event.getNewValue() instanceof jmri.jmrix.loconet.TranspondingTag) {
817                        String repVal = ((jmri.Reportable) event.getNewValue()).toReportString();
818                        int locoAddress = arp.getLocoAddress(repVal).getNumber();
819                        log.debug("Reporter repVal: {}, number: {}", repVal, locoAddress);
820                        // Check: is loco address valid?
821                        if (decoderInBlock.containsKey(locoAddress)) {
822                            VSDecoder d = decoderInBlock.get(locoAddress);
823                            // look for additional geometric layout information
824                            if (geofile_ok) {
825                                Reporter rp = (Reporter) event.getSource();
826                                int new_rp = 0;
827                                try {
828                                    new_rp = Integer.parseInt(Manager.getSystemSuffix(rp.getSystemName()));
829                                } catch (java.lang.NumberFormatException e) {
830                                    log.warn("Invalid Reporter system name '{}'", rp.getSystemName());
831                                }
832                                // Check: Reporter must be valid for GeoData processing
833                                //    use the current Reporter list as a filter (changeable by a Train selection)
834                                if (reporterlists.get(d.setup_index).contains(new_rp)) {
835                                    if (arp.getDirection(repVal) == PhysicalLocationReporter.Direction.ENTER) {
836                                        handleAlf(d, locoAddress, new_rp); // Advanced Location Following version 1
837                                    }
838                                } else {
839                                    log.info("Reporter {} not valid for {} setup {}", new_rp, VSDGeoFile.VSDGeoDataFileName, d.setup_index + 1);
840                                }
841                            } else {
842                                if (arp.getDirection(repVal) == PhysicalLocationReporter.Direction.ENTER) {
843                                    d.savedSound.setTunnel(arp.getPhysicalLocation(repVal).isTunnel());
844                                    d.setPosition(arp.getPhysicalLocation(repVal));
845                                    log.debug("position set to: {}", arp.getPhysicalLocation(repVal));
846                                }
847                            }
848                        } else {
849                            log.info(" decoder address {} is not valid!", locoAddress);
850                        }
851                        return;
852                    } else {
853                        // newValue is of IdTag type.
854                        // Dcc4Pc, Ecos,
855                        // Assume Reporter "arp" is the most recent seen location
856                        IdTag newValue = (IdTag) event.getNewValue();
857                        decoderInBlock.get(arp.getLocoAddress(newValue.getTagID()).getNumber()).savedSound.setTunnel(arp.getPhysicalLocation(null).isTunnel());
858                        setDecoderPositionByAddr(arp.getLocoAddress(newValue.getTagID()), arp.getPhysicalLocation(null));
859                    }
860                } else {
861                    log.info("Reporter's return type is not supported.");
862                }
863            } else {
864                log.debug("Reporter doesn't support physical location reporting or isn't reporting new info.");
865            }
866        }
867        return;
868    }
869
870    public void reporterManagerPropertyChange(PropertyChangeEvent event) {
871        String eventName = event.getPropertyName();
872
873        log.debug("VSDecoder received Reporter Manager Property Change: {}", eventName);
874        if (eventName.equals("length")) { // NOI18N
875
876            // Re-register for all the reporters. The registerReporterListener() will skip
877            // any that we're already registered for.
878            for (Reporter r : jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getNamedBeanSet()) {
879                registerReporterListener(r.getSystemName());
880            }
881
882            // It could be that we lost a Reporter.  But since we aren't keeping a list anymore
883            // we don't care.
884        }
885    }
886
887    // handle Advanced Location Following version 1
888    private void handleAlf(VSDecoder d, int locoAddress, int new_rp) {
889        int new_rp_index = reporterlists.get(d.setup_index).indexOf(new_rp);
890        int old_rp = -1; // set to "undefined"
891        int old_rp_index = -1; // set to "undefined"
892        int ix = getArrayIndex(locoAddress);
893        if (ix < locoInBlock.length) {
894            old_rp = locoInBlock[ix][BLOCK];
895            if (old_rp == 0) old_rp = -1; // set to "undefined"
896            old_rp_index = reporterlists.get(d.setup_index).indexOf(old_rp); // -1 if not found (undefined)
897        } else {
898            log.warn(" Array locoInBlock INDEX {} IS NOT VALID! Set to 0.", ix);
899            ix = 0;
900        }
901        log.debug("new_rp: {}, old_rp: {}, new index: {}, old index: {}", new_rp, old_rp, new_rp_index, old_rp_index);
902        // Validation check: don't proceed when it's the same reporter
903        if (new_rp != old_rp) {
904            // Validation check: reporter must be a new or a neighbour reporter or must rotating in a circle
905            int lastrepix = reporterlists.get(d.setup_index).size() - 1; // Get the index of the last Reporter
906            if ((old_rp == -1) // Loco can be in any section, if it's the first reported section; old rp is "undefined"
907                    || (old_rp_index + d.dirfn == new_rp_index) // Loco is running forward or reverse
908                    || (circlelist.get(d.setup_index) && d.dirfn == -1 && old_rp_index == 0 && new_rp_index == lastrepix) // Loco is running reverse and circling
909                    || (circlelist.get(d.setup_index) && d.dirfn ==  1 && old_rp_index == lastrepix && new_rp_index == 0)) { // Loco is running forward and circling
910                // Validation check: OK
911                locoInBlock[ix][BLOCK] = new_rp; // Set new block number (int)
912                log.debug(" distance rest (old) to go in block {}: {} cm", old_rp, locoInBlock[ix][DISTANCE_TO_GO]);
913                locoInBlock[ix][DISTANCE_TO_GO] = Math.round(blockParameter[d.setup_index][new_rp_index][LENGTH] * 100.0f); // block distance init: block length in cm
914                log.debug(" distance rest (new) to go in block {}: {} cm", new_rp, locoInBlock[ix][DISTANCE_TO_GO]);
915                // get the new sound position point (depends on the loco traveling direction)
916                if (d.dirfn == 1) {
917                    d.posToSet = blockPositionlists.get(d.setup_index).get(new_rp_index); // Start position
918                } else {
919                    d.posToSet = blockPositionlists.get(d.setup_index).get(new_rp_index + 1); // End position
920                }
921                if (old_rp == -1 && d.startPos != null) { // Special case start position: first choice; if found, overwrite it.
922                    d.posToSet = d.startPos;
923                }
924                d.savedSound.setTunnel(blockPositionlists.get(d.setup_index).get(new_rp_index).isTunnel()); // set the tunnel status
925                log.debug("address {}: position to set: {}", d.getAddress(), d.posToSet);
926                d.setPosition(d.posToSet); // Sound set position
927                changeDirection(d, locoAddress, new_rp_index);
928                stopSoundPositionTimer(d);
929                startSoundPositionTimer(d); // timer restart
930            } else {
931                log.info(" Validation failed! Last reporter: {}, new reporter: {}, dirfn: {} for {}", old_rp, new_rp, d.dirfn, locoAddress);
932            }
933        } else {
934            log.info(" Same PhysicalLocationReporter, position not set!");
935        }
936    }
937
938    // handle Advanced Location Following version 2
939    private void handleAlf2(VSDecoder d, int locoAddress, Block newBlock) {
940        if (currentBlock.get(d) != newBlock) {
941            int ix = getArrayIndex(locoAddress); // ix = decoder number 0 - max_decoder-1
942            if (locoInBlock[ix][DIR_FN] == 0) { // at start
943                if (d.getLayoutTrack() == null) {
944                    if (possibleStartBlocks.get(newBlock) != null) {
945                        d.setModels(possibleStartBlocks.get(newBlock)); // get the models from the HashMap via block
946                        log.debug("Block: {}, models: {}", newBlock, d.getModels());
947                        TrackSegment ts = null;
948                        for (LayoutTrack lt : d.getModels().getLayoutTracks()) {
949                            if (lt instanceof TrackSegment) {
950                                ts = (TrackSegment) lt;
951                                if (ts.getLayoutBlock() != null && ts.getLayoutBlock().getBlock() == newBlock) {
952                                    break;
953                                }
954                            }
955                        }
956                        if (ts != null) {
957                            TrackSegmentView tsv = d.getModels().getTrackSegmentView(ts);
958                            d.setLayoutTrack(ts);
959                            d.setReturnTrack(d.getLayoutTrack());
960                            d.setReturnLastTrack(tsv.getConnect2());
961                            d.setLastTrack(tsv.getConnect1());
962                            d.setReturnDistance(MathUtil.distance(d.getModels().getCoords(tsv.getConnect1(), tsv.getType1()),
963                                    d.getModels().getCoords(tsv.getConnect2(), tsv.getType2())));
964                            d.setDistance(0);
965                            d.distanceOnTrack = 0.5d * d.getReturnDistance(); // halved to get starting position (mid or centre of the track)
966                            if (d.dirfn == -1) { // in case the loco is running in reverse direction
967                                d.setLayoutTrack(d.getReturnTrack());
968                                d.setLastTrack(d.getReturnLastTrack());
969                            }
970                            locoInBlock[ix][DIR_FN] = d.dirfn;
971                            currentBlock.put(d, newBlock);
972                            // prepare navigation
973                            d.posToSet = new PhysicalLocation(0.0f, 0.0f, 0.0f);
974                            log.info("at start - TS: {}, block: {}, loco: {}, panel: {}", ts.getName(), newBlock, locoAddress, d.getModels().getTitle());
975                        }
976                    } else {
977                        log.warn("block {} is not a valid start block; valid start blocks are: {}", newBlock, possibleStartBlocks);
978                    }
979                }
980
981            } else {
982
983                currentBlock.put(d, newBlock);
984                // new block; if end point is already reached, d.distanceOnTrack is zero
985                if (d.distanceOnTrack > 0) {
986                    // it's still on this track
987                    // handle a block change, if the loco reaches the next block before the calculated end
988                    boolean result = true; // new block, so go to the next track
989                    d.distanceOnTrack = 0;
990                    // go to next track
991                    LayoutTrack last = d.getLayoutTrack();
992                    if (d.getLayoutTrack() instanceof TrackSegment) {
993                        TrackSegmentView tsv = d.getModels().getTrackSegmentView((TrackSegment) d.getLayoutTrack());
994                        log.debug(" true - layout track: {}, last track: {}, connect1: {}, connect2: {}, last block: {}",
995                                d.getLayoutTrack().getName(), d.getLastTrack().getName(), tsv.getConnect1(), tsv.getConnect2(), tsv.getBlockName());
996                        if (tsv.getConnect1().equals(d.getLastTrack())) {
997                            d.setLayoutTrack(tsv.getConnect2());
998                        } else if (tsv.getConnect2().equals(d.getLastTrack())) {
999                            d.setLayoutTrack(tsv.getConnect1());
1000                        } else { // OOPS! we're lost!
1001                            log.info(" TS lost, c1: {}, c2: {}, last track: {}", tsv.getConnect1(), tsv.getConnect2(), d.getLastTrack());
1002                            result = false;
1003                        }
1004                        if (result) {
1005                            d.setLastTrack(last);
1006                            d.setReturnTrack(d.getLayoutTrack());
1007                            d.setReturnLastTrack(d.getLayoutTrack());
1008                            log.debug(" next track (layout track): {}, last track: {}", d.getLayoutTrack(), d.getLastTrack());
1009                        }
1010                    } else if (d.getLayoutTrack() instanceof LayoutTurnout
1011                            || d.getLayoutTrack() instanceof LayoutSlip
1012                            || d.getLayoutTrack() instanceof LevelXing
1013                            || d.getLayoutTrack() instanceof LayoutTurntable) {
1014                        // go to next track
1015                        if (d.nextLayoutTrack != null) {
1016                            d.setLayoutTrack(d.nextLayoutTrack);
1017                        } else { // OOPS! we're lost!
1018                            result = false;
1019                        }
1020                        if (result) {
1021                            d.setLastTrack(last);
1022                            d.setReturnTrack(d.getLayoutTrack());
1023                            d.setReturnLastTrack(d.getLayoutTrack());
1024                        }
1025                    }
1026                }
1027            }
1028            startSoundPositionTimer(d);
1029        } else {
1030           log.warn(" Same PhysicalLocationReporter, position not set!");
1031        }
1032    }
1033
1034    private void changeDirection(VSDecoder d, int locoAddress, int new_rp_index) {
1035        PhysicalLocation point1 = blockPositionlists.get(d.setup_index).get(new_rp_index);
1036        PhysicalLocation point2 = blockPositionlists.get(d.setup_index).get(new_rp_index + 1);
1037        Point2D coords1 = new Point2D.Double(point1.x, point1.y);
1038        Point2D coords2 = new Point2D.Double(point2.x, point2.y);
1039        int direct;
1040        if (d.dirfn == 1) {
1041            direct = Path.computeDirection(coords1, coords2);
1042        } else {
1043            direct = Path.computeDirection(coords2, coords1);
1044        }
1045        locoInBlock[getArrayIndex(locoAddress)][DIRECTION] = direct;
1046        log.debug("direction: {} ({})", Path.decodeDirection(direct), direct);
1047    }
1048
1049    /**
1050     * Get index of a decoder.
1051     * @param number The loco address number.
1052     * @return the index of a decoder's loco address number
1053     *         in the array or the length of the array.
1054     */
1055    public int getArrayIndex(int number) {
1056        for (int i = 0; i < locoInBlock.length; i++) {
1057            if (locoInBlock[i][ADDRESS] == number) {
1058                return i;
1059            }
1060        }
1061        return locoInBlock.length;
1062    }
1063
1064    public void locoInBlockRemove(int numb) {
1065        // Works only for <locoInBlock.length> rows
1066        //  find index first
1067        int remove_index = 0;
1068        for (int i = 0; i < locoInBlock.length; i++) {
1069            if (locoInBlock[i][ADDRESS] == numb) {
1070                remove_index = i;
1071            }
1072        }
1073        for (int i = remove_index; i < locoInBlock.length - 1; i++) {
1074            for (int k = 0; k < locoInBlock[i].length; k++) {
1075                locoInBlock[i][k] = locoInBlock[i + 1][k];
1076            }
1077        }
1078        // Delete last row
1079        int il = locoInBlock.length - 1;
1080        for (int k = 0; k < locoInBlock[il].length; k++) {
1081            locoInBlock[il][k] = 0;
1082        }
1083    }
1084
1085    public void loadProfiles(VSDFile vf) {
1086        Element root;
1087        String pname;
1088        root = vf.getRoot();
1089        if (root == null) {
1090            return;
1091        }
1092
1093        ArrayList<String> new_entries = new ArrayList<>();
1094
1095        java.util.Iterator<Element> i = root.getChildren("profile").iterator(); // NOI18N
1096        while (i.hasNext()) {
1097            Element e = i.next();
1098            pname = e.getAttributeValue("name");
1099            log.debug("Profile name: {}", pname);
1100            if ((pname != null) && !(pname.isEmpty())) { // NOI18N
1101                profiletable.put(pname, vf.getName());
1102                new_entries.add(pname);
1103            }
1104        }
1105
1106        if (!GraphicsEnvironment.isHeadless()) {
1107            fireMyEvent(new VSDManagerEvent(this, VSDManagerEvent.EventType.PROFILE_LIST_CHANGE, new_entries));
1108        }
1109    }
1110
1111    void initSoundPositionTimer(VSDecoder d) {
1112        if (geofile_ok) {
1113            Timer t = new Timer(check_time, new ActionListener() {
1114                @Override
1115                public void actionPerformed(ActionEvent e) {
1116                    if (alf_version == 1) {
1117                        calcNewPosition(d);
1118                    } else if (alf_version == 2) {
1119                        int ix = getArrayIndex(d.getAddress().getNumber()); // ix = decoder number 0-3 (max_decoder)
1120                        float actualspeed = d.getEngineSound().getActualSpeed();
1121                        if (locoInBlock[ix][DIR_FN] != d.dirfn) {
1122                            // traveling direction has changed
1123                            if (d.getEngineSound().isEngineStarted()) {
1124                                locoInBlock[ix][DIR_FN] = d.dirfn; // save traveling direction info
1125                                if (d.distanceOnTrack <= d.getReturnDistance()) {
1126                                    d.distanceOnTrack = d.getReturnDistance() - d.distanceOnTrack;
1127                                } else {
1128                                    d.distanceOnTrack = d.getReturnDistance();
1129                                }
1130                                d.setLayoutTrack(d.getReturnTrack());
1131                                d.setLastTrack(d.getReturnLastTrack());
1132                                log.debug("direction changed to {}, layout: {}, last: {}, return: {}, d.getReturnDistance: {}, d.distanceOnTrack: {}, d.getDistance: {}",
1133                                        d.dirfn, d.getLayoutTrack(), d.getLastTrack(), d.getReturnTrack(), d.getReturnDistance(), d.distanceOnTrack, d.getDistance());
1134                                d.setDistance(0);
1135                                d.navigate();
1136                            }
1137                        }
1138                        if ((d.getEngineSound().isEngineStarted() && actualspeed > 0.0f) || d.getLayoutTrack() instanceof LayoutTurntable) {
1139                            float speed_ms = actualspeed * (d.dirfn == 1 ? d.topspeed : d.topspeed_rev) * 0.44704f / layout_scale; // calculate the speed
1140                            d.setDistance(d.getDistance() + speed_ms * check_time / 10.0); // d.getDistance() normally is 0, but can content an overflow
1141                            d.navigate();
1142                            Point2D loc = d.getLocation();
1143                            Point2D loc2 = new Point2D.Double(((float) loc.getX() - models_origin.x) * 0.01f, (models_origin.y - (float) loc.getY()) * 0.01f);
1144                            d.posToSet.x = (float) loc2.getX();
1145                            d.posToSet.y = (float) loc2.getY();
1146                            d.posToSet.z = 0.0f;
1147                            log.debug("address {} position to set: {}, location: {}", d.getAddress(), d.posToSet, loc);
1148                            d.setPosition(d.posToSet);
1149                        }
1150                    }
1151                }
1152            });
1153            t.setRepeats(true);
1154            timertable.put(d.getId(), t);
1155            log.debug("timer {} created for decoder {}, id: {}", t, d, d.getId());
1156        } else {
1157            log.debug("No timer created, GeoData not available");
1158        }
1159    }
1160
1161    void startSoundPositionTimer(VSDecoder d) {
1162        Timer t = timertable.get(d.getId());
1163        if (t != null) {
1164            t.setInitialDelay(check_time);
1165            t.start();
1166            log.debug("timer {} started for decoder id {}, {}, check time: {}", t, d.getId(), d, check_time);
1167        }
1168    }
1169
1170    void stopSoundPositionTimer(VSDecoder d) {
1171        Timer t = timertable.get(d.getId());
1172        if (t != null) {
1173            if (t.isRunning()) {
1174                t.stop();
1175                log.debug("timer {} stopped for {}", t, d);
1176            } else {
1177                log.debug("timer {} was not running", t);
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}