blob: a2289c715bb0e4ab828913ca9837b317bfba6b9e [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.geometry.wrapper;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.logging.Logger;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.geometry.AbstractEnvelope;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.WraparoundMethod;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.privy.AxisDirections;
import org.apache.sis.system.Loggers;
import org.apache.sis.math.Vector;
import org.apache.sis.setup.GeometryLibrary;
import org.apache.sis.util.resources.Errors;
// Specific to the main and geoapi-3.1 branches:
import org.opengis.geometry.MismatchedDimensionException;
/**
* 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 {@code readResolve()} methods defined in each subclass.
*
* @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)
*/
public abstract class Geometries<G> implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 1856503921463395122L;
/**
* The logger for operations on geometries.
*/
public static final Logger LOGGER = Logger.getLogger(Loggers.GEOMETRY);
/**
* 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.
*
* <h4>Temporarily permitted change</h4>
* {@link GeometryFactories#setStandard(Geometries)} temporarily permits a change of this field,
* but only for {@link GeometryLibrary#GEOAPI}. This is internal API and a temporary flexibility
* for experimenting different GeoAPI implementations.
*/
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 but that library is not available.
*/
public static Geometries<?> factory(final GeometryLibrary library) {
Geometries<?> g = GeometryFactories.DEFAULT;
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 as 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<?> factory(final Class<?> type) {
for (Geometries<?> g = GeometryFactories.DEFAULT; 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.DEFAULT; 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 type.
* This is the type of instances returned by {@link GeometryWrapper#implementation()}.
*
* @param type type of geometry for which the class is desired.
* @return implementation class for the geometry of the specified type.
*
* @see #getGeometry(GeometryWrapper)
*/
public Class<?> getGeometryClass(final GeometryType type) {
switch (type) {
default: return rootClass;
case POINT: return pointClass;
case LINESTRING: return polylineClass;
case POLYGON: return polygonClass;
}
}
/**
* Returns the geometry object to return to the user in public API.
* This is the kind of object specified by {@link GeometryLibrary}.
* It is usually the {@linkplain GeometryWrapper#implementation() implementation},
* unless the user has requested GeoAPI interfaces in which case this method
* returns the wrapper directly.
*
* @param wrapper the wrapper for which to get the geometry, or {@code null}.
* @return the geometry instance of the library requested by user, or {@code null} if the given wrapper was null.
* @throws ClassCastException if the given wrapper is not an instance of the class expected by this factory.
*
* @see #getGeometryClass(GeometryType)
* @see #implementation(Object)
*/
public Object getGeometry(final GeometryWrapper wrapper) {
if (wrapper == null) {
return null;
}
final Geometries<?> other = wrapper.factory();
if (other.library != library) {
throw new ClassCastException(Resources.format(Resources.Keys.MismatchedGeometryLibrary_2, library, other.library));
}
return wrapper.implementation();
}
/**
* 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)
* @see #implementation(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.DEFAULT; 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} 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}, 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 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 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.
*
* @see GeometryWrapper#implementation()
*/
protected static Object implementation(final Object geometry) {
return (geometry instanceof GeometryWrapper) ? ((GeometryWrapper) geometry).implementation() : geometry;
}
/**
* Parses the given Well Known Text (WKT).
*
* @param wkt the WKT to parse. Cannot be null.
* @return the geometry object for the given WKT (never {@code null}).
* @throws Exception if the WKT cannot be parsed. The exception sub-class depends on the implementation.
*
* @see GeometryWrapper#formatWKT(double)
*/
public abstract GeometryWrapper 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. Cannot be null.
* @return decoded geometry (never {@code null}).
* @throws Exception if the WKB cannot be parsed. The exception sub-class depends on the implementation.
*/
public abstract GeometryWrapper parseWKB(ByteBuffer data) throws Exception;
/**
* Creates and wraps a point from the given position.
*
* @param point the point to convert to a geometry.
* @return the given point converted to a geometry.
*/
public final GeometryWrapper createPoint(final DirectPosition point) {
final Object geometry;
final int n = point.getDimension();
switch (n) {
case 2: geometry = createPoint(point.getCoordinate(0), point.getCoordinate(1)); break;
case 3: geometry = createPoint(point.getCoordinate(0), point.getCoordinate(1), point.getCoordinate(2)); break;
default: throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_3, "point", (n <= 2) ? 2 : 3, n));
}
final GeometryWrapper wrapper = castOrWrap(geometry);
if (point.getCoordinateReferenceSystem() != null) {
wrapper.setCoordinateReferenceSystem(point.getCoordinateReferenceSystem());
}
return wrapper;
}
/**
* 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 cannot 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 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 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. If wraparound may happen on at least one axis, then this method may
* add intermediate points on the axes where the envelope crosses the axis limit.
*
* @param xd dimension of first axis.
* @param yd dimension of second axis.
* @param expand whether to expand the envelope to full axis range if there is a wraparound.
* @param addPts whether to allow insertion of intermediate points on edges of axis domains.
* @return a polyline made of a sequence of at least 5 points describing the given rectangle.
*/
private GeometryWrapper createGeometry2D(final Envelope envelope, final int xd, final int yd,
final boolean expand, final boolean addPts)
{
final double xmin, ymin, xmax, ymax;
if (expand) {
xmin = envelope.getMinimum(xd);
ymin = envelope.getMinimum(yd);
xmax = envelope.getMaximum(xd);
ymax = envelope.getMaximum(yd);
} else {
final DirectPosition lc = envelope.getLowerCorner();
final DirectPosition uc = envelope.getUpperCorner();
xmin = lc.getCoordinate(xd);
ymin = lc.getCoordinate(yd);
xmax = uc.getCoordinate(xd);
ymax = uc.getCoordinate(yd);
}
final double[] coordinates;
/*
* Find if some intermediate points need to be added. We add points only at the edges of axis domain,
* for example at 180°E or 180°W. Furthermore, we add points only on axes having increasing values,
* i.e. we do not add points on axes using the "end point < start point" convention.
*/
final CoordinateReferenceSystem crs;
if (addPts && (crs = envelope.getCoordinateReferenceSystem()) != null) {
final double xminIn, yminIn, xmaxIn, ymaxIn; // Intermediate min/max.
final boolean addXmin, addYmin, addXmax, addYmax; // Whether to add intermediate min/max.
int n = 5*BIDIMENSIONAL; // Number of coordinate values.
final CoordinateSystem cs = crs.getCoordinateSystem();
CoordinateSystemAxis axis = cs.getAxis(xd);
xminIn = axis.getMinimumValue();
xmaxIn = axis.getMaximumValue();
axis = cs.getAxis(yd);
yminIn = axis.getMinimumValue();
ymaxIn = axis.getMaximumValue();
final boolean addX = xmin <= xmax; // Whether we can add intermediates X/Y.
final boolean addY = ymin <= ymax;
if (addXmin = (addX && xminIn > xmin)) n += 2*BIDIMENSIONAL;
if (addYmin = (addY && yminIn > ymin)) n += 2*BIDIMENSIONAL;
if (addXmax = (addX && xmaxIn < xmax)) n += 2*BIDIMENSIONAL;
if (addYmax = (addY && ymaxIn < ymax)) n += 2*BIDIMENSIONAL;
int i = 0;
coordinates = new double[n];
/*Envelope*/ {coordinates[i++] = xmin; coordinates[i++] = ymin;}
if (addYmin) {coordinates[i++] = xmin; coordinates[i++] = yminIn;}
if (addYmax) {coordinates[i++] = xmin; coordinates[i++] = ymaxIn;}
/*Envelope*/ {coordinates[i++] = xmin; coordinates[i++] = ymax;}
if (addXmin) {coordinates[i++] = xminIn; coordinates[i++] = ymax;}
if (addXmax) {coordinates[i++] = xmaxIn; coordinates[i++] = ymax;}
/*Envelope*/ {coordinates[i++] = xmax; coordinates[i++] = ymax;}
if (addYmax) {coordinates[i++] = xmax; coordinates[i++] = ymaxIn;}
if (addYmin) {coordinates[i++] = xmax; coordinates[i++] = yminIn;}
/*Envelope*/ {coordinates[i++] = xmax; coordinates[i++] = ymin;}
if (addXmax) {coordinates[i++] = xmaxIn; coordinates[i++] = ymin;}
if (addXmin) {coordinates[i++] = xminIn; coordinates[i++] = ymin;}
/*Envelope*/ {coordinates[i++] = xmin; coordinates[i++] = ymin;}
assert i == n : i;
} else {
coordinates = new double[] {xmin, ymin, xmin, ymax, xmax, ymax, xmax, ymin, xmin, ymin};
}
return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(coordinates)));
}
/**
* 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 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 result;
switch (strategy) {
case NORMALIZE: {
throw new IllegalArgumentException();
}
/*
* TODO: `addPts` is `false` in all cases. We have not yet determined
* what could be a public API for enabling this option.
*/
case NONE: {
result = createGeometry2D(envelope, xd, yd, false, false);
break;
}
default: {
final GeneralEnvelope ge = new GeneralEnvelope(envelope);
ge.normalize();
ge.wraparound(strategy);
result = createGeometry2D(ge, xd, yd, true, false);
break;
}
case SPLIT: {
final Envelope[] parts = AbstractEnvelope.castOrCopy(envelope).toSimpleEnvelopes();
if (parts.length == 1) {
result = createGeometry2D(parts[0], xd, yd, true, false);
break;
}
@SuppressWarnings({"unchecked", "rawtypes"})
final GeometryWrapper[] polygons = new GeometryWrapper[parts.length];
for (int i=0; i<parts.length; i++) {
polygons[i] = createGeometry2D(parts[i], xd, yd, true, false);
polygons[i].setCoordinateReferenceSystem(crs);
}
result = createMultiPolygon(polygons);
break;
}
}
result.setCoordinateReferenceSystem(crs);
return result;
}
/**
* 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 createWrapper(G geometry);
/**
* 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);
}
}