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