blob: d6e3e6cfc67dea9629536af1b64bd0a46b70cf1b [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 java.util.Map;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
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.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 Tracer} is used for doing a single iteration over all image pixels.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.3
*
* @see <a href="https://en.wikipedia.org/wiki/Marching_squares">Marching squares on Wikipedia</a>
*
* @since 1.1
* @module
*/
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.
*
* <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.
* The range is 0 inclusive to {@code domain.width} exclusive.
*/
int x;
/**
* Pixel coordinate on the top side of the cell where to interpolate.
* The range is 0 inclusive to {@code domain.height} exclusive.
*/
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).
*/
Tracer(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 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,
* 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 {@link PolylineBuffer#isEmpty() is empty} if the cell in previous iteration was like below
* (no line cross the right border):
*
* {@preformat text
* ○╌╲╌╌╌╌● ○╌╌╌┼╌╌●
* ╎ ╲ ╎ ╎ │ ╎
* ╎ ╲ ╎ ╎ │ ╎
* ○╌╌╌╌╲╌● ○╌╌╌┼╌╌●
* }
*/
private final PolylineBuffer 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 PolylineBuffer[] polylinesOnTop;
/**
* 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
* by the {@code ~} operator. For each point, there is at most one coordinate having such fraction digits.
*
* <h4>Map values</h4>
* {@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,Fragments> partialPaths;
/**
* Builder of isolines as a Java2D shape, created when first needed.
* The {@link PolylineBuffer} coordinates are copied in this path when a geometry is closed
* and transformed using {@link #gridToCRS}. This is almost final result; the only difference
* compared to {@link #shape} is that the coordinates are not yet wrapped in a {@link Shape}.
*
* @see #writeTo(Joiner, PolylineBuffer[], boolean)
* @see PolylineStage#FINAL
*/
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.
*
* @see PolylineStage#FINAL
*/
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 PolylineBuffer();
polylinesOnTop = new PolylineBuffer[width];
for (int i=0; i<width; i++) {
polylinesOnTop[i] = new PolylineBuffer();
}
}
/**
* 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 PolylineBuffer 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 PolylineBuffer polylineOnTop = polylinesOnTop[x];
if (LLtoUR) {
closeLeftWithTop(polylineOnTop);
interpolateOnRightSide();
interpolateOnBottomSide(polylineOnTop.attach(polylineOnLeft));
} else {
interpolateMissingLeftSide();
final PolylineBuffer swap = new PolylineBuffer().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 PolylineBuffer polylineOnTop) {
if (polylineOnTop.size == 0) {
interpolateOnTopSide(polylineOnTop);
}
}
/**
* Appends to the given polyline a point interpolated on the top side.
*/
private void interpolateOnTopSide(final PolylineBuffer 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 PolylineBuffer 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 {@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
* ●╌╱╌╌╌╌○ ○╌╱╌╌╌╌● ○╌╱╌╌╌╌●╱
* ╎╱ ╎ ╎╱ ╎ ╎╱ ╱
* ╱ ╎ ╱ ╎ ╱ ╱╎
* ╱○╌╌╌╌╌╌○ ╱●╌╌╌╌╌╌● ╱●╌╌╌╌╱╌○
* }
*
* 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 Tracer#gridToCRS} transform can not be applied.
*/
private void closeLeftWithTop(final PolylineBuffer polylineOnTop) throws TransformException {
interpolateMissingLeftSide();
interpolateMissingTopSide(polylineOnTop);
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 `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 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
* in the `path` now. But maybe we can close the polygon later after more polylines are attached.
*/
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.
* Better to write the polylines now for avoiding temporary copies of their coordinates.
*/
polylines = new PolylineBuffer[] {
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 writeFragment(final PolylineBuffer polyline) throws TransformException {
final Fragments fragment = new Fragments(polyline, null);
final PolylineBuffer[] polylines;
final boolean close;
if (fragment.isEmpty()) {
close = false;
polylines = new PolylineBuffer[] {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 {@link PolylineBuffer} will not be continued by cells on next rows.
*/
final void finishedRow() throws TransformException {
if (!polylineOnLeft.transferToOpposite()) {
writeFragment(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 {@link Level} instance 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++) {
writeFragment(polylinesOnTop[i]);
polylinesOnTop[i] = null;
}
assert isConsistent();
}
/**
* Verifies that {@link #partialPaths} consistency. Used for assertions only.
*/
private boolean isConsistent() {
for (final Map.Entry<Point,Fragments> 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<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)) {
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,Fragments> entry : partialPaths.entrySet()) {
final Fragments 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;
}
}
/**
* Appends the pixel coordinates of this level 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 Isolines#toRawPath()
*/
@Debug
final void toRawPath(final Map<PolylineStage,Path2D> appendTo) {
PolylineStage.FINAL.add(appendTo, (path != null) ? path.snapshot() : shape);
polylineOnLeft.toRawPath(appendTo);
for (final PolylineBuffer p : polylinesOnTop) {
if (p != null) p.toRawPath(appendTo);
}
}
}
/**
* 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.
* @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 PolylineBuffer[] polylines, final boolean close) throws TransformException {
for (int pi=0; pi < polylines.length; pi++) {
final PolylineBuffer 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;
}
}