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

Last change on this file since 917 was 917, checked in by jts21, 10 years ago

Changed Line.getEndPointToDisconnect to improve line disconnection behaviour

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