blob: f9cfe104fa1226e1dd0cdc39a0d9ec88dd58614c [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.coverage;
import java.util.Arrays;
import java.util.AbstractList;
import java.io.Serializable;
import java.io.ObjectStreamException;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.matrix.Matrix1;
import org.apache.sis.io.wkt.UnformattableObjectException;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.measure.NumberRange;
import static java.lang.Double.isNaN;
import static java.lang.Double.doubleToRawLongBits;
/**
* An immutable list of categories and a <cite>transfer function</cite> implementation backed by that list.
* The category list (exposed by the {@link java.util.List} interface) has the following properties:
*
* <ul>
* <li>Categories are sorted by their sample values.</li>
* <li>Overlapping ranges of sample values are not allowed.</li>
* <li>A {@code CategoryList} can contain a mix of qualitative and quantitative categories.</li>
* </ul>
*
* The transfer function exposed by the {@link MathTransform1D} interface is used only if this list contains
* at least 2 categories. More specifically:
*
* <ul>
* <li>If this list contains 0 category, then the {@linkplain SampleDimension#getTransferFunction() transfer function}
* shall be absent.</li>
* <li>If this list contains 1 category, then the transfer function should be {@linkplain Category#getTransferFunction()
* the function provided by that single category}, without the indirection level implemented by {@code CategoryList}.</li>
* <li>If this list contains 2 or more categories, then the transfer function implementation provided by this
* {@code CategoryList} is necessary for {@linkplain #search(double) searching the category} where belong
* each sample value.</li>
* </ul>
*
* The transfer function allows some extrapolations if a sample values to convert falls in a gap between two categories.
* The category immediately below will be used (i.e. its domain is expanded up to the next category), except if one category
* is qualitative while the next category is quantitative. In the later case, the quantitative category has precedence.
* The reason for allowing some extrapolations is because the range of values given to {@link Category} are often only
* estimations, and we don't want the transfer function to fail because a value is slightly outside the estimated domain.
*
* <p>Instances of {@link CategoryList} are immutable and thread-safe.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.1
* @since 1.0
* @module
*/
final class CategoryList extends AbstractList<Category> implements MathTransform1D, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -457688134719705403L;
/**
* An empty list of categories.
*/
static final CategoryList EMPTY = new CategoryList();
/**
* The union of the ranges of every categories, excluding {@code NaN} values.
* May be {@code null} if this list has no non-{@code NaN} category.
*
* <p>A {@link NumberRange} object gives more information than a (minimum, maximum) tuple since
* it contains also the type (integer, float, etc.) and inclusion/exclusion information.</p>
*/
final NumberRange<?> range;
/**
* List of minimum values (inclusive) for each category in {@link #categories}, in strictly increasing order.
* For each category, {@code minimums[i]} is often equal to {@code categories[i].range.getMinDouble(true)} but
* may also be lower for filling the gap between a quantitative category and its preceding qualitative category.
* We do not store maximum values; range of a category is assumed to span up to the start of the next category.
*
* <p>This array <strong>must</strong> be in increasing order, with {@link Double#NaN} values last.
* This is the need to sort this array that determines the element order in {@link #categories}.</p>
*/
private final double[] minimums;
/**
* The list of categories to use for decoding samples. This list must be sorted in increasing
* order of {@link Category#range} minimum. Qualitative categories with NaN values are last.
*/
private final Category[] categories;
/**
* Minimum and maximum values (inclusive) of {@link Category#converse} for each category.
* For each category at index {@code i}, the converse minimum is at index {@code i*2} and
* the converse maximum is at index {@code i*2+1}. This information is used for ensuring
* that extrapolated values (i.e. the result of a conversion when the input value was not
* in the range of any category) do not accidentally fall in the range of another category.
* This field may be {@code null} if there is no need to perform such verification because
* there is less than 2 categories bounded by real (non-NaN) values.
*/
private final double[] converseRanges;
/**
* Index of the last used category. We assume that this category is the most likely to be
* requested in the next {@code transform(…)} method invocation. This field does not need
* to be volatile because it is not a problem if a thread see an outdated value; this is
* only a hint, and the arrays used with this index are immutable.
*/
private transient int lastUsed;
/**
* The {@code CategoryList} that describes values after {@linkplain #getTransferFunction() transfer function}
* has been applied, or if this {@code CategoryList} is already converted then the original {@code CategoryList}.
* Never null, but may be {@code this} if the transfer function is the identity function.
* May also be {@link #EMPTY} if this category list has no quantitative category.
*
* <p>Exempt for the {@link #EMPTY} special case, this field establishes a bidirectional navigation between
* sample values and real values. This is in contrast with methods named {@code converted()}, which establish
* a unidirectional navigation from sample values to real values.</p>
*
* @see Category#converse
* @see SampleDimension#converse
*/
final CategoryList converse;
/**
* The constructor for the {@link #EMPTY} constant.
*/
private CategoryList() {
range = null;
minimums = ArraysExt.EMPTY_DOUBLE;
categories = new Category[0];
converseRanges = null;
converse = this;
}
/**
* Constructs a category list using the specified array of categories.
* The {@code categories} array should contain at least one element,
* otherwise the {@link #EMPTY} constant should be used.
*
* @param categories the list of categories. This array is not cloned and is modified in-place.
* @param converse if we are creating the list of categories after conversion from samples to real values,
* the original list before conversion. Otherwise {@code null}.
* @throws IllegalSampleDimensionException if two or more categories have overlapping sample value range.
*/
private CategoryList(final Category[] categories, CategoryList converse) {
this.categories = categories;
final int count = categories.length;
/*
* If users specify Category instances themselves, maybe they took existing instances from another
* sample dimension. A list of "non-converted" categories should not contain any ConvertedCategory
* instances, otherwise confusion will occur later. Note that the converse is not true: a list of
* converted categories may contain plain Category instances if the conversion is identity.
*/
if (converse == null) {
for (int i=0; i<count; i++) {
final Category c = categories[i];
if (c instanceof ConvertedCategory) {
categories[i] = new Category(c, null);
}
}
}
Arrays.sort(categories, Category.COMPARATOR);
/*
* Constructs the array of minimum values (inclusive). This array shall be in increasing order since
* we sorted the categories based on that criterion. We also collect the minimum and maximum values
* expected after conversion, but those values are not necessarily in any order.
*/
final double[] extremums;
extremums = new double[count << 1];
minimums = new double[count];
int countOfFiniteRanges = 0;
NumberRange<?> range = null;
for (int i=count; --i >= 0;) { // Reverse order for making computation of 'range' more convenient.
final Category category = categories[i];
if (!isNaN(minimums[i] = category.range.getMinDouble(true))) {
/*
* Initialize with the union of ranges at index 0 and index i. In most cases, the result will cover the whole
* range so all future calls to 'range.unionAny(…)' will be no-op. The 'categories[0].range' field should not
* be NaN because categories with NaN ranges are sorted last.
*/
if (range == null) {
range = categories[0].range;
assert !isNaN(range.getMinDouble()) : range;
}
range = range.unionAny(category.range);
}
final int j = i << 1;
final NumberRange<?> cr = category.converse.range;
if (!isNaN(extremums[j | 1] = cr.getMaxDouble(true)) |
!isNaN(extremums[j ] = cr.getMinDouble(true)))
{
countOfFiniteRanges++;
}
}
this.range = range;
this.converseRanges = (countOfFiniteRanges > 1) ? extremums : null;
assert ArraysExt.isSorted(minimums, false);
/*
* Verify that the ranges do not overlap and perform adjustments in 'minimums' values for filling some gaps:
* if we find a qualitative category followed by a quantitative category and empty space between them, then
* the quantitative category takes that empty space. We do not perform similar check for the opposite side
* (quantitative followed by qualitative) because CategoryList does not store maximum values; each category
* take all spaces up to the next category.
*/
for (int i=1; i<count; i++) {
final Category category = categories[i];
final Category previous = categories[i-1];
final double minimum = minimums[i];
if (Category.compare(minimum, previous.range.getMaxDouble(true)) <= 0) {
throw new IllegalSampleDimensionException(Resources.format(Resources.Keys.CategoryRangeOverlap_4,
previous.name, previous.getRangeLabel(),
category.name, category.getRangeLabel()));
}
// No overlapping check for 'converse' ranges here; see next block below.
final double limit = previous.range.getMaxDouble(false);
if (minimum > limit && previous.converse.isConvertedQualitative() // (a>b) implies that values are not NaN.
&& !category.converse.isConvertedQualitative())
{
minimums[i] = limit; // Expand the range of quantitative 'category' to the limit of qualitative 'previous'.
}
}
assert ArraysExt.isSorted(minimums, true);
/*
* If we are creating the list of "samples to real values" conversions, we need to create the list of categories
* resulting from conversions to real values. Note that this will indirectly test if some coverted ranges overlap,
* since this block invokes recursively this CategoryList constructor with a non-null 'converse' argument. Note
* also that converted categories may not be in the same order.
*/
if (converse == null) {
boolean isQualitative = true;
boolean isIdentity = true;
final Category[] convertedCategories = new Category[count];
for (int i=0; i<count; i++) {
final Category category = categories[i];
final Category converted = category.converse;
convertedCategories[i] = converted;
isQualitative &= converted.isConvertedQualitative();
isIdentity &= (category == converted);
}
if (isQualitative) {
converse = EMPTY;
} else if (isIdentity) {
converse = this;
} else {
converse = new CategoryList(convertedCategories, this);
if (converseRanges != null) {
/*
* For "samples to real values" conversion (only that direction, not the converse) and only if there
* is two or more quantitative categories (should be very rare), adjust the converted maximum values
* for filling gaps between converted categories.
*/
for (int i=1; i<converseRanges.length; i+=2) {
final double maximum = converseRanges[i];
final int p = ~Arrays.binarySearch(converse.minimums, maximum);
if (p >= 0 && p < count) {
double limit = Math.nextDown(converse.minimums[p]); // Minimum value of next category - ε
if (isNaN(limit)) limit = Double.POSITIVE_INFINITY; // Because NaN are last, no higher values.
if (limit > maximum) converseRanges[i] = limit; // Expand this category to fill the gap.
if (p == 1) {
converseRanges[i-1] = Double.NEGATIVE_INFINITY; // Consistent with converse.minimums[0] = −∞
}
} else if (p == count) {
converseRanges[i] = Double.POSITIVE_INFINITY; // No higher category; take all the space.
}
}
}
}
}
this.converse = converse;
if (count != 0 && !isNaN(minimums[0])) {
minimums[0] = Double.NEGATIVE_INFINITY;
}
}
/**
* Returns a shared instance if applicable.
*
* @return the object to use after deserialization.
* @throws ObjectStreamException if the serialized object contains invalid data.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private Object readResolve() throws ObjectStreamException {
return (categories.length == 0) ? EMPTY : this;
}
/**
* Constructs a category list using the specified array of categories.
* The {@code categories} array should contain at least one element,
* otherwise the {@link #EMPTY} constant should be used.
*
* <p>This is defined as a static method for allowing the addition of a caching mechanism in the future if desired.</p>
*
* @param categories the list of categories. This array is not cloned and is modified in-place.
* @throws IllegalSampleDimensionException if two or more categories have overlapping sample value range.
*/
static CategoryList create(final Category[] categories) {
return new CategoryList(categories, null);
}
/**
* Returns the <cite>transfer function</cite> from sample values to real values, including conversion
* of "no data" values to NaNs. Callers shall ensure that there is at least one quantitative category
* before to invoke this method.
*
* @see SampleDimension#getTransferFunction()
*/
final MathTransform1D getTransferFunction() {
MathTransform1D tr = categories[0].toConverse; // See condition in javadoc.
for (int i=categories.length; --i >= 1;) {
if (!tr.equals(categories[i].toConverse)) {
tr = this;
break;
}
}
return tr;
}
/**
* Performs a bi-linear search of the specified value in the given sorted array. If an exact match is found,
* its index is returned. If no exact match is found, index of the highest value smaller than {@code sample}
* is returned. If no such index exists, -1 is returned. Said otherwise, if the return value is positive and
* the given array is {@link #minimums}, then this method returns the index in the {@link #categories} array
* of the {@link Category} to use for a given sample value.
*
* <p>This method differs from {@link Arrays#binarySearch(double[],double)} in the following aspects:</p>
* <ul>
* <li>If differentiates the various NaN values.</li>
* <li>It does not differentiate exact matches from insertion points.</li>
* </ul>
*
* @param minimums {@link #minimums}.
* @param sample the sample value to search.
* @return index of the category to use, or -1 if none.
*/
static int binarySearch(final double[] minimums, final double sample) {
int low = 0;
int high = minimums.length - 1;
final boolean sampleIsNaN = isNaN(sample);
while (low <= high) {
final int mid = (low + high) >>> 1;
final double midVal = minimums[mid];
if (midVal < sample) { // Neither value is NaN, midVal is smaller.
low = mid + 1;
continue;
}
if (midVal > sample) { // Neither value is NaN, midVal is larger.
high = mid - 1;
continue;
}
final long midRawBits = doubleToRawLongBits(midVal);
final long smpRawBits = doubleToRawLongBits(sample);
if (midRawBits == smpRawBits) {
return mid; // Exact match found.
}
final boolean midIsNaN = isNaN(midVal);
final boolean adjustLow;
if (sampleIsNaN) {
/*
* If (mid,sample)==(!NaN, NaN): mid is lower.
* If two NaN arguments, compare NaN bits.
*/
adjustLow = (!midIsNaN || midRawBits < smpRawBits);
} else {
/*
* If (mid,sample)==(NaN, !NaN): mid is greater.
* Otherwise, case for (-0.0, 0.0) and (0.0, -0.0).
*/
adjustLow = (!midIsNaN && midRawBits < smpRawBits);
}
if (adjustLow) low = mid + 1;
else high = mid - 1;
}
/*
* If we reach this point and the sample is NaN, then it is not one of the NaN values known
* to CategoryList constructor and can not be mapped to a category. Otherwise we found the
* index of "insertion point" (~i). This means that 'sample' is lower than category minimum
* at that index. Consequently if the sample value is inside the range of some category, it
* can only be the previous category (~i-1).
*/
return sampleIsNaN ? -1 : low - 1;
}
/**
* Returns the category of the specified sample value.
* If no category fits, then this method returns {@code null}.
*
* @param sample the value.
* @return the category of the supplied value, or {@code null}.
*/
final Category search(final double sample) {
final int i = binarySearch(minimums, sample);
return (i >= 0) ? categories[i] : null;
}
/**
* Transforms a list of coordinate point ordinal values. This implementation accepts
* float or double arrays, since the quasi-totality of the implementation is the same.
* Locale variables still of the {@code double} type because this is the type used in
* {@link Category} objects.
*/
private void transform(final double[] srcPts, final float[] srcFloat, int srcOff,
final double[] dstPts, final float[] dstFloat, int dstOff,
int numPts) throws TransformException
{
final int srcToDst = dstOff - srcOff;
final int direction;
if (srcOff >= dstOff || (srcFloat != null ? srcFloat != dstFloat : srcPts != dstPts)) {
direction = +1;
} else {
direction = -1;
// dstOff += numPts-1; // Not updated because not used.
srcOff += numPts-1;
}
/*
* Scan every points. Transforms will be applied by blocks, each time the loop detects that
* the category has changed. The break condition (numPts >= 0) is near the end of the loop,
* after we have done the conversion but before to change category.
*/
int index = lastUsed;
double value = Double.NaN;
for (int peekOff = srcOff; /* numPts >= 0 */; peekOff += direction) {
final double minimum = minimums[index];
final double limit = (index+1 < minimums.length) ? minimums[index+1] : Double.NaN;
final long rawBits = doubleToRawLongBits(minimum);
while (--numPts >= 0) {
value = (srcFloat != null) ? srcFloat[peekOff] : srcPts[peekOff];
if (value >= minimum) {
if (value >= limit) {
break; // Category has changed; stop the search.
}
} else if (doubleToRawLongBits(value) != rawBits) {
break; // Not the expected NaN value.
}
peekOff += direction;
}
/*
* The category has changed. Compute the start point (which depends on 'direction') and perform
* the conversion on many values in a single 'transform' method call.
*/
int count = peekOff - srcOff; // May be negative if we are going backward.
if (count < 0) {
count = -count;
srcOff -= count - 1;
}
final int stepOff = srcOff + srcToDst;
final MathTransform1D piece = categories[index].toConverse;
if (srcFloat != null) {
if (dstFloat != null) {
piece.transform(srcFloat, srcOff, dstFloat, stepOff, count);
} else {
piece.transform(srcFloat, srcOff, dstPts, stepOff, count);
}
} else {
if (dstFloat != null) {
piece.transform(srcPts, srcOff, dstFloat, stepOff, count);
} else {
piece.transform(srcPts, srcOff, dstPts, stepOff, count);
}
}
/*
* If we need safety against extrapolations (for avoiding that a value falls in the range of another category),
* verify that transformed values are in expected ranges. Values out of range will be clamped.
*/
if (converseRanges != null) {
dstOff = srcOff + srcToDst;
if (dstFloat != null) { // Loop for the 'float' version.
final float min = (float) converseRanges[(index << 1) ];
final float max = (float) converseRanges[(index << 1) | 1];
while (--count >= 0) {
final float check = dstFloat[dstOff];
if (check < min) {
dstFloat[dstOff] = min;
} else if (check > max) {
dstFloat[dstOff] = max;
}
dstOff++;
}
} else { // Loop for the 'double' version.
final double min = converseRanges[(index << 1) ];
final double max = converseRanges[(index << 1) | 1];
while (--count >= 0) {
final double check = dstPts[dstOff];
if (check < min) {
dstPts[dstOff] = min;
} else if (check > max) {
dstPts[dstOff] = max;
}
dstOff++;
}
}
}
/*
* Transformation is now finished for all points in the range [srcOff … peekOff]
* (not including 'peekOff'). If there is more points to examine, get the new
* category for the next points.
*/
if (numPts < 0) break;
index = binarySearch(minimums, value);
if (index < 0) {
throw new TransformException(Resources.format(Resources.Keys.NoCategoryForValue_1, value));
}
srcOff = peekOff;
}
lastUsed = index;
}
/**
* Transforms a list of coordinate point ordinal values. This method can be invoked only if {@link #categories}
* contains at least two elements, otherwise a {@code MathTransform} implementation from another package is used.
*/
@Override
public final void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) throws TransformException {
transform(srcPts, null, srcOff, dstPts, null, dstOff, numPts);
}
/**
* Transforms a list of coordinate point ordinal values. This method can be invoked only if {@link #categories}
* contains at least two elements, otherwise a {@code MathTransform} implementation from another package is used.
*/
@Override
public final void transform(float[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) throws TransformException {
transform(null, srcPts, srcOff, null, dstPts, dstOff, numPts);
}
/**
* Transforms a list of coordinate point ordinal values. This method can be invoked only if {@link #categories}
* contains at least two elements, otherwise a {@code MathTransform} implementation from another package is used.
*/
@Override
public final void transform(float[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) throws TransformException {
transform(null, srcPts, srcOff, dstPts, null, dstOff, numPts);
}
/**
* Transforms a list of coordinate point ordinal values. This method can be invoked only if {@link #categories}
* contains at least two elements, otherwise a {@code MathTransform} implementation from another package is used.
*/
@Override
public final void transform(double[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) throws TransformException {
transform(srcPts, null, srcOff, null, dstPts, dstOff, numPts);
}
/**
* Transforms the specified value. This method can be invoked only if {@link #categories} contains at
* least two elements, otherwise a {@code MathTransform} implementation from another package is used.
*
* @param value the value to transform.
* @return the transformed value.
* @throws TransformException if the value can not be transformed.
*/
@Override
public final double transform(double value) throws TransformException {
int index = lastUsed;
final double minimum = minimums[index];
if (value >= minimum ? (index+1 < minimums.length && value >= minimums[index+1])
: doubleToRawLongBits(value) != doubleToRawLongBits(minimum))
{
index = binarySearch(minimums, value);
if (index < 0) {
throw new TransformException(Resources.format(Resources.Keys.NoCategoryForValue_1, value));
}
lastUsed = index;
}
value = categories[index].toConverse.transform(value);
if (converseRanges != null) {
double bound;
if (value < (bound = converseRanges[(index << 1) ])) return bound;
if (value > (bound = converseRanges[(index << 1) | 1])) return bound;
}
return value;
}
/**
* Gets the derivative of this function at a value. This method can be invoked only if {@link #categories}
* contains at least two elements, otherwise a {@code MathTransform} implementation from another package is used.
*
* @param value the value where to evaluate the derivative.
* @return the derivative at the specified point.
* @throws TransformException if the derivative can not be evaluated at the specified point.
*/
@Override
public final double derivative(final double value) throws TransformException {
int index = lastUsed;
final double minimum = minimums[index];
if (value >= minimum ? (index+1 < minimums.length && value >= minimums[index+1])
: doubleToRawLongBits(value) != doubleToRawLongBits(minimum))
{
index = binarySearch(minimums, value);
if (index < 0) {
throw new TransformException(Resources.format(Resources.Keys.NoCategoryForValue_1, value));
}
lastUsed = index;
}
return categories[index].toConverse.derivative(value);
}
/**
* Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}.
*/
@Override
public final DirectPosition transform(final DirectPosition ptSrc, DirectPosition ptDst) throws TransformException {
ArgumentChecks.ensureNonNull("ptSrc", ptSrc);
ArgumentChecks.ensureDimensionMatches("ptSrc", 1, ptSrc);
if (ptDst == null) {
ptDst = new GeneralDirectPosition(1);
} else {
ArgumentChecks.ensureDimensionMatches("ptDst", 1, ptDst);
}
ptDst.setOrdinate(0, transform(ptSrc.getOrdinate(0)));
return ptDst;
}
/**
* Gets the derivative of this transform at a point.
*/
@Override
public final Matrix derivative(final DirectPosition point) throws TransformException {
ArgumentChecks.ensureNonNull("point", point);
ArgumentChecks.ensureDimensionMatches("point", 1, point);
return new Matrix1(derivative(point.getOrdinate(0)));
}
/**
* Tests whether this transform does not move any points.
*/
@Override
public boolean isIdentity() {
return converse == this;
}
/**
* Returns the inverse transform of this object, which may be {@code this} if this transform is identity.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public final MathTransform1D inverse() {
return converse;
}
/**
* Gets the dimension of input points, which is 1.
*/
@Override
public final int getSourceDimensions() {
return 1;
}
/**
* Gets the dimension of output points, which is 1.
*/
@Override
public final int getTargetDimensions() {
return 1;
}
/**
* Returns the number of categories in this list.
*/
@Override
public final int size() {
return categories.length;
}
/**
* Returns the element at the specified position in this list.
*/
@Override
public final Category get(final int i) {
return categories[i];
}
/**
* Compares the specified object with this category list for equality.
*/
@Override
public boolean equals(final Object object) {
if (object instanceof CategoryList) {
final CategoryList that = (CategoryList) object;
if (Arrays.equals(categories, that.categories)) {
assert Arrays.equals(minimums, that.minimums);
} else {
return false;
}
}
return super.equals(object);
}
/**
* Returns a <cite>Well Known Text</cite> (WKT) for this object. This operation
* may fail if an object is too complex for the WKT format capability.
*
* @return the Well Know Text for this object.
* @throws UnsupportedOperationException if this object can not be formatted as WKT.
*
* @todo Not yet implemented.
*/
@Override
public String toWKT() throws UnsupportedOperationException {
throw new UnformattableObjectException("Not yet implemented.");
}
}