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/14 22:27:53 UTC

[sis] branch geoapi-4.0 updated (d81b368aef -> 50f768333d)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from d81b368aef Move the `Unclosed` inner class to a top-level class, renamed `Fragments`. There is no code change (other than move/renaming) in this commit.
     new a44364b67c Move the `Joiner` inner class to a top-level class. There is no code change (other than move) in this commit.
     new b071e11ed3 When debugging isoline generations using `StepsViewer`, use different colors for polylines at different stages.
     new 50f768333d Try to show more debugging information during isoline generation.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../sis/internal/feature/j2d/MultiPolylines.java   |  13 +-
 .../sis/internal/feature/j2d/PathBuilder.java      |  13 +-
 .../apache/sis/internal/feature/j2d/Polyline.java  |  13 +-
 .../sis/internal/feature/j2d/package-info.java     |   2 +-
 .../sis/internal/processing/isoline/Fragments.java |   7 +-
 .../sis/internal/processing/isoline/Isolines.java  |  23 +--
 .../sis/internal/processing/isoline/Joiner.java    | 168 +++++++++++++++++++++
 .../processing/isoline/PolylineBuffer.java         |  13 +-
 .../internal/processing/isoline/PolylineStage.java | 116 ++++++++++++++
 .../sis/internal/processing/isoline/Tracer.java    | 154 ++-----------------
 .../internal/processing/isoline/StepsViewer.java   |  76 +++++++---
 11 files changed, 407 insertions(+), 191 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Joiner.java
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineStage.java


[sis] 01/03: Move the `Joiner` inner class to a top-level class. There is no code change (other than move) in this commit.

Posted by de...@apache.org.
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 a44364b67c38da390dbb94a3dbed715f8affbe2d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Aug 14 10:55:13 2022 +0200

    Move the `Joiner` inner class to a top-level class.
    There is no code change (other than move) in this commit.
---
 .../sis/internal/processing/isoline/Fragments.java |   7 +-
 .../sis/internal/processing/isoline/Joiner.java    | 168 +++++++++++++++++++++
 .../sis/internal/processing/isoline/Tracer.java    | 141 -----------------
 3 files changed, 172 insertions(+), 144 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Fragments.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Fragments.java
index da97430340..698fc1b247 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Fragments.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Fragments.java
@@ -37,11 +37,12 @@ import org.apache.sis.internal.util.Numerics;
  * of {@code double[]} arrays: because even indices become odd and odd indices become even, points order are
  * implicitly reverted without the need to rewrite all {@code double[]} array contents.
  *
- * @see Tracer.Level#partialPaths
- *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.3
- * @since   1.1
+ *
+ * @see Tracer.Level#partialPaths
+ *
+ * @since 1.1
  * @module
  */
 @SuppressWarnings({"CloneableImplementsClone", "serial"})           // Not intended to be cloned or serialized.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Joiner.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Joiner.java
new file mode 100644
index 0000000000..b9ccfcfe87
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Joiner.java
@@ -0,0 +1,168 @@
+/*
+ * 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 org.apache.sis.internal.feature.j2d.PathBuilder;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+
+
+/**
+ * Assembles arbitrary amount of {@link PolylineBuffer}s in a single Java2D {@link Shape} for an isoline level.
+ * This class extends {@link PathBuilder} with two additional features: remove spikes caused by ambiguities,
+ * then apply a {@link MathTransform} on all coordinate values.
+ *
+ * <h2>Spikes</h2>
+ * If the shape delimited by given polylines has a part with zero width or height ({@literal i.e.} a spike),
+ * truncates the polylines for removing that spike. This situation happens when some pixel values are exactly
+ * equal to isoline value, as in the picture below:
+ *
+ * {@preformat text
+ *     ●╌╌╌╲╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○
+ *     ╎    ╲ ╎      ╎      ╎      ╎
+ *     ╎     ╲╎      ╎   →  ╎      ╎
+ *     ●╌╌╌╌╌╌●──────●──────●⤸╌╌╌╌╌○
+ *     ╎     ╱╎      ╎   ←  ╎      ╎
+ *     ╎    ╱ ╎      ╎      ╎      ╎
+ *     ●╌╌╌╱╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○
+ * }
+ *
+ * The spike may appear or not depending on the convention adopted for strictly equal values.
+ * In above picture, the spike appears because the convention used in this implementation is:
+ *
+ * <ul>
+ *   <li>○: {@literal pixel value < isoline value}.</li>
+ *   <li>●: {@literal pixel value ≥ isoline value}.</li>
+ * </ul>
+ *
+ * If the following convention was used instead, the spike would not appear in above figure
+ * (but would appear in different situations):
+ *
+ * <ul>
+ *   <li>○: {@literal pixel value ≤ isoline value}.</li>
+ *   <li>●: {@literal pixel value > isoline value}.</li>
+ * </ul>
+ *
+ * This class detects and removes those spikes for avoiding convention-dependent results.
+ * We assume that spikes can appear only at the junction between two {@link PolylineBuffer} instances.
+ * Rational: having a spike require that we move forward then backward on the same coordinates,
+ * which is possible only with a non-null {@link PolylineBuffer#opposite} field.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.1
+ * @module
+ */
+final class Joiner extends PathBuilder {
+    /**
+     * Final transform to apply on coordinates, or {@code null} if none.
+     */
+    private final MathTransform gridToCRS;
+
+    /**
+     * Creates an initially empty set of isoline shapes.
+     */
+    Joiner(final MathTransform gridToCRS) {
+        this.gridToCRS = gridToCRS;
+    }
+
+    /**
+     * Detects and removes spikes for avoiding convention-dependent results.
+     * See {@link Joiner} class-javadoc for a description of the problem.
+     *
+     * <p>We perform the analysis in this method instead of in {@link #filterFull(double[], int)} on the
+     * the assumption that spikes can appear only between two calls to {@code append(…)} (because having
+     * a spike requires that we move forward then backward on the same coordinates, which happen only with
+     * two distinct {@link PolylineBuffer} instances). It reduce the amount of coordinates to examine since
+     * we can check only the extremities instead of looking for spikes anywhere in the array.</p>
+     *
+     * @param  coordinates  the coordinates to filter. Values can be modified in-place.
+     * @param  lower        index of first coordinate to filter. Always even.
+     * @param  upper        index after the last coordinate to filter. Always even.
+     * @return number of valid coordinates after filtering.
+     */
+    @Override
+    protected int filterChunk(final double[] coordinates, final int lower, final int upper) {
+        int spike0 = lower;         // Will be index where (x,y) become different than (xo,yo).
+        int spike1 = lower;         // Idem, but searching forward instead of backward.
+        if (spike1 < upper) {
+            final double xo = coordinates[spike1++];
+            final double yo = coordinates[spike1++];
+            int equalityMask = 3;                   // Bit 1 set if (x == xo), bit 2 set if (y == yo).
+            while (spike1 < upper) {
+                final int before = equalityMask;
+                if (coordinates[spike1++] != xo) equalityMask &= ~1;
+                if (coordinates[spike1++] != yo) equalityMask &= ~2;
+                if (equalityMask == 0) {
+                    equalityMask = before;                  // For keeping same search criterion.
+                    spike1 -= PolylineBuffer.DIMENSION;      // Restore previous position before mismatch.
+                    break;
+                }
+            }
+            while (spike0 > 0) {
+                if (coordinates[--spike0] != yo) equalityMask &= ~2;
+                if (coordinates[--spike0] != xo) equalityMask &= ~1;
+                if (equalityMask == 0) {
+                    spike0 += PolylineBuffer.DIMENSION;
+                    break;
+                }
+            }
+        }
+        /*
+         * Here we have range of indices where the polygon has a width or height of zero.
+         * Search for a common point, then truncate at that point. Indices are like below:
+         *
+         *     0       spike0    lower          spike1         upper
+         *     ●────●────●────●────●────●────●────●────●────●────●
+         *                    └╌╌╌╌remove╌╌╌╌┘
+         * where:
+         *  - `lower` and `spike0` are inclusive.
+         *  - `upper` and `spike1` are exclusive.
+         *  - the region to remove are sowewhere between `spike0` and `spike1`.
+         */
+        final int limit = spike1;
+        int base;
+        while ((base = spike0 + 2*PolylineBuffer.DIMENSION) < limit) {    // Spikes exist only with at least 3 points.
+            final double xo = coordinates[spike0++];
+            final double yo = coordinates[spike0++];
+            spike1 = limit;
+            do {
+                if (coordinates[spike1 - 2] == xo && coordinates[spike1 - 1] == yo) {
+                    /*
+                     * Remove points between the common point (xo,yo). The common point is kept on the
+                     * left side (`spike0` is already after that point) and removed on the right side.
+                     */
+                    System.arraycopy(coordinates, spike1, coordinates, spike0, upper - spike1);
+                    return upper - (spike1 - spike0);
+                }
+            } while ((spike1 -= PolylineBuffer.DIMENSION) > base);
+        }
+        return upper;       // Nothing to remove.
+    }
+
+    /**
+     * Applies user-specified coordinate transform on all points of the whole polyline.
+     * This method is invoked after {@link #filterChunk(double[], int, int)}.
+     */
+    @Override
+    protected int filterFull(final double[] coordinates, final int upper) throws TransformException {
+        if (gridToCRS != null) {
+            gridToCRS.transform(coordinates, 0, coordinates, 0, upper / PolylineBuffer.DIMENSION);
+        }
+        return upper;
+    }
+}
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 17c777e460..5bfbbac58d 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
@@ -698,147 +698,6 @@ final class Tracer {
         }
     }
 
-    /**
-     * Assembles arbitrary amount of {@link PolylineBuffer}s in a single Java2D {@link Shape} for an isoline level.
-     * This class extends {@link PathBuilder} with two additional features: remove spikes caused by ambiguities,
-     * then apply a {@link MathTransform} on all coordinate values.
-     *
-     * <h2>Spikes</h2>
-     * If the shape delimited by given polylines has a part with zero width or height ({@literal i.e.} a spike),
-     * truncates the polylines for removing that spike. This situation happens when some pixel values are exactly
-     * equal to isoline value, as in the picture below:
-     *
-     * {@preformat text
-     *     ●╌╌╌╲╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○
-     *     ╎    ╲ ╎      ╎      ╎      ╎
-     *     ╎     ╲╎      ╎   →  ╎      ╎
-     *     ●╌╌╌╌╌╌●──────●──────●⤸╌╌╌╌╌○
-     *     ╎     ╱╎      ╎   ←  ╎      ╎
-     *     ╎    ╱ ╎      ╎      ╎      ╎
-     *     ●╌╌╌╱╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○╌╌╌╌╌╌○
-     * }
-     *
-     * The spike may appear or not depending on the convention adopted for strictly equal values.
-     * In above picture, the spike appears because the convention used in this implementation is:
-     *
-     * <ul>
-     *   <li>○: {@literal pixel value < isoline value}.</li>
-     *   <li>●: {@literal pixel value ≥ isoline value}.</li>
-     * </ul>
-     *
-     * If the following convention was used instead, the spike would not appear in above figure
-     * (but would appear in different situations):
-     *
-     * <ul>
-     *   <li>○: {@literal pixel value ≤ isoline value}.</li>
-     *   <li>●: {@literal pixel value > isoline value}.</li>
-     * </ul>
-     *
-     * This class detects and removes those spikes for avoiding convention-dependent results.
-     * We assume that spikes can appear only at the junction between two {@link PolylineBuffer} instances.
-     * Rational: having a spike require that we move forward then backward on the same coordinates,
-     * which is possible only with a non-null {@link PolylineBuffer#opposite} field.
-     */
-    private static final class Joiner extends PathBuilder {
-        /**
-         * Final transform to apply on coordinates, or {@code null} if none.
-         */
-        private final MathTransform gridToCRS;
-
-        /**
-         * Creates an initially empty set of isoline shapes.
-         */
-        Joiner(final MathTransform gridToCRS) {
-            this.gridToCRS = gridToCRS;
-        }
-
-        /**
-         * Detects and removes spikes for avoiding convention-dependent results.
-         * See {@link Joiner} class-javadoc for a description of the problem.
-         *
-         * <p>We perform the analysis in this method instead of in {@link #filterFull(double[], int)} on the
-         * the assumption that spikes can appear only between two calls to {@code append(…)} (because having
-         * a spike requires that we move forward then backward on the same coordinates, which happen only with
-         * two distinct {@link PolylineBuffer} instances). It reduce the amount of coordinates to examine since
-         * we can check only the extremities instead of looking for spikes anywhere in the array.</p>
-         *
-         * @param  coordinates  the coordinates to filter. Values can be modified in-place.
-         * @param  lower        index of first coordinate to filter. Always even.
-         * @param  upper        index after the last coordinate to filter. Always even.
-         * @return number of valid coordinates after filtering.
-         */
-        @Override
-        protected int filterChunk(final double[] coordinates, final int lower, final int upper) {
-            int spike0 = lower;         // Will be index where (x,y) become different than (xo,yo).
-            int spike1 = lower;         // Idem, but searching forward instead of backward.
-            if (spike1 < upper) {
-                final double xo = coordinates[spike1++];
-                final double yo = coordinates[spike1++];
-                int equalityMask = 3;                   // Bit 1 set if (x == xo), bit 2 set if (y == yo).
-                while (spike1 < upper) {
-                    final int before = equalityMask;
-                    if (coordinates[spike1++] != xo) equalityMask &= ~1;
-                    if (coordinates[spike1++] != yo) equalityMask &= ~2;
-                    if (equalityMask == 0) {
-                        equalityMask = before;                  // For keeping same search criterion.
-                        spike1 -= PolylineBuffer.DIMENSION;      // Restore previous position before mismatch.
-                        break;
-                    }
-                }
-                while (spike0 > 0) {
-                    if (coordinates[--spike0] != yo) equalityMask &= ~2;
-                    if (coordinates[--spike0] != xo) equalityMask &= ~1;
-                    if (equalityMask == 0) {
-                        spike0 += PolylineBuffer.DIMENSION;
-                        break;
-                    }
-                }
-            }
-            /*
-             * Here we have range of indices where the polygon has a width or height of zero.
-             * Search for a common point, then truncate at that point. Indices are like below:
-             *
-             *     0       spike0    lower          spike1         upper
-             *     ●────●────●────●────●────●────●────●────●────●────●
-             *                    └╌╌╌╌remove╌╌╌╌┘
-             * where:
-             *  - `lower` and `spike0` are inclusive.
-             *  - `upper` and `spike1` are exclusive.
-             *  - the region to remove are sowewhere between `spike0` and `spike1`.
-             */
-            final int limit = spike1;
-            int base;
-            while ((base = spike0 + 2*PolylineBuffer.DIMENSION) < limit) {    // Spikes exist only with at least 3 points.
-                final double xo = coordinates[spike0++];
-                final double yo = coordinates[spike0++];
-                spike1 = limit;
-                do {
-                    if (coordinates[spike1 - 2] == xo && coordinates[spike1 - 1] == yo) {
-                        /*
-                         * Remove points between the common point (xo,yo). The common point is kept on the
-                         * left side (`spike0` is already after that point) and removed on the right side.
-                         */
-                        System.arraycopy(coordinates, spike1, coordinates, spike0, upper - spike1);
-                        return upper - (spike1 - spike0);
-                    }
-                } while ((spike1 -= PolylineBuffer.DIMENSION) > base);
-            }
-            return upper;       // Nothing to remove.
-        }
-
-        /**
-         * Applies user-specified coordinate transform on all points of the whole polyline.
-         * This method is invoked after {@link #filterChunk(double[], int, int)}.
-         */
-        @Override
-        protected int filterFull(final double[] coordinates, final int upper) throws TransformException {
-            if (gridToCRS != null) {
-                gridToCRS.transform(coordinates, 0, coordinates, 0, upper / PolylineBuffer.DIMENSION);
-            }
-            return upper;
-        }
-    }
-
     /**
      * Writes all given polylines to the specified path builder. Null {@code PolylineBuffer} instances are ignored.
      * {@code PolylineBuffer} instances at even index are written with their points in reverse order.


[sis] 03/03: Try to show more debugging information during isoline generation.

Posted by de...@apache.org.
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 50f768333dd74e6bcfb386ec7f37acc6998bb0ce
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Aug 15 00:25:28 2022 +0200

    Try to show more debugging information during isoline generation.
---
 .../sis/internal/feature/j2d/MultiPolylines.java   | 13 +++++++-
 .../apache/sis/internal/feature/j2d/Polyline.java  | 13 +++++++-
 .../sis/internal/feature/j2d/package-info.java     |  2 +-
 .../processing/isoline/PolylineBuffer.java         |  9 +-----
 .../internal/processing/isoline/PolylineStage.java | 36 +++++++++++++++++++++-
 .../sis/internal/processing/isoline/Tracer.java    |  1 +
 .../internal/processing/isoline/StepsViewer.java   | 23 ++++++++------
 7 files changed, 75 insertions(+), 22 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
index aea1b7ae40..1139819964 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
@@ -23,6 +23,7 @@ import java.awt.geom.Rectangle2D;
 import java.awt.geom.PathIterator;
 import java.awt.geom.AffineTransform;
 import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+import org.apache.sis.util.Classes;
 
 
 /**
@@ -36,7 +37,7 @@ import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -178,4 +179,14 @@ final class MultiPolylines extends FlatShape {
         }
         return null;
     }
+
+    /**
+     * Returns a string representation for debugging purposes.
+     *
+     * @return a debug string representation.
+     */
+    @Override
+    public String toString() {
+        return Classes.getShortClassName(this) + '[' + (polylines.length / 2) + " polylines]";
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
index 2272236471..5b9c8acc5d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
@@ -24,6 +24,7 @@ import java.awt.geom.Rectangle2D;
 import java.awt.geom.PathIterator;
 import java.awt.geom.AffineTransform;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.Classes;
 
 
 /**
@@ -57,7 +58,7 @@ import org.apache.sis.internal.util.Numerics;
  * </ol>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -304,4 +305,14 @@ class Polyline extends FlatShape {
             return (position == 0) ? SEG_MOVETO : SEG_LINETO;
         }
     }
+
+    /**
+     * Returns a string representation for debugging purposes.
+     *
+     * @return a debug string representation.
+     */
+    @Override
+    public String toString() {
+        return Classes.getShortClassName(this) + '[' + (coordinates.length / 2) + " points]";
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java
index d7ee3c90d5..f88948692d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java
@@ -25,7 +25,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see org.apache.sis.internal.referencing.j2d
  *
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 be7b42e4f2..d7c1687462 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
@@ -205,13 +205,6 @@ final class PolylineBuffer {
      */
     @Debug
     final void toRawPath(final Map<PolylineStage,Path2D> appendTo) {
-        int i = 0;
-        if (i < size) {
-            final Path2D p = PolylineStage.BUFFER.destination(appendTo);
-            p.moveTo(coordinates[i++], coordinates[i++]);
-            while (i < size) {
-                p.lineTo(coordinates[i++], coordinates[i++]);
-            }
-        }
+        PolylineStage.BUFFER.add(appendTo, coordinates, size);
     }
 }
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
index 7ad1733809..477aa3a583 100644
--- 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
@@ -64,10 +64,44 @@ enum PolylineStage {
      * @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) {
+    private Path2D destination(final Map<PolylineStage,Path2D> appendTo) {
         return appendTo.computeIfAbsent(this, (k) -> new Path2D.Float());
     }
 
+    /**
+     * Adds coordinates to the specified map.
+     *
+     * @param  appendTo     where to append the coordinates.
+     * @param  coordinates  (x,y) tuples to append, starting with the coordinate at index 0.
+     * @param  size         number of coordinates to add (twice the number of tuples).
+     */
+    final void add(final Map<PolylineStage,Path2D> appendTo, final double[] coordinates, final int size) {
+        int i = 0;
+        if (i < size) {
+            final Path2D p = destination(appendTo);
+            p.moveTo(coordinates[i++], coordinates[i++]);
+            while (i < size) {
+                p.lineTo(coordinates[i++], coordinates[i++]);
+            }
+        }
+    }
+
+    /**
+     * Adds polylines in the values of the given map. Keys are ignored.
+     *
+     * @param  appendTo      where to append the coordinates.
+     * @param  partialPaths  map of polylines to add.
+     */
+    final void add(final Map<PolylineStage,Path2D> appendTo, final Map<?,Fragments> partialPaths) {
+        for (final Fragments fragment : partialPaths.values()) {
+            for (final double[] coordinates : fragment) {
+                if (coordinates != null) {
+                    add(appendTo, coordinates, coordinates.length);
+                }
+            }
+        }
+    }
+
     /**
      * Adds polylines to the specified map.
      *
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 d6e3e6cfc6..e88944a163 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
@@ -695,6 +695,7 @@ final class Tracer {
         @Debug
         final void toRawPath(final Map<PolylineStage,Path2D> appendTo) {
             PolylineStage.FINAL.add(appendTo, (path != null) ? path.snapshot() : shape);
+            PolylineStage.FRAGMENT.add(appendTo, partialPaths);
             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 4f540210bf..ce40d28571 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
@@ -145,7 +145,7 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,I
     @SuppressWarnings("ThisEscapedInObjectConstruction")
     private StepsViewer(final RenderedImage data, final Container pane) {
         isolines    = new EnumMap<>(PolylineStage.class);
-        stageColors = new Color[] {Color.YELLOW, Color.CYAN, Color.GRAY};
+        stageColors = new Color[] {Color.YELLOW, Color.CYAN, Color.LIGHT_GRAY};
         setBackground(Color.BLACK);
         setOpaque(true);
         final double scaleX = (CANVAS_WIDTH  - 2*PADDING) / (double) data.getWidth();
@@ -215,9 +215,9 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,I
             gh.draw(bounds);
         }
         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]);
+            final PolylineStage stage = entry.getKey();
+            gh.setStroke(new BasicStroke(stage == PolylineStage.BUFFER ? 2 : 0));
+            gh.setColor(stageColors[stage.ordinal()]);
             gh.draw(entry.getValue());
         }
     }
@@ -286,13 +286,16 @@ public final class StepsViewer extends JComponent implements BiConsumer<String,I
                         isolines.remove(stage);
                     } else {
                         isolines.put(stage, update);
-                        if (stage == PolylineStage.BUFFER) {
+                        if (b == null) {
                             b = update.getBounds();
-                            b.x      -= PADDING;
-                            b.y      -= PADDING;
-                            b.width  += PADDING * 2;
-                            b.height += PADDING * 2;
-                            bounds = b;
+                            if (b.isEmpty()) {
+                                b = null;
+                            } else {
+                                b.x      -= PADDING;
+                                b.y      -= PADDING;
+                                b.width  += PADDING * 2;
+                                b.height += PADDING * 2;
+                            }
                         }
                     }
                 }


[sis] 02/03: When debugging isoline generations using `StepsViewer`, use different colors for polylines at different stages.

Posted by de...@apache.org.
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 <ma...@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()) {