| /* |
| * 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 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.internal.feature.AttributeConvention; |
| import org.apache.sis.internal.feature.FeatureUtilities; |
| import org.apache.sis.internal.feature.Geometries; |
| import org.apache.sis.internal.util.CollectionsExt; |
| import org.apache.sis.geometry.Envelopes; |
| import org.apache.sis.geometry.GeneralEnvelope; |
| 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> |
| * |
| * <div class="section">Limitations</div> |
| * 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) |
| * @version 0.7 |
| * @since 0.7 |
| * @module |
| */ |
| final class EnvelopeOperation extends AbstractOperation { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 6250548001562807671L; |
| |
| /** |
| * 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. |
| */ |
| final CoordinateReferenceSystem crs; |
| |
| /** |
| * 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 #crs}. |
| * |
| * <p>This array contains null element when the {@code MathTransform} associated to the coordinate operation |
| * is the identity transform.</p> |
| */ |
| 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 crs 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 crs, |
| final AbstractIdentifiedType[] geometryAttributes) throws FactoryException |
| { |
| super(identification); |
| String defaultGeometry = null; |
| final String characteristicName = AttributeConvention.CRS_CHARACTERISTIC.toString(); |
| /* |
| * 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 than 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 (AbstractIdentifiedType property : geometryAttributes) { |
| if (AttributeConvention.isGeometryAttribute(property)) { |
| 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; |
| while (property instanceof AbstractOperation) { |
| property = ((AbstractOperation) property).getResult(); |
| } |
| /* |
| * At this point 'property' is an attribute, otherwise isGeometryAttribute(property) would have |
| * returned false. Set 'characterizedByCRS' to true if we find at least one attribute which may |
| * have the "CRS" characteristic. Note that we can not rely on 'attributeCRS' being non-null |
| * because an attribute may be characterized by a CRS without providing default CRS. |
| */ |
| final DefaultAttributeType<?> at = ((DefaultAttributeType<?>) property).characteristics().get(characteristicName); |
| if (at != null && CoordinateReferenceSystem.class.isAssignableFrom(at.getValueClass())) { |
| attributeCRS = (CoordinateReferenceSystem) at.getDefaultValue(); // May still null. |
| if (crs == null && isDefault) { |
| crs = 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 (crs == null) { |
| crs = value; // Fallback if default geometry has no CRS. |
| } |
| final CoordinateOperation op = CRS.findOperation(value, crs, null); |
| if (!op.getMathTransform().isIdentity()) { |
| attributeToCRS[i] = op; |
| } |
| } |
| } |
| } |
| resultType = FeatureOperations.POOL.unique(new DefaultAttributeType<>( |
| resultIdentification(identification), Envelope.class, 1, 1, null)); |
| this.crs = crs; |
| } |
| |
| /** |
| * 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 AbstractAttribute<Envelope> { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 926172863066901618L; |
| |
| /** |
| * The feature specified to the {@code StringJoinOperation.apply(Feature, ParameterValueGroup)} method. |
| */ |
| private final AbstractFeature feature; |
| |
| /** |
| * Creates a new attribute for the given feature. |
| */ |
| Result(final AbstractFeature feature) { |
| super(resultType); |
| this.feature = 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. |
| */ |
| @Override |
| public Envelope getValue() throws IllegalStateException { |
| final String[] attributeNames = EnvelopeOperation.this.attributeNames; |
| GeneralEnvelope envelope = null; // Union of all envelopes. |
| for (int i=0; i<attributeNames.length; i++) { |
| Envelope genv; // Envelope of a single geometry. |
| final String name = attributeNames[i]; |
| if (attributeToCRS == null) { |
| /* |
| * If there is no CRS characteristic on any of the properties to query, then invoke the |
| * Feature.getPropertyValue(String) method instead than Feature.getProperty(String) in |
| * order to avoid forcing DenseFeature and SparseFeature implementations to wrap the |
| * property values into real property instances. This is an optimization for reducing |
| * the amount of objects to create. |
| */ |
| genv = Geometries.getEnvelope(feature.getPropertyValue(name)); |
| if (genv == null) continue; |
| } else { |
| /* |
| * If there is at least one CRS characteristic to query, then we need the full Property instance. |
| * We do not distinguish which particular property may have a CRS characteristic because SIS 0.7 |
| * implementations of DenseFeature and SparseFeature have a "all of nothing" behavior anyway. |
| */ |
| final Property property = (Property) feature.getProperty(name); |
| genv = Geometries.getEnvelope(property.getValue()); |
| if (genv == null) continue; |
| /* |
| * Get the CRS characteristic if present. Most of the time, 'at' will be null and we will |
| * fallback on the 'attributeToCRS' operations computed at construction time. In the rare |
| * cases where a CRS characteristic is associated to a particular feature, we will let |
| * Envelopes.transform(…) searches a coordinate operation. |
| */ |
| final AbstractAttribute<?> at = ((AbstractAttribute<?>) property).characteristics() |
| .get(AttributeConvention.CRS_CHARACTERISTIC.toString()); |
| try { |
| if (at == null) { |
| final CoordinateOperation op = attributeToCRS[i]; |
| if (op != null) { // Null operation means identity transform. |
| genv = Envelopes.transform(op, genv); |
| } |
| } else { // Should be a rare case. |
| final Object geomCRS = at.getValue(); |
| if (!(geomCRS instanceof CoordinateReferenceSystem)) { |
| throw new IllegalStateException(Errors.format(Errors.Keys.UnspecifiedCRS)); |
| } |
| ((GeneralEnvelope) genv).setCoordinateReferenceSystem((CoordinateReferenceSystem) geomCRS); |
| genv = Envelopes.transform(genv, crs); |
| } |
| } catch (TransformException e) { |
| throw new IllegalStateException(Errors.format(Errors.Keys.CanNotTransformEnvelope), e); |
| } |
| } |
| if (envelope == null) { |
| envelope = GeneralEnvelope.castOrCopy(genv); // Should always be a cast without copy. |
| } else { |
| envelope.add(genv); |
| } |
| } |
| return envelope; |
| } |
| |
| /** |
| * Unconditionally throws an {@link UnsupportedOperationException}. |
| */ |
| @Override |
| public void setValue(Envelope value) { |
| throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, AbstractAttribute.class)); |
| } |
| } |
| |
| /** |
| * 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(crs, that.crs); |
| } |
| return false; |
| } |
| } |