View Javadoc

1   /*
2    *  File: DefaultRelationRenderer.java 
3    *  Copyright (c) 2004-2008  Peter Kliem (Peter.Kliem@jaret.de)
4    *  A commercial license is available, see http://www.jaret.de.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation; either version 2 of the License, or
9    *  (at your option) any later version.
10   *
11   *  This program is distributed in the hope that it will be useful,
12   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   *  GNU General Public License for more details.
15   *
16   *  You should have received a copy of the GNU General Public License
17   *  along with this program; if not, write to the Free Software
18   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19   */
20  package de.jaret.util.ui.timebars.swing.renderer;
21  
22  import java.awt.BasicStroke;
23  import java.awt.Color;
24  import java.awt.Graphics;
25  import java.awt.Graphics2D;
26  import java.awt.Point;
27  import java.awt.Rectangle;
28  import java.awt.Stroke;
29  import java.util.ArrayList;
30  import java.util.List;
31  
32  import de.jaret.util.date.Interval;
33  import de.jaret.util.ui.timebars.TimeBarViewerDelegate;
34  import de.jaret.util.ui.timebars.model.IIntervalRelation;
35  import de.jaret.util.ui.timebars.model.IIntervalRelation.Type;
36  import de.jaret.util.ui.timebars.model.IRelationalInterval;
37  import de.jaret.util.ui.timebars.model.TimeBarRow;
38  
39  /**
40   * Renderer rendering relations between intervals that implement the IRelationalInterval interface. Does not support
41   * vertical orientation.
42   * 
43   * @author kliem
44   * @version $Id: DefaultRelationRenderer.java 1073 2010-11-22 21:25:33Z kliem $
45   */
46  public class DefaultRelationRenderer implements IRelationRenderer {
47      /** default cache size. */
48      private static final int DEFAULT_CACHE_SIZE = 200;
49  
50      /** default color for the lines. */
51      private static final Color DEFAULT_LINE_COLOR = Color.BLACK;
52      /** default color for the selected lines. */
53      private static final Color DEFAULT_SELECTED_COLOR = Color.BLUE;
54      /** default line width for the connection lines. */
55      private static final int DEFAULT_LINE_WIDTH = 1;
56      /** default arrow size. */
57      private static final int DEFAULT_ARROW_SIZE = 5;
58  
59      /** cache holding the drawn lines. */
60      private List<Line> _cache;
61      /** color used for the lines. */
62      protected Color _lineColor = DEFAULT_LINE_COLOR;
63      /** color used for the lines when selected. */
64      protected Color _selectedColor = DEFAULT_SELECTED_COLOR;
65  
66      /** linewidth to use. */
67      protected int _lineWidth = DEFAULT_LINE_WIDTH;
68      /** arrow size. */
69      protected int _arrowSize = DEFAULT_ARROW_SIZE;
70  
71      /**
72       * Default constructor.
73       */
74      public DefaultRelationRenderer() {
75      }
76  
77      /**
78       * {@inheritDoc} Processes all rows that are displayed.
79       */
80      public void renderRelations(TimeBarViewerDelegate delegate, Graphics graphics) {
81          _cache = new ArrayList<Line>(DEFAULT_CACHE_SIZE);
82  
83          int firstRow = delegate.getFirstRow();
84  
85          // set the clipping to include only the area of the diagram rect
86          Rectangle clipSave = graphics.getClipBounds();
87          Rectangle nc = new Rectangle(delegate.getDiagramRect().x, delegate.getDiagramRect().y, delegate
88                  .getDiagramRect().width, delegate.getDiagramRect().height);
89          graphics.setClip(graphics.getClipBounds().intersection(nc));
90  
91          int upperYBound = delegate.getDiagramRect().y;
92          int lowerYBound = upperYBound + delegate.getDiagramRect().height;
93  
94          int rowsDisplayed = delegate.getRowsDisplayed();
95          for (int r = firstRow; r <= firstRow + rowsDisplayed + 1 && r < delegate.getRowCount(); r++) {
96              TimeBarRow row = delegate.getRow(r);
97              int y = delegate.yForRow(row);
98              int rowHeight = delegate.getTimeBarViewState().getRowHeight(row);
99              if (y == -1) {
100                 // no coord -> is not displayed
101                 break;
102             }
103 
104             // row is drawn if either the beginning or the end is inside the
105             // clipping rect
106             // or if the upperBound is inside the row rect (clipping rect is
107             // inside the row rect
108             if ((y >= upperYBound && y <= lowerYBound)
109                     || (y + rowHeight >= upperYBound && y + rowHeight <= lowerYBound)
110                     || (upperYBound > y && upperYBound < y + rowHeight)) {
111                 drawRow(delegate, graphics, delegate.getRow(r), y);
112             }
113         }
114         graphics.setClip(clipSave);
115 
116     }
117 
118     /**
119      * Proceses a single row.
120      * 
121      * @param delegate the delegate
122      * @param graphics Graphics to draw on
123      * @param row row to process
124      * @param y y coordinate of the row
125      */
126     private void drawRow(TimeBarViewerDelegate delegate, Graphics graphics, TimeBarRow row, int y) {
127         for (Interval interval : row.getIntervals()) {
128             // do not process filtered intervals
129             if (delegate.getIntervalFilter() == null || delegate.getIntervalFilter().isInResult(interval)) {
130                 if (interval instanceof IRelationalInterval) {
131                     IRelationalInterval rInterval = (IRelationalInterval) interval;
132                     for (IIntervalRelation relation : rInterval.getRelations()) {
133                         // double check both ends of the relation to be unfiltered
134                         if (delegate.getIntervalFilter() == null
135                                 || (delegate.getIntervalFilter().isInResult(relation.getStartInterval()) && delegate
136                                         .getIntervalFilter().isInResult(relation.getEndInterval()))) {
137                             if (!hasBeenDrawn(relation)) {
138                                 drawDependency(delegate, graphics, rInterval, y, row, relation);
139                             }
140                         }
141                     }
142                 }
143             }
144         }
145 
146     }
147 
148     /**
149      * Draw a single relation.
150      * 
151      * @param delegate the delegate
152      * @param graphics the graphics to draw on
153      * @param rInterval the interval from which we are coming
154      * @param y y of the row
155      * @param row the row the interval is on
156      * @param relation the relation to draw
157      */
158     private void drawDependency(TimeBarViewerDelegate delegate, Graphics graphics, IRelationalInterval rInterval,
159             int y, TimeBarRow row, IIntervalRelation relation) {
160         int off = _arrowSize + _arrowSize / 2;
161 
162         boolean selected = delegate.getSelectionModel().isSelected(relation);
163 
164         Graphics2D g2d = (Graphics2D) graphics;
165 
166         Color fg = graphics.getColor();
167         Stroke lineWidth = g2d.getStroke();
168 
169         Stroke stroke = new BasicStroke(_lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
170         g2d.setStroke(stroke);
171 
172         Color color = null;
173         if (selected) {
174             color = _selectedColor;
175         } else {
176             color = _lineColor;
177         }
178         graphics.setColor(color);
179 
180         IRelationalInterval beginTask;
181         TimeBarRow beginRow;
182         Point begin;
183 
184         IRelationalInterval endTask;
185         TimeBarRow endRow;
186         Point end;
187 
188         if (relation.getStartInterval().equals(rInterval)) {
189             beginTask = rInterval;
190             beginRow = row;
191 
192             endTask = relation.getEndInterval();
193             endRow = getRowForInterval(delegate, endTask);
194         } else {
195             endTask = rInterval;
196             endRow = row;
197 
198             beginTask = relation.getStartInterval();
199             beginRow = getRowForInterval(delegate, beginTask);
200         }
201 
202         int rheight = delegate.getTimeBarViewState().getRowHeight(beginRow);
203 
204         if (relation.getType() == IIntervalRelation.Type.END_BEGIN) {
205             begin = getRightPoint(delegate, beginRow, beginTask);
206             end = getLeftPoint(delegate, endRow, endTask);
207             int ydir = end.y > begin.y ? 1 : -1;
208 
209             int[] points = new int[] {begin.x, begin.y, begin.x + off, begin.y, begin.x + off,
210                     begin.y + (rheight / 2 * ydir), end.x - off, begin.y + (rheight / 2 * ydir), end.x - off, end.y,
211                     end.x, end.y};
212             g2d.drawPolyline(xPoints(points), yPoints(points), points.length / 2);
213             registerLines(relation, points);
214 
215             drawArrow(graphics, begin, end, color, relation.getDirection(), relation.getType());
216 
217         } else if (relation.getType() == IIntervalRelation.Type.BEGIN_BEGIN) {
218             begin = getLeftPoint(delegate, beginRow, beginTask);
219             end = getLeftPoint(delegate, endRow, endTask);
220             int ydir = end.y > begin.y ? 1 : -1;
221 
222             int[] points = new int[] {begin.x, begin.y, begin.x - off, begin.y, begin.x - off,
223                     begin.y + (rheight / 2 * ydir), end.x - off, begin.y + (rheight / 2 * ydir), end.x - off, end.y,
224                     end.x, end.y};
225 
226             g2d.drawPolyline(xPoints(points), yPoints(points), points.length / 2);
227             registerLines(relation, points);
228             drawArrow(graphics, begin, end, color, relation.getDirection(), relation.getType());
229         } else if (relation.getType() == IIntervalRelation.Type.END_END) {
230             begin = getRightPoint(delegate, beginRow, beginTask);
231             end = getRightPoint(delegate, endRow, endTask);
232             int ydir = end.y > begin.y ? 1 : -1;
233 
234             int[] points = new int[] {begin.x, begin.y, begin.x + off, begin.y, begin.x + off,
235                     begin.y + (rheight / 2 * ydir), end.x + off, begin.y + (rheight / 2 * ydir), end.x + off, end.y,
236                     end.x, end.y};
237             g2d.drawPolyline(xPoints(points), yPoints(points), points.length / 2);
238             registerLines(relation, points);
239             drawArrow(graphics, begin, end, color, relation.getDirection(), relation.getType());
240         } else if (relation.getType() == IIntervalRelation.Type.BEGIN_END) {
241             begin = getLeftPoint(delegate, beginRow, beginTask);
242             end = getRightPoint(delegate, endRow, endTask);
243             int ydir = end.y > begin.y ? 1 : -1;
244             // unused int xdir = end.x > begin.x ? 1 : -1;
245 
246             int[] points = new int[] {begin.x, begin.y, begin.x - off, begin.y, begin.x - off,
247                     begin.y + (rheight / 2 * ydir), end.x + off, begin.y + (rheight / 2 * ydir), end.x + off, end.y,
248                     end.x, end.y};
249             g2d.drawPolyline(xPoints(points), yPoints(points), points.length / 2);
250             registerLines(relation, points);
251             drawArrow(graphics, begin, end, color, relation.getDirection(), relation.getType());
252         }
253         graphics.setColor(fg);
254         g2d.setStroke(lineWidth);
255     }
256 
257     /**
258      * Extract the x points from a joined x/y array of coordinates.
259      * 
260      * @param points the array containing all coordinates (x1,y1,x2,y2,...)
261      * @return an array that only contains the x values (x1,x2,...)
262      */
263     private int[] xPoints(int[] points) {
264         int[] result = new int[points.length / 2];
265         for (int i = 0; i < points.length; i += 2) {
266             result[i / 2] = points[i];
267         }
268         return result;
269     }
270 
271     /**
272      * Extract the y points from a joined x/y array of coordinates.
273      * 
274      * @param points the array containing all coordinates (x1,y1,x2,y2,...)
275      * @return an array that only contains the y values (y1,y2,...)
276      */
277     private int[] yPoints(int[] points) {
278         int[] result = new int[points.length / 2];
279         for (int i = 1; i < points.length; i += 2) {
280             result[i / 2] = points[i];
281         }
282         return result;
283     }
284 
285     /**
286      * Calculate the point on the right side of an interval for connection.
287      * 
288      * @param delegate the delegate
289      * @param row the row
290      * @param interval the interval
291      * @return the point on the right side of the interval to connect the draw relation to
292      */
293     private Point getRightPoint(TimeBarViewerDelegate delegate, TimeBarRow row, Interval interval) {
294         java.awt.Rectangle rect = delegate.getIntervalBounds(row, interval);
295         return new Point(rect.x + rect.width, rect.y + rect.height / 2);
296     }
297 
298     /**
299      * Calculate the point on the left side of an interval for connection.
300      * 
301      * @param delegate the delegate
302      * @param row the row
303      * @param interval the interval
304      * @return the point on the left side of the interval to connect the draw relation to
305      */
306     private Point getLeftPoint(TimeBarViewerDelegate delegate, TimeBarRow row, Interval interval) {
307         java.awt.Rectangle rect = delegate.getIntervalBounds(row, interval);
308         return new Point(rect.x, rect.y + rect.height / 2);
309     }
310 
311     /**
312      * Wrapper supplying the row for a given interval.
313      * 
314      * @param delegate the delegate
315      * @param interval interval to search the
316      * @return row or <code>null</code>
317      */
318     private TimeBarRow getRowForInterval(TimeBarViewerDelegate delegate, Interval interval) {
319         return delegate.getModel().getRowForInterval(interval);
320     }
321 
322     /**
323      * Check whether a relation has already been drawn.
324      * 
325      * @param relation relation to check
326      * @return <code>true</code> if the relation has already been drawn
327      */
328     private boolean hasBeenDrawn(IIntervalRelation relation) {
329         for (Line l : _cache) {
330             if (l.relation.equals(relation)) {
331                 return true;
332             }
333         }
334         return false;
335     }
336 
337     /**
338      * Register a line for later recognition.
339      * 
340      * @param relation the relation
341      * @param coords pairs of xy coords
342      */
343     private void registerLines(IIntervalRelation relation, int[] coords) {
344         for (int i = 0; i <= coords.length - 4; i += 2) {
345             _cache.add(new Line(relation, coords[i], coords[i + 1], coords[i + 2], coords[i + 3]));
346         }
347     }
348 
349     /**
350      * Draw begin and/or end arrows for a relation depending on the direction.
351      * 
352      * @param graphics Graphics to paint on
353      * @param begin begin point
354      * @param end end point
355      * @param color color to use
356      * @param direction direction
357      * @param type type of the relation used to determine which arrows to draw
358      */
359     private void drawArrow(Graphics graphics, Point begin, Point end, Color color,
360             IIntervalRelation.Direction direction, Type type) {
361 
362         if (direction.equals(IIntervalRelation.Direction.BACK) || direction.equals(IIntervalRelation.Direction.BI)) {
363             if (type.equals(Type.END_BEGIN) || type.equals(Type.END_END)) {
364                 drawArrow(graphics, begin, false, color);
365             } else {
366                 drawArrow(graphics, begin, true, color);
367             }
368         }
369         if (direction.equals(IIntervalRelation.Direction.FORWARD) || direction.equals(IIntervalRelation.Direction.BI)) {
370             if (type.equals(Type.BEGIN_END) || type.equals(Type.END_END)) {
371                 drawArrow(graphics, end, false, color);
372             } else {
373                 drawArrow(graphics, end, true, color);
374             }
375         }
376     }
377 
378     /**
379      * Draw an arrow triangle.
380      * 
381      * @param graphics Graphics to paint with
382      * @param p Point of the vertex
383      * @param leftToRight <code>true</code> for left to right pointing
384      * @param color color to use
385      */
386     private void drawArrow(Graphics graphics, Point p, boolean leftToRight, Color color) {
387         Color bg = graphics.getColor();
388         graphics.setColor(color);
389         int off = _arrowSize;
390         int[] points;
391         if (leftToRight) {
392             int[] pts = {p.x, p.y, p.x - off, p.y - off, p.x - off, p.y + off};
393             points = pts;
394         } else {
395             int[] pts = {p.x, p.y, p.x + off, p.y - off, p.x + off, p.y + off};
396             points = pts;
397         }
398         Graphics2D g2d = (Graphics2D) graphics;
399         g2d.fillPolygon(xPoints(points), yPoints(points), points.length / 2);
400         graphics.setColor(bg);
401     }
402 
403     /**
404      * {@inheritDoc}
405      */
406     public List<IIntervalRelation> getRelationsForCoord(int x, int y) {
407         List<IIntervalRelation> result = new ArrayList<IIntervalRelation>(2);
408         for (Line line : _cache) {
409             if (line.hit(x, y, 2)) {
410                 result.add(line.relation);
411             }
412         }
413 
414         return result;
415     }
416 
417     /**
418      * {@inheritDoc}
419      */
420     public String getTooltip(int x, int y) {
421         List<IIntervalRelation> result = getRelationsForCoord(x, y);
422         if (result.size() == 0) {
423             return null;
424         } else {
425             return result.get(0).toString();
426         }
427     }
428 
429     /**
430      * Internal line represenation for caching.
431      * 
432      * @author kliem
433      * @version $Id: DefaultRelationRenderer.java 1073 2010-11-22 21:25:33Z kliem $
434      */
435     public class Line {
436         /** relation the line belongs to. */
437         public IIntervalRelation relation;
438         /** start x coordinate. */
439         public int x1;
440         /** start y coordinate. */
441         public int y1;
442         /** end x coordinate. */
443         public int x2;
444         /** end y coordinate. */
445         public int y2;
446 
447         /**
448          * Construct a line.
449          * 
450          * @param relation relation the line belongs to
451          * @param x1 start x
452          * @param y1 start y
453          * @param x2 end x
454          * @param y2 end y
455          */
456         public Line(IIntervalRelation relation, int x1, int y1, int x2, int y2) {
457             this.relation = relation;
458             this.x1 = x1;
459             this.y1 = y1;
460             this.x2 = x2;
461             this.y2 = y2;
462         }
463 
464         /**
465          * Check whether the line is hit by a certain coordinate.
466          * 
467          * @param x x coord
468          * @param y y coord
469          * @param tolerance the max delta
470          * @return true if hit
471          */
472         public boolean hit(int x, int y, int tolerance) {
473             if (x1 == x2) {
474                 if (x1 - tolerance <= x && x <= x1 + tolerance) {
475                     if ((y1 < y2 && y1 - tolerance <= y && y <= y2 + tolerance)
476                             || (y2 < y1 && y2 - tolerance <= y && y <= y1 + tolerance)) {
477                         return true;
478                     }
479                 }
480             } else {
481                 if (y1 == y2) {
482                     if (y1 - tolerance <= y && y <= y1 + tolerance) {
483                         if ((x1 < x2 && x1 - tolerance <= x && x <= x2 + tolerance)
484                                 || (x2 < x1 && x2 - tolerance <= x && x <= x1 + tolerance)) {
485                             return true;
486                         }
487                     }
488                 }
489             }
490             return false;
491         }
492     }
493 
494     /**
495      * Retrieve the current line width.
496      * 
497      * @return the current line width
498      */
499     public int getLineWidth() {
500         return _lineWidth;
501     }
502 
503     /**
504      * Set the line width for the connection lines.
505      * 
506      * @param lineWidth line width to set
507      */
508     public void setLineWidth(int lineWidth) {
509         _lineWidth = lineWidth;
510     }
511 
512     /**
513      * Retrieve the current size set for the arrows.
514      * 
515      * @return the arrow size
516      */
517     public int getArrowSize() {
518         return _arrowSize;
519     }
520 
521     /**
522      * Set the arrow size.
523      * 
524      * @param arrowSize arrow size
525      */
526     public void setArrowSize(int arrowSize) {
527         _arrowSize = arrowSize;
528     }
529 
530 }