When debugging isoline generations using `StepsViewer`, use different colors for polylines at different stages.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
index 3977234..f4ae2b2 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
@@ -221,7 +221,7 @@
* The {@link #createPolyline(boolean)} method should be invoked before this method
* for making sure that there are no pending polylines.
*
- * @return the polyline, polygon or collector of polylines.
+ * @return the polyline, polygon or collection of polylines.
* May be {@code null} if no polyline or polygon has been created.
*/
public final Shape build() {
@@ -233,6 +233,17 @@
}
/**
+ * Returns a snapshot of currently added polylines or polygons without modifying the state of this builder.
+ * It is safe to continue building the shape and invoke this method again later for progressive rendering.
+ *
+ * @return the polyline, polygon or collection of polylines added so far.
+ * May be {@code null} if no polyline or polygon has been created.
+ */
+ public final Shape snapshot() {
+ return build();
+ }
+
+ /**
* Returns a string representation of the polyline under construction for debugging purposes.
* Current implementation formats only the first and last points, and tells how many points are between.
*/
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Isolines.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Isolines.java
index a1e22aa..abb8792 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Isolines.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Isolines.java
@@ -19,6 +19,8 @@
import java.util.AbstractList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
+import java.util.EnumMap;
import java.util.TreeMap;
import java.util.NavigableMap;
import java.util.function.BiConsumer;
@@ -49,7 +51,7 @@
*
* @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>
*
@@ -69,7 +71,7 @@
* by step.
*/
@Debug
- private static final BiConsumer<String,Path2D> LISTENER = null;
+ private static final BiConsumer<String,Isolines> LISTENER = null;
/**
* Creates an initially empty set of isolines for the given levels. The given {@code values}
@@ -399,8 +401,7 @@
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());
+ LISTENER.accept(String.format("After row %d of %d (%3.1f%%)", y, h, 100f*y/h), isolines[b]);
}
}
tracer.x = 0;
@@ -414,7 +415,7 @@
level.finish();
}
if (LISTENER != null) {
- LISTENER.accept("Finished band " + b, isolines[b].toRawPath());
+ LISTENER.accept("Finished band " + b, isolines[b]);
}
}
return isolines;
@@ -526,18 +527,18 @@
}
/**
- * Appends the pixel coordinates of all level to the given path, for debugging purposes only.
+ * Returns the pixel coordinates of all level, 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.
+ * @return the pixel coordinates.
*/
@Debug
- private Path2D toRawPath() {
- final Path2D path = new Path2D.Float();
+ final Map<PolylineStage,Path2D> toRawPath() {
+ final Map<PolylineStage,Path2D> appendTo = new EnumMap<>(PolylineStage.class);
for (final Tracer.Level level : levels) {
- level.toRawPath(path);
+ level.toRawPath(appendTo);
}
- return path;
+ return appendTo;
}
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineBuffer.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineBuffer.java
index 5da2768..be7b42e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineBuffer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineBuffer.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.internal.processing.isoline;
+import java.util.Map;
import java.util.Arrays;
import java.awt.geom.Path2D;
import org.apache.sis.internal.feature.j2d.PathBuilder;
@@ -200,15 +201,16 @@
*
* @param appendTo where to append the coordinates.
*
- * @see Tracer.Level#toRawPath(Path2D)
+ * @see Tracer.Level#toRawPath(Map)
*/
@Debug
- final void toRawPath(final Path2D appendTo) {
+ final void toRawPath(final Map<PolylineStage,Path2D> appendTo) {
int i = 0;
if (i < size) {
- appendTo.moveTo(coordinates[i++], coordinates[i++]);
+ final Path2D p = PolylineStage.BUFFER.destination(appendTo);
+ p.moveTo(coordinates[i++], coordinates[i++]);
while (i < size) {
- appendTo.lineTo(coordinates[i++], coordinates[i++]);
+ p.lineTo(coordinates[i++], coordinates[i++]);
}
}
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineStage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineStage.java
new file mode 100644
index 0000000..7ad1733
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineStage.java
@@ -0,0 +1,82 @@
+/*
+ * 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.isoline;
+
+import java.util.Map;
+import java.awt.Shape;
+import java.awt.geom.Path2D;
+import org.apache.sis.util.Debug;
+
+
+/**
+ * Tells at which stage are the polylines represented by a Java2D {@link Shape}.
+ * A set of polylines way still be under construction in {@link PolylineBuffer}
+ * during iteration over pixel values, or the polylines may have been classified
+ * as incomplete after iteration over a row, or the polylines may be final result.
+ *
+ * <p>This is used only for debugging purposes because end users should see only the final result.
+ * This information allows {@code StepsViewer} (in test package) to use different colors for different stages.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since 1.3
+ * @module
+ */
+@Debug
+enum PolylineStage {
+ /**
+ * The polylines are under construction in various {@link PolylineBuffer} instances.
+ * This is the first stage, which happens during iteration over pixel values.
+ */
+ BUFFER,
+
+ /**
+ * The polylines are no longer in the buffers filled by the iteration over pixel values,
+ * but are still incomplete. It happens when, after finishing iteration over a row, some
+ * polylines will not be continued by iteration on the next row and those polylines have
+ * not yet been closed as polygons. Those polyline fragments are moved to a "pending" list,
+ * as they may be closed later after more polylines fragments become available.
+ */
+ FRAGMENT,
+
+ /**
+ * The polylines are final result to be show to user.
+ */
+ FINAL;
+
+ /**
+ * Returns the destination where to write polylines for this stage.
+ *
+ * @param appendTo map of path for different stages.
+ * @return the path to use for writing polylines at this stage.
+ */
+ final Path2D destination(final Map<PolylineStage,Path2D> appendTo) {
+ return appendTo.computeIfAbsent(this, (k) -> new Path2D.Float());
+ }
+
+ /**
+ * Adds polylines to the specified map.
+ *
+ * @param appendTo where to append the polylines.
+ * @param polylines the polylines to append to the map, or {@code null} if none.
+ */
+ final void add(final Map<PolylineStage,Path2D> appendTo, final Shape polylines) {
+ if (polylines != null) {
+ destination(appendTo).append(polylines, false);
+ }
+ }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Tracer.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Tracer.java
index 5bfbbac..d6e3e6c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Tracer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Tracer.java
@@ -222,15 +222,20 @@
/**
* Builder of isolines as a Java2D shape, created when first needed.
- * The {@link PolylineBuffer} coordinates are copied in this path when a geometry is closed.
+ * The {@link PolylineBuffer} coordinates are copied in this path when a geometry is closed
+ * and transformed using {@link #gridToCRS}. This is almost final result; the only difference
+ * compared to {@link #shape} is that the coordinates are not yet wrapped in a {@link Shape}.
*
* @see #writeTo(Joiner, PolylineBuffer[], boolean)
+ * @see PolylineStage#FINAL
*/
private Joiner path;
/**
* The isolines as a Java2D shape, created by {@link #finish()}.
* This is the shape to be returned to user for this level after we finished to process all cells.
+ *
+ * @see PolylineStage#FINAL
*/
Shape shape;
@@ -688,9 +693,8 @@
* @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);
+ final void toRawPath(final Map<PolylineStage,Path2D> appendTo) {
+ PolylineStage.FINAL.add(appendTo, (path != null) ? path.snapshot() : shape);
polylineOnLeft.toRawPath(appendTo);
for (final PolylineBuffer p : polylinesOnTop) {
if (p != null) p.toRawPath(appendTo);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/StepsViewer.java b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/StepsViewer.java
index 0618d21..4f54021 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/StepsViewer.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/StepsViewer.java
@@ -16,6 +16,8 @@
*/
package org.apache.sis.internal.processing.isoline;
+import java.util.Map;
+import java.util.EnumMap;
import java.awt.Shape;
import java.awt.Color;
import java.awt.Graphics;
@@ -58,7 +60,7 @@
* @module
*/
@SuppressWarnings("serial")
-public final class StepsViewer extends JComponent implements BiConsumer<String,Path2D>, ChangeListener, ActionListener {
+public final class StepsViewer extends JComponent implements BiConsumer<String,Isolines>, 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.
@@ -116,7 +118,13 @@
/**
* The isolines to show.
*/
- private Path2D isolines;
+ private final Map<PolylineStage,Path2D> isolines;
+
+ /**
+ * The colors to associate to the isoline for each stage.
+ * Array indices are {@link PolylineStage#ordinal()} values.
+ */
+ private final Color[] stageColors;
/**
* Bounds of {@link #isolines}, slightly expanded for making easier to see.
@@ -136,6 +144,10 @@
*/
@SuppressWarnings("ThisEscapedInObjectConstruction")
private StepsViewer(final RenderedImage data, final Container pane) {
+ isolines = new EnumMap<>(PolylineStage.class);
+ stageColors = new Color[] {Color.YELLOW, Color.CYAN, Color.GRAY};
+ setBackground(Color.BLACK);
+ setOpaque(true);
final double scaleX = (CANVAS_WIDTH - 2*PADDING) / (double) data.getWidth();
final double scaleY = (CANVAS_HEIGHT - 2*PADDING) / (double) data.getHeight();
sourceToCanvas = new AffineTransform2D(
@@ -186,7 +198,6 @@
for (final Shape shape : iso.polylines().values()) {
path.append(shape, false);
}
- viewer.accept("Final result", path);
}
/**
@@ -196,15 +207,18 @@
protected void paintComponent(final Graphics g) {
super.paintComponent(g);
final Graphics2D gh = (Graphics2D) g;
+ gh.setColor(getBackground());
+ gh.fillRect(0, 0, getWidth(), getHeight());
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);
+ for (final Map.Entry<PolylineStage,Path2D> entry : isolines.entrySet()) {
+ final int stage = entry.getKey().ordinal();
+ gh.setStroke(new BasicStroke(stageColors.length - stage));
+ gh.setColor(stageColors[stage]);
+ gh.draw(entry.getValue());
}
}
@@ -246,27 +260,48 @@
* 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.
+ * @param title description of current state.
+ * @param generator new generator of isolines.
*/
@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;
+ public void accept(final String title, final Isolines generator) {
+ final Map<PolylineStage, Path2D> paths = generator.toRawPath();
+ for (final Map.Entry<PolylineStage,Path2D> entry : paths.entrySet()) {
+ entry.getValue().transform(sourceToCanvas);
+ }
try {
final CountDownLatch c = new CountDownLatch(1);
EventQueue.invokeLater(() -> {
- if (isolines != null && equal(isolines.getPathIterator(null), update.getPathIterator(null))) {
+ Rectangle b = null;
+ boolean unchanged = true;
+ for (final PolylineStage stage : PolylineStage.values()) {
+ final Path2D current = isolines.get(stage);
+ final Path2D update = paths.get(stage);
+ if (unchanged && current != update && !(current != null && update != null &&
+ equal(current.getPathIterator(null), update.getPathIterator(null))))
+ {
+ unchanged = false;
+ }
+ if (update == null) {
+ isolines.remove(stage);
+ } else {
+ isolines.put(stage, update);
+ if (stage == PolylineStage.BUFFER) {
+ b = update.getBounds();
+ b.x -= PADDING;
+ b.y -= PADDING;
+ b.width += PADDING * 2;
+ b.height += PADDING * 2;
+ bounds = b;
+ }
+ }
+ }
+ bounds = b;
+ if (unchanged) {
stepTitle.setText(title + " (no change)");
c.countDown();
} else {
stepTitle.setText(title);
- isolines = update;
- bounds = b;
repaint();
assertNull(blocker);
if (next.getModel().isPressed()) {