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