This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 7e0a63e4511da34d79bf95baffca1282c81db417 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Sep 23 11:34:28 2022 +0200 Add convenience method for deriving a projection from an existing one, and for resampling a grid coverage by specifying only the new CRS. --- .../sis/coverage/grid/GridCoverageProcessor.java | 24 +++++ .../referencing/GeodeticObjectBuilder.java | 102 +++++++++++++++++---- .../factory/CommonAuthorityFactory.java | 2 +- .../referencing/GeodeticObjectBuilderTest.java | 57 ++++++++++++ .../sis/test/suite/ReferencingTestSuite.java | 1 + .../apache/sis/storage/landsat/MetadataReader.java | 2 +- 6 files changed, 167 insertions(+), 21 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java index db6cc2652b..9733d1fc19 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -27,6 +27,8 @@ import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import javax.measure.Quantity; import org.opengis.util.FactoryException; +import org.opengis.referencing.datum.PixelInCell; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; import org.apache.sis.coverage.RegionOfInterest; @@ -480,6 +482,28 @@ public class GridCoverageProcessor implements Cloneable { return resampled.forConvertedValues(isConverted); } + /** + * Creates a new coverage with a different coordinate reference system. + * The grid extent and "grid to CRS" transform are determined automatically + * with default values preserving the resolution of source coverage at its + * {@linkplain GridExtent#getPointOfInterest(PixelInCell) point of interest}. + * + * <p>See {@link #resample(GridCoverage, GridGeometry)} for more information + * about interpolation and allowed optimizations.</p> + * + * @param source the grid coverage to resample. + * @param target the desired coordinate reference system. + * @return a grid coverage with the given coordinate reference system. + * @throws IncompleteGridGeometryException if the source grid geometry is missing an information. + * @throws TransformException if some coordinates can not be transformed to the specified target. + * + * @since 1.3 + */ + public GridCoverage resample(final GridCoverage source, final CoordinateReferenceSystem target) throws TransformException { + ArgumentChecks.ensureNonNull("target", target); + return resample(source, new GridGeometry(null, PixelInCell.CELL_CENTER, null, target)); + } + /** * Invoked when an ignorable exception occurred. * diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java index 62ef1b36ee..bbd3da6fd4 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java @@ -20,12 +20,15 @@ import java.util.Map; import java.util.Date; import java.util.Collections; import java.util.Locale; +import java.util.function.BiConsumer; import javax.measure.Unit; import javax.measure.quantity.Time; import javax.measure.quantity.Length; import org.opengis.util.GenericName; import org.opengis.util.FactoryException; +import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; +import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.InvalidParameterValueException; import org.opengis.referencing.IdentifiedObject; @@ -55,11 +58,15 @@ import org.apache.sis.measure.Latitude; import org.apache.sis.referencing.Builder; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.parameter.Parameters; /** * Helper methods for building Coordinate Reference Systems and related objects. * + * In current version, each builder instance should be used for creating only one CRS. + * Reusing the same builder for creating many CRS has unspecified behavior. + * * <p>For now, this class is defined in the internal package because this API needs more experimentation. * However this class may move in a public package later if we feel confident that its API is mature enough.</p> * @@ -105,30 +112,20 @@ public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> { private final Locale locale; /** - * Creates a new builder. + * Creates a new builder with default locale and set of factories. */ public GeodeticObjectBuilder() { - this(null); - } - - /** - * Creates a new builder using the given locale for message in exceptions. - * - * @param locale the locale for error message in exceptions. - */ - public GeodeticObjectBuilder(final Locale locale) { - factories = new ReferencingFactoryContainer(); - this.locale = locale; + this(null, null); } /** * Creates a new builder using the given factories and locale. * - * @param factories the factories to use for geodetic objects creation. - * @param locale the locale for error message in exceptions. + * @param factories the factories to use for geodetic objects creation, or {@code null} for default. + * @param locale the locale for error message in exceptions, or {@code null} for default. */ public GeodeticObjectBuilder(final ReferencingFactoryContainer factories, final Locale locale) { - this.factories = factories; + this.factories = (factories != null) ? factories : new ReferencingFactoryContainer(); this.locale = locale; } @@ -275,6 +272,73 @@ public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> { return this; } + /** + * Replaces the current operation method by a new one with parameter values derived form the old method. + * This method can be invoked for replacing a projection by another one with a similar set of parameters. + * + * <p>If non-null, the given {@code mapper} is used for copying parameter values from the old projection. + * The {@code accept(ParameterValue<?> source, Parameters target)} method is invoked where {@code source} + * is a parameter value of the old projection and {@code target} is the group of parameters where to set + * the values for new projection. If {@code mapper} is null, then the default implementation is as below:</p> + * + * {@preformat java + * target.getOrCreate(source.getDescriptor()).setValue(source.getValue()); + * } + * + * @param newMethod name of the new operation method, or {@code null} if no change. + * @param mapper mapper from old parameters to new parameters, or {@code null} for verbatim copy. + * @return {@code this}, for method calls chaining. + * @throws IllegalStateException if {@link #setConversionMethod(String)} has not been invoked before this method. + * @throws FactoryException if the operation method of the given name can not be obtained. + * @throws ClassCastException if a parameter value of the old projection is not an instance of {@link ParameterValue} + * (this restriction may change in a future version). + */ + public GeodeticObjectBuilder changeConversion(final String newMethod, + BiConsumer<ParameterValue<?>, Parameters> mapper) throws FactoryException + { + ensureConversionMethodSet(); + if (mapper == null) { + mapper = GeodeticObjectBuilder::copyParameterValue; + } + final ParameterValueGroup source = parameters; + if (newMethod != null) { + method = null; + setConversionMethod(newMethod); + } + final Parameters target = Parameters.castOrWrap(parameters); + for (final GeneralParameterValue param : source.values()) { + mapper.accept((ParameterValue<?>) param, target); // ClassCastException is part of current method contract. + } + return this; + } + + /** + * The default {@code mapper} of {@code changeConversion(String, BiConsumer)}. + * + * @param source parameter value of the old projection. + * @param target group of parameters of the new projection where to copy source parameter value. + */ + private static void copyParameterValue(final ParameterValue<?> source, final Parameters target) { + target.getOrCreate(source.getDescriptor()).setValue(source.getValue()); + } + + /** + * Sets the operation method, parameters, conversion name and datum for the same projection than the given CRS. + * Metadata such as domain of validity are inherited, except identifiers. + * + * @param crs the projected CRS from which to inherit the properties. + * @return {@code this}, for method call chaining. + */ + public GeodeticObjectBuilder apply(final ProjectedCRS crs) { + final Conversion c = crs.getConversionFromBase(); + conversionName = c.getName().getCode(); + method = c.getMethod(); + parameters = c.getParameterValues(); + datum = crs.getDatum(); + properties.putAll(IdentifiedObjects.getProperties(crs, ProjectedCRS.IDENTIFIERS_KEY)); + return this; + } + /** * Sets the operation method, parameters and conversion name for a Transverse Mercator projection. * This convenience method delegates to the following methods: @@ -311,8 +375,8 @@ public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> { * * @see CommonCRS#universal(double, double) */ - public GeodeticObjectBuilder setTransverseMercator(TransverseMercator.Zoner zoner, double latitude, double longitude) - throws FactoryException + public GeodeticObjectBuilder applyTransverseMercator(TransverseMercator.Zoner zoner, + double latitude, double longitude) throws FactoryException { ArgumentChecks.ensureBetween("latitude", Latitude.MIN_VALUE, Latitude.MAX_VALUE, latitude); ArgumentChecks.ensureBetween("longitude", -Formulas.LONGITUDE_MAX, Formulas.LONGITUDE_MAX, longitude); @@ -347,7 +411,7 @@ public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> { * @throws FactoryException if the operation method for the Polar Stereographic (variant A) * projection can not be obtained. */ - public GeodeticObjectBuilder setPolarStereographic(final boolean north) throws FactoryException { + public GeodeticObjectBuilder applyPolarStereographic(final boolean north) throws FactoryException { setConversionMethod(PolarStereographicA.NAME); setConversionName(PolarStereographicA.setParameters(parameters, north)); return this; @@ -412,7 +476,7 @@ public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> { } /** - * Creates a projected CRS with base CRS on the specified datum and with default axes. + * Creates a projected CRS with base CRS on the datum previously specified to this builder and with default axes. * The base CRS uses the ellipsoid specified by {@link #setFlattenedSphere(String, double, double, Unit)}. * * @return the projected CRS. diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java index a80f26a879..c301310a72 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java @@ -643,7 +643,7 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements if (isUTM && crs != null) { builder.addName(crs.getName()); } // else default to the conversion name, which is "Transverse Mercator". - builder.setTransverseMercator(isUTM ? Zoner.UTM : Zoner.ANY, latitude, longitude); + builder.applyTransverseMercator(isUTM ? Zoner.UTM : Zoner.ANY, latitude, longitude); } else { builder.setConversionMethod(method) .addName(PROJECTION_NAMES[projection - FIRST_PROJECTION_CODE]) diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/GeodeticObjectBuilderTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/GeodeticObjectBuilderTest.java new file mode 100644 index 0000000000..38f295b7d6 --- /dev/null +++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/GeodeticObjectBuilderTest.java @@ -0,0 +1,57 @@ +/* + * 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.referencing; + +import java.util.function.BiConsumer; +import org.opengis.util.FactoryException; +import org.opengis.referencing.crs.ProjectedCRS; +import org.opengis.parameter.ParameterValueGroup; +import org.apache.sis.measure.Units; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * Tests {@link GeodeticObjectBuilder}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +public final strictfp class GeodeticObjectBuilderTest extends TestCase { + /** + * Tests {@link GeodeticObjectBuilder#changeConversion(String, BiConsumer)}. + * + * @throws FactoryException if an operation method name is not supported. + */ + @Test + public void testChangeConversion() throws FactoryException { + final GeodeticObjectBuilder b = new GeodeticObjectBuilder(); + assertSame(b, b.setConversionName("Dummy projection")); + assertSame(b, b.setConversionMethod("Popular Visualisation Pseudo Mercator")); + assertSame(b, b.setParameter("Longitude of natural origin", 40, Units.DEGREE)); + assertSame(b, b.setParameter("Scale factor at natural origin", 0.5, Units.UNITY)); + assertSame(b, b.changeConversion("Mercator (Spherical)", null)); + final ProjectedCRS crs = b.createProjectedCRS(); + final ParameterValueGroup p = crs.getConversionFromBase().getParameterValues(); + assertEquals(40, p.parameter("Longitude of natural origin").doubleValue(), STRICT); + assertEquals(0.5, p.parameter("Scale factor at natural origin").doubleValue(), STRICT); + } +} diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java index 2e5bbe50ee..a689d192ef 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java @@ -43,6 +43,7 @@ import org.junit.BeforeClass; org.apache.sis.internal.referencing.ExtentSelectorTest.class, org.apache.sis.internal.referencing.WKTKeywordsTest.class, org.apache.sis.internal.referencing.WKTUtilitiesTest.class, + org.apache.sis.internal.referencing.GeodeticObjectBuilderTest.class, org.apache.sis.internal.jaxb.referencing.CodeTest.class, org.apache.sis.internal.jaxb.referencing.SecondDefiningParameterTest.class, diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/MetadataReader.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/MetadataReader.java index 411542f275..9280e64bcf 100644 --- a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/MetadataReader.java +++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/MetadataReader.java @@ -885,7 +885,7 @@ final class MetadataReader extends MetadataBuilder { || projection.parameter(Constants.FALSE_NORTHING) .doubleValue() != 0 || projection.parameter(Constants.CENTRAL_MERIDIAN).doubleValue() != 0) { - crs = new GeodeticObjectBuilder(listeners.getLocale()) + crs = new GeodeticObjectBuilder(factories, listeners.getLocale()) .addName("Polar stereographic").setConversion(projection) .createProjectedCRS(datum.geographic(), crs.getCoordinateSystem()); }