/**
 * Copyright 2010, 2011 Jingjing Li.
 *
 * This file is part of jplot2d.
 *
 * jplot2d is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 *
 * jplot2d is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with jplot2d. If not, see .
 */
package org.jplot2d.util;

import static org.jplot2d.util.NumberUtils.approximate;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;

/**
 * The LineHatchPaint class provides a way to fill a Shape with line hatch pattern. Because Java
 * require instances of classes implementing Paint must be read-only, this class is more like a
 * factory to create paint instance for renderer threads.
 * 
 * @author Jingjing Li
 */
public class LineHatchPaint implements Paint {

	private static final double EPSILON = Math.pow(10, -14);

	/**
	 * ascend X
	 */
	public static final class PointComparatorX implements Comparator {

		private static Comparator instance = new PointComparatorX();

		public static Comparator getInstance() {
			return instance;
		}

		public int compare(Point2D p1, Point2D p2) {
			double d = p1.getX() - p2.getX();
			if (d > 0) {
				return 1;
			} else if (d < 0) {
				return -1;
			} else {
				return 0;
			}
		}

	};

	/**
	 * descend X
	 */
	public static final class PointComparatorNX implements Comparator {

		private static Comparator instance = new PointComparatorNX();

		public static Comparator getInstance() {
			return instance;
		}

		public int compare(Point2D p1, Point2D p2) {
			double d = p1.getX() - p2.getX();
			if (d > 0) {
				return -1;
			} else if (d < 0) {
				return 1;
			} else {
				return 0;
			}
		}

	};

	/**
	 * ascend Y
	 */
	public static final class PointComparatorY implements Comparator {

		private static Comparator instance = new PointComparatorY();

		public static Comparator getInstance() {
			return instance;
		}

		public int compare(Point2D p1, Point2D p2) {
			double d = p1.getY() - p2.getY();
			if (d > 0) {
				return 1;
			} else if (d < 0) {
				return -1;
			} else {
				return 0;
			}
		}

	};

	/**
	 * descend Y
	 */
	public static final class PointComparatorNY implements Comparator {

		private static Comparator instance = new PointComparatorNY();

		public static Comparator getInstance() {
			return instance;
		}

		public int compare(Point2D p1, Point2D p2) {
			double d = p1.getY() - p2.getY();
			if (d > 0) {
				return -1;
			} else if (d < 0) {
				return 1;
			} else {
				return 0;
			}
		}

	};

	private static final Color DEFAULT_COLOR = Color.BLACK;

	private final Color color;

	private final BasicStroke stroke;

	private final double angle;

	private final double spacing;

	private final Comparator pointComparator;

	/**
	 * Construct a LineHatchPaint with the given angle. The default values:
	 * 
    *
  • color: BLACK
  • *
  • line width: 0.0
  • *
  • line style: solid
  • *
  • line spacing: 6pt
  • *
* * @param angle * the counterclockwise angle in degrees from horizontal */ public LineHatchPaint(double angle) { this(0, angle, 6); } /** * Construct a LineHatchPaint with the given angle. The default values: *
    *
  • color: BLACK
  • *
  • line style: solid
  • *
* * @param width * the line width * @param angle * the counterclockwise angle in degrees from horizontal * @param spacing * the spacing, in point(1/72 inch), between the parallel lines */ public LineHatchPaint(float width, double angle, double spacing) { this(DEFAULT_COLOR, new BasicStroke(width), angle, spacing); } /** * Construct a LineHatchPaint with given arguments. * * @param width * the line width * @param dash * the dash array. null means solid line * @param angle * the counterclockwise angle in degrees from horizontal * @param spacing * the spacing, in point(1/72 inch), between the parallel lines */ public LineHatchPaint(Color color, BasicStroke stroke, double angle, double spacing) { this.color = color; this.stroke = stroke; this.angle = angle; this.spacing = spacing; angle = angle % 360; if (angle < 0) { angle += 360; } if (90 - 45 < angle && angle < 90 + 45) { pointComparator = PointComparatorNY.getInstance(); } else if (270 - 45 < angle && angle < 270 + 45) { pointComparator = PointComparatorY.getInstance(); } else if (180 - 45 <= angle && angle <= 180 + 45) { pointComparator = PointComparatorNX.getInstance(); } else { pointComparator = PointComparatorX.getInstance(); } } public Color getColor() { return color; } public BasicStroke getStroke() { return stroke; } public double getAngle() { return angle; } public double getSpacing() { return spacing; } public PaintContext createContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints) { throw new UnsupportedOperationException(); } public int getTransparency() { throw new UnsupportedOperationException(); } /** * Calculate hatch shapes to fill the given shape. * * @param shape * the shape to hatch * @param clip * the clip * @param scale * @return hatch shapes */ public Shape[] calcHatchShapes(Shape shape, Rectangle2D clip, double scale) { Line2D[] clippedLines = calcHatchLinesClipped(this, clip, scale); ArrayList result = new ArrayList(); for (Line2D line : clippedLines) { Line2D[] ilines = calcLineSegInside(line, shape); result.addAll(Arrays.asList(ilines)); } return result.toArray(new Shape[result.size()]); } public static Line2D[] calcHatchLinesClipped(LineHatchPaint lhp, Rectangle2D clip, double scale) { // the angle to positive x axis or negative x axis double anglex = lhp.getAngle() % 180.0; if (anglex > 90) { anglex = 180 - anglex; } else if (anglex < -90) { anglex += 180; } else if (anglex < -0) { anglex = -anglex; } anglex *= Math.PI / 180; double diagonalLength = Math.hypot(clip.getHeight(), clip.getWidth()); double diagonalAngle = Math.atan(clip.getHeight() / clip.getWidth()); double halfLength = diagonalLength / 2 * Math.cos(anglex - diagonalAngle); double halfRange = diagonalLength / 2 * Math.sin(anglex + diagonalAngle); double spacing = lhp.getSpacing() * scale; int halfn = (int) (halfRange / spacing); Line2D[] result = new Line2D[2 * halfn + 1]; // angle is positive x toward positive y (downward in graphic coordinate system) double angle = -lhp.getAngle() * Math.PI / 180; double a0x = clip.getCenterX() - halfLength * Math.cos(angle); double a0y = clip.getCenterY() - halfLength * Math.sin(angle); double b0x = clip.getCenterX() + halfLength * Math.cos(angle); double b0y = clip.getCenterY() + halfLength * Math.sin(angle); for (int i = 0; i < result.length; i++) { double aix = a0x - (i - halfn) * spacing * Math.sin(angle); double aiy = a0y + (i - halfn) * spacing * Math.cos(angle); double bix = b0x - (i - halfn) * spacing * Math.sin(angle); double biy = b0y + (i - halfn) * spacing * Math.cos(angle); result[i] = new Line2D.Double(aix, aiy, bix, biy); } return result; } private Line2D[] calcLineSegInside(Line2D line, Shape shape) { // all intersect points HashSet inersects = new HashSet(); inersects.add(line.getP1()); inersects.add(line.getP2()); double segCoords[] = new double[6]; double inersectCoords[] = new double[2]; double lastMoveX = 0.0, prex = 0.0; double lastMoveY = 0.0, prey = 0.0; for (PathIterator pi = shape.getPathIterator(null); !pi.isDone(); pi.next()) { int segType = pi.currentSegment(segCoords); switch (segType) { case PathIterator.SEG_MOVETO: lastMoveX = prex = segCoords[0]; lastMoveY = prey = segCoords[1]; break; case PathIterator.SEG_LINETO: if ((segCoords[0] != lastMoveX) || (segCoords[1] != lastMoveY)) { boolean its = intersectLines(line, prex, prey, segCoords[0], segCoords[1], inersectCoords); if (its) { inersects.add(new Point2D.Double(inersectCoords[0], inersectCoords[1])); } prex = segCoords[0]; prey = segCoords[1]; } break; case PathIterator.SEG_QUADTO: boolean itqs = intersectLines(line, prex, prey, segCoords[2], segCoords[3], inersectCoords); if (itqs) { inersects.add(new Point2D.Double(inersectCoords[0], inersectCoords[1])); } prex = segCoords[2]; prey = segCoords[3]; break; case PathIterator.SEG_CUBICTO: boolean itcs = intersectLines(line, prex, prey, segCoords[4], segCoords[5], inersectCoords); if (itcs) { inersects.add(new Point2D.Double(inersectCoords[0], inersectCoords[1])); } prex = segCoords[4]; prey = segCoords[5]; break; case PathIterator.SEG_CLOSE: if ((prex != lastMoveX) || (prey != lastMoveY)) { boolean its = intersectLines(line, prex, prey, lastMoveX, lastMoveY, inersectCoords); if (its) { inersects.add(new Point2D.Double(inersectCoords[0], inersectCoords[1])); } } break; } } Point2D[] ips = inersects.toArray(new Point2D[inersects.size()]); Arrays.sort(ips, pointComparator); List result = new ArrayList(); for (int i = 0, j = 1; j < ips.length; i++, j++) { double x = (ips[i].getX() + ips[j].getX()) / 2; double y = (ips[i].getY() + ips[j].getY()) / 2; if (shape.contains(x, y)) { result.add(new Line2D.Double(ips[i], ips[j])); } } return result.toArray(new Line2D[result.size()]); } /** * The method checks up if line and line (x3, y3) - (x4, y4) intersect. If lines intersect then * the result parameters are saved to point array. The size of array point must be at least 2. * Note the algorithm is not symmetric. The given line is the primary line. * * @param line * the primary line * @param x3 * @param y3 * @param x4 * @param y4 * @param point * @return true if two lines intersect */ public static boolean intersectLines(Line2D line, double x3, double y3, double x4, double y4, double[] point) { double dxa = line.getX1() - line.getX2(); double dya = line.getY1() - line.getY2(); double dxb = x3 - x4; double dyb = y3 - y4; double ca = line.getX1() * line.getY2() - line.getY1() * line.getX2(); double cb = x3 * y4 - y3 * x4; double coefParallel = dxa * dyb - dya * dxb; // point on primary line if (approximate(x3, x4, 4) && approximate(y3, y4, 4)) { double x = (x3 + x4) / 2; double y = (y3 + y4) / 2; if (approximate(dxa * y - dya * x, ca, 4)) { point[0] = x; point[1] = y; return true; } } // parallel if (Math.abs(coefParallel) < EPSILON) { return false; } // vertical if (approximate(line.getX1(), line.getX2(), 4)) { point[0] = (line.getX1() + line.getX2()) / 2; point[1] = (point[0] * dyb + cb) / dxb; return approxBetween(line.getY1(), line.getY2(), point[1]) && approxBetween(x3, x4, point[0]); } // horizontal if (approximate(line.getY1(), line.getY2(), 4)) { point[1] = (line.getY1() + line.getY2()) / 2; point[0] = (point[1] * dxb - cb) / dyb; return approxBetween(line.getX1(), line.getX2(), point[0]) && approxBetween(y3, y4, point[1]); } point[0] = (ca * dxb - dxa * cb) / coefParallel; point[1] = (ca * dyb - dya * cb) / coefParallel; boolean inlineA = false, inlineB = false; if (Math.abs(dxa) > Math.abs(dya)) { inlineA = approxBetween(line.getX1(), line.getX2(), point[0]); } else { inlineA = approxBetween(line.getY1(), line.getY2(), point[1]); } if (Math.abs(dxb) > Math.abs(dyb)) { inlineB = approxBetween(x3, x4, point[0]); } else { inlineB = approxBetween(y3, y4, point[1]); } return inlineA && inlineB; } public static boolean approxBetween(double a, double b, double v) { if (a > b) { if (a >= v && v >= b) { return true; } } else { if (a <= v && v <= b) { return true; } } if (approximate(a, v, 4) || approximate(b, v, 4)) { return true; } return false; } }

Ads help maintain this website.