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