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