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