blob: b9ccfcfe87b6b9c5db1208326ff79a641a4e9930 [file] [log] [blame]
/*
* 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;
}
}