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 turnoutControlled = false;
064    private double radius = 25.0;
065    private int lastKnownIndex = -1;
066
067    // persistent instance variables (saved between sessions)
068
069    // temporary: this is referenced directly from LayoutTurntable, which
070    // should be using _functional_ accessors here.
071    public final List<RayTrack> rayTrackList = new ArrayList<>(); // list of Ray Track objects
072
073    /**
074     * Get a string that represents this object. This should only be used for
075     * debugging.
076     *
077     * @return the string
078     */
079    @Override
080    @Nonnull
081    public String toString() {
082        return "LayoutTurntable " + getName();
083    }
084
085    //
086    // Accessor methods
087    //
088    /**
089     * Get the radius for this turntable.
090     *
091     * @return the radius for this turntable
092     */
093    public double getRadius() {
094        return radius;
095    }
096
097    /**
098     * Set the radius for this turntable.
099     *
100     * @param r the radius for this turntable
101     */
102    public void setRadius(double r) {
103        radius = r;
104    }
105
106    /**
107     * @return the layout block name
108     */
109    @Nonnull
110    public String getBlockName() {
111        String result = null;
112        if (namedLayoutBlock != null) {
113            result = namedLayoutBlock.getName();
114        }
115        return ((result == null) ? "" : result);
116    }
117
118    /**
119     * @return the layout block
120     */
121    @CheckForNull
122    public LayoutBlock getLayoutBlock() {
123        return (namedLayoutBlock != null) ? namedLayoutBlock.getBean() : null;
124    }
125
126    /**
127     * Set up a LayoutBlock for this LayoutTurntable.
128     *
129     * @param newLayoutBlock the LayoutBlock to set
130     */
131    public void setLayoutBlock(@CheckForNull LayoutBlock newLayoutBlock) {
132        LayoutBlock layoutBlock = getLayoutBlock();
133        if (layoutBlock != newLayoutBlock) {
134            /// block has changed, if old block exists, decrement use
135            if (layoutBlock != null) {
136                layoutBlock.decrementUse();
137            }
138            if (newLayoutBlock != null) {
139                String newName = newLayoutBlock.getUserName();
140                if ((newName != null) && !newName.isEmpty()) {
141                    namedLayoutBlock = InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(newName, newLayoutBlock);
142                } else {
143                    namedLayoutBlock = null;
144                }
145            } else {
146                namedLayoutBlock = null;
147            }
148        }
149    }
150
151    /**
152     * Set up a LayoutBlock for this LayoutTurntable.
153     *
154     * @param name the name of the new LayoutBlock
155     */
156    public void setLayoutBlockByName(@CheckForNull String name) {
157        if ((name != null) && !name.isEmpty()) {
158            setLayoutBlock(models.provideLayoutBlock(name));
159        }
160    }
161
162    /**
163     * Add a ray at the specified angle.
164     *
165     * @param angle the angle
166     * @return the RayTrack
167     */
168    public RayTrack addRay(double angle) {
169        RayTrack rt = new RayTrack(angle, getNewIndex());
170        rayTrackList.add(rt);
171        return rt;
172    }
173
174    private int getNewIndex() {
175        int index = -1;
176        if (rayTrackList.isEmpty()) {
177            return 0;
178        }
179
180        boolean found = true;
181        while (found) {
182            index++;
183            found = false; // assume failure (pessimist!)
184            for (RayTrack rt : rayTrackList) {
185                if (index == rt.getConnectionIndex()) {
186                    found = true;
187                }
188            }
189        }
190        return index;
191    }
192
193    // the following method is only for use in loading layout turntables
194    public void addRayTrack(double angle, int index, String name) {
195        RayTrack rt = new RayTrack(angle, index);
196        /// if (ray!=null) {
197        rayTrackList.add(rt);
198        rt.connectName = name;
199        //}
200    }
201
202    /**
203     * Get the connection for the ray with this index.
204     *
205     * @param index the index
206     * @return the connection for the ray with this value of getConnectionIndex
207     */
208    @CheckForNull
209    public TrackSegment getRayConnectIndexed(int index) {
210        TrackSegment result = null;
211        for (RayTrack rt : rayTrackList) {
212            if (rt.getConnectionIndex() == index) {
213                result = rt.getConnect();
214                break;
215            }
216        }
217        return result;
218    }
219
220    /**
221     * Get the connection for the ray at the index in the rayTrackList.
222     *
223     * @param i the index in the rayTrackList
224     * @return the connection for the ray at that index in the rayTrackList or null
225     */
226    @CheckForNull
227    public TrackSegment getRayConnectOrdered(int i) {
228        TrackSegment result = null;
229
230        if (i < rayTrackList.size()) {
231            RayTrack rt = rayTrackList.get(i);
232            if (rt != null) {
233                result = rt.getConnect();
234            }
235        }
236        return result;
237    }
238
239    /**
240     * Set the connection for the ray at the index in the rayTrackList.
241     *
242     * @param ts    the connection
243     * @param index the index in the rayTrackList
244     */
245    public void setRayConnect(@CheckForNull TrackSegment ts, int index) {
246        for (RayTrack rt : rayTrackList) {
247            if (rt.getConnectionIndex() == index) {
248                rt.setConnect(ts);
249                break;
250            }
251        }
252    }
253
254    // should only be used by xml save code
255    @Nonnull
256    public List<RayTrack> getRayTrackList() {
257        return rayTrackList;
258    }
259
260    /**
261     * Get the number of rays on turntable.
262     *
263     * @return the number of rays
264     */
265    public int getNumberRays() {
266        return rayTrackList.size();
267    }
268
269    /**
270     * Get the index for the ray at this position in the rayTrackList.
271     *
272     * @param i the position in the rayTrackList
273     * @return the index
274     */
275    public int getRayIndex(int i) {
276        int result = 0;
277        if (i < rayTrackList.size()) {
278            RayTrack rt = rayTrackList.get(i);
279            result = rt.getConnectionIndex();
280        }
281        return result;
282    }
283
284    /**
285     * Get the angle for the ray at this position in the rayTrackList.
286     *
287     * @param i the position in the rayTrackList
288     * @return the angle
289     */
290    public double getRayAngle(int i) {
291        double result = 0.0;
292        if (i < rayTrackList.size()) {
293            RayTrack rt = rayTrackList.get(i);
294            result = rt.getAngle();
295        }
296        return result;
297    }
298
299    /**
300     * Set the turnout and state for the ray with this index.
301     *
302     * @param index       the index
303     * @param turnoutName the turnout name
304     * @param state       the state
305     */
306    public void setRayTurnout(int index, @CheckForNull String turnoutName, int state) {
307        boolean found = false; // assume failure (pessimist!)
308        for (RayTrack rt : rayTrackList) {
309            if (rt.getConnectionIndex() == index) {
310                rt.setTurnout(turnoutName, state);
311                found = true;
312                break;
313            }
314        }
315        if (!found) {
316            log.error("{}.setRayTurnout({}, {}, {}); Attempt to add Turnout control to a non-existant ray track",
317                    getName(), index, turnoutName, state);
318        }
319    }
320
321    /**
322     * Get the name of the turnout for the ray at this index.
323     *
324     * @param i the index
325     * @return name of the turnout for the ray at this index
326     */
327    @CheckForNull
328    public String getRayTurnoutName(int i) {
329        String result = null;
330        if (i < rayTrackList.size()) {
331            RayTrack rt = rayTrackList.get(i);
332            result = rt.getTurnoutName();
333        }
334        return result;
335    }
336
337    /**
338     * Get the turnout for the ray at this index.
339     *
340     * @param i the index
341     * @return the turnout for the ray at this index
342     */
343    @CheckForNull
344    public Turnout getRayTurnout(int i) {
345        Turnout result = null;
346        if (i < rayTrackList.size()) {
347            RayTrack rt = rayTrackList.get(i);
348            result = rt.getTurnout();
349        }
350        return result;
351    }
352
353    /**
354     * Get the state of the turnout for the ray at this index.
355     *
356     * @param i the index
357     * @return state of the turnout for the ray at this index
358     */
359    public int getRayTurnoutState(int i) {
360        int result = 0;
361        if (i < rayTrackList.size()) {
362            RayTrack rt = rayTrackList.get(i);
363            result = rt.getTurnoutState();
364        }
365        return result;
366    }
367
368    /**
369     * Get if the ray at this index is disabled.
370     *
371     * @param i the index
372     * @return true if disabled
373     */
374    public boolean isRayDisabled(int i) {
375        boolean result = false;    // assume not disabled
376        if (i < rayTrackList.size()) {
377            RayTrack rt = rayTrackList.get(i);
378            result = rt.isDisabled();
379        }
380        return result;
381    }
382
383    /**
384     * Set the disabled state of the ray at this index.
385     *
386     * @param i   the index
387     * @param boo the state
388     */
389    public void setRayDisabled(int i, boolean boo) {
390        if (i < rayTrackList.size()) {
391            RayTrack rt = rayTrackList.get(i);
392            rt.setDisabled(boo);
393        }
394    }
395
396    /**
397     * Get the disabled when occupied state of the ray at this index.
398     *
399     * @param i the index
400     * @return the state
401     */
402    public boolean isRayDisabledWhenOccupied(int i) {
403        boolean result = false;    // assume not disabled when occupied
404        if (i < rayTrackList.size()) {
405            RayTrack rt = rayTrackList.get(i);
406            result = rt.isDisabledWhenOccupied();
407        }
408        return result;
409    }
410
411    /**
412     * Set the disabled when occupied state of the ray at this index.
413     *
414     * @param i   the index
415     * @param boo the state
416     */
417    public void setRayDisabledWhenOccupied(int i, boolean boo) {
418        if (i < rayTrackList.size()) {
419            RayTrack rt = rayTrackList.get(i);
420            rt.setDisabledWhenOccupied(boo);
421        }
422    }
423
424    /**
425     * {@inheritDoc}
426     */
427    @Override
428    public LayoutTrack getConnection(HitPointType connectionType) throws jmri.JmriException {
429        LayoutTrack result = null;
430        if (HitPointType.isTurntableRayHitType(connectionType)) {
431            result = getRayConnectIndexed(connectionType.turntableTrackIndex());
432        } else {
433            String errString = MessageFormat.format("{0}.getCoordsForConnectionType({1}); Invalid connection type",
434                    getName(), connectionType); // NOI18N
435            log.error("will throw {}", errString); // NOI18N
436            throw new jmri.JmriException(errString);
437        }
438        return result;
439    }
440
441    /**
442     * {@inheritDoc}
443     */
444    @Override
445    public void setConnection(HitPointType connectionType, @CheckForNull LayoutTrack o, HitPointType type) throws jmri.JmriException {
446        if ((type != HitPointType.TRACK) && (type != HitPointType.NONE)) {
447            String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid type",
448                    getName(), connectionType, (o == null) ? "null" : o.getName(), type); // NOI18N
449            log.error("will throw {}", errString); // NOI18N
450            throw new jmri.JmriException(errString);
451        }
452        if (HitPointType.isTurntableRayHitType(connectionType)) {
453            if ((o == null) || (o instanceof TrackSegment)) {
454                setRayConnect((TrackSegment) o, connectionType.turntableTrackIndex());
455            } else {
456                String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid object: {4}",
457                        getName(), connectionType, o.getName(),
458                        type, o.getClass().getName()); // NOI18N
459                log.error("will throw {}", errString); // NOI18N
460                throw new jmri.JmriException(errString);
461            }
462        } else {
463            String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid connection type",
464                    getName(), connectionType, (o == null) ? "null" : o.getName(), type); // NOI18N
465            log.error("will throw {}", errString); // NOI18N
466            throw new jmri.JmriException(errString);
467        }
468    }
469
470    /**
471     * Test if ray with this index is a mainline track or not.
472     * <p>
473     * Defaults to false (not mainline) if connecting track segment is missing.
474     *
475     * @param index the index
476     * @return true if connecting track segment is mainline
477     */
478    public boolean isMainlineIndexed(int index) {
479        boolean result = false; // assume failure (pessimist!)
480
481        for (RayTrack rt : rayTrackList) {
482            if (rt.getConnectionIndex() == index) {
483                TrackSegment ts = rt.getConnect();
484                if (ts != null) {
485                    result = ts.isMainline();
486                    break;
487                }
488            }
489        }
490        return result;
491    }
492
493    /**
494     * Test if ray at this index is a mainline track or not.
495     * <p>
496     * Defaults to false (not mainline) if connecting track segment is missing
497     *
498     * @param i the index
499     * @return true if connecting track segment is mainline
500     */
501    public boolean isMainlineOrdered(int i) {
502        boolean result = false; // assume failure (pessimist!)
503        if (i < rayTrackList.size()) {
504            RayTrack rt = rayTrackList.get(i);
505            if (rt != null) {
506                TrackSegment ts = rt.getConnect();
507                if (ts != null) {
508                    result = ts.isMainline();
509                }
510            }
511        }
512        return result;
513    }
514
515    @Override
516    public boolean isMainline() {
517        return false;
518    }
519
520
521    public String tLayoutBlockName = "";
522
523    /**
524     * Initialization method The name of each track segment connected to a ray
525     * track is initialized by by LayoutTurntableXml, then the following method
526     * is called after the entire LayoutEditor is loaded to set the specific
527     * TrackSegment objects.
528     *
529     * @param p the layout editor
530     */
531    @Override
532    public void setObjects(@Nonnull LayoutEditor p) {
533        if (tLayoutBlockName != null && !tLayoutBlockName.isEmpty()) {
534            setLayoutBlockByName(tLayoutBlockName);
535        }
536        tLayoutBlockName = null; /// release this memory
537
538        rayTrackList.forEach((rt) -> {
539            rt.setConnect(p.getFinder().findTrackSegmentByName(rt.connectName));
540        });
541    }
542
543    /**
544     * Is this turntable turnout controlled?
545     *
546     * @return true if so
547     */
548    public boolean isTurnoutControlled() {
549        return turnoutControlled;
550    }
551
552    /**
553     * Set if this turntable is turnout controlled.
554     *
555     * @param boo set true if so
556     */
557    public void setTurnoutControlled(boolean boo) {
558        turnoutControlled = boo;
559    }
560
561    /**
562     * Set turntable position to the ray with this index.
563     *
564     * @param index the index
565     */
566    public void setPosition(int index) {
567        if (isTurnoutControlled()) {
568            boolean found = false; // assume failure (pessimist!)
569            for (RayTrack rt : rayTrackList) {
570                if (rt.getConnectionIndex() == index) {
571                    lastKnownIndex = index;
572                    rt.setPosition();
573                    models.redrawPanel();
574                    models.setDirty();
575                    found = true;
576                    break;
577                }
578            }
579            if (!found) {
580                log.error("{}.setPosition({}); Attempt to set the position on a non-existant ray track",
581                        getName(), index);
582            }
583        }
584    }
585
586    /**
587     * Get the turntable position.
588     *
589     * @return the turntable position
590     */
591    public int getPosition() {
592        return lastKnownIndex;
593    }
594
595    /**
596     * Delete this ray track.
597     *
598     * @param rayTrack the ray track
599     */
600    public void deleteRay(@Nonnull RayTrack rayTrack) {
601        TrackSegment t = null;
602        if (rayTrackList == null) {
603            log.error("{}.deleteRay(null); rayTrack is null", getName());
604        } else {
605            t = rayTrack.getConnect();
606            getRayTrackList().remove(rayTrack.getConnectionIndex());
607            rayTrack.dispose();
608        }
609        if (t != null) {
610            models.removeTrackSegment(t);
611        }
612
613        // update the panel
614        models.redrawPanel();
615        models.setDirty();
616    }
617
618    /**
619     * Remove this object from display and persistance.
620     */
621    public void remove() {
622        // remove from persistance by flagging inactive
623        active = false;
624    }
625
626    private boolean active = true;
627
628    /**
629     * Get if turntable is active.
630     * "active" means that the object is still displayed, and should be stored.
631     * @return true if active, else false.
632     */
633    public boolean isActive() {
634        return active;
635    }
636
637    public class RayTrack {
638
639        /**
640         * constructor for RayTracks
641         *
642         * @param angle its angle
643         * @param index its index
644         */
645        public RayTrack(double angle, int index) {
646            rayAngle = MathUtil.wrapPM360(angle);
647            connect = null;
648            connectionIndex = index;
649
650            disabled = false;
651            disableWhenOccupied = false;
652        }
653
654        // persistant instance variables
655        private double rayAngle = 0.0;
656        private TrackSegment connect = null;
657        private int connectionIndex = -1;
658
659        private boolean disabled = false;
660        private boolean disableWhenOccupied = false;
661
662        //
663        // Accessor routines
664        //
665        /**
666         * Set ray track disabled.
667         *
668         * @param boo set true to disable
669         */
670        public void setDisabled(boolean boo) {
671            if (disabled != boo) {
672                disabled = boo;
673                if (models != null) {
674                    models.redrawPanel();
675                }
676            }
677        }
678
679        /**
680         * Is this ray track disabled?
681         *
682         * @return true if so
683         */
684        public boolean isDisabled() {
685            return disabled;
686        }
687
688        /**
689         * Set ray track disabled if occupied.
690         *
691         * @param boo set true to disable if occupied
692         */
693        public void setDisabledWhenOccupied(boolean boo) {
694            if (disableWhenOccupied != boo) {
695                disableWhenOccupied = boo;
696                if (models != null) {
697                    models.redrawPanel();
698                }
699            }
700        }
701
702        /**
703         * Is ray track disabled if occupied?
704         *
705         * @return true if so
706         */
707        public boolean isDisabledWhenOccupied() {
708            return disableWhenOccupied;
709        }
710
711        /**
712         * get the track segment connected to this ray
713         *
714         * @return the track segment connected to this ray
715         */
716        // @CheckForNull termporary until we know whether this really can be null or not
717        public TrackSegment getConnect() {
718            return connect;
719        }
720
721        /**
722         * Set the track segment connected to this ray.
723         *
724         * @param ts the track segment to connect to this ray
725         */
726        public void setConnect(TrackSegment ts) {
727            connect = ts;
728        }
729
730        /**
731         * Get the angle for this ray.
732         *
733         * @return the angle for this ray
734         */
735        public double getAngle() {
736            return rayAngle;
737        }
738
739        /**
740         * Set the angle for this ray.
741         *
742         * @param an the angle for this ray
743         */
744        public void setAngle(double an) {
745            rayAngle = MathUtil.wrapPM360(an);
746        }
747
748        /**
749         * Get the connection index for this ray.
750         *
751         * @return the connection index for this ray
752         */
753        public int getConnectionIndex() {
754            return connectionIndex;
755        }
756
757        /**
758         * Is this ray occupied?
759         *
760         * @return true if occupied
761         */
762        public boolean isOccupied() {  // temporary - accessed by View - is this topology or visualization?
763            boolean result = false; // assume not
764            if (connect != null) {  // does it have a connection? (yes)
765                LayoutBlock lb = connect.getLayoutBlock();
766                if (lb != null) {   // does the connection have a block? (yes)
767                    // is the block occupied?
768                    result = (lb.getOccupancy() == LayoutBlock.OCCUPIED);
769                }
770            }
771            return result;
772        }
773
774        // initialization instance variable (used when loading a LayoutEditor)
775        public String connectName = "";
776
777        private NamedBeanHandle<Turnout> namedTurnout;
778        // Turnout t;
779        private int turnoutState;
780        private PropertyChangeListener mTurnoutListener;
781
782        /**
783         * Set the turnout and state for this ray track.
784         *
785         * @param turnoutName the turnout name
786         * @param state       its state
787         */
788        @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE",
789                justification="2nd check of turnoutName is considered redundant by SpotBugs, but required by ecj") // temporary
790        public void setTurnout(@Nonnull String turnoutName, int state) {
791            Turnout turnout = null;
792            if (mTurnoutListener == null) {
793                mTurnoutListener = (PropertyChangeEvent e) -> {
794                    if (getTurnout().getKnownState() == turnoutState) {
795                        lastKnownIndex = connectionIndex;
796                        models.redrawPanel();
797                        models.setDirty();
798                    }
799                };
800            }
801            if (turnoutName != null) {
802                turnout = jmri.InstanceManager.turnoutManagerInstance().getTurnout(turnoutName);
803            }
804            if (namedTurnout != null && namedTurnout.getBean() != turnout) {
805                namedTurnout.getBean().removePropertyChangeListener(mTurnoutListener);
806            }
807            if (turnout != null && (namedTurnout == null || namedTurnout.getBean() != turnout)) {
808                if (turnoutName != null && !turnoutName.isEmpty()) {
809                    namedTurnout = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(turnoutName, turnout);
810                    turnout.addPropertyChangeListener(mTurnoutListener, turnoutName, "Layout Editor Turntable");
811                }
812            }
813            if (turnout == null) {
814                namedTurnout = null;
815            }
816
817            if (this.turnoutState != state) {
818                this.turnoutState = state;
819            }
820        }
821
822        /**
823         * Set the position for this ray track.
824         */
825        public void setPosition() {
826            if (namedTurnout != null) {
827                if (disableWhenOccupied && isOccupied()) {
828                    log.debug("Can not setPosition of turntable ray when it is occupied");
829                } else {
830                    getTurnout().setCommandedState(turnoutState);
831                }
832            }
833        }
834
835        /**
836         * Get the turnout for this ray track.
837         *
838         * @return the turnout or null
839         */
840       // @CheckForNull temporary until we have central paradigm for null
841        public Turnout getTurnout() {
842            if (namedTurnout == null) {
843                return null;
844            }
845            return namedTurnout.getBean();
846        }
847
848        /**
849         * Get the turnout name for the ray track.
850         *
851         * @return the turnout name
852         */
853        @CheckForNull
854        public String getTurnoutName() {
855            if (namedTurnout == null) {
856                return null;
857            }
858            return namedTurnout.getName();
859        }
860
861        /**
862         * Get the state for the turnout for this ray track.
863         *
864         * @return the state
865         */
866        public int getTurnoutState() {
867            return turnoutState;
868        }
869
870        /**
871         * Dispose of this ray track.
872         */
873        void dispose() {
874            if (getTurnout() != null) {
875                getTurnout().removePropertyChangeListener(mTurnoutListener);
876            }
877            if (lastKnownIndex == connectionIndex) {
878                lastKnownIndex = -1;
879            }
880        }
881    }   // class RayTrack
882
883    /**
884     * {@inheritDoc}
885     */
886    @Override
887    protected void reCheckBlockBoundary() {
888        // nothing to see here... move along...
889    }
890
891    /**
892     * {@inheritDoc}
893     */
894    @Override
895    @CheckForNull
896    protected List<LayoutConnectivity> getLayoutConnectivity() {
897        // nothing to see here... move along...
898        return null;
899    }
900
901    /**
902     * {@inheritDoc}
903     */
904    @Override
905    @Nonnull
906    public List<HitPointType> checkForFreeConnections() {
907        List<HitPointType> result = new ArrayList<>();
908
909        for (int k = 0; k < getNumberRays(); k++) {
910            if (getRayConnectOrdered(k) == null) {
911                result.add(HitPointType.turntableTrackIndexedValue(k));
912            }
913        }
914        return result;
915    }
916
917    /**
918     * {@inheritDoc}
919     */
920    @Override
921    public boolean checkForUnAssignedBlocks() {
922        // Layout turnouts get their block information from the
923        // track segments attached to their rays so...
924        // nothing to see here... move along...
925        return true;
926    }
927
928    /**
929     * {@inheritDoc}
930     */
931    @Override
932    public void checkForNonContiguousBlocks(
933            @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetsMap) {
934        /*
935        * For each (non-null) blocks of this track do:
936        * #1) If it's got an entry in the blockNamesToTrackNameSetMap then
937        * #2) If this track is already in the TrackNameSet for this block
938        *     then return (done!)
939        * #3) else add a new set (with this block// track) to
940        *     blockNamesToTrackNameSetMap and check all the connections in this
941        *     block (by calling the 2nd method below)
942        * <p>
943        *     Basically, we're maintaining contiguous track sets for each block found
944        *     (in blockNamesToTrackNameSetMap)
945         */
946
947        // We're using a map here because it is convient to
948        // use it to pair up blocks and connections
949        Map<LayoutTrack, String> blocksAndTracksMap = new HashMap<>();
950        for (int k = 0; k < getNumberRays(); k++) {
951            TrackSegment ts = getRayConnectOrdered(k);
952            if (ts != null) {
953                String blockName = ts.getBlockName();
954                blocksAndTracksMap.put(ts, blockName);
955            }
956        }
957
958        List<Set<String>> TrackNameSets;
959        Set<String> TrackNameSet;
960        for (Map.Entry<LayoutTrack, String> entry : blocksAndTracksMap.entrySet()) {
961            LayoutTrack theConnect = entry.getKey();
962            String theBlockName = entry.getValue();
963
964            TrackNameSet = null;    // assume not found (pessimist!)
965            TrackNameSets = blockNamesToTrackNameSetsMap.get(theBlockName);
966            if (TrackNameSets != null) { // (#1)
967                for (Set<String> checkTrackNameSet : TrackNameSets) {
968                    if (checkTrackNameSet.contains(getName())) { // (#2)
969                        TrackNameSet = checkTrackNameSet;
970                        break;
971                    }
972                }
973            } else {    // (#3)
974                log.debug("*New block (''{}'') trackNameSets", theBlockName);
975                TrackNameSets = new ArrayList<>();
976                blockNamesToTrackNameSetsMap.put(theBlockName, TrackNameSets);
977            }
978            if (TrackNameSet == null) {
979                TrackNameSet = new LinkedHashSet<>();
980                TrackNameSets.add(TrackNameSet);
981            }
982            if (TrackNameSet.add(getName())) {
983                log.debug("*    Add track ''{}'' to trackNameSet for block ''{}''", getName(), theBlockName);
984            }
985            theConnect.collectContiguousTracksNamesInBlockNamed(theBlockName, TrackNameSet);
986        }
987    }
988
989    /**
990     * {@inheritDoc}
991     */
992    @Override
993    public void collectContiguousTracksNamesInBlockNamed(@Nonnull String blockName,
994            @Nonnull Set<String> TrackNameSet) {
995        if (!TrackNameSet.contains(getName())) {
996            // for all the rays with matching blocks in this turnout
997            //  #1) if its track segment's block is in this block
998            //  #2)     add turntable to TrackNameSet (if not already there)
999            //  #3)     if the track segment isn't in the TrackNameSet
1000            //  #4)         flood it
1001            for (int k = 0; k < getNumberRays(); k++) {
1002                TrackSegment ts = getRayConnectOrdered(k);
1003                if (ts != null) {
1004                    String blk = ts.getBlockName();
1005                    if ((!blk.isEmpty()) && (blk.equals(blockName))) { // (#1)
1006                        // if we are added to the TrackNameSet
1007                        if (TrackNameSet.add(getName())) {
1008                            log.debug("*    Add track ''{}'' for block ''{}''", getName(), blockName);
1009                        }
1010                        // it's time to play... flood your neighbours!
1011                        ts.collectContiguousTracksNamesInBlockNamed(blockName,
1012                                TrackNameSet); // (#4)
1013                    }
1014                }
1015            }
1016        }
1017    }
1018
1019    /**
1020     * {@inheritDoc}
1021     */
1022    @Override
1023    public void setAllLayoutBlocks(LayoutBlock layoutBlock) {
1024        // turntables don't have blocks...
1025        // nothing to see here, move along...
1026    }
1027
1028    /**
1029     * {@inheritDoc}
1030     */
1031    @Override
1032    public boolean canRemove() {
1033        return true;
1034    }
1035
1036    /**
1037     * {@inheritDoc}
1038     */
1039    @Override
1040    public String getTypeName() {
1041        return Bundle.getMessage("TypeName_Turntable");
1042    }
1043
1044    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTurntable.class);
1045
1046}