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