source: trunk/src/org/expeditee/items/Line.java@ 475

Last change on this file since 475 was 475, checked in by davidb, 11 years ago

Additional work needed to support Nibb attribute for arrowhead tip thickness

File size: 30.9 KB
Line 
1package org.expeditee.items;
2
3import java.awt.BasicStroke;
4import java.awt.Color;
5import java.awt.Graphics2D;
6import java.awt.Point;
7import java.awt.Polygon;
8import java.awt.Rectangle;
9import java.awt.RenderingHints;
10import java.awt.Shape;
11import java.awt.Stroke;
12import java.awt.geom.AffineTransform;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.HashSet;
16import java.util.Iterator;
17import java.util.LinkedList;
18import java.util.List;
19
20import org.expeditee.gui.DisplayIO;
21import org.expeditee.gui.Frame;
22
23/**
24 * Implements a line that is drawn on the screen. A line is represented as two
25 * Items that share a common Line ID.
26 *
27 * @author jdm18
28 *
29 */
30public class Line extends Item {
31
32 private static final int AUTO_ARROWHEAD_LENGTH = -1;
33
34 private static final int ARROW_HEAD_TO_LENGTH_RATIO = 8;
35
36 private static final int MINIMUM_ARROW_HEAD_LENGTH = 8;
37
38 private static final int MAXIMUM_ARROW_HEAD_LENGTH = 50;
39
40
41 public static Polygon createArrowheadPolygon(double x0, double y0, float arrowLength0, double arrowRatio0, double arrowNibPerc0)
42 {
43
44 Polygon arrowhead = new Polygon();
45
46 int ix0 = (int)Math.round(x0);
47 int iy0 = (int)Math.round(y0);
48
49 int flange_ix_project = (int) Math.round(x0 - arrowLength0);
50 int ix_nib = (int)Math.round(x0 - (arrowLength0*arrowNibPerc0));
51
52 double flange_y_width = (arrowLength0 * arrowRatio0);
53 int flange_iy_left = (int) Math.round((y0 - flange_y_width));
54 int flange_iy_right = (int) Math.round((y0 + flange_y_width));
55
56
57 arrowhead.addPoint(ix0, iy0);
58 arrowhead.addPoint(flange_ix_project, flange_iy_left);
59
60 arrowhead.addPoint(ix_nib, iy0);
61
62 arrowhead.addPoint(flange_ix_project, flange_iy_right);
63
64 return arrowhead;
65 }
66
67 private Item _start;
68
69 private Item _end;
70
71 // old point locations, used for clearing
72 private Point _startOffset = new Point(0, 0);
73
74 private Point _endOffset = new Point(0, 0);
75
76 // brush strokes used for painting this line and highlighting
77 private Stroke _lineStroke = new BasicStroke(DEFAULT_THICKNESS, CAP, JOIN, 4.0F);
78
79 Stroke getStroke() {
80 return _lineStroke;
81 }
82
83 /**
84 * Constructs a new Line with the given start and end point and Item ID.
85 *
86 * @param start
87 * The starting Point of this Line.
88 * @param end
89 * The end Point of this Line.
90 * @param id
91 * The Item ID of this Line.
92 */
93 public Line(Item start, Item end, int id) {
94 super();
95 setID(id);
96 _start = start;
97 _end = end;
98
99 // update the two end points to add this line
100 _start.addLine(this);
101 _end.addLine(this);
102
103 // initialise the line size
104 float thick = (_start.getThickness() + _end.getThickness()) / 2.0f;
105
106 if (thick < 0)
107 setThickness(DEFAULT_THICKNESS);
108 else {
109 refreshStroke(thick);
110 }
111
112 }
113
114 public void refreshStroke(float thick) {
115 thick = Math.round(thick);
116
117 int[] pattern = _start.getLinePattern();
118 if (pattern == null)
119 pattern = _end.getLinePattern();
120
121 if (pattern != null) {
122 float[] dash = new float[pattern.length];
123 for (int i = 0; i < pattern.length; i++)
124 dash[i] = (float) pattern[i];
125 _lineStroke = new BasicStroke(Math.max(thick,
126 MINIMUM_PAINT_THICKNESS), CAP, JOIN, 10f, dash, 0.0f);
127 } else {
128 _lineStroke = new BasicStroke(Math.max(thick,
129 MINIMUM_PAINT_THICKNESS), CAP, JOIN);
130 }
131
132 updatePolygon();
133 }
134
135 /**
136 * Returns the Dot at the start of this Line.
137 *
138 * @return The Dot that is the start of this line.
139 */
140 public Item getStartItem() {
141 return _start;
142 }
143
144 /**
145 * Returns the Dot that is the end of this Line.
146 *
147 * @return The Dot that is the end of this line.
148 */
149 public Item getEndItem() {
150 return _end;
151 }
152
153 /**
154 * Returns the Dot at the opposite end to the Dot given. If the given Dot is
155 * not part of this line, null is returned.
156 *
157 * @param fromThis
158 * The Dot at the opposite end than this Dot is returned
159 * @return The Dot at the opposite end of this Line to the given Dot, or
160 * Null if the given Dot is not on this Line.
161 */
162 public Item getOppositeEnd(Item fromThis) {
163 if (_start == fromThis)
164 return _end;
165
166 if (_end == fromThis)
167 return _start;
168
169 return null;
170 }
171
172 /**
173 * Replaces either the start or end point of this Line with the given
174 * LineEnd. This is used when merging LineEnds. It will also add and remove
175 * the line from the LineEnds as well as adjust constraints.
176 *
177 * @param replace
178 * Either the start or end Dot of this Line indicating which is
179 * to be replaced.
180 * @param with
181 * The Dot to replace the start or end Dot with.
182 */
183 public void replaceLineEnd(Item replace, Item with) {
184 Item otherEnd = null;
185 if (_start == replace) {
186 setStartItem(with);
187 otherEnd = _end;
188 } else if (_end == replace) {
189 setEndItem(with);
190 otherEnd = _start;
191 }
192 // if no end was replaced
193 if (otherEnd == null)
194 return;
195 // Copy the constraints list so the endPoints list can be modified
196 List<Constraint> constraints = new ArrayList<Constraint>(replace
197 .getConstraints());
198 // Now change all the constraints for this line only
199 for (int i = 0; i < constraints.size(); i++) {
200 Constraint c = constraints.get(i);
201 if (c.contains(otherEnd)) {
202 c.replaceEnd(replace, with);
203 }
204 }
205 }
206
207 public void setStartItem(Item start) {
208 if (start != null) {
209 _start.removeLine(this);
210
211 /*
212 * if (!(start instanceof Dot)) { _startOffset.x = _start.getX() -
213 * start.getX(); _startOffset.y = _start.getY() - start.getY(); }
214 * else
215 */{
216 _startOffset.x = 0;
217 _startOffset.y = 0;
218 }
219
220 _start = start;
221 _start.addLine(this);
222 updatePolygon();
223 }
224 }
225
226 public void setEndItem(Item end) {
227 if (end != null) {
228 _end.removeLine(this);
229
230 /*
231 * if (!(end instanceof Dot)) { _endOffset.x = _end.getX() -
232 * end.getX(); _endOffset.y = _end.getY() - end.getY(); } else
233 */{
234 _endOffset.x = 0;
235 _endOffset.y = 0;
236 }
237
238 _end = end;
239 _end.addLine(this);
240 updatePolygon();
241 }
242 }
243
244 /**
245 * Returns the Y value of the start Dot of this Line. To determine the end
246 * Dot Y value use getEndDot().getY()
247 *
248 * @return The Y value of the starting Dot of this Line.
249 * @see #getStartDot()
250 * @see #getEndDot()
251 */
252 public int getY() {
253 return _start.getY();
254 }
255
256 /**
257 * Returns the X value of the start Dot of this Line. To determine the end
258 * Dot X value use getEndDot().getX()
259 *
260 * @return The X value of the starting Dot of this Line.
261 * @see #getStartDot()
262 * @see #getEndDot()
263 */
264 public int getX() {
265 // return 0;
266 return _start.getX();
267 }
268
269 @Override
270 public void setX(float x) {
271 }
272
273 @Override
274 public void setY(float y) {
275 }
276
277 @Override
278 public void setPosition(float x, float y) {
279 }
280
281 @Override
282 public String getLink() {
283 return null;
284 }
285
286 @Override
287 public void setLinePattern(int[] pattern) {
288 super.setLinePattern(pattern);
289
290 float thick = Math.round(getThickness());
291
292 if (thick < 0)
293 thick = DEFAULT_THICKNESS;
294
295 if (pattern != null) {
296 float[] dash = new float[pattern.length];
297 for (int i = 0; i < pattern.length; i++)
298 dash[i] = (float) pattern[i];
299 _lineStroke = new BasicStroke(Math.max(thick, MINIMUM_THICKNESS),
300 CAP, JOIN, 10f, dash, 0.0f);
301 } else
302 _lineStroke = new BasicStroke(Math.max(thick, MINIMUM_THICKNESS),
303 CAP, JOIN);
304
305 if (_start.getLinePattern() != pattern)
306 _start.setLinePattern(pattern);
307
308 if (_end.getLinePattern() != pattern)
309 _end.setLinePattern(pattern);
310
311 invalidateAll();
312 }
313
314 @Override
315 public void paint(Graphics2D g) {
316 if (!isVisible())
317 return;
318
319 // Dont paint lines with thickness 0 if they are the border on a filled
320 // shape
321 if (dontPaint())
322 return;
323
324 g.setColor(getPaintColor());
325
326 // if (this._mode == Item.SelectedMode.Disconnect)
327 // System.out.println("Disconnect mode!");
328 g.setStroke(_lineStroke);
329 // Get a path of points
330 int[][][] paths = getPaths();
331 for (int i = 0; i < paths.length; i++) {
332 int[][] path = paths[i];
333 int last = path[0].length - 1;
334 if (path[0][0] == path[0][last] && path[1][0] == path[1][last]) {
335 g.drawPolygon(path[0], path[1], last);
336 } else {
337 g.drawPolyline(path[0], path[1], last + 1);
338 }
339 }
340 // paint the arrowhead (if necessary)
341 paintArrows(g);
342
343 if (showLineHighlight() && isHighlighted()) {
344 g.setColor(getHighlightColor());
345 g.setStroke(HIGHLIGHT_STROKE);
346 ((Graphics2D) g).draw(this.getArea());
347 }
348 }
349
350 protected boolean dontPaint() {
351 return getThickness() <= 0 && _start.isEnclosed()
352 && _start.getFillColor() != null;
353 }
354
355 protected int[][][] getPaths() {
356 List<List<Point>> pointPaths = new LinkedList<List<Point>>();
357 Collection<Line> visited = new HashSet<Line>();
358 LinkedList<Line> toExplore = new LinkedList<Line>();
359 toExplore.add(this);
360 while (toExplore.size() > 0) {
361 Line nextLine = toExplore.remove(0);
362 // Start at the item we have already explored... unless both have
363 // been explored
364 if (!visited.contains(nextLine)) {
365 pointPaths.add(nextLine.getPath(visited, toExplore));
366 }
367 }
368 // Put the paths into int arrays
369 int[][][] paths = new int[pointPaths.size()][][];
370 Iterator<List<Point>> iter = pointPaths.iterator();
371
372 for (int i = 0; i < paths.length; i++) {
373 List<Point> pointPath = iter.next();
374 int[][] path = new int[2][pointPath.size()];
375 paths[i] = path;
376 // Add all the x and y's to the array
377 for (int j = 0; j < path[0].length; j++) {
378 path[0][j] = pointPath.get(j).x;
379 path[1][j] = pointPath.get(j).y;
380 }
381 }
382 return paths;
383 }
384
385 protected List<Point> getPath(Collection<Line> visited,
386 LinkedList<Line> toExplore) {
387 LinkedList<Point> points = new LinkedList<Point>();
388 // put the start item points into our list
389 Item start = getStartItem();
390 Item end = getEndItem();
391 visited.add(this);
392
393 start.appendPath(visited, points, true, toExplore);
394 end.appendPath(visited, points, false, toExplore);
395 return points;
396 }
397
398 public void paintArrows(Graphics2D g) {
399 if (dontPaint())
400 return;
401
402 g.setColor(getPaintColor());
403 g.setStroke(new BasicStroke(getPaintThickness(), CAP,
404 BasicStroke.JOIN_MITER));
405 paintArrow(g, _start, _startOffset, _endOffset);
406 paintArrow(g, _end, _endOffset, _startOffset);
407
408 if (_virtualSpot != null) {
409 _virtualSpot.paint(g);
410 invalidateVirtualSpot();
411 _virtualSpot = null;
412 }
413 }
414
415 private float getPaintThickness() {
416 float thickness = getThickness();
417 if (thickness > 0) {
418 return thickness;
419 }
420 return MINIMUM_PAINT_THICKNESS;
421 }
422
423 private void invalidateVirtualSpot() {
424 assert (_virtualSpot != null);
425
426 invalidate(_virtualSpot.getDrawingArea());
427
428 }
429
430 /**
431 * Based on code from DeSL (Arrow2D) http://sourceforge.net/projects/desl/
432 */
433 private void paintArrow(Graphics2D g, Item withArrow, Point startOffset,
434 Point endOffset) {
435 boolean disconnectMode = withArrow._mode == Item.HighlightMode.Disconnect;
436 // only draw an arrowhead if necessary
437 if (!(this._mode == Item.HighlightMode.Disconnect && disconnectMode)
438 && (!withArrow.hasVisibleArrow() || withArrow.getLines().size() > 1))
439 return;
440
441 int x0, x1, y0, y1;
442
443 x1 = withArrow.getX() + startOffset.x;
444 y1 = withArrow.getY() + startOffset.y;
445 x0 = getOppositeEnd(withArrow).getX() + endOffset.x;
446 y0 = getOppositeEnd(withArrow).getY() + endOffset.y;
447
448 float arrowLength = withArrow.getArrowheadLength();
449 double arrowRatio = withArrow.getArrowheadRatio();
450 double arrowNibPerc = withArrow.getArrowheadNibPerc();
451
452 // set the size of the disconnect indicator arrowhead
453 if (this._mode == Item.HighlightMode.Disconnect) {
454 arrowLength = 15;
455 arrowRatio = 0.3;
456 arrowNibPerc = 0.5;
457 }
458
459 // if the arrowhead is 'auto', then one and only one end must be
460 // floating
461 if (arrowLength != AUTO_ARROWHEAD_LENGTH && (arrowRatio < 0 || arrowNibPerc < 0)) {
462 return;
463 }
464
465 int deltaX = x1 - x0;
466 int deltaY = y1 - y0;
467
468 int length = (int) Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
469
470 // The length of the line must at least be as long as the arrow or we
471 // wont show the arrow
472 if (length <= MINIMUM_ARROW_HEAD_LENGTH)
473 return;
474 if (arrowLength == AUTO_ARROWHEAD_LENGTH) {
475 arrowLength = getAutoArrowheadLength(length);
476 withArrow.setArrowhead(null);
477 if (arrowRatio < 0) {
478 arrowRatio = DEFAULT_ARROWHEAD_RATIO;
479 }
480 if (arrowNibPerc < 0) {
481 arrowNibPerc = DEFAULT_ARROWHEAD_NIB_PERC;
482 }
483 }
484
485 // only calculate the arrowhead polygon if necessary
486 Polygon arrowhead = withArrow.getArrowhead();
487 if (arrowhead == null || disconnectMode) {
488 arrowhead = createArrowheadPolygon(x0,y0,arrowLength,arrowRatio,arrowNibPerc);
489
490 if (!disconnectMode)
491 withArrow.setArrowhead(arrowhead);
492 }
493 double rad = calcAngle((float) x0, (float) y0, (float) x1, (float) y1);
494 arrowhead.translate((x0 + length) - arrowhead.xpoints[0], y0
495 - arrowhead.ypoints[0]);
496 AffineTransform tx = AffineTransform.getRotateInstance(rad, x0, y0);
497
498
499 // TODO
500 // **** Need to find a more principled (and one-off) way to switch
501 // this on for 'Graphics' variables in Expeditee in general
502
503 // Switching on Aliasing on 'g' at this point a stop-gap for now
504 // ****
505
506 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
507
508 g.draw(tx.createTransformedShape((Shape) arrowhead));
509 g.fill(tx.createTransformedShape((Shape) arrowhead));
510
511 }
512
513 /**
514 *
515 * @param length
516 * The length of the line
517 * @return
518 */
519 private float getAutoArrowheadLength(float length) {
520 float arrowLength;
521 arrowLength = length / ARROW_HEAD_TO_LENGTH_RATIO;
522 arrowLength = Math.max(MINIMUM_ARROW_HEAD_LENGTH, arrowLength);
523 arrowLength = Math.min(MAXIMUM_ARROW_HEAD_LENGTH, arrowLength);
524 return arrowLength;
525 }
526
527 /**
528 * Calcuates the angle of rotation (in radians) of the arrowhead Based on
529 * code from DeSL (Arrow2D) http://sourceforge.net/projects/desl/
530 */
531 private double calcAngle(float x1, float y1, float x2, float y2) {
532 double rad = 0.0d;
533 float dx = x2 - x1;
534 float dy = y2 - y1;
535
536 if (dx == 0.0) {
537 if (dy == 0.0) {
538 rad = 0.0;
539 } else if (dy > 0.0) {
540 rad = Math.PI / 2.0;
541 } else {
542 rad = Math.PI * 3.0 / 2.0;
543 }
544 } else if (dy == 0.0) {
545 if (dx > 0.0) {
546 rad = 0.0;
547 } else {
548 rad = Math.PI;
549 }
550 } else {
551 if (dx < 0.0) {
552 rad = Math.atan(dy / dx) + Math.PI;
553 } else if (dy < 0.0) {
554 rad = Math.atan(dy / dx) + (2 * Math.PI);
555 } else {
556 rad = Math.atan(dy / dx);
557 }
558 }
559
560 return rad;
561 }
562
563 @Override
564 public Line copy() {
565 Item s = _start.copy();
566 Item e = _end.copy();
567
568 Line temp = new Line(s, e, getID());
569
570 Item.DuplicateItem(this, temp);
571
572 temp.setLinePattern(getLinePattern());
573 temp.setThickness(getThickness());
574
575 temp.setArrow(getArrowheadLength(), getArrowheadRatio(), getArrowheadNibPerc());
576
577 return temp;
578 }
579
580 public Item getEndPointToDisconnect(int x, int y) {
581 if (!hasPermission(UserAppliedPermission.full))
582 return null;
583
584 Item start = getStartItem();
585 Item end = getEndItem();
586 int deltaX = start.getX() - end.getX();
587 int deltaY = start.getY() - end.getY();
588 int lineLength = (int) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
589
590 int startX = start.getX() - x;
591 int endX = end.getX() - x;
592 int startY = start.getY() - y;
593 int endY = end.getY() - y;
594
595 int distStart = (int) Math.sqrt(startX * startX + startY * startY);
596 int distEnd = (int) Math.sqrt(endX * endX + endY * endY);
597
598 final double DISCONNECT_THRESHHOLD = Math.min(0.2 * lineLength, 25.0);
599 final double NORMAL_THRESHHOLD = Math.min(0.1 * lineLength, 13.0);
600
601 if (distStart < NORMAL_THRESHHOLD) {
602 start._mode = Item.HighlightMode.Normal;
603 return start;
604 } else if (distEnd < NORMAL_THRESHHOLD) {
605 end._mode = Item.HighlightMode.Normal;
606 return end;
607 } else if (distStart < DISCONNECT_THRESHHOLD) {
608 if (start.getLines().size() > 1
609 || start.getConstraints().size() > 0)
610 start._mode = Item.HighlightMode.Disconnect;
611 else
612 start._mode = Item.HighlightMode.Normal;
613 return start;
614 } else if (distEnd < DISCONNECT_THRESHHOLD) {
615 if (end.getLines().size() > 1 || end.getConstraints().size() > 0)
616 end._mode = Item.HighlightMode.Disconnect;
617 else
618 end._mode = Item.HighlightMode.Normal;
619 return end;
620 }
621
622 return null;
623 }
624
625 @Override
626 public int setHighlightColor() {
627 super.setHighlightColor();
628
629 return Item.UNCHANGED_CURSOR;
630 }
631
632 /**
633 * Sets the Color of this Line and the two Dots at the ends.
634 *
635 * @param c
636 * The Color to paint this Line in.
637 */
638 @Override
639 public void setColor(Color c) {
640 super.setColor(c);
641
642 _start.lineColorChanged(c);
643 _end.lineColorChanged(c);
644 }
645
646 @Override
647 public Color getFillColor() {
648 return _start.getFillColor();
649 }
650
651 @Override
652 public void setThickness(float thick, boolean setConnectedThickness) {
653
654 float oldThickness = this.getThickness();
655 if (thick == oldThickness)
656 return;
657 boolean bigger = thick > oldThickness;
658
659 if (!bigger)
660 invalidateCommonTrait(ItemAppearence.Thickness);
661
662 if (thick < 0)
663 thick = DEFAULT_THICKNESS;
664
665 if (setConnectedThickness) {
666 if (_start.getThickness() != thick)
667 _start.setThickness(thick);
668
669 if (_end.getThickness() != thick)
670 _end.setThickness(thick);
671 }
672
673 refreshStroke(thick);
674
675 if (bigger)
676 invalidateCommonTrait(ItemAppearence.Thickness);
677 }
678
679 @Override
680 public final float getThickness() {
681 /*
682 * Don't change this method of getting the thickness otherwise setting
683 * thickness for connected lines breaks. The method for setting
684 * thickness of connected lines is inefficient and should be changed at
685 * some stage!
686 */
687 return (_start.getThickness() + _end.getThickness()) / 2;
688 }
689
690 @Override
691 public void setAnnotation(boolean val) {
692 if (_end.isAnnotation() != val)
693 _end.setAnnotation(val);
694 else if (_start instanceof Text && _start.isAnnotation() != val)
695 _start.setAnnotation(val);
696 }
697
698 @Override
699 public boolean isAnnotation() {
700 return _start.isAnnotation() || _end.isAnnotation();
701 }
702
703 @Override
704 public boolean isConnectedToAnnotation() {
705 return _start.isConnectedToAnnotation()
706 || _end.isConnectedToAnnotation();
707 }
708
709 /**
710 * Fixes the length of the arrowhead on the end of the line.
711 */
712 public void fixArrowheadLength() {
713 if (_end.getArrowheadLength() == AUTO_ARROWHEAD_LENGTH) {
714 fixArrowheadLength(_end);
715 }
716 }
717
718 public void autoArrowheadLength() {
719 _end.setArrowheadLength(AUTO_ARROWHEAD_LENGTH);
720 }
721
722 public void fixArrowheadLength(Item arrow) {
723 // if the arrow isnt there turn it on!
724 int x0, x1, y0, y1;
725
726 x1 = arrow.getX();
727 y1 = arrow.getY();
728 x0 = getOppositeEnd(arrow).getX();
729 y0 = getOppositeEnd(arrow).getY();
730
731 int deltaX = x1 - x0;
732 int deltaY = y1 - y0;
733
734 int length = (int) Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
735 length /= ARROW_HEAD_TO_LENGTH_RATIO;
736 arrow.setArrowheadLength(Math.min(MAXIMUM_ARROW_HEAD_LENGTH, Math.max(
737 MINIMUM_ARROW_HEAD_LENGTH, length)));
738 }
739
740 /**
741 * This method toggles the arrow on the floating end point of a line. If the
742 * line does not have one floating end and one fixed end this method does
743 * nothing.
744 *
745 */
746 public void toggleArrow() {
747 Item arrow;
748 if (_start.isFloating() && !_end.isFloating())
749 arrow = _start;
750 else if (_end.isFloating() && !_start.isFloating())
751 arrow = _end;
752 else
753 return;
754
755 invalidateAll();
756 // if there already an arrow, turn it off
757 if (arrow.getArrowheadLength() != 0 && arrow.getArrowheadRatio() > 0) {
758 arrow.setArrowheadLength(0);
759 return;
760 }
761
762 fixArrowheadLength(arrow);
763 invalidateAll();
764 }
765
766 private static double[] ArrowHeadRatios = new double[] { 0.25, 0.5, 1.0 };
767
768 /**
769 * This will toggle the arrow head between the various types of UML arrow
770 * heads.
771 *
772 * @param amount
773 */
774 public void toggleArrowHeadRatio(int amount) {
775 Item arrow = null;
776 if (_start.isFloating() && !_end.isFloating())
777 arrow = _start;
778 else if (_end.isFloating() && !_start.isFloating())
779 arrow = _end;
780 else
781 return;
782
783 // find the index of the current line pattern
784 double currentRatio = arrow.getArrowheadRatio();
785
786 // Find the current pattern and move to the next pattern in the wheel
787 for (int i = 0; i < ArrowHeadRatios.length; i++) {
788 if (currentRatio - ArrowHeadRatios[i] < 0.001) {
789 i += ArrowHeadRatios.length + amount;
790 i %= ArrowHeadRatios.length;
791 arrow.setArrowheadRatio(ArrowHeadRatios[i]);
792 return;
793 }
794 }
795 }
796
797 private static int[] ArrowHeadLength = new int[] { /* AUTO_ARROWHEAD_LENGTH, */
798 0, 20, 40, MAXIMUM_ARROW_HEAD_LENGTH };
799
800 /**
801 * Changes the length of the arrow head. In the future this may toggle the
802 * arrow head between the various types of UML arrow heads.
803 *
804 * @param amount
805 */
806 public void toggleArrowHeadLength(int amount) {
807 Item arrow = null;
808 if (_start.isFloating() && !_end.isFloating())
809 arrow = _start;
810 else if (_end.isFloating() && !_start.isFloating())
811 arrow = _end;
812 else
813 return;
814
815 // find the index of the current line pattern
816 float currentLength = arrow.getArrowheadLength();
817
818 // Find the current pattern and move to the next pattern in the wheel
819 for (int i = 0; i < ArrowHeadLength.length; i++) {
820 if (currentLength <= ArrowHeadLength[i]) {
821 i += ArrowHeadLength.length + amount;
822 i %= ArrowHeadLength.length;
823 arrow.setArrowheadLength(ArrowHeadLength[i]);
824 return;
825 }
826 }
827 }
828
829 private Item _virtualSpot = null;
830
831 public void showVirtualSpot(Item orig, int mouseX, int mouseY) {
832 if (orig.getLines().size() != 1)
833 return;
834
835 // lines that are in 'connected' mode, also cannot be attached
836 if (orig.getLines().get(0).getOppositeEnd(orig).isFloating())
837 return;
838
839 Item spot = new Dot(-1, orig.getX(), orig.getY());
840 Item.DuplicateItem(orig, spot);
841 spot.setThickness(Math.max(this.getThickness(), 5));
842 if (this.getColor() != Color.RED)
843 spot.setColor(Color.RED);
844 else
845 spot.setColor(Color.BLUE);
846
847 // unhighlight all the dots
848 for (Item conn : getAllConnected()) {
849 conn.setHighlightMode(Item.HighlightMode.None);
850 }
851
852 // calculate nearest point on line from spot
853 double slope1;
854 double slope2;
855 double x, y;
856
857 slope1 = (_start.getY() - _end.getY() * 1.0)
858 / (_start.getX() - _end.getX());
859 slope2 = -1 / slope1;
860
861 // if the line is horizontal
862 if (slope1 == 0) {
863 x = spot.getX();
864 y = _start.getY();
865 // if the line is vertical
866 } else if (slope2 == 0) {
867 x = _start.getX();
868 y = spot.getY();
869 // otherwise, the line is sloped
870 } else {
871 x = (-1 * slope2 * spot.getX() + spot.getY() - _start.getY() + slope1
872 * _start.getX())
873 / (slope1 - slope2);
874 y = slope1 * (x - _start.getX()) + _start.getY();
875 }
876
877 // position spot on the line
878 spot.setPosition((int) x, (int) y);
879 _virtualSpot = spot;
880 invalidateVirtualSpot();
881 }
882
883 public void showVirtualSpot(int mouseX, int mouseY) {
884 Item spot = new Dot(mouseX, mouseY, -1);
885 spot.setThickness(Math.max(this.getThickness(), 5));
886 if (DEFAULT_HIGHLIGHT.equals(this.getColor()))
887 spot.setColor(ALTERNATE_HIGHLIGHT);
888 else
889 spot.setColor(DEFAULT_HIGHLIGHT);
890
891 // calculate nearest point on line from spot
892 double slope1;
893 double slope2;
894 double x, y;
895
896 slope1 = (_start.getY() - _end.getY() * 1.0)
897 / (_start.getX() - _end.getX());
898 slope2 = -1 / slope1;
899
900 // if the line is horizontal
901 if (slope1 == 0) {
902 x = spot.getX();
903 y = _start.getY();
904 // if the line is vertical
905 } else if (slope2 == 0) {
906 x = _start.getX();
907 y = spot.getY();
908 // otherwise, the line is sloped
909 } else {
910 x = (-1 * slope2 * spot.getX() + spot.getY() - _start.getY() + slope1
911 * _start.getX())
912 / (slope1 - slope2);
913 y = slope1 * (x - _start.getX()) + _start.getY();
914 }
915
916 // position spot on the line
917 spot.setPosition((int) x, (int) y);
918 _virtualSpot = spot;
919 invalidateVirtualSpot();
920 }
921
922 public Item forceMerge(Item spot, int mouseX, int mouseY) {
923
924 // calculate nearest point on line from spot
925 double slope1;
926 double slope2;
927 double x, y;
928
929 slope1 = (_start.getY() - _end.getY() * 1.0)
930 / (_start.getX() - _end.getX());
931 slope2 = -1 / slope1;
932
933 // if the line is horizontal
934 if (slope1 == 0) {
935 x = spot.getX();
936 y = _start.getY();
937 // if the line is vertical
938 } else if (slope2 == 0) {
939 x = _start.getX();
940 y = spot.getY();
941 // otherwise, the line is sloped
942 } else {
943 x = (-1 * slope2 * spot.getX() + spot.getY() - _start.getY() + slope1
944 * _start.getX())
945 / (slope1 - slope2);
946 y = slope1 * (x - _start.getX()) + _start.getY();
947 }
948
949 // position spot on the line
950 spot.setPosition((int) x, (int) y);
951
952 // Keep constraints
953 // If its a constrained line dont merge
954 for (Constraint c : _end.getConstraints()) {
955 if (c.getOppositeEnd(_end).equals(_start)) {
956 // c.replaceEnd(_start, spot);
957 new Constraint(spot, _start, getParentOrCurrentFrame()
958 .getNextItemID(), c.getType());
959 new Constraint(spot, _end, getParentOrCurrentFrame()
960 .getNextItemID(), c.getType());
961 return null;
962 }
963 }
964
965 Line temp = copy();
966
967 Frame currentFrame = DisplayIO.getCurrentFrame();
968 temp.setID(currentFrame.getNextItemID());
969 temp.setEndItem(_end);
970 temp.setStartItem(spot);
971 currentFrame.addItem(temp);
972
973 setEndItem(spot);
974
975 // if(_arrowEnd == spot)
976 // setArrow(-1,-1);
977
978 return spot;
979 }
980
981 @Override
982 public Item merge(Item merger, int mouseX, int mouseY) {
983
984 if (!(merger.isLineEnd()))
985 return merger;
986
987 Item spot = merger;
988
989 // dots with multiple lines cannot be attached, nor can dots with no
990 // lines
991 if (spot.getLines().size() != 1)
992 return merger;
993
994 // lines that are in 'connected' mode, also cannot be attached
995 if (spot.isFloating()
996 && spot.getLines().get(0).getOppositeEnd(spot).isFloating())
997 return merger;
998
999 return forceMerge(merger, mouseX, mouseY);
1000 }
1001
1002 @Override
1003 public Collection<Item> getConnected() {
1004 return getAllConnected();
1005 }
1006
1007 @Override
1008 public void addAllConnected(Collection<Item> connected) {
1009 if (!connected.contains(this))
1010 connected.add(this);
1011
1012 if (!connected.contains(_end))
1013 _end.addAllConnected(connected);
1014
1015 if (!connected.contains(_start))
1016 _start.addAllConnected(connected);
1017 }
1018
1019 @Override
1020 public boolean equals(Object o) {
1021 if (!(o instanceof Line))
1022 return false;
1023
1024 return ((Item) o).getID() == getID();
1025 }
1026
1027 @Override
1028 public void setActions(List<String> actions) {
1029 }
1030
1031 @Override
1032 public List<String> getAction() {
1033 return null;
1034 }
1035
1036 public String getLineEnds() {
1037 return _start.getID() + " " + _end.getID();
1038 }
1039
1040 public int getLineType() {
1041 return 1;
1042 }
1043
1044 @Override
1045 public boolean isFloating() {
1046 return _start.isFloating() && _end.isFloating();
1047 }
1048
1049 @Override
1050 public void updatePolygon() {
1051 _poly = new Polygon();
1052
1053 Rectangle one;
1054 Rectangle two;
1055
1056 /**
1057 * TODO: Fake dots seems inefficient, Fix it.
1058 */
1059
1060 // line goes from left->right, one->two
1061 if (_start.getX() <= _end.getX()) {
1062 /*
1063 * one = _start.getPolygon().getBounds(); two =
1064 * _end.getPolygon().getBounds();
1065 */
1066
1067 Item s = new Dot(_start.getX(), _start.getY(), -1);
1068 Item e = new Dot(_end.getX(), _end.getY(), -1);
1069
1070 one = s.getPolygon().getBounds();
1071 two = e.getPolygon().getBounds();
1072 } else {
1073 Item s = new Dot(_start.getX(), _start.getY(), -1);
1074 Item e = new Dot(_end.getX(), _end.getY(), -1);
1075
1076 one = e.getPolygon().getBounds();
1077 two = s.getPolygon().getBounds();
1078 /*
1079 * one = _end.getPolygon().getBounds(); two =
1080 * _start.getPolygon().getBounds();
1081 */
1082 }
1083
1084 // if one is above two
1085 if (one.getY() < two.getY()) {
1086 _poly.addPoint((int) one.getMaxX(), (int) one.getMinY());
1087 _poly.addPoint((int) two.getMaxX(), (int) two.getMinY());
1088 _poly.addPoint((int) two.getMinX(), (int) two.getMaxY());
1089 _poly.addPoint((int) one.getMinX(), (int) one.getMaxY());
1090 // if one is below two
1091 } else {
1092 _poly.addPoint((int) one.getMinX(), (int) one.getMinY());
1093 _poly.addPoint((int) two.getMinX(), (int) two.getMinY());
1094 _poly.addPoint((int) two.getMaxX(), (int) two.getMaxY());
1095 _poly.addPoint((int) one.getMaxX(), (int) one.getMaxY());
1096 }
1097
1098 }
1099
1100 @Override
1101 public void delete() {
1102 super.delete();
1103
1104 _start.removeLine(this);
1105 _end.removeLine(this);
1106
1107 //_start.invalidateAll();
1108 //_end.invalidateAll();
1109
1110 if (_start.getLines().size() == 0)
1111 _start.delete();
1112 if (_end.getLines().size() == 0)
1113 _end.delete();
1114 }
1115
1116 @Override
1117 public void anchor() {
1118 Frame current = getParentOrCurrentFrame();
1119 // Check if either end is off the current frame
1120 Frame parentEnd = getEndItem().getParent();
1121 if (parentEnd != null && parentEnd != current) {
1122 this.setParent(parentEnd);
1123 } else {
1124 Frame parentStart = getStartItem().getParent();
1125 if (parentStart != null && parentStart != current)
1126 this.setParent(parentStart);
1127 }
1128
1129 super.anchor();
1130 fixArrowheadLength();
1131 }
1132
1133 /**
1134 * Removes constraints from the end points of this line.
1135 */
1136 @Override
1137 public void removeAllConstraints() {
1138 // Find all constraints that include both the start and end.
1139 for (Constraint c : _start.getConstraints()) {
1140 if (c.contains(_end)) {
1141 _start.removeConstraint(c);
1142 _end.removeConstraint(c);
1143 }
1144 }
1145 }
1146
1147 public double getLength() {
1148 return getLength(_start.getPosition(), _end.getPosition());
1149 }
1150
1151 public Integer getPossibleConstraint() {
1152 if (_start.getY() == _end.getY() && _start.getX() != _end.getX())
1153 return Constraint.HORIZONTAL;
1154 else if (_start.getX() == _end.getX() && _start.getY() != _end.getY())
1155 return Constraint.VERTICAL;
1156 return null;
1157 }
1158
1159 public static double getLength(Point p1, Point p2) {
1160 return Point.distance(p1.x, p1.y, p2.x, p2.y);
1161 }
1162
1163 @Override
1164 public Rectangle[] getDrawingArea() { // TODO: CACHE - LIKE UPDATE POLYGON
1165
1166 float currentThickness = this.getThickness() + 4;
1167
1168 // Establish bounds
1169 int x = Math.min(_start.getX(), _end.getX());
1170 int y = Math.min(_start.getY(), _end.getY());
1171 int w = Math.max(_start.getX(), _end.getX()) - x + 1;
1172 int h = Math.max(_start.getY(), _end.getY()) - y + 1;
1173
1174 int thickness = (int) Math.ceil(currentThickness);
1175 if (thickness < 4)
1176 thickness = 4;
1177 int halfThickness = (int) Math.ceil(currentThickness / 2);
1178
1179 // Consider line thickness
1180 w += thickness;
1181 h += thickness;
1182 x -= halfThickness;
1183 y -= halfThickness;
1184
1185 Rectangle bounds = new Rectangle(x, y, w, h);
1186
1187 // TODO: Cap bounds
1188
1189 if (_end.hasVisibleArrow()) {
1190
1191 double arrowLength = _end.getArrowheadLength();
1192
1193 if (arrowLength == AUTO_ARROWHEAD_LENGTH) {
1194 arrowLength = Math
1195 .ceil(getAutoArrowheadLength((float) getLength()));
1196 } else {
1197 arrowLength = Math.ceil(arrowLength);
1198 }
1199
1200 arrowLength += ((thickness * 2) + 1);
1201 int nArrowLength = (int) (arrowLength + 1);
1202
1203 /* NAIVE version - but probably more efficient in the long run...
1204 overall as opposed to advanced/expensive calculations for getting exact
1205 bounding box */
1206 Rectangle arrowBounds = new Rectangle(_end.getX() - nArrowLength,
1207 _end.getY() - nArrowLength, 2 * nArrowLength,
1208 2 * nArrowLength);
1209
1210 if (currentThickness > 0.0f) {
1211
1212 arrowBounds = new Rectangle(arrowBounds.x - halfThickness,
1213 arrowBounds.y - halfThickness, arrowBounds.width
1214 + thickness, arrowBounds.height + thickness);
1215 }
1216
1217 return new Rectangle[] { bounds, arrowBounds };
1218 } else {
1219 return new Rectangle[] { bounds };
1220 }
1221
1222 }
1223
1224 @Override
1225 public boolean hasPermission(UserAppliedPermission p) {
1226 return _start.hasPermission(p);
1227 }
1228
1229 @Override
1230 public void setPermission(UserAppliedPermission p) {
1231 _start.setPermission(p);
1232 _end.setPermission(p);
1233 }
1234
1235 @Override
1236 public boolean dontSave() {
1237 return true;
1238 }
1239
1240 @Override
1241 public void scale(Float scale, int originX, int originY) {
1242 }
1243}
Note: See TracBrowser for help on using the repository browser.