sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Provide control on the colors used for rendering different range of values (categories) of a GridCoverage.
Date Tue, 11 Aug 2020 18:34:06 GMT
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 1d075eb2fbfbdbbbecca27371e9944f416b82cdd
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Aug 11 20:33:23 2020 +0200

    Provide control on the colors used for rendering different range of values (categories) of a GridCoverage.
---
 .../apache/sis/gui/coverage/CategoryColors.java    | 165 ++++++++++
 .../sis/gui/coverage/CategoryColorsCell.java       | 364 +++++++++++++++++++++
 .../java/org/apache/sis/gui/coverage/Controls.java |  12 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  23 ++
 .../apache/sis/gui/coverage/CoverageControls.java  | 110 +++----
 .../apache/sis/gui/coverage/CoverageStyling.java   | 158 +++++++++
 .../org/apache/sis/internal/gui/ColorName.java     |  87 +++++
 .../org/apache/sis/internal/gui/GUIUtilities.java  |  33 ++
 .../sis/gui/coverage/CoverageStylingApp.java       |  83 +++++
 .../apache/sis/internal/gui/GUIUtilitiesTest.java  |  27 ++
 .../org/apache/sis/util/resources/Vocabulary.java  |  15 +
 .../sis/util/resources/Vocabulary.properties       |   3 +
 .../sis/util/resources/Vocabulary_fr.properties    |   3 +
 13 files changed, 1013 insertions(+), 70 deletions(-)

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/gui/coverage/CategoryColors.java
new file mode 100644
index 0000000..e4cf01f
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
@@ -0,0 +1,165 @@
+/*
+ * 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 java.util.Arrays;
+import javafx.collections.ObservableList;
+import javafx.scene.control.ComboBox;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.CycleMethod;
+import javafx.scene.paint.LinearGradient;
+import javafx.scene.paint.Paint;
+import javafx.scene.paint.Stop;
+import org.apache.sis.internal.gui.ColorName;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.gui.GUIUtilities;
+
+
+/**
+ * Represents a single color or a color ramp that can be represented in {@link CategoryColorsCell}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class CategoryColors {
+    /**
+     * Default color palette.
+     */
+    static final CategoryColors GRAYSCALE = new CategoryColors(0xFF000000, 0xFFFFFFFF),
+            BELL = new CategoryColors(0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFF0000);
+
+    /**
+     * ARGB codes of this single color or color ramp.
+     * If null or empty, then default to transparent.
+     */
+    final int[] colors;
+
+    /**
+     * The paint for this palette, created when first needed.
+     */
+    private transient Paint paint;
+
+    /**
+     * A name for this palette, computed when first needed.
+     *
+     * @see #toString()
+     */
+    private transient String name;
+
+    /**
+     * Creates a new palette for the given colors.
+     */
+    CategoryColors(final int... colors) {
+        this.colors = colors;
+    }
+
+    /**
+     * 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.
+     */
+    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);
+        }
+        colorRampChooser.getSelectionModel().select(i);
+    }
+
+    /**
+     * Returns the first color, or {@code null} if none.
+     * This is used for qualitative categories, which are expected to contain only one color.
+     */
+    final Color firstColor() {
+        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.
+     */
+    final Paint paint() {
+        if (paint == null) {
+            switch (colors.length) {
+                case 0: break;
+                case 1: {
+                    paint = GUIUtilities.fromARGB(colors[0]);
+                    break;
+                }
+                default: {
+                    final Stop[] stops = new Stop[colors.length];
+                    final double scale = 1d / (stops.length - 1);
+                    for (int i=0; i<stops.length; i++) {
+                        stops[i] = new Stop(scale*i, GUIUtilities.fromARGB(colors[i]));
+                    }
+                    paint = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
+                }
+            }
+        }
+        return paint;
+    }
+
+    /**
+     * Returns a string representation of this color palette.
+     * This string representation will appear in the combo box when that box is shown.
+     */
+    @Override
+    public String toString() {
+        if (name == null) {
+            final int n;
+            if (colors == null || (n = colors.length) == 0) {
+                name = Vocabulary.format(Vocabulary.Keys.Transparent);
+            } else if (equals(GRAYSCALE)) {
+                name = Vocabulary.format(Vocabulary.Keys.Grayscale);
+            } else {
+                name = ColorName.of(colors[0]);
+                if (n > 1) {
+                    final StringBuilder buffer = new StringBuilder(name);
+                    if (n > 2) {
+                        buffer.append(" … ").append(ColorName.of(colors[n / 2]));
+                    }
+                    name = buffer.append(" … ").append(ColorName.of(colors[n - 1])).toString();
+                }
+            }
+        }
+        return name;
+    }
+
+    /**
+     * Returns a hash code value for this palette.
+     * Defined mostly for consistency with {@link #equals(Object)}.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(colors) ^ 81;
+    }
+
+    /**
+     * Returns whether the given object is equal to this {@code CategoryColors}.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        return (other instanceof CategoryColors) && Arrays.equals(colors, ((CategoryColors) other).colors);
+    }
+}
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
new file mode 100644
index 0000000..bbae3e2
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
@@ -0,0 +1,364 @@
+/*
+ * 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.Node;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+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.ColorPicker;
+import javafx.scene.control.ContentDisplay;
+import javafx.geometry.Pos;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+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.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 TableCell<Category,CategoryColors> implements EventHandler<ActionEvent> {
+    /**
+     * Space (in pixels) to remove on right side of the rectangle representing colors.
+     */
+    private static final double WIDTH_ADJUST = -9;
+
+    /**
+     * Height (in pixels) of the rectangle representing colors.
+     */
+    private static final double HEIGHT = 16;
+
+    /**
+     * 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 single color, or {@code null} if not yet created.
+     * This applies to qualitative categories.
+     */
+    private ColorPicker colorPicker;
+
+    /**
+     * The control for selecting a color ramp, or {@code null} if not yet created.
+     * This applies to quantitative categories.
+     *
+     * @see Category#isQuantitative()
+     */
+    private ComboBox<CategoryColors> colorRampChooser;
+
+    /**
+     * The single color shown in the table. Created when first needed.
+     */
+    private Rectangle singleColor;
+
+    /**
+     * Colors to restore if user cancels an edit action.
+     */
+    private CategoryColors restoreOnCancel;
+
+    /**
+     * 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;
+        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+    }
+
+    /**
+     * Invoked when the color in this cell changed. It may be because of user selection in the combo box,
+     * or because this cell is now used for a new {@link Category} instance.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * this method should not invoke {@link #setGraphic(Node)} if the current graphic is a {@link ComboBoxBase}
+     * because this method may be invoked at any time, including during execution of {@link #startEdit()} or
+     * {@link #commitEdit(Object)}. Adding or removing {@link ColorPicker} or {@link ComboBox} 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>
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    protected void updateItem(final CategoryColors colors, final boolean empty) {
+        super.updateItem(colors, empty);
+        final Node control = getGraphic();
+        if (colors != null) {
+            if (control == null) {
+                setColorView(colors);
+            } else if (control instanceof Rectangle) {
+                ((Rectangle) control).setFill(colors.paint());
+            } else if (control instanceof ColorPicker) {
+                ((ColorPicker) control).setValue(colors.firstColor());
+            } else {
+                // A ClassCastException here would be a bug in CategoryColorsCell editors management.
+                colors.asSelectedItem(((ComboBox<CategoryColors>) control));
+            }
+        } else if (control instanceof Rectangle) {
+            setGraphic(null);
+        }
+    }
+
+    /**
+     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser} has the focus.
+     * This is used for assertions.
+     */
+    private boolean controlNotFocused() {
+        return (colorPicker == null || !colorPicker.isFocused()) &&
+                (colorRampChooser == null || !colorRampChooser.isFocused());
+    }
+
+    /**
+     * Sets the color representation when no editing is under way. It is caller responsibility to ensure
+     * that the current graphic is not a combo box, or that it is safe to remove that combo box from the
+     * scene (i.e. that combo box does not have focus anymore).
+     */
+    private void setColorView(final CategoryColors colors) {
+        assert controlNotFocused();
+        Rectangle view = null;
+        if (colors != null) {
+            final Paint paint = colors.paint();
+            if (paint != null) {
+                if (singleColor == null) {
+                    singleColor = createRectangle(WIDTH_ADJUST);
+                }
+                view = singleColor;
+                view.setFill(paint);
+            }
+        }
+        setGraphic(view);
+    }
+
+    /**
+     * Creates the graphic to draw in a table cell or combo box cell for representing a color or color ramp.
+     *
+     * @param  adjust  amount of space (in pixels) to add or remove on the right size.
+     *                 Should be a negative number for removing space.
+     */
+    private Rectangle createRectangle(final double adjust) {
+        final Rectangle gr = new Rectangle();
+        gr.setHeight(HEIGHT);
+        gr.widthProperty().bind(widthProperty().add(adjust));
+        return gr;
+    }
+
+    /**
+     * 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());
+            }
+        }
+    }
+
+    /**
+     * 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.
+     */
+    @Override
+    public void startEdit() {
+        restoreOnCancel = getItem();
+        final CategoryColors colors = (restoreOnCancel != null) ? restoreOnCancel : CategoryColors.GRAYSCALE;
+        final ComboBoxBase<?> control;
+        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);
+            control = colorRampChooser;
+        } else {
+            if (colorPicker == null) {
+                colorPicker = new ColorPicker();
+                addListeners(colorPicker);
+            }
+            colorPicker.setValue(colors.firstColor());
+            control = colorPicker;
+        }
+        /*
+         * 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();
+    }
+
+    /**
+     * Finishes configuration of a newly created combo box.
+     */
+    private void addListeners(final ComboBoxBase<?> control) {
+        control.setOnAction(this);
+        control.setOnHidden((e) -> hidden());
+    }
+
+    /**
+     * Invoked when a combo box has been hidden. This method sets the focus to the table before to remove
+     * the combo box. This is necessary for causing the combo box to lost focus, otherwise focus problems
+     * appear next time that the combo box is shown.
+     *
+     * <p>IF the cell was in editing mode when this method is invoked, it means that the user clicked outside
+     * the combo box area without validating his/her choice. In this case {@link #commitEdit(Object)} has not
+     * 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.
+            }
+            setItem(restoreOnCancel);
+            super.cancelEdit();
+        }
+        restoreOnCancel = null;
+        getTableView().requestFocus();  // Must be before `setGraphic(…)` for causing ColorPicker to lost focus.
+        setColorView(getItem());
+    }
+
+    /**
+     * 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.
+     */
+    @Override
+    public void cancelEdit() {
+        setItem(restoreOnCancel);
+        restoreOnCancel = null;
+        super.cancelEdit();
+        assert controlNotFocused();
+        setColorView(getItem());
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Invoked when the user selected a new value in the color picker or color ramp chooser.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handle(final ActionEvent event) {
+        if (isEditing()) {
+            final Object source = event.getSource();
+            final CategoryColors value;
+            if (source instanceof ColorPicker) {
+                final Color color = ((ColorPicker) source).getValue();
+                value = (color != null) ? new CategoryColors(GUIUtilities.toARGB(color)) : null;
+            } else {
+                // A ClassCastException here would be a bug in CategoryColorsCell editors management.
+                value = ((ComboBox<CategoryColors>) source).getValue();
+            }
+            commitEdit(value);
+        }
+    }
+
+    /**
+     * 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");
+
+        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/Controls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java
index 0bd74af..d3ff50d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/Controls.java
@@ -51,16 +51,10 @@ abstract class Controls {
     private static final Insets NEXT_CAPTION_MARGIN = new Insets(30, 0, 6, 0);
 
     /**
-     * Margin for adding an indentation to a node when the node is inside a group
-     * created by {@link Styles#createControlGrid(int, Label...)}.
+     * Same indentation as {@link Styles#FORM_INSETS}, but without the space on other sides.
+     * This is used when the node is outside a group created by {@link Styles#createControlGrid(int, Label...)}.
      */
-    static final Insets INDENT = new Insets(0, 0, 0, 15);
-
-    /**
-     * Margin for adding an indentation to a node when the node is outside a group
-     * created by {@link Styles#createControlGrid(int, Label...)}.
-     */
-    static final Insets INDENT_OUTSIDE = new Insets(0, 0, 0, 15 + Styles.FORM_INSETS.getLeft());
+    static final Insets CONTENT_MARGIN = new Insets(0, 0, 0, Styles.FORM_INSETS.getLeft());
 
     /**
      * The toolbar button for selecting this view.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index cfafb5d..802163c 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.function.Function;
 import java.awt.Graphics2D;
 import java.awt.Rectangle;
 import java.awt.RenderingHints;
@@ -54,6 +55,7 @@ import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.Interpolation;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.gui.map.MapCanvasAWT;
 import org.apache.sis.gui.map.RenderingMode;
@@ -306,6 +308,25 @@ public class CoverageCanvas extends MapCanvasAWT {
     }
 
     /**
+     * Returns the colors to use for given categories of sample values, or {@code null} is unspecified.
+     */
+    final Function<Category, java.awt.Color[]> getCategoryColors() {
+        return data.processor.getCategoryColors();
+    }
+
+    /**
+     * Sets the colors to use for given categories in image. Invoking this method causes a repaint event,
+     * so it should be invoked only if at least one color is known to have changed.
+     *
+     * @param  colors  colors to use for arbitrary categories of sample values, or {@code null} for default.
+     */
+    final void setCategoryColors(final Function<Category, java.awt.Color[]> colors) {
+        data.processor.setCategoryColors(colors);
+        resampledImage = null;
+        requestRepaint();
+    }
+
+    /**
      * Sets the background, as a color for now but more patterns may be allowed in a future version.
      */
     final void setBackground(final Color color) {
@@ -432,6 +453,8 @@ public class CoverageCanvas extends MapCanvasAWT {
 
     /**
      * Invoked when a new interpolation has been specified.
+     *
+     * @see #setInterpolation(Interpolation)
      */
     private void onInterpolationSpecified(final Interpolation newValue) {
         data.processor.setInterpolation(newValue);
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 69bf462..f07e984 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
@@ -16,11 +16,11 @@
  */
 package org.apache.sis.gui.coverage;
 
+import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.lang.ref.Reference;
 import javafx.scene.control.Accordion;
-import javafx.scene.control.ColorPicker;
 import javafx.scene.control.Control;
 import javafx.scene.control.TitledPane;
 import javafx.scene.layout.BorderPane;
@@ -30,15 +30,15 @@ import javafx.scene.layout.VBox;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
-import javafx.collections.ObservableList;
-import javafx.scene.Node;
 import javafx.scene.control.ChoiceBox;
 import javafx.scene.control.Label;
+import javafx.scene.control.TableView;
 import javafx.scene.control.Tooltip;
 import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
 import javafx.util.StringConverter;
 import org.apache.sis.storage.Resource;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.gui.map.MapMenu;
@@ -64,6 +64,11 @@ final class CoverageControls extends Controls {
     private final CoverageCanvas view;
 
     /**
+     * The control showing categories and their colors for the current coverage.
+     */
+    private final TableView<Category> categoryTable;
+
+    /**
      * The controls for changing {@link #view}.
      */
     private final Accordion controls;
@@ -84,9 +89,8 @@ final class CoverageControls extends Controls {
                      final RecentReferenceSystems referenceSystems)
     {
         final Resources resources = Resources.forLocale(vocabulary.getLocale());
-        final Color background = Color.BLACK;
         view = new CoverageCanvas(vocabulary.getLocale());
-        view.setBackground(background);
+        view.setBackground(Color.BLACK);
         final StatusBar statusBar = new StatusBar(referenceSystems, view);
         view.statusBar = statusBar;
         imageAndStatus = new BorderPane(view.getView());
@@ -97,64 +101,52 @@ final class CoverageControls extends Controls {
          * "Display" section with the following controls:
          *    - Current CRS
          *    - Interpolation
-         *    - Color stretching
-         *    - Background color
          */
         final VBox displayPane;
         {   // Block for making variables locale to this scope.
-            final Font  font     = fontOfGroup();
-            final Label crsLabel = new Label(vocabulary.getString(Vocabulary.Keys.ReferenceSystem));
-            final Label crsShown = new Label();
-            crsLabel.setLabelFor(crsShown);
-            crsLabel.setFont(font);
-            crsLabel.setPadding(Styles.FORM_INSETS);
-            crsShown.setPadding(INDENT_OUTSIDE);
-            crsShown.setTooltip(new Tooltip(resources.getString(Resources.Keys.SelectCrsByContextMenu)));
-            menu.selectedReferenceSystem().ifPresent((text) -> crsShown.textProperty().bind(text));
+            final Label crsControl = new Label();
+            final Label crsHeader  = labelOfGroup(vocabulary, Vocabulary.Keys.ReferenceSystem, crsControl, true);
+            crsControl.setPadding(CONTENT_MARGIN);
+            crsControl.setTooltip(new Tooltip(resources.getString(Resources.Keys.SelectCrsByContextMenu)));
+            menu.selectedReferenceSystem().ifPresent((text) -> crsControl.textProperty().bind(text));
             /*
-             * The pane containing controls will be divided in sections separated by labels:
-             * ones for values and one for colors.
+             * Creates a "Values" sub-section with the following controls:
+             *   - Interpolation
              */
-            final int valuesHeader = 0;
-            final int colorsHeader = 2;
-            final GridPane gp;
-            gp = Styles.createControlGrid(valuesHeader + 1,
-                label(vocabulary, Vocabulary.Keys.Interpolation, createInterpolationButton(vocabulary.getLocale())),
-                label(vocabulary, Vocabulary.Keys.Stretching, Stretching.createButton((p,o,n) -> view.setStyling(n))),
-                label(vocabulary, Vocabulary.Keys.Background, createBackgroundButton(background)));
+            final GridPane valuesControl = Styles.createControlGrid(0,
+                label(vocabulary, Vocabulary.Keys.Interpolation, createInterpolationButton(vocabulary.getLocale())));
+            final Label valuesHeader = labelOfGroup(vocabulary, Vocabulary.Keys.Values, valuesControl, false);
             /*
-             * Insert space (one row) between "interpolation" and "stretching"
-             * so we can insert the "colors" section header.
+             * All sections put together.
              */
-            final ObservableList<Node> items = gp.getChildren();
-            for (final Node item : items) {
-                if (GridPane.getColumnIndex(item) == 0) {
-                    ((Label) item).setPadding(INDENT);
-                }
-                final int row = GridPane.getRowIndex(item);
-                if (row >= colorsHeader) {
-                    GridPane.setRowIndex(item, row + 1);
-                }
-            }
-            final Label values = new Label(vocabulary.getString(Vocabulary.Keys.Values));
-            final Label colors = new Label(vocabulary.getString(Vocabulary.Keys.Colors));
-            values.setFont(font);
-            colors.setFont(font);
-            GridPane.setConstraints(values, 0, valuesHeader, 2, 1);    // Span 2 columns.
-            GridPane.setConstraints(colors, 0, colorsHeader, 2, 1);
-            items.addAll(values, colors);
-            displayPane = new VBox(crsLabel, crsShown, gp);
+            displayPane = new VBox(crsHeader, crsControl, valuesHeader, valuesControl);
+        }
+        /*
+         * "Colors" section with the following controls:
+         *    - Colors for each category
+         *    - Color stretching
+         */
+        final VBox colorsPane;
+        {   // Block for making variables locale to this scope.
+            final CoverageStyling styling = new CoverageStyling(view);
+            categoryTable = CategoryColorsCell.createTable(styling, vocabulary);
+            final GridPane gp = Styles.createControlGrid(0,
+                label(vocabulary, Vocabulary.Keys.Stretching, Stretching.createButton((p,o,n) -> view.setStyling(n))));
+
+            colorsPane = new VBox(
+                    labelOfGroup(vocabulary, Vocabulary.Keys.Categories, categoryTable, true), categoryTable, gp);
         }
         /*
          * Put all sections together and have the first one expanded by default.
          * The "Properties" section will be built by `PropertyPaneCreator` only if requested.
          */
-        final TitledPane p1 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Display), displayPane);
-        final TitledPane p2 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Properties), null);
-        controls = new Accordion(p1, p2);
+        final TitledPane p1 = new TitledPane(vocabulary.getString(Vocabulary.Keys.SpatialRepresentation), displayPane);
+        final TitledPane p2 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Colors), colorsPane);
+        final TitledPane p3 = new TitledPane(vocabulary.getString(Vocabulary.Keys.Properties), null);
+        controls = new Accordion(p1, p2, p3);
         controls.setExpandedPane(p1);
         view.coverageProperty.bind(coverage);
-        p2.expandedProperty().addListener(new PropertyPaneCreator(view, p2));
+        p3.expandedProperty().addListener(new PropertyPaneCreator(view, p3));
     }
 
     /**
@@ -223,17 +215,6 @@ final class CoverageControls extends Controls {
     }
 
     /**
-     * Creates the button for selecting a background color.
-     */
-    private ColorPicker createBackgroundButton(final Color background) {
-        final ColorPicker b = new ColorPicker(background);
-        b.setOnAction((e) -> {
-            view.setBackground(((ColorPicker) e.getSource()).getValue());
-        });
-        return b;
-    }
-
-    /**
      * Invoked the first time that the "Properties" pane is opened for building the JavaFX visual components.
      * We deffer the creation of this pane because it is often not requested at all, since this is more for
      * developers than users.
@@ -272,6 +253,13 @@ final class CoverageControls extends Controls {
     @Override
     final void coverageChanged(final GridCoverage data, final Reference<Resource> originator) {
         view.setOriginator(originator);
+        if (data != null) {
+            final int visibleBand = 0;          // TODO: provide a selector for the band to show.
+            final List<SampleDimension> bands = data.getSampleDimensions();
+            categoryTable.getItems().setAll(bands.get(visibleBand).getCategories());
+        } else {
+            categoryTable.getItems().clear();
+        }
     }
 
     /**
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
new file mode 100644
index 0000000..fca46ed
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -0,0 +1,158 @@
+/*
+ * 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 java.awt.Color;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.function.Function;
+import javafx.util.Callback;
+import javafx.scene.control.TableColumn;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.property.SimpleObjectProperty;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.internal.coverage.j2d.Colorizer;
+
+
+/**
+ * Colors to apply on coverages based on their {@link Category} instances.
+ *
+ * <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 CoverageStyling implements Function<Category,Color[]>,
+        Callback<TableColumn.CellDataFeatures<Category,CategoryColors>, ObservableValue<CategoryColors>>
+{
+    /**
+     * Customized colors selected by user. Keys are English names of categories.
+     *
+     * @see #key(Category)
+     */
+    private final Map<String,int[]> customizedColors;
+
+    /**
+     * The fallback to use if no color is defined in this {@code CoverageStyling} for a category.
+     */
+    private final Function<Category,Color[]> fallback;
+
+    /**
+     * The view to notify when a color changed, or {@code null} if none.
+     */
+    private final CoverageCanvas canvas;
+
+    /**
+     * Creates a new styling instance.
+     */
+    CoverageStyling(final CoverageCanvas canvas) {
+        customizedColors = new HashMap<>();
+        this.canvas = canvas;
+        if (canvas != null) {
+            final Function<Category, Color[]> c = canvas.getCategoryColors();
+            if (c != null) {
+                fallback = c;
+                return;
+            }
+        }
+        fallback = Colorizer.GRAYSCALE;
+    }
+
+    /**
+     * Returns the key to use in {@link #customizedColors} for the given category.
+     */
+    private static String key(final Category category) {
+        return category.getName().toString(Locale.ENGLISH);
+    }
+
+    /**
+     * Associates colors to the given category.
+     */
+    final void setARGB(final Category category, final int[] colors) {
+        final String key = key(category);
+        final int[] old;
+        if (colors != null && colors.length != 0) {
+            old = customizedColors.put(key, colors);
+        } else {
+            old = customizedColors.remove(key);
+        }
+        if (canvas != null && !Arrays.equals(colors, old)) {
+            canvas.setCategoryColors(this);                     // Causes a repaint event.
+        }
+    }
+
+    /**
+     * 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>
+     */
+    private int[] getARGB(final Category category) {
+        int[] ARGB = customizedColors.get(key(category));
+        if (ARGB == null) {
+            final Color[] colors = fallback.apply(category);
+            if (colors != null) {
+                ARGB = new int[colors.length];
+                for (int i=0; i<colors.length; i++) {
+                    ARGB[i] = colors[i].getRGB();
+                }
+            }
+        }
+        return ARGB;
+    }
+
+    /**
+     * Returns the colors to apply for the given category, or {@code null} for transparent.
+     * This method returns copies of internal arrays; changes to the returned array do not
+     * affect this {@code CoverageStyling} (assuming {@link #fallback} also does copies).
+     *
+     * @param  category  the category for which to get the colors.
+     * @return colors to apply for the given category, or {@code null}.
+     */
+    @Override
+    public Color[] apply(final Category category) {
+        final int[] ARGB = customizedColors.get(key(category));
+        if (ARGB != null) {
+            final Color[] colors = new Color[ARGB.length];
+            for (int i=0; i<colors.length; i++) {
+                colors[i] = new Color(ARGB[i], true);
+            }
+            return colors;
+        }
+        return fallback.apply(category);
+    }
+
+    /**
+     * 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.
+     */
+    @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;
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ColorName.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ColorName.java
new file mode 100644
index 0000000..4647fd7
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ColorName.java
@@ -0,0 +1,87 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import javafx.scene.paint.Color;
+
+
+/**
+ * Provides a name for a given {@link Color} instance.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class ColorName {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private ColorName() {
+    }
+
+    /**
+     * The color names.
+     */
+    private static final Map<Color,String> NAMES = new HashMap<>(175);
+    static {
+        final StringBuilder buffer = new StringBuilder();
+        for (final Field field : Color.class.getFields()) {
+            if (Modifier.isStatic(field.getModifiers()) && Color.class.equals(field.getType())) try {
+                final String name = field.getName();
+                buffer.append(name.toLowerCase());          // Default locale is okay here.
+                buffer.setCharAt(0, name.charAt(0));        // Code point not used in Color API.
+                NAMES.put((Color) field.get(null), buffer.toString());
+                buffer.setLength(0);
+            } catch (Exception e) {
+                // Ignore. The map is only informative.
+            }
+        }
+    }
+
+    /**
+     * Returns the name of given color.
+     *
+     * @param  color  color for which to get a name.
+     * @return name of given color, or hexadecimal code if the given code does not have a known name.
+     */
+    public static String of(final Color color) {
+        String name = NAMES.get(color);
+        if (name == null) {
+            name = Integer.toHexString(GUIUtilities.toARGB(color));
+        }
+        return name;
+    }
+
+    /**
+     * Returns the name of given ARGB code.
+     *
+     * @param  color  color for which to get a name.
+     * @return name of given color, or hexadecimal code if the given code does not have a known name.
+     */
+    public static String of(final int color) {
+        String name = NAMES.get(GUIUtilities.fromARGB(color));
+        if (name == null) {
+            name = Integer.toHexString(color);
+        }
+        return name;
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
index eb346ef..3d8bfe9 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
@@ -28,6 +28,7 @@ import javafx.scene.control.ContextMenu;
 import javafx.scene.control.MenuItem;
 import javafx.scene.shape.Rectangle;
 import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
 import javafx.stage.Window;
 import javax.measure.Unit;
 import javax.measure.Quantity;
@@ -299,4 +300,36 @@ public final class GUIUtilities extends Static {
         m = Units.METRE.getConverterTo(unit).convert(Math.max(m, Formulas.LINEAR_TOLERANCE));
         return Quantities.create(m, unit);
     }
+
+    /**
+     * Returns a color from a ARGB value packed in an integer.
+     *
+     * @param  code  the ARGB value.
+     * @return color for the given ARGB value.
+     */
+    public static Color fromARGB(final int code) {
+        return Color.rgb(0xFF & (code >>> Byte.SIZE*2),     // Red
+                         0xFF & (code >>> Byte.SIZE),       // Green
+                         0xFF & (code));                    // Blue
+    }
+
+    /**
+     * Returns a ARGB value packed in an integer.
+     *
+     * @param  color  color for which to get the ARGB value.
+     * @return ARGB value for the given color.
+     */
+    public static int toARGB(final Color color) {
+        return (toByte(color.getOpacity()) << 3*Byte.SIZE)
+             | (toByte(color.getRed())     << 2*Byte.SIZE)
+             | (toByte(color.getGreen())   <<   Byte.SIZE)
+             |  toByte(color.getBlue());
+    }
+
+    /**
+     * Converts a floating point value in the 0 … 1 range to an integer value in the 0 … 255 range.
+     */
+    private static int toByte(final double value) {
+        return (int) Math.round(value * 255);
+    }
 }
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
new file mode 100644
index 0000000..a66da1d
--- /dev/null
+++ b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
@@ -0,0 +1,83 @@
+/*
+ * 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 java.util.Locale;
+import javafx.stage.Stage;
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.BorderPane;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.measure.Units;
+
+
+/**
+ * Shows category table built by {@link CoverageStyling} with arbitrary data.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class CoverageStylingApp extends Application {
+    /**
+     * Starts the test application.
+     *
+     * @param args  ignored.
+     */
+    public static void main(final String[] args) {
+        launch(args);
+    }
+
+    /**
+     * Creates and starts the test application.
+     *
+     * @param  window  where to show the application.
+     */
+    @Override
+    public void start(final Stage window) {
+        final BorderPane pane = new BorderPane();
+        pane.setCenter(createCategoryTable());
+        window.setTitle("BandColorsTable Test");
+        window.setScene(new Scene(pane));
+        window.setWidth (400);
+        window.setHeight(300);
+        window.show();
+    }
+
+    /**
+     * Creates a table with arbitrary categories to show.
+     */
+    private static TableView<Category> createCategoryTable() {
+        final SampleDimension band = new SampleDimension.Builder()
+                .addQualitative("Background", 0)
+                .addQualitative("Cloud",      1)
+                .addQualitative("Land",       2)
+                .addQuantitative("Temperature", 5, 255, 0.15, -5, Units.CELSIUS)
+                .setName("Sea Surface Temperature")
+                .build();
+
+        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));
+        table.getItems().setAll(band.getCategories());
+        return table;
+    }
+}
diff --git a/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java b/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
index 11c14d7..61b4219 100644
--- a/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
+++ b/application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.gui;
 
 import java.util.Arrays;
 import java.util.List;
+import javafx.scene.paint.Color;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -42,4 +43,30 @@ public final strictfp class GUIUtilitiesTest extends TestCase {
         final List<Integer> y = Arrays.asList(1, 2,    3, 7, 8);
         assertEquals(Arrays.asList(1, 2, 7), GUIUtilities.longestCommonSubsequence(x, y));
     }
+
+    /**
+     * Tests {@link GUIUtilities#fromARGB(int)}.
+     */
+    @Test
+    public void testFromARGB() {
+        final java.awt.Color reference = java.awt.Color.ORANGE;
+        final Color color = GUIUtilities.fromARGB(reference.getRGB());
+        assertEquals(reference.getRed(),   StrictMath.round(255 * color.getRed()));
+        assertEquals(reference.getGreen(), StrictMath.round(255 * color.getGreen()));
+        assertEquals(reference.getBlue(),  StrictMath.round(255 * color.getBlue()));
+        assertEquals(reference.getAlpha(), StrictMath.round(255 * color.getOpacity()));
+   }
+
+    /**
+     * Tests {@link GUIUtilities#toARGB(Color)}.
+     */
+    @Test
+    public void testToARGB() {
+        final int ARGB = GUIUtilities.toARGB(Color.ORANGE);
+        final java.awt.Color reference = new java.awt.Color(ARGB);
+        assertEquals(0xFF, reference.getRed());
+        assertEquals(0xA5, reference.getGreen());
+        assertEquals(0x00, reference.getBlue());
+        assertEquals(0xFF, reference.getAlpha());
+    }
 }
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 1465229..8b43adc 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
@@ -160,6 +160,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Cardinality = 20;
 
         /**
+         * Categories
+         */
+        public static final short Categories = 248;
+
+        /**
          * Caused by {0}
          */
         public static final short CausedBy_1 = 21;
@@ -560,6 +565,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Gray = 95;
 
         /**
+         * Grayscale
+         */
+        public static final short Grayscale = 250;
+
+        /**
          * Green
          */
         public static final short Green = 96;
@@ -1160,6 +1170,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Transparency = 201;
 
         /**
+         * Transparent
+         */
+        public static final short Transparent = 249;
+
+        /**
          * Truncated Julian
          */
         public static final short TruncatedJulian = 202;
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 e3440eb..10cf9a5 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
@@ -34,6 +34,7 @@ Bilinear                = Bilinear
 Black                   = Black
 Blue                    = Blue
 Cardinality             = Cardinality
+Categories              = Categories
 CausedBy_1              = Caused by {0}
 Cells                   = Cells
 CellCount_1             = {0} cells
@@ -115,6 +116,7 @@ Geographic              = Geographic
 GeographicExtent        = Geographic extent
 GeographicIdentifier    = Geographic identifier
 Gray                    = Gray
+Grayscale               = Grayscale
 Green                   = Green
 GridExtent              = Grid extent
 Height                  = Height
@@ -235,6 +237,7 @@ Trace                   = Trace
 Transformation          = Transformation
 TransformationAccuracy  = Transformation accuracy
 Transparency            = Transparency
+Transparent             = Transparent
 TruncatedJulian         = Truncated Julian
 Type                    = Type
 TypeOfResource          = Type of resource
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 c3c7670..409c3d5 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
@@ -41,6 +41,7 @@ Bilinear                = Bilin\u00e9aire
 Black                   = Noir
 Blue                    = Bleu
 Cardinality             = Cardinalit\u00e9
+Categories              = Cat\u00e9gories
 CausedBy_1              = Caus\u00e9e par {0}
 Cells                   = Cellules
 CellCount_1             = {0} cellules
@@ -122,6 +123,7 @@ Geographic              = G\u00e9ographique
 GeographicExtent        = \u00c9tendue g\u00e9ographique
 GeographicIdentifier    = Identifiant g\u00e9ographique
 Gray                    = Gris
+Grayscale               = Niveaux de gris
 Green                   = Vert
 GridExtent              = \u00c9tendue de la grille
 Height                  = Hauteur
@@ -242,6 +244,7 @@ Trace                   = Trace
 Transformation          = Transformation
 TransformationAccuracy  = Pr\u00e9cision de la transformation
 Transparency            = Transparence
+Transparent             = Transparent
 TruncatedJulian         = Julien tronqu\u00e9
 Type                    = Type
 TypeOfResource          = Type de ressource


Mime
View raw message