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 2022/08/12 16:13:33 UTC
[sis] branch geoapi-4.0 updated: Add an `IsolineViewer` widget for watching isoline generation step-by-step. For debugging purposes only.
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 357e4d946b Add an `IsolineViewer` widget for watching isoline generation step-by-step. For debugging purposes only.
357e4d946b is described below
commit 357e4d946b145a7361a748b28deb8686c647aed9
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Aug 12 18:12:29 2022 +0200
Add an `IsolineViewer` widget for watching isoline generation step-by-step.
For debugging purposes only.
---
.../internal/processing/image/IsolineTracer.java | 45 ++-
.../sis/internal/processing/image/Isolines.java | 40 ++-
.../internal/processing/image/package-info.java | 2 +-
.../internal/processing/image/IsolineViewer.java | 333 +++++++++++++++++++++
4 files changed, 416 insertions(+), 4 deletions(-)
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 63dcb38366..b0e55cadd0 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
@@ -25,11 +25,13 @@ import java.util.Collections;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
+import java.awt.geom.Path2D;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.internal.feature.j2d.PathBuilder;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Debug;
/**
@@ -39,7 +41,7 @@ import org.apache.sis.util.ArraysExt;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
*
* @see <a href="https://en.wikipedia.org/wiki/Marching_squares">Marching squares on Wikipedia</a>
*
@@ -71,11 +73,13 @@ final class IsolineTracer {
/**
* Pixel coordinate on the left side of the cell where to interpolate.
+ * The range is 0 inclusive to {@code domain.width} exclusive.
*/
int x;
/**
* Pixel coordinate on the top side of the cell where to interpolate.
+ * The range is 0 inclusive to {@code domain.height} exclusive.
*/
int y;
@@ -676,6 +680,25 @@ final class IsolineTracer {
path = null;
}
}
+
+ /**
+ * Appends the pixel coordinates of this level to the given path, for debugging purposes only.
+ * The {@link #gridToCRS} transform is <em>not</em> applied by this method.
+ * For avoiding confusing behavior, that transform should be null.
+ *
+ * @param appendTo where to append the coordinates.
+ *
+ * @see Isolines#toRawPath()
+ */
+ @Debug
+ final void toRawPath(final Path2D appendTo) {
+ final Shape s = (path != null) ? path.build() : shape;
+ if (s != null) appendTo.append(s, false);
+ polylineOnLeft.toRawPath(appendTo);
+ for (final Polyline p : polylinesOnTop) {
+ if (p != null) p.toRawPath(appendTo);
+ }
+ }
}
/**
@@ -842,6 +865,26 @@ final class IsolineTracer {
public String toString() {
return PathBuilder.toString(coordinates, size);
}
+
+ /**
+ * Appends the pixel coordinates of this polyline to the given path, for debugging purposes only.
+ * The {@link #gridToCRS} transform is <em>not</em> applied by this method.
+ * For avoiding confusing behavior, that transform should be null.
+ *
+ * @param appendTo where to append the coordinates.
+ *
+ * @see Level#toRawPath(Path2D)
+ */
+ @Debug
+ private void toRawPath(final Path2D appendTo) {
+ int i = 0;
+ if (i < size) {
+ appendTo.moveTo(coordinates[i++], coordinates[i++]);
+ while (i < size) {
+ appendTo.lineTo(coordinates[i++], coordinates[i++]);
+ }
+ }
+ }
}
/**
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 abc87cd60b..a3532ecb56 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
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.NavigableMap;
+import java.util.function.BiConsumer;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.CompletionException;
@@ -34,6 +35,7 @@ import org.apache.sis.image.PixelIterator;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Debug;
import static org.apache.sis.internal.processing.image.IsolineTracer.UPPER_LEFT;
import static org.apache.sis.internal.processing.image.IsolineTracer.UPPER_RIGHT;
@@ -59,6 +61,15 @@ public final class Isolines {
*/
private final IsolineTracer.Level[] levels;
+ /**
+ * A consumer to notify about the current state of isoline generation, or {@code null} if none.
+ * This is used for debugging purposes only. This field is temporarily set to a non-null value
+ * when using {@code IsolineViewer} (in test package) for following an isoline generation step
+ * by step.
+ */
+ @Debug
+ private static final BiConsumer<String,Path2D> LISTENER = null;
+
/**
* Creates an initially empty set of isolines for the given levels. The given {@code values}
* array should be one of the arrays validated by {@link #cloneAndSort(double[][])}.
@@ -377,13 +388,19 @@ abort: while (iterator.next()) {
}
}
/*
- * Finished iteration on a row. Clear flags and update position
- * before to move to next row.
+ * Finished iteration on a row. Clear flags and update position before to move to next row.
+ * If there is listeners to notify (for debugging purposes), notify them.
*/
for (int b=0; b<numBands; b++) {
for (final IsolineTracer.Level level : isolines[b].levels) {
level.finishedRow();
}
+ if (LISTENER != null) {
+ final int y = tracer.y;
+ final int h = iterator.getDomain().height;
+ LISTENER.accept(String.format("After row %d of %d (%3.1f%%)", y, h, 100f*y/h),
+ isolines[b].toRawPath());
+ }
}
tracer.x = 0;
tracer.y++;
@@ -395,6 +412,9 @@ abort: while (iterator.next()) {
for (final IsolineTracer.Level level : isolines[b].levels) {
level.finish();
}
+ if (LISTENER != null) {
+ LISTENER.accept("Finished band " + b, isolines[b].toRawPath());
+ }
}
return isolines;
}
@@ -503,4 +523,20 @@ abort: while (iterator.next()) {
return isolines().clone();
}
}
+
+ /**
+ * Appends the pixel coordinates of all level to the given path, for debugging purposes only.
+ * The {@link #gridToCRS} transform is <em>not</em> applied by this method.
+ * For avoiding confusing behavior, that transform should be null.
+ *
+ * @param appendTo where to append the coordinates.
+ */
+ @Debug
+ private Path2D toRawPath() {
+ final Path2D path = new Path2D.Float();
+ for (final IsolineTracer.Level level : levels) {
+ level.toRawPath(path);
+ }
+ return path;
+ }
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/package-info.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/package-info.java
index a0873312f0..6dd6be1b79 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/package-info.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/package-info.java
@@ -25,7 +25,7 @@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
* @since 1.1
* @module
*/
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolineViewer.java b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolineViewer.java
new file mode 100644
index 0000000000..446946b93c
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolineViewer.java
@@ -0,0 +1,333 @@
+/*
+ * 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.processing.image;
+
+import java.awt.Shape;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.BasicStroke;
+import java.awt.EventQueue;
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.image.RenderedImage;
+import javax.swing.Timer;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JLabel;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.ButtonModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.util.function.BiConsumer;
+import java.util.concurrent.CountDownLatch;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * A viewer for showing isoline generation step-by-step.
+ * For enabling the use of this class, temporarily remove {@code private} and {@code final} keywords in
+ * {@link Isolines#LISTENER}, then uncomment the {@link #setListener(IsolineViewer)} constructor body.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since 1.3
+ * @module
+ */
+@SuppressWarnings("serial")
+public final class IsolineViewer extends JComponent implements BiConsumer<String,Path2D>, ChangeListener, ActionListener {
+ /**
+ * Sets the component to be notified after each row of isolines generated from the rendered image.
+ * The body of this method is commented-out because {@link Isolines#LISTENER} is private and final.
+ * The body should be uncommented only temporarily during debugging phases.
+ */
+ private static void setListener(final IsolineViewer listener) {
+ // Isolines.LISTENER = listener;
+ }
+
+ /**
+ * Entry point for debugging. Edit this method body as needed for loading an image to use as test data.
+ *
+ * @param args ignored.
+ * @throws Exception if an error occurred during I/O or isoline generation.
+ */
+ public static void main(final String[] args) throws Exception {
+ // showStepByStep(local.test.DebugIsoline.data(), 0);
+ }
+
+ /**
+ * Size of the window and spacing between borders and isolines. All values are in pixels.
+ */
+ private static final int CANVAS_WIDTH = 1600, CANVAS_HEIGHT = 1000, PADDING = 3;
+
+ /**
+ * Whether to flip X and/or Y axis.
+ */
+ private static final boolean FLIP_X = false, FLIP_Y = true;
+
+ /**
+ * Description of current step. This title is updated at each isoline generation step,
+ * when {@link #accept(String, Shape)} is invoked.
+ */
+ private final JLabel stepTitle;
+
+ /**
+ * The button for moving to the next step. When this button is enabled, the isoline process is blocked
+ * by {@link #blocker} until this button is pressed. When this button is pressed, the isoline process
+ * continue until {@link #accept(String, Shape)} is invoked again.
+ *
+ * @see #actionPerformed(ActionEvent)
+ */
+ private final JButton next;
+
+ /**
+ * Simulate a "next" action after some delay. This is used when users keep the "Next" button pressed.
+ */
+ private final Timer delayedNext;
+
+ /**
+ * Blocks the isoline computation thread until the user is ready to see the next step.
+ */
+ private CountDownLatch blocker;
+
+ /**
+ * The isolines to show.
+ */
+ private Path2D isolines;
+
+ /**
+ * Bounds of {@link #isolines}, slightly expanded for making easier to see.
+ */
+ private Rectangle bounds;
+
+ /**
+ * Conversion from pixel indices in the source image to pixel indices in the displayed window.
+ */
+ private final AffineTransform2D sourceToCanvas;
+
+ /**
+ * Creates a new viewer.
+ *
+ * @param data the source of data for isolines.
+ * @param pane the container where to add components.
+ */
+ @SuppressWarnings("ThisEscapedInObjectConstruction")
+ private IsolineViewer(final RenderedImage data, final Container pane) {
+ final double scaleX = (CANVAS_WIDTH - 2*PADDING) / (double) data.getWidth();
+ final double scaleY = (CANVAS_HEIGHT - 2*PADDING) / (double) data.getHeight();
+ sourceToCanvas = new AffineTransform2D(
+ FLIP_X ? -scaleX : scaleX, 0, 0, FLIP_Y ? -scaleY : scaleY,
+ scaleX * (PADDING + data.getMinX() + (FLIP_X ? data.getWidth() : 0)),
+ scaleY * (PADDING + data.getMinY() + (FLIP_Y ? data.getHeight() : 0)));
+
+ stepTitle = new JLabel();
+ next = new JButton("Next");
+ next.setEnabled(false);
+ next.addActionListener(this);
+ next.getModel().addChangeListener(this);
+ delayedNext = new Timer(1000, this::fastForward); // 1 second delay before fast forward.
+ delayedNext.setRepeats(false);
+
+ final JPanel bar = new JPanel(new BorderLayout());
+ bar .add(stepTitle, BorderLayout.CENTER);
+ bar .add(next, BorderLayout.EAST);
+ pane.add(bar, BorderLayout.NORTH);
+ pane.add(this, BorderLayout.CENTER);
+ }
+
+ /**
+ * Generates isolines for the given image and show the result step by step.
+ * The given image shall have only one band.
+ *
+ * @param data the source of data for isolines.
+ * @param levels levels of isolones to generate.
+ */
+ public static void showStepByStep(final RenderedImage data, final double... levels) {
+ assertEquals("Unsupported number of bands.", 1, data.getSampleModel().getNumBands());
+ final JFrame frame = new JFrame("Step-by-step isoline viewer");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setLayout(new BorderLayout());
+ final IsolineViewer viewer = new IsolineViewer(data, frame.getContentPane());
+ final Isolines iso;
+ try {
+ setListener(viewer);
+ frame.setVisible(true);
+ frame.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);
+ iso = Isolines.generate(data, new double[][] {levels}, null)[0];
+ } catch (TransformException e) {
+ throw new AssertionError(e); // Should not happen because we specified an identity transform.
+ } finally {
+ setListener(null);
+ }
+ final Path2D path = new Path2D.Float();
+ for (final Shape shape : iso.polylines().values()) {
+ path.append(shape, false);
+ }
+ viewer.accept("Final result", path);
+ }
+
+ /**
+ * Invoked when the isolines need to be drawn.
+ */
+ @Override
+ protected void paintComponent(final Graphics g) {
+ super.paintComponent(g);
+ final Graphics2D gh = (Graphics2D) g;
+ if (bounds != null) {
+ gh.setStroke(new BasicStroke(2));
+ gh.setColor(Color.RED);
+ gh.draw(bounds);
+ }
+ if (isolines != null) {
+ gh.setStroke(new BasicStroke(1));
+ gh.setColor(Color.BLUE);
+ gh.draw(isolines);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the shapes described by given iterators are equal.
+ * This is used for deciding if it is worth to bother the user with a request
+ * for pressing the "Next" button.
+ */
+ private static boolean equal(final PathIterator it1, final PathIterator it2) {
+ final float[] a1 = new float[6];
+ final float[] a2 = new float[6];
+ while (!it1.isDone()) {
+ if (it2.isDone()) return false;
+ final int code = it1.currentSegment(a1);
+ if (code != it2.currentSegment(a2)) {
+ return false;
+ }
+ int n;
+ switch (code) {
+ case PathIterator.SEG_MOVETO:
+ case PathIterator.SEG_LINETO: n = 2; break;
+ case PathIterator.SEG_QUADTO: n = 4; break;
+ case PathIterator.SEG_CUBICTO: n = 6; break;
+ case PathIterator.SEG_CLOSE: n = 0; break;
+ default: throw new AssertionError(code);
+ }
+ while (--n >= 0) {
+ if (Float.floatToIntBits(a1[n]) != Float.floatToIntBits(a2[n])) {
+ return false;
+ }
+ }
+ it1.next();
+ it2.next();
+ }
+ return it2.isDone();
+ }
+
+ /**
+ * Invoked after a row has been processed during the isoline generation.
+ * This is invoked from the main thread (<strong>not</strong> the Swing thread).
+ *
+ * @param title description of current state.
+ * @param update new isolines to show.
+ */
+ @Override
+ public void accept(final String title, final Path2D update) {
+ update.transform(sourceToCanvas);
+ final Rectangle b = update.getBounds();
+ b.x -= PADDING;
+ b.y -= PADDING;
+ b.width += PADDING * 2;
+ b.height += PADDING * 2;
+ try {
+ final CountDownLatch c = new CountDownLatch(1);
+ EventQueue.invokeLater(() -> {
+ if (isolines != null && equal(isolines.getPathIterator(null), update.getPathIterator(null))) {
+ stepTitle.setText(title + " (no change)");
+ c.countDown();
+ } else {
+ stepTitle.setText(title);
+ isolines = update;
+ bounds = b;
+ repaint();
+ assertNull(blocker);
+ if (next.getModel().isPressed()) {
+ c.countDown();
+ } else {
+ blocker = c;
+ next.setEnabled(true);
+ }
+ }
+ });
+ c.await();
+ } catch (InterruptedException e) {
+ throw new AssertionError(e); // Stop the test.
+ }
+ }
+
+ /**
+ * Invoked by Swing when user presses the "Next" button.
+ * This method resumes isoline computation.
+ *
+ * @param event ignored.
+ */
+ @Override
+ public void actionPerformed(final ActionEvent event) {
+ next.setEnabled(false);
+ if (blocker != null) {
+ blocker.countDown();
+ blocker = null;
+ }
+ }
+
+ /**
+ * Invoked when the "Next" button is kept pressed.
+ * The effect is to start the "fast forward" mode.
+ * This method shall be invoked in Swing thread.
+ *
+ * @param event ignored.
+ */
+ private void fastForward(final ActionEvent event) {
+ if (next.getModel().isPressed()) {
+ if (blocker != null) {
+ blocker.countDown();
+ blocker = null;
+ }
+ }
+ }
+
+ /**
+ * Invoked by Swing when the state of the "Next" button (pressed or not) changed.
+ * If the button is pressed one second without being released, then we enter a
+ * "fast forward" mode until the button is released.
+ *
+ * @param event ignored.
+ */
+ @Override
+ public void stateChanged(final ChangeEvent event) {
+ final ButtonModel m = (ButtonModel) event.getSource();
+ if (m.isPressed()) {
+ delayedNext.restart();
+ } else {
+ delayedNext.stop();
+ }
+ }
+}