/* * @(#)GridConstrainer.java * * Copyright (c) 1996-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * 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 $Id$ */ public class GridConstrainer extends AbstractConstrainer { private static final long serialVersionUID = 1L; /** * 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. */ @Override public Point2D.Double constrainPoint(Point2D.Double p, Figure ... figure) { 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, Figure ... figure) { Point2D.Double p0 = constrainPoint((Point2D.Double) p.clone(), figure); 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. */ @Override public Point2D.Double translatePoint(Point2D.Double p, TranslationDirection dir, Figure ... figure) { Point2D.Double p0 = constrainPoint((Point2D.Double) p.clone(), figure); 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; } @Override public Rectangle2D.Double constrainRectangle(Rectangle2D.Double r, Figure ... figure) { Point2D.Double p0 = constrainPoint(new Point2D.Double(r.x, r.y), figure); Point2D.Double p1 = constrainPoint(new Point2D.Double(r.x + r.width, r.y + r.height), figure); 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, Figure ... figure) { Point2D.Double p0 = new Point2D.Double(r.x, r.y); switch (dir) { case NORTH: case NORTH_WEST: case WEST: constrainPoint(p0, dir, figure); break; case EAST: case NORTH_EAST: p0.x += r.width; constrainPoint(p0, dir, figure); p0.x -= r.width; break; case SOUTH: case SOUTH_WEST: p0.y += r.height; constrainPoint(p0, dir, figure); p0.y -= r.height; break; case SOUTH_EAST: p0.y += r.height; p0.x += r.width; constrainPoint(p0, dir, figure); p0.y -= r.height; p0.x -= r.width; break; } r.x = p0.x; r.y = p0.y; return r; } @Override public Rectangle2D.Double translateRectangle(Rectangle2D.Double r, TranslationDirection dir, Figure ... figure) { double x = r.x; double y = r.y; constrainRectangle(r, dir, figure); 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; } @Override 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(); } @Override 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, viewBounds.y, (int) viewPoint.x, 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, viewBounds.y, (int) viewPoint.x, 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(viewBounds.x, (int) viewPoint.y, 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(viewBounds.x, (int) viewPoint.y, viewBounds.x + viewBounds.width, (int) viewPoint.y); } } } } } @Override public double constrainAngle(double angle, Figure ... figure) { // No step specified then no constraining if (theta == 0) { return angle; } double factor = Math.round(angle / theta); return theta * factor; } @Override public double rotateAngle(double angle, RotationDirection dir, Figure ... figure) { // Check parameters if (dir == null) { throw new IllegalArgumentException("dir must not be null"); } // Rotate into the specified direction by theta angle = constrainAngle(angle, figure); switch (dir) { case CLOCKWISE : angle += theta; break; case COUNTER_CLOCKWISE : default: angle -= theta; break; } return angle; } }