blob: d9d2b82bb74461a5dfc1785fb7933b9816f0d258 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.sis.internal.feature.j2d;
import java.util.List;
import java.util.Iterator;
import java.util.function.BiPredicate;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.geom.PathIterator;
import org.opengis.geometry.DirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.internal.feature.Geometries;
import org.apache.sis.internal.feature.GeometryWithCRS;
import org.apache.sis.internal.feature.GeometryWrapper;
import org.apache.sis.internal.filter.sqlmm.SQLMM;
import org.apache.sis.internal.referencing.j2d.ShapeUtilities;
import org.apache.sis.internal.referencing.j2d.AbstractShape;
import org.apache.sis.internal.jdk9.JDK9;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Debug;
// Branch-dependent imports
import org.opengis.filter.SpatialOperatorName;
/**
* The wrapper of Java2D geometries.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
final class Wrapper extends GeometryWithCRS<Shape> {
/**
* The wrapped implementation.
*/
final Shape geometry;
/**
* Creates a new wrapper around the given geometry.
*/
Wrapper(final Shape geometry) {
this.geometry = geometry;
}
/**
* Returns the implementation-dependent factory of geometric object.
*/
@Override
public Geometries<Shape> factory() {
return Factory.INSTANCE;
}
/**
* Returns the geometry specified at construction time.
*/
@Override
public Object implementation() {
return geometry;
}
/**
* Returns the geometry as an {@link Area} object, creating it on the fly if necessary.
* The returned area shall not be modified because it may be the {@link #geometry} instance.
*/
private Area area() {
return (geometry instanceof Area) ? (Area) geometry : new Area(geometry);
}
/**
* Returns the Java2D envelope as an Apache SIS implementation.
*
* @return the envelope of the geometry.
*/
@Override
public GeneralEnvelope getEnvelope() {
final Rectangle2D bounds = geometry.getBounds2D();
final GeneralEnvelope env = createEnvelope();
env.setRange(0, bounds.getMinX(), bounds.getMaxX());
env.setRange(1, bounds.getMinY(), bounds.getMaxY());
return env;
}
/**
* Returns the centroid of the wrapped geometry as a direct position.
*/
@Override
public DirectPosition getCentroid() {
final RectangularShape frame = (geometry instanceof RectangularShape)
? (RectangularShape) geometry : geometry.getBounds2D();
return new DirectPosition2D(getCoordinateReferenceSystem(), frame.getCenterX(), frame.getCenterY());
}
/**
* Returns {@code null} since {@link Shape} are never points in Java2D API.
*/
@Override
public double[] getPointCoordinates() {
return null;
}
/**
* Returns all coordinate tuples in the wrapped geometry.
* This method is currently used for testing purpose only.
*/
@Debug
@Override
public double[] getAllCoordinates() {
final List<double[]> coordinates = new ShapeProperties(geometry).coordinatesAsDoubles();
switch (coordinates.size()) {
case 0: return ArraysExt.EMPTY_DOUBLE;
case 1: return coordinates.get(0);
default: {
/*
* Concatenate the coordinates of all polygons in a single array. We lost the distinction
* between the different polygons, which is why this method should not be used except for
* testing.
*/
final double[] tgt = new double[coordinates.stream().mapToInt((a) -> a.length).sum()];
int p = 0;
for (final double[] src : coordinates) {
System.arraycopy(src, 0, tgt, p, src.length);
p += src.length;
}
return tgt;
}
}
}
/**
* Merges a sequence of points or paths after this geometry.
*
* @throws ClassCastException if an element in the iterator is not a {@link Shape} or a {@link Point2D}.
*/
@Override
protected Shape mergePolylines(final Iterator<?> polylines) {
return mergePolylines(geometry, polylines);
}
/**
* Implementation of {@link #mergePolylines(Iterator)} also shared by {@link PointWrapper}.
*/
static Shape mergePolylines(Object next, final Iterator<?> polylines) {
boolean isFloat = AbstractShape.isFloat(next);
Path2D path = isFloat ? new Path2D.Float() : new Path2D.Double();
boolean lineTo = false;
add: for (;;) {
if (next instanceof Point2D) {
final double x = ((Point2D) next).getX();
final double y = ((Point2D) next).getY();
if (Double.isNaN(x) || Double.isNaN(y)) {
lineTo = false;
} else if (lineTo) {
path.lineTo(x, y);
} else {
path.moveTo(x, y);
lineTo = true;
}
} else {
path.append((Shape) next, false);
lineTo = false;
}
/*
* 'polylines.hasNext()' check is conceptually part of 'for' instruction,
* except that we need to skip this condition during the first iteration.
*/
do if (!polylines.hasNext()) break add;
while ((next = polylines.next()) == null);
/*
* Convert the path from single-precision to double-precision if needed.
*/
if (isFloat && !AbstractShape.isFloat(next)) {
path = new Path2D.Double(path);
isFloat = false;
}
}
return ShapeUtilities.toPrimitive(path);
}
/**
* Applies a filter predicate between this geometry and another geometry.
* This method assumes that the two geometries are in the same CRS (this is not verified).
*/
@Override
protected boolean predicateSameCRS(final SpatialOperatorName type, final GeometryWrapper<Shape> other) {
final int ordinal = type.ordinal();
if (ordinal >= 0 && ordinal < PREDICATES.length) {
final BiPredicate<Wrapper,Object> op = PREDICATES[ordinal];
if (op != null) {
return op.test(this, other);
}
}
return super.predicateSameCRS(type, other);
}
/**
* All predicates recognized by {@link #predicateSameCRS(SpatialOperatorName, GeometryWrapper)}.
* Array indices are {@link SpatialOperatorName#ordinal()} values.
*/
@SuppressWarnings({"unchecked","rawtypes"})
private static final BiPredicate<Wrapper,Object>[] PREDICATES =
new BiPredicate[SpatialOperatorName.OVERLAPS.ordinal() + 1];
static {
PREDICATES[SpatialOperatorName.OVERLAPS .ordinal()] = // Fallback on intersects.
PREDICATES[SpatialOperatorName.INTERSECTS.ordinal()] = Wrapper::intersect;
PREDICATES[SpatialOperatorName.CONTAINS .ordinal()] = Wrapper::contain;
PREDICATES[SpatialOperatorName.WITHIN .ordinal()] = Wrapper::within;
PREDICATES[SpatialOperatorName.BBOX .ordinal()] = Wrapper::bbox;
PREDICATES[SpatialOperatorName.EQUALS .ordinal()] = Wrapper::equal;
PREDICATES[SpatialOperatorName.DISJOINT .ordinal()] = (w,o) -> !w.intersect(o);
}
/**
* Applies a SQLMM operation on this geometry.
*
* @param operation the SQLMM operation to apply.
* @param other the other geometry, or {@code null} if the operation requires only one geometry.
* @param argument an operation-specific argument, or {@code null} if not applicable.
* @return result of the specified operation.
*/
@Override
protected Object operationSameCRS(final SQLMM operation, final GeometryWrapper<Shape> other, final Object argument) {
final Shape result;
switch (operation) {
case ST_Dimension:
case ST_CoordDim: return 2;
case ST_Is3D:
case ST_IsMeasured: return Boolean.FALSE;
case ST_IsEmpty: {
if (geometry instanceof RectangularShape) {
return ((RectangularShape) geometry).isEmpty();
} else {
return geometry.getPathIterator(null).isDone();
}
}
case ST_Overlaps: // Our approximate algorithm can not distinguish with intersects.
case ST_Intersects: return intersect(other);
case ST_Disjoint: return !intersect(other);
case ST_Contains: return contain (other);
case ST_Within: return within (other);
case ST_Equals: return equal (other);
case ST_Envelope: return getEnvelope();
case ST_Boundary: result = geometry.getBounds2D(); break;
case ST_Centroid: {
final RectangularShape frame = (geometry instanceof RectangularShape)
? (RectangularShape) geometry : geometry.getBounds2D();
return new Point2D.Double(frame.getCenterX(), frame.getCenterY());
}
case ST_Intersection: {
final Area area = new Area(geometry);
area.intersect(((Wrapper) other).area());
result = area;
break;
}
case ST_Union: {
final Area area = new Area(geometry);
area.add(((Wrapper) other).area());
result = area;
break;
}
case ST_Difference: {
final Area area = new Area(geometry);
area.subtract(((Wrapper) other).area());
result = area;
break;
}
case ST_SymDifference: {
final Area area = new Area(geometry);
area.exclusiveOr(((Wrapper) other).area());
result = area;
break;
}
default: return super.operationSameCRS(operation, other, argument);
}
// No metadata to copy in current version.
return result;
}
/**
* Estimates whether the wrapped geometry is equal to the geometry of the given wrapper.
*
* @param wrapper instance of {@link Wrapper}.
*/
private boolean equal(final Object wrapper) { // "s" omitted for avoiding confusion with super.equals(…).
if (wrapper instanceof Wrapper) {
final Shape other = ((Wrapper) wrapper).geometry;
final PathIterator it1 = geometry.getPathIterator(null);
final PathIterator it2 = other.getPathIterator(null);
if (it1.getWindingRule() == it2.getWindingRule()) {
final double[] p1 = new double[6];
final double[] p2 = new double[6];
while (!it1.isDone()) {
if (it2.isDone()) return false;
final int c = it1.currentSegment(p1);
if (c != it2.currentSegment(p2)) {
return false;
}
it1.next();
it2.next();
final int n;
switch (c) {
case PathIterator.SEG_CLOSE: continue;
case PathIterator.SEG_MOVETO:
case PathIterator.SEG_LINETO: n=2; break;
case PathIterator.SEG_QUADTO: n=4; break;
default: n=6; break;
}
if (!JDK9.equals(p1, 0, n, p2, 0, n)) {
return false;
}
}
return it2.isDone();
}
}
return false;
}
/**
* Estimates whether the wrapped geometry is contained by the geometry of the given wrapper.
* This method may conservatively returns {@code false} if an accurate computation would be
* too expansive.
*
* @param wrapper instance of {@link Wrapper}.
*/
private boolean within(final Object wrapper) {
if (wrapper instanceof Wrapper) {
final Shape other = ((Wrapper) wrapper).geometry;
return other.contains(geometry.getBounds2D());
}
return false;
}
/**
* Estimates whether the wrapped geometry contains the geometry of the given wrapper.
* This method may conservatively returns {@code false} if an accurate computation would
* be too expansive.
*
* @param wrapper instance of {@link Wrapper} or {@link PointWrapper}.
* @throws ClassCastException if the given object is not a recognized wrapper.
*/
private boolean contain(final Object wrapper) { // "s" omitted for avoiding confusion with super.contains(…).
if (wrapper instanceof PointWrapper) {
return geometry.contains(((PointWrapper) wrapper).point);
}
final Shape other = ((Wrapper) wrapper).geometry;
return geometry.contains(other.getBounds2D());
}
/**
* Estimates whether the wrapped geometry intersects the geometry of the given wrapper.
* This method may conservatively returns {@code true} if an accurate computation would
* be too expansive.
*
* @param wrapper instance of {@link Wrapper} or {@link PointWrapper}.
* @throws ClassCastException if the given object is not a recognized wrapper.
*/
private boolean intersect(final Object wrapper) { // "s" omitted for avoiding confusion with super.intersects(…).
if (wrapper instanceof PointWrapper) {
return geometry.contains(((PointWrapper) wrapper).point);
}
final Shape other = ((Wrapper) wrapper).geometry;
return geometry.intersects(other.getBounds2D()) && other.intersects(geometry.getBounds2D());
}
/**
* Estimates whether the wrapped geometry intersects the geometry of the given wrapper, testing only
* the bounding box of the {@code wrapper} argument. This method may be more accurate than required
* by OGC Filter Encoding specification in that this geometry is not simplified to a bounding box.
* But Java2D implementations sometime use bounding box approximation, so the result may be the same.
*
* @param wrapper instance of {@link Wrapper} or {@link PointWrapper}.
* @throws ClassCastException if the given object is not a recognized wrapper.
*/
private boolean bbox(final Object wrapper) {
if (wrapper instanceof PointWrapper) {
return geometry.contains(((PointWrapper) wrapper).point);
}
final Shape other = ((Wrapper) wrapper).geometry;
return geometry.intersects(other.getBounds2D());
}
/**
* Builds a WKT representation of the wrapped shape.
* Current implementation assumes that all closed shapes are polygons and that polygons have no hole
* (i.e. if a polygon is followed by more data, this method assumes that the additional data is a disjoint polygon).
*
* @see <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">Well-known text on Wikipedia</a>
*/
@Override
public String formatWKT(final double flatness) {
return new ShapeProperties(geometry).toWKT(flatness);
}
}