/******************************************************************************* * Copyright (c) 2006-2012 * Software Technology Group, Dresden University of Technology * DevBoost GmbH, Berlin, Amtsgericht Charlottenburg, HRB 140026 * * 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: * Software Technology Group - TU Dresden, Germany; * DevBoost GmbH - Berlin, Germany * - initial API and implementation ******************************************************************************/ /* * @(#)GridConstrainer.java 4.0 2007-12-17 * * Copyright (c) 1996-2007 by the original authors of JHotDraw * and all its contributors. * All rights reserved. * * The copyright of this software is owned by the authors and * contributors of the JHotDraw project ("the copyright holders"). * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * the copyright holders. For details see accompanying license terms. */ package org.jhotdraw.draw; import java.awt.*; import java.awt.geom.*; /** * Constrains a point such that it falls on a grid. * * @author Werner Randelshofer * @version 4.0 2007-12-17 Huw Jones, Werner Randelshofer: Adapted due to changes * in Constrainer interface. * <br>3.1 2007-09-15 Werner Randelshofer: Added constructor which allows to * control the visiblity of the grid. * <br>3.0 2007-08-01 Werner Randelshofer: Reworked. * <br>2.1.1 2006-07-05 Werner Randelshofer: Fixed drawing bug. * <br>2.1 2006-07-03 Werner Randelshofer: Method isVisible added. * <br>2.0 2006-01-14 Werner Randelshofer: Changed to support double precision coordinates. * <br>1.0 2004-03-17 Werner Randelshofer: Created. */ public class GridConstrainer extends AbstractConstrainer { /** * The width of a minor grid cell. * The value 0 turns the constrainer off for the horizontal axis. */ private double width; /** * The height of a minor grid cell. * The value 0 turns the constrainer off for the vertical axis. */ private double height; /** * The theta for constrained rotations on the grid. * The value 0 turns the constrainer off for rotations. */ private double theta; /** * If this variable is true, the grid is drawn. * Note: Grid cells are only drawn, if they are at least two pixels apart * on the view coordinate system. */ private boolean isVisible; /** * The color for minor grid cells. */ private static Color minorColor = new Color(0xebebeb); /** * The color for major grid cells. */ private static Color majorColor = new Color(0xcacaca); /** * The spacing factor for a major grid cell. */ private int majorGridSpacing = 5; /** * Creates a new instance with a grid of 1x1. */ public GridConstrainer() { this(1d, 1d, 0d, false); } /** * Creates a new instance with the specified grid size, * and by 11.25° (in degrees) for rotations. * The grid is visible. * * @param width The width of a grid cell. * @param height The height of a grid cell. */ public GridConstrainer(double width, double height) { this(width, height, Math.PI / 8d, true); } /** * Creates a new instance with the specified grid size. * and by 11.25° (in degrees) for rotations. * * @param width The width of a grid cell. * @param height The height of a grid cell. * @param visible Wether the grid is visible or not. */ public GridConstrainer(double width, double height, boolean visible) { this(width, height, Math.PI / 8d, visible); } /** * Creates a new instance with the specified grid size. * * @param width The width of a grid cell. * @param height The height of a grid cell. * @param theta The theta for rotations in radians. * @param visible Wether the grid is visible or not. */ public GridConstrainer(double width, double height, double theta, boolean visible) { if (width <= 0 || height <= 0) { throw new IllegalArgumentException("Width or height is <= 0"); } this.width = width; this.height = height; this.theta = theta; this.isVisible = visible; } public double getWidth() { return width; } public double getHeight() { return height; } public double getTheta() { return theta; } public void setWidth(double newValue) { double oldValue = width; width = newValue; firePropertyChange("width", oldValue, newValue); fireStateChanged(); } public void setHeight(double newValue) { double oldValue = height; height = newValue; firePropertyChange("height", oldValue, newValue); fireStateChanged(); } public void setTheta(double newValue) { double oldValue = theta; theta = newValue; firePropertyChange("theta", oldValue, newValue); fireStateChanged(); } /** * Constrains a point to the closest grid point in any direction. */ public Point2D.Double constrainPoint(Point2D.Double p) { p.x = Math.round(p.x / width) * width; p.y = Math.round(p.y / height) * height; return p; } /** * Constrains the placement of a point towards a direction. * <p> * This method changes the point which is passed as a parameter. * * @param p A point on the drawing. * @param dir A direction. * @return Returns the constrained point. */ protected Point2D.Double constrainPoint(Point2D.Double p, TranslationDirection dir) { Point2D.Double p0 = constrainPoint((Point2D.Double) p.clone()); switch (dir) { case NORTH: case NORTH_WEST: case NORTH_EAST: if (p0.y < p.y) { p.y = p0.y; } else if (p0.y > p.y) { p.y = p0.y - height; } break; case SOUTH: case SOUTH_WEST: case SOUTH_EAST: if (p0.y < p.y) { p.y = p0.y + height; } else if (p0.y > p.y) { p.y = p0.y; } break; } switch (dir) { case WEST: case NORTH_WEST: case SOUTH_WEST: if (p0.x < p.x) { p.x = p0.x; } else if (p0.x > p.x) { p.x = p0.x - width; } break; case EAST: case NORTH_EAST: case SOUTH_EAST: if (p0.x < p.x) { p.x = p0.x + width; } else if (p0.x > p.x) { p.x = p0.x; } break; } return p; } /** * Moves a point to the closest grid point in a direction. */ public Point2D.Double translatePoint(Point2D.Double p, TranslationDirection dir) { Point2D.Double p0 = constrainPoint((Point2D.Double) p.clone()); switch (dir) { case NORTH: case NORTH_WEST: case NORTH_EAST: p.y = p0.y - height; break; case SOUTH: case SOUTH_WEST: case SOUTH_EAST: p.y = p0.y + height; break; } switch (dir) { case WEST: case NORTH_WEST: case SOUTH_WEST: p.x = p0.x - width; break; case EAST: case NORTH_EAST: case SOUTH_EAST: p.x = p0.x + width; break; } return p; } public Rectangle2D.Double constrainRectangle(Rectangle2D.Double r) { Point2D.Double p0 = constrainPoint(new Point2D.Double(r.x, r.y)); Point2D.Double p1 = constrainPoint(new Point2D.Double(r.x + r.width, r.y + r.height)); if (Math.abs(p0.x - r.x) < Math.abs(p1.x - r.x - r.width)) { r.x = p0.x; } else { r.x = p1.x - r.width; } if (Math.abs(p0.y - r.y) < Math.abs(p1.y - r.y - r.height)) { r.y = p0.y; } else { r.y = p1.y - r.height; } return r; } /** * Constrains the placement of a rectangle towards a direction. * <p> * This method changes the location of the rectangle which is passed as a * parameter. This method does not change the size of the rectangle. * * @param r A rectangle on the drawing. * @param dir A direction. * @return Returns the constrained rectangle. */ protected Rectangle2D.Double constrainRectangle(Rectangle2D.Double r, TranslationDirection dir) { Point2D.Double p0 = new Point2D.Double(r.x, r.y); switch (dir) { case NORTH: case NORTH_WEST: case WEST: constrainPoint(p0, dir); break; case EAST: case NORTH_EAST: p0.x += r.width; constrainPoint(p0, dir); p0.x -= r.width; break; case SOUTH: case SOUTH_WEST: p0.y += r.height; constrainPoint(p0, dir); p0.y -= r.height; break; case SOUTH_EAST: p0.y += r.height; p0.x += r.width; constrainPoint(p0, dir); p0.y -= r.height; p0.x -= r.width; break; } r.x = p0.x; r.y = p0.y; return r; } public Rectangle2D.Double translateRectangle(Rectangle2D.Double r, TranslationDirection dir) { double x = r.x; double y = r.y; constrainRectangle(r, dir); switch (dir) { case NORTH: case NORTH_WEST: case NORTH_EAST: if (y == r.y) { r.y -= height; } break; case SOUTH: case SOUTH_WEST: case SOUTH_EAST: if (y == r.y) { r.y += height; } break; } switch (dir) { case WEST: case NORTH_WEST: case SOUTH_WEST: if (x == r.x) { r.x -= width; } break; case EAST: case NORTH_EAST: case SOUTH_EAST: if (x == r.x) { r.x += width; } break; } return r; } public String toString() { return super.toString() + "[" + width + "," + height + "]"; } public boolean isVisible() { return isVisible; } public void setVisible(boolean newValue) { boolean oldValue = isVisible; isVisible = newValue; firePropertyChange("visible", oldValue, newValue); fireStateChanged(); } /** * Spacing between major grid lines. */ public int getMajorGridSpacing() { return majorGridSpacing; } /** * Spacing between major grid lines. */ public void setMajorGridSpacing(int newValue) { int oldValue = majorGridSpacing; majorGridSpacing = newValue; firePropertyChange("majorGridSpacing", oldValue, newValue); fireStateChanged(); } public void draw(Graphics2D g, DrawingView view) { if (isVisible) { AffineTransform t = view.getDrawingToViewTransform(); Rectangle viewBounds = g.getClipBounds(); Rectangle2D.Double bounds = view.viewToDrawing(viewBounds); Point2D.Double origin = constrainPoint(new Point2D.Double(bounds.x, bounds.y)); Point2D.Double point = new Point2D.Double(); Point2D.Double viewPoint = new Point2D.Double(); // vertical grid lines are only drawn, if they are at least two // pixels apart on the view coordinate system. if (width * view.getScaleFactor() > 2) { g.setColor(minorColor); for (int i = (int) (origin.x / width), m = (int) ((origin.x + bounds.width) / width) + 1; i <= m; i++) { g.setColor((i % majorGridSpacing == 0) ? majorColor : minorColor); point.x = width * i; t.transform(point, viewPoint); g.drawLine((int) viewPoint.x, (int) viewBounds.y, (int) viewPoint.x, (int) (viewBounds.y + viewBounds.height)); } } else if (width * majorGridSpacing * view.getScaleFactor() > 2) { g.setColor(majorColor); for (int i = (int) (origin.x / width), m = (int) ((origin.x + bounds.width) / width) + 1; i <= m; i++) { if (i % majorGridSpacing == 0) { point.x = width * i; t.transform(point, viewPoint); g.drawLine((int) viewPoint.x, (int) viewBounds.y, (int) viewPoint.x, (int) (viewBounds.y + viewBounds.height)); } } } // horizontal grid lines are only drawn, if they are at least two // pixels apart on the view coordinate system. if (height * view.getScaleFactor() > 2) { g.setColor(minorColor); for (int i = (int) (origin.y / height), m = (int) ((origin.y + bounds.height) / height) + 1; i <= m; i++) { g.setColor((i % majorGridSpacing == 0) ? majorColor : minorColor); point.y = height * i; t.transform(point, viewPoint); g.drawLine((int) viewBounds.x, (int) viewPoint.y, (int) (viewBounds.x + viewBounds.width), (int) viewPoint.y); } } else if (height * majorGridSpacing * view.getScaleFactor() > 2) { g.setColor(majorColor); for (int i = (int) (origin.y / height), m = (int) ((origin.y + bounds.height) / height) + 1; i <= m; i++) { if (i % majorGridSpacing == 0) { point.y = height * i; t.transform(point, viewPoint); g.drawLine((int) viewBounds.x, (int) viewPoint.y, (int) (viewBounds.x + viewBounds.width), (int) viewPoint.y); } } } } } public double constrainAngle(double angle) { // No step specified then no constraining if (theta == 0) { return angle; } double factor = Math.round(angle / theta); return theta * factor; } public double rotateAngle(double angle, RotationDirection dir) { // Check parameters if (dir == null) { throw new IllegalArgumentException("dir must not be null"); } // Rotate into the specified direction by theta angle = constrainAngle(angle); switch (dir) { case CLOCKWISE : angle += theta; break; case COUNTER_CLOCKWISE : default: angle -= theta; break; } return angle; } }