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