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 2021/01/07 18:08:29 UTC

[sis] branch geoapi-4.0 updated (d33c28c -> f2701ff)

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 d33c28c  Move part of `CategoryColorCell` into a new `ColorCell` base class. The intent is to share this code with other kinds of color tables, to be created later for isolines.
     new da676c5  Move remaining code from `CategoryColorsCell` into `ColorCell` for sharing (in a future commit) with isoline table. The methods that were previously overridden in `CategoryColorsCell` class are now overridden in `ColorColumnHandler`. Cell behavior has been reworked for reducing the amount of mouse clicks and for enabling keyboard usage.
     new 34f7b23  Let `Polyline` computes the bounds itself.
     new e9ea08a  Fix an `IndexOutOfBoundsException` when CRS has more dimensions than the dimensions for which a precision has been specified.
     new f2701ff  Support the case where one of the CRS axes in a netCDF file is a scalar (number of dimensions = 0). In such case, the scale factor does not exist in the "gridToCRS" matrix; we have only a translation term. In other words, there is no corresponding source coordinate, only a target coordinate which is constant. Some netCDF files use scalar for specifying for example that all values are located at a height of 2 metres.

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:
 .../sis/gui/coverage/CategoryColorsCell.java       | 220 -----------
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   |  85 ++++-
 .../sis/internal/gui/ImmutableObjectProperty.java  |   5 +-
 .../apache/sis/internal/gui/control/ColorCell.java | 421 +++++++++++++--------
 .../internal/gui/control/ColorColumnHandler.java   | 124 ++++++
 .../gui/control/ColorRamp.java}                    | 131 ++++---
 .../sis/gui/coverage/CoverageStylingApp.java       |   6 +-
 .../apache/sis/internal/feature/j2d/FlatShape.java |  22 ++
 .../sis/internal/feature/j2d/PathBuilder.java      |  20 +-
 .../apache/sis/internal/feature/j2d/Polygon.java   |   7 +-
 .../apache/sis/internal/feature/j2d/Polyline.java  |  14 +-
 .../sis/internal/feature/j2d/FlatShapeTest.java    |   9 +-
 .../org/apache/sis/geometry/CoordinateFormat.java  |   2 +-
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  12 +-
 .../java/org/apache/sis/internal/netcdf/Grid.java  |  24 ++
 .../sis/internal/netcdf/ucar/GridWrapper.java      |  10 +-
 17 files changed, 608 insertions(+), 506 deletions(-)
 delete mode 100644 application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
 create mode 100644 application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
 rename application/sis-javafx/src/main/java/org/apache/sis/{gui/coverage/CategoryColors.java => internal/gui/control/ColorRamp.java} (55%)


[sis] 02/04: Let `Polyline` computes the bounds itself.

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 34f7b23ac1e9d696250f25bb1d178067efca1b4c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jan 1 14:36:43 2021 +0100

    Let `Polyline` computes the bounds itself.
---
 .../apache/sis/internal/feature/j2d/FlatShape.java | 22 ++++++++++++++++++++++
 .../sis/internal/feature/j2d/PathBuilder.java      | 20 +++-----------------
 .../apache/sis/internal/feature/j2d/Polygon.java   |  7 ++-----
 .../apache/sis/internal/feature/j2d/Polyline.java  | 14 +++++++-------
 .../sis/internal/feature/j2d/FlatShapeTest.java    |  9 +++------
 5 files changed, 37 insertions(+), 35 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
index b3ed57b..9c5e738 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
@@ -54,6 +54,28 @@ abstract class FlatShape extends AbstractGeometry implements Shape {
     }
 
     /**
+     * Creates a shape with bounds initialized to minimum and maximum coordinates of given array.
+     * The {@code coordinates} array shall not be empty.
+     *
+     * @param  coordinates  the coordinate values as (x,y) tuples.
+     * @param  size         number of valid value in {@code coordinates} array.
+     */
+    FlatShape(final double[] coordinates, final int size) {
+        double xmax, ymax;
+        double xmin = xmax = coordinates[0];
+        double ymin = ymax = coordinates[1];
+        for (int i=2; i<size;) {
+            final double x = coordinates[i++];
+            final double y = coordinates[i++];
+            if (x < xmin) xmin = x;
+            if (x > xmax) xmax = x;
+            if (y < ymin) ymin = y;
+            if (y > ymax) ymax = y;
+        }
+        bounds = new IntervalRectangle(xmin, ymin, xmax, ymax);
+    }
+
+    /**
      * Returns an integer rectangle that completely encloses the shape.
      * There is no guarantee that the rectangle is the smallest bounding box that encloses the shape.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
index 830509c..faf54a8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
@@ -21,7 +21,6 @@ import java.util.Arrays;
 import java.util.ArrayList;
 import java.awt.Shape;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
 
 
 /**
@@ -192,26 +191,13 @@ public class PathBuilder {
          * be equal because `append(…)` filtered repetitive points.
          */
         if (size >= 2*DIMENSION) {
-            double xmin = coordinates[0];
-            double ymin = coordinates[1];
-            if (xmin == coordinates[size - 2] &&
-                ymin == coordinates[size - 1])
+            if (coordinates[0] == coordinates[size - 2] &&
+                coordinates[1] == coordinates[size - 1])
             {
                 size -= DIMENSION;
                 close = true;
             }
-            double xmax = xmin;
-            double ymax = ymin;
-            for (int i=DIMENSION; i<size;) {
-                final double x = coordinates[i++];
-                final double y = coordinates[i++];
-                if (x < xmin) xmin = x;
-                if (x > xmax) xmax = x;
-                if (y < ymin) ymin = y;
-                if (y > ymax) ymax = y;
-            }
-            final IntervalRectangle bounds = new IntervalRectangle(xmin, ymin, xmax, ymax);
-            polylines.add(close ? new Polygon(bounds, coordinates, size) : new Polyline(bounds, coordinates, size));
+            polylines.add(close ? new Polygon(coordinates, size) : new Polyline(coordinates, size));
         }
         size = 0;
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java
index ad8e022..0184b83 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java
@@ -16,8 +16,6 @@
  */
 package org.apache.sis.internal.feature.j2d;
 
-import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
-
 
 /**
  * A polygons as a Java2D {@link java.awt.Shape}.
@@ -32,11 +30,10 @@ final class Polygon extends Polyline {
      * Creates a new polygon with the given coordinates.
      * The {@code coordinates} array shall not be empty.
      *
-     * @param  bounds       the polygon bounds (not cloned).
      * @param  coordinates  the coordinate values as (x,y) tuples.
      * @param  size         number of valid value in {@code coordinates} array.
      */
-    Polygon(final IntervalRectangle bounds, final double[] coordinates, final int size) {
-        super(bounds, coordinates, size);
+    Polygon(final double[] coordinates, final int size) {
+        super(coordinates, size);
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
index 70d69f8..9c19b7e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
@@ -23,7 +23,7 @@ import java.awt.geom.Path2D;
 import java.awt.geom.Rectangle2D;
 import java.awt.geom.PathIterator;
 import java.awt.geom.AffineTransform;
-import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+import org.apache.sis.internal.util.Numerics;
 
 
 /**
@@ -78,13 +78,11 @@ class Polyline extends FlatShape {
      * Creates a new polylines with the given coordinates.
      * The {@code coordinates} array shall not be empty.
      *
-     * @param  bounds       the polyline bounds (not cloned).
      * @param  coordinates  the coordinate values as (x,y) tuples.
      * @param  size         number of valid value in {@code coordinates} array.
      */
-    Polyline(final IntervalRectangle bounds, final double[] coordinates, final int size) {
-        super(bounds);
-        assert size >= 2 : size;                // Required by our PathIterator.
+    Polyline(final double[] coordinates, final int size) {
+        super(coordinates, size);
         this.coordinates = new float[size];
         final double tx = round(bounds.getCenterX(), bounds.xmin, bounds.xmax);
         final double ty = round(bounds.getCenterY(), bounds.ymin, bounds.ymax);
@@ -93,11 +91,13 @@ class Polyline extends FlatShape {
     }
 
     /**
-     * Rounds the translation to an arbitrary number of bits (currently 8).
+     * Rounds the translation to an arbitrary number of bits (currently 20).
      * The intent is to avoid that zero values become something like 1E-9.
+     * The number of bits that we kept should be less that the number of bits
+     * in the significand (mantissa) of {@code float} type.
      */
     private static double round(final double center, final double min, final double max) {
-        final int e = Math.getExponent(Math.max(Math.abs(min), Math.abs(max))) - 8;
+        final int e = Math.getExponent(Math.max(Math.abs(min), Math.abs(max))) - (Numerics.SIGNIFICAND_SIZE_OF_FLOAT - 3);
         return Math.scalb(Math.round(Math.scalb(center, -e)), e);
     }
 
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java
index 88b1550..9066908 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java
@@ -18,7 +18,6 @@ package org.apache.sis.internal.feature.j2d;
 
 import java.awt.geom.Path2D;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -57,9 +56,8 @@ public final strictfp class FlatShapeTest extends TestCase {
         final double[] coordinates = {
             4,5, 6,3, 8,5, -2,5, 10,4
         };
-        final IntervalRectangle bounds = new IntervalRectangle(-2,3, 19,5);
-        final Polyline p = closed ? new Polygon (bounds, coordinates, coordinates.length)
-                                  : new Polyline(bounds, coordinates, coordinates.length);
+        final Polyline p = closed ? new Polygon (coordinates, coordinates.length)
+                                  : new Polyline(coordinates, coordinates.length);
 
         final Path2D.Double r = new Path2D.Double(Path2D.WIND_NON_ZERO);
         createReferenceShape(r, coordinates, closed);
@@ -96,11 +94,10 @@ public final strictfp class FlatShapeTest extends TestCase {
             {9,3, 7,5, -4,3},
             {3,5, 6,1, -2,7, 3,8}
         };
-        final IntervalRectangle bounds = new IntervalRectangle();           // Dummy bounds for this test.
         final Polyline[] polylines = new Polyline[coordinates.length];
         final Path2D.Double r = new Path2D.Double(Path2D.WIND_NON_ZERO);
         for (int i=0; i < polylines.length; i++) {
-            polylines[i] = new Polyline(bounds, coordinates[i], coordinates[i].length);
+            polylines[i] = new Polyline(coordinates[i], coordinates[i].length);
             createReferenceShape(r, coordinates[i], false);
         }
         final MultiPolylines p = new MultiPolylines(polylines);


[sis] 04/04: Support the case where one of the CRS axes in a netCDF file is a scalar (number of dimensions = 0). In such case, the scale factor does not exist in the "gridToCRS" matrix; we have only a translation term. In other words, there is no corresponding source coordinate, only a target coordinate which is constant. Some netCDF files use scalar for specifying for example that all values are located at a height of 2 metres.

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 f2701ff293df5c802e2281dcf38bab50b6f2b144
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Thu Jan 7 19:04:27 2021 +0100

    Support the case where one of the CRS axes in a netCDF file is a scalar (number of dimensions = 0).
    In such case, the scale factor does not exist in the "gridToCRS" matrix; we have only a translation term.
    In other words, there is no corresponding source coordinate, only a target coordinate which is constant.
    Some netCDF files use scalar for specifying for example that all values are located at a height of 2 metres.
---
 .../java/org/apache/sis/internal/netcdf/Axis.java  | 12 +++++++++--
 .../java/org/apache/sis/internal/netcdf/Grid.java  | 24 ++++++++++++++++++++++
 .../sis/internal/netcdf/ucar/GridWrapper.java      | 10 ++++++---
 3 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
index 281754d..5c1db79 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
@@ -669,9 +669,17 @@ public final class Axis extends NamedElement {
     {
         switch (getNumDimensions()) {
             /*
-             * Defined as a matter of principle, but should never happen.
+             * Variable is a scalar, which is represented by an array of length 1.
+             * There is no source dimension, only a target dimension fixed to a constant value.
+             * Consequently the scale factor does not exist in the matrix, only the translation.
              */
-            case 0: return true;
+            case 0: {
+                final Vector data = read();
+                if (!data.isEmpty()) {
+                    gridToCRS.setElement(tgtDim, gridToCRS.getNumCol() - 1, data.doubleValue(0));
+                }
+                return true;
+            }
             /*
              * Normal case where the axis has only one dimension.
              */
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index 432cef3..f3f3d45 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -228,6 +228,30 @@ public abstract class Grid extends NamedElement {
                     i++;
                 }
             }
+            /*
+             * If some variables are scalar, those variables can safely be moved anywhere because they do not
+             * use any input coordinate (their output coordinates are constant). Try to move those scalars to
+             * more natural positions. We rely on `AxisDirection` ordering: North, East, South, West, Up/Down,
+             * Future/Past directions in that order.
+             */
+            for (i=0; i < axes.length; i++) {
+                final Axis axis = axes[i];
+                if (axis.getNumDimensions() == 0) {
+                    int p = i;
+                    for (int j=0; j<axes.length; j++) {
+                        if (axis.direction.compareTo(axes[j].direction) > 0) {
+                            p = j + 1;      // After the last element that should be ordered before `axis`.
+                        }
+                    }
+                    if (p != i) {
+                        if (p > i) p--;
+                        System.arraycopy(axes, i+1, axes, i, axes.length - (i+1));      // Remove element at i.
+                        System.arraycopy(axes, p, axes, p+1, axes.length - (p+1));      // Insert a space at p.
+                        axes[p] = axis;
+                        break;
+                    }
+                }
+            }
         }
         return axes;
     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
index 0896332..e102591 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
@@ -292,11 +292,15 @@ next:       for (final String name : axisNames) {
          * in the same order we presume that this is the "netCDF" order (as opposed to a "coordinates"
          * attribute value order).
          */
-        for (int i = Math.min(domain.size(), range.size()); --i >= 0;) {
+        int i = range.size();
+        int j = domain.size();
+        while (--i >= 0) {
             final List<Dimension> dimensions = range.get(i).getDimensions();
-            if (dimensions.size() != 1 || !dimensions.get(0).equals(domain.get(i))) {
-                return axes;
+            switch (dimensions.size()) {
+                case 0: continue;           // Ignore scalars as they can appear anywhere.
+                case 1: if (--j >= 0 && dimensions.get(0).equals(domain.get(j))) continue;
             }
+            return axes;
         }
         ArraysExt.reverse(axes);
         return axes;


[sis] 01/04: Move remaining code from `CategoryColorsCell` into `ColorCell` for sharing (in a future commit) with isoline table. The methods that were previously overridden in `CategoryColorsCell` class are now overridden in `ColorColumnHandler`. Cell behavior has been reworked for reducing the amount of mouse clicks and for enabling keyboard usage.

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 da676c5da6a88495fa87b36e6d0921c40692a5e5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Jan 4 16:23:32 2021 +0100

    Move remaining code from `CategoryColorsCell` into `ColorCell` for sharing (in a future commit) with isoline table.
    The methods that were previously overridden in `CategoryColorsCell` class are now overridden in `ColorColumnHandler`.
    Cell behavior has been reworked for reducing the amount of mouse clicks and for enabling keyboard usage.
---
 .../sis/gui/coverage/CategoryColorsCell.java       | 220 -----------
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   |  85 ++++-
 .../sis/internal/gui/ImmutableObjectProperty.java  |   5 +-
 .../apache/sis/internal/gui/control/ColorCell.java | 421 +++++++++++++--------
 .../internal/gui/control/ColorColumnHandler.java   | 124 ++++++
 .../gui/control/ColorRamp.java}                    | 131 ++++---
 .../sis/gui/coverage/CoverageStylingApp.java       |   6 +-
 8 files changed, 529 insertions(+), 465 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
deleted file mode 100644
index d35cd29..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * 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.gui.coverage;
-
-import javafx.scene.paint.Color;
-import javafx.scene.shape.Rectangle;
-import javafx.scene.control.ComboBox;
-import javafx.scene.control.ComboBoxBase;
-import javafx.scene.control.ListCell;
-import javafx.scene.control.TableCell;
-import javafx.scene.control.TableView;
-import javafx.scene.control.TableColumn;
-import javafx.scene.control.ContentDisplay;
-import javafx.geometry.Pos;
-import javafx.beans.value.ObservableValue;
-import javafx.beans.property.ReadOnlyObjectWrapper;
-import org.opengis.util.InternationalString;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.internal.gui.GUIUtilities;
-import org.apache.sis.internal.gui.control.ColorCell;
-import org.apache.sis.coverage.Category;
-
-
-/**
- * Cell representing the color of a qualitative or quantitative category.
- * The color can be modified by selecting the table row, then clicking on the color.
- *
- * <p>The interfaces implemented by this class are implementation convenience
- * that may change in any future version.</p>
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- * @module
- */
-final class CategoryColorsCell extends ColorCell<Category,CategoryColors> {
-    /**
-     * The function that determines which colors to apply on a given category.
-     * This same instance is shared by all cells of the same category table.
-     */
-    private final CoverageStyling styling;
-
-    /**
-     * The control for selecting a color ramp, or {@code null} if not yet created.
-     * This applies to quantitative categories. By contrast, {@link #colorPicker}
-     * applies to qualitative categories.
-     *
-     * @see Category#isQuantitative()
-     */
-    private ComboBox<CategoryColors> colorRampChooser;
-
-    /**
-     * Creates a cell for the colors column.
-     * This constructor is for {@link #createTable(CoverageStyling, Vocabulary)} usage only.
-     */
-    private CategoryColorsCell(final CoverageStyling styling) {
-        this.styling = styling;
-    }
-
-    /**
-     * Returns the initial item for a new cell.
-     */
-    @Override
-    protected CategoryColors getDefaultItem() {
-        return CategoryColors.GRAYSCALE;
-    }
-
-    /**
-     * Creates an item for a new color selected by user.
-     * The given object may be an instance of one of the following classes:
-     *
-     * <ul>
-     *   <li>{@link Color} if the chooser was {@link javafx.scene.control.ColorPicker}.</li>
-     *   <li>{@link CategoryColors} if the chooser was {@code ComboBox<CategoryColors>}.</li>
-     * </ul>
-     *
-     * @param  value  the color or gradient paint selected by the user.
-     * @return the item to store in this cell for the given color or gradient.
-     */
-    @Override
-    protected CategoryColors createItemForSelection(final Object value) {
-        if (value instanceof Color) {
-            return new CategoryColors(GUIUtilities.toARGB((Color) value));
-        } else {
-            // A ClassCastException here would be a bug in ColorsCell editors management.
-            return (CategoryColors) value;
-        }
-    }
-
-    /**
-     * Returns a color or gradient chooser initialized to the given item.
-     * This is invoked when the user clicks on a cell for modifying the color value.
-     *
-     * @param  colors  the initial color or gradient to show.
-     * @return the control to show to user.
-     */
-    @Override
-    protected ComboBoxBase<?> getControlForEdit(final CategoryColors colors) {
-        if (getTableRow().getItem().isQuantitative()) {
-            if (colorRampChooser == null) {
-                colorRampChooser = new ComboBox<>();
-                colorRampChooser.setEditable(false);
-                colorRampChooser.setCellFactory((column) -> new Ramp());
-                colorRampChooser.getItems().setAll(CategoryColors.GRAYSCALE, CategoryColors.BELL);
-                addListeners(colorRampChooser);
-            }
-            colors.asSelectedItem(colorRampChooser);
-            return colorRampChooser;
-        } else {
-            return super.getControlForEdit(colors);
-        }
-    }
-
-    /**
-     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser} has the focus.
-     * This is used for assertions.
-     */
-    @Override
-    protected boolean controlNotFocused() {
-        return super.controlNotFocused() && (colorRampChooser == null || !colorRampChooser.isFocused());
-    }
-
-    /**
-     * Cell for a color ramp in a {@link ComboBox}.
-     */
-    private final class Ramp extends ListCell<CategoryColors> {
-        /** Creates a new cell. */
-        Ramp() {
-            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
-            setMaxWidth(Double.POSITIVE_INFINITY);
-        }
-
-        /** Sets the colors to show in the combo box item. */
-        @Override
-        protected void updateItem(final CategoryColors colors, final boolean empty) {
-            super.updateItem(colors, empty);
-            if (colors == null) {
-                setGraphic(null);
-            } else {
-                Rectangle r = (Rectangle) getGraphic();
-                if (r == null) {
-                    r = createRectangle(-40);
-                    setGraphic(r);
-                }
-                r.setFill(colors.paint());
-            }
-        }
-    }
-
-    /**
-     * Invoked when users confirmed that (s)he wants to use the selected colors.
-     *
-     * @param  colors  the colors to use.
-     */
-    @Override
-    public void commitEdit(final CategoryColors colors) {
-        super.commitEdit(colors);
-        styling.setARGB(getTableRow().getItem(), colors.colors);
-    }
-
-    /**
-     * Creates a table of categories.
-     *
-     * @param  styling     function that determines which colors to apply on a given category.
-     * @param  vocabulary  resources for the locale in use.
-     */
-    static TableView<Category> createTable(final CoverageStyling styling, final Vocabulary vocabulary) {
-        final TableColumn<Category,String> name = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
-        name.setCellValueFactory(CategoryColorsCell::getCategoryName);
-        name.setCellFactory(CategoryColorsCell::createNameCell);
-        name.setEditable(false);
-        name.setId("name");
-
-        final TableColumn<Category,CategoryColors> colors = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Colors));
-        colors.setCellValueFactory(styling);
-        colors.setCellFactory((column) -> new CategoryColorsCell(styling));
-        colors.setId("colors");
-        colors.setSortable(false);
-
-        final TableView<Category> table = new TableView<>();
-        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
-        table.getColumns().setAll(name, colors);
-        table.setEditable(true);
-        return table;
-    }
-
-    /**
-     * Invoked for creating a cell for the "name" column.
-     * Returns the JavaFX default cell except for vertical alignment, which is centered.
-     */
-    private static TableCell<Category,String> createNameCell(final TableColumn<Category,String> column) {
-        @SuppressWarnings("unchecked")
-        final TableCell<Category,String> cell =
-                (TableCell<Category,String>) TableColumn.DEFAULT_CELL_FACTORY.call(column);
-        cell.setAlignment(Pos.CENTER_LEFT);
-        return cell;
-    }
-
-    /**
-     * Invoked when the table needs to render a text in the "Name" column of the category table.
-     */
-    private static ObservableValue<String> getCategoryName(final TableColumn.CellDataFeatures<Category,String> cell) {
-        final InternationalString name = cell.getValue().getName();
-        return (name != null) ? new ReadOnlyObjectWrapper<>(name.toString()) : null;
-    }
-}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index ab2e257..29d0fad 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -130,7 +130,7 @@ final class CoverageControls extends Controls {
         final VBox colorsPane;
         {   // Block for making variables locale to this scope.
             final CoverageStyling styling = new CoverageStyling(view);
-            categoryTable = CategoryColorsCell.createTable(styling, vocabulary);
+            categoryTable = styling.createCategoryTable(vocabulary);
             final GridPane gp = Styles.createControlGrid(0,
                 label(vocabulary, Vocabulary.Keys.Stretching, Stretching.createButton((p,o,n) -> view.setStyling(n))));
 
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
index fca46ed..2e3f7ca 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -22,12 +22,18 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.function.Function;
-import javafx.util.Callback;
+import javafx.geometry.Pos;
+import javafx.scene.control.TableCell;
 import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
 import javafx.beans.value.ObservableValue;
-import javafx.beans.property.SimpleObjectProperty;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.internal.coverage.j2d.Colorizer;
+import org.apache.sis.internal.gui.ImmutableObjectProperty;
+import org.apache.sis.internal.gui.control.ColorRamp;
+import org.apache.sis.internal.gui.control.ColorColumnHandler;
+import org.apache.sis.util.resources.Vocabulary;
+import org.opengis.util.InternationalString;
 
 
 /**
@@ -41,9 +47,7 @@ import org.apache.sis.internal.coverage.j2d.Colorizer;
  * @since   1.1
  * @module
  */
-final class CoverageStyling implements Function<Category,Color[]>,
-        Callback<TableColumn.CellDataFeatures<Category,CategoryColors>, ObservableValue<CategoryColors>>
-{
+final class CoverageStyling extends ColorColumnHandler<Category> implements Function<Category,Color[]> {
     /**
      * Customized colors selected by user. Keys are English names of categories.
      *
@@ -86,6 +90,8 @@ final class CoverageStyling implements Function<Category,Color[]>,
 
     /**
      * Associates colors to the given category.
+     *
+     * @param  colors  the new color for the given category, or {@code null} for resetting default value.
      */
     final void setARGB(final Category category, final int[] colors) {
         final String key = key(category);
@@ -101,11 +107,13 @@ final class CoverageStyling implements Function<Category,Color[]>,
     }
 
     /**
-     * Same as {@link #apply(Category)}, but returns colors as an array of ARGB codes.
-     * Contrarily to {@link #apply(Category)}, this method may return references to
-     * internal arrays; <strong>do not modify.</strong>
+     * Invoked by {@link TableColumn} for computing value of a {@link ColorCell}.
+     * Does the same work as {@link #apply(Category)}, but returns colors as an array of ARGB codes.
+     * Contrarily to {@link #apply(Category)}, this method may return references to internal arrays;
+     * <strong>do not modify.</strong>
      */
-    private int[] getARGB(final Category category) {
+    @Override
+    protected int[] getARGB(final Category category) {
         int[] ARGB = customizedColors.get(key(category));
         if (ARGB == null) {
             final Color[] colors = fallback.apply(category);
@@ -141,18 +149,55 @@ final class CoverageStyling implements Function<Category,Color[]>,
     }
 
     /**
-     * Invoked by {@link TableColumn} for computing value of a {@link CategoryColorsCell}.
-     * This method is public as an implementation side-effect; do not rely on that.
+     * Invoked when users confirmed that (s)he wants to use the selected colors.
+     *
+     * @param  value    the category for which to assign new color(s).
+     * @param  newItem  the new color for the given category, or {@code null} for resetting default value.
+     * @return the type of color (solid or gradient) shown for the given value.
      */
     @Override
-    public ObservableValue<CategoryColors> call(final TableColumn.CellDataFeatures<Category,CategoryColors> cell) {
-        final Category category = cell.getValue();
-        if (category != null) {
-            final int[] ARGB = getARGB(category);
-            if (ARGB != null) {
-                return new SimpleObjectProperty<>(new CategoryColors(ARGB));
-            }
-        }
-        return null;
+    protected ColorRamp.Type applyColors(final Category value, ColorRamp newItem) {
+        setARGB(value, (newItem != null) ? newItem.colors : null);
+        return value.isQuantitative() ? ColorRamp.Type.GRADIENT : ColorRamp.Type.SOLID;
+    }
+
+    /**
+     * Creates a table showing the color of a qualitative or quantitative coverage categories.
+     * The color can be modified by selecting the table row, then clicking on the color.
+     *
+     * @param  vocabulary  resources for the locale in use.
+     */
+    final TableView<Category> createCategoryTable(final Vocabulary vocabulary) {
+        final TableColumn<Category,String> name = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
+        name.setCellValueFactory(CoverageStyling::getCategoryName);
+        name.setCellFactory(CoverageStyling::createNameCell);
+        name.setEditable(false);
+        name.setId("name");
+
+        final TableView<Category> table = new TableView<>();
+        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        table.getColumns().add(name);
+        addColumnTo(table, vocabulary);
+        table.setEditable(true);
+        return table;
+    }
+
+    /**
+     * Invoked for creating a cell for the "name" column.
+     * Returns the JavaFX default cell except for vertical alignment, which is centered.
+     */
+    private static TableCell<Category,String> createNameCell(final TableColumn<Category,String> column) {
+        @SuppressWarnings("unchecked")
+        final TableCell<Category,String> cell = (TableCell<Category,String>) TableColumn.DEFAULT_CELL_FACTORY.call(column);
+        cell.setAlignment(Pos.CENTER_LEFT);
+        return cell;
+    }
+
+    /**
+     * Invoked when the table needs to render a text in the "Name" column of the category table.
+     */
+    private static ObservableValue<String> getCategoryName(final TableColumn.CellDataFeatures<Category,String> cell) {
+        final InternationalString name = cell.getValue().getName();
+        return (name != null) ? new ImmutableObjectProperty<>(name.toString()) : null;
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
index c933c5e..28faa57 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
@@ -26,7 +26,10 @@ import javafx.beans.property.ReadOnlyObjectProperty;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
- * @since   1.1
+ *
+ * @param  <T>  the type of value stored in the property.
+ *
+ * @since 1.1
  * @module
  */
 public class ImmutableObjectProperty<T> extends ReadOnlyObjectProperty<T> {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
index 03cabba..4ddd450 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
@@ -16,92 +16,44 @@
  */
 package org.apache.sis.internal.gui.control;
 
+import javafx.collections.ObservableList;
 import javafx.event.ActionEvent;
 import javafx.event.EventHandler;
 import javafx.scene.Node;
 import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBox;
 import javafx.scene.control.ComboBoxBase;
 import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Control;
+import javafx.scene.control.ListCell;
 import javafx.scene.control.TableCell;
+import javafx.scene.control.TableRow;
+import javafx.scene.control.TableView;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Rectangle;
+import org.apache.sis.internal.gui.GUIUtilities;
 
 
 /**
- * Cell representing the color of an object.
+ * Cell showing the color of an object (category, isoline, <i>etc</i>).
  * The color can be modified by selecting the table row, then clicking on the color.
- * Subclasses should override the following methods:
+ * The conversion between {@link ColorRamp} and the {@code <S>} object for row data
+ * is handled by a {@link ColorColumnHandler}. The same handler may be shared by all
+ * {@code ColorCell}s in a table. All methods are invoked in JavaFX thread.
  *
- * <ul>
- *   <li>{@link #getDefaultItem()}</li>
- *   <li>{@link #createItemForSelection(Object)}</li>
- *   <li>{@link #getControlForEdit(T)} (unless showing only solid colors)</li>
- *   <li>{@link #controlNotFocused()} (unless showing only solid colors)</li>
- *   <li>{@link #commitEdit(Object)}</li>
- * </ul>
- *
- * All methods are invoked in JavaFX thread.
- *
- * <p>The interfaces implemented by this class are implementation convenience
- * that may change in any future version.</p>
+ * <p>The interfaces implemented by this class are implementation convenience that may change in any future version.
+ * {@link EventHandler} is for reacting to user color selection using the control shown.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  *
- * @param  <S>  the type of the {@code TableView} generic type.
- * @param  <T>  the type of the item contained within the cell.
+ * @param  <S>  the type of row data as declared in the {@code TableView} generic type.
  *
  * @since 1.1
  * @module
  */
-public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,T> implements EventHandler<ActionEvent> {
-    /**
-     * Gradient paint, colors or string representation of the rectangle to show in {@link ColorCell}.
-     * This is the object stored in {@link TableCell} after conversion from user value of type {@code <S>}.
-     *
-     * @see TableCell#getItem()
-     */
-    public abstract static class Item {
-        /**
-         * Returns the paint to use for filling a rectangle in {@link ColorCell}, or {@code null} if none.
-         * The default implementation returns the solid {@linkplain #color() color} (no gradient).
-         *
-         * @return color or gradient paint for table cell, or {@code null} if none.
-         */
-        protected Paint paint() {
-            return color();
-        }
-
-        /**
-         * Returns a solid color to use for filling a rectangle in {@link ColorCell}.
-         * If this view has many colors (for example because it uses a gradient),
-         * then some representative color or an arbitrary color should be returned.
-         *
-         * @return color for table cell, or {@code null} if none.
-         */
-        protected abstract Color color();
-
-        /**
-         * Updates a control with the current color of this item. Default implementation
-         * recognizes {@link ColorPicker}. Subclasses should override if different kinds
-         * of controls need to be handled.
-         *
-         * @param  control  the control to update.
-         * @return whether the given control has been recognized.
-         */
-        protected boolean updateControl(final Node control) {
-            if (control instanceof Rectangle) {
-                ((Rectangle) control).setFill(paint());
-            } else if (control instanceof ColorPicker) {
-                ((ColorPicker) control).setValue(color());
-            } else {
-                return false;
-            }
-            return true;
-        }
-    }
-
+final class ColorCell<S> extends TableCell<S,ColorRamp> implements EventHandler<ActionEvent> {
     /**
      * Space (in pixels) to remove on right side of the rectangle showing colors.
      */
@@ -113,89 +65,157 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     private static final double HEIGHT = 16;
 
     /**
+     * The converter between row data and the item to store in this {@code ColorCell}.
+     * There is typically only one instance for the whole color column.
+     */
+    private final ColorColumnHandler<S> handler;
+
+    /**
+     * The type of color ramp as determined by {@link ColorColumnHandler#applyColors(Object, ColorRamp)}.
+     * This is updated by {@link #updateItem(ColorRamp, boolean)} when the value changes and stored for
+     * keeping that value stable (this class does not support mutable colors type).
+     * May be {@code null} if there is no value in the row of this cell.
+     */
+    private ColorRamp.Type type;
+
+    /**
      * The control for selecting a single color, or {@code null} if not yet created.
      */
     private ColorPicker colorPicker;
 
     /**
-     * The single color shown in a cell of the "color" column. Created when first needed.
-     * User can modify the color of this rectangle with {@link #colorPicker} or other control
-     * provided by {@link #getControlForEdit(T)}.
+     * The control for selecting a color ramp, or {@code null} if not yet created.
      */
-    private Rectangle singleColor;
+    private ComboBox<ColorRamp> colorRampChooser;
 
     /**
-     * Colors to restore if user cancels an edit action.
+     * The color(s) shown in this cell when no control is shown, or {@code null} if not yet created.
+     * User can modify the color of this rectangle with {@link #colorPicker} or {@link #colorRampChooser}.
      */
-    private T restoreOnCancel;
+    private Rectangle colorView;
 
     /**
      * Creates a new cell for the colors column.
+     *
+     * @param  handler  the converter between row data and the item to store in this {@code ColorCell}.
      */
-    protected ColorCell() {
+    ColorCell(final ColorColumnHandler<S> handler) {
+        this.handler = handler;
         setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+        setOnMouseClicked((event) -> requestEdit());
     }
 
     /**
-     * Returns the initial item for a new cell. This is invoked when the
-     * user wants to edit a cell but {@link #getItem()} is still null.
+     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser} has the focus.
+     * This is used for assertions: we should not add or remove (by calls to {@link #setGraphic(Node)})
+     * one of those {@link ComboBoxBase}s before it has lost focus, otherwise it causes problems with
+     * the focus system.
      *
-     * @return initial color or paint for a new cell.
+     * @return {@code true} if no control has the focus.
      */
-    protected abstract T getDefaultItem();
+    private boolean controlNotFocused() {
+        return (colorPicker == null || !colorPicker.isFocused()) &&
+                (colorRampChooser == null || !colorRampChooser.isFocused());
+    }
 
     /**
-     * Creates an item for a new color selected using {@link ColorPicker} or other chooser.
-     * The given object may be an instance of one of the following classes:
-     *
-     * <ul>
-     *   <li>{@link Color} if the chooser was {@link ColorPicker}.</li>
-     *   <li>{@code <T>} if the chooser was {@code ComboBox<T>}.</li>
-     *   <li>Any other kind of value depending on {@link #getControlForEdit(T)}.</li>
-     * </ul>
+     * Shows the control button (not the popup window) for choosing color(s) in this cell. The control type will be
+     * {@link ColorPicker} or {@link ComboBox} depending on {@link ColorColumnHandler#applyColors(Object, ColorRamp)}.
+     * This method is invoked when edition started and does nothing if the button is already visible in the cell.
      *
-     * @param  value  the color or gradient paint selected by the user.
-     * @return the item to store in this cell for the given color or gradient.
-     */
-    protected abstract T createItemForSelection(Object value);
-
-    /**
-     * Returns a color or gradient chooser initialized to the given item.
-     * This is invoked when the user clicks on a cell for modifying the color value.
-     * The default implementation returns a {@link ColorPicker}.
+     * @return the control shown, or {@code null} if none.
      *
-     * @param  colors  the initial color or gradient to show.
-     * @return the control to show to user.
+     * @see #hideControlButton()
      */
-    protected ComboBoxBase<?> getControlForEdit(final T colors) {
-        if (colorPicker == null) {
-            colorPicker = new ColorPicker();
-            addListeners(colorPicker);
+    private ComboBoxBase<?> showControlButton() {
+        final Node current = getGraphic();
+        if (current instanceof ComboBoxBase<?>) {
+            return (ComboBoxBase<?>) current;
         }
-        colorPicker.setValue(colors.color());
-        return colorPicker;
+        assert controlNotFocused();
+        final ComboBoxBase<?> control;
+        if (type == null) {
+            control = null;
+        } else {
+            final boolean isNewControl;
+            switch (type) {
+                default: throw new AssertionError(type);
+                case SOLID: {
+                    if (isNewControl = (colorPicker == null)) {
+                        colorPicker = new ColorPicker();
+                        colorPicker.setMaxWidth(Double.MAX_VALUE);      // Take all the width inside the cell.
+                        updateColorPicker(getItem());
+                    }
+                    control = colorPicker;
+                    break;
+                }
+                case GRADIENT: {
+                    if (isNewControl = (colorRampChooser == null)) {
+                        colorRampChooser = new ComboBox<>();
+                        colorRampChooser.setEditable(false);
+                        colorRampChooser.setMaxWidth(Double.MAX_VALUE);
+                        colorRampChooser.setCellFactory((column) -> new RampChoice());
+                        colorRampChooser.getItems().setAll(ColorRamp.GRAYSCALE, ColorRamp.BELL);
+                        updateColorRampChooser(getItem());
+                    }
+                    control = colorRampChooser;
+                    break;
+                }
+            }
+            /*
+             * Add listeners only after the control got its initial value, for avoiding change event.
+             * We do not need to update the value here after control creation because future updates
+             * are handled by `updateItem(…)`.
+             */
+            if (isNewControl) {
+                control.setOnAction(this);
+                control.setOnShown((event) -> requestEdit());
+                control.setOnHidden((event) -> hidden());
+            }
+        }
+        setGraphic(control);
+        return control;
     }
 
     /**
-     * Returns {@code true} if the {@link #colorPicker} does not have the focus.
-     * This is used for assertions. Subclasses should override if there is more
-     * controls that may have the focus.
-     *
-     * @return {@code true} if no control has the focus.
+     * Cell for a color ramp in a list of choices shown by {@link ComboBox}.
+     * This is used by {@link #showControlButton()} for building {@link #colorRampChooser}.
      */
-    protected boolean controlNotFocused() {
-        return (colorPicker == null) || !colorPicker.isFocused();
+    private final class RampChoice extends ListCell<ColorRamp> {
+        /** Creates a new combo box choice. */
+        RampChoice() {
+            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+            setMaxWidth(Double.POSITIVE_INFINITY);
+        }
+
+        /** Sets the colors to show in the combo box item. */
+        @Override protected void updateItem(final ColorRamp colors, final boolean empty) {
+            super.updateItem(colors, empty);
+            if (colors == null) {
+                setGraphic(null);
+            } else {
+                Rectangle r = (Rectangle) getGraphic();
+                if (r == null) {
+                    r = createRectangle(-40);
+                    setGraphic(r);
+                }
+                r.setFill(colors.paint());
+            }
+        }
     }
 
     /**
-     * Creates the graphic to draw in a table cell or combo box cell for representing a color or color ramp.
-     * This method may be invoked by subclasses for building an editor in {@link #getControlForEdit(T)}.
+     * Creates the graphic to draw in a table cell or combo box cell for showing color(s).
+     * This method is invoked for building an editor in {@link #showControlButton()} or for
+     * rendering the {@link ColorRamp} in the table.
      *
      * @param  adjust  amount of space (in pixels) to add on the right size.
      *                 Can be a negative number for removing space.
      * @return graphic to draw in a table cell or combo box cell.
+     *
+     * @see #setColorItem(ColorRamp)
      */
-    protected final Rectangle createRectangle(final double adjust) {
+    private Rectangle createRectangle(final double adjust) {
         final Rectangle gr = new Rectangle();
         gr.setHeight(HEIGHT);
         gr.widthProperty().bind(widthProperty().add(adjust));
@@ -203,30 +223,39 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     }
 
     /**
-     * Finishes configuration of a newly created combo box.
-     * This method may be invoked by subclasses for building an editor in {@link #getControlForEdit(T)}.
-     *
-     * @param  control  the {@link ColorPicker} or other combo box on which to add listeners.
+     * Updates {@link #colorPicker} for the new item value.
      */
-    protected final void addListeners(final ComboBoxBase<?> control) {
-        control.setOnAction(this);
-        control.setOnHidden((e) -> hidden());
+    private void updateColorPicker(final ColorRamp item) {
+        colorPicker.setValue(item != null ? item.color() : null);
     }
 
-
-    //
-    // Methods below this line should not be called or overridden by subclasses.
-    //
-
+    /**
+     * Updates {@link #colorRampChooser} for the new item value. This method declares the given {@link ColorRamp}
+     * as the selected item in the chooser. If the item is not found, then it is added to the chooser list.
+     */
+    private void updateColorRampChooser(final ColorRamp item) {
+        if (item != null) {
+            final ObservableList<ColorRamp> items = colorRampChooser.getItems();
+            int i = items.indexOf(item);
+            if (i < 0) {
+                i = items.size();
+                items.add(item);
+            }
+            colorRampChooser.getSelectionModel().select(i);
+        } else {
+            colorRampChooser.getSelectionModel().clearSelection();
+        }
+    }
 
     /**
      * Invoked when the color in this cell changed. It may be because of user selection in a combo box,
-     * or because this cell is now used for a new {@code <S>} instance.
+     * or because this cell is now used for a new {@code <S>} instance. This method is invoked when the
+     * row value (of type {@code <S>}) is modified.
      *
      * <div class="note"><b>Implementation note:</b>
      * this method should not invoke {@link #setGraphic(Node)} if the current graphic is a {@link ComboBoxBase}
-     * (the parent of {@link ColorPicker}) because this method may be invoked at any time, including during the
-     * execution of {@link #startEdit()} or {@link #commitEdit(Object)} methods.
+     * (the parent of {@link ComboBox} and {@link ColorPicker}) because this method may be invoked at any time,
+     * including during the execution of {@link #startEdit()} or {@link #commitEdit(Object)} methods.
      * Adding or removing {@link ComboBoxBase} in this method cause problems with focus system.
      * In particular we must be sure to remove {@link ColorPicker} only after it has lost focus.</div>
      *
@@ -234,17 +263,47 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
      * @param  empty   {@code true} if this method is invoked for creating an empty cell.
      */
     @Override
-    protected final void updateItem(final T colors, final boolean empty) {
+    protected final void updateItem(final ColorRamp colors, final boolean empty) {
         super.updateItem(colors, empty);
-        final Node control = getGraphic();
-        if (colors != null) {
-            if (control == null) {
-                setColorItem(colors);
-            } else {
-                colors.updateControl(control);
+        /*
+         * Associate the new colors to the row in a way determined by the `ColorColumnHandler` class.
+         * Then get the new color type (solid or gradient) for the current row. Note that `TableRow`
+         * may be null early in the `TableCell` lifecycle.
+         */
+        type = null;
+        if (!empty) {
+            final TableRow<S> row = getTableRow();
+            if (row != null) {
+                final S item = row.getItem();
+                if (item != null) {
+                    type = handler.applyColors(item, colors);
+                }
             }
-        } else if (control instanceof Rectangle) {
-            setGraphic(null);
+        }
+        /*
+         * Update the visual representation. Update also the control even if it is hidden, because
+         * those updates should be less frequent than show/hide cycles. It avoids the need to update
+         * the control every time that it is shown.
+         */
+        if (type != null) {
+            switch (type) {
+                default: throw new AssertionError(type);
+                case SOLID: {
+                    if (colorPicker != null) {
+                        updateColorPicker(colors);
+                    }
+                    break;
+                }
+                case GRADIENT: {
+                    if (colorRampChooser != null) {
+                        updateColorRampChooser(colors);
+                    }
+                    break;
+                }
+            }
+        }
+        if (!(getGraphic() instanceof Control)) {
+            setColorItem(colors);
         }
     }
 
@@ -255,16 +314,16 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
      *
      * @param  colors  current value of {@link #getItem()}.
      */
-    private void setColorItem(final T colors) {
+    private void setColorItem(final ColorRamp colors) {
         assert controlNotFocused();
         Rectangle view = null;
         if (colors != null) {
             final Paint paint = colors.paint();
             if (paint != null) {
-                if (singleColor == null) {
-                    singleColor = createRectangle(WIDTH_ADJUST);
+                if (colorView == null) {
+                    colorView = createRectangle(WIDTH_ADJUST);
                 }
-                view = singleColor;
+                view = colorView;
                 view.setFill(paint);
             }
         }
@@ -272,38 +331,70 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     }
 
     /**
+     * Hides the control button for choosing color(s) in this cell. The focus is transferred to the parent table.
+     * This method does nothing if the control is already hidden.
+     *
+     * @see #showControlButton()
+     */
+    private void hideControlButton() {
+        final Node control = getGraphic();
+        if (control instanceof Control) {
+            if (control.isFocused()) {
+                // Must be before `setGraphic(…)` for causing ColorPicker to lost focus.
+                getTableView().requestFocus();
+            }
+            setColorItem(getItem());
+        }
+    }
+
+    /**
+     * Requests this cell to transition to editing state. The request is made on the {@link TableView},
+     * which will invoke {@link #startEdit()}. The edition request must be done on {@code TableView} for
+     * allowing the table to know to row and column index of the cell being edited.
+     */
+    private void requestEdit() {
+        if (isEditing()) {
+            showControlButton();
+        } else {
+            final int row = getTableRow().getIndex();
+            final TableView<S> table = getTableView();
+            table.getSelectionModel().select(row);
+            table.edit(row, getTableColumn());
+            // JavaFX will call `startEdit()`.
+        }
+    }
+
+    /**
      * Transitions from non-editing state to editing state. This method is automatically invoked when a
      * row is selected and the user clicks on the color cell in that row. This method sets the combo box
      * as the graphic element in that cell and shows it immediately. The immediate {@code control.show()}
      * is for avoiding to force users to perform a third mouse click.
+     *
+     * <p>This method should not be invoked directly. Invoke {@link #requestEdit()} instead.</p>
      */
     @Override
     public final void startEdit() {
-        restoreOnCancel = getItem();
-        final T colors = (restoreOnCancel != null) ? restoreOnCancel : getDefaultItem();
-        final ComboBoxBase<?> control = getControlForEdit(colors);
+        final ComboBoxBase<?> control = showControlButton();
         /*
          * Call `startEdit()` only after above call to `setValue(…)` because we want `isEditing()`
          * to return false during above value change. This is for preventing change listeners to
          * misinterpret the value change as a user selection.
          */
         super.startEdit();
-        setGraphic(control);            // Must be before `requestFocus()`, otherwise focus request is ignored.
-        control.requestFocus();         // Must be before `show()`, otherwise there is apparent focus confusion.
-        control.show();
+        if (control != null) {
+            control.requestFocus();     // Must be before `show()`, otherwise there is apparent focus confusion.
+            control.show();             // For requiring one less mouse click by user.
+        }
     }
 
     /**
      * Transitions from an editing state into a non-editing state without saving any user input.
-     * This method is automatically invoked when the user click on another table row.
+     * This method is automatically invoked when the user clicks on another table row.
      */
     @Override
     public final void cancelEdit() {
-        setItem(restoreOnCancel);
-        restoreOnCancel = null;
         super.cancelEdit();
-        assert controlNotFocused();
-        setColorItem(getItem());
+        hideControlButton();
     }
 
     /**
@@ -316,20 +407,24 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
      * been invoked and we need to either commit now or cancel. Current implementation cancels.</p>
      */
     private void hidden() {
-        if (isEditing()) {
-            if (isHover()) {
-                return;                 // Keep editing state.
+        if (!isHover()) {
+            if (isEditing()) {
+                getTableView().edit(-1, null);          // Cancel editing.
             }
-            setItem(restoreOnCancel);
-            super.cancelEdit();
+            hideControlButton();
         }
-        restoreOnCancel = null;
-        getTableView().requestFocus();  // Must be before `setGraphic(…)` for causing ColorPicker to lost focus.
-        setColorItem(getItem());
     }
 
     /**
      * Invoked when the user selected a new value in the color picker or color ramp chooser.
+     * This handler creates an item for the new color(s). The selected value may be an instance
+     * of one of the following classes:
+     *
+     * <ul>
+     *   <li>{@link Color} if the chooser was {@link ColorPicker}.</li>
+     *   <li>{@code ColorRamp} if the chooser was {@code ComboBox<ColorRamp>}.</li>
+     * </ul>
+     *
      * This method is public as an implementation side-effect and should never be invoked directly.
      *
      * @param  event  the {@link ComboBoxBase} on which a selection occurred.
@@ -337,14 +432,18 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     @Override
     public final void handle(final ActionEvent event) {
         if (isEditing()) {
+            final ColorRamp colors;
             final Object source = event.getSource();
-            final T value;
             if (source instanceof ComboBoxBase<?>) {
-                value = createItemForSelection(((ComboBoxBase<?>) source).getValue());
-            } else {
-                value = restoreOnCancel;
+                final Object value = ((ComboBoxBase<?>) source).getValue();
+                if (value instanceof Color) {
+                    colors = new ColorRamp(GUIUtilities.toARGB((Color) value));
+                } else {
+                    // A ClassCastException here would be a bug in ColorCell editors management.
+                    colors = (ColorRamp) value;
+                }
+                commitEdit(colors);
             }
-            commitEdit(value);
         }
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
new file mode 100644
index 0000000..e89b02c
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
@@ -0,0 +1,124 @@
+/*
+ * 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.gui.control;
+
+import javafx.util.Callback;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TableColumn;
+import javafx.beans.value.ObservableValue;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.gui.ImmutableObjectProperty;
+
+
+/**
+ * Builds and configures a {@link TableColumn} for colors. The {@link TableView} owner may be a table of
+ * coverage categories or a table of isolines among others. {@code ColorColumnHandler} does conversions
+ * between row data and {@link ColorRamp} items. There is typically a single {@code ColorColumnHandler}
+ * instance shared by all {@link ColorCell} in the same column of a table.
+ *
+ * <p>The interfaces implemented by this class are implementation convenience
+ * that may change in any future version.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @param  <S>  the type of row data as declared in the {@link TableView} generic type.
+ *
+ * @since 1.1
+ * @module
+ */
+public abstract class ColorColumnHandler<S> implements Callback<TableColumn.CellDataFeatures<S,ColorRamp>, ObservableValue<ColorRamp>> {
+    /**
+     * Builds a new color table handler.
+     */
+    protected ColorColumnHandler() {
+    }
+
+    /**
+     * Sets the color(s) associated to the given row item and returns the color type (solid or gradient).
+     * The color type does not necessarily depend on the given {@code ColorRamp}; it may depend on the row
+     * item instead, at implementation choice. The type determines which control (color picker, combo box,
+     * <i>etc.</i>) will be shown if user wants to edit the color.
+     *
+     * @param  row      the row to update.
+     * @param  newItem  the new color(s) to assign to the given row item. May be {@code null}.
+     * @return the type of color (solid or gradient) shown for the given value.
+     */
+    protected abstract ColorRamp.Type applyColors(S row, ColorRamp newItem);
+
+    /**
+     * Gets the ARGB codes of colors to shown in the cell for the given row data.
+     *
+     * @param  row  the row item for which to get ARGB codes to show in color cell.
+     * @return the colors as ARGB codes.
+     */
+    protected abstract int[] getARGB(S row);
+
+    /**
+     * Invoked by {@link TableColumn} for computing the value of a {@link ColorCell}.
+     * This method is public as an implementation side-effect; do not rely on that.
+     *
+     * @param  cell  the row value together with references to column and table where the show the color cell.
+     * @return the color cell value.
+     */
+    @Override
+    public final ObservableValue<ColorRamp> call(final TableColumn.CellDataFeatures<S,ColorRamp> cell) {
+        final S value = cell.getValue();
+        if (value != null) {
+            final int[] ARGB = getARGB(value);
+            if (ARGB != null) {
+                return new ImmutableObjectProperty<>(new ColorRamp(ARGB));
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds a colors column to the specified table.
+     * This method also modifies the table configuration.
+     *
+     * @param  table       the table where to add a colors column.
+     * @param  vocabulary  localized resources, provided in argument because often already known by caller.
+     */
+    protected final void addColumnTo(final TableView<S> table, final Vocabulary vocabulary) {
+        final TableColumn<S,ColorRamp> colors = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Colors));
+        colors.setCellFactory((column) -> new ColorCell<S>(this));
+        colors.setCellValueFactory(this);
+        colors.setSortable(false);
+        colors.setId("colors");
+        /*
+         * Filters are invoked during the phase when events are propagated from root to target (in contrast
+         * to handlers which are invoked in a later phase when events are propagated in opposite direction).
+         * By registering a filter, we intercept (consume) the event early and avoid that `TableCell` tries
+         * to handle it. This is necessary for avoiding `NullPointerException` observed in our experiments.
+         * That exception occurred in JavaFX code that we do not control. Note: we tried to register filter
+         * directly on the cell, but it is apparently too late for preventing the `NullPointerException`.
+         */
+        table.addEventFilter(KeyEvent.KEY_PRESSED, (event) -> {
+            if (event.getCode() == KeyCode.ENTER) {
+                event.consume();
+                final int row = table.getSelectionModel().getSelectedIndex();
+                if (row >= 0) {
+                    table.edit(row, colors);
+                }
+            }
+        });
+        table.getColumns().add(colors);
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
similarity index 55%
rename from application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
rename to application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
index adf2e56..017016a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
@@ -14,11 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.gui.coverage;
+package org.apache.sis.internal.gui.control;
 
 import java.util.Arrays;
-import javafx.collections.ObservableList;
-import javafx.scene.Node;
+import javafx.scene.control.ColorPicker;
 import javafx.scene.control.ComboBox;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.CycleMethod;
@@ -27,102 +26,107 @@ import javafx.scene.paint.Paint;
 import javafx.scene.paint.Stop;
 import org.apache.sis.internal.gui.ColorName;
 import org.apache.sis.internal.gui.GUIUtilities;
-import org.apache.sis.internal.gui.control.ColorCell;
 import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
- * Represents a single color or a color ramp that can be represented in {@link CategoryColorsCell}.
+ * A single color or a gradient of colors shown as a rectangle in a {@link ColorCell}.
+ * Can also produce a string representation to be shown in a list.
+ * Instances should be considered immutable.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
- * @since   1.1
+ *
+ * @see ColorCell#getItem()
+ *
+ * @since 1.1
  * @module
  */
-final class CategoryColors extends ColorCell.Item {
+public final class ColorRamp {
+    /**
+     * The type of colors to shown in a cell.
+     * The type determines how user can choose a value.
+     */
+    public enum Type {
+        /**
+         * Single color selected by {@link ColorPicker}.
+         */
+        SOLID,
+
+        /**
+         * Gradient of colors (or a color ramp) selected by {@link ComboBox}.
+         */
+        GRADIENT
+    }
+
     /**
-     * Default color palette.
+     * Default color ramp.
      */
-    static final CategoryColors GRAYSCALE = new CategoryColors(0xFF000000, 0xFFFFFFFF);
+    static final ColorRamp GRAYSCALE = new ColorRamp(0xFF000000, 0xFFFFFFFF);
 
     /**
      * Blue – Cyan – White – Yellow – Red.
      */
-    static final CategoryColors BELL = new CategoryColors(0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFF0000);
+    static final ColorRamp BELL = new ColorRamp(0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFF0000);
 
     /**
      * ARGB codes of this single color or color ramp.
      * If null or empty, then default to transparent.
+     *
+     * <p><strong>This array should be read-only.</strong> We make it public because this class is internal.
+     * If this {@code ColorRamp} class moves to public API, then we would need to replace this public access
+     * by an accessor doing a copy.</p>
      */
-    final int[] colors;
+    public final int[] colors;
 
     /**
-     * The paint for this palette, created when first needed.
+     * A single color created from {@link #colors} when first needed.
+     *
+     * @see #color()
+     */
+    private transient Color color;
+
+    /**
+     * A gradient of colors created from {@link #colors} when first needed.
+     *
+     * @see #paint()
      */
     private transient Paint paint;
 
     /**
-     * A name for this palette, computed when first needed.
+     * A name for this color ramp created from {@link #colors} when first needed.
      *
      * @see #toString()
      */
     private transient String name;
 
     /**
-     * Creates a new palette for the given colors.
+     * Creates a new item for the given colors.
      */
-    CategoryColors(final int... colors) {
+    ColorRamp(final int... colors) {
         this.colors = colors;
     }
 
     /**
-     * Updates a control with the current color of this view.
+     * Returns a solid color to use for filling a rectangle in {@link ColorCell}.
+     * If this item has many colors (for example because it uses a gradient),
+     * then an arbitrary color is returned.
      *
-     * @return whether the given control has been recognized.
-     */
-    @Override
-    @SuppressWarnings("unchecked")
-    protected boolean updateControl(final Node control) {
-        if (!super.updateControl(control)) {
-            // A ClassCastException here would be a bug in CategoryColorsCell editors management.
-            asSelectedItem((ComboBox<CategoryColors>) control);
-        }
-        return true;
-    }
-
-    /**
-     * Declares this {@code CategoryColors} as the selected item in the given chooser.
-     * If this instance is not found, then it is added to the chooser list.
+     * @return single color to shown in table cell, or {@code null} if none.
      */
-    final void asSelectedItem(final ComboBox<CategoryColors> colorRampChooser) {
-        final ObservableList<CategoryColors> items = colorRampChooser.getItems();
-        int i = items.indexOf(this);
-        if (i < 0) {
-            i = items.size();
-            items.add(this);
+    final Color color() {
+        if (color == null && colors != null && colors.length != 0) {
+            color = GUIUtilities.fromARGB(colors[colors.length / 2]);
         }
-        colorRampChooser.getSelectionModel().select(i);
+        return color;
     }
 
     /**
-     * Returns the first color, or {@code null} if none.
-     * This is used for qualitative categories, which are expected to contain only one color.
-     */
-    @Override
-    protected final Color color() {
-        if (colors != null && colors.length != 0) {
-            return GUIUtilities.fromARGB(colors[0]);
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Gets the paint to use for filling a rectangle using this color palette.
-     * Returns {@code null} if this {@code CategoryColors} contains no color.
+     * Returns the paint to use for filling a rectangle in {@link ColorCell}, or {@code null} if none.
+     *
+     * @return color or gradient paint for table cell, or {@code null} if none.
      */
-    @Override
-    protected final Paint paint() {
+    final Paint paint() {
         if (paint == null) {
             switch (colors.length) {
                 case 0: break;
@@ -137,6 +141,7 @@ final class CategoryColors extends ColorCell.Item {
                         stops[i] = new Stop(scale*i, GUIUtilities.fromARGB(colors[i]));
                     }
                     paint = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
+                    break;
                 }
             }
         }
@@ -144,8 +149,8 @@ final class CategoryColors extends ColorCell.Item {
     }
 
     /**
-     * Returns a string representation of this color palette.
-     * This string representation will appear in the combo box when that box is shown.
+     * Returns a string representation of this color ramp.
+     * It may be used as an alternative to colored rectangle.
      */
     @Override
     public String toString() {
@@ -170,8 +175,10 @@ final class CategoryColors extends ColorCell.Item {
     }
 
     /**
-     * Returns a hash code value for this palette.
+     * Returns a hash code value for this color ramp.
      * Defined mostly for consistency with {@link #equals(Object)}.
+     *
+     * @return a hash code value for this color ramp.
      */
     @Override
     public int hashCode() {
@@ -179,10 +186,14 @@ final class CategoryColors extends ColorCell.Item {
     }
 
     /**
-     * Returns whether the given object is equal to this {@code CategoryColors}.
+     * Returns whether the given object is equal to this {@code ColorRamp}.
+     * This is used for locating this {@code ColorRamp} in a {@link ComboBox}.
+     *
+     * @param  other  the object to compare with {@code this} for equality.
+     * @return whether the given object is equal to this color ramp.
      */
     @Override
     public boolean equals(final Object other) {
-        return (other instanceof CategoryColors) && Arrays.equals(colors, ((CategoryColors) other).colors);
+        return (other instanceof ColorRamp) && Arrays.equals(colors, ((ColorRamp) other).colors);
     }
 }
diff --git a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
index a66da1d..815d5d8 100644
--- a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
+++ b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
@@ -20,6 +20,7 @@ import java.util.Locale;
 import javafx.stage.Stage;
 import javafx.application.Application;
 import javafx.scene.Scene;
+import javafx.scene.control.Button;
 import javafx.scene.control.TableView;
 import javafx.scene.layout.BorderPane;
 import org.apache.sis.coverage.Category;
@@ -55,7 +56,8 @@ public final strictfp class CoverageStylingApp extends Application {
     public void start(final Stage window) {
         final BorderPane pane = new BorderPane();
         pane.setCenter(createCategoryTable());
-        window.setTitle("BandColorsTable Test");
+        pane.setBottom(new Button("Focus here"));
+        window.setTitle("CategoryTable Test");
         window.setScene(new Scene(pane));
         window.setWidth (400);
         window.setHeight(300);
@@ -76,7 +78,7 @@ public final strictfp class CoverageStylingApp extends Application {
 
         final CoverageStyling styling = new CoverageStyling(null);
         styling.setARGB(band.getCategories().get(1), new int[] {0xFF607080});
-        final TableView<Category> table = CategoryColorsCell.createTable(styling, Vocabulary.getResources((Locale) null));
+        final TableView<Category> table = styling.createCategoryTable(Vocabulary.getResources((Locale) null));
         table.getItems().setAll(band.getCategories());
         return table;
     }


[sis] 03/04: Fix an `IndexOutOfBoundsException` when CRS has more dimensions than the dimensions for which a precision has been specified.

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 e9ea08a99eb502daccf4d6bb50299852140e11ce
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Thu Jan 7 16:59:23 2021 +0100

    Fix an `IndexOutOfBoundsException` when CRS has more dimensions than the dimensions for which a precision has been specified.
---
 .../src/main/java/org/apache/sis/geometry/CoordinateFormat.java         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
index 1b23db2..0731a84 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
@@ -1079,7 +1079,7 @@ abort:  if (dimensions != 0 && groundAccuracy != null) try {
     private void updateAccuracyVisibility() {
         isAccuracyVisible = (accuracyText != null);
         if (isAccuracyVisible && desiredPrecisions != null) {
-            long dimensions = groundDimensions;
+            long dimensions = groundDimensions & (Numerics.bitmask(desiredPrecisions.length) - 1);
             if (dimensions != 0) {
                 isAccuracyVisible = false;
                 do {