blob: 63dcb383667973a6917557b1c6b2cc3ee0ff401b [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.image;
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;
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;
/**
* 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.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
*
* @see <a href="https://en.wikipedia.org/wiki/Marching_squares">Marching squares on Wikipedia</a>
*
* @since 1.1
* @module
*/
final class IsolineTracer {
/**
* 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.
*
* <p>Note: there is some hard-coded dependencies to those exact values.
* If values are changed, search for example for {@code log2(UPPER_RIGHT)} in comments.</p>
*/
static final int UPPER_LEFT = 1, UPPER_RIGHT = 2, LOWER_LEFT = 4, LOWER_RIGHT = 8;
/**
* The 2×2 window containing pixel values in the 4 corners of current contouring grid cell.
* Values are always stored with band index varying fastest, then column index, then row index.
* The length of this array is <var>(number of bands)</var> × 2 (width) × 2 (height).
*/
private final double[] window;
/**
* Increment to the position for reading next sample value.
* It corresponds to the number of bands in {@link #window}.
*/
private final int pixelStride;
/**
* Pixel coordinate on the left side of the cell where to interpolate.
*/
int x;
/**
* Pixel coordinate on the top side of the cell where to interpolate.
*/
int y;
/**
* Translation to apply on coordinates. For isolines computed sequentially, this is the image origin
* (often 0,0 but not necessarily). For isolines computed in parallel, the translations are different
* for each computation tile.
*/
private final double translateX, translateY;
/**
* Final transform to apply on coordinates (integer source coordinates at pixel centers).
* Can be {@code null} if none.
*/
private final MathTransform gridToCRS;
/**
* Creates a new position for the given data window.
*
* @param window the 2×2 window containing pixel values in the 4 corners of current contouring grid cell.
* @param pixelStride increment to the position in {@code window} for reading next sample value.
* @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) {
this.window = window;
this.pixelStride = pixelStride;
this.translateX = domain.x;
this.translateY = domain.y;
this.gridToCRS = gridToCRS;
}
/**
* Builder of polylines for a single level. The segments to create are determined by a set
* of {@linkplain #isDataAbove four flags} (one for each corner) encoded in an integer.
* The meaning of those flags is described in Wikipedia "Marching squares" article,
* except that this implementation uses different values.
*/
final class Level {
/**
* Band number where to read values in the {@link #window} array.
*/
private final int band;
/**
* The level value.
*
* @see #interpolate(int, int)
*/
final double value;
/**
* Bitset telling which corners have a value greater than this isoline level {@linkplain #value}.
* Each corner is associated to one of the bits illustrated below, where bit (0) is the less significant.
* Note that this bit order is different than the order used in Wikipedia "Marching squares" article.
* The order used in this class allows more direct bitwise operations as described in next section.
*
* {@preformat text
* (0)╌╌╌(1)
* ╎ ╎
* (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.
*
* <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,
* bits 0 and 2 can be obtained from the bit pattern of previous iteration with a simple bit shift.
*
* @see #UPPER_LEFT
* @see #UPPER_RIGHT
* @see #LOWER_LEFT
* @see #LOWER_RIGHT
*/
int isDataAbove;
/**
* The polyline to be continued on the next column. This is a single instance because iteration happens
* from left to right before top to bottom. This instance is non-empty if the cell in previous iteration
* was like below (all those examples have a line crossing the right border):
*
* {@preformat text
* ●╌╌╌╌╌╌● ○╌╱╌╌╌╌●╱ ○╌╌╌╌╲╌●
* ╎ ╎ ╎╱ ╱ ╎ ╲╎
* ─┼──────┼─ ╱ ╱╎ ╎ ╲
* ○╌╌╌╌╌╌○ ╱●╌╌╌╌╱╌○ ○╌╌╌╌╌╌○╲
* }
*
* This field {@linkplain Polyline#isEmpty() is empty} if the cell in previous iteration was like below
* (no line cross the right border):
*
* {@preformat text
* ○╌╲╌╌╌╌● ○╌╌╌┼╌╌●
* ╎ ╲ ╎ ╎ │ ╎
* ╎ ╲ ╎ ╎ │ ╎
* ○╌╌╌╌╲╌● ○╌╌╌┼╌╌●
* }
*/
private final Polyline polylineOnLeft;
/**
* The polylines in each column which need to be continued on the next row.
* This array contains empty instances in columns where there are no polylines to continue on next row.
* For non-empty element at index <var>x</var>, values on the left border are given by pixels at coordinate
* {@code x} and values on the right border are given by pixels at coordinate {@code x+1}. Example:
*
* {@preformat text
* ○╌╌╌╌╌╌●╱
* ╎ Top ╱
* ╎ [x] ╱╎
* ●╌╌╌╌╌╌●╌╌╌╌╱╌○
* ╎ Left ╎██████╎ ← Cell where to create a segment
* ─┼──────┼██████╎
* ○╌╌╌╌╌╌○╌╌╌╌╌╌○
* ↑
* x coordinate of first pixel (upper-left corner)
* }
*/
private final Polyline[] 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.
*
* <h4>Map keys</h4>
* Keys are grid coordinates rounded toward 0. The coordinate having fraction digits has its bits inverted
* 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
* 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)
*/
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.
*
* @see #writeTo(Joiner, Polyline[], boolean)
*/
private Joiner path;
/**
* The isolines as a Java2D shape, created by {@link #finish()}.
* This is the shape to be returned to user for this level after we finished to process all cells.
*/
Shape shape;
/**
* Creates new isoline levels for the given value.
*
* @param band band number where to read values in the {@link #window} array.
* @param value the isoline level value.
* @param width the contouring grid cell width (one cell smaller than image width).
*/
Level(final int band, final double value, final int width) {
this.band = band;
this.value = value;
partialPaths = new HashMap<>();
polylineOnLeft = new Polyline();
polylinesOnTop = new Polyline[width];
for (int i=0; i<width; i++) {
polylinesOnTop[i] = new Polyline();
}
}
/**
* Initializes the {@link #isDataAbove} value with values for the column on the right side.
* After this method call, the {@link #UPPER_RIGHT} and {@link #LOWER_RIGHT} bits still need to be set.
*
* @see Isolines#setMaskBit(double, int)
*/
final void nextColumn() {
/*
* Move bits on the right side to the left side.
* The 1 operand in >>> is the hard-coded value
* of log2(UPPER_RIGHT) - log2(UPPER_LEFT)
* and log2(LOWER_RIGHT) - log2(LOWER_LEFT).
*/
isDataAbove = (isDataAbove & (UPPER_RIGHT | LOWER_RIGHT)) >>> 1;
}
/**
* Adds segments computed for values in a single pixel. Interpolations are determined by the 4 lowest bits
* of {@link #isDataAbove}. The {@link #polylineOnLeft} and {@code polylinesOnTop[x]} elements are updated
* by this method.
*
* <h4>How NaN values are handled</h4>
* This algorithm does not need special attention for {@link Double#NaN} values. Interpolations will produce
* {@code NaN} values and append them to the correct polyline (which does not depend on interpolation result)
* like real values. Those NaN values will be filtered later in another method, when copying coordinates in
* the {@link PathBuilder}.
*/
@SuppressWarnings("AssertWithSideEffects")
final void interpolate() throws TransformException {
/*
* Note: `interpolateMissingLeftSide()` and `interpolateMissingTopSide(…)` should do interpolations
* only for cells in the first column and first row respectively. We could avoid those method calls
* for all other cells if we add two flags in the `isDataAbove` bitmask: FIRST_ROW and FIRST_COLUMN.
* The switch cases then become something like below:
*
* case <bitmask> | FIRST_COLUMN | FIRST_ROW:
* case <bitmask> | FIRST_COLUMN: {
* interpolateMissingLeftSide();
* // Fall through
* }
* case <bitmask> | FIRST_ROW:
* case <bitmask>: {
* // Interpolations on other borders.
* break;
* }
*
* We tried that approach, but benchmarking on Java 15 suggested a small performance decrease
* instead of an improvement. It may be worth to try again in the future, after advancement
* in compiler technology.
*/
switch (isDataAbove) {
default: {
throw new AssertionError(isDataAbove); // Should never happen.
}
/* ○╌╌╌╌╌╌○ ●╌╌╌╌╌╌●
* ╎ ╎ ╎ ╎
* ╎ ╎ ╎ ╎
* ○╌╌╌╌╌╌○ ●╌╌╌╌╌╌●
*/
case 0:
case UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT: {
assert polylinesOnTop[x].isEmpty();
assert polylineOnLeft .isEmpty();
break;
}
/* ○╌╌╌╌╌╌○ ●╌╌╌╌╌╌●
* ─┼──────┼─ ─┼──────┼─
* ╎ ╎ ╎ ╎
* ●╌╌╌╌╌╌● ○╌╌╌╌╌╌○
*/
case LOWER_LEFT | LOWER_RIGHT:
case UPPER_LEFT | UPPER_RIGHT: {
assert polylinesOnTop[x].isEmpty();
interpolateMissingLeftSide();
interpolateOnRightSide(); // Will be the left side of next column.
break;
}
/* ○╌╌╌┼╌╌● ●╌╌╌┼╌╌○
* ╎ │ ╎ ╎ │ ╎
* ╎ │ ╎ ╎ │ ╎
* ○╌╌╌┼╌╌● ●╌╌╌┼╌╌○
*/
case UPPER_RIGHT | LOWER_RIGHT:
case UPPER_LEFT | LOWER_LEFT: {
assert polylineOnLeft.isEmpty();
final Polyline polylineOnTop = polylinesOnTop[x];
interpolateMissingTopSide(polylineOnTop);
interpolateOnBottomSide(polylineOnTop); // Will be top side of next row.
break;
}
/* ╲○╌╌╌╌╌╌○ ╲●╌╌╌╌╌╌●
* ╲ ╎ ╲ ╎
* ╎╲ ╎ ╎╲ ╎
* ●╌╲╌╌╌╌○ ○╌╲╌╌╌╌●
*/
case LOWER_LEFT:
case UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT: {
assert polylinesOnTop[x].isEmpty();
interpolateMissingLeftSide();
interpolateOnBottomSide(polylinesOnTop[x].transferFrom(polylineOnLeft));
break;
}
/* ○╌╌╌╌╲╌● ●╌╌╌╌╲╌○
* ╎ ╲╎ ╎ ╲╎
* ╎ ╲ ╎ ╲
* ○╌╌╌╌╌╌○╲ ●╌╌╌╌╌╌●╲
*/
case UPPER_RIGHT:
case UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT: {
assert polylineOnLeft.isEmpty();
interpolateMissingTopSide(polylineOnLeft.transferFrom(polylinesOnTop[x]));
interpolateOnRightSide();
break;
}
/* ○╌╌╌╌╌╌○╱ ●╌╌╌╌╌╌●╱
* ╎ ╱ ╎ ╱
* ╎ ╱╎ ╎ ╱╎
* ○╌╌╌╌╱╌● ●╌╌╌╌╱╌○
*/
case LOWER_RIGHT:
case UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT: {
assert polylinesOnTop[x].isEmpty();
assert polylineOnLeft .isEmpty();
interpolateOnRightSide();
interpolateOnBottomSide(polylinesOnTop[x].attach(polylineOnLeft));
// Bottom of this cell will be top of next row.
break;
}
/* ●╌╱╌╌╌╌○ ○╌╱╌╌╌╌●
* ╎╱ ╎ ╎╱ ╎
* ╱ ╎ ╱ ╎
* ╱○╌╌╌╌╌╌○ ╱●╌╌╌╌╌╌●
*/
case UPPER_LEFT:
case UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT: {
closeLeftWithTop(polylinesOnTop[x]);
break;
}
/* ○╌╱╌╌╌╌●╱ ╲●╌╌╌╌╲╌○
* ╎╱ ╱ ╲ ╲╎
* ╱ ╱╎ ╎╲ ╲
* ╱●╌╌╌╌╱╌○ ○╌╲╌╌╌╌●╲
*
* Disambiguation of saddle points: use the average data value for the center of the cell.
* If the estimated center value is greater than the isoline value, the above drawings are
* okay and we do not need to change `isDataAbove`. This is the left side illustrated below.
* But if the center value is below isoline value, then we need to flip `isDataAbove` bits
* (conceptually; not really because we need to keep `isDataAbove` value for next iteration).
* This is the right side illustrated below.
*
* ○╱╌╌●╱ ╲●╌╌╲○ ╲○╌╌╲● ●╱╌╌○╱
* ╱ ● ╱ ╲ ● ╲ ╲ ○ ╲ ╱ ○ ╱
* ╱●╌╌╱○ ○╲╌╌●╲ ●╲╌╌○╲ ╱○╌╌╱●
*/
case UPPER_RIGHT | LOWER_LEFT:
case UPPER_LEFT | LOWER_RIGHT: {
double average = 0;
{ // Compute sum of 4 corners.
final double[] data = window;
int p = band;
do average += data[p];
while ((p += pixelStride) < data.length);
assert (p -= band) == pixelStride * 4 : p;
average /= 4;
}
boolean LLtoUR = isDataAbove == (LOWER_LEFT | UPPER_RIGHT);
LLtoUR ^= (average <= value);
final Polyline polylineOnTop = polylinesOnTop[x];
if (LLtoUR) {
closeLeftWithTop(polylineOnTop);
interpolateOnRightSide();
interpolateOnBottomSide(polylineOnTop.attach(polylineOnLeft));
} else {
interpolateMissingLeftSide();
final Polyline swap = new Polyline().transferFrom(polylineOnTop);
interpolateOnBottomSide(polylineOnTop.transferFrom(polylineOnLeft));
interpolateMissingTopSide(polylineOnLeft.transferFrom(swap));
interpolateOnRightSide();
}
break;
}
}
}
/**
* Appends to {@link #polylineOnLeft} a point interpolated on the left side if that point is missing.
* This interpolation should happens only in the first column.
*/
private void interpolateMissingLeftSide() {
if (polylineOnLeft.size == 0) {
polylineOnLeft.append(translateX + (x),
translateY + (y + interpolate(0, 2*pixelStride)));
}
}
/**
* 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) {
if (polylineOnTop.size == 0) {
interpolateOnTopSide(polylineOnTop);
}
}
/**
* Appends to the given polyline a point interpolated on the top side.
*/
private void interpolateOnTopSide(final Polyline appendTo) {
appendTo.append(translateX + (x + interpolate(0, pixelStride)),
translateY + (y));
}
/**
* Appends to {@link #polylineOnLeft} a point interpolated on the right side.
* The polyline on right side will become {@code polylineOnLeft} in next column.
*/
private void interpolateOnRightSide() {
polylineOnLeft.append(translateX + (x + 1),
translateY + (y + interpolate(pixelStride, 3*pixelStride)));
}
/**
* 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) {
polylineOnTop.append(translateX + (x + interpolate(2*pixelStride, 3*pixelStride)),
translateY + (y + 1));
}
/**
* Interpolates the position where the isoline passes between two values.
*
* @param i1 index of first value in the buffer, ignoring band offset.
* @param i2 index of second value in the buffer, ignoring band offset.
* @return a value interpolated between the values at the two given indices.
*/
private double interpolate(final int i1, final int i2) {
final double[] data = window;
final int p = band;
final double v1 = data[p + i1];
final double v2 = data[p + i2];
return (value - v1) / (v2 - v1);
}
/**
* 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:
*
* {@preformat text
* ●╌╱╌╌╌╌○ ○╌╱╌╌╌╌● ○╌╱╌╌╌╌●╱
* ╎╱ ╎ ╎╱ ╎ ╎╱ ╱
* ╱ ╎ ╱ ╎ ╱ ╱╎
* ╱○╌╌╌╌╌╌○ ╱●╌╌╌╌╌╌● ╱●╌╌╌╌╱╌○
* }
*
* This method does itself the interpolations on left side and top side. The two polylines
* {@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.
*/
private void closeLeftWithTop(final Polyline polylineOnTop) throws TransformException {
interpolateMissingLeftSide();
interpolateMissingTopSide(polylineOnTop);
final Polyline[] 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.
* Consequently the points we just interpolated will be first point and last point before closing.
*/
polylines = new Polyline[] {polylineOnTop, polylineOnLeft}; // (reverse, forward) point order.
} else {
/*
* 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);
if (fragment.isEmpty()) {
/*
* 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[] {
polylineOnLeft.opposite, polylineOnLeft, polylineOnTop, polylineOnTop.opposite
};
} else if (fragment.addOrMerge(partialPaths)) {
/*
* The fragment has been merged with previously existing fragments and became a polygon.
* We can write the polygon immediately. There are no more references to those coordinates
* in the `partialPaths` map.
*/
polylines = fragment.toPolylines();
} else {
return;
}
}
path = writeTo(path, polylines, true);
}
/**
* 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 {
final Unclosed fragment = new Unclosed(polyline, null);
final Polyline[] polylines;
final boolean close;
if (fragment.isEmpty()) {
close = false;
polylines = new Polyline[] {polyline.opposite, polyline}; // (reverse, forward) point order.
} else {
close = fragment.addOrMerge(partialPaths);
if (!close) {
// Keep in `partialPaths`. Maybe it can be closed later.
return;
}
polylines = fragment.toPolylines();
}
path = writeTo(path, polylines, close);
}
/**
* 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.
*/
final void finishedRow() throws TransformException {
if (!polylineOnLeft.transferToOpposite()) {
writeUnclosed(polylineOnLeft);
}
isDataAbove = 0;
}
/**
* 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.
*/
final void finish() throws TransformException {
assert polylineOnLeft.isEmpty();
polylineOnLeft.coordinates = null;
/*
* This method sets various values to null for letting the garbage collector do its work.
* This is okay for a `Level` instance which is not going to be used anymore, except for
* reading the `shape` field.
*/
for (int i=0; i < polylinesOnTop.length; i++) {
writeUnclosed(polylinesOnTop[i]);
polylinesOnTop[i] = null;
}
assert isConsistent();
}
/**
* Verifies that {@link #partialPaths} consistency. Used for assertions only.
*/
private boolean isConsistent() {
for (final Map.Entry<Point,Unclosed> entry : partialPaths.entrySet()) {
if (!entry.getValue().isExtremity(entry.getKey())) return false;
}
return true;
}
/**
* Transfers all {@code other} polylines into this instance. The {@code other} instance should be a neighbor,
* i.e. an instance sharing a border with this instance. The {@code other} instance will become empty after
* this method call.
*
* @param other a neighbor level (on top, left, right or bottom) to merge with this level.
* @throws TransformException if an error occurred during polylines creation.
*/
final void merge(final Level other) throws TransformException {
assert other != this && other.value == value;
if (path == null) {
path = other.path;
} else {
path.append(other.path);
}
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();
if (done.put(fragment, Boolean.TRUE) == null) {
assert fragment.isExtremity(entry.getKey());
if (fragment.addOrMerge(partialPaths)) {
path = writeTo(path, fragment.toPolylines(), true);
fragment.clear();
}
}
entry.setValue(null); // Let the garbage collector do its work.
}
}
/**
* Flushes any pending {@link #partialPaths} to {@link #path}. This method is invoked after
* {@link #finish()} has been invoked for all sub-regions (many sub-regions may exist if
* isoline generation has been splitted for parallel computation).
*
* @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();
assert fragment.isExtremity(entry.getKey());
if (!fragment.isEmpty()) {
path = writeTo(path, fragment.toPolylines(), false);
fragment.clear(); // Necessary because the same list appears twice in the map.
}
entry.setValue(null); // Let the garbage collector do its work.
}
if (path != null) {
shape = path.build();
path = null;
}
}
}
/**
* 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 IsolineTracer#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);
}
}
/**
* 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
* 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 Polyline polylineOnLeft, final Polyline polylineOnTop) {
/*
* Search for first point and last point by inspecting `Polyline`s 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 Polyline (iteration 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 `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.
*/
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).
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) {
Polyline 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 Polyline 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 Polyline} instances.
* {@code Polyline} instances at even index should be written with their points in reverse order.
*
* @see #writeTo(Joiner, Polyline[], boolean)
*/
final Polyline[] toPolylines() {
final Polyline[] polylines = new Polyline[size()];
for (int i=0; i<polylines.length; i++) {
final double[] coordinates = get(i);
if (coordinates != null) {
polylines[i] = new Polyline(coordinates);
}
}
return polylines;
}
}
/**
* 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.
*
* <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 Polyline} 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.
*/
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 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>
*
* @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 -= Polyline.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 += Polyline.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*Polyline.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 -= Polyline.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 / Polyline.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.
* All given polylines are cleared by this method.
*
* @param path where to write the polylines, or {@code null} if not yet created.
* @param polylines the polylines to write.
* @param close whether to close the polygon.
* @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 {
for (int pi=0; pi < polylines.length; pi++) {
final Polyline p = polylines[pi];
if (p == null) {
continue;
}
final int size = p.size;
if (size == 0) {
assert p.isEmpty();
continue;
}
if (path == null) {
path = new Joiner(gridToCRS);
}
path.append(p.coordinates, size, (pi & 1) == 0);
p.clear();
}
if (path != null) {
path.createPolyline(close);
}
return path;
}
}