| /* |
| * 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; |
| } |
| } |