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