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 b071e11ed38be47343db9ffacf3ae136c7abdf5d Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Aug 14 15:09:46 2022 +0200 When debugging isoline generations using `StepsViewer`, use different colors for polylines at different stages. --- .../sis/internal/feature/j2d/PathBuilder.java | 13 +++- .../sis/internal/processing/isoline/Isolines.java | 23 +++--- .../processing/isoline/PolylineBuffer.java | 10 +-- .../internal/processing/isoline/PolylineStage.java | 82 ++++++++++++++++++++++ .../sis/internal/processing/isoline/Tracer.java | 12 ++-- .../internal/processing/isoline/StepsViewer.java | 73 ++++++++++++++----- 6 files changed, 174 insertions(+), 39 deletions(-) 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 3977234d43..f4ae2b28b5 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 @@ public class PathBuilder { * 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() { @@ -232,6 +232,17 @@ public class PathBuilder { } } + /** + * 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 a1e22aa3fe..abb8792332 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 @@ package org.apache.sis.internal.processing.isoline; 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 @@ import static org.apache.sis.internal.processing.isoline.Tracer.LOWER_RIGHT; * * @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 @@ public final class Isolines { * 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 @@ abort: while (iterator.next()) { 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 @@ abort: while (iterator.next()) { 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 @@ abort: while (iterator.next()) { } /** - * 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 5da2768203..be7b42e4f2 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 @@ final class PolylineBuffer { * * @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 0000000000..7ad1733809 --- /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 5bfbbac58d..d6e3e6cfc6 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 @@ final class Tracer { /** * 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 @@ final class Tracer { * @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 0618d21a10..4f540210bf 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 @@ import static org.junit.Assert.*; * @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 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,P /** * 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 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,P */ @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 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,P for (final Shape shape : iso.polylines().values()) { path.append(shape, false); } - viewer.accept("Final result", path); } /** @@ -196,15 +207,18 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,P 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 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,P * 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()) {