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