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

Last change on this file since 558 was 558, checked in by jts21, 11 years ago

Allow completely invisible rectangles for use with web page importing (Apparently transparent rectangles are to be used as divs and having borders would make them ugly). Adding an alpha value to color settings for all Expeditee items was also explored, however it seemed to cause significant lag when redrawing a translucent/transparent item (dragging an item over a transparent rectangle caused noticable stuttering), so I removed it

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 // enable invisible shapes (for web browser divs)
353 // && _start.getFillColor() != null;
354 }
355
356 protected int[][][] getPaths() {
357 List<List<Point>> pointPaths = new LinkedList<List<Point>>();
358 Collection<Line> visited = new HashSet<Line>();
359 LinkedList<Line> toExplore = new LinkedList<Line>();
360 toExplore.add(this);
361 while (toExplore.size() > 0) {
362 Line nextLine = toExplore.remove(0);
363 // Start at the item we have already explored... unless both have
364 // been explored
365 if (!visited.contains(nextLine)) {
366 pointPaths.add(nextLine.getPath(visited, toExplore));
367 }
368 }
369 // Put the paths into int arrays
370 int[][][] paths = new int[pointPaths.size()][][];
371 Iterator<List<Point>> iter = pointPaths.iterator();
372
373 for (int i = 0; i < paths.length; i++) {
374 List<Point> pointPath = iter.next();
375 int[][] path = new int[2][pointPath.size()];
376 paths[i] = path;
377 // Add all the x and y's to the array
378 for (int j = 0; j < path[0].length; j++) {
379 path[0][j] = pointPath.get(j).x;
380 path[1][j] = pointPath.get(j).y;
381 }
382 }
383 return paths;
384 }
385
386 protected List<Point> getPath(Collection<Line> visited,
387 LinkedList<Line> toExplore) {
388 LinkedList<Point> points = new LinkedList<Point>();
389 // put the start item points into our list
390 Item start = getStartItem();
391 Item end = getEndItem();
392 visited.add(this);
393
394 start.appendPath(visited, points, true, toExplore);
395 end.appendPath(visited, points, false, toExplore);
396 return points;
397 }
398
399 public void paintArrows(Graphics2D g) {
400 if (dontPaint())
401 return;
402
403 g.setColor(getPaintColor());
404 g.setStroke(new BasicStroke(getPaintThickness(), CAP,
405 BasicStroke.JOIN_MITER));
406 paintArrow(g, _start, _startOffset, _endOffset);
407 paintArrow(g, _end, _endOffset, _startOffset);
408
409 if (_virtualSpot != null) {
410 _virtualSpot.paint(g);
411 invalidateVirtualSpot();
412 _virtualSpot = null;
413 }
414 }
415
416 private float getPaintThickness() {
417 float thickness = getThickness();
418 if (thickness > 0) {
419 return thickness;
420 }
421 return MINIMUM_PAINT_THICKNESS;
422 }
423
424 private void invalidateVirtualSpot() {
425 assert (_virtualSpot != null);
426
427 invalidate(_virtualSpot.getDrawingArea());
428
429 }
430
431 /**
432 * Based on code from DeSL (Arrow2D) http://sourceforge.net/projects/desl/
433 */
434 private void paintArrow(Graphics2D g, Item withArrow, Point startOffset,
435 Point endOffset) {
436 boolean disconnectMode = withArrow._mode == Item.HighlightMode.Disconnect;
437 // only draw an arrowhead if necessary
438 if (!(this._mode == Item.HighlightMode.Disconnect && disconnectMode)
439 && (!withArrow.hasVisibleArrow() || withArrow.getLines().size() > 1))
440 return;
441
442 int x0, x1, y0, y1;
443
444 x1 = withArrow.getX() + startOffset.x;
445 y1 = withArrow.getY() + startOffset.y;
446 x0 = getOppositeEnd(withArrow).getX() + endOffset.x;
447 y0 = getOppositeEnd(withArrow).getY() + endOffset.y;
448
449 float arrowLength = withArrow.getArrowheadLength();
450 double arrowRatio = withArrow.getArrowheadRatio();
451 double arrowNibPerc = withArrow.getArrowheadNibPerc();
452
453 // set the size of the disconnect indicator arrowhead
454 if (this._mode == Item.HighlightMode.Disconnect) {
455 arrowLength = 15;
456 arrowRatio = 0.3;
457 arrowNibPerc = 0.5;
458 }
459
460 // if the arrowhead is 'auto', then one and only one end must be
461 // floating
462 if (arrowLength != AUTO_ARROWHEAD_LENGTH && (arrowRatio < 0 || arrowNibPerc < 0)) {
463 return;
464 }
465
466 int deltaX = x1 - x0;
467 int deltaY = y1 - y0;
468
469 int length = (int) Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
470
471 // The length of the line must at least be as long as the arrow or we
472 // wont show the arrow
473 if (length <= MINIMUM_ARROW_HEAD_LENGTH)
474 return;
475 if (arrowLength == AUTO_ARROWHEAD_LENGTH) {
476 arrowLength = getAutoArrowheadLength(length);
477 withArrow.setArrowhead(null);
478 if (arrowRatio < 0) {
479 arrowRatio = DEFAULT_ARROWHEAD_RATIO;
480 }
481 if (arrowNibPerc < 0) {
482 arrowNibPerc = DEFAULT_ARROWHEAD_NIB_PERC;
483 }
484 }
485
486 // only calculate the arrowhead polygon if necessary
487 Polygon arrowhead = withArrow.getArrowhead();
488 if (arrowhead == null || disconnectMode) {
489 arrowhead = createArrowheadPolygon(x0,y0,arrowLength,arrowRatio,arrowNibPerc);
490
491 if (!disconnectMode)
492 withArrow.setArrowhead(arrowhead);
493 }
494 double rad = calcAngle((float) x0, (float) y0, (float) x1, (float) y1);
495 arrowhead.translate((x0 + length) - arrowhead.xpoints[0], y0
496 - arrowhead.ypoints[0]);
497 AffineTransform tx = AffineTransform.getRotateInstance(rad, x0, y0);
498
499
500 // TODO
501 // **** Need to find a more principled (and one-off) way to switch
502 // this on for 'Graphics' variables in Expeditee in general
503
504 // Switching on Aliasing on 'g' at this point a stop-gap for now
505 // ****
506
507 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
508
509 g.draw(tx.createTransformedShape((Shape) arrowhead));
510 g.fill(tx.createTransformedShape((Shape) arrowhead));
511
512 }
513
514 /**
515 *
516 * @param length
517 * The length of the line
518 * @return
519 */
520 private float getAutoArrowheadLength(float length) {
521 float arrowLength;
522 arrowLength = length / ARROW_HEAD_TO_LENGTH_RATIO;
523 arrowLength = Math.max(MINIMUM_ARROW_HEAD_LENGTH, arrowLength);
524 arrowLength = Math.min(MAXIMUM_ARROW_HEAD_LENGTH, arrowLength);
525 return arrowLength;
526 }
527
528 /**
529 * Calcuates the angle of rotation (in radians) of the arrowhead Based on
530 * code from DeSL (Arrow2D) http://sourceforge.net/projects/desl/
531 */
532 private double calcAngle(float x1, float y1, float x2, float y2) {
533 double rad = 0.0d;
534 float dx = x2 - x1;
535 float dy = y2 - y1;
536
537 if (dx == 0.0) {
538 if (dy == 0.0) {
539 rad = 0.0;
540 } else if (dy > 0.0) {
541 rad = Math.PI / 2.0;
542 } else {
543 rad = Math.PI * 3.0 / 2.0;
544 }
545 } else if (dy == 0.0) {
546 if (dx > 0.0) {
547 rad = 0.0;
548 } else {
549 rad = Math.PI;
550 }
551 } else {
552 if (dx < 0.0) {
553 rad = Math.atan(dy / dx) + Math.PI;
554 } else if (dy < 0.0) {
555 rad = Math.atan(dy / dx) + (2 * Math.PI);
556 } else {
557 rad = Math.atan(dy / dx);
558 }
559 }
560
561 return rad;
562 }
563
564 @Override
565 public Line copy() {
566 Item s = _start.copy();
567 Item e = _end.copy();
568
569 Line temp = new Line(s, e, getID());
570
571 Item.DuplicateItem(this, temp);
572
573 temp.setLinePattern(getLinePattern());
574 temp.setThickness(getThickness());
575
576 temp.setArrow(getArrowheadLength(), getArrowheadRatio(), getArrowheadNibPerc());
577
578 return temp;
579 }
580
581 public Item getEndPointToDisconnect(int x, int y) {
582 if (!hasPermission(UserAppliedPermission.full))
583 return null;
584
585 Item start = getStartItem();
586 Item end = getEndItem();
587 int deltaX = start.getX() - end.getX();
588 int deltaY = start.getY() - end.getY();
589 int lineLength = (int) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
590
591 int startX = start.getX() - x;
592 int endX = end.getX() - x;
593 int startY = start.getY() - y;
594 int endY = end.getY() - y;
595
596 int distStart = (int) Math.sqrt(startX * startX + startY * startY);
597 int distEnd = (int) Math.sqrt(endX * endX + endY * endY);
598
599 final double DISCONNECT_THRESHHOLD = Math.min(0.2 * lineLength, 25.0);
600 final double NORMAL_THRESHHOLD = Math.min(0.1 * lineLength, 13.0);
601
602 if (distStart < NORMAL_THRESHHOLD) {
603 start._mode = Item.HighlightMode.Normal;
604 return start;
605 } else if (distEnd < NORMAL_THRESHHOLD) {
606 end._mode = Item.HighlightMode.Normal;
607 return end;
608 } else if (distStart < DISCONNECT_THRESHHOLD) {
609 if (start.getLines().size() > 1
610 || start.getConstraints().size() > 0)
611 start._mode = Item.HighlightMode.Disconnect;
612 else
613 start._mode = Item.HighlightMode.Normal;
614 return start;
615 } else if (distEnd < DISCONNECT_THRESHHOLD) {
616 if (end.getLines().size() > 1 || end.getConstraints().size() > 0)
617 end._mode = Item.HighlightMode.Disconnect;
618 else
619 end._mode = Item.HighlightMode.Normal;
620 return end;
621 }
622
623 return null;
624 }
625
626 @Override
627 public int setHighlightColor() {
628 super.setHighlightColor();
629
630 return Item.UNCHANGED_CURSOR;
631 }
632
633 /**
634 * Sets the Color of this Line and the two Dots at the ends.
635 *
636 * @param c
637 * The Color to paint this Line in.
638 */
639 @Override
640 public void setColor(Color c) {
641 super.setColor(c);
642
643 _start.lineColorChanged(c);
644 _end.lineColorChanged(c);
645 }
646
647 @Override
648 public Color getFillColor() {
649 return _start.getFillColor();
650 }
651
652 @Override
653 public void setThickness(float thick, boolean setConnectedThickness) {
654
655 float oldThickness = this.getThickness();
656 if (thick == oldThickness)
657 return;
658 boolean bigger = thick > oldThickness;
659
660 if (!bigger)
661 invalidateCommonTrait(ItemAppearence.Thickness);
662
663 if (thick < 0)
664 thick = DEFAULT_THICKNESS;
665
666 if (setConnectedThickness) {
667 if (_start.getThickness() != thick)
668 _start.setThickness(thick);
669
670 if (_end.getThickness() != thick)
671 _end.setThickness(thick);
672 }
673
674 refreshStroke(thick);
675
676 if (bigger)
677 invalidateCommonTrait(ItemAppearence.Thickness);
678 }
679
680 @Override
681 public final float getThickness() {
682 /*
683 * Don't change this method of getting the thickness otherwise setting
684 * thickness for connected lines breaks. The method for setting
685 * thickness of connected lines is inefficient and should be changed at
686 * some stage!
687 */
688 return (_start.getThickness() + _end.getThickness()) / 2;
689 }
690
691 @Override
692 public void setAnnotation(boolean val) {
693 if (_end.isAnnotation() != val)
694 _end.setAnnotation(val);
695 else if (_start instanceof Text && _start.isAnnotation() != val)
696 _start.setAnnotation(val);
697 }
698
699 @Override
700 public boolean isAnnotation() {
701 return _start.isAnnotation() || _end.isAnnotation();
702 }
703
704 @Override
705 public boolean isConnectedToAnnotation() {
706 return _start.isConnectedToAnnotation()
707 || _end.isConnectedToAnnotation();
708 }
709
710 /**
711 * Fixes the length of the arrowhead on the end of the line.
712 */
713 public void fixArrowheadLength() {
714 if (_end.getArrowheadLength() == AUTO_ARROWHEAD_LENGTH) {
715 fixArrowheadLength(_end);
716 }
717 }
718
719 public void autoArrowheadLength() {
720 _end.setArrowheadLength(AUTO_ARROWHEAD_LENGTH);
721 }
722
723 public void fixArrowheadLength(Item arrow) {
724 // if the arrow isnt there turn it on!
725 int x0, x1, y0, y1;
726
727 x1 = arrow.getX();
728 y1 = arrow.getY();
729 x0 = getOppositeEnd(arrow).getX();
730 y0 = getOppositeEnd(arrow).getY();
731
732 int deltaX = x1 - x0;
733 int deltaY = y1 - y0;
734
735 int length = (int) Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
736 length /= ARROW_HEAD_TO_LENGTH_RATIO;
737 arrow.setArrowheadLength(Math.min(MAXIMUM_ARROW_HEAD_LENGTH, Math.max(
738 MINIMUM_ARROW_HEAD_LENGTH, length)));
739 }
740
741 /**
742 * This method toggles the arrow on the floating end point of a line. If the
743 * line does not have one floating end and one fixed end this method does
744 * nothing.
745 *
746 */
747 public void toggleArrow() {
748 Item arrow;
749 if (_start.isFloating() && !_end.isFloating())
750 arrow = _start;
751 else if (_end.isFloating() && !_start.isFloating())
752 arrow = _end;
753 else
754 return;
755
756 invalidateAll();
757 // if there already an arrow, turn it off
758 if (arrow.getArrowheadLength() != 0 && arrow.getArrowheadRatio() > 0) {
759 arrow.setArrowheadLength(0);
760 return;
761 }
762
763 fixArrowheadLength(arrow);
764 invalidateAll();
765 }
766
767 private static double[] ArrowHeadRatios = new double[] { 0.25, 0.5, 1.0 };
768
769 /**
770 * This will toggle the arrow head between the various types of UML arrow
771 * heads.
772 *
773 * @param amount
774 */
775 public void toggleArrowHeadRatio(int amount) {
776 Item arrow = null;
777 if (_start.isFloating() && !_end.isFloating())
778 arrow = _start;
779 else if (_end.isFloating() && !_start.isFloating())
780 arrow = _end;
781 else
782 return;
783
784 // find the index of the current line pattern
785 double currentRatio = arrow.getArrowheadRatio();
786
787 // Find the current pattern and move to the next pattern in the wheel
788 for (int i = 0; i < ArrowHeadRatios.length; i++) {
789 if (currentRatio - ArrowHeadRatios[i] < 0.001) {
790 i += ArrowHeadRatios.length + amount;
791 i %= ArrowHeadRatios.length;
792 arrow.setArrowheadRatio(ArrowHeadRatios[i]);
793 return;
794 }
795 }
796 }
797
798 private static int[] ArrowHeadLength = new int[] { /* AUTO_ARROWHEAD_LENGTH, */
799 0, 20, 40, MAXIMUM_ARROW_HEAD_LENGTH };
800
801 /**
802 * Changes the length of the arrow head. In the future this may toggle the
803 * arrow head between the various types of UML arrow heads.
804 *
805 * @param amount
806 */
807 public void toggleArrowHeadLength(int amount) {
808 Item arrow = null;
809 if (_start.isFloating() && !_end.isFloating())
810 arrow = _start;
811 else if (_end.isFloating() && !_start.isFloating())
812 arrow = _end;
813 else
814 return;
815
816 // find the index of the current line pattern
817 float currentLength = arrow.getArrowheadLength();
818
819 // Find the current pattern and move to the next pattern in the wheel
820 for (int i = 0; i < ArrowHeadLength.length; i++) {
821 if (currentLength <= ArrowHeadLength[i]) {
822 i += ArrowHeadLength.length + amount;
823 i %= ArrowHeadLength.length;
824 arrow.setArrowheadLength(ArrowHeadLength[i]);
825 return;
826 }
827 }
828 }
829
830 private Item _virtualSpot = null;
831
832 public void showVirtualSpot(Item orig, int mouseX, int mouseY) {
833 if (orig.getLines().size() != 1)
834 return;
835
836 // lines that are in 'connected' mode, also cannot be attached
837 if (orig.getLines().get(0).getOppositeEnd(orig).isFloating())
838 return;
839
840 Item spot = new Dot(-1, orig.getX(), orig.getY());
841 Item.DuplicateItem(orig, spot);
842 spot.setThickness(Math.max(this.getThickness(), 5));
843 if (this.getColor() != Color.RED)
844 spot.setColor(Color.RED);
845 else
846 spot.setColor(Color.BLUE);
847
848 // unhighlight all the dots
849 for (Item conn : getAllConnected()) {
850 conn.setHighlightMode(Item.HighlightMode.None);
851 }
852
853 // calculate nearest point on line from spot
854 double slope1;
855 double slope2;
856 double x, y;
857
858 slope1 = (_start.getY() - _end.getY() * 1.0)
859 / (_start.getX() - _end.getX());
860 slope2 = -1 / slope1;
861
862 // if the line is horizontal
863 if (slope1 == 0) {
864 x = spot.getX();
865 y = _start.getY();
866 // if the line is vertical
867 } else if (slope2 == 0) {
868 x = _start.getX();
869 y = spot.getY();
870 // otherwise, the line is sloped
871 } else {
872 x = (-1 * slope2 * spot.getX() + spot.getY() - _start.getY() + slope1
873 * _start.getX())
874 / (slope1 - slope2);
875 y = slope1 * (x - _start.getX()) + _start.getY();
876 }
877
878 // position spot on the line
879 spot.setPosition((int) x, (int) y);
880 _virtualSpot = spot;
881 invalidateVirtualSpot();
882 }
883
884 public void showVirtualSpot(int mouseX, int mouseY) {
885 Item spot = new Dot(mouseX, mouseY, -1);
886 spot.setThickness(Math.max(this.getThickness(), 5));
887 if (DEFAULT_HIGHLIGHT.equals(this.getColor()))
888 spot.setColor(ALTERNATE_HIGHLIGHT);
889 else
890 spot.setColor(DEFAULT_HIGHLIGHT);
891
892 // calculate nearest point on line from spot
893 double slope1;
894 double slope2;
895 double x, y;
896
897 slope1 = (_start.getY() - _end.getY() * 1.0)
898 / (_start.getX() - _end.getX());
899 slope2 = -1 / slope1;
900
901 // if the line is horizontal
902 if (slope1 == 0) {
903 x = spot.getX();
904 y = _start.getY();
905 // if the line is vertical
906 } else if (slope2 == 0) {
907 x = _start.getX();
908 y = spot.getY();
909 // otherwise, the line is sloped
910 } else {
911 x = (-1 * slope2 * spot.getX() + spot.getY() - _start.getY() + slope1
912 * _start.getX())
913 / (slope1 - slope2);
914 y = slope1 * (x - _start.getX()) + _start.getY();
915 }
916
917 // position spot on the line
918 spot.setPosition((int) x, (int) y);
919 _virtualSpot = spot;
920 invalidateVirtualSpot();
921 }
922
923 public Item forceMerge(Item spot, int mouseX, int mouseY) {
924
925 // calculate nearest point on line from spot
926 double slope1;
927 double slope2;
928 double x, y;
929
930 slope1 = (_start.getY() - _end.getY() * 1.0)
931 / (_start.getX() - _end.getX());
932 slope2 = -1 / slope1;
933
934 // if the line is horizontal
935 if (slope1 == 0) {
936 x = spot.getX();
937 y = _start.getY();
938 // if the line is vertical
939 } else if (slope2 == 0) {
940 x = _start.getX();
941 y = spot.getY();
942 // otherwise, the line is sloped
943 } else {
944 x = (-1 * slope2 * spot.getX() + spot.getY() - _start.getY() + slope1
945 * _start.getX())
946 / (slope1 - slope2);
947 y = slope1 * (x - _start.getX()) + _start.getY();
948 }
949
950 // position spot on the line
951 spot.setPosition((int) x, (int) y);
952
953 // Keep constraints
954 // If its a constrained line dont merge
955 for (Constraint c : _end.getConstraints()) {
956 if (c.getOppositeEnd(_end).equals(_start)) {
957 // c.replaceEnd(_start, spot);
958 new Constraint(spot, _start, getParentOrCurrentFrame()
959 .getNextItemID(), c.getType());
960 new Constraint(spot, _end, getParentOrCurrentFrame()
961 .getNextItemID(), c.getType());
962 return null;
963 }
964 }
965
966 Line temp = copy();
967
968 Frame currentFrame = DisplayIO.getCurrentFrame();
969 temp.setID(currentFrame.getNextItemID());
970 temp.setEndItem(_end);
971 temp.setStartItem(spot);
972 currentFrame.addItem(temp);
973
974 setEndItem(spot);
975
976 // if(_arrowEnd == spot)
977 // setArrow(-1,-1);
978
979 return spot;
980 }
981
982 @Override
983 public Item merge(Item merger, int mouseX, int mouseY) {
984
985 if (!(merger.isLineEnd()))
986 return merger;
987
988 Item spot = merger;
989
990 // dots with multiple lines cannot be attached, nor can dots with no
991 // lines
992 if (spot.getLines().size() != 1)
993 return merger;
994
995 // lines that are in 'connected' mode, also cannot be attached
996 if (spot.isFloating()
997 && spot.getLines().get(0).getOppositeEnd(spot).isFloating())
998 return merger;
999
1000 return forceMerge(merger, mouseX, mouseY);
1001 }
1002
1003 @Override
1004 public Collection<Item> getConnected() {
1005 return getAllConnected();
1006 }
1007
1008 @Override
1009 public void addAllConnected(Collection<Item> connected) {
1010 if (!connected.contains(this))
1011 connected.add(this);
1012
1013 if (!connected.contains(_end))
1014 _end.addAllConnected(connected);
1015
1016 if (!connected.contains(_start))
1017 _start.addAllConnected(connected);
1018 }
1019
1020 @Override
1021 public boolean equals(Object o) {
1022 if (!(o instanceof Line))
1023 return false;
1024
1025 return ((Item) o).getID() == getID();
1026 }
1027
1028 @Override
1029 public void setActions(List<String> actions) {
1030 }
1031
1032 @Override
1033 public List<String> getAction() {
1034 return null;
1035 }
1036
1037 public String getLineEnds() {
1038 return _start.getID() + " " + _end.getID();
1039 }
1040
1041 public int getLineType() {
1042 return 1;
1043 }
1044
1045 @Override
1046 public boolean isFloating() {
1047 return _start.isFloating() && _end.isFloating();
1048 }
1049
1050 @Override
1051 public void updatePolygon() {
1052 _poly = new Polygon();
1053
1054 Rectangle one;
1055 Rectangle two;
1056
1057 /**
1058 * TODO: Fake dots seems inefficient, Fix it.
1059 */
1060
1061 // line goes from left->right, one->two
1062 if (_start.getX() <= _end.getX()) {
1063 /*
1064 * one = _start.getPolygon().getBounds(); two =
1065 * _end.getPolygon().getBounds();
1066 */
1067
1068 Item s = new Dot(_start.getX(), _start.getY(), -1);
1069 Item e = new Dot(_end.getX(), _end.getY(), -1);
1070
1071 one = s.getPolygon().getBounds();
1072 two = e.getPolygon().getBounds();
1073 } else {
1074 Item s = new Dot(_start.getX(), _start.getY(), -1);
1075 Item e = new Dot(_end.getX(), _end.getY(), -1);
1076
1077 one = e.getPolygon().getBounds();
1078 two = s.getPolygon().getBounds();
1079 /*
1080 * one = _end.getPolygon().getBounds(); two =
1081 * _start.getPolygon().getBounds();
1082 */
1083 }
1084
1085 // if one is above two
1086 if (one.getY() < two.getY()) {
1087 _poly.addPoint((int) one.getMaxX(), (int) one.getMinY());
1088 _poly.addPoint((int) two.getMaxX(), (int) two.getMinY());
1089 _poly.addPoint((int) two.getMinX(), (int) two.getMaxY());
1090 _poly.addPoint((int) one.getMinX(), (int) one.getMaxY());
1091 // if one is below two
1092 } else {
1093 _poly.addPoint((int) one.getMinX(), (int) one.getMinY());
1094 _poly.addPoint((int) two.getMinX(), (int) two.getMinY());
1095 _poly.addPoint((int) two.getMaxX(), (int) two.getMaxY());
1096 _poly.addPoint((int) one.getMaxX(), (int) one.getMaxY());
1097 }
1098
1099 }
1100
1101 @Override
1102 public void delete() {
1103 super.delete();
1104
1105 _start.removeLine(this);
1106 _end.removeLine(this);
1107
1108 //_start.invalidateAll();
1109 //_end.invalidateAll();
1110
1111 if (_start.getLines().size() == 0)
1112 _start.delete();
1113 if (_end.getLines().size() == 0)
1114 _end.delete();
1115 }
1116
1117 @Override
1118 public void anchor() {
1119 Frame current = getParentOrCurrentFrame();
1120 // Check if either end is off the current frame
1121 Frame parentEnd = getEndItem().getParent();
1122 if (parentEnd != null && parentEnd != current) {
1123 this.setParent(parentEnd);
1124 } else {
1125 Frame parentStart = getStartItem().getParent();
1126 if (parentStart != null && parentStart != current)
1127 this.setParent(parentStart);
1128 }
1129
1130 super.anchor();
1131 fixArrowheadLength();
1132 }
1133
1134 /**
1135 * Removes constraints from the end points of this line.
1136 */
1137 @Override
1138 public void removeAllConstraints() {
1139 // Find all constraints that include both the start and end.
1140 for (Constraint c : _start.getConstraints()) {
1141 if (c.contains(_end)) {
1142 _start.removeConstraint(c);
1143 _end.removeConstraint(c);
1144 }
1145 }
1146 }
1147
1148 public double getLength() {
1149 return getLength(_start.getPosition(), _end.getPosition());
1150 }
1151
1152 public Integer getPossibleConstraint() {
1153 if (_start.getY() == _end.getY() && _start.getX() != _end.getX())
1154 return Constraint.HORIZONTAL;
1155 else if (_start.getX() == _end.getX() && _start.getY() != _end.getY())
1156 return Constraint.VERTICAL;
1157 return null;
1158 }
1159
1160 public static double getLength(Point p1, Point p2) {
1161 return Point.distance(p1.x, p1.y, p2.x, p2.y);
1162 }
1163
1164 @Override
1165 public Rectangle[] getDrawingArea() { // TODO: CACHE - LIKE UPDATE POLYGON
1166
1167 float currentThickness = this.getThickness() + 4;
1168
1169 // Establish bounds
1170 int x = Math.min(_start.getX(), _end.getX());
1171 int y = Math.min(_start.getY(), _end.getY());
1172 int w = Math.max(_start.getX(), _end.getX()) - x + 1;
1173 int h = Math.max(_start.getY(), _end.getY()) - y + 1;
1174
1175 int thickness = (int) Math.ceil(currentThickness);
1176 if (thickness < 4)
1177 thickness = 4;
1178 int halfThickness = (int) Math.ceil(currentThickness / 2);
1179
1180 // Consider line thickness
1181 w += thickness;
1182 h += thickness;
1183 x -= halfThickness;
1184 y -= halfThickness;
1185
1186 Rectangle bounds = new Rectangle(x, y, w, h);
1187
1188 // TODO: Cap bounds
1189
1190 if (_end.hasVisibleArrow()) {
1191
1192 double arrowLength = _end.getArrowheadLength();
1193
1194 if (arrowLength == AUTO_ARROWHEAD_LENGTH) {
1195 arrowLength = Math
1196 .ceil(getAutoArrowheadLength((float) getLength()));
1197 } else {
1198 arrowLength = Math.ceil(arrowLength);
1199 }
1200
1201 arrowLength += ((thickness * 2) + 1);
1202 int nArrowLength = (int) (arrowLength + 1);
1203
1204 /* NAIVE version - but probably more efficient in the long run...
1205 overall as opposed to advanced/expensive calculations for getting exact
1206 bounding box */
1207 Rectangle arrowBounds = new Rectangle(_end.getX() - nArrowLength,
1208 _end.getY() - nArrowLength, 2 * nArrowLength,
1209 2 * nArrowLength);
1210
1211 if (currentThickness > 0.0f) {
1212
1213 arrowBounds = new Rectangle(arrowBounds.x - halfThickness,
1214 arrowBounds.y - halfThickness, arrowBounds.width
1215 + thickness, arrowBounds.height + thickness);
1216 }
1217
1218 return new Rectangle[] { bounds, arrowBounds };
1219 } else {
1220 return new Rectangle[] { bounds };
1221 }
1222
1223 }
1224
1225 @Override
1226 public boolean hasPermission(UserAppliedPermission p) {
1227 return _start.hasPermission(p);
1228 }
1229
1230 @Override
1231 public void setPermission(UserAppliedPermission p) {
1232 _start.setPermission(p);
1233 _end.setPermission(p);
1234 }
1235
1236 @Override
1237 public boolean dontSave() {
1238 return true;
1239 }
1240
1241 @Override
1242 public void scale(Float scale, int originX, int originY) {
1243 }
1244}
Note: See TracBrowser for help on using the repository browser.