You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2019/01/12 16:55:17 UTC

[sis] branch geoapi-4.0 updated (110cb92 -> 8a7eee3)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 110cb92  Partial revert of previous commit: be overflow-tolerant only if we are going to clip the grid.
     new 639ebdd  Make test class name consistent with tested class.
     new 01d63a7  Move the declaration of ESRI_DATUM_PREFIX constant in order to allow its reuse from other classes.
     new 5f28b17  Improve the efficiency of search of EPSG codes by reducing the amount of GeodeticDatum to create. We do that by filtering on datum names. Note that it requires to take aliases in account.
     new 8a7eee3  Replace some GridGeometry methods by a Modifier inner class.

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/coverage/grid/GridCoverage.java |   5 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |   7 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java | 384 ++++++++++++---------
 .../sis/coverage/grid/SubgridCalculator.java       |  16 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  26 +-
 .../sis/internal/referencing/WKTUtilities.java     |   5 +
 .../sis/referencing/datum/AbstractDatum.java       |  20 +-
 .../referencing/factory/sql/EPSGDataAccess.java    | 299 +++++++++++-----
 .../util/{UtilitiesTest.java => StringsTest.java}  |   2 +-
 .../apache/sis/test/suite/UtilityTestSuite.java    |   2 +-
 10 files changed, 477 insertions(+), 289 deletions(-)
 rename core/sis-utility/src/test/java/org/apache/sis/internal/util/{UtilitiesTest.java => StringsTest.java} (96%)


[sis] 02/04: Move the declaration of ESRI_DATUM_PREFIX constant in order to allow its reuse from other classes.

Posted by de...@apache.org.
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 01d63a7ff4720fbbdc6e70748302ac693b8910ee
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 12 16:03:21 2019 +0100

    Move the declaration of ESRI_DATUM_PREFIX constant in order to allow its reuse from other classes.
---
 .../sis/internal/referencing/WKTUtilities.java       |  5 +++++
 .../apache/sis/referencing/datum/AbstractDatum.java  | 20 ++++++++++----------
 2 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WKTUtilities.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WKTUtilities.java
index 9c32785..a0596a2 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WKTUtilities.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WKTUtilities.java
@@ -68,6 +68,11 @@ import org.apache.sis.math.DecimalFunctions;
  */
 public final class WKTUtilities extends Static {
     /**
+     * The prefix used by ESRI at the beginning of datum names.
+     */
+    public static final String ESRI_DATUM_PREFIX = "D_";
+
+    /**
      * Do not allow instantiation of this class.
      */
     private WKTUtilities() {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/AbstractDatum.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/AbstractDatum.java
index 1355ac2..8d8957d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/AbstractDatum.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/AbstractDatum.java
@@ -42,6 +42,7 @@ import org.apache.sis.io.wkt.Formatter;
 
 import static org.apache.sis.util.Utilities.deepEquals;
 import static org.apache.sis.util.collection.Containers.property;
+import static org.apache.sis.internal.referencing.WKTUtilities.ESRI_DATUM_PREFIX;
 
 // Branch-dependent imports
 import java.util.Objects;
@@ -95,11 +96,6 @@ public class AbstractDatum extends AbstractIdentifiedObject implements Datum {
     private static final long serialVersionUID = -4894180465652474930L;
 
     /**
-     * The prefix used by ESRI at the beginning of datum names.
-     */
-    private static final String ESRI_PREFIX = "D_";
-
-    /**
      * Description, possibly including coordinates, of the point or points used to anchor the datum
      * to the Earth. Also known as the "origin", especially for Engineering and Image Datums.
      *
@@ -364,6 +360,10 @@ public class AbstractDatum extends AbstractIdentifiedObject implements Datum {
     /**
      * A function for simplifying a {@link Datum} name before comparison.
      *
+     * <p>Note: if heuristic rules are modified, consider updating {@code EPSGDataAccess} accordingly.</p>
+     *
+     * @see org.apache.sis.referencing.factory.sql.EPSGDataAccess#toDatumPattern(String, StringBuilder)
+     *
      * @since 0.7
      */
     static class Simplifier extends NameToIdentifier.Simplifier {
@@ -376,8 +376,8 @@ public class AbstractDatum extends AbstractIdentifiedObject implements Datum {
         /** Simplify the given datum name. */
         @Override protected CharSequence apply(CharSequence name) {
             name = super.apply(name);
-            if (CharSequences.startsWith(name, ESRI_PREFIX, false)) {
-                name = name.subSequence(ESRI_PREFIX.length(), name.length());
+            if (CharSequences.startsWith(name, ESRI_DATUM_PREFIX, false)) {
+                name = name.subSequence(ESRI_DATUM_PREFIX.length(), name.length());
             }
             return name;
         }
@@ -463,11 +463,11 @@ public class AbstractDatum extends AbstractIdentifiedObject implements Datum {
         String name = IdentifiedObjects.getName(this, authority);
         if (name == null) {
             name = IdentifiedObjects.getName(this, null);
-            if (name == null) { // Should never happen, but be safe.
+            if (name == null) {                                 // Should never happen, but be safe.
                 return super.formatTo(formatter);
             }
-            if ("ESRI".equalsIgnoreCase(Citations.toCodeSpace(authority)) && !name.startsWith(ESRI_PREFIX)) {
-                name = ESRI_PREFIX + name;
+            if ("ESRI".equalsIgnoreCase(Citations.toCodeSpace(authority)) && !name.startsWith(ESRI_DATUM_PREFIX)) {
+                name = ESRI_DATUM_PREFIX + name;
             }
         }
         formatter.append(name, ElementKind.DATUM);


[sis] 03/04: Improve the efficiency of search of EPSG codes by reducing the amount of GeodeticDatum to create. We do that by filtering on datum names. Note that it requires to take aliases in account.

Posted by de...@apache.org.
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 5f28b17b7a080c2aeaa17943d42c199d98c83987
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 12 16:22:51 2019 +0100

    Improve the efficiency of search of EPSG codes by reducing the amount of GeodeticDatum to create.
    We do that by filtering on datum names. Note that it requires to take aliases in account.
---
 .../referencing/factory/sql/EPSGDataAccess.java    | 299 +++++++++++++++------
 1 file changed, 210 insertions(+), 89 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 986dcd3..3c7f02f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -82,6 +82,7 @@ import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.system.Semaphores;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.metadata.iso.ImmutableIdentifier;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
@@ -120,11 +121,9 @@ import org.apache.sis.measure.Units;
 
 import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
 import static org.apache.sis.internal.util.StandardDateFormat.UTC;
+import static org.apache.sis.internal.referencing.WKTUtilities.ESRI_DATUM_PREFIX;
 import static org.apache.sis.internal.referencing.ServicesForMetadata.CONNECTION;
 
-// Branch-dependent imports
-import org.apache.sis.internal.util.StandardDateFormat;
-
 
 /**
  * <cite>Data Access Object</cite> (DAO) creating geodetic objects from a JDBC connection to an EPSG database.
@@ -516,9 +515,7 @@ addURIs:    for (int i=0; ; i++) {
      * {@code Set.toString()} result, <i>etc.</i> with one exception:
      * a call to {@code Set.contains(…)} will return {@code true} if the given identifier exists
      * for a deprecated object, even if that identifier does not show up in iterations.
-     *
-     * <p>An other point of view could be to said that the returned collection behaves as if the deprecated codes
-     * were included in the set but invisible.</p>
+     * In other words, the returned collection behaves as if deprecated codes were included in the set but invisible.
      *
      * @param  type  the spatial reference objects type (may be {@code Object.class}).
      * @return the set of authority codes for spatial reference objects of the given type (may be an empty set).
@@ -3107,18 +3104,22 @@ next:               while (r.next()) {
          */
         @Override
         protected Set<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException {
-            String select = "COORD_REF_SYS_CODE";
-            String from   = "Coordinate Reference System";
-            final String where;
-            final Set<Number> codes;
-            final TableInfo table;
-            boolean isFloat = false;
+            /*
+             * SQL query will be of the form shown below, except that the WHERE clause will be modified
+             * to accommodate the cases where <filters> is a floating point value or a list of integers.
+             *
+             *     SELECT <codeColumn> FROM <table> WHERE <where> = <filters>
+             */
+            final String      where;                // Column to use for filtering, or null if none.
+            final TableInfo   table;                // Contains 'codeColumn' and 'table' names.
+            final Set<Number> filters;              // Values to put in the WHERE clause, or null if none.
+            boolean isFloat = false;                // Whether 'filters' shall be handled as a floating point value.
+            Set<String>   namePatterns = null;      // SQL patterns for filtering by names, or null for no filtering.
+            StringBuilder buffer       = null;      // Temporary buffer for building SQL queries, created when first needed.
             if (object instanceof Ellipsoid) {
-                select  = "ELLIPSOID_CODE";
-                from    = "Ellipsoid";
                 where   = "SEMI_MAJOR_AXIS";
                 table   = TableInfo.ELLIPSOID;
-                codes   = Collections.singleton(((Ellipsoid) object).getSemiMajorAxis());
+                filters = Collections.singleton(((Ellipsoid) object).getSemiMajorAxis());
                 isFloat = true;
             } else {
                 final IdentifiedObject dependency;
@@ -3130,19 +3131,33 @@ next:               while (r.next()) {
                     dependency = ((SingleCRS) object).getDatum();
                     where      = "DATUM_CODE";
                     table      = TableInfo.CRS;
-                } else if (object instanceof GeodeticDatum) {
-                    dependency = ((GeodeticDatum) object).getEllipsoid();
-                    select     = "DATUM_CODE";
-                    from       = "Datum";
-                    where      = "ELLIPSOID_CODE";
+                } else if (object instanceof Datum) {
                     table      = TableInfo.DATUM;
-                } else {
-                    if (object instanceof VerticalDatum) {
-                        final VerticalDatumType type = ((VerticalDatum) object).getVerticalDatumType();
-                        if (type != null && !type.equals(VERTICAL_DATUM_TYPE)) {
-                            return Collections.emptySet();
+                    if (object instanceof GeodeticDatum) {
+                        dependency = ((GeodeticDatum) object).getEllipsoid();
+                        where      = "ELLIPSOID_CODE";
+                    } else {
+                        dependency = null;
+                        where      = null;
+                        if (object instanceof VerticalDatum) {
+                            final VerticalDatumType type = ((VerticalDatum) object).getVerticalDatumType();
+                            if (type != null && !type.equals(VERTICAL_DATUM_TYPE)) {
+                                return Collections.emptySet();
+                            }
                         }
                     }
+                    /*
+                     * We currently have no better way to filter datum (or reference frames) than their names.
+                     * Filtering must be at least as tolerant as AbstractDatum.isHeuristicMatchForName(String).
+                     * We initialize a larger StringBuilder since SQL query using names may be long.
+                     */
+                    buffer = new StringBuilder(350);
+                    namePatterns = new LinkedHashSet<>();
+                    namePatterns.add(toDatumPattern(object.getName().getCode(), buffer));
+                    for (final GenericName id : object.getAlias()) {
+                        namePatterns.add(toDatumPattern(id.tip().toString(), buffer));
+                    }
+                } else {
                     // Not a supported type. Returns all codes.
                     return super.getCodeCandidates(object);
                 }
@@ -3154,98 +3169,204 @@ next:               while (r.next()) {
                  * cache. This is desirable since this method may be invoked (indirectly) in a loop for many CRS objects
                  * sharing the same CoordinateSystem or Datum dependencies.
                  */
-                final boolean previous = isIgnoringAxes();
-                final Set<IdentifiedObject> find;
-                try {
-                    setIgnoringAxes(true);
-                    find = find(dependency);
-                } finally {
-                    setIgnoringAxes(previous);
-                }
-                codes = new LinkedHashSet<>(Containers.hashMapCapacity(find.size()));
-                for (final IdentifiedObject dep : find) {
-                    Identifier id = IdentifiedObjects.getIdentifier(dep, Citations.EPSG);
-                    if (id != null) try {           // Should never be null, but let be safe.
-                        codes.add(Integer.parseInt(id.getCode()));
-                    } catch (NumberFormatException e) {
-                        Logging.recoverableException(Logging.getLogger(Loggers.CRS_FACTORY), Finder.class, "getCodeCandidates", e);
+                if (dependency != null) {
+                    final boolean previous = isIgnoringAxes();
+                    final Set<IdentifiedObject> find;
+                    try {
+                        setIgnoringAxes(true);
+                        find = find(dependency);
+                    } finally {
+                        setIgnoringAxes(previous);
+                    }
+                    filters = new LinkedHashSet<>(Containers.hashMapCapacity(find.size()));
+                    for (final IdentifiedObject dep : find) {
+                        Identifier id = IdentifiedObjects.getIdentifier(dep, Citations.EPSG);
+                        if (id != null) try {           // Should never be null, but let be safe.
+                            filters.add(Integer.parseInt(id.getCode()));
+                        } catch (NumberFormatException e) {
+                            Logging.recoverableException(Logging.getLogger(Loggers.CRS_FACTORY), Finder.class, "getCodeCandidates", e);
+                        }
                     }
-                }
-                if (codes.isEmpty()) {
-                    // Dependency not found.
-                    return Collections.emptySet();
+                    if (filters.isEmpty()) {
+                        // Dependency not found.
+                        return Collections.emptySet();
+                    }
+                } else {
+                    filters = null;
                 }
             }
             /*
-             * Build the SQL statement. The parameters depend on whether the search criterion is an EPSG code
-             * or a numeric value.
-             *
-             * - If EPSG code, there is only one parameter which is the code to search.
-             * - If numeric, there is 3 parameters: lower value, upper value, exact value to search.
+             * At this point we collected the information needed for creating the main SQL query. We need an
+             * additional query if we are going to filter by names, since we will need to take aliases in account.
              */
-            final StringBuilder buffer = new StringBuilder(200);
-            buffer.append("SELECT ").append(select).append(" FROM [").append(from).append(']');
-            table.where(object.getClass(), buffer);
-            buffer.append(where);
-            if (isFloat) {
-                buffer.append(">=? AND ").append(where).append("<=?");
-            } else {
-                buffer.append("=?");
+            if (buffer == null) {
+                buffer = new StringBuilder(200);
             }
-            buffer.append(getSearchDomain() == Domain.ALL_DATASET
-                          ? " ORDER BY ABS(DEPRECATED), "
-                          : " AND DEPRECATED=0 ORDER BY ");     // Do not put spaces around "=" - SQLTranslator searches for this exact match.
-            if (isFloat) {
-                buffer.append("ABS(").append(where).append("-?), ");
+            final String aliasSQL;
+            if (namePatterns != null) {
+                buffer.setLength(0);
+                buffer.append("SELECT OBJECT_CODE FROM [Alias] WHERE OBJECT_TABLE_NAME='Datum'");
+                String separator = " AND (";
+                for (final String pattern : namePatterns) {
+                    appendFilterByName(buffer.append(separator), "ALIAS", pattern);
+                    separator = " OR ";
+                }
+                aliasSQL = translator.apply(buffer.append(')').toString());
+            } else {
+                aliasSQL = null;
             }
-            buffer.append(select);          // Only for making order determinist.
             /*
-             * Run the SQL statement. The parameter can be any of the following types:
+             * Prepare the first part of SQL statement:
              *
-             * - A String, which represent a foreigner key as an integer value.
-             *   The search will require an exact match.
+             *    SELECT <codeColumn> FROM <table> WHERE <where> = <filters>
              *
-             * - A floating point number, in which case the search will be performed
-             *   with a tolerance threshold of 1 cm for a planet of the size of Earth.
+             * The filters depend on whether the search criterion is any code in a list of EPSG codes or a numeric value.
+             * In the later case, the numeric value is assumed a linear distance in metres and the tolerance threshold is
+             * 1 cm for a planet of the size of Earth.
              */
-            final Set<String> result = new LinkedHashSet<>();       // We need to preserve order in this set.
-            try {
-                try (PreparedStatement s = connection.prepareStatement(translator.apply(buffer.toString()))) {
-                    for (final Number code : codes) {
-                        if (isFloat) {
-                            final double value = code.doubleValue();
-                            final double tolerance = Math.abs(value * (Formulas.LINEAR_TOLERANCE / ReferencingServices.AUTHALIC_RADIUS));
-                            s.setDouble(1, value - tolerance);
-                            s.setDouble(2, value + tolerance);
-                            s.setDouble(3, value);
-                        } else {
-                            s.setInt(1, code.intValue());
-                        }
-                        try (ResultSet r = s.executeQuery()) {
-                            while (r.next()) {
-                                result.add(r.getString(1));
+            buffer.setLength(0);
+            buffer.append("SELECT ").append(table.codeColumn).append(" FROM ").append(table.table);
+            if (filters != null) {
+                table.where(object.getClass(), buffer);
+                buffer.append(where);
+                if (isFloat) {
+                    final double value = filters.iterator().next().doubleValue();
+                    final double tolerance = Math.abs(value * (Formulas.LINEAR_TOLERANCE / ReferencingServices.AUTHALIC_RADIUS));
+                    buffer.append(">=").append(value - tolerance).append(" AND ").append(where)
+                          .append("<=").append(value + tolerance);
+                } else {
+                    String separator = " IN (";
+                    for (Number code : filters) {
+                        buffer.append(separator).append(code.intValue());
+                        separator = ",";
+                    }
+                    buffer.append(')');
+                }
+            }
+            /*
+             * We did not finished to build the SQL query, but the remaining part may require a JDBC connection.
+             * We do not use PreparedStatement because the number of parameters varies, and we may need to use a
+             * Statement two times for completely different queries.
+             */
+            try (Statement stmt = connection.createStatement()) {
+                if (namePatterns != null) {
+                    String separator = (where == null) ? " WHERE (" : " AND (";
+                    for (final String pattern : namePatterns) {
+                        appendFilterByName(buffer.append(separator), table.nameColumn, pattern);
+                        separator = " OR ";
+                    }
+                    boolean hasAlias = false;
+                    try (ResultSet result = stmt.executeQuery(aliasSQL)) {
+                        while (result.next()) {
+                            final int code = result.getInt(1);
+                            if (!result.wasNull()) {            // Should never be null but we are paranoiac.
+                                buffer.append(separator);
+                                if (!hasAlias) {
+                                    hasAlias = true;
+                                    buffer.append(table.codeColumn).append(" IN (");
+                                }
+                                buffer.append(code);
+                                separator = ", ";
                             }
                         }
                     }
+                    if (hasAlias) buffer.append(')');
+                    buffer.append(')');
                 }
-                result.remove(null);    // Should not have null element, but let be safe.
+                buffer.append(getSearchDomain() == Domain.ALL_DATASET
+                              ? " ORDER BY ABS(DEPRECATED), "
+                              : " AND DEPRECATED=0 ORDER BY ");     // Do not put spaces around "=" - SQLTranslator searches for this exact match.
+                if (isFloat) {
+                    @SuppressWarnings("null")
+                    final double value = filters.iterator().next().doubleValue();
+                    buffer.append("ABS(").append(where).append('-').append(value).append("), ");
+                }
+                buffer.append(table.codeColumn);          // Only for making order determinist.
                 /*
-                 * Sort the result by taking in account the supersession table.
+                 * At this point the SQL query is complete. Run it, preserving order.
+                 * The sort the result by taking in account the supersession table.
                  */
+                final Set<String> result = new LinkedHashSet<>();       // We need to preserve order in this set.
+                try (ResultSet r = stmt.executeQuery(translator.apply(buffer.toString()))) {
+                    while (r.next()) {
+                        result.add(r.getString(1));
+                    }
+                }
+                result.remove(null);                    // Should not have null element, but let be safe.
                 if (result.size() > 1) {
                     final Object[] id = result.toArray();
-                    if (sort(select, id)) {
+                    if (sort(table.codeColumn, id)) {
                         result.clear();
                         for (final Object c : id) {
                             result.add((String) c);
                         }
                     }
                 }
+                return result;
             } catch (SQLException exception) {
-                throw databaseFailure(Identifier.class, String.valueOf(CollectionsExt.first(codes)), exception);
+                throw databaseFailure(Identifier.class, String.valueOf(CollectionsExt.first(filters)), exception);
+            }
+        }
+    }
+
+    /**
+     * Returns a SQL pattern for the given datum name. The name is returned in all lower cases for allowing
+     * case-insensitive searches. Punctuations are replaced by any sequence of characters ({@code '%'}) and
+     * non-ASCII letters are replaced by any single character ({@code '_'}). The returned pattern should be
+     * flexible enough for accepting all names considered equal in {@code DefaultGeodeticDatum} comparisons.
+     * In case of doubt, it is okay to return a pattern accepting more names.
+     *
+     * @param  name    the datum name for which to return a SQL pattern.
+     * @param  buffer  temporary buffer to use for creating the pattern.
+     * @return the SQL pattern for the given name.
+     *
+     * @see org.apache.sis.referencing.datum.DefaultGeodeticDatum#isHeuristicMatchForName(String)
+     */
+    private static String toDatumPattern(String name, final StringBuilder buffer) {
+        int i = 0;
+        if (name.startsWith(ESRI_DATUM_PREFIX)) {
+            i = ESRI_DATUM_PREFIX.length();
+        }
+        int end = name.indexOf('(');        // Ignore "Paris" in "Nouvelle Triangulation Française (Paris)".
+        if (end < 0) end = name.length();
+        end = CharSequences.skipTrailingWhitespaces(name, i, end);
+        buffer.setLength(0);
+        while (i < end) {
+            final int c = name.codePointAt(i);
+            if (Character.isLetterOrDigit(c)) {
+                if (c < 128) {                                   // Use only ASCII characters in the search.
+                    buffer.appendCodePoint(Character.toLowerCase(c));
+                } else {
+                    appendIfNotRedundant(buffer, '_');
+                }
+            } else {
+                appendIfNotRedundant(buffer, '%');
             }
-            return result;
+            i += Character.charCount(c);
+        }
+        appendIfNotRedundant(buffer, '%');
+        for (i=0; (i = buffer.indexOf("_%", i)) >= 0;) {
+            buffer.deleteCharAt(i);
         }
+        return buffer.toString();
+    }
+
+    /**
+     * Appends the given wildcard character to the given buffer if the buffer does not ends with {@code '%'}.
+     */
+    private static void appendIfNotRedundant(final StringBuilder buffer, final char wildcard) {
+        final int length = buffer.length();
+        if (length == 0 || buffer.charAt(length - 1) != '%') {
+            buffer.append(wildcard);
+        }
+    }
+
+    /**
+     * Appends to the given buffer the SQL statement for filtering datum names using a pattern created by
+     * {@link #toDatumPattern(String, StringBuilder)}.
+     */
+    private static void appendFilterByName(final StringBuilder buffer, final String column, final String pattern) {
+        buffer.append("LOWER(").append(column).append(") LIKE '").append(pattern).append('\'');
     }
 
     /**


[sis] 01/04: Make test class name consistent with tested class.

Posted by de...@apache.org.
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 639ebdd65a3053cef27fd69a085bf090256c651f
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 12 16:01:46 2019 +0100

    Make test class name consistent with tested class.
---
 .../apache/sis/internal/util/{UtilitiesTest.java => StringsTest.java}   | 2 +-
 .../src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java       | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/UtilitiesTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
similarity index 96%
rename from core/sis-utility/src/test/java/org/apache/sis/internal/util/UtilitiesTest.java
rename to core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
index 7dbb01f..3b8e76f 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/UtilitiesTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
@@ -31,7 +31,7 @@ import static org.junit.Assert.*;
  * @since   0.3
  * @module
  */
-public final strictfp class UtilitiesTest extends TestCase {
+public final strictfp class StringsTest extends TestCase {
     /**
      * Tests the {@link Strings#toString(Class, Object[])} method.
      */
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
index 3e69b65..1b626b5 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
@@ -62,7 +62,7 @@ import org.junit.BeforeClass;
     org.apache.sis.math.DecimalFunctionsTest.class,
     org.apache.sis.math.StatisticsTest.class,
     org.apache.sis.math.StatisticsFormatTest.class,
-    org.apache.sis.internal.util.UtilitiesTest.class,
+    org.apache.sis.internal.util.StringsTest.class,
     org.apache.sis.internal.util.DoubleDoubleTest.class,
     org.apache.sis.math.LineTest.class,
     org.apache.sis.math.PlaneTest.class,


[sis] 04/04: Replace some GridGeometry methods by a Modifier inner class.

Posted by de...@apache.org.
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 8a7eee34f4845590c48eda42df3e07554e871ed4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 12 17:54:43 2019 +0100

    Replace some GridGeometry methods by a Modifier inner class.
---
 .../org/apache/sis/coverage/grid/GridCoverage.java |   5 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |   7 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java | 384 ++++++++++++---------
 .../sis/coverage/grid/SubgridCalculator.java       |  16 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  26 +-
 5 files changed, 250 insertions(+), 188 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index d921ef2..52e9709 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -146,8 +146,9 @@ public abstract class GridCoverage {
      *
      * Then:
      *
-     * <blockquote><code>sliceExtent = {@linkplain #getGridGeometry()}.{@linkplain GridGeometry#subExtent(DirectPosition)
-     * subExtent}(slicePoint);</code></blockquote>
+     * <blockquote><code>sliceExtent = {@linkplain #getGridGeometry()}.{@linkplain GridGeometry#modify()
+     * modify()}.{@linkplain GridGeometry.Modifier#slice(DirectPosition)
+     * slice}(slicePoint).{@linkplain GridGeometry.Modifier#apply() apply()};</code></blockquote>
      *
      * If the {@code slicePoint} CRS is different than this grid coverage CRS (except for the number of dimensions),
      * a coordinate transformation will be applied as needed.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index e325a5b..c1d7230 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -68,7 +68,8 @@ import org.opengis.coverage.PointOutsideCoverageException;
  * We follow this specification for all getters methods, but developers should keep in mind
  * that this is the opposite of Java2D usage where {@link java.awt.Rectangle} maximal values are exclusive.</div>
  *
- * {@code GridExtent} instances are unmodifiable, so they can be shared between different {@link GridGeometry} instances.
+ * <p>{@code GridExtent} instances are immutable and thread-safe.
+ * The same instance can be shared by different {@link GridGeometry} instances.</p>
  *
  * <div class="note"><b>Upcoming API generalization:</b>
  * this class may implement the {@code GridEnvelope} interface in a future Apache SIS version.
@@ -890,7 +891,7 @@ public class GridExtent implements Serializable {
      * @return the sub-envelope, or {@code this} if the given array contains all dimensions of this grid extent.
      * @throws IndexOutOfBoundsException if an index is out of bounds.
      *
-     * @see GridGeometry#reduce(int...)
+     * @see GridGeometry.Modifier#reduce(int...)
      */
     public GridExtent reduce(final int... dimensions) {
         final int sd = getDimension();
@@ -934,7 +935,7 @@ public class GridExtent implements Serializable {
      * @return the subsampled extent, or {@code this} is subsampling results in the same extent.
      * @throws IllegalArgumentException if a period is not greater than zero.
      *
-     * @see GridGeometry#subgrid(Envelope, GridRoundingMode, double...)
+     * @see GridGeometry.Modifier#subgrid(Envelope, double...)
      */
     public GridExtent subsample(final int... periods) {
         ArgumentChecks.ensureNonNull("periods", periods);
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 6e13e5f..cfd50af 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -90,6 +90,9 @@ import org.opengis.coverage.PointOutsideCoverageException;
  * By default, any request for an undefined property will throw an {@link IncompleteGridGeometryException}.
  * In order to check if a property is defined, use {@link #isDefined(int)}.
  *
+ * <p>{@code GridGeometry} instances are immutable and thread-safe.
+ * The same instance can be shared by different {@link GridCoverage} instances.</p>
+ *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @version 1.0
  * @since   1.0
@@ -239,7 +242,7 @@ public class GridGeometry implements Serializable {
     /**
      * Creates a new grid geometry derived from the given grid geometry with a new extent and a modified transform.
      * This constructor is used for creating a grid geometry over a subregion (for example with the grid extent
-     * computed by {@link #subExtent(Envelope, GridRoundingMode)}) or grid geometry for a subsampled raster.
+     * computed by {@link Modifier#subgrid(Envelope, double...)}) or grid geometry for a subsampled raster.
      *
      * <p>If {@code toOther} is non-null, it should be a transform from the given {@code extent} coordinates to the
      * {@code other} grid coordinates. That transform should be merely a {@linkplain MathTransforms#scale(double...)
@@ -258,8 +261,7 @@ public class GridGeometry implements Serializable {
      * @throws NullPointerException if {@code extent} is {@code null} and the other grid geometry contains no other information.
      * @throws TransformException if the math transform can not compute the geospatial envelope from the grid extent.
      *
-     * @see #subExtent(Envelope, GridRoundingMode)
-     * @see #subgrid(Envelope, GridRoundingMode, double...)
+     * @see Modifier#subgrid(Envelope, double...)
      */
     GridGeometry(final GridGeometry other, final GridExtent extent, final MathTransform toOther) throws TransformException {
         final int dimension = other.getDimension();
@@ -507,7 +509,7 @@ public class GridGeometry implements Serializable {
      * @param  dimensions  the dimensions to select, in strictly increasing order (this may not be verified).
      * @throws FactoryException if an error occurred while separating the "grid to CRS" transform.
      *
-     * @see #reduce(int...)
+     * @see Modifier#reduce(int...)
      */
     private GridGeometry(final GridGeometry other, int[] dimensions) throws FactoryException {
         extent = (other.extent != null) ? other.extent.reduce(dimensions) : null;
@@ -628,7 +630,7 @@ public class GridGeometry implements Serializable {
      * {@linkplain #getGridToCRS(PixelInCell) transformed} to the "real world" coordinate system.
      * The initial envelope encompasses all cell surfaces, from the left border of leftmost cell
      * to the right border of the rightmost cell and similarly along other axes.
-     * If this grid geometry is a {@linkplain #subgrid(Envelope, GridRoundingMode, double...) subgrid}, then the envelope is also
+     * If this grid geometry is a {@linkplain Modifier#subgrid(Envelope, double...) subgrid}, then the envelope is also
      * {@linkplain GeneralEnvelope#intersect(Envelope) clipped} to the envelope of the original (non subsampled) grid geometry.
      *
      * @return the bounding box in "real world" coordinates (never {@code null}).
@@ -663,70 +665,6 @@ public class GridGeometry implements Serializable {
     }
 
     /**
-     * Returns the coordinate range of the grid intersecting the given spatiotemporal region.
-     * The given envelope does not need to be expressed in the same coordinate reference system
-     * (CRS) than {@linkplain #getCoordinateReferenceSystem() the one of this grid geometry};
-     * coordinate conversions or transformations will be applied as needed.
-     * That envelope CRS may have fewer dimensions than this grid geometry CRS,
-     * in which case grid dimensions not mapped to envelope dimensions will be returned unchanged.
-     * In other words, the {@code GridGeometry} dimensions not found in the given {@code areaOfInterest}
-     * will be unfiltered (not discarded).
-     *
-     * <p>If the envelope CRS is not specified, then it is assumed the same than the CRS of this grid geometry.
-     * In such case the envelope needs to contain all dimensions.</p>
-     *
-     * <p>This method does not reduce the number of dimensions of this grid geometry.
-     * For dimensionality reduction, see {@link #reduce(int...)}.</p>
-     *
-     * @param  areaOfInterest  the desired spatiotemporal region in any CRS (transformations will be applied as needed).
-     * @param  roundingMode  the grid extent envelope rounding mode.
-     * @return a grid extent of the same dimension than the grid geometry which intersects the given area of interest.
-     * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
-     * @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
-     *
-     * @see #subgrid(Envelope, GridRoundingMode, double...)
-     */
-    public GridExtent subExtent(final Envelope areaOfInterest, final GridRoundingMode roundingMode) {
-        ArgumentChecks.ensureNonNull("areaOfInterest", areaOfInterest);
-        requireGridToCRS();
-        try {
-            return new SubgridCalculator(this, cornerToCRS, areaOfInterest, null, roundingMode).extent;
-        } catch (FactoryException | TransformException e) {
-            throw new IllegalGridGeometryException(e, "areaOfInterest");
-        }
-    }
-
-    /**
-     * Returns the coordinate range of a slice of this grid geometry at the given point.
-     * The given position can be expressed in any coordinate reference system (CRS).
-     * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
-     * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
-     * without those dimensions, or by assigning the NaN value to some coordinates.
-     * See {@link #slice(DirectPosition)} for examples.
-     *
-     * <p>This method does not reduce the number of dimensions of this grid geometry.
-     * For dimensionality reduction, see {@link #reduce(int...)}.</p>
-     *
-     * @param  slicePoint   the coordinates where to get a slice.
-     * @return a slice of the grid extent at the given slice point.
-     * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
-     * @throws PointOutsideCoverageException if the given point is outside the grid extent.
-     * @throws IllegalGridGeometryException if an error occurred while converting the point coordinates to grid coordinates.
-     *
-     * @see #slice(DirectPosition)
-     * @see GridCoverage#render(GridExtent)
-     */
-    public GridExtent subExtent(final DirectPosition slicePoint) {
-        ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
-        requireGridToCRS();
-        try {
-            return new SubgridCalculator(this, cornerToCRS, slicePoint).extent;
-        } catch (TransformException e) {
-            throw new IllegalGridGeometryException(e, "slicePoint");
-        }
-    }
-
-    /**
      * Returns the conversion from grid coordinates to "real world" coordinates.
      * The conversion is often an affine transform, but not necessarily.
      * Conversions from cell indices to geospatial coordinates can be performed for example as below:
@@ -1038,113 +976,237 @@ public class GridGeometry implements Serializable {
     }
 
     /**
-     * Returns a grid geometry over a sub-region of this grid geometry and optionally with subsampling.
-     * The given envelope does not need to be expressed in the same coordinate reference system (CRS)
-     * than {@linkplain #getCoordinateReferenceSystem() the CRS of this grid geometry};
-     * coordinate conversions or transformations will be applied as needed.
-     * That envelope CRS may have fewer dimensions than this grid geometry CRS,
-     * in which case grid dimensions not mapped to envelope dimensions will be returned unchanged.
-     * The target resolution, if provided, shall be in same units and same order than the given envelope axes.
-     * If the length of {@code targetResolution} array is less than the number of dimensions of {@code areaOfInterest},
-     * then no subsampling will be applied on the missing dimensions.
+     * Returns an object that can be used for creating a new grid geometry derived from this grid geometry.
+     * Despite its name, {@code Modifier} does not change the state of this {@code GridGeometry} but instead
+     * creates new instances as needed. Examples of modifications include clipping to a sub-area, applying a
+     * sub-sampling, or selecting some grid dimensions.
      *
-     * <p>This method does not reduce the number of dimensions of this grid geometry.
-     * For dimensionality reduction, see {@link #reduce(int...)}.</p>
+     * <div class="note"><b>Example:</b>
+     * for clipping this grid geometry to a sub-area, one can use:
+     *
+     * {@preformat java
+     *     GridGeometry gg = ...;
+     *     Envelope areaOfInterest = ...;
+     *     gg = gg.modify().rounding(GridRoundingMode.ENCLOSING)
+     *                     .subgrid(areaOfInterest).apply();
+     * }
+     * </div>
      *
-     * @param  areaOfInterest    the desired spatiotemporal region in any CRS (transformations will be applied as needed),
-     *                           or {@code null} for not restricting the sub-grid to a sub-area.
-     * @param  roundingMode  the grid extent envelope rounding mode.
-     * @param  targetResolution  the desired resolution in the same units and order than the axes of the given envelope,
-     *                           or {@code null} or an empty array if no subsampling is desired.
-     * @return a grid geometry over the specified sub-region of this grid geometry with the specified resolution.
-     * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
-     * @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
+     * Each {@code Modifier} instance can be used only once.
      *
-     * @see #subExtent(Envelope, GridRoundingMode)
-     * @see GridExtent#subsample(int[])
+     * @return an object for deriving a grid geometry from {@code this}.
      */
-    public GridGeometry subgrid(final Envelope areaOfInterest, final GridRoundingMode roundingMode,
-            double... targetResolution) {
-        requireGridToCRS();
-        try {
-            final SubgridCalculator sub = new SubgridCalculator(this, cornerToCRS, areaOfInterest, targetResolution, roundingMode);
-            if (sub.toSubsampled != null || sub.extent != extent) {
-                return new GridGeometry(this, sub.extent, sub.toSubsampled);
-            }
-        } catch (FactoryException | TransformException e) {
-            throw new IllegalGridGeometryException(e, "areaOfInterest");
-        }
-        return this;
+    public Modifier modify() {
+        return new Modifier();
     }
 
     /**
-     * Returns a grid geometry for a slice at the given point.
-     * The given position can be expressed in any coordinate reference system (CRS).
-     * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
-     * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
-     * without those dimensions, or by assigning the NaN value to some coordinates.
-     *
-     * <div class="note"><b>Example:</b>
-     * if the {@linkplain #getCoordinateReferenceSystem() coordinate reference system} of this grid geometry has
-     * (<var>longitude</var>, <var>latitude</var>, <var>time</var>) axes, then a (<var>longitude</var>, <var>latitude</var>)
-     * slice at time <var>t</var> can be created with one of the following two positions:
-     * <ul>
-     *   <li>A three-dimensional position with ({@link Double#NaN}, {@link Double#NaN}, <var>t</var>) coordinates.</li>
-     *   <li>A one-dimensional position with (<var>t</var>) coordinate and the coordinate reference system set to
-     *       {@linkplain org.apache.sis.referencing.CRS#getTemporalComponent(CoordinateReferenceSystem) the temporal component}
-     *       of the grid geometry CRS.</li>
-     * </ul></div>
+     * Creates a new grid geometry derived from the enclosing grid geometry with different extent or resolution.
+     * {@code Modifier} are created by calls to {@link GridGeometry#modify()}. Properties of the desired grid
+     * geometry can be specified by {@link #rounding rounding}, {@link #subgrid subgrid}, {@link #slice slice}
+     * or {@link #reduce reduce} methods, and the grid geometry is created by {@link #apply()}.
      *
-     * This method does not reduce the number of dimensions of this grid geometry.
-     * For dimensionality reduction, see {@link #reduce(int...)}.
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.0
      *
-     * @param  slicePoint   the coordinates where to get a slice.
-     * @return a slice of this grid geometry at the given slice point. May be {@code this}.
-     * @throws IllegalGridGeometryException if an error occurred while converting the point coordinates to grid coordinates.
-     * @throws PointOutsideCoverageException if the given point is outside the grid extent.
+     * @see GridGeometry#modify()
      *
-     * @see #subExtent(DirectPosition)
+     * @since 1.0
+     * @module
      */
-    public GridGeometry slice(final DirectPosition slicePoint) {
-        ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
-        requireGridToCRS();
-        try {
-            final GridExtent slice = new SubgridCalculator(this, cornerToCRS, slicePoint).extent;
-            if (slice != extent) {
-                return new GridGeometry(this, slice, null);
+    public class Modifier {
+        /**
+         * Builder of grid geometry based on a slice of enclosing grid geometry, or {@code null} if none.
+         */
+        private SubgridCalculator subgrid;
+
+        /**
+         * The grid dimension to keep, or {@code null} if no filtering is applied.
+         */
+        private int[] dimensions;
+
+        /**
+         * Controls behavior of rounding from floating point values to integers.
+         */
+        private GridRoundingMode rounding;
+
+        /**
+         * Creates a new modifier based on the enclosing grid geometry.
+         * This constructor is for subclasses only.
+         *
+         * @see GridGeometry#modify()
+         */
+        protected Modifier() {
+            rounding = GridRoundingMode.NEAREST;
+        }
+
+        /**
+         * Verifies that a sub-grid has not yet been defined.
+         */
+        private void ensureBeforeSubgrid() {
+            if (subgrid != null) {
+                throw new IllegalStateException();
             }
-        } catch (TransformException e) {
-            throw new IllegalGridGeometryException(e, "slicePoint");
         }
-        return this;
-    }
 
-    /**
-     * Returns a grid geometry that encompass only some dimensions of the grid extent.
-     * This method copies the specified dimensions into a new grid geometry.
-     * The selection is applied on {@linkplain #getExtent() grid extent} dimensions;
-     * they are not necessarily the same than the {@linkplain #getEnvelope() envelope} dimensions.
-     * The given dimensions must be in strictly ascending order without duplicated values.
-     * The number of dimensions of the sub grid geometry will be {@code dimensions.length}.
-     *
-     * <p>This method performs a <cite>dimensionality reduction</cite>.
-     * This method can not be used for changing dimension order.</p>
-     *
-     * @param  dimensions  the grid (not CRS) dimensions to select, in strictly increasing order.
-     * @return the sub grid geometry, or {@code this} if the given array contains all dimensions of this grid grid geometry.
-     * @throws IndexOutOfBoundsException if an index is out of bounds.
-     *
-     * @see GridExtent#getSubspaceDimensions(int)
-     * @see GridExtent#reduce(int...)
-     * @see org.apache.sis.referencing.CRS#reduce(CoordinateReferenceSystem, int...)
-     */
-    public GridGeometry reduce(final int... dimensions) {
-        if (GridExtent.verifyDimensions(dimensions, getDimension())) {
+        /**
+         * Controls behavior of rounding from floating point values to integers.
+         * This method can be invoked before any method expecting an {@link Envelope} argument.
+         * If this method is never invoked, the default value is {@link GridRoundingMode#NEAREST}.
+         * If this method is invoked too late, an {@link IllegalStateException} is thrown.
+         *
+         * @param  mode  the new rounding mode.
+         * @return {@code this} for method call chaining.
+         * @throws IllegalStateException if {@link #subgrid(Envelope, double...)} has already been invoked.
+         */
+        public Modifier rounding(final GridRoundingMode mode) {
+            ArgumentChecks.ensureNonNull("mode", mode);
+            ensureBeforeSubgrid();
+            rounding = mode;
+            return this;
+        }
+
+        /**
+         * Returns a grid geometry over a sub-region of this grid geometry and optionally with subsampling.
+         * The given envelope does not need to be expressed in the same coordinate reference system (CRS)
+         * than {@linkplain #getCoordinateReferenceSystem() the CRS of this grid geometry};
+         * coordinate conversions or transformations will be applied as needed.
+         * That envelope CRS may have fewer dimensions than this grid geometry CRS,
+         * in which case grid dimensions not mapped to envelope dimensions will be returned unchanged.
+         * The target resolution, if provided, shall be in same units and same order than the given envelope axes.
+         * If the length of {@code targetResolution} array is less than the number of dimensions of {@code areaOfInterest},
+         * then no subsampling will be applied on the missing dimensions.
+         *
+         * <p>This method does not reduce the number of dimensions of this grid geometry.
+         * For dimensionality reduction, see {@link #reduce(int...)}.</p>
+         *
+         * @param  areaOfInterest    the desired spatiotemporal region in any CRS (transformations will be applied as needed),
+         *                           or {@code null} for not restricting the sub-grid to a sub-area.
+         * @param  targetResolution  the desired resolution in the same units and order than the axes of the given envelope,
+         *                           or {@code null} or an empty array if no subsampling is desired.
+         * @return {@code this} for method call chaining.
+         * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
+         * @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
+         *
+         * @see GridExtent#subsample(int[])
+         */
+        public Modifier subgrid(final Envelope areaOfInterest, double... targetResolution) {
+            ensureBeforeSubgrid();
+            requireGridToCRS();
+            try {
+                subgrid = new SubgridCalculator(GridGeometry.this, cornerToCRS, areaOfInterest, targetResolution, rounding);
+            } catch (FactoryException | TransformException e) {
+                throw new IllegalGridGeometryException(e, "areaOfInterest");
+            }
+            return this;
+        }
+
+        /**
+         * Requests a grid geometry for a slice at the given point.
+         * The given position can be expressed in any coordinate reference system (CRS).
+         * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
+         * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
+         * without those dimensions, or by assigning the NaN value to some coordinates.
+         *
+         * <div class="note"><b>Example:</b>
+         * if the {@linkplain #getCoordinateReferenceSystem() coordinate reference system} of current grid geometry has
+         * (<var>longitude</var>, <var>latitude</var>, <var>time</var>) axes, then a (<var>longitude</var>, <var>latitude</var>)
+         * slice at time <var>t</var> can be created with one of the following two positions:
+         * <ul>
+         *   <li>A three-dimensional position with ({@link Double#NaN}, {@link Double#NaN}, <var>t</var>) coordinates.</li>
+         *   <li>A one-dimensional position with (<var>t</var>) coordinate and the coordinate reference system set to
+         *       {@linkplain org.apache.sis.referencing.CRS#getTemporalComponent(CoordinateReferenceSystem) the temporal component}
+         *       of the grid geometry CRS.</li>
+         * </ul></div>
+         *
+         * This method does not reduce the number of dimensions of this grid geometry.
+         * For dimensionality reduction, see {@link #reduce(int...)}.
+         *
+         * @param  slicePoint   the coordinates where to get a slice.
+         * @return {@code this} for method call chaining.
+         * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
+         * @throws IllegalGridGeometryException if an error occurred while converting the point coordinates to grid coordinates.
+         * @throws PointOutsideCoverageException if the given point is outside the grid extent.
+         */
+        public Modifier slice(final DirectPosition slicePoint) {
+            ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
+            ensureBeforeSubgrid();
+            requireGridToCRS();
+            try {
+                subgrid = new SubgridCalculator(GridGeometry.this, cornerToCRS, slicePoint);
+            } catch (TransformException e) {
+                throw new IllegalGridGeometryException(e, "slicePoint");
+            }
             return this;
-        } else try {
-            return new GridGeometry(this, dimensions);
-        } catch (FactoryException e) {
-            throw new IllegalGridGeometryException(e, "dimensions");
+        }
+
+        /**
+         * Requests a grid geometry that encompass only some dimensions of the grid extent.
+         * The specified dimensions will be copied into a new grid geometry.
+         * The selection is applied on {@linkplain #getExtent() grid extent} dimensions;
+         * they are not necessarily the same than the {@linkplain #getEnvelope() envelope} dimensions.
+         * The given dimensions must be in strictly ascending order without duplicated values.
+         * The number of dimensions of the sub grid geometry will be {@code dimensions.length}.
+         *
+         * <p>This method performs a <cite>dimensionality reduction</cite>.
+         * This method can not be used for changing dimension order.</p>
+         *
+         * @param  dimensions  the grid (not CRS) dimensions to select, in strictly increasing order.
+         * @return {@code this} for method call chaining.
+         * @throws IndexOutOfBoundsException if an index is out of bounds.
+         *
+         * @see GridExtent#getSubspaceDimensions(int)
+         * @see GridExtent#reduce(int...)
+         * @see org.apache.sis.referencing.CRS#reduce(CoordinateReferenceSystem, int...)
+         */
+        public Modifier reduce(int... dimensions) {
+            if (GridExtent.verifyDimensions(dimensions, getDimension())) {
+                this.dimensions = null;
+            } else {
+                this.dimensions = dimensions.clone();
+            }
+            return this;
+        }
+
+        /**
+         * Returns the extent of the modified grid geometry. This method is more efficient than
+         * {@link #apply()} if only the grid extent is desired instead than the full grid geometry.
+         *
+         * @return the modified grid geometry extent.
+         */
+        public GridExtent extent() {
+            GridExtent e = (subgrid != null) ? subgrid.extent : getExtent();
+            if (dimensions != null) {
+                e = e.reduce(dimensions);
+            }
+            return e;
+        }
+
+        /**
+         * Builds a grid geometry with the configuration specified by the other methods in this {@code Modifier} class.
+         *
+         * @return the modified grid geometry. May be the enclosing geometry if no change apply.
+         */
+        public GridGeometry apply() {
+            GridGeometry gg = GridGeometry.this;
+            String cause = null;
+            try {
+                cause = "subgrid";
+                if (subgrid != null) {
+                    final GridExtent    slice        = subgrid.extent;
+                    final MathTransform toSubsampled = subgrid.toSubsampled;
+                    if (toSubsampled != null || slice != extent) {
+                        gg = new GridGeometry(gg, slice, toSubsampled);
+                    }
+                }
+                cause = "dimensions";
+                if (dimensions != null) {
+                    gg = new GridGeometry(gg, dimensions);
+                }
+            } catch (FactoryException | TransformException e) {
+                throw new IllegalGridGeometryException(e, cause);
+            }
+            return gg;
         }
     }
 
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/SubgridCalculator.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/SubgridCalculator.java
index 4db895e..4d21eff 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/SubgridCalculator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/SubgridCalculator.java
@@ -113,12 +113,11 @@ final class SubgridCalculator {
      * @param  areaOfInterest  the desired spatiotemporal region in any CRS, or {@code null} for the whole area.
      * @param  resolution      the desired resolution in the same units and order than the axes of the AOI envelope,
      *                         or {@code null} or an empty array if no subsampling is desired.
-     * @param  roundingMode    the grid extent envelope rounding mode.
+     * @param  rounding        controls behavior of rounding from floating point values to integers.
      * @throws TransformException if an error occurred while converting the envelope coordinates to grid coordinates.
      */
     SubgridCalculator(final GridGeometry grid, MathTransform cornerToCRS, final Envelope areaOfInterest,
-            double[] resolution, final GridRoundingMode roundingMode)
-            throws TransformException, FactoryException
+            double[] resolution, final GridRoundingMode rounding) throws TransformException, FactoryException
     {
         /*
          * If the envelope CRS is different than the expected CRS, concatenate the envelope transformation
@@ -146,7 +145,7 @@ final class SubgridCalculator {
         GeneralEnvelope indices = null;
         if (areaOfInterest != null) {
             indices = Envelopes.transform(cornerToCRS.inverse(), areaOfInterest);
-            setExtent(indices, extent, roundingMode);
+            setExtent(indices, extent, rounding);
         }
         if (indices == null || indices.getDimension() != dimension) {
             indices = new GeneralEnvelope(dimension);
@@ -193,7 +192,7 @@ final class SubgridCalculator {
              */
             if (modified) {
                 final GridExtent unscaled = extent;
-                setExtent(indices, null, roundingMode);
+                setExtent(indices, null, rounding);
                 m = Matrices.createIdentity(dimension + 1);
                 for (int k=0; k<resolution.length; k++) {
                     final double s = resolution[k];
@@ -236,11 +235,10 @@ final class SubgridCalculator {
      *
      * @param  indices    the envelope to use for setting the grid extent.
      * @param  enclosing  the enclosing grid extent if a subsampling is not yet applied, {@code null} otherwise.
-     * @param  roundingMode  the grid extent envelope rounding mode.
+     * @param  rounding   controls behavior of rounding from floating point values to integers.
      */
-    private void setExtent(final GeneralEnvelope indices, final GridExtent enclosing,
-            final GridRoundingMode roundingMode) {
-        final GridExtent sub = new GridExtent(indices, roundingMode, null, enclosing, modifiedDimensions);
+    private void setExtent(final GeneralEnvelope indices, final GridExtent enclosing, final GridRoundingMode rounding) {
+        final GridExtent sub = new GridExtent(indices, rounding, null, enclosing, modifiedDimensions);
         if (!sub.equals(extent)) {
             extent = sub;
         }
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
index 9fb9e86..709f86a 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -239,7 +239,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry#subExtent(Envelope, GridRoundingMode)}.
+     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)}.
      */
     @Test
     @DependsOnMethod("testFromGeospatialEnvelope")
@@ -265,11 +265,11 @@ public final strictfp class GridGeometryTest extends TestCase {
         envelope.setRange(0, -70.001, +80.002);
         envelope.setRange(1,   4.997,  15.003);
         assertExtentEquals(new long[] {370,  40,  4},
-                           new long[] {389, 339, 10}, grid.subExtent(envelope, GridRoundingMode.NEAREST));
+                           new long[] {389, 339, 10}, grid.modify().subgrid(envelope).extent());
     }
 
     /**
-     * Tests {@link GridGeometry#subExtent(Envelope, GridRoundingMode)} with a non-linear "grid to CRS" transform.
+     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)} with a non-linear "grid to CRS" transform.
      */
     @Test
     @DependsOnMethod({"testNonLinear", "testSubExtent"})
@@ -299,14 +299,14 @@ public final strictfp class GridGeometryTest extends TestCase {
         final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84);
         envelope.setRange(0, -70.001, +80.002);
         envelope.setRange(1,  -4.997,  15.003);
-        final GridExtent actual = grid.subExtent(envelope, GridRoundingMode.NEAREST);
+        final GridExtent actual = grid.modify().subgrid(envelope).extent();
         assertEquals(extent.getAxisType(0), actual.getAxisType(0));
         assertExtentEquals(new long[] { 56, 69, 2},
                            new long[] {130, 73, 4}, actual);
     }
 
     /**
-     * Tests {@link GridGeometry#subgrid(Envelope, GridRoundingMode, double...)}.
+     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)}.
      *
      * @throws TransformException if an error occurred during computation.
      */
@@ -329,7 +329,7 @@ public final strictfp class GridGeometryTest extends TestCase {
          */
         envelope.setRange(0, -50, +30);
         envelope.setRange(1,   8,  12);
-        grid = grid.subgrid(envelope, GridRoundingMode.NEAREST, 1, 2);
+        grid = grid.modify().subgrid(envelope, 1, 2).apply();
         assertExtentEquals(new long[] {94, 40}, new long[] {95, 119}, grid.getExtent());
         assertEnvelopeEquals(envelope, grid.getEnvelope(), STRICT);
         assertMatrixEquals("gridToCRS", new Matrix3(
@@ -341,7 +341,7 @@ public final strictfp class GridGeometryTest extends TestCase {
          * It will force GridGeometry to adjust the translation term to compensate. We verify that the adustment
          * is correct by verifying that we still get the same envelope.
          */
-        grid = grid.subgrid(envelope, GridRoundingMode.NEAREST, 3, 2);
+        grid = grid.modify().subgrid(envelope, 3, 2).apply();
         assertExtentEquals(new long[] {94, 13}, new long[] {95, 39}, grid.getExtent());
         assertEnvelopeEquals(envelope, grid.getEnvelope(), STRICT);
         MathTransform cornerToCRS = grid.getGridToCRS(PixelInCell.CELL_CORNER);
@@ -358,7 +358,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry#slice(DirectPosition)}.
+     * Tests {@link GridGeometry.Modifier#slice(DirectPosition)}.
      */
     @Test
     public void testSlice() {
@@ -372,7 +372,7 @@ public final strictfp class GridGeometryTest extends TestCase {
         /*
          * There is two ways to ask for a slice. The first way is to set some coordinates to NaN.
          */
-        GridGeometry slice = grid.slice(new GeneralDirectPosition(Double.NaN, Double.NaN, 15));
+        GridGeometry slice = grid.modify().slice(new GeneralDirectPosition(Double.NaN, Double.NaN, 15)).apply();
         assertNotSame(grid, slice);
         assertSame("gridToCRS", grid.gridToCRS, slice.gridToCRS);
         final long[] expectedLow  = {336,  20, 6};
@@ -384,14 +384,14 @@ public final strictfp class GridGeometryTest extends TestCase {
          */
         GeneralDirectPosition p = new GeneralDirectPosition(HardCodedCRS.ELLIPSOIDAL_HEIGHT_cm);
         p.setOrdinate(0, 1500);
-        slice = grid.slice(p);
+        slice = grid.modify().slice(p).apply();
         assertNotSame(grid, slice);
         assertSame("gridToCRS", grid.gridToCRS, slice.gridToCRS);
         assertExtentEquals(expectedLow, expectedHigh, slice.getExtent());
     }
 
     /**
-     * Tests {@link GridGeometry#reduce(int...)}.
+     * Tests {@link GridGeometry.Modifier#reduce(int...)}.
      */
     @Test
     public void testReduce() {
@@ -405,7 +405,7 @@ public final strictfp class GridGeometryTest extends TestCase {
         /*
          * Tests on the two first dimensions.
          */
-        GridGeometry reduced = grid.reduce(0, 1);
+        GridGeometry reduced = grid.modify().reduce(0, 1).apply();
         assertNotSame(grid, reduced);
         assertExtentEquals(new long[] {336, 20}, new long[] {401, 419}, reduced.getExtent());
         assertSame("CRS", HardCodedCRS.WGS84, reduced.getCoordinateReferenceSystem());
@@ -417,7 +417,7 @@ public final strictfp class GridGeometryTest extends TestCase {
         /*
          * Tests on the last dimension.
          */
-        reduced = grid.reduce(2);
+        reduced = grid.modify().reduce(2).apply();
         assertNotSame(grid, reduced);
         assertExtentEquals(new long[] {4}, new long[] {10}, reduced.getExtent());
         assertSame("CRS", HardCodedCRS.GRAVITY_RELATED_HEIGHT, reduced.getCoordinateReferenceSystem());