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}