/*
* Geopaparazzi - Digital field mapping on Android based devices
* Copyright (C) 2016 HydroloGIS (www.hydrologis.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.geopaparazzi.library.routing.osmbonuspack;
import java.util.ArrayList;
/**
* Reduces the number of points in a shape using the Douglas-Peucker algorithm. <br>
* <p/>
* 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 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
* @param shape The shape to reduce
*/
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<>(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.getLatitudeE6() * lineEnd.getLongitudeE6()
+ 1.0 * lineEnd.getLatitudeE6() * point.getLongitudeE6()
+ 1.0 * point.getLatitudeE6() * lineStart.getLongitudeE6()
- 1.0 * lineEnd.getLatitudeE6() * lineStart.getLongitudeE6()
- 1.0 * point.getLatitudeE6() * lineEnd.getLongitudeE6()
- 1.0 * lineStart.getLatitudeE6() * point.getLongitudeE6()
) / 2.0
);
double bottom = Math.hypot(
lineStart.getLatitudeE6() - lineEnd.getLatitudeE6(),
lineStart.getLongitudeE6() - lineEnd.getLongitudeE6()
);
return (area / bottom * 2.0);
}
}