blob: 5b9c8acc5d7843da64775f60efc2aad8485c0ac4 [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.Iterator;
import java.util.Collections;
import java.awt.Shape;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.PathIterator;
import java.awt.geom.AffineTransform;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.Classes;
/**
* A polylines or polygons as a Java2D {@link Shape}. This class has some similarities
* with {@link java.awt.geom.Path2D} with the following differences:
*
* <ul>
* <li>No synchronization.</li>
* <li>Line segments only (no Bézier curves).</li>
* <li>No multi-polylines (e.g. no "move to" operation in the middle).</li>
* <li>Coordinates "compressed" (with a simple translation) as {@code float}.</li>
* </ul>
*
* <h2>Precision and pseudo-compression</h2>
* Coordinates are stored with {@code float} precision for reducing memory usage with large polylines.
* This is okay if coordinates are approximate anyway, for example if they are values interpolated by
* the {@code Isolines} class. For attenuating the precision lost, coordinate values are converted by
* applications of the two following steps:
*
* <ol class="verbose">
* <li>First, translate coordinates toward zero. For example latitude or longitude values in the
* [50 … 60]° range have a precision of about 4E-6° (about 0.4 meter). But translating those
* coordinates to the [-5 … 5]° range increases their precision to 0.05 meter. The precision
* gain is more important when the original coordinates are projected coordinates with high
* "false easting" / "false northing" parameters.</li>
* <li>Next, if minimum or maximum coordinate values are outside the range allowed by {@code float}
* exponent values, multiply coordinates by a power of 2 (<strong>Not yet implemented)</strong>.
* Note that precision gain happens only when values are made closer to zero by a translation.
* Making coordinates closer to zero by a multiplication has no effect on the precision.
* This step is required only for avoiding overflow or underflow.</li>
* </ol>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.3
* @since 1.1
* @module
*/
class Polyline extends FlatShape {
/**
* The "compressed" coordinate values as (x,y) tuples. To get the desired coordinates,
* those values must be converted by the {@link #inflate} transform.
*/
private final float[] coordinates;
/**
* The transform from {@link #coordinates} values to the values given by {@link Iter}.
* This transform is usually only a translation.
*/
private final AffineTransform inflate;
/**
* Creates a new polylines with the given coordinates.
* The {@code coordinates} array shall not be empty.
*
* @param coordinates the coordinate values as (x,y) tuples.
* @param size number of valid value in {@code coordinates} array.
*/
Polyline(final double[] coordinates, final int size) {
super(coordinates, size);
this.coordinates = new float[size];
final double tx = round(bounds.getCenterX(), bounds.xmin, bounds.xmax);
final double ty = round(bounds.getCenterY(), bounds.ymin, bounds.ymax);
inflate = AffineTransform.getTranslateInstance(tx, ty);
AffineTransform.getTranslateInstance(-tx, -ty).transform(coordinates, 0, this.coordinates, 0, size / 2);
}
/**
* Rounds the translation to an arbitrary number of bits (currently 20).
* The intent is to avoid that zero values become something like 1E-9.
* The number of bits that we kept should be less that the number of bits
* in the significand (mantissa) of {@code float} type.
*/
private static double round(final double center, final double min, final double max) {
final int e = Math.getExponent(Math.max(Math.abs(min), Math.abs(max))) - (Numerics.SIGNIFICAND_SIZE_OF_FLOAT - 3);
return Math.scalb(Math.round(Math.scalb(center, -e)), e);
}
/**
* Tests if the given coordinates are inside the boundary of this shape.
*/
@Override
public boolean contains(final double x, final double y) {
return bounds.contains(x, y) && Path2D.contains(iterator(), x, y);
}
/**
* Tests if the interior of this shape intersects the interior of the given rectangle.
* May conservatively return {@code true} if an intersection is probable but accurate
* answer would be too costly to compute.
*/
@Override
public boolean intersects(final double x, final double y, final double w, final double h) {
return bounds.intersects(x, y, w, h) && Path2D.intersects(iterator(), x, y, w, h);
}
/**
* Tests if the interior of this shape intersects the interior of the given rectangle.
* May conservatively return {@code true} if an intersection is probable but accurate
* answer would be too costly to compute.
*/
@Override
public boolean intersects(final Rectangle2D r) {
return bounds.intersects(r) && Path2D.intersects(iterator(), r);
}
/**
* Tests if the interior of this shape entirely contains the interior of the given rectangle.
* May conservatively return {@code false} if an accurate answer would be too costly to compute.
*/
@Override
public boolean contains(final double x, final double y, final double w, final double h) {
return bounds.contains(x, y, w, h) && Path2D.contains(iterator(), x, y, w, h);
}
/**
* Tests if the interior of this shape entirely contains the interior of the given rectangle.
* May conservatively return {@code false} if an accurate answer would be too costly to compute.
*/
@Override
public boolean contains(final Rectangle2D r) {
return bounds.contains(r) && Path2D.contains(iterator(), r);
}
/**
* Returns an iterator over coordinates without user transform.
*/
private PathIterator iterator() {
return getPathIterator(null);
}
/**
* Returns an iterator over coordinates in this polyline.
*/
@Override
public final PathIterator getPathIterator(final AffineTransform at) {
return new Iter(at, this, Collections.emptyIterator());
}
/**
* Iterator over polyline(s) or polygon(s) coordinates. This implementation requires that all {@link Polyline}
* instances have non-empty coordinates array, otherwise {@link ArrayIndexOutOfBoundsException} will occur.
*/
static final class Iter implements PathIterator {
/**
* The user-specified transform, or {@code null} if none.
*/
private final AffineTransform toUserSpace;
/**
* The transform to apply on each coordinate tuple. This is the concatenation of user-specified
* transform with {@link Polyline#inflate}. Shall not be null, unless the iterator is empty.
*/
private AffineTransform inflate;
/**
* Next polylines on which to iterate, or an empty iterator if none.
*/
private final Iterator<Polyline> polylines;
/**
* Coordinates to return (after conversion by {@link #inflate}) in calls to {@link #currentSegment(double[])}.
*/
private float[] coordinates;
/**
* Current position in {@link #coordinates} array.
*/
private int position;
/**
* {@code true} if {@link #currentSegment(double[])} shall return {@link #SEG_CLOSE}.
*/
private boolean closing;
/**
* Whether current coordinates make a polygon. If {@code true}, then iteration shall
* emit a closing {@link #SEG_CLOSE} type before to move to next polyline or polygon.
*/
private boolean isPolygon;
/**
* Whether iteration is finished.
*/
private boolean isDone;
/**
* Creates an empty iterator.
*/
Iter() {
toUserSpace = null;
polylines = null;
isDone = true;
}
/**
* Creates a new iterator.
*
* @param at the transform to apply on each coordinate tuple.
* @param first the first polyline or polygon.
* @param next all other polylines or polygons.
*/
Iter(final AffineTransform at, final Polyline first, final Iterator<Polyline> next) {
if (at != null) {
inflate = new AffineTransform();
}
toUserSpace = at;
polylines = next;
setSource(first);
}
/**
* Initializes the {@link #coordinates}, {@link #isPolygon} and {@link #inflate} fields
* for iteration over coordinate values given by the specified polyline.
*/
private void setSource(final Polyline polyline) {
isPolygon = (polyline instanceof Polygon);
coordinates = polyline.coordinates;
if (toUserSpace != null) {
inflate.setTransform(toUserSpace);
inflate.concatenate(polyline.inflate);
} else {
inflate = polyline.inflate;
}
}
/**
* Arbitrary winding rule, since enclosing class do not yet compute shape interior.
*/
@Override
public int getWindingRule() {
return WIND_NON_ZERO;
}
/**
* Returns {@code true} if there are no more points to iterate.
*/
@Override
public boolean isDone() {
return isDone;
}
/**
* Moves to the next point.
*/
@Override
public void next() {
if ((position += 2) >= coordinates.length) {
if (isPolygon) {
closing = !closing;
if (closing) return;
}
if (polylines.hasNext()) {
setSource(polylines.next());
position = 0;
} else {
isDone = true;
}
}
}
/**
* Returns coordinates of current line segment.
*/
@Override
public int currentSegment(final float[] coords) {
if (closing) return SEG_CLOSE;
inflate.transform(coordinates, position, coords, 0, 1);
return (position == 0) ? SEG_MOVETO : SEG_LINETO;
}
/**
* Returns coordinates of current line segment.
*/
@Override
public int currentSegment(final double[] coords) {
if (closing) return SEG_CLOSE;
inflate.transform(coordinates, position, coords, 0, 1);
return (position == 0) ? SEG_MOVETO : SEG_LINETO;
}
}
/**
* Returns a string representation for debugging purposes.
*
* @return a debug string representation.
*/
@Override
public String toString() {
return Classes.getShortClassName(this) + '[' + (coordinates.length / 2) + " points]";
}
}