| /* |
| * 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; |
| |
| import java.io.ObjectStreamException; |
| import java.io.Serializable; |
| import java.nio.ByteBuffer; |
| import java.util.Optional; |
| import java.util.Iterator; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.apache.sis.geometry.AbstractEnvelope; |
| import org.apache.sis.geometry.GeneralEnvelope; |
| import org.apache.sis.geometry.WraparoundMethod; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.cs.AxesConvention; |
| import org.apache.sis.internal.referencing.AxisDirections; |
| import org.apache.sis.math.Vector; |
| import org.apache.sis.setup.GeometryLibrary; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.Classes; |
| |
| |
| /** |
| * Utility methods on geometric objects defined in libraries outside Apache SIS. |
| * We use this class for isolating dependencies from the {@code org.apache.feature} package |
| * to ESRI's API or to Java Topology Suite (JTS) API. |
| * This gives us a single place to review if we want to support different geometry libraries, |
| * or if Apache SIS come with its own implementation. |
| * |
| * <h2>Serialization</h2> |
| * All fields except {@link #library} should be declared {@code transient}. |
| * Deserialized {@code Geometries} instances shall be replaced by a unique instance, |
| * which is given by {@link #readResolve()}. |
| * |
| * @param <G> the base class of all geometry objects (except point in some implementations). |
| * |
| * @author Johann Sorel (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| * @author Alexis Manin (Geomatys) |
| * @version 1.1 |
| * @since 0.7 |
| * @module |
| */ |
| public abstract class Geometries<G> implements Serializable { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 1856503921463395122L; |
| |
| /** |
| * The {@value} value, used by subclasses for identifying code that assume two- or three-dimensional objects. |
| */ |
| public static final int BIDIMENSIONAL = 2, TRIDIMENSIONAL = 3; |
| |
| /** |
| * The enumeration value that identifies which geometry library is used. |
| */ |
| public final GeometryLibrary library; |
| |
| /** |
| * The root geometry class. |
| */ |
| public final transient Class<G> rootClass; |
| |
| /** |
| * The class for points. |
| * This is often a subclass of {@link #rootClass} but not necessarily. |
| */ |
| public final transient Class<?> pointClass; |
| |
| /** |
| * The class for polylines and polygons. |
| */ |
| public final transient Class<? extends G> polylineClass, polygonClass; |
| |
| /** |
| * The fallback implementation to use if the default one is not available. |
| * This is set by {@link GeometryFactories} and should not change after initialization. |
| * We do not synchronize accesses to this field because we keep it stable after |
| * {@link GeometryFactories} class initialization. |
| */ |
| transient Geometries<?> fallback; |
| |
| /** |
| * {@code true} if {@link #pointClass} is not a subtype of {@link #rootClass}. |
| * This is true for Java2D and false for JTS and ESRI libraries. |
| */ |
| private final transient boolean isPointClassDistinct; |
| |
| /** |
| * Creates a new adapter for the given root geometry class. |
| * |
| * @param library the enumeration value that identifies which geometry library is used. |
| * @param rootClass the root geometry class. |
| * @param pointClass the class for points. |
| * @param polylineClass the class for polylines. |
| * @param polygonClass the class for polygons. |
| */ |
| protected Geometries(final GeometryLibrary library, final Class<G> rootClass, final Class<?> pointClass, |
| final Class<? extends G> polylineClass, final Class<? extends G> polygonClass) |
| { |
| this.library = library; |
| this.rootClass = rootClass; |
| this.pointClass = pointClass; |
| this.polylineClass = polylineClass; |
| this.polygonClass = polygonClass; |
| isPointClassDistinct = !rootClass.isAssignableFrom(pointClass); |
| } |
| |
| /** |
| * Returns a factory backed by the specified geometry library implementation, |
| * of the default implementation if the specified library is {@code null}. |
| * |
| * @param library the desired library, or {@code null} for the default. |
| * @return the specified or the default geometry implementation (never {@code null}). |
| * @throws IllegalArgumentException if a non-null library is specified by that library is not available. |
| */ |
| public static Geometries<?> implementation(final GeometryLibrary library) { |
| Geometries<?> g = GeometryFactories.implementation; |
| if (library == null) { |
| return g; |
| } |
| while (g != null) { |
| if (g.library == library) return g; |
| g = g.fallback; |
| } |
| throw new IllegalArgumentException(Resources.format(Resources.Keys.UnavailableGeometryLibrary_1, library)); |
| } |
| |
| /** |
| * Returns a factory backed by the same implementation than the given type. |
| * If the given type is not recognized, then this method returns {@code null}. |
| * |
| * @param type the type for which to get a geometry factory. |
| * @return a geometry factory compatible with the given type if possible, or {@code null} otherwise. |
| */ |
| public static Geometries<?> implementation(final Class<?> type) { |
| for (Geometries<?> g = GeometryFactories.implementation; g != null; g = g.fallback) { |
| if (g.isSupportedType(type)) return g; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns {@code true} if the given type is one of the geometry types known to Apache SIS. |
| * |
| * @param type the type to verify. |
| * @return {@code true} if the given type is one of the geometry types known to SIS. |
| */ |
| public static boolean isKnownType(final Class<?> type) { |
| for (Geometries<?> g = GeometryFactories.implementation; g != null; g = g.fallback) { |
| if (g.isSupportedType(type)) return true; |
| } |
| return GeometryWrapper.class.isAssignableFrom(type); |
| } |
| |
| /** |
| * Returns {@code true} if the given class is a geometry type supported by the underlying library. |
| */ |
| private boolean isSupportedType(final Class<?> type) { |
| return rootClass.isAssignableFrom(type) || (isPointClassDistinct && pointClass.isAssignableFrom(type)); |
| } |
| |
| /** |
| * Returns the geometry class of the given instance. |
| * |
| * @param type type of geometry for which the class is desired. |
| * @return implementation class for the geometry of the specified type. |
| */ |
| public Class<?> getGeometryClass(final GeometryType type) { |
| switch (type) { |
| default: return rootClass; |
| case POINT: return pointClass; |
| case LINESTRING: return polylineClass; |
| case POLYGON: return polygonClass; |
| } |
| } |
| |
| /** |
| * Wraps the given geometry implementation if recognized. |
| * If the given object is already an instance of {@link GeometryWrapper}, then it is returned as-is. |
| * If the given object is not recognized, then this method returns an empty value. |
| * |
| * @param geometry the geometry instance to wrap (can be {@code null}). |
| * @return a wrapper for the given geometry implementation, or empty value. |
| * |
| * @see #castOrWrap(Object) |
| */ |
| public static Optional<GeometryWrapper<?>> wrap(final Object geometry) { |
| if (geometry != null) { |
| if (geometry instanceof GeometryWrapper<?>) { |
| return Optional.of((GeometryWrapper<?>) geometry); |
| } |
| for (Geometries<?> g = GeometryFactories.implementation; g != null; g = g.fallback) { |
| if (g.isSupportedType(geometry.getClass())) { |
| return Optional.of(g.castOrWrap(geometry)); |
| } |
| } |
| } |
| return Optional.empty(); |
| } |
| |
| /** |
| * Returns a wrapper for the given {@code <G>} or {@code GeometryWrapper<G>} instance. |
| * The given object can be one of the following choices: |
| * |
| * <ul> |
| * <li>{@code null}, in which case this method returns {@code null}.</li> |
| * <li>An instance of {@code GeometryWrapper<G>}, in which case the given object is returned unchanged. |
| * Note that instances of {@code GeometryWrapper<?>} for implementations other than {@code <G>} |
| * will cause a {@link ClassCastException} to be thrown.</li> |
| * <li>An instance of {@link #rootClass} or {@link #pointClass}.</li> |
| * </ul> |
| * |
| * This method can be used as an alternative to {@link #wrap(Object)} when the specified |
| * geometry shall be an implementation of the specific {@linkplain #library}. |
| * |
| * @param geometry the geometry instance to wrap (can be {@code null}). |
| * @return a wrapper for the given geometry implementation, or {@code null} if the given object was null. |
| * @throws ClassCastException if the the given object is not a wrapper or a geometry object |
| * of the implementation of the library identified by {@link #library}. |
| * |
| * @see #wrap(Object) |
| */ |
| public abstract GeometryWrapper<G> castOrWrap(Object geometry); |
| |
| /** |
| * If the given object is an instance of {@link GeometryWrapper}, returns the wrapped geometry implementation. |
| * Otherwise returns the given geometry unchanged. |
| * |
| * @param geometry the geometry to unwrap (can be {@code null}). |
| * @return the geometry implementation, or the given geometry as-is. |
| */ |
| protected static Object unwrap(final Object geometry) { |
| return (geometry instanceof GeometryWrapper<?>) ? ((GeometryWrapper<?>) geometry).implementation() : geometry; |
| } |
| |
| /** |
| * Parses the given Well Known Text (WKT). |
| * |
| * @param wkt the WKT to parse. Can not be null. |
| * @return the geometry object for the given WKT (never {@code null}). |
| * @throws Exception if the WKT can not be parsed. The exception sub-class depends on the implementation. |
| * |
| * @see GeometryWrapper#formatWKT(double) |
| */ |
| public abstract GeometryWrapper<G> parseWKT(String wkt) throws Exception; |
| |
| /** |
| * Reads the given bytes as a Well Known Binary (WKB) encoded geometry. |
| * Whether this method changes the buffer position or not is implementation-dependent. |
| * |
| * @param data the binary data in WKB format. Can not be null. |
| * @return decoded geometry (never {@code null}). |
| * @throws Exception if the WKB can not be parsed. The exception sub-class depends on the implementation. |
| */ |
| public abstract GeometryWrapper<G> parseWKB(ByteBuffer data) throws Exception; |
| |
| /** |
| * Returns whether this library can produce geometry backed by the {@code float} primitive type |
| * instead of the {@code double} primitive type. If single-precision mode is supported, using |
| * that mode may reduce memory usage. This method is used for checking whether it is worth to |
| * invoke {@link Vector#isSinglePrecision()} for example. |
| * |
| * @return whether the library support single-precision values. |
| * |
| * @see Vector#isSinglePrecision() |
| */ |
| public boolean supportSinglePrecision() { |
| return false; |
| } |
| |
| /** |
| * Single-precision variant of {@link #createPoint(double, double)}. |
| * Default implementation delegates to the double-precision variant. |
| * |
| * @param x the first coordinate value. |
| * @param y the second coordinate value. |
| * @return the point for the given coordinate values. |
| * |
| * @see #supportSinglePrecision() |
| */ |
| public Object createPoint(float x, float y) { |
| return createPoint((double) x, (double) y); |
| } |
| |
| /** |
| * Creates a two-dimensional point from the given coordinates. If the CRS is geographic, then the |
| * (x,y) values should be (longitude, latitude) for compliance with usage in ESRI and JTS libraries. |
| * The returned object will be an instance of {@link #pointClass}. |
| * |
| * @param x the first coordinate value. |
| * @param y the second coordinate value. |
| * @return the point for the given coordinate values. |
| * |
| * @see GeometryWrapper#getPointCoordinates() |
| */ |
| public abstract Object createPoint(double x, double y); |
| |
| /** |
| * Creates a three-dimensional point from the given coordinates. If the CRS is geographic, then the |
| * (x,y) values should be (longitude, latitude) for compliance with usage in ESRI and JTS libraries. |
| * The returned object will be an instance of {@link #pointClass}. |
| * |
| * @param x the first coordinate value. |
| * @param y the second coordinate value. |
| * @param z the third coordinate value. |
| * @return the point for the given coordinate values. |
| * |
| * @see GeometryWrapper#getPointCoordinates() |
| */ |
| public abstract Object createPoint(double x, double y, double z); |
| |
| /** |
| * Creates a path, polyline or polygon from the given coordinate values. |
| * The array of coordinate values will be handled as if all vectors were |
| * concatenated in a single vector, ignoring {@code null} array elements. |
| * Each {@link Double#NaN} coordinate value in the concatenated vector starts a new path. |
| * The implementation returned by this method is an instance of {@link #rootClass}. |
| * |
| * <p>If the {@code polygon} argument is {@code true}, then the coordinates should |
| * make a closed line (e.g: a linear ring), otherwise an exception is thrown. |
| * |
| * @param polygon whether to return the path as a polygon instead of polyline. |
| * @param dimension the number of dimensions ({@value #BIDIMENSIONAL} or {@value #TRIDIMENSIONAL}). |
| * @param coordinates sequence of (x,y) or (x,y,z) tuples. |
| * @return the geometric object for the given points. |
| * @throws UnsupportedOperationException if the geometry library can not create the requested path. |
| * @throws IllegalArgumentException if a polygon was requested but the given coordinates do not make |
| * a closed shape (linear ring). |
| */ |
| public abstract G createPolyline(final boolean polygon, int dimension, Vector... coordinates); |
| |
| /** |
| * Creates a multi-polygon from an array of geometries (polygons or linear rings). |
| * Callers must ensure that the given objects are instances of geometric classes |
| * of the underlying library. |
| * |
| * If some geometries are actually linear rings, current behavior is not well defined. |
| * Some implementations may convert polylines to polygons but this is not guaranteed. |
| * |
| * @param geometries the polygons or linear rings to put in a multi-polygons. |
| * @return the multi-polygon. |
| * @throws ClassCastException if an element in the array is not an implementation of backing library. |
| * |
| * @todo Consider a more general method creating a multi-polygon or multi-line depending on object types, |
| * or returning a more primitive geometry type if the given array contains only one element. |
| * We may want to return null if the array is empty (to be decided later). |
| */ |
| public abstract GeometryWrapper<G> createMultiPolygon(final Object[] geometries); |
| |
| /** |
| * Creates a geometry from components. |
| * The expected {@code components} type depend on the target geometry type: |
| * <ul> |
| * <li>If {@code type} is a multi-geometry, then the components should be implementation-specific |
| * {@code Point[]}, {@code Geometry[]}, {@code LineString[]} or {@code Polygon[]}, |
| * depending on the desired target type.</li> |
| * <li>Otherwise the components should be an array or collection of {@code Point} or {@code Coordinate} |
| * instances, or some implementation-specific object such as {@code CoordinateSequence}.</li> |
| * </ul> |
| * |
| * @param type type of geometry to create. |
| * @param components the components. Valid classes depend on the type of geometry to create. |
| * @return geometry built from the given components. |
| * @throws ClassCastException if the given object is not an array or a collection of supported geometry components. |
| */ |
| public abstract GeometryWrapper<G> createFromComponents(GeometryType type, Object components); |
| |
| /** |
| * Creates a polyline made of points describing a rectangle whose start point is the lower left corner. |
| * The sequence of points describes each corner, going in clockwise direction and repeating the starting |
| * point to properly close the ring. |
| * |
| * @param xd dimension of first axis. |
| * @param yd dimension of second axis. |
| * @return a polyline made of a sequence of 5 points describing the given rectangle. |
| */ |
| private GeometryWrapper<G> createGeometry2D(final Envelope envelope, final int xd, final int yd) { |
| final DirectPosition lc = envelope.getLowerCorner(); |
| final DirectPosition uc = envelope.getUpperCorner(); |
| final double xmin = lc.getOrdinate(xd); |
| final double ymin = lc.getOrdinate(yd); |
| final double xmax = uc.getOrdinate(xd); |
| final double ymax = uc.getOrdinate(yd); |
| return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(new double[] { |
| xmin, ymin, xmin, ymax, xmax, ymax, xmax, ymin, xmin, ymin}))); |
| } |
| |
| /** |
| * Transforms an envelope to a two-dimensional polygon whose start point is lower corner |
| * and other points are the envelope corners in clockwise order. The specified envelope |
| * should be two-dimensional (see for example {@link GeneralEnvelope#horizontal()}) but |
| * the coordinates does not need to be in (longitude, latitude) order; this method will |
| * preserve envelope horizontal axis order. It means that any non-2D axis will be ignored, |
| * and the first horizontal axis in the envelope will be the first axis (x) in the resulting geometry. |
| * To force {@link AxesConvention#RIGHT_HANDED}, should transform the bounding box before calling this method. |
| * |
| * @param envelope the envelope to convert. |
| * @param strategy how to resolve wrap-around ambiguities on the envelope. |
| * @return the envelope as a polygon, or potentially as two polygons in {@link WraparoundMethod#SPLIT} case. |
| */ |
| public GeometryWrapper<G> toGeometry2D(final Envelope envelope, final WraparoundMethod strategy) { |
| int xd = 0, yd = 1; |
| CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem(); |
| final int dimension = envelope.getDimension(); |
| if (dimension != BIDIMENSIONAL) { |
| if (dimension < BIDIMENSIONAL) { |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyEnvelope2D)); |
| } |
| final CoordinateReferenceSystem crsND = crs; |
| crs = CRS.getHorizontalComponent(crsND); |
| if (crs == null) { |
| crs = CRS.getComponentAt(crsND, 0, BIDIMENSIONAL); |
| } else if (crs != crsND) { |
| final CoordinateSystem csND = crsND.getCoordinateSystem(); |
| final CoordinateSystem cs = crs .getCoordinateSystem(); |
| xd = AxisDirections.indexOfColinear(csND, cs.getAxis(0).getDirection()); |
| yd = AxisDirections.indexOfColinear(csND, cs.getAxis(1).getDirection()); |
| if (xd == yd) yd++; // Paranoiac check (e.g. CS with 2 temporal axes). |
| /* |
| * `indexOfColinear` returns -1 if the axis has not been found, but it should never |
| * happen here because we ask for axis directions that are known to exist in the CRS. |
| */ |
| } |
| } |
| final GeometryWrapper<G> result; |
| switch (strategy) { |
| case NORMALIZE: { |
| throw new IllegalArgumentException(); |
| } |
| case NONE: { |
| result = createGeometry2D(envelope, xd, yd); |
| break; |
| } |
| default: { |
| final GeneralEnvelope ge = new GeneralEnvelope(envelope); |
| ge.normalize(); |
| ge.wraparound(strategy); |
| result = createGeometry2D(ge, xd, yd); |
| break; |
| } |
| case SPLIT: { |
| final Envelope[] parts = AbstractEnvelope.castOrCopy(envelope).toSimpleEnvelopes(); |
| if (parts.length == 1) { |
| result = createGeometry2D(parts[0], xd, yd); |
| break; |
| } |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| final GeometryWrapper<G>[] polygons = new GeometryWrapper[parts.length]; |
| for (int i=0; i<parts.length; i++) { |
| polygons[i] = createGeometry2D(parts[i], xd, yd); |
| polygons[i].setCoordinateReferenceSystem(crs); |
| } |
| result = createMultiPolygon(polygons); |
| break; |
| } |
| } |
| result.setCoordinateReferenceSystem(crs); |
| return result; |
| } |
| |
| /** |
| * Merges a sequence of points or polylines into a single polyline instances. |
| * Each previous polyline will be a separated path in the new polyline instances. |
| * The implementation returned by this method is an instance of {@link #rootClass}. |
| * |
| * <p>Contrarily to other methods in this class, this method does <strong>not</strong> unwrap |
| * the geometries contained in {@link GeometryWrapper}. It is caller responsibility to do so |
| * if needed.</p> |
| * |
| * @param paths the points or polylines to merge in a single polyline object. |
| * @return the merged polyline, or {@code null} if the given iterator has no element. |
| * @throws ClassCastException if collection elements are not instances of a supported library, |
| * or not all elements are instances of the same library. |
| */ |
| public static Object mergePolylines(final Iterator<?> paths) { |
| while (paths.hasNext()) { |
| final Object first = paths.next(); |
| if (first != null) { |
| final Optional<GeometryWrapper<?>> w = wrap(first); |
| if (w.isPresent()) return w.get().mergePolylines(paths); |
| /* |
| * Use the same exception type than `mergePolylines(…)` implementations. |
| * Also the same type than exception occurring elsewhere in the code of |
| * the caller (GroupAsPolylineOperation). |
| */ |
| throw new ClassCastException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getClass(first))); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Creates a wrapper for the given geometry instance. |
| * The given object shall be an instance of {@link #rootClass}. |
| * |
| * @param geometry the geometry to wrap. |
| * @return wrapper for the given geometry. |
| * @throws ClassCastException if the given geometry is not an instance of valid type. |
| * |
| * @see #castOrWrap(Object) |
| */ |
| protected abstract GeometryWrapper<G> createWrapper(G geometry); |
| |
| /** |
| * Invoked at deserialization time for obtaining the unique instance of this {@code Geometries} class. |
| * |
| * @return the unique {@code Geometries} instance for this class. |
| * @throws ObjectStreamException if the object state is invalid. |
| */ |
| protected abstract Object readResolve() throws ObjectStreamException; |
| |
| /** |
| * Returns an error message for an unsupported operation. This error message is used by non-abstract methods |
| * in {@code Geometries} subclasses, after we identified the geometry library implementation to use but that |
| * library does not provided the required functionality. |
| * |
| * @param operation name of the unsupported operation. |
| * @return error message to put in the exception to be thrown. |
| */ |
| protected static String unsupported(final String operation) { |
| return Errors.format(Errors.Keys.UnsupportedOperation_1, operation); |
| } |
| |
| /** |
| * Returns an error message for an unsupported number of dimensions in a geometry object. |
| * |
| * @param dimension number of dimensions (2 or 3) requested for the geometry object. |
| * @return error message to put in the exception to be thrown. |
| */ |
| protected static String unsupported(final int dimension) { |
| return Resources.format(Resources.Keys.UnsupportedGeometryObject_1, dimension); |
| } |
| } |