blob: 826a26718ef2300ad9fc7684a29e68f4b175964a [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;
import java.util.Set;
import java.util.Objects;
import java.util.Iterator;
import java.util.OptionalInt;
import javax.measure.Unit;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
import javax.measure.IncommensurableException;
import org.opengis.geometry.Geometry;
import org.opengis.geometry.Boundary;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.TransfiniteSet;
import org.opengis.geometry.complex.Complex;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.filter.sqlmm.SQLMM;
import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.NullArgumentException;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Debug;
import org.apache.sis.util.resources.Errors;
// Branch-dependent imports
import org.opengis.filter.SpatialOperatorName;
import org.opengis.filter.DistanceOperatorName;
import org.opengis.filter.InvalidFilterValueException;
/**
* Wraps a JTS, ESRI or Java2D geometry behind a {@code Geometry} interface.
* This is a temporary class to be refactored later as a more complete geometry framework.
* The methods provided in this class are not committed API, and often not even clean API.
* They are only utilities added for very specific Apache SIS needs and will certainly
* change without warning in future Apache SIS version.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
*
* @param <G> root class of geometry instances of the underlying library (i.e. {@link Geometries#rootClass}).
* This is not necessarily the class of the wrapped geometry returned by {@link #implementation()}.
*
* @see Geometries#wrap(Object)
*
* @since 0.8
* @module
*/
public abstract class GeometryWrapper<G> extends AbstractGeometry implements Geometry {
/**
* Creates a new geometry object.
*/
protected GeometryWrapper() {
}
/**
* Returns the implementation-dependent factory of geometric objects.
* This is typically a system-wide factory shared by all geometry instances.
*
* @return the factory of implementation-dependent geometric objects (never {@code null}).
*/
public abstract Geometries<G> factory();
/**
* Returns the JTS, ESRI or Java2D geometry implementation wrapped by this {@code GeometryWrapper} instance.
* This returned object will be an instance of {@link Geometries#rootClass} or {@link Geometries#pointClass}
* (note that {@code pointClass} is not necessarily a subtype of {@code rootClass}).
*
* @return the geometry implementation wrapped by this instance (never {@code null}).
*/
public abstract Object implementation();
/**
* Returns the Spatial Reference System Identifier (SRID) if available.
* The SRID is used in database such as PostGIS and is generally database-dependent.
* This is <em>not</em> necessarily an EPSG code, even it is common practice to use
* the same numerical values than EPSG. Note that the absence of SRID does not mean
* that {@link #getCoordinateReferenceSystem()} would return no CRS.
*
* <p>Users should invoke the {@link #getCoordinateReferenceSystem()} method instead.
* This {@code getSRID()} method is provided for classes such as {@code DataStore} backed by an SQL database.
* Those classes have a connection to a {@code "spatial_ref_sys} table providing the mapping from SRID codes
* to authority codes such as EPSG. Those {@code DataStore} will typically get the SRID soon after geometry
* creation, resolves its CRS and invoke {@link #setCoordinateReferenceSystem(CoordinateReferenceSystem)}.</p>
*
* @return the Spatial Reference System Identifier of the geometry.
*/
public OptionalInt getSRID() {
return OptionalInt.empty();
}
/**
* Gets the Coordinate Reference System (CRS) of this geometry. In some libraries (for example JTS) the CRS
* is stored in the {@link Geometries#rootClass} instances of that library. In other libraries (e.g. Java2D)
* the CRS is stored only in this {@code GeometryWrapper} instance.
*
* @return the geometry CRS, or {@code null} if unknown.
* @throws BackingStoreException if the CRS is defined by a SRID code and that code can not be used.
*/
@Override
public abstract CoordinateReferenceSystem getCoordinateReferenceSystem();
/**
* Sets the coordinate reference system.
* This method should be invoked only for newly created geometries. If the geometry library supports
* user objects (e.g. JTS), there is no guarantee that this method will not overwrite user setting.
*
* @param crs the coordinate reference system to set.
*
* @see #transform(CoordinateReferenceSystem)
*/
public abstract void setCoordinateReferenceSystem(CoordinateReferenceSystem crs);
/**
* Returns the geometry bounding box, together with its coordinate reference system.
*
* @return the geometry envelope. Should never be {@code null}.
* Note though that for an empty geometry or a single point, the returned envelope will be empty.
*/
@Override
public abstract GeneralEnvelope getEnvelope();
/**
* Returns the mathematical centroid (if possible) or center (as a fallback) as a direct position.
*
* @return the centroid of the wrapped geometry.
*
* @todo Consider a {@code getCentroid2D()} method avoiding the cost of fetching the CRS.
*/
@Override
public abstract DirectPosition getCentroid();
/**
* If the geometry implementation is a point, returns its coordinates. Otherwise returns {@code null}.
* If non-null, the returned array may have a length of 2 or 3. If the CRS is geographic, then the (x,y)
* values should be in (longitude, latitude) order for compliance with usage in ESRI and JTS libraries.
*
* @return the coordinate of the point as an array of length 2 or 3,
* or {@code null} if the geometry is not a point.
*
* @see #getCoordinateReferenceSystem()
* @see Geometries#createPoint(double, double)
*/
public abstract double[] getPointCoordinates();
/**
* Returns all geometry coordinate tuples. This method is currently used for testing purpose only because
* it does not separate the sequence of coordinates for different polygons in a multi-polygon.
*
* @return the sequence of all coordinate values in the wrapped geometry,
* or {@code null} if they can not be obtained.
*
* @todo Replace by a {@code toJava2D()} method returning a {@link java.awt.Shape},
* so we can use the path iterator instead of this array.
*/
@Debug
public abstract double[] getAllCoordinates();
/**
* Appends a sequence of points or polylines after this geometry.
* Each previous polyline will be a separated path in the new polyline instance.
*
* <p>The given iterator shall return instances of {@link Geometries#rootClass} or
* {@link Geometries#pointClass}, not <strong>not</strong> {@link GeometryWrapper}
* (it is caller responsibility to unwrap if needed).</p>
*
* @param paths the points or polylines to merge in a single polyline instance.
* @return the merged polyline (may be the wrapper geometry but never {@code null}).
* @throws ClassCastException if collection elements are not instances of the point or geometry class.
*/
protected abstract G mergePolylines(final Iterator<?> paths);
/**
* Applies a filter predicate between this geometry and another geometry.
* This method transforms the two geometries to the same CRS if needed.
*
* @param type the predicate operation to apply.
* @param other the other geometry to test with this geometry.
* @param distance the buffer distance around the geometry of the second expression.
* @param context the preferred CRS and other context to use if geometry transformations are needed.
* @return result of applying the specified predicate.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws InvalidFilterValueException if an error occurred while executing the operation on given geometries.
*/
public final boolean predicate(final DistanceOperatorName type, final GeometryWrapper<G> other,
final Quantity<Length> distance, final SpatialOperationContext context)
{
@SuppressWarnings({"unchecked", "rawtypes"})
final GeometryWrapper<G>[] geometries = new GeometryWrapper[] {this, other};
try {
if (context.transform(geometries)) {
double dv = distance.getValue().doubleValue();
final Unit<?> unit = ReferencingUtilities.getUnit(context.commonCRS);
if (unit != null) {
dv = distance.getUnit().getConverterToAny(unit).convert(dv);
}
return geometries[0].predicateSameCRS(type, geometries[1], dv);
}
} catch (FactoryException | TransformException | IncommensurableException e) {
throw new InvalidFilterValueException(e);
}
/*
* No common CRS. Consider that we have no intersection, no overlap, etc.
* since the two geometries are existing in different coordinate spaces.
*/
return SpatialOperationContext.negativeResult(type);
}
/**
* Applies a filter predicate between this geometry and another geometry.
* This method transforms the two geometries to the same CRS if needed.
*
* <p><b>Note:</b> {@link SpatialOperatorName#BBOX} is implemented by {@code NOT DISJOINT}.
* It is caller's responsibility to ensure that one of the geometries is rectangular.</p>
*
* @param type the predicate operation to apply.
* @param other the other geometry to test with this geometry.
* @param context the preferred CRS and other context to use if geometry transformations are needed.
* @return result of applying the specified predicate.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws InvalidFilterValueException if an error occurred while executing the operation on given geometries.
*/
public final boolean predicate(final SpatialOperatorName type, final GeometryWrapper<G> other,
final SpatialOperationContext context)
{
@SuppressWarnings({"unchecked", "rawtypes"})
final GeometryWrapper<G>[] geometries = new GeometryWrapper[] {this, other};
try {
if (context.transform(geometries)) {
return geometries[0].predicateSameCRS(type, geometries[1]);
}
} catch (FactoryException | TransformException | IncommensurableException e) {
throw new InvalidFilterValueException(e);
}
/*
* No common CRS. Consider that we have no intersection, no overlap, etc.
* since the two geometries are existing in different coordinate spaces.
*/
return SpatialOperationContext.negativeResult(type);
}
/**
* Applies a SQLMM operation on this geometry.
* This method shall be invoked only for operations without non-geometric parameters.
*
* @param operation the SQLMM operation to apply.
* @return result of the specified operation.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws ClassCastException if the operation can only be executed on some specific geometry subclasses
* (for example polylines) and the wrapped geometry is not of that class.
*/
public final Object operation(final SQLMM operation) {
assert operation.geometryCount() == 1 && operation.maxParamCount == 1 : operation;
final Object result = operationSameCRS(operation, null, null);
assert isInstance(operation, result) : result;
return result;
}
/**
* Applies a SQLMM operation on two geometries.
* This method shall be invoked only for operations without non-geometric parameters.
* The second geometry is transformed to the same CRS than this geometry for conformance with SQLMM standard.
*
* @param operation the SQLMM operation to apply.
* @param other the other geometry. It is caller's responsibility to check that this value is non-null.
* @return result of the specified operation.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws ClassCastException if the operation can only be executed on some specific geometry subclasses
* (for example polylines) and the wrapped geometry is not of that class.
* @throws TransformException if it was necessary to transform the other geometry and that transformation failed.
*/
public final Object operation(final SQLMM operation, final GeometryWrapper<G> other)
throws TransformException
{
assert operation.geometryCount() == 2 && operation.maxParamCount == 2 : operation;
final Object result = operationSameCRS(operation, toSameCRS(other), null);
assert isInstance(operation, result) : result;
return result;
}
/**
* Applies a SQLMM operation on this geometry with one operation-specific argument.
* The argument shall be non-null, unless the argument is optional.
*
* @param operation the SQLMM operation to apply.
* @param argument an operation-specific argument.
* @return result of the specified operation.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws ClassCastException if the operation can only be executed on some specific geometry subclasses
* (for example polylines) and the wrapped geometry is not of that class.
*/
public final Object operationWithArgument(final SQLMM operation, final Object argument) {
assert operation.geometryCount() == 1 && operation.maxParamCount == 2 : operation;
if (argument == null && operation.minParamCount > 1) {
// TODO: fetch argument name.
throw new NullArgumentException(Errors.format(Errors.Keys.NullArgument_1, "arg1"));
}
final Object result = operationSameCRS(operation, null, argument);
assert isInstance(operation, result) : result;
return result;
}
/**
* Applies a SQLMM operation on two geometries with one operation-specific argument.
* The argument shall be non-null, unless the argument is optional.
* The second geometry is transformed to the same CRS than this geometry for conformance with SQLMM standard.
*
* @param operation the SQLMM operation to apply.
* @param other the other geometry. It is caller's responsibility to check that this value is non-null.
* @param argument an operation-specific argument.
* @return result of the specified operation.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws ClassCastException if the operation can only be executed on some specific geometry subclasses
* (for example polylines) and the wrapped geometry is not of that class.
* @throws TransformException if it was necessary to transform the other geometry and that transformation failed.
*/
public final Object operationWithArgument(final SQLMM operation, final GeometryWrapper<G> other, final Object argument)
throws TransformException
{
assert operation.geometryCount() == 2 && operation.maxParamCount == 3 : operation;
if (argument == null && operation.minParamCount > 2) {
// TODO: fetch argument name.
throw new NullArgumentException(Errors.format(Errors.Keys.NullArgument_1, "arg2"));
}
final Object result = operationSameCRS(operation, toSameCRS(other), argument);
assert isInstance(operation, result) : result;
return result;
}
/**
* Transforms the {@code other} geometry to the same CRS than this geometry.
* This method should be cheap for the common case where the other geometry
* already uses the CRS, in which case it is returned unchanged.
*
* <p>If this geometry does not define a CRS, then current implementation
* returns the other geometry unchanged.</p>
*
* @param other the other geometry.
* @return the other geometry in the same CRS than this geometry.
* @throws TransformException if the other geometry can not be transformed.
* If may be because the other geometry does not define its CRS.
*/
private GeometryWrapper<G> toSameCRS(final GeometryWrapper<G> other) throws TransformException {
if (isSameCRS(other)) {
return other;
}
final CoordinateReferenceSystem crs = getCoordinateReferenceSystem();
return (crs != null) ? other.transform(crs) : this;
}
/**
* Returns {@code true} if a result is of the expected type.
* This is used for assertion purposes only.
*/
private boolean isInstance(final SQLMM operation, final Object result) {
return (result == null) || operation.getReturnType(factory()).isInstance(result);
}
/**
* 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).
*
* <p><b>Note:</b> {@link SpatialOperatorName#BBOX} is implemented by {@code NOT DISJOINT}.
* It is caller's responsibility to ensure that one of the geometries is rectangular.</p>
*
* @param type the predicate operation to apply.
* @param other the other geometry to test with this geometry.
* @return result of applying the specified predicate.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
*/
protected boolean predicateSameCRS(final SpatialOperatorName type, final GeometryWrapper<G> other) {
throw new UnsupportedOperationException(Geometries.unsupported(type.name()));
}
/**
* Applies a filter predicate between this geometry and another geometry within a given distance.
* This method assumes that the two geometries are in the same CRS and that the unit of measurement
* is the same for {@code distance} than for axes (this is not verified).
*
* @param type the predicate operation to apply.
* @param other the other geometry to test with this geometry.
* @param distance distance to test between the geometries.
* @return result of applying the specified predicate.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
*/
protected boolean predicateSameCRS(final DistanceOperatorName type, final GeometryWrapper<G> other, final double distance) {
throw new UnsupportedOperationException(Geometries.unsupported(type.name()));
}
/**
* 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.
* @throws UnsupportedOperationException if the operation can not be performed with current implementation.
* @throws ClassCastException if the operation can only be executed on some specific geometry subclasses
* (for example polylines) and the wrapped geometry is not of that class.
*/
protected Object operationSameCRS(final SQLMM operation, final GeometryWrapper<G> other, final Object argument) {
throw new UnsupportedOperationException(Geometries.unsupported(operation.name()));
}
/**
* Converts the given geometry to the specified type.
* If the geometry is already of that type, it is returned unchanged.
* Otherwise coordinates are copied in a new geometry of the requested type.
*
* <p>The following conversions are illegal and will cause an {@link IllegalArgumentException} to be thrown:</p>
* <ul>
* <li>From point to polyline or polygon.</li>
* <li>From geometry collection (except multi-point) to polyline.</li>
* <li>From geometry collection (except multi-point and multi-line string) to polygon.</li>
* <li>From geometry collection containing nested collections.</li>
* </ul>
*
* The conversion from {@code MultiLineString} to {@code Polygon} is defined as following:
* the first {@code LineString} is taken as the exterior {@code LinearRing} and all others
* {@code LineString}s are interior {@code LinearRing}s.
* This rule is defined by some SQLMM operations.
*
* @param target the desired type.
* @return the converted geometry.
* @throws IllegalArgumentException if the geometry can not be converted to the specified type.
*/
public GeometryWrapper<G> toGeometryType(GeometryType target) {
final Class<?> type = factory().getGeometryClass(target);
final Object geometry = implementation();
if (type.isInstance(geometry)) {
return this;
}
throw new UnconvertibleObjectException(Errors.format(Errors.Keys.CanNotConvertFromType_2, geometry.getClass(), type));
}
/**
* Transforms this geometry using the given coordinate operation.
* If the operation is {@code null}, then the geometry is returned unchanged.
* If the geometry uses a different CRS than the source CRS of the given operation
* and {@code validate} is {@code true},
* then a new operation to the target CRS will be automatically computed.
*
* <p>This method is preferred to {@link #transform(CoordinateReferenceSystem)}
* when possible because not all geometry libraries store the CRS of their objects.</p>
*
* @param operation the coordinate operation to apply, or {@code null}.
* @param validate whether to validate the operation source CRS.
* @return the transformed geometry (may be the same geometry instance, but never {@code null}).
* @throws UnsupportedOperationException if this operation is not supported for current implementation.
* @throws FactoryException if transformation to the target CRS can not be found.
* @throws TransformException if the geometry can not be transformed.
*/
public GeometryWrapper<G> transform(final CoordinateOperation operation, final boolean validate)
throws FactoryException, TransformException
{
throw new UnsupportedOperationException(Geometries.unsupported("transform"));
}
/**
* Transforms this geometry to the specified Coordinate Reference System (CRS).
* If the given CRS is null, then the geometry is returned unchanged.
* If this geometry has no Coordinate Reference System, a {@link TransformException} is thrown.
*
* <p>Consider using {@link #transform(CoordinateOperation)} instead of this method as much as possible,
* both for performance reasons and because not all geometry libraries provide information about the CRS
* of their geometries.</p>
*
* @param targetCRS the target coordinate reference system, or {@code null}.
* @return the transformed geometry (may be the same geometry but never {@code null}).
* @throws UnsupportedOperationException if this operation is not supported for current implementation.
* @throws TransformException if the given geometry has no CRS or can not be transformed.
*
* @see #getCoordinateReferenceSystem()
*/
@Override
public GeometryWrapper<G> transform(final CoordinateReferenceSystem targetCRS) throws TransformException {
if (targetCRS == null) {
return this;
}
throw new UnsupportedOperationException(Geometries.unsupported("transform"));
}
/**
* Returns {@code true} if the given geometry use the same CRS than this geometry, or conservatively
* returns {@code false} in case of doubt. This method should perform only a cheap test; it is used
* as a way to filter rapidly if {@link #transform(CoordinateReferenceSystem)} needs to be invoked.
* If this method wrongly returned {@code false}, the {@code transform(…)} method will return the
* geometry unchanged anyway.
*
* <p>If both CRS are undefined (null), then they are considered the same.</p>
*
* @param other the second geometry.
* @return {@code true} if the two geometries use equivalent CRS or if the CRS is undefined on both side,
* or {@code false} in case of doubt.
*/
public abstract boolean isSameCRS(GeometryWrapper<G> other);
/**
* Formats the wrapped geometry in Well Known Text (WKT).
* If the geometry contains curves, then the {@code flatness} parameter specifies the maximum distance that
* the line segments used in the Well Known Text are allowed to deviate from any point on the original curve.
* This parameter is ignored if the geometry does not contain curves.
*
* @param flatness maximal distance between the approximated WKT and any point on the curve.
* @return the Well Known Text for the wrapped geometry (never {@code null}).
*
* @see Geometries#parseWKT(String)
*/
public abstract String formatWKT(double flatness);
/**
* Methods from the {@link Geometry} interface. The {@link Override} annotation is intentionally omitted
* for reducing the risk of compilation failures during the upcoming revision of GeoAPI interfaces since
* some of those methods will be removed.
*/
@Deprecated public final Geometry getMbRegion() {throw new UnsupportedOperationException();}
@Deprecated public final DirectPosition getRepresentativePoint() {throw new UnsupportedOperationException();}
@Deprecated public final Boundary getBoundary() {throw new UnsupportedOperationException();}
@Deprecated public final Complex getClosure() {throw new UnsupportedOperationException();}
@Deprecated public final boolean isSimple() {throw new UnsupportedOperationException();}
@Deprecated public final boolean isCycle() {throw new UnsupportedOperationException();}
@Deprecated public final double distance(Geometry geometry) {throw new UnsupportedOperationException();}
@Deprecated public final int getDimension(DirectPosition point) {throw new UnsupportedOperationException();}
@Deprecated public final int getCoordinateDimension() {throw new UnsupportedOperationException();}
@Deprecated public final Set<Complex> getMaximalComplex() {throw new UnsupportedOperationException();}
@Deprecated public final Geometry getConvexHull() {throw new UnsupportedOperationException();}
@Deprecated public final Geometry getBuffer(double distance) {throw new UnsupportedOperationException();}
@Deprecated public final Geometry clone() throws CloneNotSupportedException {throw new CloneNotSupportedException();}
@Deprecated public final boolean contains(TransfiniteSet pointSet) {throw new UnsupportedOperationException();}
@Deprecated public final boolean contains(DirectPosition point) {throw new UnsupportedOperationException();}
@Deprecated public final boolean intersects(TransfiniteSet pointSet) {throw new UnsupportedOperationException();}
@Deprecated public final boolean equals(TransfiniteSet pointSet) {throw new UnsupportedOperationException();}
@Deprecated public final TransfiniteSet union(TransfiniteSet pointSet) {throw new UnsupportedOperationException();}
@Deprecated public final TransfiniteSet intersection(TransfiniteSet pointSet) {throw new UnsupportedOperationException();}
@Deprecated public final TransfiniteSet difference(TransfiniteSet pointSet) {throw new UnsupportedOperationException();}
@Deprecated public final TransfiniteSet symmetricDifference(TransfiniteSet ps) {throw new UnsupportedOperationException();}
/**
* Returns {@code true} if the given object is a wrapper of the same class
* and the wrapped geometry implementations are equal.
*
* @param obj the object to compare with this wrapper.
* @return whether the two objects are wrapping geometry implementations that are themselves equal.
*/
@Override
public final boolean equals(final Object obj) {
return (obj != null) && obj.getClass().equals(getClass()) &&
Objects.equals(((GeometryWrapper) obj).implementation(), implementation());
}
/**
* Returns a hash code value based on the wrapped geometry.
*/
@Override
public final int hashCode() {
return ~Objects.hashCode(implementation());
}
/**
* Returns the string representation of the wrapped geometry
* (typically the class name and the bounds).
*/
@Override
public final String toString() {
/*
* Get a short string representation of the class name, ignoring the primitive type specialization.
* For example if the geometry class is `Rectangle2D.Float`, then get the "Rectangle2D" class name.
*/
final Class<?> c = implementation().getClass();
final Class<?> e = c.getEnclosingClass();
String s = Classes.getShortName(e != null ? e : c);
final GeneralEnvelope envelope = getEnvelope();
if (envelope != null) {
final String bbox = envelope.toString();
s += bbox.substring(bbox.indexOf('('));
}
return s;
}
}