| /* |
| * 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; |
| } |
| } |