blob: cf97915b6e7fc7780cf99927c8e9d6613b57c5c7 [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.feature;
import java.util.Arrays;
import java.util.Set;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Optional;
import org.opengis.util.GenericName;
import org.opengis.util.FactoryException;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.feature.privy.AttributeConvention;
import org.apache.sis.feature.privy.FeatureUtilities;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryWrapper;
import org.apache.sis.util.privy.CollectionsExt;
import org.apache.sis.referencing.CRS;
import org.apache.sis.util.resources.Errors;
/**
* An operation computing the envelope that encompass all geometries found in a list of attributes.
* Geometries can be in different coordinate reference systems; they will be transformed to the first
* non-null CRS in the following choices:
*
* <ol>
* <li>the CRS specified at construction time,</li>
* <li>the CRS of the default geometry, or</li>
* <li>the CRS of the first non-empty geometry.</li>
* </ol>
*
* <h2>Limitations</h2>
* If a geometry contains other geometries, this operation queries only the envelope of the root geometry.
* It is the root geometry responsibility to take in account the envelope of all its children.
*
* <p>This operation is read-only. Calls to {@code Attribute.setValue(Envelope)} will result in an
* {@link IllegalStateException} to be thrown.</p>
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
*/
final class EnvelopeOperation extends AbstractOperation {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 8034615858550405350L;
/**
* The parameter descriptor for the "Envelope" operation, which does not take any parameter.
*/
private static final ParameterDescriptorGroup EMPTY_PARAMS = FeatureUtilities.parameters("Envelope");
/**
* The names of all properties containing a geometry object.
*/
private final String[] attributeNames;
/**
* The coordinate reference system of the envelope to compute, or {@code null}
* for using the CRS of the default geometry or the first non-empty geometry.
* Note that this is the CRS desired by user of this {@link EnvelopeOperation};
* it may be unrelated to the CRS of stored geometries.
*/
@SuppressWarnings("serial") // Most SIS implementations are serializable.
final CoordinateReferenceSystem targetCRS;
/**
* The coordinate conversions or transformations from the CRS used by the geometries to the CRS requested
* by the user, or {@code null} if there is no operation to apply. If non-null, the length of this array
* shall be equal to the length of the {@link #attributeNames} array and element at index <var>i</var> is
* the operation from the {@code attributeNames[i]} geometry CRS to the {@link #targetCRS}. It may be the
* identity operation, and may also be {@code null} if the property at index <var>i</var> does not declare
* a default CRS.
*
* <h4>Performance note</h4>
* If this array is {@code null}, then {@link AbstractFeature#getProperty(String)} does not need to be invoked at all.
* A null array is a signal that invoking only the cheaper {@link AbstractFeature#getPropertyValue(String)} method is
* sufficient. However, this array become non-null as soon as there is at least one CRS characteristic to check.
* We do not distinguish which particular property may have a CRS characteristic because as of Apache SIS 1.0,
* implementations of {@link DenseFeature} and {@link SparseFeature} have a "all of nothing" behavior anyway.
* So there is no performance gain to expect from a fine-grained knowledge of which properties declare a CRS.
*/
@SuppressWarnings("serial") // Most SIS implementations are serializable.
private final CoordinateOperation[] attributeToCRS;
/**
* The property names as an unmodifiable set, created when first needed.
*/
private transient Set<String> dependencies;
/**
* The type of the result returned by the envelope operation.
*/
private final DefaultAttributeType<Envelope> resultType;
/**
* Creates a new operation computing the envelope of features of the given type.
*
* @param identification the name and other information to be given to this operation.
* @param targetCRS the coordinate reference system of envelopes to computes, or {@code null}.
* @param geometryAttributes the operation or attribute type from which to get geometry values.
*/
EnvelopeOperation(final Map<String,?> identification, CoordinateReferenceSystem targetCRS,
final AbstractIdentifiedType[] geometryAttributes) throws FactoryException
{
super(identification);
String defaultGeometry = null;
/*
* Get all property names without duplicated values. If a property is a link to an attribute,
* then the key will be the name of the referenced attribute instead of the operation name.
* The intent is to avoid querying the same geometry twice if the attribute is also specified
* explicitly in the array of properties.
*
* The map values will be the default Coordinate Reference System, or null if none.
*/
boolean characterizedByCRS = false;
final Map<String,CoordinateReferenceSystem> names = new LinkedHashMap<>(4);
for (final AbstractIdentifiedType property : geometryAttributes) {
final Optional<DefaultAttributeType<?>> at = Features.toAttribute(property);
if (at.isPresent() && Geometries.isKnownType(at.get().getValueClass())) {
final GenericName name = property.getName();
final String attributeName = (property instanceof LinkOperation)
? ((LinkOperation) property).referentName : name.toString();
final boolean isDefault = AttributeConvention.GEOMETRY_PROPERTY.equals(name);
if (isDefault) {
defaultGeometry = attributeName;
}
CoordinateReferenceSystem attributeCRS = null;
/*
* Set `characterizedByCRS` to true if we find at least one attribute which may have the
* "CRS" characteristic. Note that we cannot rely on `attributeCRS` being non-null
* because an attribute may be characterized by a CRS without providing default CRS.
*/
final DefaultAttributeType<?> ct = at.get().characteristics().get(AttributeConvention.CRS);
if (ct != null && CoordinateReferenceSystem.class.isAssignableFrom(ct.getValueClass())) {
attributeCRS = (CoordinateReferenceSystem) ct.getDefaultValue(); // May still null.
if (targetCRS == null && isDefault) {
targetCRS = attributeCRS;
}
characterizedByCRS = true;
}
names.putIfAbsent(attributeName, attributeCRS);
}
}
/*
* Copy the names in an array with the default geometry first. If possible, find the coordinate operations
* now in order to avoid the potentially costly call to CRS.findOperation(…) for each feature on which this
* EnvelopeOperation will be applied.
*/
names.remove(null); // Paranoiac safety.
attributeNames = new String[names.size()];
attributeToCRS = characterizedByCRS ? new CoordinateOperation[attributeNames.length] : null;
int n = (defaultGeometry == null) ? 0 : 1;
for (final Map.Entry<String,CoordinateReferenceSystem> entry : names.entrySet()) {
final int i;
final String name = entry.getKey();
if (name.equals(defaultGeometry)) {
defaultGeometry = null;
i = 0;
} else {
i = n++;
}
attributeNames[i] = name;
if (characterizedByCRS) {
final CoordinateReferenceSystem value = entry.getValue();
if (value != null) {
if (targetCRS == null) {
targetCRS = value; // Fallback if default geometry has no CRS.
}
/*
* The following operation is often identity. We do not filter identity operations
* because their source CRS is still a useful information (it is the CRS instance
* found in the attribute characteristic, not necessarily identical to `targetCRS`)
* and because we keep the null value for meaning that attribute CRS is unspecified.
*/
attributeToCRS[i] = CRS.findOperation(value, targetCRS, null);
}
}
}
resultType = FeatureOperations.POOL.unique(new DefaultAttributeType<>(
resultIdentification(identification), Envelope.class, 1, 1, null));
this.targetCRS = targetCRS;
}
/**
* Returns an empty group of parameters since this operation does not require any parameter.
*
* @return empty parameter group.
*/
@Override
public ParameterDescriptorGroup getParameters() {
return EMPTY_PARAMS;
}
/**
* Returns the type of results computed by this operation, which is {@code AttributeType<Envelope>}.
* The attribute type name depends on the value of {@code "result.*"} properties (if any)
* given at construction time.
*
* @return an {@code AttributeType<Envelope>}.
*/
@Override
public AbstractIdentifiedType getResult() {
return resultType;
}
/**
* Returns the names of feature properties that this operation needs for performing its task.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public synchronized Set<String> getDependencies() {
if (dependencies == null) {
dependencies = CollectionsExt.immutableSet(true, attributeNames);
}
return dependencies;
}
/**
* Returns an attribute whose value is the union of the envelopes of all geometries in the given feature
* found in properties specified at construction time.
*
* @param feature the feature on which to execute the operation.
* @param parameters ignored (can be {@code null}).
* @return the envelope of geometries in feature property values.
*/
@Override
public Property apply(AbstractFeature feature, ParameterValueGroup parameters) {
return new Result(feature);
}
/**
* The attributes that contains the result of union of all envelope extracted from other attributes.
* Value is calculated each time it is accessed.
*/
private final class Result extends OperationResult<Envelope> {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 4900962888075807964L;
/**
* Creates a new attribute for the given feature.
*/
Result(final AbstractFeature feature) {
super(resultType, feature);
}
/**
* Computes an envelope which is the union of envelope of geometry values of all properties
* specified to the {@link EnvelopeOperation} constructor.
*
* @return the union of envelopes of all geometries in the attribute specified to the constructor,
* or {@code null} if none.
* @throws FeatureOperationException if the envelope cannot be computed.
*/
@Override
public Envelope getValue() throws FeatureOperationException {
final String[] attributeNames = EnvelopeOperation.this.attributeNames;
GeneralEnvelope envelope = null; // Union of all envelopes.
GeneralEnvelope[] deferred = null; // Envelopes not yet included in union envelope.
boolean hasUnknownCRS = false; // Whether at least one geometry has no known CRS.
for (int i = 0; i < attributeNames.length; i++) {
/*
* Call `Feature.getPropertyValue(…)` instead of `Feature.getProperty(…).getValue()`
* in order to avoid forcing DenseFeature and SparseFeature implementations to wrap
* the property values into new `Property` objects. The potentially costly call to
* `Feature.getProperty(…)` can be avoided in two scenarios:
*
* - The constructor determined that no attribute should have CRS characteristics.
* This scenario is identified by (attributeToCRS == null).
*
* - The geometry already declares its CRS, in which case that CRS has precedence
* over attribute characteristics, so we don't need to fetch them.
*
* Inconvenient is that in a third scenario (CRS is defined by attribute characteristics),
* we will do two calls to some `Feature.getProperty…` method, which results in two lookups
* in hash table. We presume that the gain from the optimistic assumption is worth the cost.
*/
GeneralEnvelope genv = Geometries.wrap(feature.getPropertyValue(attributeNames[i]))
.map(GeometryWrapper::getEnvelope).orElse(null);
if (genv == null) {
continue;
}
/*
* Get the CRS either directly from the geometry or indirectly from property characteristic.
* The CRS associated with the geometry will be kept consistent with `sourceCRS` and will be
* null only if no CRS has been found anywhere. Note that `sourceCRS` may be different than
* `op.getSourceCRS()`. This difference will be handled by `Envelopes.transform(…)` later.
*/
CoordinateReferenceSystem sourceCRS = genv.getCoordinateReferenceSystem();
CoordinateOperation op = null;
if (attributeToCRS != null) {
op = attributeToCRS[i];
if (sourceCRS == null) {
/*
* Try to get CRS from property characteristic. Usually `at` is null and we fallback
* on the coordinate operation computed at construction time. In the rare case where
* a CRS characteristic is associated to a particular feature, setting `op` to null
* will cause a new coordinate operation to be searched.
*/
final AbstractAttribute<?> at = ((AbstractAttribute<?>) feature.getProperty(attributeNames[i]))
.characteristics().get(AttributeConvention.CRS);
final Object geomCRS;
if (at != null && (geomCRS = at.getValue()) != null) {
if (!(geomCRS instanceof CoordinateReferenceSystem)) {
throw new FeatureOperationException(Resources.formatInternational(
Resources.Keys.IllegalCharacteristicsType_3,
AttributeConvention.CRS_CHARACTERISTIC,
CoordinateReferenceSystem.class,
geomCRS.getClass()));
}
sourceCRS = (CoordinateReferenceSystem) geomCRS;
} else if (op != null) {
sourceCRS = op.getSourceCRS();
}
genv.setCoordinateReferenceSystem(sourceCRS);
}
}
/*
* If the geometry CRS is unknown (sourceCRS == null), leave the geometry CRS to null.
* Do not set it to `targetCRS` because that value is the desired CRS, not necessarily
* the actual CRS.
*/
if (sourceCRS != null && targetCRS != null) try {
if (op == null) {
op = CRS.findOperation(sourceCRS, targetCRS, null);
}
if (!op.getMathTransform().isIdentity()) {
genv = Envelopes.transform(op, genv);
}
} catch (FactoryException | TransformException e) {
throw new FeatureOperationException(Errors.formatInternational(Errors.Keys.CanNotTransformEnvelope), e);
}
/*
* If there is only one geometry, we will return that geometry as-is even if its CRS is unknown.
* It will be up to the user to decide what to do with that. Otherwise (two or more geometries)
* we throw an exception if a CRS is unknown, because we don't know how to combine them.
*/
hasUnknownCRS |= (sourceCRS == null);
if (envelope == null) {
envelope = genv;
} else if (hasUnknownCRS) {
throw new FeatureOperationException(Errors.formatInternational(Errors.Keys.UnspecifiedCRS));
} else if (targetCRS != null) {
envelope.add(genv);
} else {
if (deferred == null) {
deferred = new GeneralEnvelope[attributeNames.length];
deferred[0] = envelope;
}
deferred[i] = genv;
}
}
if (deferred == null) {
return envelope;
} else try {
return Envelopes.union(deferred);
} catch (TransformException e) {
throw new FeatureOperationException(Errors.formatInternational(Errors.Keys.CanNotTransformEnvelope), e);
}
}
}
/**
* Computes a hash-code value for this operation.
*/
@Override
public int hashCode() {
return super.hashCode() + Arrays.hashCode(attributeNames) + Arrays.hashCode(attributeToCRS);
}
/**
* Compares this operation with the given object for equality.
*/
@Override
public boolean equals(final Object obj) {
if (super.equals(obj)) {
// `this.result` is compared (indirectly) by the super class.
final EnvelopeOperation that = (EnvelopeOperation) obj;
return Arrays.equals(attributeNames, that.attributeNames) &&
Arrays.equals(attributeToCRS, that.attributeToCRS) &&
Objects.equals(targetCRS, that.targetCRS);
}
return false;
}
}