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