blob: f4ae2b28b5b9a44cb0cf019f4f91a4d3ebf625f2 [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.feature.j2d;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.awt.Shape;
import org.opengis.referencing.operation.TransformException;
/**
* Builds a {@link Polyline}, {@link Polygon} or {@link MultiPolylines} from given coordinates.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
public class PathBuilder {
/**
* Number of coordinates in a tuple.
*/
private static final int DIMENSION = 2;
/**
* The coordinates as (x,y) tuples. The number of valid coordinates is given by {@link #size}
* and this array is expanded as needed. Shall not contains {@link Double#NaN} values.
*/
private double[] coordinates;
/**
* Number of valid coordinates. This is twice the amount of points.
*/
private int size;
/**
* The polylines built from the coordinates.
*/
private final List<Polyline> polylines;
/**
* Creates a new builder.
*/
public PathBuilder() {
coordinates = new double[100];
polylines = new ArrayList<>();
}
/**
* Verifies that {@link #size} is even, positive and smaller than the given limit.
* This method is used for assertions.
*/
private boolean isValidSize(final int limit) {
return size >= 0 && size <= limit && (size & 1) == 0;
}
/**
* Adds all polylines defined in the other builder. The other builder shall have no polylines under
* construction, i.e. {@link #append(double[], int, boolean)} shall not have been invoked since last
* {@link #createPolyline(boolean)} invocation.
*
* @param other the other builder for which to add polylines, or {@code null} if none.
*/
public final void append(final PathBuilder other) {
if (other != null) {
assert other.size == 0;
polylines.addAll(other.polylines);
}
}
/**
* Appends the given coordinates to current polyline, omitting repetitive points.
* Coordinates are added to the same polyline than the one updated by previous calls
* to this method, unless {@link #createPolyline(boolean)} has been invoked before.
* The {@link #filterChunk(double[], int, int)} method is invoked after the points have been added
* for allowing subclasses to apply customized filtering in addition to the above-cited removal
* of repetitive points.
*
* <h4>NaN coordinate values</h4>
* If the given array contains {@link Double#NaN} values, then the coordinates before and after NaNs are stored
* in two distinct polylines. This is an exception to above paragraph saying that this method does not create
* new polyline. The {@link #filterChunk(double[], int, int)} method will be invoked for each of those polylines.
*
* @param source coordinates to copy.
* @param limit index after the last coordinate to copy. Must be an even number.
* @param reverse whether to copy (x,y) tuples in reverse order.
* @throws TransformException if {@link #filterFull(double[], int)} wanted to apply a coordinate operation
* and that transform failed.
*/
public final void append(final double[] source, final int limit, final boolean reverse) throws TransformException {
assert limit >= 0 && (limit & 1) == 0 : limit;
int offset = size;
if (limit >= coordinates.length - offset) {
coordinates = Arrays.copyOf(coordinates, Math.addExact(offset, Math.max(offset, limit)));
}
final double[] coordinates = this.coordinates;
double px, py; // Previous point.
if (offset != 0) {
px = coordinates[offset - 2];
py = coordinates[offset - 1];
} else {
px = py = Double.NaN;
}
for (int i=0; i<limit;) {
final double x, y;
if (reverse) {
y = source[limit - ++i];
x = source[limit - ++i];
} else {
x = source[i++];
y = source[i++];
}
if (x != px || y != py) {
if (Double.isNaN(x) || Double.isNaN(y)) {
if (offset != 0) {
size = filterChunk(coordinates, size, offset);
assert isValidSize(offset) : size;
createPolyline(false);
offset = 0;
}
} else {
coordinates[offset++] = x;
coordinates[offset++] = y;
}
px = x;
py = y;
}
}
size = filterChunk(coordinates, size, offset);
assert isValidSize(offset) : size;
}
/**
* Applies a custom filtering on the coordinates added by a call to {@link #append(double[], int, boolean)}.
* The default implementation does nothing. Subclasses can override this method for changing or removing some
* coordinate values.
*
* <p>This method is invoked at least once per {@link #append(double[], int, boolean)} call.
* Consequently it is not necessarily invoked with the coordinates of a complete polyline or polygon,
* because caller can build a polyline with multiple calls to {@code append(…)}.
* If those {@code append(…)} calls correspond to some logical chunks (at users choice),
* this {@code filterChunk(…)} method allows users to exploit this subdivision in their processing.</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.
* Should be {@code upper}, unless some coordinates have been removed.
* Must be an even number ≥ 0 and ≤ upper.
*/
protected int filterChunk(double[] coordinates, int lower, int upper) {
return upper;
}
/**
* Applies a custom filtering on the coordinates of a polyline or polygon.
* The default implementation does nothing. Subclasses can override this method for changing or removing some
* coordinate values. For example a subclass could decimate points using Ramer–Douglas–Peucker algorithm.
* Contrarily to {@link #filterChunk(double[], int, int)}, this method is invoked when the coordinates of
* the full polyline or polygon are available. If polyline points need to be transformed before to build
* the final geometry, this is the right place to do so.
*
* @param coordinates the coordinates to filter. Values can be modified in-place.
* @param upper index after the last coordinate to filter. Always even.
* @return number of valid coordinates after filtering.
* Should be {@code upper}, unless some coordinates have been removed.
* Must be an even number ≥ 0 and ≤ upper.
* @throws TransformException if this method wanted to apply a coordinate operation
* and that transform failed.
*/
protected int filterFull(double[] coordinates, int upper) throws TransformException {
return upper;
}
/**
* Creates a new polyline or polygon with the coordinates added by {@link #append(double[], int, boolean)}.
* If the first point and last point have the same coordinates, then the polyline is automatically closed as
* a polygon. After this method call, next calls to {@code append(…)} will add coordinates in a new polyline.
*
* @param close whether to force a polygon even if source and last points are different.
* @throws TransformException if {@link #filterFull(double[], int)} wanted to apply a coordinate operation
* and that transform failed.
*/
public final void createPolyline(boolean close) throws TransformException {
size = filterFull(coordinates, size);
assert isValidSize(coordinates.length) : size;
/*
* If the point would be alone, discard the lonely point because it would be invisible
* (a "move to" operation without "line to"). If there is two points, they should not
* be equal because `append(…)` filtered repetitive points.
*/
if (size >= 2*DIMENSION) {
if (coordinates[0] == coordinates[size - 2] &&
coordinates[1] == coordinates[size - 1])
{
size -= DIMENSION;
close = true;
}
polylines.add(close ? new Polygon(coordinates, size) : new Polyline(coordinates, size));
}
size = 0;
}
/**
* Returns a shape containing all polylines or polygons added to this builder.
* The {@link #createPolyline(boolean)} method should be invoked before this method
* for making sure that there are no pending polylines.
*
* @return the polyline, polygon or collection of polylines.
* May be {@code null} if no polyline or polygon has been created.
*/
public final Shape build() {
switch (polylines.size()) {
case 0: return null;
case 1: return polylines.get(0);
default: return new MultiPolylines(polylines.toArray(new Polyline[polylines.size()]));
}
}
/**
* Returns a snapshot of currently added polylines or polygons without modifying the state of this builder.
* It is safe to continue building the shape and invoke this method again later for progressive rendering.
*
* @return the polyline, polygon or collection of polylines added so far.
* May be {@code null} if no polyline or polygon has been created.
*/
public final Shape snapshot() {
return build();
}
/**
* Returns a string representation of the polyline under construction for debugging purposes.
* Current implementation formats only the first and last points, and tells how many points are between.
*/
@Override
public String toString() {
return toString(coordinates, size);
}
/**
* Returns a string representation of the given coordinates for debugging purposes.
* Current implementation formats only the first and last points, and tells how many
* points are between.
*
* @param coordinates the coordinates for which to return a string representation.
* @param size index after the last valid coordinate in {@code coordinates}.
* @return a string representation for debugging purposes.
*/
public static String toString(final double[] coordinates, final int size) {
final StringBuilder b = new StringBuilder(30).append('[');
if (size >= DIMENSION) {
b.append((float) coordinates[0]).append(", ").append((float) coordinates[1]);
final int n = size - DIMENSION;
if (n >= DIMENSION) {
b.append(", ");
if (size >= DIMENSION*3) {
b.append(" … (").append(size / DIMENSION - 2).append(" pts) … ");
}
b.append((float) coordinates[n]).append(", ").append((float) coordinates[n+1]);
}
}
return b.append(']').toString();
}
}