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

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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