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 <[email protected]> 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
