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}