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 61e556777c68ad48f41bd776e95034eec51e2117 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Aug 3 16:48:14 2022 +0200 Construct a better CRS from grid mapping attributes: - More informative ellipsoid/datum name than "Unnamed". - If the CRS inferred from coordinate variables has 0°…360° longitude range, apply that range to the CRS created from grid mapping attributes. --- .../org/apache/sis/util/resources/Vocabulary.java | 5 + .../sis/util/resources/Vocabulary.properties | 1 + .../sis/util/resources/Vocabulary_fr.properties | 1 + .../org/apache/sis/internal/netcdf/CRSBuilder.java | 1 + .../org/apache/sis/internal/netcdf/CRSMerger.java | 76 +++++++++ .../apache/sis/internal/netcdf/GridMapping.java | 188 +++++++++++++-------- 6 files changed, 199 insertions(+), 73 deletions(-) diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java index f7f2fe3b5b..2bbe39973d 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java @@ -1154,6 +1154,11 @@ public final class Vocabulary extends IndexedResourceBundle { */ public static final short SpatialRepresentation = 184; + /** + * Sphere + */ + public static final short Sphere = 271; + /** * Standard deviation */ diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties index 1ad786ab8e..21efb2b707 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties @@ -234,6 +234,7 @@ Slowness = Slowness Source = Source SouthBound = South bound SpatialRepresentation = Spatial representation +Sphere = Sphere StandardDeviation = Standard deviation StartDate = Start date StartPoint = Start point diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties index 4bff86e19e..39059bff17 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties @@ -241,6 +241,7 @@ Slowness = Lenteur Source = Source SouthBound = Limite sud SpatialRepresentation = Repr\u00e9sentation spatiale +Sphere = Sph\u00e8re StandardDeviation = \u00c9cart type StartDate = Date de d\u00e9part StartPoint = Point de d\u00e9part diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java index 275c40bad2..5e73914d95 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java @@ -446,6 +446,7 @@ previous: for (int i=components.size(); --i >= 0;) { if (range.getMinDouble() >= 0 && range.getMaxDouble() > axis.getMaximumValue()) { referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem) .forConvention(AxesConvention.POSITIVE_RANGE); + coordinateSystem = null; break; } } diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java new file mode 100644 index 0000000000..80c0153d54 --- /dev/null +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java @@ -0,0 +1,76 @@ +/* + * 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.netcdf; + +import org.opengis.util.FactoryException; +import org.opengis.referencing.cs.AxisDirection; +import org.opengis.referencing.cs.EllipsoidalCS; +import org.opengis.referencing.cs.CoordinateSystem; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.internal.referencing.AxisDirections; +import org.apache.sis.internal.referencing.GeodeticObjectBuilder; +import org.apache.sis.referencing.cs.AxesConvention; +import org.apache.sis.referencing.crs.AbstractCRS; +import org.apache.sis.util.Utilities; + + +/** + * Merges the CRS declared in grid mapping attributes with the CRS inferred from coordinate variables. + * The former (called "explicit CRS") may have map projection parameters that are difficult to infer + * from the coordinate variables. The latter (called "implicit CRS") have better name and all required + * dimensions, while the explicit CRS often has only the horizontal dimensions. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +final class CRSMerger extends GeodeticObjectBuilder { + /** + * Creates a new builder for the given netCDF reader. + */ + CRSMerger(final Decoder decoder) { + super(decoder, decoder.listeners.getLocale()); + } + + /** + * Replaces the component starting at given index by the given component, possibly with adjusted longitude range. + * The implicit CRS has been inferred from coordinate variables, while the explicit CRS has been inferred from + * the grid mapping attributes. The explicit CRS is the one to use, but its longitude range is the default one + * because thar range depends on the coordinate variable, which was inspected by the implicit CRS. + * + * @param implicit the coordinate reference system in which to replace a component. + * @param firstDimension index of the first dimension to replace. + * @param explicit the component to insert in place of the CRS component at given index. + * @return a CRS with the component replaced. + * @throws FactoryException if the object creation failed. + */ + @Override + public CoordinateReferenceSystem replaceComponent(final CoordinateReferenceSystem implicit, + final int firstDimension, CoordinateReferenceSystem explicit) throws FactoryException + { + final CoordinateSystem cs = implicit.getCoordinateSystem(); + if (cs instanceof EllipsoidalCS) { + final int i = AxisDirections.indexOfColinear(cs, AxisDirection.EAST); + if (i >= 0 && cs.getAxis(i).getMinimumValue() >= 0) { // The `i >= 0` check is paranoiac. + explicit = AbstractCRS.castOrCopy(explicit).forConvention(AxesConvention.POSITIVE_RANGE); + } + } + final CoordinateReferenceSystem result = super.replaceComponent(implicit, firstDimension, explicit); + return Utilities.equalsIgnoreMetadata(implicit, result) ? implicit : result; + } +} diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java index 839e69c98b..aedeccc301 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java @@ -21,9 +21,15 @@ import java.util.List; import java.util.HashMap; import java.util.Iterator; import java.util.Collections; +import java.util.Locale; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.function.Supplier; +import java.text.NumberFormat; +import java.text.FieldPosition; import java.text.ParseException; +import javax.measure.Unit; +import javax.measure.quantity.Length; import org.opengis.util.FactoryException; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; @@ -58,7 +64,6 @@ import org.apache.sis.internal.referencing.AxisDirections; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridExtent; -import org.apache.sis.internal.referencing.GeodeticObjectBuilder; import org.apache.sis.internal.referencing.provider.PseudoPlateCarree; import org.apache.sis.internal.system.Modules; import org.apache.sis.internal.util.Constants; @@ -80,7 +85,7 @@ import ucar.nc2.constants.CF; * which creates Coordinate Reference Systems by inspecting coordinate system axes. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.3 * * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a> * @@ -89,8 +94,9 @@ import ucar.nc2.constants.CF; */ final class GridMapping { /** - * The Coordinate Reference System, or {@code null} if none. This CRS can be constructed from Well Known Text - * or EPSG codes declared in {@code "spatial_ref"}, {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes. + * The Coordinate Reference System inferred from grid mapping attribute values, or {@code null} if none. + * This CRS may have been constructed from Well Known Text or EPSG codes declared in {@code "spatial_ref"}, + * {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes. * * <div class="note"><b>Note:</b> this come from different information than the one used by {@link CRSBuilder}, * which creates CRS by inspection of coordinate system axes.</div> @@ -104,21 +110,27 @@ final class GridMapping { private final MathTransform gridToCRS; /** - * Whether the {@link #crs} where defined by an EPSG code. + * Whether the {@link #crs} was defined by a WKT string. */ - private final boolean isEPSG; + private final boolean isWKT; /** * Creates an instance for the given {@link #crs} and {@link #gridToCRS} values. + * + * @param crs CRS inferred from grid mapping attribute values, or {@code null} if none. + * @param gridToCRS transform from GDAL conventions, or {@code null} if none. + * @param isWKT wether the {@code crs} was defined by a WKT string. */ - private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isEPSG) { + private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isWKT) { this.crs = crs; this.gridToCRS = gridToCRS; - this.isEPSG = isEPSG; + this.isWKT = isWKT; } /** * Fetches grid geometry information from attributes associated to the given variable. + * This method should be invoked only once per variable, but may return a shared {@code GridMapping} instance + * for all variables because there is typically only one set of grid mapping attributes for the whole file. * * @param variable the variable for which to create a grid geometry. */ @@ -302,7 +314,8 @@ final class GridMapping { final PrimeMeridian meridian; if (greenwichLongitude instanceof Number) { final double longitude = ((Number) greenwichLongitude).doubleValue(); - final Map<String,?> properties = properties(definition, Convention.PRIME_MERIDIAN_NAME, null); + final Map<String,?> properties = properties(definition, + Convention.PRIME_MERIDIAN_NAME, (longitude == 0) ? "Greenwich" : null); meridian = datumFactory.createPrimeMeridian(properties, longitude, Units.DEGREE); isSpecified = true; } else { @@ -316,14 +329,34 @@ final class GridMapping { */ Ellipsoid ellipsoid; try { - final double semiMajor = parameters.parameter(Constants.SEMI_MAJOR).doubleValue(); - final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, null); - if (parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue()) { - final double ivf = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue(); - ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, ivf, Units.METRE); + final ParameterValue<?> p = parameters.parameter(Constants.SEMI_MAJOR); + final Unit<Length> axisUnit = p.getUnit().asType(Length.class); + final double semiMajor = p.doubleValue(); + final double secondDefiningParameter; + final boolean isSphere; + final boolean isIvfDefinitive = parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue(); + if (isIvfDefinitive) { + secondDefiningParameter = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue(); + isSphere = (secondDefiningParameter == 0) || Double.isInfinite(secondDefiningParameter); + } else { + secondDefiningParameter = parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit); + isSphere = secondDefiningParameter == semiMajor; + } + final Supplier<Object> fallback = () -> { // Default ellipsoid name if not specified. + final Locale locale = decoder.listeners.getLocale(); + final NumberFormat f = NumberFormat.getNumberInstance(locale); + f.setMaximumFractionDigits(5); // Centimetric precision. + final double km = axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor); + final StringBuffer b = new StringBuffer() + .append(Vocabulary.getResources(locale).getString(isSphere ? Vocabulary.Keys.Sphere : Vocabulary.Keys.Ellipsoid)) + .append(isSphere ? " R=" : " a="); + return f.format(km, b, new FieldPosition(0)).append(" km").toString(); + }; + final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, fallback); + if (isIvfDefinitive) { + ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, secondDefiningParameter, axisUnit); } else { - final double semiMinor = parameters.parameter(Constants.SEMI_MINOR).doubleValue(); - ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, semiMinor, Units.METRE); + ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, secondDefiningParameter, axisUnit); } isSpecified = true; } catch (ParameterNotFoundException | IllegalStateException e) { @@ -371,13 +404,15 @@ final class GridMapping { private static Map<String,Object> properties(final Map<String,Object> definition, final String nameAttribute, final Object fallback) { Object name = definition.remove(nameAttribute); if (name == null) { - if (fallback instanceof IdentifiedObject) { + if (fallback == null) { + // Note: IdentifiedObject.name does not accept InternationalString. + name = Vocabulary.format(Vocabulary.Keys.Unnamed); + } else if (fallback instanceof IdentifiedObject) { name = ((IdentifiedObject) fallback).getName(); - } else if (fallback != null) { - name = fallback.toString(); + } else if (fallback instanceof Supplier<?>) { + name = ((Supplier<?>) fallback).get(); } else { - name = Vocabulary.format(Vocabulary.Keys.Unnamed); - // Note: IdentifiedObject.name does not accept InternationalString. + name = fallback.toString(); } } return Collections.singletonMap(IdentifiedObject.NAME_KEY, name); @@ -424,7 +459,7 @@ final class GridMapping { } catch (ParseException | NumberFormatException e) { canNotCreate(mapping, message, e); } - return new GridMapping(crs, gridToCRS, false); + return new GridMapping(crs, gridToCRS, wkt != null); } /** @@ -444,14 +479,13 @@ final class GridMapping { * @return whether this method found grid geometry attributes. */ private static GridMapping parseNonStandard(final Node variable) { - boolean isEPSG = false; String code = variable.getAttributeAsString("ESRI_pe_string"); - if (code == null) { + final boolean isWKT = (code != null); + if (!isWKT) { code = variable.getAttributeAsString("EPSG_code"); if (code == null) { return null; } - isEPSG = true; } /* * The Coordinate Reference System stored in those attributes often use the GeoTIFF flavor of EPSG codes, @@ -462,16 +496,16 @@ final class GridMapping { */ CoordinateReferenceSystem crs; try { - if (isEPSG) { - crs = CRS.forCode(Constants.EPSG + ':' + isEPSG); - } else { + if (isWKT) { crs = createFromWKT(variable, code); + } else { + crs = CRS.forCode(Constants.EPSG + ':' + code); } } catch (FactoryException | ParseException | ClassCastException e) { canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e); crs = null; } - return new GridMapping(crs, null, isEPSG); + return new GridMapping(crs, null, isWKT); } /** @@ -512,10 +546,12 @@ final class GridMapping { } /** - * Creates a new grid geometry for the given extent. - * This method should be invoked only when no existing {@link GridGeometry} can be used as template. + * Creates a new grid geometry with the extent of the given variable and a potentially null CRS. + * This method should be invoked only as a fallback when no existing {@link GridGeometry} can be used. + * The CRS and "grid to CRS" transform are null, unless some partial information was found for example + * as WKT string. */ - GridGeometry createGridCRS(final Variable variable) { + final GridGeometry createGridCRS(final Variable variable) { final List<Dimension> dimensions = variable.getGridDimensions(); final long[] upper = new long[dimensions.size()]; for (int i=0; i<upper.length; i++) { @@ -526,42 +562,48 @@ final class GridMapping { } /** - * Creates the grid geometry from the {@link #crs} and {@link #gridToCRS} field, - * completing missing information with the given template. + * Creates the grid geometry from the {@link #crs} and {@link #gridToCRS} fields, + * completing missing information with the implicit grid geometry derived from coordinate variables. + * For example {@code GridMapping} may contain information only about the horizontal dimensions, so + * the given {@code implicit} geometry is used for completing with vertical and temporal dimensions. * * @param variable the variable for which to create a grid geometry. - * @param template template to use for completing missing information. + * @param implicit template to use for completing missing information. * @param anchor whether we computed "grid to CRS" transform relative to pixel center or pixel corner. * @return the grid geometry with modified CRS and "grid to CRS" transform, or {@code null} in case of failure. */ - GridGeometry adaptGridCRS(final Variable variable, final GridGeometry template, final PixelInCell anchor) { - CoordinateReferenceSystem givenCRS = crs; + final GridGeometry adaptGridCRS(final Variable variable, final GridGeometry implicit, final PixelInCell anchor) { + /* + * The CRS and grid geometry built from grid mapping attributes are called "explicit" in this method. + * This is by contrast with CRS derived from coordinate variables, which is only implicit. + */ + CoordinateReferenceSystem explicitCRS = crs; int firstAffectedCoordinate = 0; boolean isSameGrid = true; - if (template.isDefined(GridGeometry.CRS)) { - final CoordinateReferenceSystem templateCRS = template.getCoordinateReferenceSystem(); - if (givenCRS == null) { - givenCRS = templateCRS; + if (implicit.isDefined(GridGeometry.CRS)) { + final CoordinateReferenceSystem implicitCRS = implicit.getCoordinateReferenceSystem(); + if (explicitCRS == null) { + explicitCRS = implicitCRS; } else { /* - * The CRS built by Grid may have a different axis order than the CRS specified by grid mapping attributes. - * Check which axis order seems to fit, then replace grid CRS by given CRS (potentially with swapped axes). - * This is where the potential difference between EPSG axis order and grid axis order is handled. If we can - * not find where to substitute the CRS, assume that the given CRS describes the first dimensions. We have - * no guarantees that this later assumption is right, but it seems to match common practice. + * The CRS built by the `Grid` class (based on an inspection of coordinate variables) + * may have a different axis order than the CRS specified by grid mapping attributes + * (the CRS built by this class). This block checks which axis order seems to fit, + * then potentially replaces `Grid` implicit CRS by `GridMapping` explicit CRS. + * + * This is where the potential difference between EPSG axis order and grid axis order is handled. + * If we can not find which component to replace, assume that grid mapping describes the first dimensions. + * We have no guarantees that this latter assumption is right, but it seems to match common practice. */ - final CoordinateSystem cs = templateCRS.getCoordinateSystem(); - CoordinateSystem subCS = givenCRS.getCoordinateSystem(); - firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, subCS); + final CoordinateSystem cs = implicitCRS.getCoordinateSystem(); + firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem()); if (firstAffectedCoordinate < 0) { - givenCRS = AbstractCRS.castOrCopy(givenCRS).forConvention(AxesConvention.RIGHT_HANDED); - subCS = givenCRS.getCoordinateSystem(); - firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, subCS); + explicitCRS = AbstractCRS.castOrCopy(explicitCRS).forConvention(AxesConvention.RIGHT_HANDED); + firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem()); if (firstAffectedCoordinate < 0) { firstAffectedCoordinate = 0; - if (!isEPSG) { - givenCRS = crs; // If specified by WKT, use the given CRS verbatim. - subCS = givenCRS.getCoordinateSystem(); + if (isWKT && crs != null) { + explicitCRS = crs; // If specified by WKT, use the CRS verbatim. } } } @@ -570,15 +612,15 @@ final class GridMapping { * axis order. If the grid CRS contains more axes (for example elevation or time axis), we try to keep them. */ try { - givenCRS = new GeodeticObjectBuilder(variable.decoder, variable.decoder.listeners.getLocale()) - .replaceComponent(templateCRS, firstAffectedCoordinate, givenCRS); + explicitCRS = new CRSMerger(variable.decoder) + .replaceComponent(implicitCRS, firstAffectedCoordinate, explicitCRS); } catch (FactoryException e) { canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e); return null; } - isSameGrid = templateCRS.equals(givenCRS); + isSameGrid = implicitCRS.equals(explicitCRS); if (isSameGrid) { - givenCRS = templateCRS; // Keep existing instance if appropriate. + explicitCRS = implicitCRS; // Keep existing instance if appropriate. } } } @@ -588,31 +630,31 @@ final class GridMapping { * then we need to perform selection in target dimensions (not source dimensions) because the first affected * coordinate computed above is in CRS dimension, which is the target of "grid to CRS" transform. */ - MathTransform givenG2C = gridToCRS; - if (template.isDefined(GridGeometry.GRID_TO_CRS)) { - final MathTransform templateG2C = template.getGridToCRS(anchor); - if (givenG2C == null) { - givenG2C = templateG2C; + MathTransform explicitG2C = gridToCRS; + if (implicit.isDefined(GridGeometry.GRID_TO_CRS)) { + final MathTransform implicitG2C = implicit.getGridToCRS(anchor); + if (explicitG2C == null) { + explicitG2C = implicitG2C; } else try { int count = 0; MathTransform[] components = new MathTransform[3]; - final TransformSeparator sep = new TransformSeparator(templateG2C, variable.decoder.getMathTransformFactory()); + final TransformSeparator sep = new TransformSeparator(implicitG2C, variable.decoder.getMathTransformFactory()); if (firstAffectedCoordinate != 0) { sep.addTargetDimensionRange(0, firstAffectedCoordinate); components[count++] = sep.separate(); sep.clear(); } - components[count++] = givenG2C; - final int next = firstAffectedCoordinate + givenG2C.getTargetDimensions(); - final int upper = templateG2C.getTargetDimensions(); + components[count++] = explicitG2C; + final int next = firstAffectedCoordinate + explicitG2C.getTargetDimensions(); + final int upper = implicitG2C.getTargetDimensions(); if (next != upper) { sep.addTargetDimensionRange(next, upper); components[count++] = sep.separate(); } components = ArraysExt.resize(components, count); - givenG2C = MathTransforms.compound(components); - if (templateG2C.equals(givenG2C)) { - givenG2C = templateG2C; // Keep using existing instance if appropriate. + explicitG2C = MathTransforms.compound(components); + if (implicitG2C.equals(explicitG2C)) { + explicitG2C = implicitG2C; // Keep using existing instance if appropriate. } else { isSameGrid = false; } @@ -626,9 +668,9 @@ final class GridMapping { * If any of them have changed, create the new grid geometry. */ if (isSameGrid) { - return template; + return implicit; } else { - return new GridGeometry(template.getExtent(), anchor, givenG2C, givenCRS); + return new GridGeometry(implicit.getExtent(), anchor, explicitG2C, explicitCRS); } } }