001package jmri.jmrit.display.layoutEditor; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.text.MessageFormat; 006import java.util.*; 007 008import javax.annotation.CheckForNull; 009import javax.annotation.Nonnull; 010 011import jmri.*; 012import jmri.util.MathUtil; 013 014/** 015 * A LayoutTurntable is a representation used by LayoutEditor to display a 016 * turntable. 017 * <p> 018 * A LayoutTurntable has a variable number of connection points, called 019 * RayTracks, each radiating from the center of the turntable. Each of these 020 * points should be connected to a TrackSegment. 021 * <p> 022 * Each radiating segment (RayTrack) gets its Block information from its 023 * connected track segment. 024 * <p> 025 * Each radiating segment (RayTrack) has a unique connection index. The 026 * connection index is set when the RayTrack is created, and cannot be changed. 027 * This connection index is used to maintain the identity of the radiating 028 * segment to its connected Track Segment as ray tracks are added and deleted by 029 * the user. 030 * <p> 031 * The radius of the turntable circle is variable by the user. 032 * <p> 033 * Each radiating segment (RayTrack) connecting point is a fixed distance from 034 * the center of the turntable. The user may vary the angle of the radiating 035 * segment. Angles are measured from the vertical (12 o'clock) position in a 036 * clockwise manner. For example, 30 degrees is 1 o'clock, 60 degrees is 2 037 * o'clock, 90 degrees is 3 o'clock, etc. 038 * <p> 039 * Each radiating segment is drawn from its connection point to the turntable 040 * circle in the direction of the turntable center. 041 * 042 * @author Dave Duchamp Copyright (c) 2007 043 * @author George Warner Copyright (c) 2017-2018 044 */ 045public class LayoutTurntable extends LayoutTrack { 046 047 /** 048 * Constructor method 049 * 050 * @param id the name for the turntable 051 * @param models what layout editor panel to put it in 052 */ 053 public LayoutTurntable(@Nonnull String id, @Nonnull LayoutEditor models) { 054 super(id, models); 055 056 radius = 25.0; // initial default, change asap. 057 } 058 059 // defined constants 060 // operational instance variables (not saved between sessions) 061 private NamedBeanHandle<LayoutBlock> namedLayoutBlock = null; 062 063 private boolean dispatcherManaged = false; 064 private boolean turnoutControlled = false; 065 private double radius = 25.0; 066 private int knownIndex = -1; 067 private int commandedIndex = -1; 068 069 private int signalIconPlacement = 0; // 0: Do Not Place, 1: Left, 2: Right 070 071 private NamedBeanHandle<SignalMast> bufferSignalMast; 072 private NamedBeanHandle<SignalMast> exitSignalMast; 073 074 // persistent instance variables (saved between sessions) 075 076 // temporary: this is referenced directly from LayoutTurntable, which 077 // should be using _functional_ accessors here. 078 public final List<RayTrack> rayTrackList = new ArrayList<>(); // list of Ray Track objects 079 080 /** 081 * Get a string that represents this object. This should only be used for 082 * debugging. 083 * 084 * @return the string 085 */ 086 @Override 087 @Nonnull 088 public String toString() { 089 return "LayoutTurntable " + getName(); 090 } 091 092 // 093 // Accessor methods 094 // 095 /** 096 * Get the radius for this turntable. 097 * 098 * @return the radius for this turntable 099 */ 100 public double getRadius() { 101 return radius; 102 } 103 104 /** 105 * Set the radius for this turntable. 106 * 107 * @param r the radius for this turntable 108 */ 109 public void setRadius(double r) { 110 radius = r; 111 } 112 113 public boolean isDispatcherManaged() { 114 return dispatcherManaged; 115 } 116 117 public void setDispatcherManaged(boolean managed) { 118 dispatcherManaged = managed; 119 } 120 121 public int getSignalIconPlacement() { 122 return signalIconPlacement; 123 } 124 125 public void setSignalIconPlacement(int placement) { 126 this.signalIconPlacement = placement; 127 } 128 129 public SignalMast getBufferMast() { 130 if (bufferSignalMast == null) { 131 return null; 132 } 133 return bufferSignalMast.getBean(); 134 } 135 136 public String getBufferSignalMastName() { 137 if (bufferSignalMast == null) { 138 return ""; 139 } 140 return bufferSignalMast.getName(); 141 } 142 143 public void setBufferSignalMast(String name) { 144 if (name == null || name.isEmpty()) { 145 bufferSignalMast = null; 146 return; 147 } 148 SignalMast mast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(name); 149 if (mast != null) { 150 bufferSignalMast = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(name, mast); 151 } else { 152 bufferSignalMast = null; 153 } 154 } 155 156 public SignalMast getExitSignalMast() { 157 if (exitSignalMast == null) { 158 return null; 159 } 160 return exitSignalMast.getBean(); 161 } 162 163 public String getExitSignalMastName() { 164 if (exitSignalMast == null) { 165 return ""; 166 } 167 return exitSignalMast.getName(); 168 } 169 170 public void setExitSignalMast(String name) { 171 if (name == null || name.isEmpty()) { 172 exitSignalMast = null; 173 return; 174 } 175 SignalMast mast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(name); 176 if (mast != null) { 177 exitSignalMast = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(name, mast); 178 } else { 179 exitSignalMast = null; 180 } 181 } 182 183 /** 184 * @return the layout block name 185 */ 186 @Nonnull 187 public String getBlockName() { 188 String result = null; 189 if (namedLayoutBlock != null) { 190 result = namedLayoutBlock.getName(); 191 } 192 return ((result == null) ? "" : result); 193 } 194 195 /** 196 * @return the layout block 197 */ 198 @CheckForNull 199 public LayoutBlock getLayoutBlock() { 200 return (namedLayoutBlock != null) ? namedLayoutBlock.getBean() : null; 201 } 202 203 /** 204 * Set up a LayoutBlock for this LayoutTurntable. 205 * 206 * @param newLayoutBlock the LayoutBlock to set 207 */ 208 public void setLayoutBlock(@CheckForNull LayoutBlock newLayoutBlock) { 209 LayoutBlock layoutBlock = getLayoutBlock(); 210 if (layoutBlock != newLayoutBlock) { 211 /// block has changed, if old block exists, decrement use 212 if (layoutBlock != null) { 213 layoutBlock.decrementUse(); 214 } 215 if (newLayoutBlock != null) { 216 String newName = newLayoutBlock.getUserName(); 217 if ((newName != null) && !newName.isEmpty()) { 218 namedLayoutBlock = InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(newName, newLayoutBlock); 219 } else { 220 namedLayoutBlock = null; 221 } 222 } else { 223 namedLayoutBlock = null; 224 } 225 } 226 } 227 228 /** 229 * Set up a LayoutBlock for this LayoutTurntable. 230 * 231 * @param name the name of the new LayoutBlock 232 */ 233 public void setLayoutBlockByName(@CheckForNull String name) { 234 if ((name != null) && !name.isEmpty()) { 235 setLayoutBlock(models.provideLayoutBlock(name)); 236 } 237 } 238 239 /** 240 * Add a ray at the specified angle. 241 * 242 * @param angle the angle 243 * @return the RayTrack 244 */ 245 public RayTrack addRay(double angle) { 246 RayTrack rt = new RayTrack(angle, getNewIndex()); 247 rayTrackList.add(rt); 248 return rt; 249 } 250 251 private int getNewIndex() { 252 int index = -1; 253 if (rayTrackList.isEmpty()) { 254 return 0; 255 } 256 257 boolean found = true; 258 while (found) { 259 index++; 260 found = false; // assume failure (pessimist!) 261 for (RayTrack rt : rayTrackList) { 262 if (index == rt.getConnectionIndex()) { 263 found = true; 264 } 265 } 266 } 267 return index; 268 } 269 270 // the following method is only for use in loading layout turntables 271 public void addRayTrack(double angle, int index, String name) { 272 RayTrack rt = new RayTrack(angle, index); 273 /// if (ray!=null) { 274 rayTrackList.add(rt); 275 rt.connectName = name; 276 //} 277 } 278 279 /** 280 * Get the connection for the ray with this index. 281 * 282 * @param index the index 283 * @return the connection for the ray with this value of getConnectionIndex 284 */ 285 @CheckForNull 286 public TrackSegment getRayConnectIndexed(int index) { 287 TrackSegment result = null; 288 for (RayTrack rt : rayTrackList) { 289 if (rt.getConnectionIndex() == index) { 290 result = rt.getConnect(); 291 break; 292 } 293 } 294 return result; 295 } 296 297 /** 298 * Get the connection for the ray at the index in the rayTrackList. 299 * 300 * @param i the index in the rayTrackList 301 * @return the connection for the ray at that index in the rayTrackList or null 302 */ 303 @CheckForNull 304 public TrackSegment getRayConnectOrdered(int i) { 305 TrackSegment result = null; 306 307 if (i < rayTrackList.size()) { 308 RayTrack rt = rayTrackList.get(i); 309 if (rt != null) { 310 result = rt.getConnect(); 311 } 312 } 313 return result; 314 } 315 316 /** 317 * Set the connection for the ray at the index in the rayTrackList. 318 * 319 * @param ts the connection 320 * @param index the index in the rayTrackList 321 */ 322 public void setRayConnect(@CheckForNull TrackSegment ts, int index) { 323 for (RayTrack rt : rayTrackList) { 324 if (rt.getConnectionIndex() == index) { 325 rt.setConnect(ts); 326 break; 327 } 328 } 329 } 330 331 // should only be used by xml save code 332 @Nonnull 333 public List<RayTrack> getRayTrackList() { 334 return rayTrackList; 335 } 336 337 /** 338 * Get the number of rays on turntable. 339 * 340 * @return the number of rays 341 */ 342 public int getNumberRays() { 343 return rayTrackList.size(); 344 } 345 346 /** 347 * Get the index for the ray at this position in the rayTrackList. 348 * 349 * @param i the position in the rayTrackList 350 * @return the index 351 */ 352 public int getRayIndex(int i) { 353 int result = 0; 354 if (i < rayTrackList.size()) { 355 RayTrack rt = rayTrackList.get(i); 356 result = rt.getConnectionIndex(); 357 } 358 return result; 359 } 360 361 /** 362 * Get the angle for the ray at this position in the rayTrackList. 363 * 364 * @param i the position in the rayTrackList 365 * @return the angle 366 */ 367 public double getRayAngle(int i) { 368 double result = 0.0; 369 if (i < rayTrackList.size()) { 370 RayTrack rt = rayTrackList.get(i); 371 result = rt.getAngle(); 372 } 373 return result; 374 } 375 376 /** 377 * Set the turnout and state for the ray with this index. 378 * 379 * @param index the index 380 * @param turnoutName the turnout name 381 * @param state the state 382 */ 383 public void setRayTurnout(int index, @CheckForNull String turnoutName, int state) { 384 boolean found = false; // assume failure (pessimist!) 385 for (RayTrack rt : rayTrackList) { 386 if (rt.getConnectionIndex() == index) { 387 rt.setTurnout(turnoutName, state); 388 found = true; 389 break; 390 } 391 } 392 if (!found) { 393 log.error("{}.setRayTurnout({}, {}, {}); Attempt to add Turnout control to a non-existant ray track", 394 getName(), index, turnoutName, state); 395 } 396 } 397 398 /** 399 * Get the name of the turnout for the ray at this index. 400 * 401 * @param i the index 402 * @return name of the turnout for the ray at this index 403 */ 404 @CheckForNull 405 public String getRayTurnoutName(int i) { 406 String result = null; 407 if (i < rayTrackList.size()) { 408 RayTrack rt = rayTrackList.get(i); 409 result = rt.getTurnoutName(); 410 } 411 return result; 412 } 413 414 /** 415 * Get the turnout for the ray at this index. 416 * 417 * @param i the index 418 * @return the turnout for the ray at this index 419 */ 420 @CheckForNull 421 public Turnout getRayTurnout(int i) { 422 Turnout result = null; 423 if (i < rayTrackList.size()) { 424 RayTrack rt = rayTrackList.get(i); 425 result = rt.getTurnout(); 426 } 427 return result; 428 } 429 430 /** 431 * Get the state of the turnout for the ray at this index. 432 * 433 * @param i the index 434 * @return state of the turnout for the ray at this index 435 */ 436 public int getRayTurnoutState(int i) { 437 int result = 0; 438 if (i < rayTrackList.size()) { 439 RayTrack rt = rayTrackList.get(i); 440 result = rt.getTurnoutState(); 441 } 442 return result; 443 } 444 445 /** 446 * Get if the ray at this index is disabled. 447 * 448 * @param i the index 449 * @return true if disabled 450 */ 451 public boolean isRayDisabled(int i) { 452 boolean result = false; // assume not disabled 453 if (i < rayTrackList.size()) { 454 RayTrack rt = rayTrackList.get(i); 455 result = rt.isDisabled(); 456 } 457 return result; 458 } 459 460 /** 461 * Set the disabled state of the ray at this index. 462 * 463 * @param i the index 464 * @param boo the state 465 */ 466 public void setRayDisabled(int i, boolean boo) { 467 if (i < rayTrackList.size()) { 468 RayTrack rt = rayTrackList.get(i); 469 rt.setDisabled(boo); 470 } 471 } 472 473 /** 474 * Get the disabled when occupied state of the ray at this index. 475 * 476 * @param i the index 477 * @return the state 478 */ 479 public boolean isRayDisabledWhenOccupied(int i) { 480 boolean result = false; // assume not disabled when occupied 481 if (i < rayTrackList.size()) { 482 RayTrack rt = rayTrackList.get(i); 483 result = rt.isDisabledWhenOccupied(); 484 } 485 return result; 486 } 487 488 /** 489 * Set the disabled when occupied state of the ray at this index. 490 * 491 * @param i the index 492 * @param boo the state 493 */ 494 public void setRayDisabledWhenOccupied(int i, boolean boo) { 495 if (i < rayTrackList.size()) { 496 RayTrack rt = rayTrackList.get(i); 497 rt.setDisabledWhenOccupied(boo); 498 } 499 } 500 501 /** 502 * {@inheritDoc} 503 */ 504 @Override 505 public LayoutTrack getConnection(HitPointType connectionType) throws jmri.JmriException { 506 LayoutTrack result = null; 507 if (HitPointType.isTurntableRayHitType(connectionType)) { 508 result = getRayConnectIndexed(connectionType.turntableTrackIndex()); 509 } else { 510 String errString = MessageFormat.format("{0}.getCoordsForConnectionType({1}); Invalid connection type", 511 getName(), connectionType); // NOI18N 512 log.error("will throw {}", errString); // NOI18N 513 throw new jmri.JmriException(errString); 514 } 515 return result; 516 } 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override 522 public void setConnection(HitPointType connectionType, @CheckForNull LayoutTrack o, HitPointType type) throws jmri.JmriException { 523 if ((type != HitPointType.TRACK) && (type != HitPointType.NONE)) { 524 String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid type", 525 getName(), connectionType, (o == null) ? "null" : o.getName(), type); // NOI18N 526 log.error("will throw {}", errString); // NOI18N 527 throw new jmri.JmriException(errString); 528 } 529 if (HitPointType.isTurntableRayHitType(connectionType)) { 530 if ((o == null) || (o instanceof TrackSegment)) { 531 setRayConnect((TrackSegment) o, connectionType.turntableTrackIndex()); 532 } else { 533 String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid object: {4}", 534 getName(), connectionType, o.getName(), 535 type, o.getClass().getName()); // NOI18N 536 log.error("will throw {}", errString); // NOI18N 537 throw new jmri.JmriException(errString); 538 } 539 } else { 540 String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid connection type", 541 getName(), connectionType, (o == null) ? "null" : o.getName(), type); // NOI18N 542 log.error("will throw {}", errString); // NOI18N 543 throw new jmri.JmriException(errString); 544 } 545 } 546 547 /** 548 * Test if ray with this index is a mainline track or not. 549 * <p> 550 * Defaults to false (not mainline) if connecting track segment is missing. 551 * 552 * @param index the index 553 * @return true if connecting track segment is mainline 554 */ 555 public boolean isMainlineIndexed(int index) { 556 boolean result = false; // assume failure (pessimist!) 557 558 for (RayTrack rt : rayTrackList) { 559 if (rt.getConnectionIndex() == index) { 560 TrackSegment ts = rt.getConnect(); 561 if (ts != null) { 562 result = ts.isMainline(); 563 break; 564 } 565 } 566 } 567 return result; 568 } 569 570 /** 571 * Test if ray at this index is a mainline track or not. 572 * <p> 573 * Defaults to false (not mainline) if connecting track segment is missing 574 * 575 * @param i the index 576 * @return true if connecting track segment is mainline 577 */ 578 public boolean isMainlineOrdered(int i) { 579 boolean result = false; // assume failure (pessimist!) 580 if (i < rayTrackList.size()) { 581 RayTrack rt = rayTrackList.get(i); 582 if (rt != null) { 583 TrackSegment ts = rt.getConnect(); 584 if (ts != null) { 585 result = ts.isMainline(); 586 } 587 } 588 } 589 return result; 590 } 591 592 @Override 593 public boolean isMainline() { 594 return false; 595 } 596 597 598 public String tLayoutBlockName = ""; 599 public String tExitSignalMastName = ""; 600 public String tBufferSignalMastName = ""; 601 602 /** 603 * Initialization method The name of each track segment connected to a ray 604 * track is initialized by by LayoutTurntableXml, then the following method 605 * is called after the entire LayoutEditor is loaded to set the specific 606 * TrackSegment objects. 607 * 608 * @param p the layout editor 609 */ 610 @Override 611 public void setObjects(@Nonnull LayoutEditor p) { 612 if (tLayoutBlockName != null && !tLayoutBlockName.isEmpty()) { 613 setLayoutBlockByName(tLayoutBlockName); 614 } 615 tLayoutBlockName = null; /// release this memory 616 617 if (tBufferSignalMastName != null && !tBufferSignalMastName.isEmpty()) { 618 setBufferSignalMast(tBufferSignalMastName); 619 } 620 tBufferSignalMastName = null; 621 if (tExitSignalMastName != null && !tExitSignalMastName.isEmpty()) { 622 setExitSignalMast(tExitSignalMastName); 623 } 624 tExitSignalMastName = null; 625 626 rayTrackList.forEach((rt) -> { 627 rt.setConnect(p.getFinder().findTrackSegmentByName(rt.connectName)); 628 if (rt.approachMastName != null && !rt.approachMastName.isEmpty()) { 629 rt.setApproachMast(rt.approachMastName); 630 } 631 }); 632 } 633 634 /** 635 * Is this turntable turnout controlled? 636 * 637 * @return true if so 638 */ 639 public boolean isTurnoutControlled() { 640 return turnoutControlled; 641 } 642 643 /** 644 * Set if this turntable is turnout controlled. 645 * 646 * @param boo set true if so 647 */ 648 public void setTurnoutControlled(boolean boo) { 649 turnoutControlled = boo; 650 } 651 652 /** 653 * Set turntable position to the ray with this index. 654 * 655 * @param index the index 656 */ 657 public void setPosition(int index) { 658 if (isTurnoutControlled()) { 659 boolean found = false; // assume failure (pessimist!) 660 for (RayTrack rt : rayTrackList) { 661 if (rt.getConnectionIndex() == index) { 662 commandedIndex = index; 663 rt.setPosition(); 664 models.redrawPanel(); 665 models.setDirty(); 666 found = true; 667 break; 668 } 669 } 670 if (!found) { 671 log.error("{}.setPosition({}); Attempt to set the position on a non-existant ray track", 672 getName(), index); 673 } 674 } 675 } 676 677 /** 678 * Get the turntable position. 679 * 680 * @return the turntable position 681 */ 682 public int getPosition() { 683 return knownIndex; 684 } 685 686 public int getCommandedPosition() { 687 return commandedIndex; 688 } 689 690 /** 691 * Delete this ray track. 692 * 693 * @param rayTrack the ray track 694 */ 695 public void deleteRay(@Nonnull RayTrack rayTrack) { 696 TrackSegment t = null; 697 if (rayTrackList == null) { 698 log.error("{}.deleteRay(null); rayTrack is null", getName()); // NOI18N 699 } else { 700 t = rayTrack.getConnect(); 701 getRayTrackList().remove(rayTrack); 702 rayTrack.dispose(); 703 } 704 if (t != null) { 705 models.removeTrackSegment(t); 706 } 707 708 // update the panel 709 models.redrawPanel(); 710 models.setDirty(); 711 } 712 713 /** 714 * Remove this object from display and persistance. 715 */ 716 public void remove() { 717 // remove from persistance by flagging inactive 718 active = false; 719 } 720 721 private boolean active = true; 722 723 /** 724 * Get if turntable is active. 725 * "active" means that the object is still displayed, and should be stored. 726 * @return true if active, else false. 727 */ 728 public boolean isActive() { 729 return active; 730 } 731 732 /** 733 * Checks if the given mast is an approach mast for any ray on this turntable. 734 * @param mast The SignalMast to check. 735 * @return true if it is an approach mast for one of the rays. 736 */ 737 public boolean isApproachMast(SignalMast mast) { 738 if (mast == null) { 739 return false; 740 } 741 for (RayTrack ray : rayTrackList) { 742 if (mast.equals(ray.getApproachMast())) { 743 return true; 744 } 745 } 746 return false; 747 } 748 749 /** 750 * Checks if the given block is one of the ray blocks for this turntable. 751 * @param block The Block to check. 752 * @return true if it is a block for one of the rays. 753 */ 754 public boolean isRayBlock(Block block) { 755 if (block == null) { 756 return false; 757 } 758 for (RayTrack ray : rayTrackList) { 759 TrackSegment ts = ray.getConnect(); 760 if (ts != null && ts.getLayoutBlock() != null && block.equals(ts.getLayoutBlock().getBlock())) { 761 return true; 762 } 763 } 764 return false; 765 } 766 767 768 public class RayTrack { 769 770 /** 771 * constructor for RayTracks 772 * 773 * @param angle its angle 774 * @param index its index 775 */ 776 public RayTrack(double angle, int index) { 777 rayAngle = MathUtil.wrapPM360(angle); 778 connect = null; 779 connectionIndex = index; 780 781 disabled = false; 782 disableWhenOccupied = false; 783 } 784 785 // persistant instance variables 786 private double rayAngle = 0.0; 787 private TrackSegment connect = null; 788 private int connectionIndex = -1; 789 790 private boolean disabled = false; 791 private boolean disableWhenOccupied = false; 792 private NamedBeanHandle<SignalMast> approachMast; 793 794 // 795 // Accessor routines 796 // 797 /** 798 * Set ray track disabled. 799 * 800 * @param boo set true to disable 801 */ 802 public void setDisabled(boolean boo) { 803 if (disabled != boo) { 804 disabled = boo; 805 if (models != null) { 806 models.redrawPanel(); 807 } 808 } 809 } 810 811 /** 812 * Is this ray track disabled? 813 * 814 * @return true if so 815 */ 816 public boolean isDisabled() { 817 return disabled; 818 } 819 820 /** 821 * Set ray track disabled if occupied. 822 * 823 * @param boo set true to disable if occupied 824 */ 825 public void setDisabledWhenOccupied(boolean boo) { 826 if (disableWhenOccupied != boo) { 827 disableWhenOccupied = boo; 828 if (models != null) { 829 models.redrawPanel(); 830 } 831 } 832 } 833 834 /** 835 * Is ray track disabled if occupied? 836 * 837 * @return true if so 838 */ 839 public boolean isDisabledWhenOccupied() { 840 return disableWhenOccupied; 841 } 842 843 /** 844 * get the track segment connected to this ray 845 * 846 * @return the track segment connected to this ray 847 */ 848 // @CheckForNull termporary until we know whether this really can be null or not 849 public TrackSegment getConnect() { 850 return connect; 851 } 852 853 /** 854 * Set the track segment connected to this ray. 855 * 856 * @param ts the track segment to connect to this ray 857 */ 858 public void setConnect(TrackSegment ts) { 859 connect = ts; 860 } 861 862 /** 863 * Get the angle for this ray. 864 * 865 * @return the angle for this ray 866 */ 867 public double getAngle() { 868 return rayAngle; 869 } 870 871 /** 872 * Set the angle for this ray. 873 * 874 * @param an the angle for this ray 875 */ 876 public void setAngle(double an) { 877 rayAngle = MathUtil.wrapPM360(an); 878 } 879 880 /** 881 * Get the connection index for this ray. 882 * 883 * @return the connection index for this ray 884 */ 885 public int getConnectionIndex() { 886 return connectionIndex; 887 } 888 889 /** 890 * Get the approach signal mast for this ray. 891 * @return The signal mast, or null. 892 */ 893 public SignalMast getApproachMast() { 894 if (approachMast == null) { 895 return null; 896 } 897 return approachMast.getBean(); 898 } 899 900 public String getApproachMastName() { 901 if (approachMast == null) { 902 return ""; 903 } 904 return approachMast.getName(); 905 } 906 907 /** 908 * Set the approach signal mast for this ray by name. 909 * @param name The name of the signal mast. 910 */ 911 public void setApproachMast(String name) { 912 if (name == null || name.isEmpty()) { 913 approachMast = null; 914 return; 915 } 916 SignalMast mast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(name); 917 if (mast != null) { 918 approachMast = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(name, mast); 919 } else { 920 approachMast = null; 921 } 922 } 923 924 /** 925 * Is this ray occupied? 926 * 927 * @return true if occupied 928 */ 929 public boolean isOccupied() { // temporary - accessed by View - is this topology or visualization? 930 boolean result = false; // assume not 931 if (connect != null) { // does it have a connection? (yes) 932 LayoutBlock lb = connect.getLayoutBlock(); 933 if (lb != null) { // does the connection have a block? (yes) 934 // is the block occupied? 935 result = (lb.getOccupancy() == LayoutBlock.OCCUPIED); 936 } 937 } 938 return result; 939 } 940 941 // initialization instance variable (used when loading a LayoutEditor) 942 public String connectName = ""; 943 public String approachMastName = ""; 944 945 private NamedBeanHandle<Turnout> namedTurnout; 946 // Turnout t; 947 private int turnoutState; 948 private PropertyChangeListener mTurnoutListener; 949 950 /** 951 * Set the turnout and state for this ray track. 952 * 953 * @param turnoutName the turnout name 954 * @param state its state 955 */ 956 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", 957 justification="2nd check of turnoutName is considered redundant by SpotBugs, but required by ecj") // temporary 958 public void setTurnout(@Nonnull String turnoutName, int state) { 959 Turnout turnout = null; 960 if (mTurnoutListener == null) { 961 mTurnoutListener = (PropertyChangeEvent e) -> { // Lambda expression for listener 962 if ("KnownState".equals(e.getPropertyName())) { 963 int turnoutState = (Integer) e.getNewValue(); 964 if (turnoutState == Turnout.THROWN) { 965 // This ray is now the active one. Update the turntable's known position. 966 knownIndex = connectionIndex; 967 } else if (turnoutState == Turnout.CLOSED) { 968 // If the currently known active ray is now closed, turntable is un-aligned. 969 if (knownIndex == connectionIndex) { 970 knownIndex = -1; 971 } 972 } 973 models.redrawPanel(); 974 models.setDirty(); 975 } else if ("CommandedState".equals(e.getPropertyName())) { 976 if ((Integer) e.getNewValue() == Turnout.THROWN) { 977 commandedIndex = connectionIndex; 978 models.redrawPanel(); 979 models.setDirty(); 980 981 // This is the "Smart Listener" logic. 982 // If this ray was commanded THROWN, ensure all other rays are commanded CLOSED. 983 if ((Integer) e.getNewValue() == Turnout.THROWN) { 984 log.debug("Turntable Ray {} commanded THROWN, ensuring other rays are CLOSED.", connectionIndex); 985 for (RayTrack otherRay : LayoutTurntable.this.rayTrackList) { 986 // Check this isn't the current ray and that it has a turnout assigned. 987 if (otherRay.getConnectionIndex() != connectionIndex && otherRay.getTurnout() != null) { 988 // Only send the command if it's not already CLOSED to avoid loops. 989 if (otherRay.getTurnout().getCommandedState() != Turnout.CLOSED) { 990 otherRay.getTurnout().setCommandedState(Turnout.CLOSED); 991 } 992 } 993 } 994 } 995 } 996 } 997 }; 998 } 999 if (turnoutName != null) { 1000 turnout = jmri.InstanceManager.turnoutManagerInstance().getTurnout(turnoutName); 1001 } 1002 if (namedTurnout != null && namedTurnout.getBean() != turnout) { 1003 namedTurnout.getBean().removePropertyChangeListener(mTurnoutListener); 1004 } 1005 if (turnout != null) { 1006 if (turnoutName != null && !turnoutName.isEmpty()) { 1007 namedTurnout = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(turnoutName, turnout); 1008 turnout.addPropertyChangeListener(mTurnoutListener, turnoutName, "Layout Editor Turntable"); 1009 } 1010 } 1011 if (turnout == null) { 1012 namedTurnout = null; 1013 } 1014 1015 if (this.turnoutState != state) { 1016 this.turnoutState = state; 1017 } 1018 } 1019 1020 /** 1021 * Set the position for this ray track. 1022 */ 1023 public void setPosition() { 1024 if (namedTurnout != null) { 1025 if (disableWhenOccupied && isOccupied()) { // isOccupied is on RayTrack, so check must be here 1026 log.debug("Can not setPosition of turntable ray when it is occupied"); 1027 } else { 1028 // This method is now only called by manual clicks on the panel. 1029 // The listener above handles the interlocking for all command sources. 1030 getTurnout().setCommandedState(Turnout.THROWN); 1031 } 1032 } 1033 } 1034 1035 /** 1036 * Get the turnout for this ray track. 1037 * 1038 * @return the turnout or null 1039 */ 1040 // @CheckForNull temporary until we have central paradigm for null 1041 public Turnout getTurnout() { 1042 if (namedTurnout == null) { 1043 return null; 1044 } 1045 return namedTurnout.getBean(); 1046 } 1047 1048 /** 1049 * Get the turnout name for the ray track. 1050 * 1051 * @return the turnout name 1052 */ 1053 @CheckForNull 1054 public String getTurnoutName() { 1055 if (namedTurnout == null) { 1056 return null; 1057 } 1058 return namedTurnout.getName(); 1059 } 1060 1061 /** 1062 * Get the state for the turnout for this ray track. 1063 * 1064 * @return the state 1065 */ 1066 public int getTurnoutState() { 1067 return turnoutState; 1068 } 1069 1070 /** 1071 * Dispose of this ray track. 1072 */ 1073 void dispose() { 1074 if (getTurnout() != null) { 1075 getTurnout().removePropertyChangeListener(mTurnoutListener); 1076 } 1077 if (knownIndex == connectionIndex) { 1078 knownIndex = -1; 1079 } 1080 } 1081 } // class RayTrack 1082 1083 /** 1084 * {@inheritDoc} 1085 */ 1086 @Override 1087 protected void reCheckBlockBoundary() { 1088 // nothing to see here... move along... 1089 } 1090 1091 /** 1092 * {@inheritDoc} 1093 */ 1094 @Override 1095 @CheckForNull 1096 protected List<LayoutConnectivity> getLayoutConnectivity() { 1097 // nothing to see here... move along... 1098 return null; 1099 } 1100 1101 /** 1102 * {@inheritDoc} 1103 */ 1104 @Override 1105 @Nonnull 1106 public List<HitPointType> checkForFreeConnections() { 1107 List<HitPointType> result = new ArrayList<>(); 1108 1109 for (int k = 0; k < getNumberRays(); k++) { 1110 if (getRayConnectOrdered(k) == null) { 1111 result.add(HitPointType.turntableTrackIndexedValue(k)); 1112 } 1113 } 1114 return result; 1115 } 1116 1117 /** 1118 * {@inheritDoc} 1119 */ 1120 @Override 1121 public boolean checkForUnAssignedBlocks() { 1122 // Layout turnouts get their block information from the 1123 // track segments attached to their rays so... 1124 // nothing to see here... move along... 1125 return true; 1126 } 1127 1128 /** 1129 * Checks if the path represented by the blocks crosses this turntable. 1130 * @param block1 A block in the path. 1131 * @param block2 Another block in the path. 1132 * @return true if the path crosses this turntable. 1133 */ 1134 public boolean isTurntableBoundary(Block block1, Block block2) { 1135 if (getLayoutBlock() == null) { 1136 return false; 1137 } 1138 Block turntableBlock = getLayoutBlock().getBlock(); 1139 if (turntableBlock == null) { 1140 return false; 1141 } 1142 1143 // Case 1: Moving to/from the turntable block itself. 1144 if ((block1 == turntableBlock && isRayBlock(block2)) || 1145 (block2 == turntableBlock && isRayBlock(block1))) { 1146 return true; 1147 } 1148 1149 // Case 2: Moving between two ray blocks (crossing over the turntable). 1150 if (isRayBlock(block1) && isRayBlock(block2)) { 1151 return true; 1152 } 1153 return false; 1154 } 1155 1156 /** 1157 * Gets the list of turnouts and their required states to align the turntable 1158 * for a path defined by the given blocks. 1159 * 1160 * @param curBlock The current block in the train's path. 1161 * @param prevBlock The previous block in the train's path. 1162 * @param nextBlock The next block in the train's path. 1163 * @return A list of LayoutTrackExpectedState objects for the turnouts. 1164 */ 1165 public List<LayoutTrackExpectedState<LayoutTurnout>> getTurnoutList(Block curBlock, Block prevBlock, Block nextBlock) { 1166 List<LayoutTrackExpectedState<LayoutTurnout>> turnoutList = new ArrayList<>(); 1167 if (!isTurnoutControlled()) { 1168 return turnoutList; 1169 } 1170 1171 Block turntableBlock = (getLayoutBlock() != null) ? getLayoutBlock().getBlock() : null; 1172 if (turntableBlock == null) { 1173 return turnoutList; 1174 } 1175 1176 int targetRay = -1; 1177 1178 // Determine which ray needs to be aligned. 1179 if (prevBlock == turntableBlock) { 1180 // Train is leaving the turntable, so align to the destination ray. 1181 targetRay = getRayForBlock(curBlock); 1182 } else if (curBlock == turntableBlock) { 1183 // Train is entering the turntable, so align to the approaching ray. 1184 targetRay = getRayForBlock(prevBlock); 1185 } 1186 1187 if (targetRay != -1) { 1188 Turnout t = getRayTurnout(targetRay); 1189 if (t != null) { 1190 // Create a temporary LayoutTurnout wrapper for the dispatcher. 1191 // This object is not on a panel and is for logic purposes only. 1192 // We give the dispatcher our real turnout, and our "Smart Listener" 1193 // on CommandedState will handle the interlocking. 1194 LayoutLHTurnout tempLayoutTurnout = new LayoutLHTurnout("TURNTABLE_WRAPPER_" + getId(), models) { // NOI18N 1195 @Override 1196 public Turnout getTurnout() { 1197 // Return the real turnout. 1198 return t; 1199 } 1200 }; 1201 // We must associate the temp layout turnout with the real turnout to satisfy the dispatcher framework 1202 tempLayoutTurnout.setTurnout(t.getSystemName()); 1203 int requiredState = Turnout.THROWN; 1204 1205 log.debug("Adding turntable turnout {} to list with required state {}", t.getDisplayName(), requiredState); 1206 turnoutList.add(new LayoutTrackExpectedState<>(tempLayoutTurnout, requiredState)); 1207 } 1208 } 1209 return turnoutList; 1210 } 1211 1212 private int getRayForBlock(Block block) { 1213 if (block == null) return -1; 1214 for (int i = 0; i < getNumberRays(); i++) { 1215 TrackSegment ts = getRayConnectOrdered(i); 1216 if (ts != null && ts.getLayoutBlock() != null && ts.getLayoutBlock().getBlock() == block) { 1217 return i; 1218 } 1219 } 1220 return -1; 1221 } 1222 1223 /** 1224 * {@inheritDoc} 1225 */ 1226 @Override 1227 public void checkForNonContiguousBlocks( 1228 @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetsMap) { 1229 /* 1230 * For each (non-null) blocks of this track do: 1231 * #1) If it's got an entry in the blockNamesToTrackNameSetMap then 1232 * #2) If this track is already in the TrackNameSet for this block 1233 * then return (done!) 1234 * #3) else add a new set (with this block// track) to 1235 * blockNamesToTrackNameSetMap and check all the connections in this 1236 * block (by calling the 2nd method below) 1237 * <p> 1238 * Basically, we're maintaining contiguous track sets for each block found 1239 * (in blockNamesToTrackNameSetMap) 1240 */ 1241 1242 // We're using a map here because it is convient to 1243 // use it to pair up blocks and connections 1244 Map<LayoutTrack, String> blocksAndTracksMap = new HashMap<>(); 1245 for (int k = 0; k < getNumberRays(); k++) { 1246 TrackSegment ts = getRayConnectOrdered(k); 1247 if (ts != null) { 1248 String blockName = ts.getBlockName(); 1249 blocksAndTracksMap.put(ts, blockName); 1250 } 1251 } 1252 1253 List<Set<String>> TrackNameSets; 1254 Set<String> TrackNameSet; 1255 for (Map.Entry<LayoutTrack, String> entry : blocksAndTracksMap.entrySet()) { 1256 LayoutTrack theConnect = entry.getKey(); 1257 String theBlockName = entry.getValue(); 1258 1259 TrackNameSet = null; // assume not found (pessimist!) 1260 TrackNameSets = blockNamesToTrackNameSetsMap.get(theBlockName); 1261 if (TrackNameSets != null) { // (#1) 1262 for (Set<String> checkTrackNameSet : TrackNameSets) { 1263 if (checkTrackNameSet.contains(getName())) { // (#2) 1264 TrackNameSet = checkTrackNameSet; 1265 break; 1266 } 1267 } 1268 } else { // (#3) 1269 log.debug("*New block (''{}'') trackNameSets", theBlockName); 1270 TrackNameSets = new ArrayList<>(); 1271 blockNamesToTrackNameSetsMap.put(theBlockName, TrackNameSets); 1272 } 1273 if (TrackNameSet == null) { 1274 TrackNameSet = new LinkedHashSet<>(); 1275 TrackNameSets.add(TrackNameSet); 1276 } 1277 if (TrackNameSet.add(getName())) { 1278 log.debug("* Add track ''{}'' to trackNameSet for block ''{}''", getName(), theBlockName); 1279 } 1280 theConnect.collectContiguousTracksNamesInBlockNamed(theBlockName, TrackNameSet); 1281 } 1282 } 1283 1284 /** 1285 * {@inheritDoc} 1286 */ 1287 @Override 1288 public void collectContiguousTracksNamesInBlockNamed(@Nonnull String blockName, 1289 @Nonnull Set<String> TrackNameSet) { 1290 if (!TrackNameSet.contains(getName())) { 1291 // for all the rays with matching blocks in this turnout 1292 // #1) if its track segment's block is in this block 1293 // #2) add turntable to TrackNameSet (if not already there) 1294 // #3) if the track segment isn't in the TrackNameSet 1295 // #4) flood it 1296 for (int k = 0; k < getNumberRays(); k++) { 1297 TrackSegment ts = getRayConnectOrdered(k); 1298 if (ts != null) { 1299 String blk = ts.getBlockName(); 1300 if ((!blk.isEmpty()) && (blk.equals(blockName))) { // (#1) 1301 // if we are added to the TrackNameSet 1302 if (TrackNameSet.add(getName())) { 1303 log.debug("* Add track ''{}'' for block ''{}''", getName(), blockName); 1304 } 1305 // it's time to play... flood your neighbours! 1306 ts.collectContiguousTracksNamesInBlockNamed(blockName, 1307 TrackNameSet); // (#4) 1308 } 1309 } 1310 } 1311 } 1312 } 1313 1314 /** 1315 * {@inheritDoc} 1316 */ 1317 @Override 1318 public void setAllLayoutBlocks(LayoutBlock layoutBlock) { 1319 // turntables don't have blocks... 1320 // nothing to see here, move along... 1321 } 1322 1323 /** 1324 * {@inheritDoc} 1325 */ 1326 @Override 1327 public boolean canRemove() { 1328 return true; 1329 } 1330 1331 /** 1332 * {@inheritDoc} 1333 */ 1334 @Override 1335 public String getTypeName() { 1336 return Bundle.getMessage("TypeName_Turntable"); 1337 } 1338 1339 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTurntable.class); 1340 1341}