blob: 698fc1b24755025943101fd39cffb94cf2fe135b [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.awt.Point;
import java.util.Map;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import org.apache.sis.internal.util.Numerics;
/**
* List of {@code PolylineBuffer} coordinates that have not yet been closed.
* Each {@code double[]} in this list is a copy of a {@link PolylineBuffer} used by {@link Tracer.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 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 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.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.3
*
* @see Tracer.Level#partialPaths
*
* @since 1.1
* @module
*/
@SuppressWarnings({"CloneableImplementsClone", "serial"}) // Not intended to be cloned or serialized.
final class Fragments 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 Tracer.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.
*/
Fragments(final PolylineBuffer polylineOnLeft, final PolylineBuffer polylineOnTop) {
/*
* Search for first and last point by inspecting `PolylineBuffer` instances 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 PolylineBuffer (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 `PolylineBuffer` point is first `Fragments` point because of reverse iteration order.
* (2) First `PolylineBuffer` point is first `Fragments` point because of forward iteration order.
* (3) Last `PolylineBuffer` point is last `Fragments` point because of forward iteration order.
* (4) First `PolylineBuffer` point is last `Fragments` point because of reverse iteration order.
*/
int index = 0;
do {
PolylineBuffer 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 `Fragments` 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 `Fragments`). Leave this
* list empty for avoiding the copies done by `take(…)` calls. Instead,
* callers should write polylines in `Tracer.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) {
PolylineBuffer 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 PolylineBuffer 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 Tracer.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 Tracer.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,Fragments> partialPaths) {
final Fragments before = partialPaths.remove(firstPoint);
final Fragments 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 Fragments 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 PolylineBuffer} instances.
* {@code PolylineBuffer} instances at even index should be written with their points in reverse order.
*
* @see #writeTo(Joiner, PolylineBuffer[], boolean)
*/
final PolylineBuffer[] toPolylines() {
final PolylineBuffer[] polylines = new PolylineBuffer[size()];
for (int i=0; i<polylines.length; i++) {
final double[] coordinates = get(i);
if (coordinates != null) {
polylines[i] = new PolylineBuffer(coordinates);
}
}
return polylines;
}
}