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