/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * 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: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.scenegraph.utils; import java.awt.BasicStroke; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import org.simantics.utils.page.MarginUtils.Margins; /** * Basic utilities for geometric calculations and testing. * * @author J-P Laine * @author Tuukka Lehtonen */ public final class GeometryUtils { /** * @param p the point of distance measure * @param p1 other end of line * @param p2 other end of line * @return distance of p from the line defined by p1 and p2 */ public static double distanceFromLine(Point2D p, Point2D p1, Point2D p2) { return distanceFromLine(p1.getX(), p1.getY(), p2.getX(), p2.getY(), p.getX(), p.getY()); } /** * @param x1 line segment end 1 x * @param y1 line segment end 1 y * @param x2 line segment end 2 x * @param y2 line segment end 2 y * @param px point x * @param py point y * @return */ public static double distanceFromLine(double x1, double y1, double x2, double y2, double px, double py) { // Adjust vectors relative to x1,y1 // x2,y2 becomes relative vector from x1,y1 to end of segment x2 -= x1; y2 -= y1; // px,py becomes relative vector from x1,y1 to test point px -= x1; py -= y1; double dotprod = px * x2 + py * y2; // dotprod is the length of the px,py vector // projected on the x1,y1=>x2,y2 vector times the // length of the x1,y1=>x2,y2 vector double lineLenSq = x2 * x2 + y2 * y2; double lineLen = Math.sqrt(lineLenSq); double projLen = dotprod / lineLen; // Check whether the projection of (px,py) is outside the specified line. if (projLen < 0) { return Math.sqrt(px * px + py * py); } else if (projLen > lineLen) { double dx = px - x2; double dy = py - y2; return Math.sqrt(dx * dx + dy * dy); } return Math.sqrt(px * px + py * py - projLen * projLen); } /** * @param x1 line segment end 1 x * @param y1 line segment end 1 y * @param x2 line segment end 2 x * @param y2 line segment end 2 y * @param px point x * @param py point y * @return */ public static Point2D intersectionToLine(double x1, double y1, double x2, double y2, double px, double py) { double xx2 = x2; double yy2 = y2; // Adjust vectors relative to x1,y1 // x2,y2 becomes relative vector from x1,y1 to end of segment x2 -= x1; y2 -= y1; // px,py becomes relative vector from x1,y1 to test point px -= x1; py -= y1; double dotprod = px * x2 + py * y2; // dotprod is the length of the px,py vector // projected on the x1,y1=>x2,y2 vector times the // length of the x1,y1=>x2,y2 vector double lineLenSq = x2 * x2 + y2 * y2; double lineLen = Math.sqrt(lineLenSq); if (lineLen <= Double.MIN_VALUE) return new Point2D.Double(x1, y1); double projLen = dotprod / lineLen; // Check whether the projection of (px,py) is outside the specified line. if (projLen < 0) { return new Point2D.Double(x1, y1); } else if (projLen > lineLen) { return new Point2D.Double(xx2, yy2); } return new Point2D.Double(x1 + x2/lineLen*projLen, y1 + y2/lineLen*projLen); } /** * Expands margins to a rectangle * @param rect * @param top * @param bottom * @param left * @param right */ public static Rectangle2D expandRectangle(Rectangle2D rect, double top, double bottom, double left, double right) { if (rect==null) throw new IllegalArgumentException("null arg"); rect.setRect( rect.getX() - left, rect.getY() - top, rect.getWidth() + left + right, rect.getHeight() + top + bottom); return rect; } public static Rectangle2D expandRectangle(Rectangle2D rect, double horizontalExpand, double verticalExpand) { return expandRectangle(rect, verticalExpand, verticalExpand, horizontalExpand, horizontalExpand); } public static Rectangle2D expandRectangle(Rectangle2D rect, double evenExpand) { return expandRectangle(rect, evenExpand, evenExpand, evenExpand, evenExpand); } public static BasicStroke scaleStroke(Stroke stroke, float factor) { return scaleAndOffsetStroke(stroke, factor, 0.0f); } public static BasicStroke offsetStroke(Stroke stroke, float offset) { BasicStroke s = (BasicStroke) stroke; float[] dash = s.getDashArray(); if (dash == null) return s; return new BasicStroke( s.getLineWidth(), s.getEndCap(), s.getLineJoin(), s.getMiterLimit(), dash, s.getDashPhase() + offset ); } public static BasicStroke scaleAndOffsetStroke(Stroke stroke, float factor, float offset) { BasicStroke s = (BasicStroke) stroke; float[] dash = s.getDashArray(); if (dash!=null) { assert(factor!=0); dash = scaleArray(factor, dash, new float[dash.length]); } if (dash==null) return new BasicStroke( s.getLineWidth() * factor, s.getEndCap(), s.getLineJoin(), s.getMiterLimit() ); return new BasicStroke( s.getLineWidth() * factor, s.getEndCap(), s.getLineJoin(), s.getMiterLimit(), dash, s.getDashPhase() * factor + offset ); } public static BasicStroke scaleStrokeWidth(Stroke stroke, float factor) { BasicStroke s = (BasicStroke) stroke; return new BasicStroke( s.getLineWidth() * factor, s.getEndCap(), s.getLineJoin(), s.getMiterLimit(), s.getDashArray(), s.getDashPhase() ); } public static BasicStroke scaleAndOffsetStrokeWidth(Stroke stroke, float factor, float widthOffset) { BasicStroke s = (BasicStroke) stroke; return new BasicStroke( s.getLineWidth() * factor + widthOffset, s.getEndCap(), s.getLineJoin(), s.getMiterLimit(), s.getDashArray(), s.getDashPhase() ); } /** * Scales every element in array * @param array * @param factor * @return new scaled array */ public static float[] scaleArray(float factor, float [] array, float [] targetArray) { assert(array!=null); if (targetArray==null) targetArray = new float[array.length]; for (int i=0; i= 0.0) { /* * trace/2 +- sqrt(trace^2 / 4 - determinant) * = (a+b)/2 +- sqrt((a+b)^2 / 4 - a b) * = (a+b)/2 +- sqrt(a^2 / 4 + a b / 2 + b^2 / 4 - a b) * = (a+b)/2 +- sqrt(a^2 / 4 - a b / 2 + b^2 / 4) * = (a+b)/2 +- (a-b)/2 * = a or b * * Thus the formula below calculates the greatest * absolute value of the eigenvalues max(abs(a), abs(b)) */ return Math.abs(trace*0.5) + Math.sqrt(dd); } else { /* * If dd < 0, then the eigenvalues a and b are not real. * Because both trace and determinant are real, a and b * have form: * a = x + i y * b = x - i y * * Then * sqrt(determinant) * = sqrt(a b) * = sqrt(x^2 + y^2), * which is the absolute value of the eigenvalues. */ return Math.sqrt(determinant); } } /** * Get a transform that makes canvas area fully visible. * @param controlArea * @param diagramArea * @param margins margins * @return transform */ public static AffineTransform fitArea(Rectangle2D controlArea, Rectangle2D diagramArea) { double controlAspectRatio = controlArea.getWidth() / controlArea.getHeight(); double canvasAspectRatio = diagramArea.getWidth() / diagramArea.getHeight(); // Control is really wide => center canvas horizontally, match vertically double scale = 1.0; double tx = 0.0; double ty = 0.0; if (controlAspectRatio>canvasAspectRatio) { scale = controlArea.getHeight() / diagramArea.getHeight(); tx = ( controlArea.getWidth() - diagramArea.getWidth() * scale ) / 2; } else // Control is really tall => center canvas vertically, match horizontally { scale = controlArea.getWidth() / diagramArea.getWidth(); ty = ( controlArea.getHeight() - diagramArea.getHeight() * scale ) / 2; } AffineTransform at = new AffineTransform(); at.translate(tx, ty); at.translate(controlArea.getMinX(), controlArea.getMinY()); at.scale(scale, scale); at.translate(-diagramArea.getMinX(), -diagramArea.getMinY()); //System.out.println("FIT TRANSFORM: " + at); return at; } /** * Rotates rectangle. Note, general rotation is not supported. * @param transform * @param rect * @return transformed rectangle */ public static Rectangle2D transformRectangle(AffineTransform transform, Rectangle2D rect) { int type = transform.getType(); if (type == AffineTransform.TYPE_IDENTITY) return new Rectangle2D.Double(rect.getMinX(), rect.getMinY(), rect.getWidth(), rect.getHeight()); if ((type & (AffineTransform.TYPE_GENERAL_ROTATION|AffineTransform.TYPE_GENERAL_TRANSFORM)) == 0) { double x0 = rect.getMinX(); double y0 = rect.getMinY(); double x1 = rect.getMaxX(); double y1 = rect.getMaxY(); double m00 = transform.getScaleX(); double m10 = transform.getShearY(); double m01 = transform.getShearX(); double m11 = transform.getScaleY(); double X0, Y0, X1, Y1; if(m00 > 0.0) { X0 = m00 * x0; X1 = m00 * x1; } else { X1 = m00 * x0; X0 = m00 * x1; } if(m01 > 0.0) { X0 += m01 * y0; X1 += m01 * y1; } else { X1 += m01 * y0; X0 += m01 * y1; } if(m10 > 0.0) { Y0 = m10 * x0; Y1 = m10 * x1; } else { Y1 = m10 * x0; Y0 = m10 * x1; } if(m11 > 0.0) { Y0 += m11 * y0; Y1 += m11 * y1; } else { Y1 += m11 * y0; Y0 += m11 * y1; } return new Rectangle2D.Double(X0+transform.getTranslateX(), Y0+transform.getTranslateY(), X1-X0, Y1-Y0); } // Generic but much slower. return transform.createTransformedShape(rect).getBounds2D(); } public static Rectangle2D transformRectangleInv(AffineTransform transform, Rectangle2D rect) { assert( (transform.getType() & (AffineTransform.TYPE_GENERAL_ROTATION|AffineTransform.TYPE_GENERAL_TRANSFORM)) == 0); double[] mx = new double[6]; transform.getMatrix(mx); double x0 = rect.getMinX() - mx[4]; double y0 = rect.getMinY() - mx[5]; double x1 = rect.getMaxX() - mx[4]; double y1 = rect.getMaxY() - mx[5]; double det = mx[0]*mx[3] - mx[1]*mx[2]; double m00 = mx[3] / det; double m10 = -mx[1] / det; double m01 = -mx[2] / det; double m11 = mx[0] / det; double X0, Y0, X1, Y1; if(m00 > 0.0) { X0 = m00 * x0; X1 = m00 * x1; } else { X1 = m00 * x0; X0 = m00 * x1; } if(m01 > 0.0) { X0 += m01 * y0; X1 += m01 * y1; } else { X1 += m01 * y0; X0 += m01 * y1; } if(m10 > 0.0) { Y0 = m10 * x0; Y1 = m10 * x1; } else { Y1 = m10 * x0; Y0 = m10 * x1; } if(m11 > 0.0) { Y0 += m11 * y0; Y1 += m11 * y1; } else { Y1 += m11 * y0; Y0 += m11 * y1; } return new Rectangle2D.Double(X0, Y0, X1-X0, Y1-Y0); } /** * @param points * @return millimeters */ public static float pointToMillimeter(float points) { return points * 25.4f / 72.0f; } /** * @param points * @return millimeters */ public static double pointToMillimeter(double points) { return points * 25.4 / 72.0; } /** * Get a transform that makes canvas area fully visible. * @param controlArea * @param diagramArea * @param margins margins * @return transform */ public static AffineTransform fitArea(Rectangle2D controlArea, Rectangle2D diagramArea, Margins margins) { diagramArea = expandRectangle(diagramArea, margins.top.diagramAbsolute, margins.bottom.diagramAbsolute, margins.left.diagramAbsolute, margins.right.diagramAbsolute); controlArea = expandRectangle(controlArea, -margins.top.controlAbsolute - margins.top.controlRelative * controlArea.getHeight(), -margins.bottom.controlAbsolute - margins.bottom.controlRelative * controlArea.getHeight(), -margins.left.controlAbsolute - margins.left.controlRelative * controlArea.getWidth(), -margins.right.controlAbsolute - margins.right.controlRelative * controlArea.getWidth()); double controlAspectRatio = controlArea.getWidth() / controlArea.getHeight(); double canvasAspectRatio = diagramArea.getWidth() / diagramArea.getHeight(); // Control is really wide => center canvas horizontally, match vertically double scale = 1.0; double tx = 0.0; double ty = 0.0; if (controlAspectRatio>canvasAspectRatio) { scale = controlArea.getHeight() / diagramArea.getHeight(); tx = ( controlArea.getWidth() - diagramArea.getWidth() * scale ) / 2; } else // Control is really tall => center canvas vertically, match horizontally { scale = controlArea.getWidth() / diagramArea.getWidth(); ty = ( controlArea.getHeight() - diagramArea.getHeight() * scale ) / 2; } AffineTransform at = new AffineTransform(); at.translate(tx, ty); at.translate(controlArea.getMinX(), controlArea.getMinY()); at.scale(scale, scale); at.translate(-diagramArea.getMinX(), -diagramArea.getMinY()); //System.out.println("FIT TRANSFORM: " + at); return at; } private static class UndefinedRectangle extends Rectangle2D { @Override public void setRect(double x, double y, double w, double h) { throw new UnsupportedOperationException("UndefinedRectangle is immutable"); } @Override public int outcode(double x, double y) { return OUT_LEFT | OUT_TOP | OUT_RIGHT | OUT_BOTTOM; } private Rectangle2D copy(Rectangle2D r) { Rectangle2D dest; if (r instanceof Float) { dest = new Rectangle2D.Float(); } else { dest = new Rectangle2D.Double(); } dest.setFrame(r); return dest; } @Override public Rectangle2D createIntersection(Rectangle2D r) { return copy(r); } @Override public Rectangle2D createUnion(Rectangle2D r) { return copy(r); } @Override public double getX() { return java.lang.Double.NaN; } @Override public double getY() { return java.lang.Double.NaN; } @Override public double getWidth() { return 0; } @Override public double getHeight() { return 0; } @Override public boolean isEmpty() { return true; } } private static final UndefinedRectangle UNDEFINED_RECTANGLE = new UndefinedRectangle(); public static Rectangle2D undefinedRectangle() { return UNDEFINED_RECTANGLE; } public static boolean isUndefinedRectangle(Rectangle2D r) { return r == UNDEFINED_RECTANGLE || (r.isEmpty() && Double.isNaN(r.getX()) && Double.isNaN(r.getY())); } }