001package jmri.jmrit.display.layoutEditor;
002
003import java.awt.BasicStroke;
004import java.awt.Color;
005import java.awt.Graphics2D;
006import java.awt.event.ActionEvent;
007import java.awt.geom.Ellipse2D;
008import java.awt.geom.GeneralPath;
009import java.awt.geom.Line2D;
010import java.awt.geom.Point2D;
011import java.awt.geom.Rectangle2D;
012import java.util.*;
013
014import javax.annotation.*;
015import javax.swing.*;
016
017import jmri.util.*;
018import jmri.util.swing.JmriColorChooser;
019import jmri.util.swing.JmriJOptionPane;
020import jmri.util.swing.JmriMouseEvent;
021
022/**
023 * A LayoutShape is a set of LayoutShapePoint used to draw a shape. Each point
024 * can ether be a point on the shape or a control point that defines a curve
025 * that's part of the shape. The shape can be open (end points not connected) or
026 * closed (end points connected)
027 *
028 * @author George Warner Copyright (c) 2017-2018
029 */
030public class LayoutShape {
031
032    public static final int MAX_LINEWIDTH = 200;
033
034    // operational instance variables (not saved between sessions)
035    private LayoutEditor layoutEditor = null;
036    private String name;
037    private LayoutShapeType layoutShapeType;
038    private int level = 3;
039    private int lineWidth = 3;
040    private Color lineColor = Color.BLACK;
041    private Color fillColor = Color.DARK_GRAY;
042    private boolean hidden = false;
043
044    // these are saved
045    // list of LayoutShapePoints
046    private final ArrayList<LayoutShapePoint> shapePoints;
047
048    /**
049     * constructor method (used by XML loading code)
050     *
051     * @param name         the name of the shape
052     * @param layoutEditor reference to the LayoutEditor this shape is in
053     */
054    public LayoutShape(String name, LayoutEditor layoutEditor) {
055        this.name = name;
056        this.layoutEditor = layoutEditor;
057        this.layoutShapeType = LayoutShapeType.Open;
058        this.shapePoints = new ArrayList<>();
059    }
060
061    /**
062     * constructor method (used by XML loading code)
063     *
064     * @param name         the name of the shape
065     * @param t            the layout shape type.
066     * @param layoutEditor reference to the LayoutEditor this shape is in
067     */
068    public LayoutShape(String name, LayoutShapeType t, LayoutEditor layoutEditor) {
069        this(name, layoutEditor);
070        this.layoutShapeType = t;
071    }
072
073    /**
074     * constructor method (used by LayoutEditor)
075     *
076     * @param name         the name of the shape
077     * @param c            the Point2D for the initial point
078     * @param layoutEditor reference to the LayoutEditor this shape is in
079     */
080    public LayoutShape(String name, Point2D c, LayoutEditor layoutEditor) {
081        this(name, layoutEditor);
082        this.shapePoints.add(new LayoutShapePoint(c));
083    }
084
085    /**
086     * constructor method (used by duplicate)
087     *
088     * @param layoutShape to duplicate (deep copy)
089     */
090    public LayoutShape(LayoutShape layoutShape) {
091        this(layoutShape.getName(), layoutShape.getLayoutEditor());
092        this.setType(layoutShape.getType());
093        this.setLevel(layoutShape.getLevel());
094        this.setLineColor(layoutShape.getLineColor());
095        this.setFillColor(layoutShape.getFillColor());
096
097        for (LayoutShapePoint lsp : layoutShape.getPoints()) {
098            this.shapePoints.add(new LayoutShapePoint(lsp.getType(), lsp.getPoint()));
099        }
100    }
101
102    // this should only be used for debugging...
103    @Override
104    public String toString() {
105        return String.format("LayoutShape %s", name);
106    }
107
108    public String getDisplayName() {
109        return String.format("%s %s", Bundle.getMessage("LayoutShape"), name);
110    }
111
112    /**
113     * accessor methods
114     *
115     * @return the name of this shape
116     */
117    public String getName() {
118        return name;
119    }
120
121    public void setName(String n) {
122        name = n;
123    }
124
125    public LayoutShapeType getType() {
126        return layoutShapeType;
127    }
128
129    public void setType(LayoutShapeType t) {
130        if (layoutShapeType != t) {
131            switch (t) {
132                case Open:
133                case Closed:
134                case Filled:
135                    layoutShapeType = t;
136                    break;
137                default:    // You shouldn't ever have any invalid LayoutShapeTypes
138                    log.error("Invalid Shape Type {}", t); // I18IN
139            }
140        }
141    }
142
143    public int getLineWidth() {
144        return lineWidth;
145    }
146
147    public void setLineWidth(int w) {
148        lineWidth = Math.max(0, w);
149    }
150
151    public Color getLineColor() {
152        return lineColor;
153    }
154
155    public void setLineColor(Color color) {
156        lineColor = color;
157    }
158
159    public Color getFillColor() {
160        return fillColor;
161    }
162
163    public void setFillColor(Color color) {
164        fillColor = color;
165    }
166
167    public int getLevel() {
168        return level;
169    }
170
171    public void setLevel(int l) {
172        if (level != l) {
173            level = l;
174            layoutEditor.sortLayoutShapesByLevel();
175        }
176    }
177
178    public void setHidden(boolean hidden) {
179        this.hidden = hidden;
180        getLayoutEditor().redrawPanel();
181    }
182
183    public boolean isHidden() {
184        return hidden;
185    }
186
187    public LayoutEditor getLayoutEditor() {
188        return layoutEditor;
189    }
190
191    /**
192     * add point
193     *
194     * @param p the point to add
195     */
196    public void addPoint(Point2D p) {
197        if (shapePoints.size() < getMaxNumberPoints()) {
198            shapePoints.add(new LayoutShapePoint(p));
199        }
200    }
201
202    /**
203     * add point
204     *
205     * @param p         the point to add
206     * @param nearIndex the index of the existing point to add it near note:
207     *                  "near" is defined as before or after depending on
208     *                  closest neighbor
209     */
210    public void addPoint(Point2D p, int nearIndex) {
211        int cnt = shapePoints.size();
212        if (cnt < getMaxNumberPoints()) {
213            // this point
214            LayoutShapePoint lsp = shapePoints.get(nearIndex);
215            Point2D sp = lsp.getPoint();
216
217            // left point
218            int idxL = (nearIndex + cnt - 1) % cnt;
219            LayoutShapePoint lspL = shapePoints.get(idxL);
220            Point2D pL = lspL.getPoint();
221            double distL = MathUtil.distance(p, pL);
222
223            // right point
224            int idxR = (nearIndex + 1) % cnt;
225            LayoutShapePoint lspR = shapePoints.get(idxR);
226            Point2D pR = lspR.getPoint();
227            double distR = MathUtil.distance(p, pR);
228
229            // if nearIndex is the 1st point in open shape...
230            if ((getType() == LayoutShapeType.Open) && (nearIndex == 0)) {
231                distR = MathUtil.distance(pR, p);
232                distL = MathUtil.distance(pR, sp);
233            }
234            int beforeIndex = (distR < distL) ? idxR : nearIndex;
235
236            // if nearIndex is the last point in open shape...
237            if ((getType() == LayoutShapeType.Open) && (idxR == 0)) {
238                distR = MathUtil.distance(pL, p);
239                distL = MathUtil.distance(pL, sp);
240                beforeIndex = (distR < distL) ? nearIndex : nearIndex + 1;
241            }
242
243            if (beforeIndex >= cnt) {
244                shapePoints.add(new LayoutShapePoint(p));
245            } else {
246                shapePoints.add(beforeIndex, new LayoutShapePoint(p));
247            }
248        }
249    }
250
251    /**
252     * add point
253     *
254     * @param t the type of point to add
255     * @param p the point to add
256     */
257    public void addPoint(LayoutShapePointType t, Point2D p) {
258        if (shapePoints.size() < getMaxNumberPoints()) {
259            shapePoints.add(new LayoutShapePoint(t, p));
260        }
261    }
262
263    /**
264     * set point
265     *
266     * @param idx the index of the point to add
267     * @param p   the point to add
268     */
269    public void setPoint(int idx, Point2D p) {
270        if (idx < shapePoints.size()) {
271            shapePoints.get(idx).setPoint(p);
272        }
273    }
274
275    /**
276     * Get point.
277     *
278     * @param idx the index of the point to add.
279     * @return the 2D point of the ID, MathUtil.zeroPoint2D if no result.
280     */
281    public Point2D getPoint(int idx) {
282        Point2D result = MathUtil.zeroPoint2D;
283        if (idx < shapePoints.size()) {
284            result = shapePoints.get(idx).getPoint();
285        }
286        return result;
287    }
288
289    // should only be used by xml save code
290    public ArrayList<LayoutShapePoint> getPoints() {
291        return shapePoints;
292    }
293
294    /**
295     * get the number of points
296     *
297     * @return the number of points
298     */
299    public int getNumberPoints() {
300        return shapePoints.size();
301    }
302
303    /**
304     * get the maximum number of points
305     *
306     * @return the maximum number of points
307     */
308    public int getMaxNumberPoints() {
309        return HitPointType.NUM_SHAPE_POINTS;
310    }
311
312    /**
313     * getBounds() - return the bounds of this shape
314     *
315     * @return Rectangle2D as bound of this shape
316     */
317    public Rectangle2D getBounds() {
318        Rectangle2D result;
319
320        if (!shapePoints.isEmpty()) {
321            result = MathUtil.rectangleAtPoint(shapePoints.get(0).getPoint(), 1.0, 1.0);
322            shapePoints.forEach((lsp) -> result.add(lsp.getPoint()));
323        } else {
324            result = null;  // this should never happen... but just in case
325        }
326        return result;
327    }
328
329    /**
330     * find the hit (location) type for a point
331     *
332     * @param hitPoint      the point
333     * @param useRectangles whether to use (larger) rectangles or (smaller)
334     *                      circles for hit testing
335     * @return the hit point type for the point (or NONE)
336     */
337    protected HitPointType findHitPointType(@Nonnull Point2D hitPoint, boolean useRectangles) {
338        HitPointType result = HitPointType.NONE;  // assume point not on shape
339
340        if (useRectangles) {
341            // rather than create rectangles for all the points below and
342            // see if the passed in point is in one of those rectangles
343            // we can create a rectangle for the passed in point and then
344            // test if any of the points below are in that rectangle instead.
345            Rectangle2D r = layoutEditor.layoutEditorControlRectAt(hitPoint);
346
347            if (r.contains(getCoordsCenter())) {
348                result = HitPointType.SHAPE_CENTER;
349            }
350            for (int idx = 0; idx < shapePoints.size(); idx++) {
351                if (r.contains(shapePoints.get(idx).getPoint())) {
352                    result = HitPointType.shapePointIndexedValue(idx);
353                    break;
354                }
355            }
356        } else {
357            double distance, minDistance = LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
358            for (int idx = 0; idx < shapePoints.size(); idx++) {
359                distance = MathUtil.distance(shapePoints.get(idx).getPoint(), hitPoint);
360                if (distance < minDistance) {
361                    minDistance = distance;
362                    result = HitPointType.shapePointIndexedValue(idx);
363                }
364            }
365        }
366        return result;
367    }   // findHitPointType
368
369    public static boolean isShapeHitPointType(HitPointType t) {
370        return ((t == HitPointType.SHAPE_CENTER)
371                || HitPointType.isShapePointOffsetHitPointType(t));
372    }
373
374    /**
375     * get coordinates of center point of shape
376     *
377     * @return Point2D coordinates of center point of shape
378     */
379    public Point2D getCoordsCenter() {
380        Point2D sumPoint = MathUtil.zeroPoint2D();
381        for (LayoutShapePoint lsp : shapePoints) {
382            sumPoint = MathUtil.add(sumPoint, lsp.getPoint());
383        }
384        return MathUtil.divide(sumPoint, shapePoints.size());
385    }
386
387    /*
388    * Modify coordinates methods
389     */
390    /**
391     * set center coordinates
392     *
393     * @param p the coordinates to set
394     */
395//    @Override
396    public void setCoordsCenter(@Nonnull Point2D p) {
397        Point2D factor = MathUtil.subtract(p, getCoordsCenter());
398        if (!MathUtil.isEqualToZeroPoint2D(factor)) {
399            shapePoints.forEach((lsp) -> lsp.setPoint(MathUtil.add(factor, lsp.getPoint())));
400        }
401    }
402
403    /**
404     * scale this shapes coordinates by the x and y factors
405     *
406     * @param xFactor the amount to scale X coordinates
407     * @param yFactor the amount to scale Y coordinates
408     */
409    public void scaleCoords(double xFactor, double yFactor) {
410        Point2D factor = new Point2D.Double(xFactor, yFactor);
411        shapePoints.forEach((lsp) -> lsp.setPoint(MathUtil.multiply(lsp.getPoint(), factor)));
412    }
413
414    /**
415     * translate this shapes coordinates by the x and y factors
416     *
417     * @param xFactor the amount to translate X coordinates
418     * @param yFactor the amount to translate Y coordinates
419     */
420    public void translateCoords(double xFactor, double yFactor) {
421        Point2D factor = new Point2D.Double(xFactor, yFactor);
422        shapePoints.forEach((lsp) -> lsp.setPoint(MathUtil.add(factor, lsp.getPoint())));
423    }
424
425    /**
426     * rotate this LayoutTrack's coordinates by angleDEG's
427     *
428     * @param angleDEG the amount to rotate in degrees
429     */
430    public void rotateCoords(double angleDEG) {
431        Point2D center = getCoordsCenter();
432        shapePoints.forEach((lsp) -> lsp.setPoint(MathUtil.rotateDEG(lsp.getPoint(), center, angleDEG)));
433    }
434
435    private JPopupMenu popup = null;
436    private final JCheckBoxMenuItem hiddenCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ShapeHiddenMenuItemTitle"));
437
438    @Nonnull
439    protected JPopupMenu showShapePopUp(@CheckForNull JmriMouseEvent mouseEvent, HitPointType hitPointType) {
440        if (popup != null) {
441            popup.removeAll();
442        } else {
443            popup = new JPopupMenu();
444        }
445        if (layoutEditor.isEditable()) {
446
447            // JMenuItem jmi = popup.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("LayoutShape")) + getName());
448            JMenuItem jmi = popup.add(Bundle.getMessage("ShapeNameMenuItemTitle", getName()));
449
450            jmi.setToolTipText(Bundle.getMessage("ShapeNameMenuItemToolTip"));
451            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
452                // prompt for new name
453                String newValue = QuickPromptUtil.promptForString(layoutEditor,
454                        Bundle.getMessage("LayoutShapeName"),
455                        Bundle.getMessage("LayoutShapeName"),
456                        name);
457                LayoutEditorFindItems finder = layoutEditor.getFinder();
458                if (finder.findLayoutShapeByName(newValue) == null) {
459                    setName(newValue);
460                    layoutEditor.repaint();
461                } else {
462                    JmriJOptionPane.showMessageDialog(null,
463                        Bundle.getMessage("CanNotRename", Bundle.getMessage("Shape")),
464                        Bundle.getMessage("AlreadyExist", Bundle.getMessage("Shape")),
465                        JmriJOptionPane.ERROR_MESSAGE);
466
467                }
468            });
469
470            popup.add(new JSeparator(JSeparator.HORIZONTAL));
471
472//            if (true) { // only enable for debugging; TODO: delete or disable this for production
473//                jmi = popup.add("hitPointType: " + hitPointType);
474//                jmi.setEnabled(false);
475//            }
476
477            // add "Change Shape Type to..." menu
478            JMenu shapeTypeMenu = new JMenu(Bundle.getMessage("ChangeShapeTypeFromTo", getType().toString()));
479            if (getType() != LayoutShapeType.Open) {
480                jmi = shapeTypeMenu.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("ShapeTypeOpen")) {
481                    @Override
482                    public void actionPerformed(ActionEvent e) {
483                        setType(LayoutShapeType.Open);
484                        layoutEditor.repaint();
485                    }
486                }));
487            }
488
489            if (getType() != LayoutShapeType.Closed) {
490                jmi = shapeTypeMenu.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("ShapeTypeClosed")) {
491                    @Override
492                    public void actionPerformed(ActionEvent e) {
493                        setType(LayoutShapeType.Closed);
494                        layoutEditor.repaint();
495                    }
496                }));
497            }
498
499            if (getType() != LayoutShapeType.Filled) {
500                jmi = shapeTypeMenu.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("ShapeTypeFilled")) {
501                    @Override
502                    public void actionPerformed(ActionEvent e) {
503                        setType(LayoutShapeType.Filled);
504                        layoutEditor.repaint();
505                    }
506                }));
507            }
508
509            popup.add(shapeTypeMenu);
510
511            // Add "Change Shape Type from {0} to..." menu
512            if (hitPointType == HitPointType.SHAPE_CENTER) {
513                JMenu shapePointTypeMenu = new JMenu(Bundle.getMessage("ChangeAllShapePointTypesTo"));
514                jmi = shapePointTypeMenu.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("ShapePointTypeStraight")) {
515                    @Override
516                    public void actionPerformed(ActionEvent e) {
517                        for (LayoutShapePoint ls : shapePoints) {
518                            ls.setType(LayoutShapePointType.Straight);
519                        }
520                        layoutEditor.repaint();
521                    }
522                }));
523
524                jmi = shapePointTypeMenu.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("ShapePointTypeCurve")) {
525                    @Override
526                    public void actionPerformed(ActionEvent e) {
527                        for (LayoutShapePoint ls : shapePoints) {
528                            ls.setType(LayoutShapePointType.Curve);
529                        }
530                        layoutEditor.repaint();
531                    }
532                }));
533
534                popup.add(shapePointTypeMenu);
535            } else {
536                LayoutShapePoint lsp = shapePoints.get(hitPointType.shapePointIndex());
537                if (lsp != null) { // this should never happen... but just in case...
538                    String otherPointTypeName = (lsp.getType() == LayoutShapePointType.Straight)
539                            ? LayoutShapePointType.Curve.toString() : LayoutShapePointType.Straight.toString();
540                    jmi = popup.add(Bundle.getMessage("ChangeShapePointTypeFromTo", lsp.getType().toString(), otherPointTypeName));
541                    jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
542                        switch (lsp.getType()) {
543                            case Straight: {
544                                lsp.setType(LayoutShapePointType.Curve);
545                                break;
546                            }
547                            case Curve: {
548                                lsp.setType(LayoutShapePointType.Straight);
549                                break;
550                            }
551                            default:
552                                log.error("unexpected enum member!");
553                        }
554                        layoutEditor.repaint();
555                    });
556                }
557            }
558
559            // Add "Set Level: x" menu
560            jmi = popup.add(new JMenuItem(Bundle.getMessage("MakeLabel",
561                    Bundle.getMessage("ShapeLevelMenuItemTitle")) + level));
562            jmi.setToolTipText(Bundle.getMessage("ShapeLevelMenuItemToolTip"));
563            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
564                // prompt for level
565                int newValue = QuickPromptUtil.promptForInteger(layoutEditor,
566                        Bundle.getMessage("ShapeLevelMenuItemTitle"),
567                        Bundle.getMessage("ShapeLevelMenuItemTitle"),
568                        level, QuickPromptUtil.checkIntRange(1, 10, null));
569                setLevel(newValue);
570                layoutEditor.repaint();
571            });
572
573            jmi = popup.add(new JMenuItem(Bundle.getMessage("ShapeLineColorMenuItemTitle")));
574            jmi.setToolTipText(Bundle.getMessage("ShapeLineColorMenuItemToolTip"));
575            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
576                Color newColor = JmriColorChooser.showDialog(null, "Choose a color", lineColor);
577                if ((newColor != null) && !newColor.equals(lineColor)) {
578                    setLineColor(newColor);
579                    layoutEditor.repaint();
580                }
581            });
582            jmi.setForeground(lineColor);
583            jmi.setBackground(ColorUtil.contrast(lineColor));
584
585            if (getType() == LayoutShapeType.Filled) {
586                jmi = popup.add(new JMenuItem(Bundle.getMessage("ShapeFillColorMenuItemTitle")));
587                jmi.setToolTipText(Bundle.getMessage("ShapeFillColorMenuItemToolTip"));
588                jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
589                    Color newColor = JmriColorChooser.showDialog(null, "Choose a color", fillColor);
590                    if ((newColor != null) && !newColor.equals(fillColor)) {
591                        setFillColor(newColor);
592                        layoutEditor.repaint();
593                    }
594                });
595                jmi.setForeground(fillColor);
596                jmi.setBackground(ColorUtil.contrast(fillColor));
597            }
598
599            // add "Set Line Width: x" menu
600            jmi = popup.add(new JMenuItem(Bundle.getMessage("MakeLabel",
601                    Bundle.getMessage("ShapeLineWidthMenuItemTitle")) + lineWidth));
602            jmi.setToolTipText(Bundle.getMessage("ShapeLineWidthMenuItemToolTip"));
603            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
604                // prompt for lineWidth
605                int newValue = QuickPromptUtil.promptForInteger(layoutEditor,
606                        Bundle.getMessage("ShapeLineWidthMenuItemTitle"),
607                        Bundle.getMessage("ShapeLineWidthMenuItemTitle"),
608                        lineWidth, QuickPromptUtil.checkIntRange(1, MAX_LINEWIDTH, null));
609                setLineWidth(newValue);
610                layoutEditor.repaint();
611            });
612
613            popup.add(new JSeparator(JSeparator.HORIZONTAL));
614            if (hitPointType == HitPointType.SHAPE_CENTER) {
615                jmi = popup.add(new AbstractAction(Bundle.getMessage("ShapeDuplicateMenuItemTitle")) {
616                    @Override
617                    public void actionPerformed(ActionEvent e) {
618                        LayoutShape ls = new LayoutShape(LayoutShape.this);
619                        ls.setName(layoutEditor.getFinder().uniqueName("S"));
620
621                        double gridSize = layoutEditor.gContext.getGridSize();
622                        Point2D delta = new Point2D.Double(gridSize, gridSize);
623                        for (LayoutShapePoint lsp : ls.getPoints()) {
624                            lsp.setPoint(MathUtil.add(lsp.getPoint(), delta));
625                        }
626                        layoutEditor.getLayoutShapes().add(ls);
627                        layoutEditor.clearSelectionGroups();
628                        layoutEditor.amendSelectionGroup(ls);
629                    }
630                });
631                jmi.setToolTipText(Bundle.getMessage("ShapeDuplicateMenuItemToolTip"));
632
633                popup.add(hiddenCheckBoxMenuItem);
634                hiddenCheckBoxMenuItem.addActionListener((java.awt.event.ActionEvent e3) ->
635                        setHidden(hiddenCheckBoxMenuItem.isSelected()));
636                hiddenCheckBoxMenuItem.setToolTipText(Bundle.getMessage("ShapeHiddenMenuItemToolTip"));
637                hiddenCheckBoxMenuItem.setSelected(isHidden());
638
639                popup.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
640                    @Override
641                    public void actionPerformed(ActionEvent e) {
642                        removeShape();
643                    }
644                });
645            } else {
646                popup.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
647                    @Override
648                    public void actionPerformed(ActionEvent e) {
649                        if (shapePoints.size() == 1) {
650                            removeShape();
651                        } else {
652                            shapePoints.remove(hitPointType.shapePointIndex());
653                            layoutEditor.repaint();
654                        }
655                    }
656                });
657            }
658            if (mouseEvent != null) {
659                popup.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
660            }
661        }
662        return popup;
663    }   // showPopup
664
665    void removeShape() {
666        if (layoutEditor.removeLayoutShape(LayoutShape.this)) {
667            // Returned true if user did not cancel
668            remove();
669            dispose();
670        }
671    }
672
673    /**
674     * Clean up when this object is no longer needed. Should not be called while
675     * the object is still displayed; see remove()
676     */
677    //@Override
678    void dispose() {
679        if (popup != null) {
680            popup.removeAll();
681        }
682        popup = null;
683    }
684
685    /**
686     * Removes this object from display and persistence
687     */
688    //@Override
689    void remove() {
690    }
691
692    //@Override
693    protected void draw(Graphics2D g2) {
694        if (isHidden()) {
695            return;
696        }
697
698        GeneralPath path = new GeneralPath();
699
700        int idx, cnt = shapePoints.size();
701        for (idx = 0; idx < cnt; idx++) {
702            // this point
703            LayoutShapePoint lsp = shapePoints.get(idx);
704            Point2D p = lsp.getPoint();
705
706            // left point
707            int idxL = (idx + cnt - 1) % cnt;
708            LayoutShapePoint lspL = shapePoints.get(idxL);
709            Point2D pL = lspL.getPoint();
710            Point2D midL = MathUtil.midPoint(pL, p);
711
712            // right point
713            int idxR = (idx + 1) % cnt;
714            LayoutShapePoint lspR = shapePoints.get(idxR);
715            Point2D pR = lspR.getPoint();
716            Point2D midR = MathUtil.midPoint(p, pR);
717
718            // if this is an open shape...
719            LayoutShapePointType lspt = lsp.getType();
720            if (getType() == LayoutShapeType.Open) {
721                // and this is first or last point...
722                if ((idx == 0) || (idxR == 0)) {
723                    // then force straight shape point type
724                    lspt = LayoutShapePointType.Straight;
725                }
726            }
727            switch (lspt) {
728                case Straight: {
729                    if (idx == 0) { // if this is the first point...
730                        // ...and our shape is open...
731                        if (getType() == LayoutShapeType.Open) {
732                            path.moveTo(p.getX(), p.getY());    // then start here
733                        } else {    // otherwise
734                            path.moveTo(midL.getX(), midL.getY());  // start here
735                            path.lineTo(p.getX(), p.getY());        // draw to here
736                        }
737                    } else {
738                        path.lineTo(midL.getX(), midL.getY());  // start here
739                        path.lineTo(p.getX(), p.getY());        // draw to here
740                    }
741                    // if this is not the last point...
742                    // ...or our shape isn't open
743                    if ((idxR != 0) || (getType() != LayoutShapeType.Open)) {
744                        path.lineTo(midR.getX(), midR.getY());      // draw to here
745                    }
746                    break;
747                }
748
749                case Curve: {
750                    if (idx == 0) { // if this is the first point
751                        path.moveTo(midL.getX(), midL.getY());  // then start here
752                    }
753                    path.quadTo(p.getX(), p.getY(), midR.getX(), midR.getY());
754                    break;
755                }
756
757                default:
758                    log.error("unexpected enum member!");
759            }
760        }   // for (idx = 0; idx < cnt; idx++)
761
762        if (getType() == LayoutShapeType.Filled) {
763            g2.setColor(fillColor);
764            g2.fill(path);
765        }
766        g2.setStroke(new BasicStroke(lineWidth,
767                BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
768        g2.setColor(lineColor);
769        g2.draw(path);
770    }   // draw
771
772    protected void drawEditControls(Graphics2D g2) {
773        Color backgroundColor = layoutEditor.getBackground();
774        Color controlsColor = ColorUtil.contrast(backgroundColor);
775        controlsColor = ColorUtil.setAlpha(controlsColor, 0.5);
776        g2.setColor(controlsColor);
777
778        shapePoints.forEach((slp) -> g2.draw(layoutEditor.layoutEditorControlRectAt(slp.getPoint())));
779        if (!shapePoints.isEmpty()) {
780            Point2D end0 = shapePoints.get(0).getPoint();
781            Point2D end1 = end0;
782            for (LayoutShapePoint lsp : shapePoints) {
783                Point2D end2 = lsp.getPoint();
784                g2.draw(new Line2D.Double(end1, end2));
785                end1 = end2;
786            }
787
788            if (getType() != LayoutShapeType.Open) {
789                g2.draw(new Line2D.Double(end1, end0));
790            }
791        }
792
793        g2.draw(trackEditControlCircleAt(getCoordsCenter()));
794    }   // drawEditControls
795
796    // these are convenience methods to return circles used to draw onscreen
797    //
798    // compute the control point rect at inPoint; use the turnout circle size
799    public Ellipse2D trackEditControlCircleAt(@Nonnull Point2D inPoint) {
800        return trackControlCircleAt(inPoint);
801    }
802
803    // compute the turnout circle at inPoint (used for drawing)
804    public Ellipse2D trackControlCircleAt(@Nonnull Point2D inPoint) {
805        return new Ellipse2D.Double(inPoint.getX() - layoutEditor.circleRadius,
806                inPoint.getY() - layoutEditor.circleRadius,
807                layoutEditor.circleDiameter, layoutEditor.circleDiameter);
808    }
809
810    /**
811     * These are the points that make up the outline of the shape. Each point
812     * can be ether a straight or a control point for a curve
813     */
814    public static class LayoutShapePoint {
815
816        private LayoutShapePointType type;
817        private Point2D point;
818
819        /**
820         * constructor method
821         *
822         * @param c Point2D for initial point
823         */
824        public LayoutShapePoint(Point2D c) {
825            this.type = LayoutShapePointType.Straight;
826            this.point = c;
827        }
828
829        /**
830         * Constructor method.
831         *
832         * @param t the layout shape point type.
833         * @param c Point2D for initial point
834         */
835        public LayoutShapePoint(LayoutShapePointType t, Point2D c) {
836            this(c);
837            this.type = t;
838        }
839
840        /**
841         * accessor methods
842         *
843         * @return the LayoutShapePointType
844         */
845        public LayoutShapePointType getType() {
846            return type;
847        }
848
849        public void setType(LayoutShapePointType type) {
850            this.type = type;
851        }
852
853        public Point2D getPoint() {
854            return point;
855        }
856
857        public void setPoint(Point2D point) {
858            this.point = point;
859        }
860    }   // class LayoutShapePoint
861
862    /**
863     * enum LayoutShapeType
864     */
865    public enum LayoutShapeType {
866        Open,
867        Closed,
868        Filled;
869    }
870
871    /**
872     * enum LayoutShapePointType Straight, Curve
873     */
874    public enum LayoutShapePointType {
875        Straight,
876        Curve;
877    }
878
879    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutShape.class);
880}