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