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

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

Added license headers to all files, added full GPL3 license file, moved license header generator script to dev/bin/scripts

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