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