package it.geosolutions.jaiext.vectorbin; /* JAI-Ext - OpenSource Java Advanced Image Extensions Library * http://www.geo-solutions.it/ * Copyright 2016 GeoSolutions * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * The code was modified from the JAITools project, with permission * from the author Michael Bedward. */ import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.renderable.ParameterBlock; import java.awt.image.renderable.RenderedImageFactory; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.Interpolation; import javax.media.jai.JAI; import javax.media.jai.ParameterBlockJAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import it.geosolutions.jaiext.utilities.shape.LiteShape; import it.geosolutions.jaiext.jts.CoordinateSequence2D; import com.vividsolutions.jts.awt.ShapeReader; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryComponentFilter; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.PrecisionModel; import com.vividsolutions.jts.geom.TopologyException; import com.vividsolutions.jts.geom.prep.PreparedGeometry; import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory; import com.vividsolutions.jts.geom.util.AffineTransformation; /** * An ROI class backed by a vector object providing precision and the ability * to handle massive regions. It has a minimal memory footprint allowing it to * be used with massive images. * <p> * JAI operations often involve converting ROI objects to images. This class * implements its {@link #getAsImage()} method using the JAITools "VectorBinarize" * operator to avoid exhausting available memory when dealing with ROIs that * cover massive image areas. * <p> * Note that this class can be used to honour floating precision pixel coordinates * by setting the {@code useFixedPrecision} constructor argument to {@code false}. * The effect of the default fixed coordinate precision is to provide equivalent * behaviour to that of the standard {@code ROIShape}, where pixel coordinates * are treated as referring to the upper-left pixel corner. * * @author Michael Bedward * @author Andrea Aime * @since 1.1 * @version $Id$ */ public class ROIGeometry extends ROI { private static final Logger LOGGER = Logger.getLogger(ROIGeometry.class.getName()); /** * Default setting for use of anti-aliasing when drawing the reference * {@code Geometry} during a {@link #getAsImage()} request. * The default value is {@code true} which provides behaviour corresponding * to that of the standard JAI {@code ROIShape} class. */ public static final boolean DEFAULT_ROIGEOMETRY_ANTIALISING = true; /** * Default setting for use of fixed precision ({@code true}). */ public static final boolean DEFAULT_ROIGEOMETRY_USEFIXEDPRECISION = false; private boolean useAntialiasing = DEFAULT_ROIGEOMETRY_ANTIALISING; private boolean useFixedPrecision = DEFAULT_ROIGEOMETRY_USEFIXEDPRECISION; private static final long serialVersionUID = 1L; private static final AffineTransformation Y_INVERSION = new AffineTransformation(1, 0, 0, 0, -1, 0); private static final String UNSUPPORTED_ROI_TYPE = "The argument be either an ROIGeometry or an ROIShape"; /** The {@code Geometry} that defines the area of inclusion */ private final PreparedGeometry theGeom; /** Thread safe cache for the roi image */ private volatile PlanarImage roiImage; private final GeometryFactory geomFactory; private final static double tolerance = 1d; private final static PrecisionModel PRECISION = new PrecisionModel(tolerance); // read, remove excess ordinates, force precision and collect private final static GeometryFactory PRECISE_FACTORY = new GeometryFactory(PRECISION); private final static PrecisionModel FLOAT_PRECISION = new PrecisionModel(PrecisionModel.FLOATING_SINGLE); private final static GeometryFactory FLOAT_PRECISION_FACTORY = new GeometryFactory(FLOAT_PRECISION); private final CoordinateSequence2D testPointCS; private final com.vividsolutions.jts.geom.Point testPoint; private final CoordinateSequence2D testRectCS; private final Polygon testRect; private RenderingHints hints; /** * Constructor which takes a {@code Geometry} object to be used * as the reference against which to test inclusion of image coordinates. * The argument {@code geom} must be either a {@code Polygon} or * {@code MultiPolygon}. * The input geometry is copied so subsequent changes to it will * not be reflected in the {@code ROIGeometry} object. * * @param geom either a {@code Polygon} or {@code MultiPolygon} object * defining the area(s) of inclusion. * * @throws IllegalArgumentException if {@code geom} is {@code null} or * not an instance of either {@code Polygon} or {@code MultiPolygon} */ public ROIGeometry(Geometry geom) { this(geom, DEFAULT_ROIGEOMETRY_ANTIALISING, DEFAULT_ROIGEOMETRY_USEFIXEDPRECISION); } /** * Constructor which takes a {@code Geometry} object and a {@code boolean} * value for whether to use fixed coordinate precision (equivalent to * working with integer pixel coordinates). * The argument {@code geom} must be either a {@code Polygon} or * {@code MultiPolygon}. * The input geometry is copied so subsequent changes to it will * not be reflected in the {@code ROIGeometry} object. * * @param geom either a {@code Polygon} or {@code MultiPolygon} object * defining the area(s) of inclusion. * * @param useFixedPrecision whether to use fixed precision when comparing * pixel coordinates to the reference geometry * * @throws IllegalArgumentException if {@code geom} is {@code null} or * not an instance of either {@code Polygon} or {@code MultiPolygon} */ public ROIGeometry(Geometry geom, final boolean useFixedPrecision) { this(geom, DEFAULT_ROIGEOMETRY_ANTIALISING, useFixedPrecision); } /** * Constructors a new ROIGeometry. * The argument {@code geom} must be either a {@code Polygon} or * {@code MultiPolygon}. * The input geometry is copied so subsequent changes to it will * not be reflected in the {@code ROIGeometry} object. * * @param geom either a {@code Polygon} or {@code MultiPolygon} object * defining the area(s) of inclusion. * * @param antiAliasing whether to use anti-aliasing when converting this * ROI to an image * * @param useFixedPrecision whether to use fixed precision when comparing * pixel coordinates to the reference geometry * * @throws IllegalArgumentException if {@code geom} is {@code null} or * not an instance of either {@code Polygon} or {@code MultiPolygon} */ public ROIGeometry(Geometry geom, final boolean antiAliasing, final boolean useFixedPrecision) { this(geom, DEFAULT_ROIGEOMETRY_ANTIALISING, useFixedPrecision, null); } /** * Builds a new ROIGeometry. * The argument {@code geom} must be either a {@code Polygon} or * {@code MultiPolygon}. * The input geometry is copied so subsequent changes to it will * not be reflected in the {@code ROIGeometry} object. * * @param geom either a {@code Polygon} or {@code MultiPolygon} object * defining the area(s) of inclusion. * * @param hints The JAI hints to be used when generating the raster equivalent of this ROI * * @throws IllegalArgumentException if {@code geom} is {@code null} or * not an instance of either {@code Polygon} or {@code MultiPolygon} */ public ROIGeometry(Geometry geom, final RenderingHints hints) { this(geom, DEFAULT_ROIGEOMETRY_ANTIALISING, DEFAULT_ROIGEOMETRY_USEFIXEDPRECISION, hints); } /** * Fully-specified constructor. * The argument {@code geom} must be either a {@code Polygon} or * {@code MultiPolygon}. * The input geometry is copied so subsequent changes to it will * not be reflected in the {@code ROIGeometry} object. * * @param geom either a {@code Polygon} or {@code MultiPolygon} object * defining the area(s) of inclusion. * * @param antiAliasing whether to use anti-aliasing when converting this * ROI to an image * * @param useFixedPrecision whether to use fixed precision when comparing * pixel coordinates to the reference geometry * * @param hints The JAI hints to be used when generating the raster equivalent of this ROI * * @throws IllegalArgumentException if {@code geom} is {@code null} or * not an instance of either {@code Polygon} or {@code MultiPolygon} */ public ROIGeometry(Geometry geom, final boolean antiAliasing, final boolean useFixedPrecision, final RenderingHints hints) { if (geom == null) { throw new IllegalArgumentException("geom must not be null"); } if (!(geom instanceof Polygon || geom instanceof MultiPolygon)) { throw new IllegalArgumentException("geom must be a Polygon, MultiPolygon"); } this.useFixedPrecision = useFixedPrecision; this.hints = hints; Geometry cloned = null; if (useFixedPrecision){ geomFactory = PRECISE_FACTORY; cloned = geomFactory.createGeometry(geom); Coordinate[] coords = cloned.getCoordinates(); for (Coordinate coord : coords) { Coordinate cc1 = coord; PRECISION.makePrecise(cc1); } cloned.normalize(); } else { geomFactory = FLOAT_PRECISION_FACTORY; cloned = geomFactory.createGeometry(geom); Coordinate[] coords = cloned.getCoordinates(); for (Coordinate coord : coords) { Coordinate cc1 = coord; FLOAT_PRECISION.makePrecise(cc1); } cloned.normalize(); } theGeom = PreparedGeometryFactory.prepare(cloned); testPointCS = new CoordinateSequence2D(1); testPoint = geomFactory.createPoint(testPointCS); testRectCS = new CoordinateSequence2D(5); testRect = geomFactory.createPolygon(geomFactory.createLinearRing(testRectCS), null); } /** * Returns a new instance which is the union of this ROI and {@code roi}. * This is only possible if {@code roi} is an instance of ROIGeometry * or {@link ROIShape}. * * @param roi the ROI to add * @return the union as a new instance */ @Override public ROI add(ROI roi) { try { final Geometry geom = getGeometry(roi); if (geom != null) { Geometry union = geom.union(theGeom.getGeometry()); return buildROIGeometry(union); } } catch(TopologyException e) { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Failed to perform operation using geometries, falling back on raster path", e); } } // fallback on robust path return super.add(roi); } /** * Tests if this ROI contains the given point. * * @param p the point * * @return {@code true} if the point is within this ROI; * {@code false} otherwise */ @Override public boolean contains(Point p) { return contains(p.getX(), p.getY()); } /** * Tests if this ROI contains the given point. * * @param p the point * * @return {@code true} if the point is within this ROI; * {@code false} otherwise */ @Override public boolean contains(Point2D p) { return contains(p.getX(), p.getY()); } /** * Tests if this ROI contains the given image location. * * @param x location X ordinate * @param y location Y ordinate * * @return {@code true} if the location is within this ROI; * {@code false} otherwise */ @Override public boolean contains(int x, int y) { return contains((double)x, (double)y); } /** * Tests if this ROI contains the given image location. * * @param x location X ordinate * @param y location Y ordinate * * @return {@code true} if the location is within this ROI; * {@code false} otherwise */ @Override public boolean contains(double x, double y) { testPointCS.setX(0, x); testPointCS.setY(0, y); testPoint.geometryChanged(); return theGeom.contains(testPoint); } /** * Tests if this ROI contains the given rectangle. * * @param rect the rectangle * @return {@code true} if the rectangle is within this ROI; * {@code false} otherwise */ @Override public boolean contains(Rectangle rect) { return contains(rect.getMinX(), rect.getMinY(), rect.getWidth(), rect.getHeight()); } /** * Tests if this ROI contains the given rectangle. * * @param rect the rectangle * @return {@code true} if the rectangle is within this ROI; * {@code false} otherwise */ @Override public boolean contains(Rectangle2D rect) { return contains(rect.getMinX(), rect.getMinY(), rect.getWidth(), rect.getHeight()); } /** * Tests if this ROI contains the given rectangle. * * @param x rectangle origin X ordinate * @param y rectangle origin Y ordinate * @param w rectangle width * @param h rectangle height * * @return {@code true} if the rectangle is within this ROI; * {@code false} otherwise */ @Override public boolean contains(int x, int y, int w, int h) { return contains((double)x, (double)y, (double)w, (double)h); } /** * Tests if this ROI contains the given rectangle. * * @param x rectangle origin X ordinate * @param y rectangle origin Y ordinate * @param w rectangle width * @param h rectangle height * * @return {@code true} if the rectangle is within this ROI; * {@code false} otherwise */ @Override public boolean contains(double x, double y, double w, double h) { setTestRect(x, y, w, h); return theGeom.contains(testRect); } /** * Returns a new instance which is the exclusive OR of this ROI and {@code roi}. * This is only possible if {@code roi} is an instance of ROIGeometry * or {@link ROIShape}. * * @param roi the ROI to add * @return the union as a new instance */ @Override public ROI exclusiveOr(ROI roi) { try { final Geometry geom = getGeometry(roi); if (geom != null) { return buildROIGeometry(theGeom.getGeometry().symDifference(geom)); } } catch(TopologyException e) { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Failed to perform operation using geometries, falling back on raster path", e); } } // fallback on robust path return super.exclusiveOr(roi); } @Override public int[][] getAsBitmask(int x, int y, int width, int height, int[][] mask) { // go the cheap way, only TiledImage seems to be using this method ROI roiImage = new ROI(getAsImage()); return roiImage.getAsBitmask(x, y, width, height, mask); } /** * Gets an image representation of this ROI using the {@code VectorBinarize} * operation. For an ROI with very large bounds but simple shape(s) the * resulting image has a small memory footprint. * * @return a new image representing this ROI * @see it.geosolutions.jaiext.vectorbin.VectorBinarizeDescriptor */ @Override public PlanarImage getAsImage() { if (roiImage == null) { synchronized (this) { // this synch idiom works only if roiImage is volatile, keep it as such if (roiImage == null) { Envelope env = theGeom.getGeometry().getEnvelopeInternal(); int x = (int) Math.floor(env.getMinX()); int y = (int) Math.floor(env.getMinY()); int w = (int) Math.ceil(env.getMaxX()) - x; int h = (int) Math.ceil(env.getMaxY()) - y; ParameterBlockJAI pb = new ParameterBlockJAI("VectorBinarize"); pb.setParameter("minx", x); pb.setParameter("miny", y); pb.setParameter("width", w); pb.setParameter("height", h); pb.setParameter("geometry", theGeom); pb.setParameter("antiAliasing", useAntialiasing); roiImage = JAI.create("VectorBinarize", pb, hints); } } } return roiImage; } @Override public LinkedList getAsRectangleList(int x, int y, int width, int height) { Rectangle rect = new Rectangle(x, y, width, height); if (!intersects(rect)) { // no overlap return null; } else if (theGeom.getGeometry().isRectangle()) { // simple case, the geometry is a rectangle to start with Envelope env = theGeom.getGeometry().getEnvelopeInternal(); Envelope intersection = env.intersection(new Envelope(x, x + width, y, y + width)); int rx = (int) Math.round(intersection.getMinX()); int ry = (int) Math.round(intersection.getMinY()); int rw = (int) Math.round(intersection.getMaxX() - rx); int rh = (int) Math.round(intersection.getMaxY() - ry); LinkedList result = new LinkedList(); result.add(new Rectangle(rx, ry, rw, rh)); return result; } else { // we cannot force the base class to use our image, but // we can create a ROI around it ROI roiImage = new ROI(getAsImage()); return roiImage.getAsRectangleList(x, y, width, height); } } /** * Gets a new {@link Shape} representing this ROI. * * @return the shape */ @Override public Shape getAsShape() { return new LiteShape(theGeom.getGeometry()); } /** * Returns the ROI as a JTS {@code Geometry}. * * @return the geometry */ public Geometry getAsGeometry() { return theGeom.getGeometry(); } /** * Gets the enclosing rectangle of this ROI. * * @return a new rectangle */ @Override public Rectangle getBounds() { Envelope env = theGeom.getGeometry().getEnvelopeInternal(); return new Rectangle((int)env.getMinX(), (int)env.getMinY(), (int)env.getWidth(), (int)env.getHeight()); } /** * Gets the enclosing double-precision rectangle of this ROI. * * @return a new rectangle */ @Override public Rectangle2D getBounds2D() { Envelope env = theGeom.getGeometry().getEnvelopeInternal(); return new Rectangle2D.Double(env.getMinX(), env.getMinY(), env.getWidth(), env.getHeight()); } @Override public int getThreshold() { return super.getThreshold(); } /** * Returns a new instance which is the intersection of this ROI and {@code roi}. * This is only possible if {@code roi} is an instance of ROIGeometry * or {@link ROIShape}. * * @param roi the ROI to intersect with * @return the intersection as a new instance */ @Override public ROI intersect(ROI roi) { try { final Geometry geom = getGeometry(roi); if (geom != null) { Geometry intersect = geom.intersection(theGeom.getGeometry()); return buildROIGeometry(intersect); } } catch(TopologyException e) { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Failed to perform operation using geometries, falling back on raster path", e); } } // fallback on robust path return super.intersect(roi); } /** * Gets a {@link Geometry} from an input {@link ROI}. * * @param roi the ROI * @return a {@link Geometry} instance from the provided input; * null in case the input roi is neither a geometry, nor a shape. */ private Geometry getGeometry(ROI roi){ if (roi instanceof ROIGeometry){ return ((ROIGeometry) roi).getAsGeometry(); } else if (roi instanceof ROIShape){ final Shape shape = ((ROIShape) roi).getAsShape(); final Geometry geom = ShapeReader.read(shape, 0, geomFactory); geom.apply(Y_INVERSION); return geom; } return null; } /** * Tests if the given rectangle intersects with this ROI. * * @param rect the rectangle * @return {@code true} if there is an intersection; {@code false} otherwise */ @Override public boolean intersects(Rectangle rect) { setTestRect(rect.x, rect.y, rect.width, rect.height); return theGeom.intersects(testRect); } /** * Tests if the given rectangle intersects with this ROI. * * @param rect the rectangle * @return {@code true} if there is an intersection; {@code false} otherwise */ @Override public boolean intersects(Rectangle2D rect) { setTestRect(rect.getMinX(), rect.getMinY(), rect.getWidth(), rect.getHeight()); return theGeom.intersects(testRect); } /** * Tests if the given rectangle intersects with this ROI. * * @param x rectangle origin X ordinate * @param y rectangle origin Y ordinate * @param w rectangle width * @param h rectangle height * @return {@code true} if there is an intersection; {@code false} otherwise */ @Override public boolean intersects(int x, int y, int w, int h) { setTestRect(x, y, w, h); return theGeom.intersects(testRect); } /** * Tests if the given rectangle intersects with this ROI. * * @param x rectangle origin X ordinate * @param y rectangle origin Y ordinate * @param w rectangle width * @param h rectangle height * @return {@code true} if there is an intersection; {@code false} otherwise */ @Override public boolean intersects(double x, double y, double w, double h) { setTestRect(x, y, w, h); return theGeom.intersects(testRect); } @Override public ROI performImageOp(RenderedImageFactory RIF, ParameterBlock paramBlock, int sourceIndex, RenderingHints renderHints) { return super.performImageOp(RIF, paramBlock, sourceIndex, renderHints); } @Override public ROI performImageOp(String name, ParameterBlock paramBlock, int sourceIndex, RenderingHints renderHints) { return super.performImageOp(name, paramBlock, sourceIndex, renderHints); } @Override public void setThreshold(int threshold) { super.setThreshold(threshold); } /** * Returns a new instance which is the difference of this ROI and {@code roi}. * This is only possible if {@code roi} is an instance of ROIGeometry * or {@link ROIShape}. * * @param roi the ROI to add * @return the union as a new instance */ @Override public ROI subtract(ROI roi) { try { final Geometry geom = getGeometry(roi); if (geom != null) { Geometry difference = theGeom.getGeometry().difference(geom); return buildROIGeometry(difference); } } catch(TopologyException e) { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Failed to perform operation using geometries, falling back on raster path", e); } } // fallback on robust path return super.subtract(roi); } /** * Returns a new ROI created by applying the given transform to * this ROI. * * @param at the transform * @param interp ignored * * @return the new ROI */ @Override public ROI transform(AffineTransform at, Interpolation interp) { return transform(at); } /** * Returns a new ROI created by applying the given transform to * this ROI. * * @param at the transform * * @return the new ROI */ @Override public ROI transform(AffineTransform at) { Geometry cloned = (Geometry) theGeom.getGeometry().clone(); cloned.apply(new AffineTransformation(at.getScaleX(), at.getShearX(), at.getTranslateX(), at.getShearY(), at.getScaleY(), at.getTranslateY())); if (useFixedPrecision){ Geometry fixed = PRECISE_FACTORY.createGeometry(cloned); Coordinate[] coords = fixed.getCoordinates(); for (Coordinate coord : coords) { Coordinate precise = coord; PRECISION.makePrecise(precise); } cloned = fixed; } return buildROIGeometry(cloned); } /** * Helper function for contains and intersects methods. * * @param x rectangle origin X ordinate * @param y rectangle origin Y ordinate * @param w rectangle width * @param h rectangle height */ private void setTestRect(double x, double y, double w, double h) { testRectCS.setXY(0, x, y); testRectCS.setXY(1, x, y + h); testRectCS.setXY(2, x + w, y + h); testRectCS.setXY(3, x + w, y); testRectCS.setXY(4, x, y); testRect.geometryChanged(); } /** * Setup a ROIGeometry on top of a geometry. * It takes care of removing invalid polygon, opened line strings and so on * @param geometry the input geometry to be used as reference to create a new {@link ROIGeometry} * @return a ROI from the input geometry. */ private ROI buildROIGeometry(Geometry geometry) { // cleanup the geometry, extract only the polygons, set oriented operations might // have returned a mix of points and lines in the resulting geometries final List<Polygon> polygons = new ArrayList<Polygon>(); geometry.apply(new GeometryComponentFilter() { public void filter(Geometry geom) { if (geom instanceof Polygon) { polygons.add((Polygon) geom); } } }); // build a polygon or a multipolygon Geometry geom = null; if (polygons.size() == 0) { geom = geomFactory.createMultiPolygon(new Polygon[0]); } else if (polygons.size() == 1) { geom = polygons.get(0); } else { Polygon[] polygonArray = (Polygon[]) polygons.toArray(new Polygon[polygons.size()]); geom = geomFactory.createMultiPolygon(polygonArray); } return new ROIGeometry(geom, this.useAntialiasing, this.useFixedPrecision, this.hints); } }