package org.osmdroid.utils;
import java.util.ArrayList;
import org.oscim.core.GeoPoint;
/**
* Reduces the number of points in a shape using the Douglas-Peucker algorithm. <br>
* From:
* http://www.phpriot.com/articles/reducing-map-path-douglas-peucker-algorithm/4<br>
* Ported from PHP to Java. "marked" array added to optimize.
* @author M.Kergall
*/
public class DouglasPeuckerReducer {
/**
* Reduce the number of points in a shape using the Douglas-Peucker
* algorithm
* @param shape
* The shape to reduce
* @param tolerance
* The tolerance to decide whether or not to keep a point, in the
* coordinate system of the points (micro-degrees here)
* @return the reduced shape
*/
public static ArrayList<GeoPoint> reduceWithTolerance(ArrayList<GeoPoint> shape,
double tolerance)
{
int n = shape.size();
// if a shape has 2 or less points it cannot be reduced
if (tolerance <= 0 || n < 3) {
return shape;
}
boolean[] marked = new boolean[n]; //vertex indexes to keep will be marked as "true"
for (int i = 1; i < n - 1; i++)
marked[i] = false;
// automatically add the first and last point to the returned shape
marked[0] = marked[n - 1] = true;
// the first and last points in the original shape are
// used as the entry point to the algorithm.
douglasPeuckerReduction(
shape, // original shape
marked, // reduced shape
tolerance, // tolerance
0, // index of first point
n - 1 // index of last point
);
// all done, return the reduced shape
ArrayList<GeoPoint> newShape = new ArrayList<GeoPoint>(n); // the new shape to return
for (int i = 0; i < n; i++) {
if (marked[i])
newShape.add(shape.get(i));
}
return newShape;
}
/**
* Reduce the points in shape between the specified first and last index.
* Mark the points to keep in marked[]
* @param shape
* The original shape
* @param marked
* The points to keep (marked as true)
* @param tolerance
* The tolerance to determine if a point is kept
* @param firstIdx
* The index in original shape's point of the starting point for
* this line segment
* @param lastIdx
* The index in original shape's point of the ending point for
* this line segment
*/
private static void douglasPeuckerReduction(ArrayList<GeoPoint> shape, boolean[] marked,
double tolerance, int firstIdx, int lastIdx)
{
if (lastIdx <= firstIdx + 1) {
// overlapping indexes, just return
return;
}
// loop over the points between the first and last points
// and find the point that is the farthest away
double maxDistance = 0.0;
int indexFarthest = 0;
GeoPoint firstPoint = shape.get(firstIdx);
GeoPoint lastPoint = shape.get(lastIdx);
for (int idx = firstIdx + 1; idx < lastIdx; idx++) {
GeoPoint point = shape.get(idx);
double distance = orthogonalDistance(point, firstPoint, lastPoint);
// keep the point with the greatest distance
if (distance > maxDistance) {
maxDistance = distance;
indexFarthest = idx;
}
}
if (maxDistance > tolerance) {
//The farthest point is outside the tolerance: it is marked and the algorithm continues.
marked[indexFarthest] = true;
// reduce the shape between the starting point to newly found point
douglasPeuckerReduction(shape, marked, tolerance, firstIdx, indexFarthest);
// reduce the shape between the newly found point and the finishing point
douglasPeuckerReduction(shape, marked, tolerance, indexFarthest, lastIdx);
}
//else: the farthest point is within the tolerance, the whole segment is discarded.
}
/**
* Calculate the orthogonal distance from the line joining the lineStart and
* lineEnd points to point
* @param point
* The point the distance is being calculated for
* @param lineStart
* The point that starts the line
* @param lineEnd
* The point that ends the line
* @return The distance in points coordinate system
*/
public static double orthogonalDistance(GeoPoint point, GeoPoint lineStart, GeoPoint lineEnd)
{
double area = Math.abs(
(
1.0 * lineStart.latitudeE6 * lineEnd.longitudeE6
+ 1.0 * lineEnd.latitudeE6 * point.longitudeE6
+ 1.0 * point.latitudeE6 * lineStart.longitudeE6
- 1.0 * lineEnd.latitudeE6 * lineStart.longitudeE6
- 1.0 * point.latitudeE6 * lineEnd.longitudeE6
- 1.0 * lineStart.latitudeE6 * point.longitudeE6
) / 2.0
);
double bottom = Math.hypot(
lineStart.latitudeE6 - lineEnd.latitudeE6,
lineStart.longitudeE6 - lineEnd.longitudeE6
);
return (area / bottom * 2.0);
}
}