You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/01/12 23:22:14 UTC
[sis] branch geoapi-4.0 updated: Add isoline rendering capabilities
to `CoverageCanvas` using the information provided by user in a
`TableView`.
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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 38499ac Add isoline rendering capabilities to `CoverageCanvas` using the information provided by user in a `TableView`.
38499ac is described below
commit 38499ac799c0ad8e71b2f19b46af2a726e38f400
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jan 13 00:06:05 2021 +0100
Add isoline rendering capabilities to `CoverageCanvas` using the information provided by user in a `TableView`.
---
.../apache/sis/gui/coverage/CoverageCanvas.java | 68 +++-
.../apache/sis/gui/coverage/CoverageControls.java | 13 +-
.../apache/sis/gui/coverage/IsolineRenderer.java | 451 +++++++++++++++++++++
.../org/apache/sis/gui/coverage/RenderingData.java | 16 +
.../java/org/apache/sis/gui/map/MapCanvas.java | 23 +-
.../apache/sis/internal/gui/control/ColorRamp.java | 17 +
.../sis/internal/gui/control/ValueColorMapper.java | 69 +++-
.../sis/internal/feature/j2d/EmptyShape.java | 64 +++
.../internal/processing/image/IsolineTracer.java | 4 +-
.../sis/internal/processing/image/Isolines.java | 1 +
10 files changed, 690 insertions(+), 36 deletions(-)
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 3ccff0c..66dd5c0 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
@@ -26,6 +26,8 @@ import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.RenderedImage;
+import java.awt.Stroke;
+import java.awt.BasicStroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.lang.ref.Reference;
@@ -163,7 +165,7 @@ public class CoverageCanvas extends MapCanvasAWT {
*
* @see #createPropertyExplorer()
*/
- private ImagePropertyExplorer imageProperty;
+ private ImagePropertyExplorer propertyExplorer;
/**
* The status bar associated to this {@code MapCanvas}.
@@ -180,6 +182,14 @@ public class CoverageCanvas extends MapCanvasAWT {
private Reference<Resource> originator;
/**
+ * Renderer of isolines, or {@code null} if none. The presence of this field in this class may be temporary.
+ * A future version may replace this field by a more complete styling framework. Note that this class holds
+ * references to {@link javafx.scene.control.TableView} list of items, which are the list of isoline levels
+ * with their colors.
+ */
+ IsolineRenderer isolines;
+
+ /**
* Creates a new two-dimensional canvas for {@link RenderedImage}.
*/
public CoverageCanvas() {
@@ -211,9 +221,9 @@ public class CoverageCanvas extends MapCanvasAWT {
* is being shown.
*/
final ImagePropertyExplorer createPropertyExplorer() {
- imageProperty = new ImagePropertyExplorer(getLocale(), fixedPane.backgroundProperty());
- imageProperty.setImage(resampledImage, getVisibleImageBounds());
- return imageProperty;
+ propertyExplorer = new ImagePropertyExplorer(getLocale(), fixedPane.backgroundProperty());
+ propertyExplorer.setImage(resampledImage, getVisibleImageBounds());
+ return propertyExplorer;
}
/**
@@ -442,12 +452,16 @@ public class CoverageCanvas extends MapCanvasAWT {
* without resampling and without color ramp stretching. The call to this method is followed by a
* a repaint event, which will cause the image to be resampled in a background thread.
*
- * <p>All arguments can be {@code null} for clearing the canvas.</p>
+ * <p>All arguments can be {@code null} for clearing the canvas.
+ * This method is invoked in JavaFX thread.</p>
*/
private void setRawImage(final RenderedImage image, final GridGeometry domain, final List<SampleDimension> ranges) {
if (TRACE) {
trace(".setRawImage(%s)", image);
}
+ if (isolines != null) {
+ isolines.clear();
+ }
resampledImage = null;
derivedImages.clear();
data.setImage(image, domain, ranges);
@@ -564,7 +578,12 @@ public class CoverageCanvas extends MapCanvasAWT {
private final Reference<Resource> originator;
/**
- * Creates a new renderer.
+ * Snapshot of information required for rendering isolines, or {@code null} if none.
+ */
+ private IsolineRenderer.Snapshot[] isolines;
+
+ /**
+ * Creates a new renderer. Shall be invoked in JavaFX thread.
*/
Worker(final CoverageCanvas canvas) {
originator = canvas.originator;
@@ -577,6 +596,9 @@ public class CoverageCanvas extends MapCanvasAWT {
if (data.validateCRS(objectiveCRS)) {
resampledImage = canvas.resampledImage;
}
+ if (canvas.isolines != null) {
+ isolines = canvas.isolines.prepare();
+ }
}
/**
@@ -642,6 +664,12 @@ public class CoverageCanvas extends MapCanvasAWT {
}
}
prefetchedImage = data.prefetch(resampledImage, resampledToDisplay, displayBounds);
+ /*
+ * Create isolines if requested.
+ */
+ if (isolines != null) {
+ data.complete(isolines);
+ }
} finally {
LogHandler.loadingStop(id);
}
@@ -656,6 +684,17 @@ public class CoverageCanvas extends MapCanvasAWT {
protected void paint(final Graphics2D gr) {
gr.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
gr.drawRenderedImage(prefetchedImage, resampledToDisplay);
+ if (isolines != null) {
+ AffineTransform at = gr.getTransform();
+ final Stroke st = gr.getStroke();
+ gr.setStroke(new BasicStroke(0));
+ gr.transform((AffineTransform) objectiveToDisplay); // This cast is safe in PlanarCanvas subclass.
+ for (final IsolineRenderer.Snapshot s : isolines) {
+ s.paint(gr);
+ }
+ gr.setTransform(at);
+ gr.setStroke(st);
+ }
}
/**
@@ -665,6 +704,11 @@ public class CoverageCanvas extends MapCanvasAWT {
@Override
protected boolean commit(final MapCanvas canvas) {
((CoverageCanvas) canvas).cacheRenderingData(this);
+ if (isolines != null) {
+ for (final IsolineRenderer.Snapshot s : isolines) {
+ s.commit();
+ }
+ }
return super.commit(canvas);
}
}
@@ -684,16 +728,20 @@ public class CoverageCanvas extends MapCanvasAWT {
trace(": New objective bounds:%n%s", this);
}
/*
- * Notify the "Image properties" tab that the image changed. The `imageProperty` field is non-null
+ * Notify the "Image properties" tab that the image changed. The `propertyExplorer` field is non-null
* only if the "Properties" section in `CoverageControls` has been shown at least once.
*/
- if (imageProperty != null) {
- imageProperty.setImage(resampledImage, worker.getVisibleImageBounds());
+ if (propertyExplorer != null) {
+ propertyExplorer.setImage(resampledImage, worker.getVisibleImageBounds());
if (TRACE) {
trace(": Update image property view with visible area %s.",
- imageProperty.getVisibleImageBounds(resampledImage));
+ propertyExplorer.getVisibleImageBounds(resampledImage));
}
}
+ /*
+ * Adjust the accuracy of coordinates shown in the status bar.
+ * The number of fraction digits depend on the zoom factor.
+ */
if (statusBar != null) {
final Object value = resampledImage.getProperty(PlanarImage.POSITIONAL_ACCURACY_KEY);
Quantity<Length> accuracy = null;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index bdc1a43..949193a 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
@@ -70,6 +70,11 @@ final class CoverageControls extends Controls {
private final TableView<Category> categoryTable;
/**
+ * The renderer of isolines.
+ */
+ private final IsolineRenderer isolines;
+
+ /**
* The controls for changing {@link #view}.
*/
private final Accordion controls;
@@ -144,9 +149,11 @@ final class CoverageControls extends Controls {
*/
final VBox isolinesPane;
{ // Block for making variables locale to this scope.
- final ValueColorMapper table = new ValueColorMapper();
- final TableView<ValueColorMapper.Step> isolinesTable = table.createIsolineTable(vocabulary);
- isolinesPane = new VBox(isolinesTable); // TODO: add band selector
+ final ValueColorMapper mapper = new ValueColorMapper();
+ final TableView<ValueColorMapper.Step> table = mapper.createIsolineTable(vocabulary);
+ isolines = new IsolineRenderer(view);
+ isolines.setIsolineTables(java.util.Collections.singletonList(table));
+ isolinesPane = new VBox(table); // TODO: add band selector
}
/*
* Put all sections together and have the first one expanded by default.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
new file mode 100644
index 0000000..2646c6f
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
@@ -0,0 +1,451 @@
+/*
+ * 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.Map;
+import java.util.Set;
+import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Arrays;
+import java.awt.Shape;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.RenderedImage;
+import javafx.application.Platform;
+import javafx.scene.control.TableView;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.collections.ListChangeListener;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.gui.control.ColorRamp;
+import org.apache.sis.internal.processing.image.Isolines;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.gui.control.ValueColorMapper.Step;
+import org.apache.sis.internal.feature.j2d.EmptyShape;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * Caches and draws isoline shapes in a {@link CoverageCanvas}. This class is designed for interactive use
+ * in JavaFX widget; this is not a class for doing symbology e.g. in a web service. Most of the work done
+ * by {@code IsolineRenderer} is about listening to changes in {@link TableView}, managing data exchanges
+ * between JavaFX thread and background thread, and computes only isolines that are new compared to previous
+ * rendering.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class IsolineRenderer {
+ /**
+ * The canvas where isolines are drawn.
+ */
+ private final CoverageCanvas canvas;
+
+ /**
+ * The list of isoline values and associated shapes for each band, or {@code null} if none.
+ */
+ private Band[] bands;
+
+ /**
+ * Creates an initially empty set of isolines.
+ *
+ * @param canvas the canvas where isolines are drawn.
+ */
+ public IsolineRenderer(final CoverageCanvas canvas) {
+ if (canvas.isolines != null) {
+ throw new IllegalArgumentException();
+ }
+ this.canvas = canvas;
+ canvas.isolines = this;
+ }
+
+ /**
+ * Returns {@code true} if there is no isoline to show.
+ * This method shall be invoked in JavaFX thread.
+ */
+ private boolean isEmpty() {
+ if (bands != null) {
+ for (final Band band : bands) {
+ for (final Step level : band.steps) {
+ if (level.visible.get()) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Clears the cache. This method shall be invoked when the image used for computing isolines has changed,
+ * or when the {@code gridToCRS} transform has changed. This method shall be invoked in JavaFX thread.
+ */
+ final void clear() {
+ assert Platform.isFxApplicationThread();
+ if (bands != null) {
+ for (final Band band : bands) {
+ band.clear();
+ }
+ }
+ }
+
+ /**
+ * Sets the isoline values for all bands from the content of tables edited by user.
+ * This method registers listener on the given tables for repainting the isolines
+ * when table content changed.
+ */
+ public void setIsolineTables(final List<TableView<Step>> tables) {
+ if (bands != null) {
+ for (final Band band : bands) {
+ band.dispose();
+ }
+ }
+ bands = null;
+ if (tables != null && !tables.isEmpty()) {
+ bands = new Band[tables.size()];
+ for (int i=0; i<bands.length; i++) {
+ bands[i] = new Band(tables.get(i).getItems());
+ }
+ }
+ }
+
+ /**
+ * List of isoline values and associated shapes for a single band.
+ */
+ private final class Band implements ListChangeListener<Step>, ChangeListener<Object> {
+ /**
+ * The isoline levels to draw, together with their color.
+ * Considered as read-only (in JavaFX thread) by this class.
+ */
+ final ObservableList<Step> steps;
+
+ /**
+ * Cache of Java2D shapes for each isoline level. Shall be read in written in JavaFX thread.
+ * New isolines are computed in background thread and results stored later in JavaFX thread.
+ *
+ * @see Snapshot#isolines
+ */
+ private Map<Double,Shape> isolines;
+
+ /**
+ * Creates an initially empty set of isolines.
+ *
+ * @param steps the list of isoline levels to render.
+ */
+ @SuppressWarnings("ThisEscapedInObjectConstruction")
+ Band(final ObservableList<Step> steps) {
+ this.steps = steps;
+ addListeners(steps);
+ steps.addListener(this);
+ }
+
+ /**
+ * Clears the cache. This method shall be invoked when the image used for computing isolines has changed,
+ * or when the {@code gridToCRS} transform has changed. This method shall be invoked in JavaFX thread.
+ *
+ * @see IsolineRenderer#clear()
+ */
+ final void clear() {
+ // Force new instance instead of `Map.clear()` because previous instance may be used by `Snapshot`.
+ isolines = null;
+ }
+
+ /**
+ * Unregisters all listeners. This method should be invoked before this {@code Band} instance is discarded.
+ */
+ final void dispose() {
+ clear();
+ removeListeners(steps);
+ }
+
+ /**
+ * Invoked when steps are added or removed from the observable list.
+ * This method is public as an implementation side-effect and should not be invoked directly.
+ *
+ * @param change set of steps which have been added or removed.
+ */
+ @Override
+ public void onChanged(final Change<? extends Step> change) {
+ while (change.next()) {
+ if (!change.wasPermutated() && !change.wasUpdated()) {
+ removeListeners(change.getRemoved());
+ addListeners(change.getAddedSubList());
+ }
+ }
+ canvas.requestRepaint();
+ }
+
+ /**
+ * Unregisters listeners form all properties in the given list.
+ */
+ private void removeListeners(final List<? extends Step> list) {
+ for (final Step level : list) {
+ level.value .removeListener(this);
+ level.color .removeListener(this);
+ level.visible.removeListener(this);
+ }
+ }
+
+ /**
+ * Registers listeners on all properties in the given list.
+ */
+ private void addListeners(final List<? extends Step> list) {
+ for (final Step level : list) {
+ level.value .addListener(this);
+ level.color .addListener(this);
+ level.visible.addListener(this);
+ }
+ }
+
+ /**
+ * Invoked when an isoline value, color or visibility has changed.
+ * This method is public as an implementation side-effect and should not be invoked directly.
+ *
+ * @param property one of the properties defined in the {@link Step} class.
+ * @param oldValue the old property value, or {@code null} if none.
+ * @param newValue the new property value, or {@code null} if none.
+ */
+ @Override
+ public void changed(final ObservableValue<?> property, final Object oldValue, final Object newValue) {
+ canvas.requestRepaint();
+ }
+
+ /**
+ * Creates a snapshot of the list of isolines to draw, together with lists of cached shapes
+ * and shapes that need to be computed. This snapshot is created in JavaFX thread and used
+ * in a background thread.
+ *
+ * @param keep an empty set used by this method for listing the levels to keep in the cache.
+ * @return a snapshot of current {@code Band} state for use by a background thread.
+ *
+ * @see IsolineRenderer#prepare()
+ */
+ final Snapshot prepare(final Set<Double> keep) {
+ if (isolines == null) {
+ isolines = new HashMap<>();
+ }
+ final Snapshot s = new Snapshot(isolines, steps.size());
+ for (final Step level : steps) {
+ final Double value = level.value.get();
+ if (!value.isNaN()) {
+ keep.add(value);
+ if (level.visible.get()) {
+ final ColorRamp cr = level.color.get();
+ if (cr != null && !cr.isTransparent()) {
+ s.add(value, cr.colors[0]);
+ }
+ }
+ }
+ }
+ isolines.keySet().retainAll(keep); // Discard shapes that are no longer in use.
+ keep.clear();
+ return s;
+ }
+ }
+
+ /**
+ * Prepares a list of isolines to draw for all bands, initially populated with shapes that are already available.
+ * This method shall be invoked in JavaFX thread for having consistent information. The snapshots returned by this
+ * method will be completed and used in a background thread.
+ *
+ * @return snapshots of information about isolines in all bands, or {@code null} if none.
+ */
+ final Snapshot[] prepare() {
+ assert Platform.isFxApplicationThread();
+ if (isEmpty()) {
+ return null;
+ }
+ final Snapshot[] snapshots = new Snapshot[bands.length];
+ final Set<Double> keep = new HashSet<>();
+ for (int i=0; i < snapshots.length; i++) {
+ snapshots[i] = bands[i].prepare(keep);
+ }
+ return snapshots;
+ }
+
+ /**
+ * Continues isoline preparation by computing the missing Java2D shapes.
+ * This method shall be invoked in a background thread. After this call,
+ * isolines can be painted with {@link Snapshot#paint(Graphics2D)}.
+ *
+ * @param snapshots value of {@link #prepare()}. Shall not be {@code null}.
+ * @param data the source of data. Used only if there is new isolines to compute.
+ * @param gridToCRS transform from pixel coordinates to geometry coordinates, or {@code null} if none.
+ * Integer source coordinates are located at pixel centers.
+ * @return the {@code snapshots} array, potentially with less elements.
+ * @throws TransformException if an interpolated point can not be transformed using the given transform.
+ */
+ static Snapshot[] complete(final Snapshot[] snapshots, final RenderedImage data, final MathTransform gridToCRS)
+ throws TransformException
+ {
+ assert !Platform.isFxApplicationThread();
+ double[][] levels = null;
+ final int numBands = ImageUtilities.getNumBands(data);
+ final int numViews = Math.min(numBands, snapshots.length);
+ for (int i=0; i<numViews; i++) {
+ final Snapshot s = snapshots[i];
+ int n = s.missingLevels.size();
+ if (n != 0) {
+ if (levels == null) {
+ levels = new double[numBands][];
+ Arrays.fill(levels, ArraysExt.EMPTY_DOUBLE);
+ }
+ final double[] values = new double[n];
+ for (final Double value : s.missingLevels.keySet()) {
+ values[--n] = value;
+ }
+ assert n == 0 : n;
+ levels[i] = values;
+ }
+ }
+ /*
+ * Compute only the isolines that are not already available. For performance reasons, we need to process
+ * all bands in one single call to `Isolines.generate(…)`. Results are written in empty slots of `shapes`.
+ */
+ if (levels != null) {
+ final Isolines[] isolines = Isolines.generate(data, levels, gridToCRS);
+ for (int i=0; i<numViews; i++) {
+ snapshots[i].complete(isolines[i]);
+ }
+ }
+ return ArraysExt.resize(snapshots, numViews);
+ }
+
+ /**
+ * Snapshot of {@link Band} information before rendering. Life cycle is:
+ *
+ * <ol>
+ * <li>Created in JavaFX thread with shapes already available.</li>
+ * <li>Missing Java2D shapes completed in a background thread.</li>
+ * <li>Painting done in background thread.</li>
+ * <li>New shapes cached in JavaFX thread.</li>
+ * </ol>
+ */
+ static final class Snapshot {
+ /**
+ * Isolines available before snapshot, and where to store new isolines after completion.
+ * This map shall be read and written in JavaFX thread only. This is initially the same
+ * reference than {@link Band#isolines}, but may become different if {@link Band#clear()}
+ * is invoked while isolines computation or painting is in progress.
+ *
+ * @see Band#isolines
+ */
+ private final Map<Double,Shape> isolines;
+
+ /**
+ * The isoline levels that are missing in the {@link #isolines} map. This map is populated
+ * in JavaFX thread and consumed in the background thread.
+ */
+ private final Map<Double,Integer> missingLevels;
+
+ /**
+ * Isolines that have been created, or {@code null} if none. They are the shapes to store in
+ * the {@link #isolines} map after the isoline painting is completed. Stored in a separated map
+ * because we must wait to be back to JavaFX thread before we can write in {@link #isolines}.
+ */
+ private Map<Double,Shape> newIsolines;
+
+ /**
+ * The isoline shapes to draw. May contain {@code null} elements if some shapes are missing.
+ * Those missing shapes will be computed in a background thread.
+ */
+ private final Shape[] shapes;
+
+ /**
+ * Shape colors, obtained in JavaFX thread.
+ */
+ private final int[] colors;
+
+ /**
+ * Number of valid elements in {@link #shapes} and {@link #colors} array.
+ */
+ private int count;
+
+ /**
+ * Creates a new snapshot of {@link Band} information.
+ *
+ * @param isolines value of {@link Band#isolines} reference.
+ * @param capacity maximal amount of isoline levels that can be {@linkplain #add added}.
+ */
+ private Snapshot(final Map<Double,Shape> isolines, final int capacity) {
+ this.isolines = isolines;
+ missingLevels = new HashMap<>();
+ shapes = new Shape[capacity];
+ colors = new int[capacity];
+ }
+
+ /**
+ * Adds an isoline level. This method shall be invoked in JavaFX thread.
+ *
+ * @param value the level value.
+ * @param color color of the isolines to draw for the specified value.
+ */
+ private void add(final Double value, final int color) {
+ final Shape shape = isolines.putIfAbsent(value, EmptyShape.INSTANCE);
+ if (shape == null) {
+ missingLevels.put(value, count);
+ }
+ shapes[count] = shape;
+ colors[count] = color;
+ count++;
+ }
+
+ /**
+ * Completes the {@link #shapes} array by assigning a shapes to null elements.
+ *
+ * @param isolines missing isolines computed for the band of this snapshot.
+ */
+ private void complete(final Isolines isolines) {
+ newIsolines = isolines.polylines();
+ for (final Map.Entry<Double,Shape> entry : newIsolines.entrySet()) {
+ final Integer j = missingLevels.get(entry.getKey());
+ if (j != null) shapes[j] = entry.getValue();
+ }
+ }
+
+ /**
+ * Paints all isolines in the given graphics.
+ * This method should be invoked in a background thread.
+ *
+ * @param target where to draw isolines.
+ */
+ final void paint(final Graphics2D target) {
+ for (int i=0; i<count; i++) {
+ final Shape shape = shapes[i];
+ if (shape != null) {
+ target.setColor(new Color(colors[i], true));
+ target.draw(shape);
+ }
+ }
+ }
+
+ /**
+ * Invoked in JavaFX thread after successful rendering for caching the new isolines.
+ */
+ final void commit() {
+ assert Platform.isFxApplicationThread();
+ if (newIsolines != null) {
+ isolines.putAll(newIsolines);
+ }
+ }
+ }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index 0078220..9eb2252 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -37,6 +37,7 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.PixelTranslation;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.geometry.AbstractEnvelope;
import org.apache.sis.geometry.Envelope2D;
@@ -534,6 +535,21 @@ final class RenderingData implements Cloneable {
}
/**
+ * Prepares isolines by computing the the Java2D shapes that were not already computed in a previous rendering.
+ * This method shall be invoked in a background thread after image rendering has been completed (because this
+ * method uses some image computation results).
+ *
+ * @param isolines value of {@link IsolineRenderer#prepare()}. Shall not be {@code null}.
+ * @return the {@code isolines} array, potentially with less elements.
+ * @throws TransformException if an interpolated point can not be transformed using the given transform.
+ */
+ final IsolineRenderer.Snapshot[] complete(final IsolineRenderer.Snapshot[] isolines) throws TransformException {
+ final MathTransform centerToObjective = PixelTranslation.translate(
+ cornerToObjective, PixelInCell.CELL_CORNER, PixelInCell.CELL_CENTER);
+ return IsolineRenderer.complete(isolines, data, centerToObjective);
+ }
+
+ /**
* Creates new rendering data initialized to a copy of this instance.
*/
@Override
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index a1fdf82..a787886 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -146,7 +146,7 @@ public abstract class MapCanvas extends PlanarCanvas {
* It does not apply to the immediate feedback that the user gets from JavaFX affine transforms
* (an image with lower quality used until the higher quality image become ready).
*/
- private static final long REPAINT_DELAY = 500;
+ private static final long REPAINT_DELAY = 100;
/**
* The pane showing the map and any other JavaFX nodes to scale and translate together with the map.
@@ -323,7 +323,7 @@ public abstract class MapCanvas extends PlanarCanvas {
*/
private void onSizeChanged(final Observable property) {
sizeChanged = true;
- repaintLater();
+ requestRepaint();
}
/**
@@ -374,7 +374,7 @@ public abstract class MapCanvas extends PlanarCanvas {
final Point2D p = changeInProgress.deltaTransform(tx, ty);
transformOnNewImage.appendTranslation(p.getX(), p.getY());
if (!isFinal) {
- repaintLater();
+ requestRepaint();
}
}
if (isFinal && !transform.isIdentity()) {
@@ -445,7 +445,7 @@ public abstract class MapCanvas extends PlanarCanvas {
transform.appendRotation(angle, x, y);
transformOnNewImage.appendRotation(angle, p.getX(), p.getY());
}
- repaintLater();
+ requestRepaint();
}
if (event != null) {
event.consume();
@@ -828,18 +828,9 @@ public abstract class MapCanvas extends PlanarCanvas {
/**
* Requests the map to be rendered again, possibly with new data. Invoking this
* method does not necessarily causes the repaint process to start immediately.
- * The request will be queued and executed at an arbitrary time.
- */
- protected void requestRepaint() {
- contentChangeCount++;
- floatingPane.requestLayout();
- }
-
- /**
- * Invokes {@link #repaint()} after a short delay. This method is used when the
- * repaint event is caused by some gesture like pan, zoom or resizing the window.
+ * The request will be queued and executed at an arbitrary (short) time later.
*/
- private void repaintLater() {
+ public final void requestRepaint() {
contentChangeCount++;
if (renderingInProgress == null) {
executeRendering(new Delayed());
@@ -1022,7 +1013,7 @@ public abstract class MapCanvas extends PlanarCanvas {
* as an idle thread, and it is unlikely that other parts of this JavaFX application need that thread in same
* time (if it happens, other threads will be created).</div>
*
- * @see #repaintLater()
+ * @see #requestRepaint()
*/
private final class Delayed extends Task<Void> {
@Override protected Void call() {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
index 5445cca..8081a4c 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
@@ -119,6 +119,23 @@ public final class ColorRamp {
}
/**
+ * Returns {@code true} if this ramp has no color with a non-zero transparency.
+ * If this method returns {@code false}, then {@link #colors} is guaranteed non-empty.
+ *
+ * @return whether this color ramp is fully transparent.
+ */
+ public final boolean isTransparent() {
+ if (colors != null) {
+ for (final int code : colors) {
+ if ((code & 0xFF000000) != 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
* Returns a solid color to use for filling a rectangle in {@link ColorCell}.
* If this item has many colors (for example because it uses a gradient),
* then an arbitrary color is returned.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
index 4f0d02d..fa8f611 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.internal.gui.control;
+import java.util.Objects;
import java.text.NumberFormat;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
@@ -30,6 +31,7 @@ import javafx.scene.control.TableView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.CheckBoxTableCell;
import org.apache.sis.internal.gui.Styles;
+import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.resources.Vocabulary;
@@ -45,8 +47,13 @@ import org.apache.sis.util.resources.Vocabulary;
public final class ValueColorMapper extends ColorColumnHandler<ValueColorMapper.Step> {
/**
* Colors to associate to a given value.
+ *
+ * <h2>Ordering</h2>
+ * {@code Step} natural ordering is inconsistent with equals.
+ * Natural ordering is based on the {@linkplain #value} only,
+ * while the {@link #equals(Object)} method compares all properties.
*/
- public static final class Step {
+ public static final class Step implements Comparable<Step> {
/**
* The value for which to associate a color. The initial value is {@link Double#NaN},
* but that value should be used only for the insertion row.
@@ -56,11 +63,11 @@ public final class ValueColorMapper extends ColorColumnHandler<ValueColorMapper.
/**
* Color associated to the {@linkplain #value}.
*
- * The value type is {@link ColorRamp} for now, but if this property become public in a future version
- * then the type should be changed to {@link Color} and bidirectionally binded to another property
- * (package-private) of type {@link ColorRamp}.
+ * The value type is {@link ColorRamp} for now. But if this property become public (i.e. located
+ * in a non-internal package) in a future version then the type should be changed to {@link Color}
+ * and bidirectionally binded to another property (package-private) of type {@link ColorRamp}.
*/
- final ObjectProperty<ColorRamp> color;
+ public final ObjectProperty<ColorRamp> color;
/**
* Whether this step should be used. For example if {@code ValueColorMapper} is used as an isoline table,
@@ -86,7 +93,59 @@ public final class ValueColorMapper extends ColorColumnHandler<ValueColorMapper.
this.color.set(new ColorRamp(color));
visible.set(true);
}
+
+ /**
+ * Compares this step value with the given value for order.
+ * The comparison is applied only on the {@linkplain #value}.
+ * The color and visibility state are ignored.
+ *
+ * @param other the other value to compare with this step.
+ * @return +1 if the value of this step is higher than value of given step, -1 if smaller or 0 if equal.
+ */
+ @Override
+ public int compareTo(final Step other) {
+ return Double.compare(value.get(), other.value.get());
+ }
+
+ /**
+ * Compares the given object with this value for equality.
+ * This method compares all properties, including visibility and color.
+ *
+ * @param other the other object to compare with this step.
+ * @return whether the other object is equals to this step.
+ */
+ @Override
+ public boolean equals(final Object other) {
+ if (other instanceof Step) {
+ final Step that = (Step) other;
+ return Numerics.equals(value.get(), that.value.get()) &&
+ Objects.equals(color.get(), that.color.get()) &&
+ visible.get() == that.visible.get();
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash code value for this step.
+ *
+ * @return a hash code value for this step.
+ */
+ @Override
+ public int hashCode() {
+ return Double.hashCode(value.get()) + Objects.hashCode(color.get()) + Boolean.hashCode(visible.get());
+ }
+
+ /**
+ * Returns a string representation of this step for debugging purposes.
+ *
+ * @return a string representation of this step.
+ */
+ @Override
+ public String toString() {
+ return Double.toString(value.get()) + " = " + Objects.toString(color.get());
+ }
}
+
/**
* The format to use for formatting numerical values.
* The same instance will be shared by all {@link FormatTableCell}s in this table.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/EmptyShape.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/EmptyShape.java
new file mode 100644
index 0000000..8760712
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/EmptyShape.java
@@ -0,0 +1,64 @@
+/*
+ * 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.feature.j2d;
+
+import java.awt.Shape;
+import java.awt.Rectangle;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.util.NoSuchElementException;
+
+
+/**
+ * An empty shape.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public final class EmptyShape implements Shape, PathIterator {
+ /**
+ * The unique empty shape instance.
+ */
+ public static final Shape INSTANCE = new EmptyShape();
+
+ /**
+ * For {@link #INSTANCE} construction only.
+ */
+ private EmptyShape() {
+ }
+
+ /** Returns an empty bounds. */
+ @Override public Rectangle getBounds() {return new Rectangle();}
+ @Override public Rectangle2D getBounds2D() {return new Rectangle();}
+ @Override public int getWindingRule() {return WIND_NON_ZERO;}
+ @Override public boolean contains (Point2D p) {return false;}
+ @Override public boolean contains (Rectangle2D r) {return false;}
+ @Override public boolean intersects(Rectangle2D r) {return false;}
+ @Override public boolean contains (double x, double y) {return false;}
+ @Override public boolean contains (double x, double y, double w, double h) {return false;}
+ @Override public boolean intersects(double x, double y, double w, double h) {return false;}
+ @Override public PathIterator getPathIterator(AffineTransform at) {return this;}
+ @Override public PathIterator getPathIterator(AffineTransform at, double flatness) {return this;}
+ @Override public boolean isDone() {return true;}
+ @Override public void next() {throw new NoSuchElementException();}
+ @Override public int currentSegment( float[] coords) {throw new NoSuchElementException();}
+ @Override public int currentSegment(double[] coords) {throw new NoSuchElementException();}
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/IsolineTracer.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/IsolineTracer.java
index ddbe362..5545965 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/IsolineTracer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/IsolineTracer.java
@@ -77,7 +77,7 @@ final class IsolineTracer {
int y;
/**
- * Final transform to apply on coordinates.
+ * Final transform to apply on coordinates (integer source coordinates at pixel centers).
*/
private final MathTransform gridToCRS;
@@ -86,7 +86,7 @@ final class IsolineTracer {
*
* @param window the 2×2 window containing pixel values in the 4 corners of current contouring grid cell.
* @param pixelStride increment to the position in {@code window} for reading next sample value.
- * @param gridToCRS final transform to apply on coordinates.
+ * @param gridToCRS final transform to apply on coordinates (integer source coordinates at pixel centers).
*/
IsolineTracer(final double[] window, final int pixelStride, final MathTransform gridToCRS) {
this.window = window;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
index 8072396..3be55e0 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
@@ -112,6 +112,7 @@ public final class Isolines {
* If there is more bands than {@code levels.length}, the last array is reused for
* all remaining bands.
* @param gridToCRS transform from pixel coordinates to geometry coordinates, or {@code null} if none.
+ * Integer source coordinates are located at pixel centers.
* @return the isolines for each band in the given image.
* @throws TransformException if an interpolated point can not be transformed using the given transform.
*/