001package jmri.jmrit.display.layoutEditor;
002
003import java.awt.*;
004import java.awt.event.ActionEvent;
005import java.awt.geom.*;
006import static java.lang.Float.POSITIVE_INFINITY;
007import java.text.MessageFormat;
008import java.util.List;
009import java.util.*;
010
011import javax.annotation.CheckForNull;
012import javax.annotation.Nonnull;
013import javax.swing.*;
014
015import jmri.*;
016import jmri.jmrit.display.layoutEditor.LayoutTurntable.RayTrack;
017import jmri.util.MathUtil;
018import jmri.util.swing.JmriMouseEvent;
019
020/**
021 * MVC View component for the LayoutTurntable class.
022 *
023 * @author Bob Jacobsen  Copyright (c) 2020
024 *
025 */
026public class LayoutTurntableView extends LayoutTrackView {
027
028    // defined constants
029    // operational instance variables (not saved between sessions)
030    private final jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.LayoutTurntableEditor editor;
031
032    /**
033     * Constructor method.
034     * @param turntable the layout turntable to create view for.
035     * @param c            where to put it
036     * @param layoutEditor what layout editor panel to put it in
037     */
038    public LayoutTurntableView(@Nonnull LayoutTurntable turntable,
039                @Nonnull Point2D c,
040                @Nonnull LayoutEditor layoutEditor) {
041        super(turntable, c, layoutEditor);
042        this.turntable = turntable;
043
044        editor = new jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.LayoutTurntableEditor(layoutEditor);
045    }
046
047    final private LayoutTurntable turntable;
048
049    final public LayoutTurntable getTurntable() { return turntable; }
050
051    /**
052     * Get a string that represents this object. This should only be used for
053     * debugging.
054     *
055     * @return the string
056     */
057    @Override
058    public String toString() {
059        return "LayoutTurntable " + getName();
060    }
061
062    //
063    // Accessor methods
064    //
065    /**
066     * Get the radius for this turntable.
067     *
068     * @return the radius for this turntable
069     */
070    public double getRadius() {
071        return turntable.getRadius();
072    }
073
074    /**
075     * Set the radius for this turntable.
076     *
077     * @param r the radius for this turntable
078     */
079    public void setRadius(double r) {
080        turntable.setRadius(r);
081    }
082
083    /**
084     * @return the layout block name
085     */
086    @Nonnull
087    public String getBlockName() {
088        return turntable.getBlockName();
089    }
090
091    /**
092     * @return the layout block
093     */
094    public LayoutBlock getLayoutBlock() {
095        return turntable.getLayoutBlock();
096    }
097
098    /**
099     * Set up a LayoutBlock for this LayoutTurntable.
100     *
101     * @param newLayoutBlock the LayoutBlock to set
102     */
103    public void setLayoutBlock(@CheckForNull LayoutBlock newLayoutBlock) {
104        turntable.setLayoutBlock(newLayoutBlock);
105    }
106
107    /**
108     * Set up a LayoutBlock for this LayoutTurntable.
109     *
110     * @param name the name of the new LayoutBlock
111     */
112    public void setLayoutBlockByName(@CheckForNull String name) {
113        turntable.setLayoutBlockByName(name);
114    }
115
116    /*
117     * non-accessor methods
118     */
119    /**
120     * @return the bounds of this turntable.
121     */
122    @Override
123    public Rectangle2D getBounds() {
124        Rectangle2D result;
125
126        result = new Rectangle2D.Double(getCoordsCenter().getX(), getCoordsCenter().getY(), 0, 0);
127        for (int k = 0; k < getNumberRays(); k++) {
128            result.add(getRayCoordsOrdered(k));
129        }
130        return result;
131    }
132
133    /**
134     * Add a ray at the specified angle.
135     *
136     * @param angle the angle
137     * @return the RayTrack
138     */
139    public RayTrack addRay(double angle) {
140        return turntable.addRay(angle);
141    }
142
143    /**
144     * Get the connection for the ray with this index.
145     *
146     * @param index the index
147     * @return the connection for the ray with this value of getConnectionIndex
148     */
149    public TrackSegment getRayConnectIndexed(int index) {
150        return turntable.getRayConnectIndexed(index);
151    }
152
153    /**
154     * Get the connection for the ray at the index in the rayTrackList.
155     *
156     * @param i the index in the rayTrackList
157     * @return the connection for the ray at that index in the rayTrackList or null
158     */
159    public TrackSegment getRayConnectOrdered(int i) {
160        return turntable.getRayConnectOrdered(i);
161    }
162
163    /**
164     * Set the connection for the ray at the index in the rayTrackList.
165     *
166     * @param ts    the connection
167     * @param index the index in the rayTrackList
168     */
169    public void setRayConnect(TrackSegment ts, int index) {
170        turntable.setRayConnect(ts, index);
171    }
172
173    // should only be used by xml save code
174    public List<RayTrack> getRayTrackList() {
175        return turntable.getRayTrackList();
176    }
177
178    /**
179     * Get the number of rays on turntable.
180     *
181     * @return the number of rays
182     */
183    public int getNumberRays() {
184        return turntable.getNumberRays();
185    }
186
187    /**
188     * Get the index for the ray at this position in the rayTrackList.
189     *
190     * @param i the position in the rayTrackList
191     * @return the index
192     */
193    public int getRayIndex(int i) {
194        return turntable.getRayIndex(i);
195    }
196
197    /**
198     * Get the angle for the ray at this position in the rayTrackList.
199     *
200     * @param i the position in the rayTrackList
201     * @return the angle
202     */
203    public double getRayAngle(int i) {
204        return turntable.getRayAngle(i);
205    }
206
207    /**
208     * Set the turnout and state for the ray with this index.
209     *
210     * @param index       the index
211     * @param turnoutName the turnout name
212     * @param state       the state
213     */
214    public void setRayTurnout(int index, String turnoutName, int state) {
215        turntable.setRayTurnout(index, turnoutName, state);
216    }
217
218    /**
219     * Get the name of the turnout for the ray at this index.
220     *
221     * @param i the index
222     * @return name of the turnout for the ray at this index
223     */
224    public String getRayTurnoutName(int i) {
225        return turntable.getRayTurnoutName(i);
226    }
227
228    /**
229     * Get the turnout for the ray at this index.
230     *
231     * @param i the index
232     * @return the turnout for the ray at this index
233     */
234    public Turnout getRayTurnout(int i) {
235        return turntable.getRayTurnout(i);
236    }
237
238    /**
239     * Get the state of the turnout for the ray at this index.
240     *
241     * @param i the index
242     * @return state of the turnout for the ray at this index
243     */
244    public int getRayTurnoutState(int i) {
245        return turntable.getRayTurnoutState(i);
246    }
247
248    /**
249     * Get if the ray at this index is disabled.
250     *
251     * @param i the index
252     * @return true if disabled
253     */
254    public boolean isRayDisabled(int i) {
255        return turntable.isRayDisabled(i);
256    }
257
258    /**
259     * Set the disabled state of the ray at this index.
260     *
261     * @param i   the index
262     * @param boo the state
263     */
264    public void setRayDisabled(int i, boolean boo) {
265        turntable.setRayDisabled(i, boo);
266    }
267
268    /**
269     * Get the disabled when occupied state of the ray at this index.
270     *
271     * @param i the index
272     * @return the state
273     */
274    public boolean isRayDisabledWhenOccupied(int i) {
275        return turntable.isRayDisabledWhenOccupied(i);
276    }
277
278    /**
279     * Set the disabled when occupied state of the ray at this index.
280     *
281     * @param i   the index
282     * @param boo the state
283     */
284    public void setRayDisabledWhenOccupied(int i, boolean boo) {
285        turntable.setRayDisabledWhenOccupied(i, boo);
286    }
287
288    /**
289     * Get the coordinates for the ray with this index.
290     *
291     * @param index the index
292     * @return the coordinates
293     */
294    public Point2D getRayCoordsIndexed(int index) {
295        Point2D result = MathUtil.zeroPoint2D;
296        double rayRadius = getRadius() + LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
297        for (RayTrack rt : turntable.rayTrackList) {
298            if (rt.getConnectionIndex() == index) {
299                double angle = Math.toRadians(rt.getAngle());
300                // calculate coordinates
301                result = new Point2D.Double(
302                        (getCoordsCenter().getX() + (rayRadius * Math.sin(angle))),
303                        (getCoordsCenter().getY() - (rayRadius * Math.cos(angle))));
304                break;
305            }
306        }
307        return result;
308    }
309
310    /**
311     * Get the coordinates for the ray at this index.
312     *
313     * @param i the index; zero point returned if this is out of range
314     * @return the coordinates
315     */
316    public Point2D getRayCoordsOrdered(int i) {
317        Point2D result = MathUtil.zeroPoint2D;
318        if (i < turntable.rayTrackList.size()) {
319            RayTrack rt = turntable.rayTrackList.get(i);
320            if (rt != null) {
321                double angle = Math.toRadians(rt.getAngle());
322                double rayRadius = getRadius() + LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
323                // calculate coordinates
324                result = new Point2D.Double(
325                        (getCoordsCenter().getX() + (rayRadius * Math.sin(angle))),
326                        (getCoordsCenter().getY() - (rayRadius * Math.cos(angle))));
327            }
328        }
329        return result;
330    }
331
332    /**
333     * Set the coordinates for the ray at this index.
334     *
335     * @param x     the x coordinates
336     * @param y     the y coordinates
337     * @param index the index
338     */
339    public void setRayCoordsIndexed(double x, double y, int index) {
340        boolean found = false; // assume failure (pessimist!)
341        for (RayTrack rt : turntable.rayTrackList) {
342            if (rt.getConnectionIndex() == index) {
343                // convert these coordinates to an angle
344                double angle = Math.atan2(x - getCoordsCenter().getX(), y - getCoordsCenter().getY());
345                angle = MathUtil.wrapPM360(180.0 - Math.toDegrees(angle));
346                rt.setAngle(angle);
347                found = true;
348                break;
349            }
350        }
351        if (!found) {
352            log.error("{}.setRayCoordsIndexed({}, {}, {}); Attempt to move a non-existant ray track",
353                    getName(), x, y, index);
354        }
355    }
356
357    /**
358     * Set the coordinates for the ray at this index.
359     *
360     * @param point the new coordinates
361     * @param index the index
362     */
363    public void setRayCoordsIndexed(Point2D point, int index) {
364        setRayCoordsIndexed(point.getX(), point.getY(), index);
365    }
366
367    /**
368     * Get the coordinates for a specified connection type.
369     *
370     * @param connectionType the connection type
371     * @return the coordinates
372     */
373    @Override
374    public Point2D getCoordsForConnectionType(HitPointType connectionType) {
375        Point2D result = getCoordsCenter();
376        if (HitPointType.TURNTABLE_CENTER == connectionType) {
377            // nothing to see here, move along...
378            // (results are already correct)
379        } else if (HitPointType.isTurntableRayHitType(connectionType)) {
380            result = getRayCoordsIndexed(connectionType.turntableTrackIndex());
381        } else {
382            log.error("{}.getCoordsForConnectionType({}); Invalid connection type",
383                    getName(), connectionType); // NOI18N
384        }
385        return result;
386    }
387
388    /**
389     * {@inheritDoc}
390     */
391    @Override
392    public LayoutTrack getConnection(HitPointType connectionType) throws jmri.JmriException {
393        LayoutTrack result = null;
394        if (HitPointType.isTurntableRayHitType(connectionType)) {
395            result = getRayConnectIndexed(connectionType.turntableTrackIndex());
396        } else {
397            String errString = MessageFormat.format("{0}.getCoordsForConnectionType({1}); Invalid connection type",
398                    getName(), connectionType); // NOI18N
399            log.error("will throw {}", errString); // NOI18N
400            throw new jmri.JmriException(errString);
401        }
402        return result;
403    }
404
405    /**
406     * {@inheritDoc}
407     */
408    @Override
409    public void setConnection(HitPointType connectionType, LayoutTrack o, HitPointType type) throws jmri.JmriException {
410        if ((type != HitPointType.TRACK) && (type != HitPointType.NONE)) {
411            String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid type",
412                    getName(), connectionType, (o == null) ? "null" : o.getName(), type); // NOI18N
413            log.error("will throw {}", errString); // NOI18N
414            throw new jmri.JmriException(errString);
415        }
416        if (HitPointType.isTurntableRayHitType(connectionType)) {
417            if ((o == null) || (o instanceof TrackSegment)) {
418                setRayConnect((TrackSegment) o, connectionType.turntableTrackIndex());
419            } else {
420                String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid object: {4}",
421                        getName(), connectionType, o.getName(),
422                        type, o.getClass().getName()); // NOI18N
423                log.error("will throw {}", errString); // NOI18N
424                throw new jmri.JmriException(errString);
425            }
426        } else {
427            String errString = MessageFormat.format("{0}.setConnection({1}, {2}, {3}); Invalid connection type",
428                    getName(), connectionType, (o == null) ? "null" : o.getName(), type); // NOI18N
429            log.error("will throw {}", errString); // NOI18N
430            throw new jmri.JmriException(errString);
431        }
432    }
433
434    /**
435     * Test if ray with this index is a mainline track or not.
436     * <p>
437     * Defaults to false (not mainline) if connecting track segment is missing.
438     *
439     * @param index the index
440     * @return true if connecting track segment is mainline
441     */
442    public boolean isMainlineIndexed(int index) {
443        boolean result = false; // assume failure (pessimist!)
444
445        for (RayTrack rt : turntable.rayTrackList) {
446            if (rt.getConnectionIndex() == index) {
447                TrackSegment ts = rt.getConnect();
448                if (ts != null) {
449                    result = ts.isMainline();
450                    break;
451                }
452            }
453        }
454        return result;
455    }
456
457    /**
458     * Test if ray at this index is a mainline track or not.
459     * <p>
460     * Defaults to false (not mainline) if connecting track segment is missing
461     *
462     * @param i the index
463     * @return true if connecting track segment is mainline
464     */
465    public boolean isMainlineOrdered(int i) {
466        boolean result = false; // assume failure (pessimist!)
467        if (i < turntable.rayTrackList.size()) {
468            RayTrack rt = turntable.rayTrackList.get(i);
469            if (rt != null) {
470                TrackSegment ts = rt.getConnect();
471                if (ts != null) {
472                    result = ts.isMainline();
473                }
474            }
475        }
476        return result;
477    }
478
479    //
480    // Modify coordinates methods
481    //
482    /**
483     * Scale this LayoutTrack's coordinates by the x and y factors.
484     *
485     * @param xFactor the amount to scale X coordinates
486     * @param yFactor the amount to scale Y coordinates
487     */
488    @Override
489    public void scaleCoords(double xFactor, double yFactor) {
490        Point2D factor = new Point2D.Double(xFactor, yFactor);
491        super.setCoordsCenter(MathUtil.granulize(MathUtil.multiply(getCoordsCenter(), factor), 1.0));
492        setRadius( getRadius() * Math.hypot(xFactor, yFactor) );
493    }
494
495    /**
496     * Translate (2D move) this LayoutTrack's coordinates by the x and y
497     * factors.
498     *
499     * @param xFactor the amount to translate X coordinates
500     * @param yFactor the amount to translate Y coordinates
501     */
502    @Override
503    public void translateCoords(double xFactor, double yFactor) {
504        Point2D factor = new Point2D.Double(xFactor, yFactor);
505        super.setCoordsCenter(MathUtil.add(getCoordsCenter(), factor));
506    }
507
508    /**
509     * {@inheritDoc}
510     */
511    @Override
512    public void rotateCoords(double angleDEG) {
513        // rotate all rayTracks
514        turntable.rayTrackList.forEach((rayTrack) -> {
515            rayTrack.setAngle(rayTrack.getAngle() + angleDEG);
516        });
517    }
518
519    /**
520     * {@inheritDoc}
521     */
522    @Override
523    protected HitPointType findHitPointType(Point2D hitPoint, boolean useRectangles, boolean requireUnconnected) {
524        HitPointType result = HitPointType.NONE;  // assume point not on connection
525        // note: optimization here: instead of creating rectangles for all the
526        // points to check below, we create a rectangle for the test point
527        // and test if the points below are in that rectangle instead.
528        Rectangle2D r = layoutEditor.layoutEditorControlCircleRectAt(hitPoint);
529        Point2D p, minPoint = MathUtil.zeroPoint2D;
530
531        double circleRadius = LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
532        double distance, minDistance = POSITIVE_INFINITY;
533        if (!requireUnconnected) {
534            // check the center point
535            p = getCoordsCenter();
536            distance = MathUtil.distance(p, hitPoint);
537            if (distance < minDistance) {
538                minDistance = distance;
539                minPoint = p;
540                result = HitPointType.TURNTABLE_CENTER;
541            }
542        }
543
544        for (int k = 0; k < getNumberRays(); k++) {
545            if (!requireUnconnected || (getRayConnectOrdered(k) == null)) {
546                p = getRayCoordsOrdered(k);
547                distance = MathUtil.distance(p, hitPoint);
548                if (distance < minDistance) {
549                    minDistance = distance;
550                    minPoint = p;
551                    result = HitPointType.turntableTrackIndexedValue(k);
552                }
553            }
554        }
555        if ((useRectangles && !r.contains(minPoint))
556                || (!useRectangles && (minDistance > circleRadius))) {
557            result = HitPointType.NONE;
558        }
559        return result;
560    }
561
562    public String tLayoutBlockName = "";
563
564    /**
565     * Is this turntable turnout controlled?
566     *
567     * @return true if so
568     */
569    public boolean isTurnoutControlled() {
570        return turntable.isTurnoutControlled();
571    }
572
573    /**
574     * Set if this turntable is turnout controlled.
575     *
576     * @param boo set true if so
577     */
578    public void setTurnoutControlled(boolean boo) {
579        turntable.setTurnoutControlled(boo);
580    }
581
582    private JPopupMenu popupMenu = null;
583
584    /**
585     * {@inheritDoc}
586     */
587    @Override
588    @Nonnull
589    protected JPopupMenu showPopup(@Nonnull JmriMouseEvent mouseEvent) {
590        if (popupMenu != null) {
591            popupMenu.removeAll();
592        } else {
593            popupMenu = new JPopupMenu();
594        }
595
596        JMenuItem jmi = popupMenu.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("Turntable")) + getName());
597        jmi.setEnabled(false);
598
599        LayoutBlock lb = getLayoutBlock();
600        if (lb == null) {
601            jmi = popupMenu.add(Bundle.getMessage("NoBlock"));
602        } else {
603            String displayName = lb.getDisplayName();
604            jmi = popupMenu.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameBlock")) + displayName);
605        }
606        jmi.setEnabled(false);
607
608        /// if there are any track connections
609        if (!turntable.rayTrackList.isEmpty()) {
610            JMenu connectionsMenu = new JMenu(Bundle.getMessage("Connections"));
611            turntable.rayTrackList.forEach((rt) -> {
612                TrackSegment ts = rt.getConnect();
613                if (ts != null) {
614                    TrackSegmentView tsv = layoutEditor.getTrackSegmentView(ts);
615                    connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "" + rt.getConnectionIndex()) + ts.getName()) {
616                        @Override
617                        public void actionPerformed(ActionEvent e) {
618                            layoutEditor.setSelectionRect(tsv.getBounds());
619                            tsv.showPopup();
620                        }
621                    });
622                }
623            });
624            popupMenu.add(connectionsMenu);
625        }
626
627        popupMenu.add(new JSeparator(JSeparator.HORIZONTAL));
628
629        popupMenu.add(new AbstractAction(Bundle.getMessage("ButtonEdit")) {
630            @Override
631            public void actionPerformed(ActionEvent e) {
632                editor.editLayoutTrack(LayoutTurntableView.this);
633            }
634        });
635        popupMenu.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
636            @Override
637            public void actionPerformed(ActionEvent e) {
638                if (removeInlineLogixNG() && layoutEditor.removeTurntable(turntable)) {
639                    // Returned true if user did not cancel
640                    remove();
641                    dispose();
642                }
643            }
644        });
645        layoutEditor.setShowAlignmentMenu(popupMenu);
646        addCommonPopupItems(mouseEvent, popupMenu);
647        popupMenu.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
648        return popupMenu;
649    }
650
651    private JPopupMenu rayPopup = null;
652
653    protected void showRayPopUp(JmriMouseEvent e, int index) {
654        if (rayPopup != null) {
655            rayPopup.removeAll();
656        } else {
657            rayPopup = new JPopupMenu();
658        }
659
660        for (RayTrack rt : turntable.rayTrackList) {
661            if (rt.getConnectionIndex() == index) {
662                JMenuItem jmi = rayPopup.add("Turntable Ray " + index);
663                jmi.setEnabled(false);
664
665                rayPopup.add(new AbstractAction(
666                        Bundle.getMessage("MakeLabel",
667                                Bundle.getMessage("Connected"))
668                        + rt.getConnect().getName()) {
669                    @Override
670                    public void actionPerformed(ActionEvent e) {
671                        LayoutEditorFindItems lf = layoutEditor.getFinder();
672                        LayoutTrack lt = lf.findObjectByName(rt.getConnect().getName());
673
674                        // this shouldn't ever be null... however...
675                        if (lt != null) {
676                            LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
677                            layoutEditor.setSelectionRect(ltv.getBounds());
678                            ltv.showPopup();
679                        }
680                    }
681                });
682
683                if (rt.getTurnout() != null) {
684                    String info = rt.getTurnout().getDisplayName();
685                    String stateString = getTurnoutStateString(rt.getTurnoutState());
686                    if (!stateString.isEmpty()) {
687                        info += " (" + stateString + ")";
688                    }
689                    jmi = rayPopup.add(info);
690                    jmi.setEnabled(false);
691
692                    rayPopup.add(new JSeparator(JSeparator.HORIZONTAL));
693
694                    JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(Bundle.getMessage("Disabled"));
695                    cbmi.setSelected(rt.isDisabled());
696                    rayPopup.add(cbmi);
697                    cbmi.addActionListener((java.awt.event.ActionEvent e2) -> {
698                        JCheckBoxMenuItem o = (JCheckBoxMenuItem) e2.getSource();
699                        rt.setDisabled(o.isSelected());
700                    });
701
702                    cbmi = new JCheckBoxMenuItem(Bundle.getMessage("DisabledWhenOccupied"));
703                    cbmi.setSelected(rt.isDisabledWhenOccupied());
704                    rayPopup.add(cbmi);
705                    cbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
706                        JCheckBoxMenuItem o = (JCheckBoxMenuItem) e3.getSource();
707                        rt.setDisabledWhenOccupied(o.isSelected());
708                    });
709                }
710                rayPopup.show(e.getComponent(), e.getX(), e.getY());
711                break;
712            }
713        }
714    }
715
716    /**
717     * Set turntable position to the ray with this index.
718     *
719     * @param index the index
720     */
721    public void setPosition(int index) {
722        turntable.setPosition(index);
723    }
724
725    /**
726     * Get the turntable position.
727     *
728     * @return the turntable position
729     */
730    public int getPosition() {
731        return turntable.getPosition();
732    }
733
734    /**
735     * Delete this ray track.
736     *
737     * @param rayTrack the ray track
738     */
739    public void deleteRay(RayTrack rayTrack) {
740        TrackSegment t = null;
741        if (turntable.rayTrackList == null) {
742            log.error("{}.deleteRay(null); rayTrack is null", getName());
743        } else {
744            t = rayTrack.getConnect();
745            getRayTrackList().remove(rayTrack.getConnectionIndex());
746            rayTrack.dispose();
747        }
748        if (t != null) {
749            layoutEditor.removeTrackSegment(t);
750        }
751
752        // update the panel
753        layoutEditor.redrawPanel();
754        layoutEditor.setDirty();
755    }
756
757    /**
758     * Clean up when this object is no longer needed. Should not be called while
759     * the object is still displayed; see remove().
760     */
761    public void dispose() {
762        if (popupMenu != null) {
763            popupMenu.removeAll();
764        }
765        popupMenu = null;
766        turntable.rayTrackList.forEach((rt) -> {
767            rt.dispose();
768        });
769    }
770
771    /**
772     * Remove this object from display and persistance.
773     */
774    public void remove() {
775        // remove from persistance by flagging inactive
776        active = false;
777    }
778
779    private boolean active = true;
780
781    /**
782     * @return "active" true means that the object is still displayed, and should be stored.
783     */
784    public boolean isActive() {
785        return active;
786    }
787
788    public static class RayTrackVisuals {
789
790        // public final RayTrack track;
791
792        // persistant instance variables
793        private double rayAngle = 0.0;
794
795       /**
796         * Get the angle for this ray.
797         *
798         * @return the angle for this ray
799         */
800        public double getAngle() {
801            return rayAngle;
802        }
803
804        /**
805         * Set the angle for this ray.
806         *
807         * @param an the angle for this ray
808         */
809        public void setAngle(double an) {
810            rayAngle = MathUtil.wrapPM360(an);
811        }
812
813        public RayTrackVisuals(RayTrack track) {
814            // this.track = track;
815        }
816    }
817
818    /**
819     * Draw track decorations.
820     *
821     * This type of track has none, so this method is empty.
822     */
823    @Override
824    protected void drawDecorations(Graphics2D g2) {}
825
826    /**
827     * {@inheritDoc}
828     */
829    @Override
830    protected void draw1(Graphics2D g2, boolean isMain, boolean isBlock) {
831        log.trace("LayoutTurntable:draw1 at {}", getCoordsCenter());
832        float trackWidth = 2.F;
833        double diameter = 2.f * getRadius();
834
835        if (isBlock && isMain) {
836            double radius2 = Math.max(getRadius() / 4.f, trackWidth * 2);
837            double diameter2 = radius2 * 2.f;
838            Stroke stroke = g2.getStroke();
839            Color color = g2.getColor();
840            // draw turntable circle - default track color, side track width
841            g2.setStroke(new BasicStroke(trackWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
842            g2.setColor(layoutEditor.getDefaultTrackColorColor());
843            g2.draw(new Ellipse2D.Double(getCoordsCenter().getX() - getRadius(), getCoordsCenter().getY() - getRadius(), diameter, diameter));
844            g2.draw(new Ellipse2D.Double(getCoordsCenter().getX() - radius2, getCoordsCenter().getY() - radius2, diameter2, diameter2));
845            g2.setStroke(stroke);
846            g2.setColor(color);
847        }
848
849        // draw ray tracks
850        for (int j = 0; j < getNumberRays(); j++) {
851            boolean main = false;
852            Color color = null;
853            TrackSegment ts = getRayConnectOrdered(j);
854            if (ts != null) {
855                main = ts.isMainline();
856            }
857
858            if (isBlock) {
859                if (ts == null) {
860                    g2.setColor(layoutEditor.getDefaultTrackColorColor());
861                } else {
862                    LayoutBlock lb = ts.getLayoutBlock();
863                    if (lb != null) {
864                        color = g2.getColor();
865                        setColorForTrackBlock(g2, lb);
866                    }
867                }
868            }
869
870            Point2D pt2 = getRayCoordsOrdered(j);
871            Point2D delta = MathUtil.normalize(MathUtil.subtract(pt2, getCoordsCenter()), getRadius());
872            Point2D pt1 = MathUtil.add(getCoordsCenter(), delta);
873            if (main == isMain) {
874                g2.draw(new Line2D.Double(pt1, pt2));
875            }
876
877            int knownPosition = getPosition();
878            int commandedPosition = turntable.getCommandedPosition();
879
880            // Don't draw the bridge if animating and position is changing
881            if (layoutEditor.isAnimating() && isTurnoutControlled() && knownPosition != commandedPosition) {
882                continue;
883            }
884
885            int currentPositionIndex = (knownPosition != -1) ? getRayIndex(knownPosition) : -1;
886            if (isMain && isTurnoutControlled() && (currentPositionIndex == j) ) {
887                if (isBlock) {
888                    LayoutBlock lb = getLayoutBlock();
889                    if (lb != null) {
890                        color = (color == null) ? g2.getColor() : color;
891                        setColorForTrackBlock(g2, lb);
892                    } else {
893                        g2.setColor(layoutEditor.getDefaultTrackColorColor());
894                    }
895                }
896                // Draw an asymmetric bridge to act as a pointer.
897                // The long end (pt2) points toward the selected ray.
898                // The short end (short_pt1) points away from it, at 0.8 * radius.
899                Point2D short_delta = MathUtil.normalize(delta, getRadius() * 0.8);
900                Point2D short_pt1 = MathUtil.subtract(getCoordsCenter(), short_delta);
901                g2.draw(new Line2D.Double(short_pt1, pt2));
902            }
903            if (color != null) {
904                g2.setColor(color); /// restore previous color
905            }
906        }
907    }
908
909    /**
910     * {@inheritDoc}
911     */
912    @Override
913    protected void draw2(Graphics2D g2, boolean isMain, float railDisplacement) {
914        log.trace("LayoutTurntable:draw2 at {}", getCoordsCenter());
915
916        float trackWidth = 2.F;
917        float halfTrackWidth = trackWidth / 2.f;
918
919        // draw ray tracks
920        for (int j = 0; j < getNumberRays(); j++) {
921            boolean main = false;
922//            Color c = null;
923            TrackSegment ts = getRayConnectOrdered(j);
924            if (ts != null) {
925                main = ts.isMainline();
926//                LayoutBlock lb = ts.getLayoutBlock();
927//                if (lb != null) {
928//                    c = g2.getColor();
929//                    setColorForTrackBlock(g2, lb);
930//                }
931            }
932            Point2D pt2 = getRayCoordsOrdered(j);
933            Point2D vDelta = MathUtil.normalize(MathUtil.subtract(pt2, getCoordsCenter()), getRadius());
934            Point2D vDeltaO = MathUtil.normalize(MathUtil.orthogonal(vDelta), railDisplacement);
935            Point2D pt1 = MathUtil.add(getCoordsCenter(), vDelta);
936            Point2D pt1L = MathUtil.subtract(pt1, vDeltaO);
937            Point2D pt1R = MathUtil.add(pt1, vDeltaO);
938            Point2D pt2L = MathUtil.subtract(pt2, vDeltaO);
939            Point2D pt2R = MathUtil.add(pt2, vDeltaO);
940            if (main == isMain) {
941                log.trace("   draw main at {} {}, {} {}", pt1L, pt2L, pt1R, pt2R);
942                g2.draw(new Line2D.Double(pt1L, pt2L));
943                g2.draw(new Line2D.Double(pt1R, pt2R));
944            }
945            // getPosition() will return -1 if no ray is selected (all turnouts are closed).
946            int currentPositionIndex = (getPosition() != -1) ? getRayIndex(getPosition()) : -1;
947            if (isMain && isTurnoutControlled() && (currentPositionIndex == j)) {
948//                LayoutBlock lb = getLayoutBlock();
949//                if (lb != null) {
950//                    c = g2.getColor();
951//                    setColorForTrackBlock(g2, lb);
952//                } else {
953//                    g2.setColor(layoutEditor.getDefaultTrackColorColor());
954//                }
955                vDelta = MathUtil.normalize(vDelta, getRadius() - halfTrackWidth);
956                pt1 = MathUtil.subtract(getCoordsCenter(), vDelta);
957                pt1L = MathUtil.subtract(pt1, vDeltaO);
958                pt1R = MathUtil.add(pt1, vDeltaO);
959                log.trace("   draw not main at {} {}, {} {}", pt1L, pt2L, pt1R, pt2R);
960                g2.draw(new Line2D.Double(pt1L, pt2L));
961                g2.draw(new Line2D.Double(pt1R, pt2R));
962            }
963//            if (c != null) {
964//                g2.setColor(c); /// restore previous color
965//            }
966        }
967    }
968
969    /**
970     * {@inheritDoc}
971     */
972    @Override
973    protected void highlightUnconnected(Graphics2D g2, HitPointType specificType) {
974        log.trace("LayoutTurntable:highlightUnconnected");
975        for (int j = 0; j < getNumberRays(); j++) {
976            if (  (specificType == HitPointType.NONE)
977                    || (specificType == (HitPointType.turntableTrackIndexedValue(j)))
978                )
979            {
980                if (getRayConnectOrdered(j) == null) {
981                    Point2D pt = getRayCoordsOrdered(j);
982                    log.trace("   draw at {}", pt);
983                    g2.fill(trackControlCircleAt(pt));
984                }
985            }
986        }
987    }
988
989    /**
990     * Draw this turntable's controls.
991     *
992     * @param g2 the graphics port to draw to
993     */
994    @Override
995    protected void drawTurnoutControls(Graphics2D g2) {
996        log.trace("LayoutTurntable:drawTurnoutControls");
997        if (isTurnoutControlled()) {
998            // draw control circles at all but current position ray tracks
999            for (int j = 0; j < getNumberRays(); j++) {
1000                if (getPosition() != j) {
1001                    RayTrack rt = turntable.rayTrackList.get(j);
1002                    if (!rt.isDisabled() && !(rt.isDisabledWhenOccupied() && rt.isOccupied())) {
1003                        Point2D pt = getRayCoordsOrdered(j);
1004                        g2.draw(trackControlCircleAt(pt));
1005                    }
1006                }
1007            }
1008        }
1009    }
1010
1011    /**
1012     * Draw this turntable's edit controls.
1013     *
1014     * @param g2 the graphics port to draw to
1015     */
1016    @Override
1017    protected void drawEditControls(Graphics2D g2) {
1018        Point2D pt = getCoordsCenter();
1019        g2.setColor(layoutEditor.getDefaultTrackColorColor());
1020        g2.draw(trackControlCircleAt(pt));
1021
1022        for (int j = 0; j < getNumberRays(); j++) {
1023            pt = getRayCoordsOrdered(j);
1024
1025            if (getRayConnectOrdered(j) == null) {
1026                g2.setColor(Color.red);
1027            } else {
1028                g2.setColor(Color.green);
1029            }
1030            g2.draw(layoutEditor.layoutEditorControlRectAt(pt));
1031        }
1032    }
1033
1034    /**
1035     * {@inheritDoc}
1036     */
1037    @Override
1038    protected void reCheckBlockBoundary() {
1039        // nothing to see here... move along...
1040    }
1041
1042    /**
1043     * {@inheritDoc}
1044     */
1045    @Override
1046    protected List<LayoutConnectivity> getLayoutConnectivity() {
1047        // nothing to see here... move along...
1048        return null;
1049    }
1050
1051    /**
1052     * {@inheritDoc}
1053     */
1054    @Override
1055    public List<HitPointType> checkForFreeConnections() {
1056        List<HitPointType> result = new ArrayList<>();
1057
1058        for (int k = 0; k < getNumberRays(); k++) {
1059            if (getRayConnectOrdered(k) == null) {
1060                result.add(HitPointType.turntableTrackIndexedValue(k));
1061            }
1062        }
1063        return result;
1064    }
1065
1066    /**
1067     * {@inheritDoc}
1068     */
1069    @Override
1070    public boolean checkForUnAssignedBlocks() {
1071        // Layout turnouts get their block information from the
1072        // track segments attached to their rays so...
1073        // nothing to see here... move along...
1074        return true;
1075    }
1076
1077    /**
1078     * {@inheritDoc}
1079     */
1080    @Override
1081    public void checkForNonContiguousBlocks(
1082            @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetsMap) {
1083        /*
1084        * For each (non-null) blocks of this track do:
1085        * #1) If it's got an entry in the blockNamesToTrackNameSetMap then
1086        * #2) If this track is already in the TrackNameSet for this block
1087        *     then return (done!)
1088        * #3) else add a new set (with this block// track) to
1089        *     blockNamesToTrackNameSetMap and check all the connections in this
1090        *     block (by calling the 2nd method below)
1091        * <p>
1092        *     Basically, we're maintaining contiguous track sets for each block found
1093        *     (in blockNamesToTrackNameSetMap)
1094         */
1095
1096        // We're using a map here because it is convient to
1097        // use it to pair up blocks and connections
1098        Map<LayoutTrack, String> blocksAndTracksMap = new HashMap<>();
1099        for (int k = 0; k < getNumberRays(); k++) {
1100            TrackSegment ts = getRayConnectOrdered(k);
1101            if (ts != null) {
1102                String blockName = ts.getBlockName();
1103                blocksAndTracksMap.put(ts, blockName);
1104            }
1105        }
1106
1107        List<Set<String>> TrackNameSets;
1108        Set<String> TrackNameSet;
1109        for (Map.Entry<LayoutTrack, String> entry : blocksAndTracksMap.entrySet()) {
1110            LayoutTrack theConnect = entry.getKey();
1111            String theBlockName = entry.getValue();
1112
1113            TrackNameSet = null;    // assume not found (pessimist!)
1114            TrackNameSets = blockNamesToTrackNameSetsMap.get(theBlockName);
1115            if (TrackNameSets != null) { // (#1)
1116                for (Set<String> checkTrackNameSet : TrackNameSets) {
1117                    if (checkTrackNameSet.contains(getName())) { // (#2)
1118                        TrackNameSet = checkTrackNameSet;
1119                        break;
1120                    }
1121                }
1122            } else {    // (#3)
1123                log.debug("*New block (''{}'') trackNameSets", theBlockName);
1124                TrackNameSets = new ArrayList<>();
1125                blockNamesToTrackNameSetsMap.put(theBlockName, TrackNameSets);
1126            }
1127            if (TrackNameSet == null) {
1128                TrackNameSet = new LinkedHashSet<>();
1129                TrackNameSets.add(TrackNameSet);
1130            }
1131            if (TrackNameSet.add(getName())) {
1132                log.debug("*    Add track ''{}'' to trackNameSet for block ''{}''", getName(), theBlockName);
1133            }
1134            theConnect.collectContiguousTracksNamesInBlockNamed(theBlockName, TrackNameSet);
1135        }
1136    }
1137
1138    /**
1139     * {@inheritDoc}
1140     */
1141    @Override
1142    public void collectContiguousTracksNamesInBlockNamed(@Nonnull String blockName,
1143            @Nonnull Set<String> TrackNameSet) {
1144        if (!TrackNameSet.contains(getName())) {
1145            // for all the rays with matching blocks in this turnout
1146            //  #1) if its track segment's block is in this block
1147            //  #2)     add turntable to TrackNameSet (if not already there)
1148            //  #3)     if the track segment isn't in the TrackNameSet
1149            //  #4)         flood it
1150            for (int k = 0; k < getNumberRays(); k++) {
1151                TrackSegment ts = getRayConnectOrdered(k);
1152                if (ts != null) {
1153                    String blk = ts.getBlockName();
1154                    if ((!blk.isEmpty()) && (blk.equals(blockName))) { // (#1)
1155                        // if we are added to the TrackNameSet
1156                        if (TrackNameSet.add(getName())) {
1157                            log.debug("*    Add track ''{}'' for block ''{}''", getName(), blockName);
1158                        }
1159                        // it's time to play... flood your neighbours!
1160                        ts.collectContiguousTracksNamesInBlockNamed(blockName,
1161                                TrackNameSet); // (#4)
1162                    }
1163                }
1164            }
1165        }
1166    }
1167
1168    /**
1169     * {@inheritDoc}
1170     */
1171    @Override
1172    public void setAllLayoutBlocks(LayoutBlock layoutBlock) {
1173        // turntables don't have blocks...
1174        // nothing to see here, move along...
1175    }
1176
1177    /**
1178     * {@inheritDoc}
1179     */
1180    @Override
1181    public boolean canRemove() {
1182        return true;
1183    }
1184
1185
1186    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTurntableView.class);
1187}