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/13 21:39:54 UTC

[sis] branch geoapi-4.0 updated (357e4d946b -> d81b368aef)

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 357e4d946b Add an `IsolineViewer` widget for watching isoline generation step-by-step. For debugging purposes only.
     new 2cb6525c48 Move isoline generation code to its own package.
     new 22fb84d8e4 Move isoline `Tracer.Polyline` to a top-level class `PolylineBuffer`. There is no code change other than this move and documentation updates.
     new 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.

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:
 .../apache/sis/gui/coverage/CoverageCanvas.java    |   2 +-
 .../apache/sis/gui/coverage/IsolineRenderer.java   |   2 +-
 .../sis/gui/coverage/StyledRenderingData.java      |   2 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |   2 +-
 .../internal/processing/image/TiledProcess.java    |   6 +-
 .../sis/internal/processing/isoline/Fragments.java | 270 ++++++++++
 .../processing/{image => isoline}/Isolines.java    |  51 +-
 .../processing/isoline/PolylineBuffer.java         | 215 ++++++++
 .../IsolineTracer.java => isoline/Tracer.java}     | 585 +++------------------
 .../{image => isoline}/package-info.java           |   4 +-
 .../org/apache/sis/image/ImageProcessorTest.java   |   4 +-
 .../{image => isoline}/IsolinesTest.java           |   2 +-
 .../StepsViewer.java}                              |  12 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   2 +-
 14 files changed, 608 insertions(+), 551 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Fragments.java
 rename core/sis-feature/src/main/java/org/apache/sis/internal/processing/{image => isoline}/Isolines.java (92%)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineBuffer.java
 rename core/sis-feature/src/main/java/org/apache/sis/internal/processing/{image/IsolineTracer.java => isoline/Tracer.java} (57%)
 copy core/sis-feature/src/main/java/org/apache/sis/internal/processing/{image => isoline}/package-info.java (93%)
 rename core/sis-feature/src/test/java/org/apache/sis/internal/processing/{image => isoline}/IsolinesTest.java (99%)
 rename core/sis-feature/src/test/java/org/apache/sis/internal/processing/{image/IsolineViewer.java => isoline/StepsViewer.java} (96%)


[sis] 03/03: Move the `Unclosed` inner class to a top-level class, renamed `Fragments`. There is no code change (other than move/renaming) 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 d81b368aef7ff466c09a9e4f1ac84af652a036c5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Aug 13 23:34:15 2022 +0200

    Move the `Unclosed` inner class to a top-level class, renamed `Fragments`.
    There is no code change (other than move/renaming) in this commit.
---
 .../sis/internal/processing/isoline/Fragments.java | 270 ++++++++++++++++++++
 .../processing/isoline/PolylineBuffer.java         |   2 +-
 .../sis/internal/processing/isoline/Tracer.java    | 272 ++-------------------
 3 files changed, 285 insertions(+), 259 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
new file mode 100644
index 0000000000..da97430340
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Fragments.java
@@ -0,0 +1,270 @@
+/*
+ * 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.awt.Point;
+import java.util.Map;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import org.apache.sis.internal.util.Numerics;
+
+
+/**
+ * List of {@code PolylineBuffer} coordinates that have not yet been closed.
+ * Each {@code double[]} in this list is a copy of a {@link PolylineBuffer} used by {@link Tracer.Level}.
+ * Those copies are performed for saving data before they are overwritten by next iterated cell.
+ *
+ * <h2>List indices and ordering of points</h2>
+ * For a given {@code Fragments} list, all {@code double[]} arrays at even indices shall have their points read
+ * in reverse order and all {@code double[]} arrays at odd indices shall have their points read in forward order.
+ * The list size must be even and the list may contain null elements when there is no data in the corresponding
+ * iteration order. This convention makes easy to reverse the order of all points, simply by reversing the order
+ * 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
+ * @module
+ */
+@SuppressWarnings({"CloneableImplementsClone", "serial"})           // Not intended to be cloned or serialized.
+final class Fragments extends ArrayList<double[]> {
+    /**
+     * The first points and last point in this list of polylines. By convention the coordinate having fraction
+     * digits has all its bits inverted by the {@code ~} operator. May be {@code null} if a coordinate is NaN.
+     * Do not modify {@link Point} field values, because those instances are keys in {@link Tracer.Level#partialPaths}.
+     */
+    private Point firstPoint, lastPoint;
+
+    /**
+     * Creates a list of polylines initialized to the given items.
+     * The given polylines and their opposite directions are cleared by this method.
+     *
+     * @param  polylineOnLeft  first polyline with points in forward order. Shall not be null.
+     * @param  polylineOnTop    next polyline with points in reverse order, or {@code null} if none.
+     */
+    Fragments(final PolylineBuffer polylineOnLeft, final PolylineBuffer polylineOnTop) {
+        /*
+         * Search for first and last point by inspecting `PolylineBuffer` instances in the order shown below.
+         * The first 4 rows and the last 4 rows search for first point and last point respectively.
+         * The empty rows in the middle are an intentional gap for creating a regular pattern that
+         * we can exploit for 3 decisions that need to be done during the loop:
+         *
+         *     ✓ (index & 2) = 0    if using `polylineOnLeft` (otherwise `polylineOnTop`).
+         *     ✓ (index % 3) = 0    if using `opposite` value of polyline (may be null).
+         *     ✓ (index & 1) = 0    if fetching last point (otherwise fetch first point).
+         *
+         *  Index   PolylineBuffer        (order) !(i & 2)  !(i % 3)  !(i & 1)   Comment
+         *  ────────────────────────────────────────────────────────────────────────────
+         *   [0]    polylineOnLeft.opposite  (←)      ✓         ✓         ✓        (1)
+         *   [1]    polylineOnLeft           (→)      ✓                            (2)
+         *   [2]    polylineOnTop            (←)                          ✓        (1)
+         *   [3]    polylineOnTop.opposite   (→)                ✓                  (2)
+         *   [4]                                      ✓                   ✓
+         *   |5]                                      ✓
+         *   [6]    polylineOnTop.opposite   (→)                ✓         ✓        (3)
+         *   [7]    polylineOnTop            (←)                                   (4)
+         *   [8]    polylineOnLeft           (→)      ✓                   ✓        (3)
+         *   [9]    polylineOnLeft.opposite  (←)      ✓         ✓                  (4)
+         *
+         * Comments:
+         *   (1) Last  `PolylineBuffer` point is first `Fragments` point because of reverse iteration order.
+         *   (2) First `PolylineBuffer` point is first `Fragments` point because of forward iteration order.
+         *   (3) Last  `PolylineBuffer` point is last  `Fragments` point because of forward iteration order.
+         *   (4) First `PolylineBuffer` point is last  `Fragments` point because of reverse iteration order.
+         */
+        int index = 0;
+        do {
+            PolylineBuffer polyline = ((index & 2) == 0) ? polylineOnLeft : polylineOnTop;  // See above table (column 4).
+            if (index % 3 == 0 && polyline != null) polyline = polyline.opposite;           // See above table (column 5).
+            if (polyline != null) {
+                int n = polyline.size;
+                if (n != 0) {
+                    final double[] coordinates = polyline.coordinates;
+                    final double x, y;
+                    if (((index & 1) == 0)) {                          // See above table in comment (column 6).
+                        y = coordinates[--n];
+                        x = coordinates[--n];
+                    } else {
+                        x = coordinates[0];
+                        y = coordinates[1];
+                    }
+                    final boolean isLastPoint = (index >= 6);          // See row [6] in above table.
+                    if (Double.isFinite(x) && Double.isFinite(y)) {
+                        final Point p = new Point((int) x, (int) y);
+                        if (!Numerics.isInteger(x)) p.x = ~p.x;
+                        if (!Numerics.isInteger(y)) p.y = ~p.y;
+                        if (isLastPoint) {
+                            lastPoint = p;
+                            break;                                     // Done searching both points.
+                        }
+                        firstPoint = p;
+                    } else if (isLastPoint) {
+                        /*
+                         * If the last point was NaN, check if it was also the case of first point.
+                         * If yes, we will not be able to store this `Fragments` in `partialPaths`
+                         * because we have no point that we can use as key (it would be pointless
+                         * to search for another point further in the `coordinates` array because
+                         * that point could never be matched with another `Fragments`). Leave this
+                         * list empty for avoiding the copies done by `take(…)` calls. Instead,
+                         * callers should write polylines in `Tracer.Level.path` immediately.
+                         */
+                        if (firstPoint == null) return;
+                        break;
+                    }
+                    /*
+                     * Done searching the first point (may still be null if that point is NaN).
+                     * Row [6] in above table is the first row for the search of last point.
+                     */
+                    index = 6;
+                    continue;
+                }
+            }
+            if (++index == 4) {
+                // Found no non-empty polylines during search for first point. No need to continue searching.
+                return;
+            }
+        } while (index <= 9);
+        /*
+         * Copies coordinates only if at least one of `firstPoint` or `lastPoint` is a valid point.
+         */
+        take(polylineOnLeft.opposite);          // Point will be iterated in reverse order.
+        take(polylineOnLeft);                   // Point will be iterated in forward order.
+        if (polylineOnTop != null) {
+            PolylineBuffer suffix = polylineOnTop.opposite;
+            take(polylineOnTop);                // Inverse order. Set `polylineOnTop.opposite` to null.
+            take(suffix);                       // Forward order.
+        }
+    }
+
+    /**
+     * Takes a copy of coordinate values of given polyline, then clears that polyline.
+     */
+    private void take(final PolylineBuffer polyline) {
+        if (polyline != null && polyline.size != 0) {
+            add(Arrays.copyOf(polyline.coordinates, polyline.size));
+            polyline.clear();
+        } else {
+            add(null);                  // No data for iteration order at this position.
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given point is equal to the start point or end point.
+     * This is used in assertions for checking key validity in {@link Tracer.Level#partialPaths}.
+     */
+    final boolean isExtremity(final Point key) {
+        return key.equals(firstPoint) || key.equals(lastPoint);
+    }
+
+    /**
+     * Associates this polyline to its two extremities in the given map. If other polylines already exist
+     * for one or both extremities, then this polyline will be merged with previously existing polylines.
+     * This method returns {@code true} if the polyline has been closed, in which case caller should store
+     * the coordinates in {@link Tracer.Level#path} immediately.
+     *
+     * @param  partialPaths  where to add or merge polylines.
+     * @return {@code true} if this polyline became a closed polygon as a result of merge operation.
+     */
+    final boolean addOrMerge(final Map<Point,Fragments> partialPaths) {
+        final Fragments before = partialPaths.remove(firstPoint);
+        final Fragments after  = partialPaths.remove(lastPoint);
+        if (before != null) partialPaths.remove(addAll(before, true));
+        if (after  != null) partialPaths.remove(addAll(after, false));
+        if (firstPoint != null && firstPoint.equals(lastPoint)) {       // First/last points may have changed.
+            partialPaths.remove(firstPoint);
+            partialPaths.remove(lastPoint);
+            return true;
+        } else {
+            // Intentionally replace previous values.
+            if (firstPoint != null) partialPaths.put(firstPoint, this);
+            if (lastPoint  != null) partialPaths.put(lastPoint,  this);
+            return false;
+        }
+    }
+
+    /**
+     * Prepends or appends the given polylines to this list of polylines.
+     * Points order will be changed as needed in order to match extremities.
+     * The {@code other} instance should be forgotten after this method call.
+     *
+     * @param  other    the other polyline to append or prepend to this polyline.
+     * @param  prepend  {@code true} for prepend operation, {@code false} for append.
+     * @return extremity of {@code other} which has not been assigned to {@code this}.
+     */
+    private Point addAll(final Fragments other, final boolean prepend) {
+        assert ((size() | other.size()) & 1) == 0;      // Must have even number of elements in both lists.
+        /*
+         * In figures below, ● are the extremities to attach together.
+         * `r` is a bitmask telling which polylines to reverse:
+         * 1=this, 2=other, together with combinations 0=none and 3=other.
+         */
+        int r; if ( lastPoint != null &&  lastPoint.equals(other.firstPoint)) r = 0;    // ○──────● ●──────○
+        else   if (firstPoint != null && firstPoint.equals(other.firstPoint)) r = 1;    // ●──────○ ●──────○
+        else   if ( lastPoint != null &&  lastPoint.equals(other. lastPoint)) r = 2;    // ○──────● ○──────●
+        else   if (firstPoint != null && firstPoint.equals(other. lastPoint)) r = 3;    // ●──────○ ○──────●
+        else {
+            // Should never happen because `other` has been obtained using a point of `this`.
+            throw new AssertionError();
+        }
+        if (prepend) r ^= 3;                      // Swap order in above  ○──○ ○──○  figures.
+        if ((r & 1) != 0)  this.reverse();
+        if ((r & 2) != 0) other.reverse();
+        if (prepend) {
+            addAll(0, other);
+            firstPoint = other.firstPoint;
+            return other.lastPoint;
+        } else {
+            addAll(other);
+            lastPoint = other.lastPoint;
+            return other.firstPoint;
+        }
+    }
+
+    /**
+     * Reverse the order of all points. The last polyline will become the first polyline and vice-versa.
+     * For each polyline, points will be iterated in opposite order. The trick on point order is done by
+     * moving polylines at even indices to odd indices, and conversely (see class javadoc for convention
+     * about even/odd indices).
+     */
+    private void reverse() {
+        Collections.reverse(this);
+        final Point swap = firstPoint;
+        firstPoint = lastPoint;
+        lastPoint = swap;
+    }
+
+    /**
+     * Returns the content of this list as an array of {@link PolylineBuffer} instances.
+     * {@code PolylineBuffer} instances at even index should be written with their points in reverse order.
+     *
+     * @see #writeTo(Joiner, PolylineBuffer[], boolean)
+     */
+    final PolylineBuffer[] toPolylines() {
+        final PolylineBuffer[] polylines = new PolylineBuffer[size()];
+        for (int i=0; i<polylines.length; i++) {
+            final double[] coordinates = get(i);
+            if (coordinates != null) {
+                polylines[i] = new PolylineBuffer(coordinates);
+            }
+        }
+        return polylines;
+    }
+}
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 384c765a17..5da2768203 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
@@ -70,7 +70,7 @@ final class PolylineBuffer {
     }
 
     /**
-     * Creates a new polyline wrapping the given coordinates. Used for wrapping {@link Unclosed}
+     * Creates a new polyline wrapping the given coordinates. Used for wrapping {@link Fragments}
      * instances in objects expected by {@link Tracer#writeTo(Joiner, Polyline[], boolean)}.
      * Those {@code Polyline} instances are temporary.
      */
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 c7b4bb564e..17c777e460 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
@@ -16,12 +16,9 @@
  */
 package org.apache.sis.internal.processing.isoline;
 
-import java.util.Arrays;
-import java.util.ArrayList;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
-import java.util.Collections;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.Shape;
@@ -29,7 +26,6 @@ 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.Debug;
 
 
@@ -215,14 +211,14 @@ final class Tracer {
          * by the {@code ~} operator. For each point, there is at most one coordinate having such fraction digits.
          *
          * <h4>Map values</h4>
-         * {@code Unclosed} instances are list of {@code double[]} arrays to be concatenated in a single polygon later.
-         * For a given {@code Unclosed} list, all {@code double[]} arrays at even indices shall have their points read
+         * {@code Fragments} instances are list of {@code double[]} arrays to be concatenated in a single polygon later.
+         * For a given {@code Fragments} list, all {@code double[]} arrays at even indices shall have their points read
          * in reverse order and all {@code double[]} arrays at odd indices shall have their points read in forward order.
          * The list may contain null elements when there is no data in the corresponding iteration order.
          *
          * @see #closeLeftWithTop(PolylineBuffer)
          */
-        private final Map<Point,Unclosed> partialPaths;
+        private final Map<Point,Fragments> partialPaths;
 
         /**
          * Builder of isolines as a Java2D shape, created when first needed.
@@ -539,7 +535,7 @@ final class Tracer {
                  * Joining left and top polylines do not yet create a closed shape. Consequently we may not write
                  * in the `path` now. But maybe we can close the polygon later after more polylines are attached.
                  */
-                final Unclosed fragment = new Unclosed(polylineOnLeft, polylineOnTop);
+                final Fragments fragment = new Fragments(polylineOnLeft, polylineOnTop);
                 if (fragment.isEmpty()) {
                     /*
                      * Fragment starts and ends with NaN values. We will not be able to complete a polygon.
@@ -566,8 +562,8 @@ final class Tracer {
          * Writes the content of given polyline without closing it as a polygon.
          * The given polyline will become empty after this method call.
          */
-        private void writeUnclosed(final PolylineBuffer polyline) throws TransformException {
-            final Unclosed fragment = new Unclosed(polyline, null);
+        private void writeFragment(final PolylineBuffer polyline) throws TransformException {
+            final Fragments fragment = new Fragments(polyline, null);
             final PolylineBuffer[] polylines;
             final boolean close;
             if (fragment.isEmpty()) {
@@ -591,7 +587,7 @@ final class Tracer {
          */
         final void finishedRow() throws TransformException {
             if (!polylineOnLeft.transferToOpposite()) {
-                writeUnclosed(polylineOnLeft);
+                writeFragment(polylineOnLeft);
             }
             isDataAbove = 0;
         }
@@ -611,7 +607,7 @@ final class Tracer {
              * reading the `shape` field.
              */
             for (int i=0; i < polylinesOnTop.length; i++) {
-                writeUnclosed(polylinesOnTop[i]);
+                writeFragment(polylinesOnTop[i]);
                 polylinesOnTop[i] = null;
             }
             assert isConsistent();
@@ -621,7 +617,7 @@ final class Tracer {
          * Verifies that {@link #partialPaths} consistency. Used for assertions only.
          */
         private boolean isConsistent() {
-            for (final Map.Entry<Point,Unclosed> entry : partialPaths.entrySet()) {
+            for (final Map.Entry<Point,Fragments> entry : partialPaths.entrySet()) {
                 if (!entry.getValue().isExtremity(entry.getKey())) return false;
             }
             return true;
@@ -645,9 +641,9 @@ final class Tracer {
             other.path = null;
             assert  this.isConsistent();
             assert other.isConsistent();
-            final IdentityHashMap<Unclosed,Boolean> done = new IdentityHashMap<>(other.partialPaths.size() / 2);
-            for (final Map.Entry<Point,Unclosed> entry : other.partialPaths.entrySet()) {
-                final Unclosed fragment = entry.getValue();
+            final IdentityHashMap<Fragments,Boolean> done = new IdentityHashMap<>(other.partialPaths.size() / 2);
+            for (final Map.Entry<Point,Fragments> entry : other.partialPaths.entrySet()) {
+                final Fragments fragment = entry.getValue();
                 if (done.put(fragment, Boolean.TRUE) == null) {
                     assert fragment.isExtremity(entry.getKey());
                     if (fragment.addOrMerge(partialPaths)) {
@@ -667,8 +663,8 @@ final class Tracer {
          * @throws TransformException if an error occurred during polylines creation.
          */
         final void flush() throws TransformException {
-            for (final Map.Entry<Point,Unclosed> entry : partialPaths.entrySet()) {
-                final Unclosed fragment = entry.getValue();
+            for (final Map.Entry<Point,Fragments> entry : partialPaths.entrySet()) {
+                final Fragments fragment = entry.getValue();
                 assert fragment.isExtremity(entry.getKey());
                 if (!fragment.isEmpty()) {
                     path = writeTo(path, fragment.toPolylines(), false);
@@ -702,246 +698,6 @@ final class Tracer {
         }
     }
 
-    /**
-     * List of {@code PolylineBuffer} coordinates that have not yet been closed. Each {@code double[]} in this list is
-     * a copy of a {@link PolylineBuffer} used by {@link Level}. Those copies are performed for saving data before they
-     * are overwritten by next iterated cell.
-     *
-     * <h2>List indices and ordering of points</h2>
-     * For a given {@code Unclosed} list, all {@code double[]} arrays at even indices shall have their points read
-     * in reverse order and all {@code double[]} arrays at odd indices shall have their points read in forward order.
-     * The list size must be even and the list may contain null elements when there is no data in the corresponding
-     * iteration order. This convention makes easy to reverse the order of all points, simply by reversing the order
-     * 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 Level#partialPaths
-     */
-    @SuppressWarnings({"CloneableImplementsClone", "serial"})           // Not intended to be cloned or serialized.
-    private static final class Unclosed extends ArrayList<double[]> {
-        /**
-         * The first points and last point in this list of polylines. By convention the coordinate having fraction
-         * digits has all its bits inverted by the {@code ~} operator. May be {@code null} if a coordinate is NaN.
-         * Do not modify {@link Point} field values, because those instances are keys in {@link Level#partialPaths}.
-         */
-        private Point firstPoint, lastPoint;
-
-        /**
-         * Creates a list of polylines initialized to the given items.
-         * The given polylines and their opposite directions are cleared by this method.
-         *
-         * @param  polylineOnLeft  first polyline with points in forward order. Shall not be null.
-         * @param  polylineOnTop    next polyline with points in reverse order, or {@code null} if none.
-         */
-        Unclosed(final PolylineBuffer polylineOnLeft, final PolylineBuffer polylineOnTop) {
-            /*
-             * Search for first and last point by inspecting `PolylineBuffer` instances in the order shown below.
-             * The first 4 rows and the last 4 rows search for first point and last point respectively.
-             * The empty rows in the middle are an intentional gap for creating a regular pattern that
-             * we can exploit for 3 decisions that need to be done during the loop:
-             *
-             *     ✓ (index & 2) = 0    if using `polylineOnLeft` (otherwise `polylineOnTop`).
-             *     ✓ (index % 3) = 0    if using `opposite` value of polyline (may be null).
-             *     ✓ (index & 1) = 0    if fetching last point (otherwise fetch first point).
-             *
-             *  Index   PolylineBuffer        (order) !(i & 2)  !(i % 3)  !(i & 1)   Comment
-             *  ────────────────────────────────────────────────────────────────────────────
-             *   [0]    polylineOnLeft.opposite  (←)      ✓         ✓         ✓        (1)
-             *   [1]    polylineOnLeft           (→)      ✓                            (2)
-             *   [2]    polylineOnTop            (←)                          ✓        (1)
-             *   [3]    polylineOnTop.opposite   (→)                ✓                  (2)
-             *   [4]                                      ✓                   ✓
-             *   |5]                                      ✓
-             *   [6]    polylineOnTop.opposite   (→)                ✓         ✓        (3)
-             *   [7]    polylineOnTop            (←)                                   (4)
-             *   [8]    polylineOnLeft           (→)      ✓                   ✓        (3)
-             *   [9]    polylineOnLeft.opposite  (←)      ✓         ✓                  (4)
-             *
-             * Comments:
-             *   (1) Last  `PolylineBuffer` point is first `Unclosed` point because of reverse iteration order.
-             *   (2) First `PolylineBuffer` point is first `Unclosed` point because of forward iteration order.
-             *   (3) Last  `PolylineBuffer` point is last  `Unclosed` point because of forward iteration order.
-             *   (4) First `PolylineBuffer` point is last  `Unclosed` point because of reverse iteration order.
-             */
-            int index = 0;
-            do {
-                PolylineBuffer polyline = ((index & 2) == 0) ? polylineOnLeft : polylineOnTop;  // See above table (column 4).
-                if (index % 3 == 0 && polyline != null) polyline = polyline.opposite;           // See above table (column 5).
-                if (polyline != null) {
-                    int n = polyline.size;
-                    if (n != 0) {
-                        final double[] coordinates = polyline.coordinates;
-                        final double x, y;
-                        if (((index & 1) == 0)) {                          // See above table in comment (column 6).
-                            y = coordinates[--n];
-                            x = coordinates[--n];
-                        } else {
-                            x = coordinates[0];
-                            y = coordinates[1];
-                        }
-                        final boolean isLastPoint = (index >= 6);          // See row [6] in above table.
-                        if (Double.isFinite(x) && Double.isFinite(y)) {
-                            final Point p = new Point((int) x, (int) y);
-                            if (!Numerics.isInteger(x)) p.x = ~p.x;
-                            if (!Numerics.isInteger(y)) p.y = ~p.y;
-                            if (isLastPoint) {
-                                lastPoint = p;
-                                break;                                     // Done searching both points.
-                            }
-                            firstPoint = p;
-                        } else if (isLastPoint) {
-                            /*
-                             * If the last point was NaN, check if it was also the case of first point.
-                             * If yes, we will not be able to store this `Unclosed` in `partialPaths`
-                             * because we have no point that we can use as key (it would be pointless
-                             * to search for another point further in the `coordinates` array because
-                             * that point could never be matched with another `Unclosed`). Leave this
-                             * list empty for avoiding the copies done by `take(…)` calls. Instead,
-                             * callers should write polylines in `Level.path` immediately.
-                             */
-                            if (firstPoint == null) return;
-                            break;
-                        }
-                        /*
-                         * Done searching the first point (may still be null if that point is NaN).
-                         * Row [6] in above table is the first row for the search of last point.
-                         */
-                        index = 6;
-                        continue;
-                    }
-                }
-                if (++index == 4) {
-                    // Found no non-empty polylines during search for first point. No need to continue searching.
-                    return;
-                }
-            } while (index <= 9);
-            /*
-             * Copies coordinates only if at least one of `firstPoint` or `lastPoint` is a valid point.
-             */
-            take(polylineOnLeft.opposite);          // Point will be iterated in reverse order.
-            take(polylineOnLeft);                   // Point will be iterated in forward order.
-            if (polylineOnTop != null) {
-                PolylineBuffer suffix = polylineOnTop.opposite;
-                take(polylineOnTop);                // Inverse order. Set `polylineOnTop.opposite` to null.
-                take(suffix);                       // Forward order.
-            }
-        }
-
-        /**
-         * Takes a copy of coordinate values of given polyline, then clears that polyline.
-         */
-        private void take(final PolylineBuffer polyline) {
-            if (polyline != null && polyline.size != 0) {
-                add(Arrays.copyOf(polyline.coordinates, polyline.size));
-                polyline.clear();
-            } else {
-                add(null);                  // No data for iteration order at this position.
-            }
-        }
-
-        /**
-         * Returns {@code true} if the given point is equal to the start point or end point.
-         * This is used in assertions for checking key validity in {@link Level#partialPaths}.
-         */
-        final boolean isExtremity(final Point key) {
-            return key.equals(firstPoint) || key.equals(lastPoint);
-        }
-
-        /**
-         * Associates this polyline to its two extremities in the given map. If other polylines already exist
-         * for one or both extremities, then this polyline will be merged with previously existing polylines.
-         * This method returns {@code true} if the polyline has been closed, in which case caller should store
-         * the coordinates in {@link Level#path} immediately.
-         *
-         * @param  partialPaths  where to add or merge polylines.
-         * @return {@code true} if this polyline became a closed polygon as a result of merge operation.
-         */
-        final boolean addOrMerge(final Map<Point,Unclosed> partialPaths) {
-            final Unclosed before = partialPaths.remove(firstPoint);
-            final Unclosed after  = partialPaths.remove(lastPoint);
-            if (before != null) partialPaths.remove(addAll(before, true));
-            if (after  != null) partialPaths.remove(addAll(after, false));
-            if (firstPoint != null && firstPoint.equals(lastPoint)) {       // First/last points may have changed.
-                partialPaths.remove(firstPoint);
-                partialPaths.remove(lastPoint);
-                return true;
-            } else {
-                // Intentionally replace previous values.
-                if (firstPoint != null) partialPaths.put(firstPoint, this);
-                if (lastPoint  != null) partialPaths.put(lastPoint,  this);
-                return false;
-            }
-        }
-
-        /**
-         * Prepends or appends the given polylines to this list of polylines.
-         * Points order will be changed as needed in order to match extremities.
-         * The {@code other} instance should be forgotten after this method call.
-         *
-         * @param  other    the other polyline to append or prepend to this polyline.
-         * @param  prepend  {@code true} for prepend operation, {@code false} for append.
-         * @return extremity of {@code other} which has not been assigned to {@code this}.
-         */
-        private Point addAll(final Unclosed other, final boolean prepend) {
-            assert ((size() | other.size()) & 1) == 0;      // Must have even number of elements in both lists.
-            /*
-             * In figures below, ● are the extremities to attach together.
-             * `r` is a bitmask telling which polylines to reverse:
-             * 1=this, 2=other, together with combinations 0=none and 3=other.
-             */
-            int r; if ( lastPoint != null &&  lastPoint.equals(other.firstPoint)) r = 0;    // ○──────● ●──────○
-            else   if (firstPoint != null && firstPoint.equals(other.firstPoint)) r = 1;    // ●──────○ ●──────○
-            else   if ( lastPoint != null &&  lastPoint.equals(other. lastPoint)) r = 2;    // ○──────● ○──────●
-            else   if (firstPoint != null && firstPoint.equals(other. lastPoint)) r = 3;    // ●──────○ ○──────●
-            else {
-                // Should never happen because `other` has been obtained using a point of `this`.
-                throw new AssertionError();
-            }
-            if (prepend) r ^= 3;                      // Swap order in above  ○──○ ○──○  figures.
-            if ((r & 1) != 0)  this.reverse();
-            if ((r & 2) != 0) other.reverse();
-            if (prepend) {
-                addAll(0, other);
-                firstPoint = other.firstPoint;
-                return other.lastPoint;
-            } else {
-                addAll(other);
-                lastPoint = other.lastPoint;
-                return other.firstPoint;
-            }
-        }
-
-        /**
-         * Reverse the order of all points. The last polyline will become the first polyline and vice-versa.
-         * For each polyline, points will be iterated in opposite order. The trick on point order is done by
-         * moving polylines at even indices to odd indices, and conversely (see class javadoc for convention
-         * about even/odd indices).
-         */
-        private void reverse() {
-            Collections.reverse(this);
-            final Point swap = firstPoint;
-            firstPoint = lastPoint;
-            lastPoint = swap;
-        }
-
-        /**
-         * Returns the content of this list as an array of {@link PolylineBuffer} instances.
-         * {@code PolylineBuffer} instances at even index should be written with their points in reverse order.
-         *
-         * @see #writeTo(Joiner, PolylineBuffer[], boolean)
-         */
-        final PolylineBuffer[] toPolylines() {
-            final PolylineBuffer[] polylines = new PolylineBuffer[size()];
-            for (int i=0; i<polylines.length; i++) {
-                final double[] coordinates = get(i);
-                if (coordinates != null) {
-                    polylines[i] = new PolylineBuffer(coordinates);
-                }
-            }
-            return polylines;
-        }
-    }
-
     /**
      * 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,


[sis] 02/03: Move isoline `Tracer.Polyline` to a top-level class `PolylineBuffer`. There is no code change other than this move and documentation updates.

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 22fb84d8e4b98514ab99d677c1bcd386af9029a7
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Aug 13 10:41:31 2022 +0200

    Move isoline `Tracer.Polyline` to a top-level class `PolylineBuffer`.
    There is no code change other than this move and documentation updates.
---
 .../processing/isoline/PolylineBuffer.java         | 215 +++++++++++++
 .../sis/internal/processing/isoline/Tracer.java    | 343 +++++----------------
 2 files changed, 294 insertions(+), 264 deletions(-)

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
new file mode 100644
index 0000000000..384c765a17
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/PolylineBuffer.java
@@ -0,0 +1,215 @@
+/*
+ * 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.Arrays;
+import java.awt.geom.Path2D;
+import org.apache.sis.internal.feature.j2d.PathBuilder;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Debug;
+
+
+/**
+ * Coordinates of a polyline under construction. Coordinates can be appended in only one direction.
+ * If the polyline may growth on both directions (which happens if the polyline crosses the bottom
+ * side and the right side of a cell), then the two directions are handled by two distinct instances
+ * connected by their {@link #opposite} field.
+ *
+ * <p>When a polyline has been completed, its content is copied to {@link Tracer.Level#path}
+ * and the {@code PolylineBuffer} object is recycled for a new polyline.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.1
+ * @module
+ */
+final class PolylineBuffer {
+    /**
+     * Number of coordinates in a tuple.
+     */
+    static final int DIMENSION = 2;
+
+    /**
+     * Coordinates as (x,y) tuples. This array is expanded as needed.
+     */
+    double[] coordinates;
+
+    /**
+     * Number of valid elements in the {@link #coordinates} array.
+     * This is twice the number of points.
+     */
+    int size;
+
+    /**
+     * If the polyline has points added to its two extremities, the other extremity. Otherwise {@code null}.
+     * The first point of {@code opposite} polyline is connected to the first point of this polyline.
+     * Consequently when those two polylines are joined in a single polyline, the coordinates of either
+     * {@code this} or {@code opposite} must be iterated in reverse order.
+     */
+    PolylineBuffer opposite;
+
+    /**
+     * Creates an initially empty polyline.
+     */
+    PolylineBuffer() {
+        coordinates = ArraysExt.EMPTY_DOUBLE;
+    }
+
+    /**
+     * Creates a new polyline wrapping the given coordinates. Used for wrapping {@link Unclosed}
+     * instances in objects expected by {@link Tracer#writeTo(Joiner, Polyline[], boolean)}.
+     * Those {@code Polyline} instances are temporary.
+     */
+    PolylineBuffer(final double[] data) {
+        coordinates = data;
+        size = data.length;
+    }
+
+    /**
+     * Discards all coordinates in this polyline. This method does not clear
+     * the {@link #opposite} polyline; it is caller's responsibility to do so.
+     */
+    final void clear() {
+        opposite = null;
+        size = 0;
+    }
+
+    /**
+     * Returns whether this polyline is empty. This method is used only for {@code assert isEmpty()}
+     * statement because of the check for {@code opposite == null}: an empty polyline should not have
+     * a non-null {@link #opposite} value.
+     */
+    final boolean isEmpty() {
+        return size == 0 & (opposite == null);
+    }
+
+    /**
+     * Declares that the specified polyline will add points in the direction opposite to this polyline.
+     * This happens when the polyline crosses the bottom side and the right side of a cell (assuming an
+     * iteration from left to right and top to bottom).
+     *
+     * <p>This method is typically invoked in the following pattern (but this is not mandatory).
+     * An important aspect is that {@code this} and {@code other} should be on perpendicular axes:</p>
+     *
+     * {@preformat java
+     *     interpolateOnBottomSide(polylinesOnTop[x].attach(polylineOnLeft));
+     * }
+     *
+     * @return {@code this} for method calls chaining.
+     */
+    final PolylineBuffer attach(final PolylineBuffer other) {
+        assert (opposite == null) & (other.opposite == null);
+        other.opposite = this;
+        opposite = other;
+        return this;
+    }
+
+    /**
+     * Transfers all coordinates from given polylines to this polylines, in same order.
+     * This is used when polyline on the left side continues on bottom side,
+     * or conversely when polyline on the top side continues on right side.
+     * This polyline shall be empty before this method is invoked.
+     * The given source will become empty after this method returned.
+     *
+     * @param  source  the source from which to take data.
+     * @return {@code this} for method calls chaining.
+     */
+    final PolylineBuffer transferFrom(final PolylineBuffer source) {
+        assert isEmpty();
+        final double[] swap = coordinates;
+        coordinates = source.coordinates;
+        size        = source.size;
+        opposite    = source.opposite;
+        if (opposite != null) {
+            opposite.opposite = this;
+        }
+        source.clear();
+        source.coordinates = swap;
+        return this;
+    }
+
+    /**
+     * Transfers all coordinates from this polyline to the polyline going in opposite direction.
+     * This is used when this polyline reached the right image border, in which case its data
+     * will be lost if we do not copy them somewhere.
+     *
+     * @return {@code true} if coordinates have been transferred,
+     *         or {@code false} if there is no opposite direction.
+     */
+    final boolean transferToOpposite() {
+        if (opposite == null) {
+            return false;
+        }
+        final int sum = size + opposite.size;
+        double[] data = opposite.coordinates;
+        if (sum > data.length) {
+            data = new double[sum];
+        }
+        System.arraycopy(opposite.coordinates, 0, data, size, opposite.size);
+        for (int i=0, t=size; (t -= DIMENSION) >= 0;) {
+            data[t  ] = coordinates[i++];
+            data[t+1] = coordinates[i++];
+        }
+        opposite.size = sum;
+        opposite.coordinates = data;
+        opposite.opposite = null;
+        clear();
+        return true;
+    }
+
+    /**
+     * Appends given coordinates to this polyline.
+     *
+     * @param  x  first coordinate of the (x,y) tuple to add.
+     * @param  y  second coordinate of the (x,y) tuple to add.
+     */
+    final void append(final double x, final double y) {
+        if (size >= coordinates.length) {
+            coordinates = Arrays.copyOf(coordinates, Math.max(Math.multiplyExact(size, 2), 32));
+        }
+        coordinates[size++] = x;
+        coordinates[size++] = y;
+    }
+
+    /**
+     * Returns a string representation of this {@code Polyline} for debugging purposes.
+     */
+    @Override
+    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 Tracer.Level#toRawPath(Path2D)
+     */
+    @Debug
+    final 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/isoline/Tracer.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Tracer.java
index 3d6abbf6c2..c7b4bb564e 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
@@ -30,7 +30,6 @@ 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;
 
 
@@ -143,11 +142,11 @@ final class Tracer {
          *     (2)╌╌╌(3)
          * }
          *
-         * Bits are set to 1 where the data value is above the isoline {@linkplain #value}, and 0 where the data
-         * value is below the isoline value. Data values exactly equal to the isoline value are handled as if
-         * they were greater. It does not matter for interpolations: we could flip this convention randomly,
-         * the interpolated points would still the same. It could change the way line segments are assembled in a
-         * single {@link Polyline}, but the algorithm stay consistent if we always apply the same rule for all points.
+         * Bits are set to 1 where the data value is above the isoline {@linkplain #value}, and 0 where the data value
+         * is below the isoline value. Data values exactly equal to the isoline value are handled as if they were greater.
+         * It does not matter for interpolations: we could flip this convention randomly, the interpolated points would
+         * still be the same. It could change the way line segments are assembled in a single {@link PolylineBuffer},
+         * but the algorithm stay consistent if we always apply the same rule for all points.
          *
          * <h4>Reusing bits from previous iteration</h4>
          * We will iterate on pixels from left to right, then from top to bottom. With that iteration order,
@@ -172,7 +171,7 @@ final class Tracer {
          *     ○╌╌╌╌╌╌○             ╱●╌╌╌╌╱╌○              ○╌╌╌╌╌╌○╲
          * }
          *
-         * This field {@linkplain Polyline#isEmpty() is empty} if the cell in previous iteration was like below
+         * This field {@link PolylineBuffer#isEmpty() is empty} if the cell in previous iteration was like below
          * (no line cross the right border):
          *
          * {@preformat text
@@ -182,7 +181,7 @@ final class Tracer {
          *     ○╌╌╌╌╲╌●              ○╌╌╌┼╌╌●
          * }
          */
-        private final Polyline polylineOnLeft;
+        private final PolylineBuffer polylineOnLeft;
 
         /**
          * The polylines in each column which need to be continued on the next row.
@@ -202,13 +201,14 @@ final class Tracer {
          *     x coordinate of first pixel (upper-left corner)
          * }
          */
-        private final Polyline[] polylinesOnTop;
+        private final PolylineBuffer[] polylinesOnTop;
 
         /**
-         * Paths that have not yet been closed. The {@link Polyline} coordinates are copied in this map when iteration
-         * finished on a row and the polyline is not reused by next row, or when the {@link #closeLeftWithTop(Polyline)}
-         * method has been invoked but the geometry to close is still not complete. This map accumulates those partial
-         * shapes for assembling them later when missing parts become available.
+         * Paths that have not yet been closed. The {@link PolylineBuffer} coordinates are copied in this map when
+         * iteration finished on a row but the polyline under construction will not be continued by the next row,
+         * or when the {@link #closeLeftWithTop(PolylineBuffer)} method has been invoked but the geometry to close
+         * is still not complete. This map accumulates those partial shapes for assembling them later when missing
+         * parts become available.
          *
          * <h4>Map keys</h4>
          * Keys are grid coordinates rounded toward 0. The coordinate having fraction digits has its bits inverted
@@ -220,15 +220,15 @@ final class Tracer {
          * in reverse order and all {@code double[]} arrays at odd indices shall have their points read in forward order.
          * The list may contain null elements when there is no data in the corresponding iteration order.
          *
-         * @see #closeLeftWithTop(Polyline)
+         * @see #closeLeftWithTop(PolylineBuffer)
          */
         private final Map<Point,Unclosed> partialPaths;
 
         /**
          * Builder of isolines as a Java2D shape, created when first needed.
-         * The {@link Polyline} 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.
          *
-         * @see #writeTo(Joiner, Polyline[], boolean)
+         * @see #writeTo(Joiner, PolylineBuffer[], boolean)
          */
         private Joiner path;
 
@@ -249,10 +249,10 @@ final class Tracer {
             this.band      = band;
             this.value     = value;
             partialPaths   = new HashMap<>();
-            polylineOnLeft = new Polyline();
-            polylinesOnTop = new Polyline[width];
+            polylineOnLeft = new PolylineBuffer();
+            polylinesOnTop = new PolylineBuffer[width];
             for (int i=0; i<width; i++) {
-                polylinesOnTop[i] = new Polyline();
+                polylinesOnTop[i] = new PolylineBuffer();
             }
         }
 
@@ -341,7 +341,7 @@ final class Tracer {
                 case UPPER_RIGHT | LOWER_RIGHT:
                 case UPPER_LEFT  | LOWER_LEFT: {
                     assert polylineOnLeft.isEmpty();
-                    final Polyline polylineOnTop = polylinesOnTop[x];
+                    final PolylineBuffer polylineOnTop = polylinesOnTop[x];
                     interpolateMissingTopSide(polylineOnTop);
                     interpolateOnBottomSide(polylineOnTop);     // Will be top side of next row.
                     break;
@@ -423,14 +423,14 @@ final class Tracer {
                     }
                     boolean LLtoUR = isDataAbove == (LOWER_LEFT | UPPER_RIGHT);
                     LLtoUR ^= (average <= value);
-                    final Polyline polylineOnTop = polylinesOnTop[x];
+                    final PolylineBuffer polylineOnTop = polylinesOnTop[x];
                     if (LLtoUR) {
                         closeLeftWithTop(polylineOnTop);
                         interpolateOnRightSide();
                         interpolateOnBottomSide(polylineOnTop.attach(polylineOnLeft));
                     } else {
                         interpolateMissingLeftSide();
-                        final Polyline swap = new Polyline().transferFrom(polylineOnTop);
+                        final PolylineBuffer swap = new PolylineBuffer().transferFrom(polylineOnTop);
                         interpolateOnBottomSide(polylineOnTop.transferFrom(polylineOnLeft));
                         interpolateMissingTopSide(polylineOnLeft.transferFrom(swap));
                         interpolateOnRightSide();
@@ -455,7 +455,7 @@ final class Tracer {
          * Appends to {@code polylineOnTop} a point interpolated on the top side if that point is missing.
          * This interpolation should happens only in the first row.
          */
-        private void interpolateMissingTopSide(final Polyline polylineOnTop) {
+        private void interpolateMissingTopSide(final PolylineBuffer polylineOnTop) {
             if (polylineOnTop.size == 0) {
                 interpolateOnTopSide(polylineOnTop);
             }
@@ -464,7 +464,7 @@ final class Tracer {
         /**
          * Appends to the given polyline a point interpolated on the top side.
          */
-        private void interpolateOnTopSide(final Polyline appendTo) {
+        private void interpolateOnTopSide(final PolylineBuffer appendTo) {
             appendTo.append(translateX + (x + interpolate(0, pixelStride)),
                             translateY + (y));
         }
@@ -482,7 +482,7 @@ final class Tracer {
          * Appends to the given polyline a point interpolated on the bottom side.
          * The polyline on top side will become a {@code polylineOnBottoù} in next row.
          */
-        private void interpolateOnBottomSide(final Polyline polylineOnTop) {
+        private void interpolateOnBottomSide(final PolylineBuffer polylineOnTop) {
             polylineOnTop.append(translateX + (x + interpolate(2*pixelStride, 3*pixelStride)),
                                  translateY + (y + 1));
         }
@@ -503,10 +503,11 @@ final class Tracer {
         }
 
         /**
-         * Joins {@link #polylineOnLeft} with {@code polylineOnTop}, saves their coordinates and clear
-         * those {@code Polyline} instances for use in next cell. The coordinates are written directly
-         * to {@link #path} if we got a closed polygon, or otherwise are saved in {@link #partialPaths}
-         * for later processing. This method is invoked for cells like below:
+         * Joins {@link #polylineOnLeft} with {@code polylineOnTop}, saves their coordinates
+         * and clear those {@link PolylineBuffer} instances for use in next cell.
+         * The coordinates are written directly to {@link #path} if we got a closed polygon,
+         * or otherwise are saved in {@link #partialPaths} for later processing.
+         * This method is invoked for cells like below:
          *
          * {@preformat text
          *     ●╌╱╌╌╌╌○        ○╌╱╌╌╌╌●        ○╌╱╌╌╌╌●╱
@@ -521,18 +522,18 @@ final class Tracer {
          * @param  polylineOnTop  value of {@code polylinesOnTop[x]}.
          * @throws TransformException if the {@link Tracer#gridToCRS} transform can not be applied.
          */
-        private void closeLeftWithTop(final Polyline polylineOnTop) throws TransformException {
+        private void closeLeftWithTop(final PolylineBuffer polylineOnTop) throws TransformException {
             interpolateMissingLeftSide();
             interpolateMissingTopSide(polylineOnTop);
-            final Polyline[] polylines;
+            final PolylineBuffer[] polylines;
             if (polylineOnLeft.opposite == polylineOnTop) {
                 assert polylineOnTop.opposite == polylineOnLeft;
                 /*
                  * We have a loop: the polygon can be closed now, without copying coordinates to temporary buffers.
-                 * Points in the two `Polyline` instances will be iterated in (reverse, forward) order respectively.
+                 * Points in `PolylineBuffer` instances will be iterated in (reverse, forward) order respectively.
                  * Consequently the points we just interpolated will be first point and last point before closing.
                  */
-                polylines = new Polyline[] {polylineOnTop, polylineOnLeft};     // (reverse, forward) point order.
+                polylines = new PolylineBuffer[] {polylineOnTop, polylineOnLeft};    // (reverse, forward) point order.
             } else {
                 /*
                  * Joining left and top polylines do not yet create a closed shape. Consequently we may not write
@@ -544,7 +545,7 @@ final class Tracer {
                      * Fragment starts and ends with NaN values. We will not be able to complete a polygon.
                      * Better to write the polylines now for avoiding temporary copies of their coordinates.
                      */
-                    polylines = new Polyline[] {
+                    polylines = new PolylineBuffer[] {
                         polylineOnLeft.opposite, polylineOnLeft, polylineOnTop, polylineOnTop.opposite
                     };
                 } else if (fragment.addOrMerge(partialPaths)) {
@@ -565,13 +566,13 @@ final class Tracer {
          * Writes the content of given polyline without closing it as a polygon.
          * The given polyline will become empty after this method call.
          */
-        private void writeUnclosed(final Polyline polyline) throws TransformException {
+        private void writeUnclosed(final PolylineBuffer polyline) throws TransformException {
             final Unclosed fragment = new Unclosed(polyline, null);
-            final Polyline[] polylines;
+            final PolylineBuffer[] polylines;
             final boolean close;
             if (fragment.isEmpty()) {
                 close = false;
-                polylines = new Polyline[] {polyline.opposite, polyline};       // (reverse, forward) point order.
+                polylines = new PolylineBuffer[] {polyline.opposite, polyline};     // (reverse, forward) point order.
             } else {
                 close = fragment.addOrMerge(partialPaths);
                 if (!close) {
@@ -586,7 +587,7 @@ final class Tracer {
         /**
          * Invoked after iteration on a single row has been completed. If there is a polyline
          * finishing on the right image border, the coordinates needs to be saved somewhere
-         * because that {@code Polyline} will not be continued by cells on next rows.
+         * because that {@link PolylineBuffer} will not be continued by cells on next rows.
          */
         final void finishedRow() throws TransformException {
             if (!polylineOnLeft.transferToOpposite()) {
@@ -599,7 +600,7 @@ final class Tracer {
          * Invoked after the iteration has been completed on the full area of interest.
          * This method writes all remaining polylines to {@link #partialPaths}.
          * It assumes that {@link #finishedRow()} has already been invoked.
-         * This {@code Isoline} can not be used anymore after this call.
+         * This {@link Level} instance can not be used anymore after this call.
          */
         final void finish() throws TransformException {
             assert polylineOnLeft.isEmpty();
@@ -695,201 +696,15 @@ final class Tracer {
             final Shape s = (path != null) ? path.build() : shape;
             if (s != null) appendTo.append(s, false);
             polylineOnLeft.toRawPath(appendTo);
-            for (final Polyline p : polylinesOnTop) {
+            for (final PolylineBuffer p : polylinesOnTop) {
                 if (p != null) p.toRawPath(appendTo);
             }
         }
     }
 
     /**
-     * Coordinates of a polyline under construction. Coordinates can be appended in only one direction.
-     * If the polyline may growth on both directions (which happens if the polyline crosses the bottom
-     * side and the right side of a cell), then the two directions are handled by two distinct instances
-     * connected by their {@link #opposite} field.
-     *
-     * <p>When a polyline has been completed, its content is copied to {@link Level#path}
-     * and the {@code Polyline} object is recycled for a new polyline.</p>
-     */
-    private static final class Polyline {
-        /**
-         * Number of coordinates in a tuple.
-         */
-        static final int DIMENSION = 2;
-
-        /**
-         * Coordinates as (x,y) tuples. This array is expanded as needed.
-         */
-        double[] coordinates;
-
-        /**
-         * Number of valid elements in the {@link #coordinates} array.
-         * This is twice the number of points.
-         */
-        int size;
-
-        /**
-         * If the polyline has points added to its two extremities, the other extremity. Otherwise {@code null}.
-         * The first point of {@code opposite} polyline is connected to the first point of this polyline.
-         * Consequently when those two polylines are joined in a single polyline, the coordinates of either
-         * {@code this} or {@code opposite} must be iterated in reverse order.
-         */
-        Polyline opposite;
-
-        /**
-         * Creates an initially empty polyline.
-         */
-        Polyline() {
-            coordinates = ArraysExt.EMPTY_DOUBLE;
-        }
-
-        /**
-         * Creates a new polyline wrapping the given coordinates. Used for wrapping {@link Unclosed}
-         * instances in objects expected by {@link Tracer#writeTo(Joiner, Polyline[], boolean)}.
-         * Those {@code Polyline} instances are temporary.
-         */
-        Polyline(final double[] data) {
-            coordinates = data;
-            size = data.length;
-        }
-
-        /**
-         * Discards all coordinates in this polyline. This method does not clear
-         * the {@link #opposite} polyline; it is caller's responsibility to do so.
-         */
-        final void clear() {
-            opposite = null;
-            size = 0;
-        }
-
-        /**
-         * Returns whether this polyline is empty. This method is used only for {@code assert isEmpty()}
-         * statement because of the check for {@code opposite == null}: an empty polyline should not have
-         * a non-null {@link #opposite} value.
-         */
-        final boolean isEmpty() {
-            return size == 0 & (opposite == null);
-        }
-
-        /**
-         * Declares that the specified polyline will add points in the direction opposite to this polyline.
-         * This happens when the polyline crosses the bottom side and the right side of a cell (assuming an
-         * iteration from left to right and top to bottom).
-         *
-         * <p>This method is typically invoked in the following pattern (but this is not mandatory).
-         * An important aspect is that {@code this} and {@code other} should be on perpendicular axes:</p>
-         *
-         * {@preformat java
-         *     interpolateOnBottomSide(polylinesOnTop[x].attach(polylineOnLeft));
-         * }
-         *
-         * @return {@code this} for method calls chaining.
-         */
-        final Polyline attach(final Polyline other) {
-            assert (opposite == null) & (other.opposite == null);
-            other.opposite = this;
-            opposite = other;
-            return this;
-        }
-
-        /**
-         * Transfers all coordinates from given polylines to this polylines, in same order.
-         * This is used when polyline on the left side continues on bottom side,
-         * or conversely when polyline on the top side continues on right side.
-         * This polyline shall be empty before this method is invoked.
-         * The given source will become empty after this method returned.
-         *
-         * @param  source  the source from which to take data.
-         * @return {@code this} for method calls chaining.
-         */
-        final Polyline transferFrom(final Polyline source) {
-            assert isEmpty();
-            final double[] swap = coordinates;
-            coordinates = source.coordinates;
-            size        = source.size;
-            opposite    = source.opposite;
-            if (opposite != null) {
-                opposite.opposite = this;
-            }
-            source.clear();
-            source.coordinates = swap;
-            return this;
-        }
-
-        /**
-         * Transfers all coordinates from this polyline to the polyline going in opposite direction.
-         * This is used when this polyline reached the right image border, in which case its data
-         * will be lost if we do not copy them somewhere.
-         *
-         * @return {@code true} if coordinates have been transferred,
-         *         or {@code false} if there is no opposite direction.
-         */
-        final boolean transferToOpposite() {
-            if (opposite == null) {
-                return false;
-            }
-            final int sum = size + opposite.size;
-            double[] data = opposite.coordinates;
-            if (sum > data.length) {
-                data = new double[sum];
-            }
-            System.arraycopy(opposite.coordinates, 0, data, size, opposite.size);
-            for (int i=0, t=size; (t -= DIMENSION) >= 0;) {
-                data[t  ] = coordinates[i++];
-                data[t+1] = coordinates[i++];
-            }
-            opposite.size = sum;
-            opposite.coordinates = data;
-            opposite.opposite = null;
-            clear();
-            return true;
-        }
-
-        /**
-         * Appends given coordinates to this polyline.
-         *
-         * @param  x  first coordinate of the (x,y) tuple to add.
-         * @param  y  second coordinate of the (x,y) tuple to add.
-         */
-        final void append(final double x, final double y) {
-            if (size >= coordinates.length) {
-                coordinates = Arrays.copyOf(coordinates, Math.max(Math.multiplyExact(size, 2), 32));
-            }
-            coordinates[size++] = x;
-            coordinates[size++] = y;
-        }
-
-        /**
-         * Returns a string representation of this {@code Polyline} for debugging purposes.
-         */
-        @Override
-        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++]);
-                }
-            }
-        }
-    }
-
-    /**
-     * List of {@code Polyline} coordinates that have not yet been closed. Each {@code double[]} in this list is
-     * a copy of a {@code Polyline} used by {@link Level}. Those copies are performed for saving data before they
+     * List of {@code PolylineBuffer} coordinates that have not yet been closed. Each {@code double[]} in this list is
+     * a copy of a {@link PolylineBuffer} used by {@link Level}. Those copies are performed for saving data before they
      * are overwritten by next iterated cell.
      *
      * <h2>List indices and ordering of points</h2>
@@ -918,9 +733,9 @@ final class Tracer {
          * @param  polylineOnLeft  first polyline with points in forward order. Shall not be null.
          * @param  polylineOnTop    next polyline with points in reverse order, or {@code null} if none.
          */
-        Unclosed(final Polyline polylineOnLeft, final Polyline polylineOnTop) {
+        Unclosed(final PolylineBuffer polylineOnLeft, final PolylineBuffer polylineOnTop) {
             /*
-             * Search for first point and last point by inspecting `Polyline`s in the order shown below.
+             * Search for first and last point by inspecting `PolylineBuffer` instances in the order shown below.
              * The first 4 rows and the last 4 rows search for first point and last point respectively.
              * The empty rows in the middle are an intentional gap for creating a regular pattern that
              * we can exploit for 3 decisions that need to be done during the loop:
@@ -929,7 +744,7 @@ final class Tracer {
              *     ✓ (index % 3) = 0    if using `opposite` value of polyline (may be null).
              *     ✓ (index & 1) = 0    if fetching last point (otherwise fetch first point).
              *
-             *  Index   Polyline   (iteration order)  !(i & 2)  !(i % 3)  !(i & 1)   Comment
+             *  Index   PolylineBuffer        (order) !(i & 2)  !(i % 3)  !(i & 1)   Comment
              *  ────────────────────────────────────────────────────────────────────────────
              *   [0]    polylineOnLeft.opposite  (←)      ✓         ✓         ✓        (1)
              *   [1]    polylineOnLeft           (→)      ✓                            (2)
@@ -943,15 +758,15 @@ final class Tracer {
              *   [9]    polylineOnLeft.opposite  (←)      ✓         ✓                  (4)
              *
              * Comments:
-             *   (1) Last  `Polyline` point is first `Unclosed` point because of reverse iteration order.
-             *   (2) First `Polyline` point is first `Unclosed` point because of forward iteration order.
-             *   (3) Last  `Polyline` point is last  `Unclosed` point because of forward iteration order.
-             *   (4) First `Polyline` point is last  `Unclosed` point because of reverse iteration order.
+             *   (1) Last  `PolylineBuffer` point is first `Unclosed` point because of reverse iteration order.
+             *   (2) First `PolylineBuffer` point is first `Unclosed` point because of forward iteration order.
+             *   (3) Last  `PolylineBuffer` point is last  `Unclosed` point because of forward iteration order.
+             *   (4) First `PolylineBuffer` point is last  `Unclosed` point because of reverse iteration order.
              */
             int index = 0;
             do {
-                Polyline polyline = ((index & 2) == 0) ? polylineOnLeft : polylineOnTop;  // See above table (column 4).
-                if (index % 3 == 0 && polyline != null) polyline = polyline.opposite;     // See above table (column 5).
+                PolylineBuffer polyline = ((index & 2) == 0) ? polylineOnLeft : polylineOnTop;  // See above table (column 4).
+                if (index % 3 == 0 && polyline != null) polyline = polyline.opposite;           // See above table (column 5).
                 if (polyline != null) {
                     int n = polyline.size;
                     if (n != 0) {
@@ -1006,7 +821,7 @@ final class Tracer {
             take(polylineOnLeft.opposite);          // Point will be iterated in reverse order.
             take(polylineOnLeft);                   // Point will be iterated in forward order.
             if (polylineOnTop != null) {
-                Polyline suffix = polylineOnTop.opposite;
+                PolylineBuffer suffix = polylineOnTop.opposite;
                 take(polylineOnTop);                // Inverse order. Set `polylineOnTop.opposite` to null.
                 take(suffix);                       // Forward order.
             }
@@ -1015,7 +830,7 @@ final class Tracer {
         /**
          * Takes a copy of coordinate values of given polyline, then clears that polyline.
          */
-        private void take(final Polyline polyline) {
+        private void take(final PolylineBuffer polyline) {
             if (polyline != null && polyline.size != 0) {
                 add(Arrays.copyOf(polyline.coordinates, polyline.size));
                 polyline.clear();
@@ -1110,17 +925,17 @@ final class Tracer {
         }
 
         /**
-         * Returns the content of this list as an array of {@link Polyline} instances.
-         * {@code Polyline} instances at even index should be written with their points in reverse order.
+         * Returns the content of this list as an array of {@link PolylineBuffer} instances.
+         * {@code PolylineBuffer} instances at even index should be written with their points in reverse order.
          *
-         * @see #writeTo(Joiner, Polyline[], boolean)
+         * @see #writeTo(Joiner, PolylineBuffer[], boolean)
          */
-        final Polyline[] toPolylines() {
-            final Polyline[] polylines = new Polyline[size()];
+        final PolylineBuffer[] toPolylines() {
+            final PolylineBuffer[] polylines = new PolylineBuffer[size()];
             for (int i=0; i<polylines.length; i++) {
                 final double[] coordinates = get(i);
                 if (coordinates != null) {
-                    polylines[i] = new Polyline(coordinates);
+                    polylines[i] = new PolylineBuffer(coordinates);
                 }
             }
             return polylines;
@@ -1128,9 +943,9 @@ final class Tracer {
     }
 
     /**
-     * Assembles arbitrary amount of {@link Polyline}s in a single Java2D {@link Shape} for a specific 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.
+     * 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),
@@ -1164,9 +979,9 @@ final class Tracer {
      * </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 Polyline} instances.
+     * 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 Polyline#opposite} field.
+     * which is possible only with a non-null {@link PolylineBuffer#opposite} field.
      */
     private static final class Joiner extends PathBuilder {
         /**
@@ -1186,10 +1001,10 @@ final class Tracer {
          * 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 require that we move forward then backward on the same coordinates, which happen only with two
-         * distinct {@link Polyline} 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>
+         * 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.
@@ -1209,8 +1024,8 @@ final class Tracer {
                     if (coordinates[spike1++] != xo) equalityMask &= ~1;
                     if (coordinates[spike1++] != yo) equalityMask &= ~2;
                     if (equalityMask == 0) {
-                        equalityMask = before;              // For keeping same search criterion.
-                        spike1 -= Polyline.DIMENSION;       // Restore previous position before mismatch.
+                        equalityMask = before;                  // For keeping same search criterion.
+                        spike1 -= PolylineBuffer.DIMENSION;      // Restore previous position before mismatch.
                         break;
                     }
                 }
@@ -1218,7 +1033,7 @@ final class Tracer {
                     if (coordinates[--spike0] != yo) equalityMask &= ~2;
                     if (coordinates[--spike0] != xo) equalityMask &= ~1;
                     if (equalityMask == 0) {
-                        spike0 += Polyline.DIMENSION;
+                        spike0 += PolylineBuffer.DIMENSION;
                         break;
                     }
                 }
@@ -1237,7 +1052,7 @@ final class Tracer {
              */
             final int limit = spike1;
             int base;
-            while ((base = spike0 + 2*Polyline.DIMENSION) < limit) {    // Spikes exist only with at least 3 points.
+            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;
@@ -1250,7 +1065,7 @@ final class Tracer {
                         System.arraycopy(coordinates, spike1, coordinates, spike0, upper - spike1);
                         return upper - (spike1 - spike0);
                     }
-                } while ((spike1 -= Polyline.DIMENSION) > base);
+                } while ((spike1 -= PolylineBuffer.DIMENSION) > base);
             }
             return upper;       // Nothing to remove.
         }
@@ -1262,15 +1077,15 @@ final class Tracer {
         @Override
         protected int filterFull(final double[] coordinates, final int upper) throws TransformException {
             if (gridToCRS != null) {
-                gridToCRS.transform(coordinates, 0, coordinates, 0, upper / Polyline.DIMENSION);
+                gridToCRS.transform(coordinates, 0, coordinates, 0, upper / PolylineBuffer.DIMENSION);
             }
             return upper;
         }
     }
 
     /**
-     * Writes all given polylines to the specified path builder. Null {@code Polyline} instances are ignored.
-     * {@code Polyline} instances at even index are written with their points in reverse order.
+     * 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.
      * All given polylines are cleared by this method.
      *
      * @param  path       where to write the polylines, or {@code null} if not yet created.
@@ -1279,9 +1094,9 @@ final class Tracer {
      * @return the given path builder, or a newly created builder if the argument was null.
      * @throws TransformException if the {@link #gridToCRS} transform can not be applied.
      */
-    private Joiner writeTo(Joiner path, final Polyline[] polylines, final boolean close) throws TransformException {
+    private Joiner writeTo(Joiner path, final PolylineBuffer[] polylines, final boolean close) throws TransformException {
         for (int pi=0; pi < polylines.length; pi++) {
-            final Polyline p = polylines[pi];
+            final PolylineBuffer p = polylines[pi];
             if (p == null) {
                 continue;
             }


[sis] 01/03: Move isoline generation code to its own package.

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 2cb6525c48b5302dd21c5faf0377b8c04bf83ef1
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Aug 13 10:36:02 2022 +0200

    Move isoline generation code to its own package.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  2 +-
 .../apache/sis/gui/coverage/IsolineRenderer.java   |  2 +-
 .../sis/gui/coverage/StyledRenderingData.java      |  2 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |  2 +-
 .../internal/processing/image/TiledProcess.java    |  6 +--
 .../processing/{image => isoline}/Isolines.java    | 51 +++++++++++-----------
 .../IsolineTracer.java => isoline/Tracer.java}     | 12 ++---
 .../internal/processing/isoline/package-info.java  | 32 ++++++++++++++
 .../org/apache/sis/image/ImageProcessorTest.java   |  4 +-
 .../{image => isoline}/IsolinesTest.java           |  2 +-
 .../StepsViewer.java}                              | 12 ++---
 .../apache/sis/test/suite/FeatureTestSuite.java    |  2 +-
 12 files changed, 81 insertions(+), 48 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index daf385014a..eab02b084d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -70,7 +70,7 @@ import org.apache.sis.gui.map.MapCanvasAWT;
 import org.apache.sis.portrayal.RenderException;
 import org.apache.sis.internal.map.coverage.RenderingWorkaround;
 import org.apache.sis.internal.coverage.j2d.TileErrorHandler;
-import org.apache.sis.internal.processing.image.Isolines;
+import org.apache.sis.internal.processing.isoline.Isolines;
 import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.GUIUtilities;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
index 63917179df..d816ab68b8 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
@@ -38,7 +38,7 @@ import javafx.collections.ListChangeListener;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.internal.gui.control.ColorRamp;
-import org.apache.sis.internal.processing.image.Isolines;
+import org.apache.sis.internal.processing.isoline.Isolines;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.gui.control.ValueColorMapper.Step;
 import org.apache.sis.internal.feature.j2d.EmptyShape;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java
index 0709fe82c2..ed19c7d66d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/StyledRenderingData.java
@@ -24,7 +24,7 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.image.ErrorHandler;
-import org.apache.sis.internal.processing.image.Isolines;
+import org.apache.sis.internal.processing.isoline.Isolines;
 import org.apache.sis.internal.map.coverage.RenderingData;
 
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index ef4923f5d7..23575b6a64 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -51,7 +51,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.coverage.j2d.TiledImage;
-import org.apache.sis.internal.processing.image.Isolines;
+import org.apache.sis.internal.processing.isoline.Isolines;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/TiledProcess.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/TiledProcess.java
index 8adf66ec51..79e33132f2 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/TiledProcess.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/TiledProcess.java
@@ -56,14 +56,14 @@ import org.apache.sis.util.ArgumentChecks;
  * Consequently tile size will be determined by other considerations such as the number of processors.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @param  <R>  the type of value computed as a result of this process.
  *
  * @since 1.1
  * @module
  */
-abstract class TiledProcess<R> {
+public abstract class TiledProcess<R> {
     /**
      * Minimal "tile" size for sub-tasks computation. That size should not be too small because the
      * fork/join processes have some extra cost compared to processing the whole image as one single tile.
@@ -221,7 +221,7 @@ abstract class TiledProcess<R> {
      * <p>This class implements {@link Callable} for {@code TiledProcess} convenience.
      * This implementation details should be ignored; it may change in any future version.</p>
      */
-    abstract class Task implements Callable<R> {
+    protected abstract class Task implements Callable<R> {
         /**
          * Index of this {@code Task} instance in the {@link TiledProcess#tasks} array.
          */
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/isoline/Isolines.java
similarity index 92%
rename from core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/Isolines.java
rename to core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Isolines.java
index a3532ecb56..a1e22aa3fe 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/isoline/Isolines.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.processing.image;
+package org.apache.sis.internal.processing.isoline;
 
 import java.util.AbstractList;
 import java.util.Arrays;
@@ -31,15 +31,16 @@ import java.awt.image.RenderedImage;
 import org.opengis.coverage.grid.SequenceType;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.processing.image.TiledProcess;
 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;
-import static org.apache.sis.internal.processing.image.IsolineTracer.LOWER_RIGHT;
+import static org.apache.sis.internal.processing.isoline.Tracer.UPPER_LEFT;
+import static org.apache.sis.internal.processing.isoline.Tracer.UPPER_RIGHT;
+import static org.apache.sis.internal.processing.isoline.Tracer.LOWER_RIGHT;
 
 
 /**
@@ -57,14 +58,14 @@ import static org.apache.sis.internal.processing.image.IsolineTracer.LOWER_RIGHT
  */
 public final class Isolines {
     /**
-     * Isoline data for each level, sorted in ascending order of {@link IsolineTracer.Level#value}.
+     * Isoline data for each level, sorted in ascending order of {@link Tracer.Level#value}.
      */
-    private final IsolineTracer.Level[] levels;
+    private final Tracer.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
+     * when using {@code StepsViewer} (in test package) for following an isoline generation step
      * by step.
      */
     @Debug
@@ -74,8 +75,8 @@ public final class Isolines {
      * 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[][])}.
      */
-    private Isolines(final IsolineTracer tracer, final int band, final double[] values, final int width) {
-        levels = new IsolineTracer.Level[values.length];
+    private Isolines(final Tracer tracer, final int band, final double[] values, final int width) {
+        levels = new Tracer.Level[values.length];
         for (int i=0; i<values.length; i++) {
             levels[i] = tracer.new Level(band, values[i], width);
         }
@@ -105,7 +106,7 @@ public final class Isolines {
     }
 
     /**
-     * Sets the specified bit on {@link IsolineTracer.Level#isDataAbove} for all levels lower than given value.
+     * Sets the specified bit on {@link Tracer.Level#isDataAbove} for all levels lower than given value.
      *
      * <h4>How strict equalities are handled</h4>
      * Sample values exactly equal to the isoline value are handled as if they were greater. It does not matter
@@ -120,13 +121,13 @@ public final class Isolines {
      * out in the final stage, when copying coordinates in {@link Path2D} objects.
      *
      * @param  value a sample values from the image.
-     * @param  bit   {@value IsolineTracer#UPPER_LEFT}, {@value IsolineTracer#UPPER_RIGHT},
-     *               {@value IsolineTracer#LOWER_LEFT} or {@value IsolineTracer#LOWER_RIGHT}.
+     * @param  bit   {@value Tracer#UPPER_LEFT}, {@value Tracer#UPPER_RIGHT},
+     *               {@value Tracer#LOWER_LEFT} or {@value Tracer#LOWER_RIGHT}.
      *
-     * @see IsolineTracer.Level#nextColumn()
+     * @see Tracer.Level#nextColumn()
      */
     private void setMaskBit(final double value, final int bit) {
-        for (final IsolineTracer.Level level : levels) {
+        for (final Tracer.Level level : levels) {
             if (level.value > value) break;                 // See above javadoc for NaN handling.
             level.isDataAbove |= bit;
         }
@@ -196,7 +197,7 @@ public final class Isolines {
      */
     private static Isolines[] flush(final Isolines[] isolines) throws TransformException {
         for (final Isolines isoline : isolines) {
-            for (final IsolineTracer.Level level : isoline.levels) {
+            for (final Tracer.Level level : isoline.levels) {
                 level.flush();
             }
         }
@@ -291,11 +292,11 @@ public final class Isolines {
         /*
          * Prepares a window of size 2×2 pixels over pixel values. Window elements are traversed
          * by incrementing indices in following order: band, column, row. The window content will
-         * be written in this method and read by IsolineTracer.
+         * be written in this method and read by Tracer.
          */
         final int numBands = iterator.getNumBands();
         final double[] window = new double[numBands * 4];
-        final IsolineTracer tracer = new IsolineTracer(window, numBands, iterator.getDomain(), gridToCRS);
+        final Tracer tracer = new Tracer(window, numBands, iterator.getDomain(), gridToCRS);
         /*
          * Prepare the set of isolines for each band in the image.
          * The number of cells on the horizontal axis is one less
@@ -336,7 +337,7 @@ abort:  while (iterator.next()) {
              *
              *  - Get values on the 4 corners.
              *  - Save value of lower-left corner for use by next row.
-             *  - Initialize `IsolineTracer.Level.isDataAbove` bits for all levels.
+             *  - Initialize `Tracer.Level.isDataAbove` bits for all levels.
              *  - Interpolate the first cell.
              */
             System.arraycopy(valuesOnPreviousRow, 0, window, 0, twoPixels);
@@ -350,7 +351,7 @@ abort:  while (iterator.next()) {
                 }
             }
             for (final Isolines iso : isolines) {
-                for (final IsolineTracer.Level level : iso.levels) {
+                for (final Tracer.Level level : iso.levels) {
                     level.interpolate();
                 }
             }
@@ -377,12 +378,12 @@ abort:  while (iterator.next()) {
                 }
                 for (int b=0; b<numBands; b++) {
                     final Isolines iso = isolines[b];
-                    for (final IsolineTracer.Level level : iso.levels) {
+                    for (final Tracer.Level level : iso.levels) {
                         level.nextColumn();
                     }
                     iso.setMaskBit(window[numBands  + b], UPPER_RIGHT);
                     iso.setMaskBit(window[lastPixel + b], LOWER_RIGHT);
-                    for (final IsolineTracer.Level level : iso.levels) {
+                    for (final Tracer.Level level : iso.levels) {
                         level.interpolate();
                     }
                 }
@@ -392,7 +393,7 @@ abort:  while (iterator.next()) {
              * 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) {
+                for (final Tracer.Level level : isolines[b].levels) {
                     level.finishedRow();
                 }
                 if (LISTENER != null) {
@@ -409,7 +410,7 @@ abort:  while (iterator.next()) {
          * Finished iteration over the whole image.
          */
         for (int b=0; b<numBands; b++) {
-            for (final IsolineTracer.Level level : isolines[b].levels) {
+            for (final Tracer.Level level : isolines[b].levels) {
                 level.finish();
             }
             if (LISTENER != null) {
@@ -426,7 +427,7 @@ abort:  while (iterator.next()) {
      */
     public final NavigableMap<Double,Shape> polylines() {
         final TreeMap<Double,Shape> paths = new TreeMap<>();
-        for (final IsolineTracer.Level level : levels) {
+        for (final Tracer.Level level : levels) {
             final Shape path = level.shape;
             if (path != null) {
                 paths.put(level.value, path);
@@ -534,7 +535,7 @@ abort:  while (iterator.next()) {
     @Debug
     private Path2D toRawPath() {
         final Path2D path = new Path2D.Float();
-        for (final IsolineTracer.Level level : levels) {
+        for (final Tracer.Level level : levels) {
             level.toRawPath(path);
         }
         return path;
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/isoline/Tracer.java
similarity index 99%
rename from core/sis-feature/src/main/java/org/apache/sis/internal/processing/image/IsolineTracer.java
rename to core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/Tracer.java
index b0e55cadd0..3d6abbf6c2 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/isoline/Tracer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.processing.image;
+package org.apache.sis.internal.processing.isoline;
 
 import java.util.Arrays;
 import java.util.ArrayList;
@@ -37,7 +37,7 @@ import org.apache.sis.util.Debug;
 /**
  * Iterator over contouring grid cells together with an interpolator and an assembler of polyline segments.
  * A single instance of this class is created by {@code Isolines.generate(…)} for all bands to process in a
- * given image. {@code IsolineTracer} is used for doing a single iteration over all image pixels.
+ * given image. {@code Tracer} is used for doing a single iteration over all image pixels.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -48,7 +48,7 @@ import org.apache.sis.util.Debug;
  * @since 1.1
  * @module
  */
-final class IsolineTracer {
+final class Tracer {
     /**
      * Mask to apply on {@link Level#isDataAbove} for telling that value in a corner is higher than the level value.
      * Values are defined in {@code PixelIterator.Window} iteration order: from left to right, then top to bottom.
@@ -104,7 +104,7 @@ final class IsolineTracer {
      * @param  domain       pixel coordinates where iteration will happen.
      * @param  gridToCRS    final transform to apply on coordinates (integer source coordinates at pixel centers).
      */
-    IsolineTracer(final double[] window, final int pixelStride, final Rectangle domain, final MathTransform gridToCRS) {
+    Tracer(final double[] window, final int pixelStride, final Rectangle domain, final MathTransform gridToCRS) {
         this.window      = window;
         this.pixelStride = pixelStride;
         this.translateX  = domain.x;
@@ -519,7 +519,7 @@ final class IsolineTracer {
          * {@link #polylineOnLeft} and {@code polylineOnTop} will become empty after this method call.
          *
          * @param  polylineOnTop  value of {@code polylinesOnTop[x]}.
-         * @throws TransformException if the {@link IsolineTracer#gridToCRS} transform can not be applied.
+         * @throws TransformException if the {@link Tracer#gridToCRS} transform can not be applied.
          */
         private void closeLeftWithTop(final Polyline polylineOnTop) throws TransformException {
             interpolateMissingLeftSide();
@@ -744,7 +744,7 @@ final class IsolineTracer {
 
         /**
          * Creates a new polyline wrapping the given coordinates. Used for wrapping {@link Unclosed}
-         * instances in objects expected by {@link IsolineTracer#writeTo(Joiner, Polyline[], boolean)}.
+         * instances in objects expected by {@link Tracer#writeTo(Joiner, Polyline[], boolean)}.
          * Those {@code Polyline} instances are temporary.
          */
         Polyline(final double[] data) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/package-info.java b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/package-info.java
new file mode 100644
index 0000000000..8f337f51b9
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/processing/isoline/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+/**
+ * Isolines generation.
+ *
+ * <p><strong>Do not use!</strong></p>
+ *
+ * This package is for internal use by SIS only. Classes in this package
+ * may change in incompatible ways in any future version without notice.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.1
+ * @module
+ */
+package org.apache.sis.internal.processing.isoline;
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
index 22dd7aedbc..6a123ba60c 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
@@ -20,7 +20,7 @@ import java.util.Map;
 import java.awt.Shape;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
-import org.apache.sis.internal.processing.image.IsolinesTest;
+import org.apache.sis.internal.processing.isoline.IsolinesTest;
 import org.apache.sis.test.DependsOn;
 import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.test.TestCase;
@@ -38,7 +38,7 @@ import static org.apache.sis.test.TestUtilities.getSingleton;
  * @since   1.1
  * @module
  */
-@DependsOn(org.apache.sis.internal.processing.image.IsolinesTest.class)
+@DependsOn(org.apache.sis.internal.processing.isoline.IsolinesTest.class)
 public final strictfp class ImageProcessorTest extends TestCase {
     /**
      * Tests {@link ImageProcessor#isolines(RenderedImage, double[][], MathTransform)}.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/IsolinesTest.java
similarity index 99%
rename from core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java
rename to core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/IsolinesTest.java
index 14568a0eb9..e5fa451e0b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolinesTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/IsolinesTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.processing.image;
+package org.apache.sis.internal.processing.isoline;
 
 import java.util.Map;
 import java.awt.Shape;
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/isoline/StepsViewer.java
similarity index 96%
rename from core/sis-feature/src/test/java/org/apache/sis/internal/processing/image/IsolineViewer.java
rename to core/sis-feature/src/test/java/org/apache/sis/internal/processing/isoline/StepsViewer.java
index 446946b93c..0618d21a10 100644
--- 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/isoline/StepsViewer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.processing.image;
+package org.apache.sis.internal.processing.isoline;
 
 import java.awt.Shape;
 import java.awt.Color;
@@ -50,7 +50,7 @@ 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.
+ * {@link Isolines#LISTENER}, then uncomment the {@link #setListener(StepsViewer)} constructor body.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.3
@@ -58,13 +58,13 @@ import static org.junit.Assert.*;
  * @module
  */
 @SuppressWarnings("serial")
-public final class IsolineViewer extends JComponent implements BiConsumer<String,Path2D>, ChangeListener, ActionListener {
+public final class StepsViewer 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) {
+    private static void setListener(final StepsViewer listener) {
         // Isolines.LISTENER = listener;
     }
 
@@ -135,7 +135,7 @@ public final class IsolineViewer extends JComponent implements BiConsumer<String
      * @param  pane  the container where to add components.
      */
     @SuppressWarnings("ThisEscapedInObjectConstruction")
-    private IsolineViewer(final RenderedImage data, final Container pane) {
+    private StepsViewer(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(
@@ -170,7 +170,7 @@ public final class IsolineViewer extends JComponent implements BiConsumer<String
         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 StepsViewer viewer = new StepsViewer(data, frame.getContentPane());
         final Isolines iso;
         try {
             setListener(viewer);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index db88eca51d..78e4a38b3e 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -88,7 +88,7 @@ import org.junit.runners.Suite;
     org.apache.sis.internal.coverage.j2d.ScaledColorSpaceTest.class,
     org.apache.sis.internal.coverage.j2d.ColorizerTest.class,
     org.apache.sis.internal.coverage.j2d.SampleModelFactoryTest.class,
-    org.apache.sis.internal.processing.image.IsolinesTest.class,
+    org.apache.sis.internal.processing.isoline.IsolinesTest.class,
     org.apache.sis.image.DataTypeTest.class,
     org.apache.sis.image.PlanarImageTest.class,
     org.apache.sis.image.ComputedImageTest.class,