/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library 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; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotools.geometry.jts; import java.awt.Rectangle; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * A thin wrapper that adapts a JTS geometry to the Shape interface so that the * geometry can be used by java2d without coordinate cloning. * <p> * This implementation supports the use of MathTransform and can be constructed * with a Decimation stratagy object (allowing you to fine tune the process by * which a Geometry is simplified into a Shape). * </p> * <p> * This implementation is very careful about cloning; and has the ability to * go faster when you are using a LiteCoordinateSequenceFactory behind your * geometry classes. * </p> * @author Jesse Eichar * * @source $URL$ * @version $Id$ */ public final class LiteShape2 implements Shape, Cloneable { /** The wrapped JTS geometry */ private Geometry geometry; private boolean generalize = false; private double maxDistance = 1; private static GeometryFactory geomFac; /** transform from dataspace to screenspace */ private MathTransform mathTransform; /** * Creates a new LiteShape object. * * @param geom - * the wrapped geometry * @param mathTransform - * the transformation applied to the geometry in order to get to * the shape points * @param decimator - * * @param generalize - * set to true if the geometry need to be generalized during * rendering * @param maxDistance - * distance used in the generalization process * @throws TransformException * @throws FactoryException */ public LiteShape2(Geometry geom, MathTransform mathTransform, Decimator decimator, boolean generalize, double maxDistance) throws TransformException, FactoryException { this(geom, mathTransform, decimator, generalize); this.maxDistance = maxDistance; } /** * Creates a new LiteShape object. * * @param geom - * the wrapped geometry * @param mathTransform - * the transformation applied to the geometry in order to get to * the shape points * @param decimator - * * @param generalize - * set to true if the geometry need to be generalized during * rendering * * @throws TransformException * @throws FactoryException */ public LiteShape2(Geometry geom, MathTransform mathTransform, Decimator decimator, boolean generalize) throws TransformException, FactoryException { this(geom, mathTransform, decimator, generalize, true); } /** * Creates a new LiteShape object. * * @param geom - * the wrapped geometry * @param mathTransform - * the transformation applied to the geometry in order to get to * the shape points * @param decimator - * * @param generalize - * set to true if the geometry need to be generalized during * rendering * * @param clone - if clone is false the original geometry may be modified directly, if true it will be * cloned to make sure the original remains untouched * * @throws TransformException * @throws FactoryException */ public LiteShape2(Geometry geom, MathTransform mathTransform, Decimator decimator, boolean generalize, boolean clone) throws TransformException, FactoryException { if (geom != null) { if(!clone && geom.getFactory().getCoordinateSequenceFactory() instanceof LiteCoordinateSequenceFactory) this.geometry = geom; else this.geometry = LiteCoordinateSequence.cloneGeometry(geom); } this.mathTransform = mathTransform; if (decimator != null) { decimator.decimateTransformGeneralize(this.geometry,this.mathTransform); this.geometry.geometryChanged(); } else { // if we have a transform a decimation span can be detected, so try to decimate anyways if (mathTransform != null && !mathTransform.isIdentity() && generalize && geometry != null) { new Decimator(mathTransform.inverse()).decimate(this.geometry); this.geometry.geometryChanged(); } if (geometry != null) { transformGeometry(geometry); this.geometry.geometryChanged(); } } this.generalize = false; } private void transformGeometry(Geometry geometry) throws TransformException, FactoryException { if (mathTransform == null || mathTransform.isIdentity()) return; if (geometry instanceof GeometryCollection) { GeometryCollection collection = (GeometryCollection) geometry; for (int i = 0; i < collection.getNumGeometries(); i++) { transformGeometry(collection.getGeometryN(i)); } } else if (geometry instanceof Point) { LiteCoordinateSequence seq = (LiteCoordinateSequence) ((Point) geometry) .getCoordinateSequence(); double[] coords = seq.getArray(); double[] newCoords = new double[coords.length]; mathTransform.transform(coords, 0, newCoords, 0, seq.size()); seq.setArray(newCoords); } else if (geometry instanceof Polygon) { Polygon polygon = (Polygon) geometry; transformGeometry(polygon.getExteriorRing()); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { transformGeometry(polygon.getInteriorRingN(i)); } } else if (geometry instanceof LineString) { LiteCoordinateSequence seq = (LiteCoordinateSequence) ((LineString) geometry) .getCoordinateSequence(); double[] coords = seq.getArray(); mathTransform.transform(coords, 0, coords, 0, seq.size()); seq.setArray(coords); } } private GeometryFactory getGeometryFactory() { if (geomFac == null) { geomFac = new GeometryFactory(new LiteCoordinateSequenceFactory()); } return geomFac; } /** * Sets the geometry contained in this lite shape. Convenient to reuse this * object instead of creating it again and again during rendering * * @param g * @throws TransformException * @throws FactoryException */ public void setGeometry(Geometry g) throws TransformException, FactoryException { if (g != null) { this.geometry = getGeometryFactory().createGeometry(g); transformGeometry(geometry); } } /** * Tests if the interior of the <code>Shape</code> entirely contains the * specified <code>Rectangle2D</code>. This method might conservatively * return <code>false</code> when: * * <ul> * <li>the <code>intersect</code> method returns <code>true</code> and * </li> * <li>the calculations to determine whether or not the <code>Shape</code> * entirely contains the <code>Rectangle2D</code> are prohibitively * expensive.</li> * </ul> * * This means that this method might return <code>false</code> even though * the <code>Shape</code> contains the <code>Rectangle2D</code>. The * <code>Area</code> class can be used to perform more accurate * computations of geometric intersection for any <code>Shape</code> * object if a more precise answer is required. * * @param r * The specified <code>Rectangle2D</code> * * @return <code>true</code> if the interior of the <code>Shape</code> * entirely contains the <code>Rectangle2D</code>; * <code>false</code> otherwise or, if the <code>Shape</code> * contains the <code>Rectangle2D</code> and the * <code>intersects</code> method returns <code>true</code> and * the containment calculations would be too expensive to perform. * * @see #contains(double, double, double, double) */ public boolean contains(Rectangle2D r) { Geometry rect = rectangleToGeometry(r); return geometry.contains(rect); } /** * Tests if a specified {@link Point2D}is inside the boundary of the * <code>Shape</code>. * * @param p * a specified <code>Point2D</code> * * @return <code>true</code> if the specified <code>Point2D</code> is * inside the boundary of the <code>Shape</code>; * <code>false</code> otherwise. */ public boolean contains(Point2D p) { Coordinate coord = new Coordinate(p.getX(), p.getY()); Geometry point = geometry.getFactory().createPoint(coord); return geometry.contains(point); } /** * Tests if the specified coordinates are inside the boundary of the * <code>Shape</code>. * * @param x * the specified coordinates, x value * @param y * the specified coordinates, y value * * @return <code>true</code> if the specified coordinates are inside the * <code>Shape</code> boundary; <code>false</code> otherwise. */ public boolean contains(double x, double y) { Coordinate coord = new Coordinate(x, y); Geometry point = geometry.getFactory().createPoint(coord); return geometry.contains(point); } /** * Tests if the interior of the <code>Shape</code> entirely contains the * specified rectangular area. All coordinates that lie inside the * rectangular area must lie within the <code>Shape</code> for the entire * rectanglar area to be considered contained within the <code>Shape</code>. * * <p> * This method might conservatively return <code>false</code> when: * * <ul> * <li>the <code>intersect</code> method returns <code>true</code> and * </li> * <li>the calculations to determine whether or not the <code>Shape</code> * entirely contains the rectangular area are prohibitively expensive.</li> * </ul> * * This means that this method might return <code>false</code> even though * the <code>Shape</code> contains the rectangular area. The * <code>Area</code> class can be used to perform more accurate * computations of geometric intersection for any <code>Shape</code> * object if a more precise answer is required. * </p> * * @param x * the coordinates of the specified rectangular area, x value * @param y * the coordinates of the specified rectangular area, y value * @param w * the width of the specified rectangular area * @param h * the height of the specified rectangular area * * @return <code>true</code> if the interior of the <code>Shape</code> * entirely contains the specified rectangular area; * <code>false</code> otherwise or, if the <code>Shape</code> * contains the rectangular area and the <code>intersects</code> * method returns <code>true</code> and the containment * calculations would be too expensive to perform. * * @see java.awt.geom.Area * @see #intersects */ public boolean contains(double x, double y, double w, double h) { Geometry rect = createRectangle(x, y, w, h); return geometry.contains(rect); } /** * Returns an integer {@link Rectangle}that completely encloses the * <code>Shape</code>. Note that there is no guarantee that the returned * <code>Rectangle</code> is the smallest bounding box that encloses the * <code>Shape</code>, only that the <code>Shape</code> lies entirely * within the indicated <code>Rectangle</code>. The returned * <code>Rectangle</code> might also fail to completely enclose the * <code>Shape</code> if the <code>Shape</code> overflows the limited * range of the integer data type. The <code>getBounds2D</code> method * generally returns a tighter bounding box due to its greater flexibility * in representation. * * @return an integer <code>Rectangle</code> that completely encloses the * <code>Shape</code>. * * @see #getBounds2D */ public Rectangle getBounds() { Rectangle2D env = getBounds2D(); return new Rectangle((int) Math.round(env.getMinX()), (int) Math.round(env.getMinY()), (int) Math.ceil(env.getWidth()), (int) Math.ceil(env.getHeight())); } /** * Returns a high precision and more accurate bounding box of the * <code>Shape</code> than the <code>getBounds</code> method. Note that * there is no guarantee that the returned {@link Rectangle2D}is the * smallest bounding box that encloses the <code>Shape</code>, only that * the <code>Shape</code> lies entirely within the indicated * <code>Rectangle2D</code>. The bounding box returned by this method is * usually tighter than that returned by the <code>getBounds</code> method * and never fails due to overflow problems since the return value can be an * instance of the <code>Rectangle2D</code> that uses double precision * values to store the dimensions. * * @return an instance of <code>Rectangle2D</code> that is a * high-precision bounding box of the <code>Shape</code>. * * @see #getBounds */ public Rectangle2D getBounds2D() { Envelope env = geometry.getEnvelopeInternal(); // note, we dont' use getWidth/getHeight since they are slower return new Rectangle2D.Double(env.getMinX(), env.getMinY(), env.getMaxX() - env.getMinX(), env.getMaxY() - env.getMinY()); } /** * Returns an iterator object that iterates along the <code>Shape</code> * boundary and provides access to the geometry of the <code>Shape</code> * outline. If an optional {@link AffineTransform}is specified, the * coordinates returned in the iteration are transformed accordingly. * * <p> * Each call to this method returns a fresh <code>PathIterator</code> * object that traverses the geometry of the <code>Shape</code> object * independently from any other <code>PathIterator</code> objects in use * at the same time. * </p> * * <p> * It is recommended, but not guaranteed, that objects implementing the * <code>Shape</code> interface isolate iterations that are in process * from any changes that might occur to the original object's geometry * during such iterations. * </p> * * <p> * Before using a particular implementation of the <code>Shape</code> * interface in more than one thread simultaneously, refer to its * documentation to verify that it guarantees that iterations are isolated * from modifications. * </p> * * @param at * an optional <code>AffineTransform</code> to be applied to * the coordinates as they are returned in the iteration, or * <code>null</code> if untransformed coordinates are desired * * @return a new <code>PathIterator</code> object, which independently * traverses the geometry of the <code>Shape</code>. */ public PathIterator getPathIterator(AffineTransform at) { PathIterator pi = null; if(this.geometry == null || this.geometry.isEmpty()) return EmptyIterator.INSTANCE; // return iterator according to the kind of geometry we include if (this.geometry instanceof Point) { pi = new PointIterator((Point) geometry, at); } if (this.geometry instanceof Polygon) { pi = new PolygonIterator((Polygon) geometry, at, generalize, maxDistance); } else if (this.geometry instanceof LineString) { pi = new LineIterator((LineString) geometry, at, generalize, (float) maxDistance); } else if (this.geometry instanceof GeometryCollection) { pi = new GeomCollectionIterator((GeometryCollection) geometry, at, generalize, maxDistance); } return pi; } /** * Returns an iterator object that iterates along the <code>Shape</code> * boundary and provides access to a flattened view of the * <code>Shape</code> outline geometry. * * <p> * Only SEG_MOVETO, SEG_LINETO, and SEG_CLOSE point types are returned by * the iterator. * </p> * * <p> * If an optional <code>AffineTransform</code> is specified, the * coordinates returned in the iteration are transformed accordingly. * </p> * * <p> * The amount of subdivision of the curved segments is controlled by the * <code>flatness</code> parameter, which specifies the maximum distance * that any point on the unflattened transformed curve can deviate from the * returned flattened path segments. Note that a limit on the accuracy of * the flattened path might be silently imposed, causing very small * flattening parameters to be treated as larger values. This limit, if * there is one, is defined by the particular implementation that is used. * </p> * * <p> * Each call to this method returns a fresh <code>PathIterator</code> * object that traverses the <code>Shape</code> object geometry * independently from any other <code>PathIterator</code> objects in use * at the same time. * </p> * * <p> * It is recommended, but not guaranteed, that objects implementing the * <code>Shape</code> interface isolate iterations that are in process * from any changes that might occur to the original object's geometry * during such iterations. * </p> * * <p> * Before using a particular implementation of this interface in more than * one thread simultaneously, refer to its documentation to verify that it * guarantees that iterations are isolated from modifications. * </p> * * @param at * an optional <code>AffineTransform</code> to be applied to * the coordinates as they are returned in the iteration, or * <code>null</code> if untransformed coordinates are desired * @param flatness * the maximum distance that the line segments used to * approximate the curved segments are allowed to deviate from * any point on the original curve * * @return a new <code>PathIterator</code> that independently traverses * the <code>Shape</code> geometry. */ public PathIterator getPathIterator(AffineTransform at, double flatness) { return getPathIterator(at); } /** * Tests if the interior of the <code>Shape</code> intersects the interior * of a specified <code>Rectangle2D</code>. This method might * conservatively return <code>true</code> when: * * <ul> * <li>there is a high probability that the <code>Rectangle2D</code> and * the <code>Shape</code> intersect, but</li> * <li>the calculations to accurately determine this intersection are * prohibitively expensive.</li> * </ul> * * This means that this method might return <code>true</code> even though * the <code>Rectangle2D</code> does not intersect the <code>Shape</code>. * * @param r * the specified <code>Rectangle2D</code> * * @return <code>true</code> if the interior of the <code>Shape</code> * and the interior of the specified <code>Rectangle2D</code> * intersect, or are both highly likely to intersect and * intersection calculations would be too expensive to perform; * <code>false</code> otherwise. * * @see #intersects(double, double, double, double) */ public boolean intersects(Rectangle2D r) { Geometry rect = rectangleToGeometry(r); return geometry.intersects(rect); } /** * Tests if the interior of the <code>Shape</code> intersects the interior * of a specified rectangular area. The rectangular area is considered to * intersect the <code>Shape</code> if any point is contained in both the * interior of the <code>Shape</code> and the specified rectangular area. * * <p> * This method might conservatively return <code>true</code> when: * * <ul> * <li>there is a high probability that the rectangular area and the * <code>Shape</code> intersect, but</li> * <li>the calculations to accurately determine this intersection are * prohibitively expensive.</li> * </ul> * * This means that this method might return <code>true</code> even though * the rectangular area does not intersect the <code>Shape</code>. The * {@link java.awt.geom.Area Area}class can be used to perform more * accurate computations of geometric intersection for any * <code>Shape</code> object if a more precise answer is required. * </p> * * @param x * the coordinates of the specified rectangular area, x value * @param y * the coordinates of the specified rectangular area, y value * @param w * the width of the specified rectangular area * @param h * the height of the specified rectangular area * * @return <code>true</code> if the interior of the <code>Shape</code> * and the interior of the rectangular area intersect, or are both * highly likely to intersect and intersection calculations would be * too expensive to perform; <code>false</code> otherwise. * * @see java.awt.geom.Area */ public boolean intersects(double x, double y, double w, double h) { Geometry rect = createRectangle(x, y, w, h); return geometry.intersects(rect); } /** * Converts the Rectangle2D passed as parameter in a jts Geometry object * * @param r * the rectangle to be converted * * @return a geometry with the same vertices as the rectangle */ private Geometry rectangleToGeometry(Rectangle2D r) { return createRectangle(r.getMinX(), r.getMinY(), r.getWidth(), r .getHeight()); } /** * Creates a jts Geometry object representing a rectangle with the given * parameters * * @param x * left coordinate * @param y * bottom coordinate * @param w * width * @param h * height * * @return a rectangle with the specified position and size */ private Geometry createRectangle(double x, double y, double w, double h) { Coordinate[] coords = { new Coordinate(x, y), new Coordinate(x, y + h), new Coordinate(x + w, y + h), new Coordinate(x + w, y), new Coordinate(x, y) }; LinearRing lr = geometry.getFactory().createLinearRing(coords); return geometry.getFactory().createPolygon(lr, null); } public MathTransform getMathTransform() { return mathTransform; } public Geometry getGeometry() { return geometry; } }