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}