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        float halfTrackWidth = trackWidth / 2.f;
834        double diameter = 2.f * getRadius();
835
836        if (isBlock && isMain) {
837            double radius2 = Math.max(getRadius() / 4.f, trackWidth * 2);
838            double diameter2 = radius2 * 2.f;
839            Stroke stroke = g2.getStroke();
840            Color color = g2.getColor();
841            // draw turntable circle - default track color, side track width
842            g2.setStroke(new BasicStroke(trackWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
843            g2.setColor(layoutEditor.getDefaultTrackColorColor());
844            g2.draw(new Ellipse2D.Double(getCoordsCenter().getX() - getRadius(), getCoordsCenter().getY() - getRadius(), diameter, diameter));
845            g2.draw(new Ellipse2D.Double(getCoordsCenter().getX() - radius2, getCoordsCenter().getY() - radius2, diameter2, diameter2));
846            g2.setStroke(stroke);
847            g2.setColor(color);
848        }
849
850        // draw ray tracks
851        for (int j = 0; j < getNumberRays(); j++) {
852            boolean main = false;
853            Color color = null;
854            TrackSegment ts = getRayConnectOrdered(j);
855            if (ts != null) {
856                main = ts.isMainline();
857            }
858
859            if (isBlock) {
860                if (ts == null) {
861                    g2.setColor(layoutEditor.getDefaultTrackColorColor());
862                } else {
863                    LayoutBlock lb = ts.getLayoutBlock();
864                    if (lb != null) {
865                        color = g2.getColor();
866                        setColorForTrackBlock(g2, lb);
867                    }
868                }
869            }
870
871            Point2D pt2 = getRayCoordsOrdered(j);
872            Point2D delta = MathUtil.normalize(MathUtil.subtract(pt2, getCoordsCenter()), getRadius());
873            Point2D pt1 = MathUtil.add(getCoordsCenter(), delta);
874            if (main == isMain) {
875                g2.draw(new Line2D.Double(pt1, pt2));
876            }
877            if (isMain && isTurnoutControlled() && (getPosition() == j)) {
878                if (isBlock) {
879                    LayoutBlock lb = getLayoutBlock();
880                    if (lb != null) {
881                        color = (color == null) ? g2.getColor() : color;
882                        setColorForTrackBlock(g2, lb);
883                    } else {
884                        g2.setColor(layoutEditor.getDefaultTrackColorColor());
885                    }
886                }
887                delta = MathUtil.normalize(delta, getRadius() - halfTrackWidth);
888                pt1 = MathUtil.subtract(getCoordsCenter(), delta);
889                g2.draw(new Line2D.Double(pt1, pt2));
890            }
891            if (color != null) {
892                g2.setColor(color); /// restore previous color
893            }
894        }
895    }
896
897    /**
898     * {@inheritDoc}
899     */
900    @Override
901    protected void draw2(Graphics2D g2, boolean isMain, float railDisplacement) {
902        log.trace("LayoutTurntable:draw2 at {}", getCoordsCenter());
903
904        float trackWidth = 2.F;
905        float halfTrackWidth = trackWidth / 2.f;
906
907        // draw ray tracks
908        for (int j = 0; j < getNumberRays(); j++) {
909            boolean main = false;
910//            Color c = null;
911            TrackSegment ts = getRayConnectOrdered(j);
912            if (ts != null) {
913                main = ts.isMainline();
914//                LayoutBlock lb = ts.getLayoutBlock();
915//                if (lb != null) {
916//                    c = g2.getColor();
917//                    setColorForTrackBlock(g2, lb);
918//                }
919            }
920            Point2D pt2 = getRayCoordsOrdered(j);
921            Point2D vDelta = MathUtil.normalize(MathUtil.subtract(pt2, getCoordsCenter()), getRadius());
922            Point2D vDeltaO = MathUtil.normalize(MathUtil.orthogonal(vDelta), railDisplacement);
923            Point2D pt1 = MathUtil.add(getCoordsCenter(), vDelta);
924            Point2D pt1L = MathUtil.subtract(pt1, vDeltaO);
925            Point2D pt1R = MathUtil.add(pt1, vDeltaO);
926            Point2D pt2L = MathUtil.subtract(pt2, vDeltaO);
927            Point2D pt2R = MathUtil.add(pt2, vDeltaO);
928            if (main == isMain) {
929                log.trace("   draw main at {} {}, {} {}", pt1L, pt2L, pt1R, pt2R);
930                g2.draw(new Line2D.Double(pt1L, pt2L));
931                g2.draw(new Line2D.Double(pt1R, pt2R));
932            }
933            if (isMain && isTurnoutControlled() && (getPosition() == j)) {
934//                LayoutBlock lb = getLayoutBlock();
935//                if (lb != null) {
936//                    c = g2.getColor();
937//                    setColorForTrackBlock(g2, lb);
938//                } else {
939//                    g2.setColor(layoutEditor.getDefaultTrackColorColor());
940//                }
941                vDelta = MathUtil.normalize(vDelta, getRadius() - halfTrackWidth);
942                pt1 = MathUtil.subtract(getCoordsCenter(), vDelta);
943                pt1L = MathUtil.subtract(pt1, vDeltaO);
944                pt1R = MathUtil.add(pt1, vDeltaO);
945                log.trace("   draw not main at {} {}, {} {}", pt1L, pt2L, pt1R, pt2R);
946                g2.draw(new Line2D.Double(pt1L, pt2L));
947                g2.draw(new Line2D.Double(pt1R, pt2R));
948            }
949//            if (c != null) {
950//                g2.setColor(c); /// restore previous color
951//            }
952        }
953    }
954
955    /**
956     * {@inheritDoc}
957     */
958    @Override
959    protected void highlightUnconnected(Graphics2D g2, HitPointType specificType) {
960        log.trace("LayoutTurntable:highlightUnconnected");
961        for (int j = 0; j < getNumberRays(); j++) {
962            if (  (specificType == HitPointType.NONE)
963                    || (specificType == (HitPointType.turntableTrackIndexedValue(j)))
964                )
965            {
966                if (getRayConnectOrdered(j) == null) {
967                    Point2D pt = getRayCoordsOrdered(j);
968                    log.trace("   draw at {}", pt);
969                    g2.fill(trackControlCircleAt(pt));
970                }
971            }
972        }
973    }
974
975    /**
976     * Draw this turntable's controls.
977     *
978     * @param g2 the graphics port to draw to
979     */
980    @Override
981    protected void drawTurnoutControls(Graphics2D g2) {
982        log.trace("LayoutTurntable:drawTurnoutControls");
983        if (isTurnoutControlled()) {
984            // draw control circles at all but current position ray tracks
985            for (int j = 0; j < getNumberRays(); j++) {
986                if (getPosition() != j) {
987                    RayTrack rt = turntable.rayTrackList.get(j);
988                    if (!rt.isDisabled() && !(rt.isDisabledWhenOccupied() && rt.isOccupied())) {
989                        Point2D pt = getRayCoordsOrdered(j);
990                        g2.draw(trackControlCircleAt(pt));
991                    }
992                }
993            }
994        }
995    }
996
997    /**
998     * Draw this turntable's edit controls.
999     *
1000     * @param g2 the graphics port to draw to
1001     */
1002    @Override
1003    protected void drawEditControls(Graphics2D g2) {
1004        Point2D pt = getCoordsCenter();
1005        g2.setColor(layoutEditor.getDefaultTrackColorColor());
1006        g2.draw(trackControlCircleAt(pt));
1007
1008        for (int j = 0; j < getNumberRays(); j++) {
1009            pt = getRayCoordsOrdered(j);
1010
1011            if (getRayConnectOrdered(j) == null) {
1012                g2.setColor(Color.red);
1013            } else {
1014                g2.setColor(Color.green);
1015            }
1016            g2.draw(layoutEditor.layoutEditorControlRectAt(pt));
1017        }
1018    }
1019
1020    /**
1021     * {@inheritDoc}
1022     */
1023    @Override
1024    protected void reCheckBlockBoundary() {
1025        // nothing to see here... move along...
1026    }
1027
1028    /**
1029     * {@inheritDoc}
1030     */
1031    @Override
1032    protected List<LayoutConnectivity> getLayoutConnectivity() {
1033        // nothing to see here... move along...
1034        return null;
1035    }
1036
1037    /**
1038     * {@inheritDoc}
1039     */
1040    @Override
1041    public List<HitPointType> checkForFreeConnections() {
1042        List<HitPointType> result = new ArrayList<>();
1043
1044        for (int k = 0; k < getNumberRays(); k++) {
1045            if (getRayConnectOrdered(k) == null) {
1046                result.add(HitPointType.turntableTrackIndexedValue(k));
1047            }
1048        }
1049        return result;
1050    }
1051
1052    /**
1053     * {@inheritDoc}
1054     */
1055    @Override
1056    public boolean checkForUnAssignedBlocks() {
1057        // Layout turnouts get their block information from the
1058        // track segments attached to their rays so...
1059        // nothing to see here... move along...
1060        return true;
1061    }
1062
1063    /**
1064     * {@inheritDoc}
1065     */
1066    @Override
1067    public void checkForNonContiguousBlocks(
1068            @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetsMap) {
1069        /*
1070        * For each (non-null) blocks of this track do:
1071        * #1) If it's got an entry in the blockNamesToTrackNameSetMap then
1072        * #2) If this track is already in the TrackNameSet for this block
1073        *     then return (done!)
1074        * #3) else add a new set (with this block// track) to
1075        *     blockNamesToTrackNameSetMap and check all the connections in this
1076        *     block (by calling the 2nd method below)
1077        * <p>
1078        *     Basically, we're maintaining contiguous track sets for each block found
1079        *     (in blockNamesToTrackNameSetMap)
1080         */
1081
1082        // We're using a map here because it is convient to
1083        // use it to pair up blocks and connections
1084        Map<LayoutTrack, String> blocksAndTracksMap = new HashMap<>();
1085        for (int k = 0; k < getNumberRays(); k++) {
1086            TrackSegment ts = getRayConnectOrdered(k);
1087            if (ts != null) {
1088                String blockName = ts.getBlockName();
1089                blocksAndTracksMap.put(ts, blockName);
1090            }
1091        }
1092
1093        List<Set<String>> TrackNameSets;
1094        Set<String> TrackNameSet;
1095        for (Map.Entry<LayoutTrack, String> entry : blocksAndTracksMap.entrySet()) {
1096            LayoutTrack theConnect = entry.getKey();
1097            String theBlockName = entry.getValue();
1098
1099            TrackNameSet = null;    // assume not found (pessimist!)
1100            TrackNameSets = blockNamesToTrackNameSetsMap.get(theBlockName);
1101            if (TrackNameSets != null) { // (#1)
1102                for (Set<String> checkTrackNameSet : TrackNameSets) {
1103                    if (checkTrackNameSet.contains(getName())) { // (#2)
1104                        TrackNameSet = checkTrackNameSet;
1105                        break;
1106                    }
1107                }
1108            } else {    // (#3)
1109                log.debug("*New block (''{}'') trackNameSets", theBlockName);
1110                TrackNameSets = new ArrayList<>();
1111                blockNamesToTrackNameSetsMap.put(theBlockName, TrackNameSets);
1112            }
1113            if (TrackNameSet == null) {
1114                TrackNameSet = new LinkedHashSet<>();
1115                TrackNameSets.add(TrackNameSet);
1116            }
1117            if (TrackNameSet.add(getName())) {
1118                log.debug("*    Add track ''{}'' to trackNameSet for block ''{}''", getName(), theBlockName);
1119            }
1120            theConnect.collectContiguousTracksNamesInBlockNamed(theBlockName, TrackNameSet);
1121        }
1122    }
1123
1124    /**
1125     * {@inheritDoc}
1126     */
1127    @Override
1128    public void collectContiguousTracksNamesInBlockNamed(@Nonnull String blockName,
1129            @Nonnull Set<String> TrackNameSet) {
1130        if (!TrackNameSet.contains(getName())) {
1131            // for all the rays with matching blocks in this turnout
1132            //  #1) if its track segment's block is in this block
1133            //  #2)     add turntable to TrackNameSet (if not already there)
1134            //  #3)     if the track segment isn't in the TrackNameSet
1135            //  #4)         flood it
1136            for (int k = 0; k < getNumberRays(); k++) {
1137                TrackSegment ts = getRayConnectOrdered(k);
1138                if (ts != null) {
1139                    String blk = ts.getBlockName();
1140                    if ((!blk.isEmpty()) && (blk.equals(blockName))) { // (#1)
1141                        // if we are added to the TrackNameSet
1142                        if (TrackNameSet.add(getName())) {
1143                            log.debug("*    Add track ''{}'' for block ''{}''", getName(), blockName);
1144                        }
1145                        // it's time to play... flood your neighbours!
1146                        ts.collectContiguousTracksNamesInBlockNamed(blockName,
1147                                TrackNameSet); // (#4)
1148                    }
1149                }
1150            }
1151        }
1152    }
1153
1154    /**
1155     * {@inheritDoc}
1156     */
1157    @Override
1158    public void setAllLayoutBlocks(LayoutBlock layoutBlock) {
1159        // turntables don't have blocks...
1160        // nothing to see here, move along...
1161    }
1162
1163    /**
1164     * {@inheritDoc}
1165     */
1166    @Override
1167    public boolean canRemove() {
1168        return true;
1169    }
1170
1171
1172    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTurntableView.class);
1173}