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/12/19 18:00:15 UTC

[sis] branch geoapi-4.0 updated (0431f2e -> 67c5d18)

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 0431f2e  Partial review of GridCoverage.evaluate(DirectPosition) method. - Add a new FractionalGridCoordinates class with some of the calculations done by GridCoverage methods. - Rename toGridCoord as toGridCoordinates and change the return type to FractionalGridCoordinates. - Remove toLongExact(DirectPosition), replaced by FractionalGridCoordinates.
     new 4358ee9  Adjustment in the exception thrown if grid coordinates are outside the given bounds.
     new 67c5d18  Add an `ImageUtilities` class for methods to be needed by `GridCoverage2D`.

The 2 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:
 .../coverage/grid/FractionalGridCoordinates.java   |  42 ++++--
 .../sis/internal/coverage/ImageUtilities.java      | 154 +++++++++++++++++++++
 .../grid/FractionalGridCoordinatesTest.java        |   6 +-
 .../sis/internal/coverage/ImageUtilitiesTest.java  |  77 +++++++++++
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 .../org/apache/sis/util/resources/Vocabulary.java  |  45 ++++++
 .../sis/util/resources/Vocabulary.properties       |   9 ++
 .../sis/util/resources/Vocabulary_fr.properties    |   9 ++
 8 files changed, 326 insertions(+), 17 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/coverage/ImageUtilities.java
 create mode 100644 core/sis-feature/src/test/java/org/apache/sis/internal/coverage/ImageUtilitiesTest.java


[sis] 02/02: Add an `ImageUtilities` class for methods to be needed by `GridCoverage2D`.

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 67c5d18936c47de51f842a93bbcb597f8d42918b
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Thu Dec 19 18:51:17 2019 +0100

    Add an `ImageUtilities` class for methods to be needed by `GridCoverage2D`.
---
 .../sis/internal/coverage/ImageUtilities.java      | 154 +++++++++++++++++++++
 .../sis/internal/coverage/ImageUtilitiesTest.java  |  77 +++++++++++
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 .../org/apache/sis/util/resources/Vocabulary.java  |  45 ++++++
 .../sis/util/resources/Vocabulary.properties       |   9 ++
 .../sis/util/resources/Vocabulary_fr.properties    |   9 ++
 6 files changed, 295 insertions(+)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/ImageUtilities.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/ImageUtilities.java
new file mode 100644
index 0000000..73fa6aa
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/ImageUtilities.java
@@ -0,0 +1,154 @@
+/*
+ * 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.coverage;
+
+import java.util.Arrays;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.PackedColorModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * Utility methods related to images and their color model or sample model.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class ImageUtilities {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private ImageUtilities() {
+    }
+
+    /**
+     * Returns names of bands based on inspection of the color model.
+     * The bands are identified by {@link Vocabulary.Keys} values for
+     * red, green, blue, cyan, magenta, yellow, black, gray, <i>etc</i>.
+     * If a band can not be identified, then its corresponding value is 0.
+     *
+     * @param  image  the image for which to get band names (can not be null).
+     * @return {@link Vocabulary.Keys} identifying the bands.
+     */
+    @SuppressWarnings("fallthrough")
+    public static short[] bandNames(final RenderedImage image) {
+        final SampleModel sm = image.getSampleModel();
+        final int n;
+        if (sm != null) {
+            n = sm.getNumBands();
+        } else {
+            // Should not happen since SampleModel is essential, but we try to be robust.
+            n = image.getTile(image.getMinTileX(), image.getMinTileY()).getNumBands();
+        }
+        final short[] keys = new short[n];
+        final ColorModel cm = image.getColorModel();
+        if (cm != null) {
+            final ColorSpace cs = cm.getColorSpace();
+            if (cs != null) {
+                /*
+                 * Get one of the following sets of color names (ignoring order for now):
+                 *
+                 *   - Red, Green, Blue
+                 *   - Cyan, Magenta, Yellow, Black
+                 *   - Gray
+                 */
+                switch (cs.getType()) {
+                    case ColorSpace.TYPE_CMYK: {
+                        if (n >= 4)  keys[3] = Vocabulary.Keys.Black;
+                        // Fallthrough
+                    }
+                    case ColorSpace.TYPE_CMY: {
+                        switch (n) {
+                            default: keys[2] = Vocabulary.Keys.Yellow;      // Fallthrough everywhere.
+                            case 2:  keys[1] = Vocabulary.Keys.Magenta;
+                            case 1:  keys[0] = Vocabulary.Keys.Cyan;
+                            case 0:  break;
+                        }
+                        break;
+                    }
+                    case ColorSpace.TYPE_RGB: {
+                        switch (n) {
+                            default: keys[2] = Vocabulary.Keys.Blue;        // Fallthrough everywhere.
+                            case 2:  keys[1] = Vocabulary.Keys.Green;
+                            case 1:  keys[0] = Vocabulary.Keys.Red;
+                            case 0:  break;
+                        }
+                        break;
+                    }
+                    case ColorSpace.TYPE_GRAY: {
+                        if (n != 0)  keys[0] = Vocabulary.Keys.Gray;
+                        break;
+                    }
+                }
+                /*
+                 * If the color model has more components than the number of colors,
+                 * then the additional component is an alpha channel.
+                 */
+                final int nc = cm.getNumColorComponents();
+                if (nc < n && nc < cm.getNumComponents()) {
+                    keys[nc] = Vocabulary.Keys.Transparency;
+                }
+                /*
+                 * In current version we do not try to adapt the bands order to the masks.
+                 * A few tests suggest that the following methods provide the same values:
+                 *
+                 *   - PackedColorModel.getMasks()
+                 *   - SinglePixelPackedSampleModel.getBitMasks()
+                 *
+                 * For a BufferedImage.TYPE_INT_ARGB, both methods give in that order:
+                 *
+                 *    masks[0]:  00FF0000     (red)
+                 *    masks[1]:  0000FF00     (green)
+                 *    masks[2]:  000000FF     (blue)
+                 *    masks[3]:  FF000000     (alpha)  —  this last element is absent with TYPE_INT_RGB.
+                 *
+                 * For a BufferedImage.TYPE_INT_BGR, both methods give in that order:
+                 *
+                 *    masks[0]:  000000FF     (red)
+                 *    masks[1]:  0000FF00     (green)
+                 *    masks[2]:  00FF0000     (blue)
+                 *
+                 * So it looks like that SampleModel already normalizes the color components
+                 * to (Red, Green, Blue) order, at least when the image has been created with
+                 * a standard constructor. However we do not know yet what would be the behavior
+                 * if masks are not the same. For now we just log a warning.
+                 */
+                int[] m1 = null;
+                int[] m2 = null;
+                if (cm instanceof PackedColorModel) {
+                    m1 = ((PackedColorModel) cm).getMasks();
+                }
+                if (sm instanceof SinglePixelPackedSampleModel) {
+                    m2 = ((SinglePixelPackedSampleModel) sm).getBitMasks();
+                }
+                if (!Arrays.equals(m1, m2)) {
+                    // If this logging happen, we should revisit this method and improve it.
+                    Logging.getLogger(Modules.RASTER).warning("Band names may be in wrong order.");
+                }
+            }
+        }
+        return keys;
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/ImageUtilitiesTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/ImageUtilitiesTest.java
new file mode 100644
index 0000000..ba21f6c
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/ImageUtilitiesTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.coverage;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link ImageUtilities}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class ImageUtilitiesTest extends TestCase {
+    /**
+     * Tests {@link ImageUtilities#bandNames(RenderedImage)}.
+     */
+    @Test
+    public void testBandNames() {
+        RenderedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+        assertArrayEquals(new short[] {
+            Vocabulary.Keys.Red,
+            Vocabulary.Keys.Green,
+            Vocabulary.Keys.Blue,
+            Vocabulary.Keys.Transparency
+        }, ImageUtilities.bandNames(image));
+        /*
+         * Same as above, but without alpha channel.
+         */
+        image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+        assertArrayEquals(new short[] {
+            Vocabulary.Keys.Red,
+            Vocabulary.Keys.Green,
+            Vocabulary.Keys.Blue
+        }, ImageUtilities.bandNames(image));
+        /*
+         * Same as above but in sample values packed in reverse order. Note that while values
+         * are packed in BGR order, the sample model is still providing the values in RGB order.
+         * For that reason, the band order below is the same than in above test.
+         */
+        image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_BGR);
+        assertArrayEquals(new short[] {
+            Vocabulary.Keys.Red,
+            Vocabulary.Keys.Green,
+            Vocabulary.Keys.Blue
+        }, ImageUtilities.bandNames(image));
+        /*
+         * One-banded image.
+         */
+        image = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);
+        assertArrayEquals(new short[] {
+            Vocabulary.Keys.Gray
+        }, ImageUtilities.bandNames(image));
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 2a59156..edd4256 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -85,6 +85,7 @@ import org.junit.runners.Suite;
     org.apache.sis.coverage.CategoryListTest.class,
     org.apache.sis.coverage.SampleDimensionTest.class,
     org.apache.sis.coverage.SampleRangeFormatTest.class,
+    org.apache.sis.internal.coverage.ImageUtilitiesTest.class,
     org.apache.sis.internal.coverage.ScaledColorSpaceTest.class,
     org.apache.sis.internal.coverage.BufferedGridCoverageTest.class,
     org.apache.sis.internal.coverage.GridCoverage2DTest.class,
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 23aeb70..cf576a6 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
@@ -122,6 +122,16 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short BarometricAltitude = 13;
 
         /**
+         * Black
+         */
+        public static final short Black = 175;
+
+        /**
+         * Blue
+         */
+        public static final short Blue = 176;
+
+        /**
          * Cardinality
          */
         public static final short Cardinality = 14;
@@ -217,6 +227,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short CurrentDirectory = 32;
 
         /**
+         * Cyan
+         */
+        public static final short Cyan = 177;
+
+        /**
          * Cycle omitted
          */
         public static final short CycleOmitted = 33;
@@ -407,6 +422,16 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short GeographicIdentifier = 70;
 
         /**
+         * Gray
+         */
+        public static final short Gray = 178;
+
+        /**
+         * Green
+         */
+        public static final short Green = 179;
+
+        /**
          * Grid extent
          */
         public static final short GridExtent = 71;
@@ -527,6 +552,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short LowerBound = 94;
 
         /**
+         * Magenta
+         */
+        public static final short Magenta = 180;
+
+        /**
          * Mandatory
          */
         public static final short Mandatory = 95;
@@ -692,6 +722,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Read = 127;
 
         /**
+         * Red
+         */
+        public static final short Red = 181;
+
+        /**
          * Remarks
          */
         public static final short Remarks = 128;
@@ -822,6 +857,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short TransformationAccuracy = 152;
 
         /**
+         * Transparency
+         */
+        public static final short Transparency = 183;
+
+        /**
          * Truncated Julian
          */
         public static final short TruncatedJulian = 153;
@@ -925,6 +965,11 @@ public final class Vocabulary extends IndexedResourceBundle {
          * Write
          */
         public static final short Write = 173;
+
+        /**
+         * Yellow
+         */
+        public static final short Yellow = 182;
     }
 
     /**
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 56682fe..89741cf 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
@@ -27,6 +27,8 @@ AxisChanges             = Axis changes
 Azimuth                 = Azimuth
 Band_1                  = Band {0}
 BarometricAltitude      = Barometric altitude
+Black                   = Black
+Blue                    = Blue
 Cardinality             = Cardinality
 CausedBy_1              = Caused by {0}
 CellCount_1             = {0} cells
@@ -47,6 +49,7 @@ Create                  = Create
 CurrentDateTime         = Current date and time
 CurrentDirectory        = Current directory
 CycleOmitted            = Cycle omitted
+Cyan                    = Cyan
 DataBase                = Database
 DataDirectory           = Data directory
 DataFormats             = Data formats
@@ -84,6 +87,8 @@ GeodesicDistance        = Geodesic distance
 GeodeticDataset         = Geodetic dataset
 GeographicExtent        = Geographic extent
 GeographicIdentifier    = Geographic identifier
+Gray                    = Gray
+Green                   = Green
 GridExtent              = Grid extent
 Height                  = Height
 Identifier              = Identifier
@@ -108,6 +113,7 @@ Localization            = Localization
 LocationType            = Location type
 Logging                 = Logging
 LowerBound              = Lower bound
+Magenta                 = Magenta
 Mandatory               = Mandatory
 Mapping                 = Mapping
 MaximumValue            = Maximum value
@@ -141,6 +147,7 @@ Plugins                 = Plug-ins
 Preprocessing           = Preprocessing
 Quoted_1                = \u201c{0}\u201d
 Read                    = Read
+Red                     = Red
 Remarks                 = Remarks
 RemoteConfiguration     = Remote configuration
 RepresentativeValue     = Representative value
@@ -167,6 +174,7 @@ Time_1                  = {0} time
 Timezone                = Timezone
 Transformation          = Transformation
 TransformationAccuracy  = Transformation accuracy
+Transparency            = Transparency
 TruncatedJulian         = Truncated Julian
 Type                    = Type
 Unknown                 = Unknown
@@ -188,3 +196,4 @@ Warnings                = Warnings
 WestBound               = West bound
 World                   = World
 Write                   = Write
+Yellow                  = Yellow
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 0e80ee4..e9e6323 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
@@ -34,6 +34,8 @@ AxisChanges             = Changements d\u2019axes
 Azimuth                 = Azimut
 Band_1                  = Bande {0}
 BarometricAltitude      = Altitude barom\u00e9trique
+Black                   = Noir
+Blue                    = Bleu
 Cardinality             = Cardinalit\u00e9
 CausedBy_1              = Caus\u00e9e par {0}
 CellCount_1             = {0} cellules
@@ -53,6 +55,7 @@ CoverageDomain          = Domaine de la couverture de donn\u00e9es
 Create                  = Cr\u00e9ation
 CurrentDateTime         = Date et heure courantes
 CurrentDirectory        = R\u00e9pertoire courant
+Cyan                    = Cyan
 CycleOmitted            = Cycle omit
 DataBase                = Base de donn\u00e9es
 DataDirectory           = R\u00e9pertoire des donn\u00e9es
@@ -91,6 +94,8 @@ GeodesicDistance        = Distance g\u00e9od\u00e9sique
 GeodeticDataset         = Base de donn\u00e9es g\u00e9od\u00e9sique
 GeographicExtent        = \u00c9tendue g\u00e9ographique
 GeographicIdentifier    = Identifiant g\u00e9ographique
+Gray                    = Gris
+Green                   = Vert
 GridExtent              = \u00c9tendue de la grille
 Height                  = Hauteur
 Identifier              = Identifiant
@@ -115,6 +120,7 @@ Localization            = R\u00e9gionalisation
 LocationType            = Type de location
 Logging                 = Journalisation
 LowerBound              = Limite basse
+Magenta                 = Magenta
 Mandatory               = Requis
 Mapping                 = Cartographie
 MaximumValue            = Valeur maximale
@@ -148,6 +154,7 @@ Plugins                 = Modules d\u2019extension
 Preprocessing           = Pr\u00e9traitement
 Quoted_1                = \u00ab\u202f{0}\u202f\u00bb
 Read                    = Lecture
+Red                     = Rouge
 Remarks                 = Remarques
 RemoteConfiguration     = Configuration distante
 RepresentativeValue     = Valeur repr\u00e9sentative
@@ -174,6 +181,7 @@ Time_1                  = Heure {0}
 Timezone                = Fuseau horaire
 Transformation          = Transformation
 TransformationAccuracy  = Pr\u00e9cision de la transformation
+Transparency            = Transparence
 TruncatedJulian         = Julien tronqu\u00e9
 Type                    = Type
 Unknown                 = Inconnu
@@ -195,3 +203,4 @@ Warnings                = Avertissements
 WestBound               = Limite ouest
 World                   = Monde
 Write                   = \u00c9criture
+Yellow                  = Jaune


[sis] 01/02: Adjustment in the exception thrown if grid coordinates are outside the given bounds.

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 4358ee9ab593983b9acff774bb00c2bc24385bd1
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Thu Dec 19 11:04:57 2019 +0100

    Adjustment in the exception thrown if grid coordinates are outside the given bounds.
---
 .../coverage/grid/FractionalGridCoordinates.java   | 42 ++++++++++++++--------
 .../grid/FractionalGridCoordinatesTest.java        |  6 ++--
 2 files changed, 31 insertions(+), 17 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
index d7df49c..89cedbb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
@@ -21,6 +21,7 @@ import java.io.Serializable;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.coverage.grid.GridCoordinates;
+import org.opengis.coverage.PointOutsideCoverageException;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -205,9 +206,10 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
      * If non-null, then this method enforces the following additional rules:</p>
      *
      * <ul>
-     *   <li>Coordinates must be inside the given bounds, otherwise an {@link DisjointExtentException} is thrown.</li>
+     *   <li>Coordinates rounded to nearest integers must be inside the given bounds,
+     *       otherwise a {@link PointOutsideCoverageException} is thrown.</li>
      *   <li>If the computed extent overlaps an area outside the bounds, then the extent will be shifted (if an explicit
-     *       size was given) or clipped (if default size is used) in order to be be fully contained inside the bounds.</li>
+     *       size was given) or clipped (if automatic size is used) in order to be be fully contained inside the bounds.</li>
      *   <li>If a given size is larger than the corresponding bounds {@linkplain GridExtent#getSize(int) size},
      *       then the returned extent will be clipped to the bounds.</li>
      * </ul>
@@ -216,7 +218,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
      * A shift may exist if necessary for keeping the extent inside the {@code bounds} argument, but will never
      * move the grid coordinates outside the [<var>low</var> … <var>high</var>+1) range of returned extent.</p>
      *
-     * @param  bounds  if the coordinates shall be contained inside a grid, that grid. Otherwise {@code null}.
+     * @param  bounds  if the coordinates shall be contained inside a grid, that grid extent. Otherwise {@code null}.
      * @param  size    the desired extent sizes as strictly positive numbers, or 0 sentinel values for automatic
      *                 sizes (1 or 2 depending on bounds and coordinate values). This array may have any length;
      *                 if shorter than the number of dimensions, missing values default to 0.
@@ -224,7 +226,8 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
      * @throws IllegalArgumentException if a {@code size} value is negative.
      * @throws ArithmeticException if a coordinate value is outside the range of {@code long} values.
      * @throws MismatchedDimensionException if {@code bounds} dimension is not equal to grid coordinates dimension.
-     * @throws DisjointExtentException if the returned extent would not intersect the given bounds.
+     * @throws PointOutsideCoverageException if the grid coordinates (rounded to nearest integers) are outside the
+     *         given bounds.
      * @return a grid extent of the given size (if possible) containing those grid coordinates.
      */
     public GridExtent toExtent(final GridExtent bounds, final long... size) {
@@ -257,24 +260,24 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
              * In such case we can no longer enforce the `lower ≤ value ≤ upper` rule. The best
              * we can do is to take the nearest neighbor.
              */
+            final long nearest = Math.round(value);
             long lower, upper;
             if (margin == 1) {
-                lower = upper = Math.round(value);
+                lower = upper = nearest;
             } else {
-                final double base = Math.floor(value);
-                lower = (long) base;                    // Inclusive.
-                upper = (long) Math.ceil(value);        // Inclusive too (lower == upper if value is an integer).
+                lower = (long) Math.floor(value);       // Inclusive.
+                upper = (long) Math.ceil (value);       // Inclusive too (lower == upper if value is an integer).
                 if (margin != 0) {
-                    margin -= (upper - lower + 1);
+                    margin -= (upper - lower + 1);      // Total number of cells to add.
                     assert margin >= 0 : margin;        // Because (upper - lower + 1) ≤ 2
                     if ((margin & 1) != 0) {
-                        if (value - base >= 0.5) {
+                        if (nearest >= upper) {
                             upper = Math.incrementExact(upper);
                         } else {
                             lower = Math.decrementExact(lower);
                         }
                     }
-                    margin /= 2;
+                    margin >>= 1;     // Number of cells to add on each side.
                     lower  = Math.subtractExact(lower, margin);
                     upper  = Math.addExact(upper, margin);
                     margin = 2;       // Any value different than 0 for remembering that it was explicitly specified.
@@ -287,8 +290,12 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
             if (bounds != null) {
                 final long validMin = bounds.getLow(i);
                 final long validMax = bounds.getHigh(i);
-                if (lower > validMax || upper < validMin) {
-                    throw new DisjointExtentException(bounds.getAxisIdentification(i,i), validMin, validMax, lower, upper);
+                if (nearest > validMax || nearest < validMin) {
+                    final StringBuilder b = new StringBuilder();
+                    writeCoordinates(b);
+                    throw new PointOutsideCoverageException(
+                            Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4,
+                            bounds.getAxisIdentification(i,i), validMin, validMax, b.toString()));
                 }
                 if (upper > validMax) {
                     if (margin != 0) {      // In automatic mode (margin = 0) just clip, don't shift.
@@ -439,11 +446,18 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable
     @Override
     public String toString() {
         final StringBuilder buffer = new StringBuilder("GridCoordinates[");
+        writeCoordinates(buffer);
+        return buffer.append(']').toString();
+    }
+
+    /**
+     * Writes coordinates in the given buffer.
+     */
+    private void writeCoordinates(final StringBuilder buffer) {
         for (int i=0; i<coordinates.length; i++) {
             if (i != 0) buffer.append(' ');
             StringBuilders.trimFractionalPart(buffer.append(coordinates[i]));
         }
-        return buffer.append(']').toString();
     }
 
     /**
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
index bb9e0b5..9e8170b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
@@ -92,10 +92,10 @@ public final strictfp class FractionalGridCoordinatesTest extends TestCase {
      */
     @Test
     public void testToExtentBounded() {
-        final GridExtent bounds = new GridExtent(null, null, new long[] {4, 2, 7}, true);
+        final GridExtent bounds = new GridExtent(null, new long[] {0, -1, 0}, new long[] {4, 2, 8}, true);
         final GridExtent extent = instance().toExtent(bounds, 3, 5, 4);
         GridExtentTest.assertExtentEquals(extent, 0,  2,  4);
-        GridExtentTest.assertExtentEquals(extent, 1,  0,  2);
-        GridExtentTest.assertExtentEquals(extent, 2,  4,  7);
+        GridExtentTest.assertExtentEquals(extent, 1, -1,  2);
+        GridExtentTest.assertExtentEquals(extent, 2,  5,  8);
     }
 }