/*******************************************************************************
* <copyright>
*
* Copyright (c) 2005, 2010 SAP AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API, implementation and documentation
*
* </copyright>
*
*******************************************************************************/
package org.eclipse.graphiti.ui.internal.figures;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.draw2d.Ellipse;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.geometry.Vector;
import org.eclipse.graphiti.mm.algorithms.AbstractText;
import org.eclipse.graphiti.mm.algorithms.styles.GradientColoredArea;
import org.eclipse.graphiti.mm.algorithms.styles.TextStyle;
import org.eclipse.graphiti.mm.algorithms.styles.TextStyleRegion;
import org.eclipse.graphiti.ui.internal.IResourceRegistry;
import org.eclipse.graphiti.ui.internal.IResourceRegistryHolder;
import org.eclipse.graphiti.ui.internal.config.IConfigurationProviderInternal;
import org.eclipse.graphiti.ui.internal.util.DataTypeTransformation;
import org.eclipse.graphiti.util.PredefinedColoredAreas;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.widgets.Display;
/**
* A utility class containing static helper-methods for Graphiti figures.
*
* @noinstantiate This class is not intended to be instantiated by clients.
* @noextend This class is not intended to be subclassed by clients.
*/
public class GFFigureUtil {
/**
* Singleton used for temporary calculations.
*/
private static final float SINGLETON_BOUNDS[] = new float[4];
/**
* Fills a given rectangle with a gradient color-flow on the given Graphics.
* For this the rectangle is separated into several inner rectangles. Those
* inner rectangles are calculated from the given rectangle by
* percentage-values. Each inner rectangle has a start-color and end-color,
* which define the gradient of the color-flow.
* <p>
* Example: The parameters [Color.red, Color.yellow, Color.green] and [0,
* 20, 80] would result in a filled rectangle, where the top 20% are flow
* from red to yellow and the next 80% flow from yellow to green.
*
* @param registryHolder
* To get the
* {@link IResourceRegistry#getSwtColor(int, int, int)}.
*
* @param rectangle
* The rectangle, which to fill with the gradient color-flow.
* @param graphics
* The Graphics, on which to fill the rectangle.
* @param coloredArea
* The area of the rectangle and the colors used for filling.
* @param zoom
* The current zoom-level
* @param vertical
* If true, fills the area vertically, otherwise horizontally
*/
public static void paintColorFlow(IResourceRegistryHolder registryHolder, Rectangle rectangle, Graphics graphics,
GradientColoredArea coloredArea, double zoom, boolean vertical) {
// calculate rectangle to fill
Rectangle fillRectangle;
if (vertical) {
int start = PredefinedColoredAreas.getLocation(coloredArea.getStart(), rectangle.height, zoom);
int end = PredefinedColoredAreas.getLocation(coloredArea.getEnd(), rectangle.height, zoom);
fillRectangle = new Rectangle(rectangle.x, rectangle.y + start, rectangle.width, end - start);
} else {
int start = PredefinedColoredAreas.getLocation(coloredArea.getStart(), rectangle.width, zoom);
int end = PredefinedColoredAreas.getLocation(coloredArea.getEnd(), rectangle.width, zoom);
fillRectangle = new Rectangle(rectangle.x + start, rectangle.y, end - start, rectangle.height);
}
// fill rectangle
org.eclipse.graphiti.mm.algorithms.styles.Color foregroundColor = coloredArea.getStart().getColor();
Color foregroundColorSWT = DataTypeTransformation.toSwtColor(registryHolder.getResourceRegistry(),
foregroundColor);
org.eclipse.graphiti.mm.algorithms.styles.Color backgroundColor = coloredArea.getEnd().getColor();
Color backgroundColorSWT = DataTypeTransformation.toSwtColor(registryHolder.getResourceRegistry(),
backgroundColor);
graphics.setForegroundColor(foregroundColorSWT);
graphics.setBackgroundColor(backgroundColorSWT);
graphics.fillGradient(fillRectangle.x, fillRectangle.y, fillRectangle.width, fillRectangle.height, vertical);
}
/**
* Returns if the given point is contained in the ellipse defined by the
* bounding rectangle. Possible results are
* <ul>
* <li>Boolean.TRUE = the point is located directly in the ellipse.</li>
* <li>Boolean.FALSE = the point is located outside the bounding box of the
* ellipse.</li>
* <li>null = the point is located inside the bounding box of the ellipse</li>
* </ul>
* defined by the bounding rectangle.
*
* @param r
* The bounding-rectangle of the ellipse.
* @param x
* The x-coordinate of the point to check.
* @param y
* The y-coordinate of the point to check.
* @return If the given point is contained in the ellipse defined by the
* bounding rectangle.
*/
public static Boolean containsPointInEllipse(Rectangle r, int x, int y) {
Ellipse ellipse = new Ellipse();
ellipse.setBounds(r);
return ellipse.containsPoint(x, y);
}
/**
* Returns if the given point is contained in this line. Possible results
* are
* <ul>
* <li>Boolean.TRUE = the point is located directly on the line.</li>
* <li>Boolean.FALSE = the point is located outside the bounding box of the
* line.</li>
* <li>null = the point is located inside the bounding box of the line</li>
* </ul>
* The lineWidth is considered in the calculation.
*
* @param x1
* The x-coordinate of the start-point of the line.
* @param y1
* The y-coordinate of the start-point of the line.
* @param x2
* The x-coordinate of the end-point of the line.
* @param y2
* The y-coordinate of the end-point of the line.
* @param x
* The x-coordinate of the point to check.
* @param y
* The y-coordinate of the point to check.
* @param lineWidth
* The width of the line.
* @return If the given point is contained in this line.
*/
public static Boolean containsPointInLine(int x1, int y1, int x2, int y2, int x, int y, int lineWidth) {
int halfLineWidth = lineWidth / 2;
Rectangle lineBounds = Rectangle.SINGLETON;
lineBounds.setSize(0, 0);
lineBounds.setLocation(x1, y1);
lineBounds.union(x2, y2);
lineBounds.expand(halfLineWidth, halfLineWidth);
// fast check
if (!lineBounds.contains(x, y))
return Boolean.FALSE;
int v1x, v1y, v2x, v2y;
int numerator, denominator;
int result = 0;
// calculates the length squared of the cross product of two vectors, v1
// & v2.
if (x1 != x2 && y1 != y2) {
v1x = x2 - x1;
v1y = y2 - y1;
v2x = x - x1;
v2y = y - y1;
numerator = v2x * v1y - v1x * v2y;
denominator = v1x * v1x + v1y * v1y;
result = (int) ((long) numerator * numerator / denominator);
}
// if it is the same point, and it passes the bounding box test,
// the result is always true.
if (result <= halfLineWidth * halfLineWidth) {
return Boolean.TRUE;
} else {
return null;
}
}
/**
* Returns if the given point is contained in this polyline. Possible
* results are
* <ul>
* <li>Boolean.TRUE = the point is located directly on the polyline.</li>
* <li>Boolean.FALSE = the point is located outside the bounding box of the
* polyline.</li>
* <li>null = the point is located inside the bounding box of the polyline</li>
* </ul>
* The lineWidth is considered in the calculation.
*
* @param points
* The points of the polyline.
* @param x
* The x-coordinate of the point to check.
* @param y
* The y-coordinate of the point to check.
* @param lineWidth
* The width of the line.
* @return If the given point is contained in this polyline.
*/
public static Boolean containsPointInPolyline(PointList points, int x, int y, int lineWidth) {
int halfLineWidth = lineWidth / 2;
Rectangle lineBounds = points.getBounds().getCopy();
lineBounds.expand(halfLineWidth, halfLineWidth);
// fast check
if (!lineBounds.contains(x, y)) {
return Boolean.FALSE;
}
// check if point on lines (regarding tolerance)
int ints[] = points.toIntArray();
for (int index = 0; index < ints.length - 3; index += 2) {
Boolean containsPointInLine = GFFigureUtil.containsPointInLine(ints[index], ints[index + 1], ints[index + 2], ints[index + 3],
x, y, lineWidth);
if (Boolean.TRUE.equals(containsPointInLine)) {
return Boolean.TRUE;
}
}
return null;
}
/**
* Returns if the given point is contained in this polygon. Possible results
* are
* <ul>
* <li>Boolean.TRUE = the point is located directly in the polygon.</li>
* <li>Boolean.FALSE = the point is located outside the bounding box of the
* polygon.</li>
* <li>null = the point is located inside the bounding box of the polygon</li>
* </ul>
*
* @param points
* The points of the polygon.
* @param x
* The x-coordinate of the point to check.
* @param y
* The y-coordinate of the point to check.
* @return If the given point is contained in this polygon.
*/
public static Boolean containsPointInPolygon(PointList points, int x, int y) {
// fast check
if (!points.getBounds().contains(x, y)) {
return Boolean.FALSE;
}
// check if point is inside polygon
boolean isOdd = false;
int[] pointsxy = points.toIntArray();
int n = pointsxy.length;
if (n > 3) { // if there are at least 2 Points (4 ints)
int x1, y1;
int x0 = pointsxy[n - 2];
int y0 = pointsxy[n - 1];
for (int i = 0; i < n; x0 = x1, y0 = y1) {
x1 = pointsxy[i++];
y1 = pointsxy[i++];
if (y0 <= y && y < y1 && crossProduct(x1, y1, x0, y0, x, y) > 0)
isOdd = !isOdd;
if (y1 <= y && y < y0 && crossProduct(x0, y0, x1, y1, x, y) > 0)
isOdd = !isOdd;
}
if (isOdd) {
return Boolean.TRUE;
}
}
return null;
}
/**
* Private helper method which calculates the cross-product of the given
* numbers.
*/
private static int crossProduct(int ax, int ay, int bx, int by, int cx, int cy) {
return (ax - cx) * (by - cy) - (ay - cy) * (bx - cx);
}
/**
* Returns a draw2d point-list of the given polygon model-element.
*
* @param polyline
* The polygon model-element for which to return the draw2d
* point-list.
* @return A draw2d point-list of the given polygon model-element.
*/
public static PointList getPointList(org.eclipse.graphiti.mm.algorithms.Polyline polyline) {
int deltaX = polyline.getX();
int deltaY = polyline.getY();
PointList pointList = new PointList();
for (org.eclipse.graphiti.mm.algorithms.styles.Point dtp : polyline.getPoints()) {
pointList.addPoint(dtp.getX() + deltaX, dtp.getY() + deltaY);
}
return pointList;
}
/**
* Returns the rectangular bounds of a given Path.
*
* @param path
* The Path for which to return the bounds.
* @return The rectangular bounds of a given Path.
*/
public static Rectangle getPathBounds(Path path) {
Rectangle ret = Rectangle.SINGLETON;
path.getBounds(SINGLETON_BOUNDS);
ret.setLocation((int) Math.floor(SINGLETON_BOUNDS[0]), (int) Math.floor(SINGLETON_BOUNDS[1]));
ret.setSize((int) Math.ceil(SINGLETON_BOUNDS[2]), (int) Math.ceil(SINGLETON_BOUNDS[3]));
return ret;
}
/**
* Returns a new instance of the input rectangle zoomed by the zoom-level
* and shrinked by the half line-width.
*
* @param rectangle
* The rectangle to zoom and shrink.
* @param zoom
* The zoom-level to use.
* @param lw
* The line-width to use.
* @return A new instance of the input rectangle zoomed by the zoom-level
* and shrinked by the half line-width. Returns null, if input
* rectangle is null.
*/
public static Rectangle getAdjustedRectangle(Rectangle rectangle, double zoom, int lw) {
if (rectangle == null) {
return null;
}
Rectangle ret = new Rectangle(rectangle);
if (zoom != 1.0) {
ret.x = (int) (Math.floor(rectangle.x * zoom));
ret.y = (int) (Math.floor(rectangle.y * zoom));
ret.width = (int) (Math.floor(((rectangle.x + rectangle.width) * zoom))) - ret.x;
ret.height = (int) (Math.floor(((rectangle.y + rectangle.height) * zoom))) - ret.y;
}
int adjustmentTopLeft = lw / 2;
int adjustmentBottomRight = lw;
ret.x += adjustmentTopLeft;
ret.y += adjustmentTopLeft;
ret.width -= adjustmentBottomRight;
ret.height -= adjustmentBottomRight;
return ret;
}
/**
* Returns a new instance of the input point-list, which is adjusted
* regarding the given zoom-factor and line-width. Concretely this means the
* following:
* <ul>
* <li>All points are zoomed by the given zoom-factor.</li>
* <li>All points are translated towards the center by half the line-width.
* This way the point-list stays insides its bounds even for a big
* line-width.</li>
* </ul>
*
* @param points
* The point-list which to adjust.
* @param zoom
* The zoom-factor by which to zoom all points.
* @param lw
* The line-width, which to consider when translating towards the
* center.
*/
public static PointList getAdjustedPointList(PointList points, double zoom, double lw) {
Rectangle zoomedBounds = points.getBounds().getCopy().scale(zoom);
double middlex = zoomedBounds.x + (zoomedBounds.width / 2);
double middley = zoomedBounds.y + (zoomedBounds.height / 2);
PointList ret = new PointList();
for (int i = 0; i < points.size(); i++) {
Point point = points.getPoint(i);
point.scale(zoom);
// translate all points towards the middle depending on the
// line-width, so that the polyline remains inside the bounds
// Note, that the delta has to be rounded up/down depending on the
// relative location from point to middle.
double dx;
double dy;
if (point.x < middlex) {
dx = Math.ceil(((middlex - point.x) / zoomedBounds.width) * lw);
} else {
dx = Math.floor(((middlex - point.x) / zoomedBounds.width) * lw);
}
if (point.y < middley) {
dy = Math.ceil(((middley - point.y) / zoomedBounds.height) * lw);
} else {
dy = Math.floor(((middley - point.y) / zoomedBounds.height) * lw);
}
point.translate((int) dx, (int) dy);
ret.addPoint(point);
}
return ret;
}
/**
* Returns a new PointList, which results from translating the given
* PointList.
*
* @param points
* The PointList, which is used as source for translation. It is
* not changed.
* @param dx
* The x-value to translate.
* @param dy
* The y-value to translate.
* @return A new PointList, which results from translating the source
* PointList.
*/
public static PointList getTranslatedPointList(PointList points, int dx, int dy) {
PointList ret = new PointList();
for (int i = 0; i < points.size(); i++) {
Point pt = points.getPoint(i);
pt.x += dx;
pt.y += dy;
ret.addPoint(pt);
}
return ret;
}
/**
* Returns a path which draws a bezier-curve through the given
* bezier-point-list.
*
* @param origPoints
* The bezier-points through which to draw the bezier-curve.
* @param isClosed
* If true, then the path will be closed, so that the
* bezier-curve ends at the start-point. Note, that the result is
* different to a path with the same start/end point but where
* isClosed is false, because in that case there is no rounded
* corner at the start/end point.
* @return A path which draws a bezier-curve through the given point-list.
*/
public static Path getBezierPath(List<BezierPoint> origPoints, boolean isClosed) {
Path path = new Path(null);
// make a copy, so that we can change it
List<BezierPoint> points = new ArrayList<BezierPoint>(origPoints.size() + 2);
points.addAll(origPoints);
// Draw simple lines, as bezier-curve doesn't make sense
if (points.size() < 3 || !hasBezierDistance(origPoints)) {
if (points.size() != 0) {
path.moveTo(points.get(0).getX(), points.get(0).getY());
for (int i = 1; i < points.size(); i++) {
path.lineTo(points.get(i).getX(), points.get(i).getY());
}
}
} else { // Draw bezier curve
// Idea for the closed bezier curve:
// The first two points are added to the end of the point-list.
// Afterwards the bezier-curve through the points is drawn as usual,
// except that the first line and the last line are not drawn.
// Adjust point-list if closing is needed: add the first two points
// again at the end
if (isClosed) {
if (!points.get(points.size() - 1).equals(points.get(0))) { // first
// !=
// last
// =>
// only
// then
// double
// first
// point
points.add(points.get(0));
}
points.add(points.get(1));
}
// Initialize the points for calculation
Point c = points.get(0).createDraw2dPoint(); // the current
// control-point
Point q = points.get(1).createDraw2dPoint(); // the point following
// the current
// control-point
Point r = new Point();
Point s = new Point();
// If not closed, draw the first line from the first point to r,
// otherwise move to r
determineBezierPoints(c, q, r, s, points.get(0).getBezierDistanceAfter(), points.get(1).getBezierDistanceBefore());
if (!isClosed) {
path.moveTo(points.get(0).getX(), points.get(0).getY());
path.lineTo(r.x, r.y);
} else {
path.moveTo(r.x, r.y);
}
for (int index = 2; index < points.size(); index++) {
// Move c and q one position forward
c.setLocation(q);
points.get(index).copyToDraw2dPoint(q);
// Update r and s
determineBezierPoints(c, q, r, s, points.get(index - 1).getBezierDistanceAfter(), points.get(index)
.getBezierDistanceBefore());
// draw the curve
path.quadTo(c.x, c.y, s.x, s.y);
path.lineTo(r.x, r.y);
}
// If not closed, draw the final line from r to the last point,
// otherwise do nothing
if (!isClosed) {
// Draw the final line from r to the last point.
path.lineTo(points.get(points.size() - 1).getX(), points.get(points.size() - 1).getY());
}
}
// The algorithm already takes care, that the line ends again at the
// start-point.
// But a final path.close() takes care of drawing problems.
if (isClosed) {
path.close();
}
return path;
}
/**
* Returns true, if at least one of the points in the list has a
* bezier-distance != 0.
*
* @param points
* The points, which bezier-distances to check.
*
* @return true, if at least one of the points in the list has a
* bezier-distance != 0.
*/
public static boolean hasBezierDistance(List<BezierPoint> points) {
for (BezierPoint point : points) {
if (point.getBezierDistanceBefore() != 0 || point.getBezierDistanceAfter() != 0)
return true;
}
return false;
}
/**
* Internal helper method used by {@link #getBezierPath(List, boolean)} to
* calculate the bezier-points 'r' and 's'. Note, that the parameters 'r'
* and 's' are changed in this method, so they are basically the result of
* this method.
*
* @param c
* The current control-point.
* @param q
* The point following the current control-point.
* @param r
* The bezier-point, which is calculated in this method. This is
* a "return-value".
* @param s
* The bezier-point, which is calculated in this method. This is
* a "return-value".
* @param distanceAfterCurrent
* The bezier distance after the current point, used to calculate
* the rounding of the bezier-curve.
* @param distanceBeforeNext
* The bezier distance before the next point, used to calculate
* the rounding of the bezier-curve.
*/
private static void determineBezierPoints(Point c, Point q, Point r, Point s, int distanceAfterCurrent, int distanceBeforeNext) {
// Determine v and m
// Ray v = new Ray();
int vx = q.x - c.x;
int vy = q.y - c.y;
Vector v = new Vector(vx, vy);
double absV = v.getLength();
// Ray m = new Ray();
int mx = Math.round(c.x + vx / 2);
int my = Math.round(c.y + vy / 2);
// Determine tolerance
// Idea:
// The vector v is the line after the current control-point c.
// If the sum of the bezier-distances is greater than the half
// line-length of v,
// then a simplified calculation for the bezier-points r and s must be
// done.
int tolerance = distanceAfterCurrent + distanceBeforeNext;
// Determine the "results" r and s
if (absV < tolerance) {
// use the the midpoint m for r and s
r.x = mx;
r.y = my;
s.x = mx;
s.y = my;
} else {
double x = (absV - distanceBeforeNext) / absV;
r.x = Math.round(c.x + (float) x * vx);
r.y = Math.round(c.y + (float) x * vy);
double y = distanceAfterCurrent / absV;
s.x = Math.round(c.x + (float) y * vx);
s.y = Math.round(c.y + (float) y * vy);
}
}
protected static void drawRichText(Graphics g, String draw, int x, int y,
IConfigurationProviderInternal configurationProvider, AbstractText text) {
drawRichText(g, draw, x, y, -1, false, 0, configurationProvider, text);
}
protected static void drawRichText(Graphics g, String draw, int x, int y, int bidiLevel, boolean mirrored,
int currentOffset, IConfigurationProviderInternal configurationProvider, AbstractText text) {
if (bidiLevel == -1) {
TextLayout tl = new TextLayout(Display.getDefault());
if (mirrored)
tl.setOrientation(SWT.RIGHT_TO_LEFT);
tl.setFont(g.getFont());
tl.setText(draw);
List<Font> fontsToDispose = new ArrayList<Font>();
for (TextStyleRegion style : text.getStyleRegions()) {
int start = style.getStart() - currentOffset;
int end = style.getEnd() - currentOffset;
if (start >= draw.length())
continue;
if (end < 0)
continue;
org.eclipse.swt.graphics.TextStyle textStyle = new org.eclipse.swt.graphics.TextStyle();
TextStyle gTextStyle = style.getStyle();
textStyle.underline = gTextStyle.isUnderline();
textStyle.strikeout = gTextStyle.isStrikeout();
textStyle.underlineStyle = gTextStyle.getUnderlineStyle().getValue();
org.eclipse.graphiti.mm.algorithms.styles.Font font = gTextStyle.getFont();
if (font != null) {
textStyle.font = DataTypeTransformation.toSwtFont(font);
fontsToDispose.add(textStyle.font);
}
org.eclipse.graphiti.mm.algorithms.styles.Color foreground = gTextStyle.getForeground();
if (foreground != null) {
textStyle.foreground = DataTypeTransformation.toSwtColor(
configurationProvider.getResourceRegistry(), foreground);
}
org.eclipse.graphiti.mm.algorithms.styles.Color background = gTextStyle.getBackground();
if (background != null) {
textStyle.background = DataTypeTransformation.toSwtColor(
configurationProvider.getResourceRegistry(), background);
}
org.eclipse.graphiti.mm.algorithms.styles.Color underlineColor = gTextStyle.getUnderlineColor();
if (underlineColor != null) {
textStyle.underlineColor = DataTypeTransformation.toSwtColor(
configurationProvider.getResourceRegistry(), underlineColor);
}
org.eclipse.graphiti.mm.algorithms.styles.Color strikeoutColor = gTextStyle.getStrikeoutColor();
if (strikeoutColor != null) {
textStyle.strikeoutColor = DataTypeTransformation.toSwtColor(
configurationProvider.getResourceRegistry(), strikeoutColor);
}
tl.setStyle(textStyle, start, end);
}
g.drawTextLayout(tl, x, y);
tl.dispose();
for (Font font : fontsToDispose) {
font.dispose();
}
} else {
TextLayout tl = new TextLayout(Display.getDefault());
if (mirrored)
tl.setOrientation(SWT.RIGHT_TO_LEFT);
tl.setFont(g.getFont());
tl.setText(draw);
g.drawTextLayout(tl, x, y);
tl.dispose();
}
}
}