Skip to content

Commit

Permalink
Merge pull request #201 from barbeau/douglas-peucker2
Browse files Browse the repository at this point in the history
Add Douglas-Peucker line simplification algorithm as PolyUtil.simplify()
  • Loading branch information
broady committed Sep 17, 2015
2 parents e6b8394 + d724493 commit 7b1481e
Show file tree
Hide file tree
Showing 5 changed files with 515 additions and 7 deletions.
1 change: 1 addition & 0 deletions demo/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
</activity>

<activity android:name=".PolyDecodeDemoActivity"/>
<activity android:name=".PolySimplifyDemoActivity"/>
<activity android:name=".IconGeneratorDemoActivity"/>
<activity android:name=".DistanceDemoActivity"/>
<activity android:name=".ClusteringDemoActivity"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ protected void onCreate(Bundle savedInstanceState) {
addDemo("Clustering: Custom Look", CustomMarkerClusteringDemoActivity.class);
addDemo("Clustering: 2K markers", BigClusteringDemoActivity.class);
addDemo("PolyUtil.decode", PolyDecodeDemoActivity.class);
addDemo("PolyUtil.simplify", PolySimplifyDemoActivity.class);
addDemo("IconGenerator", IconGeneratorDemoActivity.class);
addDemo("SphericalUtil.computeDistanceBetween", DistanceDemoActivity.class);
addDemo("Generating tiles", TileProviderAndProjectionDemo.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2015 Sean J. Barbeau
*
* Licensed 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 com.google.maps.android.utils.demo;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.PolygonOptions;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.maps.android.PolyUtil;

import android.graphics.Color;

import java.util.ArrayList;
import java.util.List;

public class PolySimplifyDemoActivity extends BaseDemoActivity {

private final static String LINE = "elfjD~a}uNOnFN~Em@fJv@tEMhGDjDe@hG^nF??@lA?n@IvAC`Ay@A{@DwCA{CF_EC{CEi@PBTFDJBJ?V?n@?D@?A@?@?F?F?LAf@?n@@`@@T@~@FpA?fA?p@?r@?vAH`@OR@^ETFJCLD?JA^?J?P?fAC`B@d@?b@A\\@`@Ad@@\\?`@?f@?V?H?DD@DDBBDBD?D?B?B@B@@@B@B@B@D?D?JAF@H@FCLADBDBDCFAN?b@Af@@x@@";
private final static String OVAL_POLYGON = "}wgjDxw_vNuAd@}AN{A]w@_Au@kAUaA?{@Ke@@_@C]D[FULWFOLSNMTOVOXO\\I\\CX?VJXJTDTNXTVVLVJ`@FXA\\AVLZBTATBZ@ZAT?\\?VFT@XGZAP";
private final static int ALPHA_ADJUSTMENT = 0x77000000;

@Override
protected void startDemo() {
GoogleMap mMap = getMap();

// Original line
List<LatLng> line = PolyUtil.decode(LINE);
mMap.addPolyline(new PolylineOptions()
.addAll(line)
.color(Color.BLACK));

getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(28.05870, -82.4090), 15));

List<LatLng> simplifiedLine;

/**
* Simplified lines - increasing the tolerance will result in fewer points in the simplified
* line
*/
double tolerance = 5; // meters
simplifiedLine = PolyUtil.simplify(line, tolerance);
mMap.addPolyline(new PolylineOptions()
.addAll(simplifiedLine)
.color(Color.RED - ALPHA_ADJUSTMENT));

tolerance = 20; // meters
simplifiedLine = PolyUtil.simplify(line, tolerance);
mMap.addPolyline(new PolylineOptions()
.addAll(simplifiedLine)
.color(Color.GREEN - ALPHA_ADJUSTMENT));

tolerance = 50; // meters
simplifiedLine = PolyUtil.simplify(line, tolerance);
mMap.addPolyline(new PolylineOptions()
.addAll(simplifiedLine)
.color(Color.MAGENTA - ALPHA_ADJUSTMENT));

tolerance = 500; // meters
simplifiedLine = PolyUtil.simplify(line, tolerance);
mMap.addPolyline(new PolylineOptions()
.addAll(simplifiedLine)
.color(Color.YELLOW - ALPHA_ADJUSTMENT));

tolerance = 1000; // meters
simplifiedLine = PolyUtil.simplify(line, tolerance);
mMap.addPolyline(new PolylineOptions()
.addAll(simplifiedLine)
.color(Color.BLUE - ALPHA_ADJUSTMENT));


// Triangle polygon - the polygon should be closed
ArrayList<LatLng> triangle = new ArrayList<>();
triangle.add(new LatLng(28.06025,-82.41030)); // Should match last point
triangle.add(new LatLng(28.06129,-82.40945));
triangle.add(new LatLng(28.06206,-82.40917));
triangle.add(new LatLng(28.06125,-82.40850));
triangle.add(new LatLng(28.06035,-82.40834));
triangle.add(new LatLng(28.06038, -82.40924));
triangle.add(new LatLng(28.06025,-82.41030)); // Should match first point

mMap.addPolygon(new PolygonOptions()
.addAll(triangle)
.fillColor(Color.BLUE - ALPHA_ADJUSTMENT)
.strokeColor(Color.BLUE)
.strokeWidth(5));

// Simplified triangle polygon
tolerance = 88; // meters
List simplifiedTriangle = PolyUtil.simplify(triangle, tolerance);
mMap.addPolygon(new PolygonOptions()
.addAll(simplifiedTriangle)
.fillColor(Color.YELLOW - ALPHA_ADJUSTMENT)
.strokeColor(Color.YELLOW)
.strokeWidth(5));

// Oval polygon - the polygon should be closed
List<LatLng> oval = PolyUtil.decode(OVAL_POLYGON);
mMap.addPolygon(new PolygonOptions()
.addAll(oval)
.fillColor(Color.BLUE - ALPHA_ADJUSTMENT)
.strokeColor(Color.BLUE)
.strokeWidth(5));

// Simplified oval polygon
tolerance = 10; // meters
List simplifiedOval= PolyUtil.simplify(oval, tolerance);
mMap.addPolygon(new PolygonOptions()
.addAll(simplifiedOval)
.fillColor(Color.YELLOW - ALPHA_ADJUSTMENT)
.strokeColor(Color.YELLOW)
.strokeWidth(5));
}
}
156 changes: 151 additions & 5 deletions library/src/com/google/maps/android/PolyUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013 Google Inc.
* Copyright 2008, 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,7 +20,9 @@

import java.util.List;
import java.util.ArrayList;
import java.util.Stack;

import static com.google.maps.android.SphericalUtil.*;
import static java.lang.Math.*;
import static com.google.maps.android.MathUtil.*;

Expand All @@ -38,11 +40,11 @@ private static double tanLatGC(double lat1, double lat2, double lng2, double lng

/**
* Returns mercator(latitude-at-lng3) on the Rhumb line (lat1, lng1) to (lat2, lng2). lng1==0.
*/
*/
private static double mercatorLatRhumb(double lat1, double lat2, double lng2, double lng3) {
return (mercator(lat1) * (lng2 - lng3) + mercator(lat2) * lng3) / lng2;
}

/**
* Computes whether the vertical segment (lat3, lng3) to South Pole intersects the segment
* (lat1, lng1) to (lat2, lng2).
Expand Down Expand Up @@ -84,7 +86,7 @@ private static boolean intersects(double lat1, double lat2, double lng2,
tan(lat3) >= tanLatGC(lat1, lat2, lng2, lng3) :
mercator(lat3) >= mercatorLatRhumb(lat1, lat2, lng2, lng3);
}

/**
* Computes whether the given point lies inside the specified polygon.
* The polygon is always cosidered closed, regardless of whether the last point equals
Expand Down Expand Up @@ -201,7 +203,7 @@ private static boolean isLocationOnEdgeOrPath(LatLng point, List<LatLng> poly, b
for (LatLng point2 : poly) {
double lat2 = toRadians(point2.latitude);
double y2 = mercator(lat2);
double lng2 = toRadians(point2.longitude);
double lng2 = toRadians(point2.longitude);
if (max(lat1, lat2) >= minAcceptable && min(lat1, lat2) <= maxAcceptable) {
// We offset longitudes by -lng1; the implicit x1 is 0.
double x2 = wrap(lng2 - lng1, -PI, PI);
Expand Down Expand Up @@ -283,6 +285,150 @@ private static boolean isOnSegmentGC(double lat1, double lng1, double lat2, doub
return sinSumAlongTrack > 0; // Compare with half-circle == PI using sign of sin().
}

/**
* Simplifies the given poly (polyline or polygon) using the Douglas-Peucker decimation
* algorithm. Increasing the tolerance will result in fewer points in the simplified polyline
* or polygon.
*
* When the providing a polygon as input, the first and last point of the list MUST have the
* same latitude and longitude (i.e., the polygon must be closed). If the input polygon is not
* closed, the resulting polygon may not be fully simplified.
*
* The time complexity of Douglas-Peucker is O(n^2), so take care that you do not call this
* algorithm too frequently in your code.
*
* @param poly polyline or polygon to be simplified. Polygon should be closed (i.e.,
* first and last points should have the same latitude and longitude).
* @param tolerance in meters. Increasing the tolerance will result in fewer points in the
* simplified poly.
* @return a simplified poly produced by the Douglas-Peucker algorithm
*/
public static List<LatLng> simplify(List<LatLng> poly, double tolerance) {
final int n = poly.size();
if (n < 1) {
throw new IllegalArgumentException("Polyline must have at least 1 point");
}
if (tolerance <= 0) {
throw new IllegalArgumentException("Tolerance must be greater than zero");
}

boolean closedPolygon = isClosedPolygon(poly);

// Check if the provided poly is a closed polygon
if (closedPolygon) {
// Add a small offset to the last point for Douglas-Peucker on polygons (see #201)
final double OFFSET = 0.00000000001;
LatLng lastPoint = poly.get(poly.size() - 1);
// LatLng.latitude and .longitude are immutable, so replace the last point
poly.remove(poly.size() - 1);
poly.add(new LatLng(lastPoint.latitude + OFFSET, lastPoint.longitude + OFFSET));
}

int idx;
int maxIdx = 0;
Stack<int[]> stack = new Stack<>();
double[] dists = new double[n];
dists[0] = 1;
dists[n - 1] = 1;
double maxDist;
double dist = 0.0;
int[] current;

if (n > 2) {
int[] stackVal = new int[]{0, (n - 1)};
stack.push(stackVal);
while (stack.size() > 0) {
current = stack.pop();
maxDist = 0;
for (idx = current[0] + 1; idx < current[1]; ++idx) {
dist = distanceToLine(poly.get(idx), poly.get(current[0]),
poly.get(current[1]));
if (dist > maxDist) {
maxDist = dist;
maxIdx = idx;
}
}
if (maxDist > tolerance) {
dists[maxIdx] = maxDist;
int[] stackValCurMax = {current[0], maxIdx};
stack.push(stackValCurMax);
int[] stackValMaxCur = {maxIdx, current[1]};
stack.push(stackValMaxCur);
}
}
}

if (closedPolygon) {
// Replace last point of input (w/ offset) with a copy of first to re-close the polygon
poly.remove(poly.size() - 1);
poly.add(new LatLng(poly.get(0).latitude, poly.get(0).longitude));
}

// Generate the simplified line
idx = 0;
ArrayList<LatLng> simplifiedLine = new ArrayList<>();
for (LatLng l : poly) {
if (dists[idx] != 0) {
simplifiedLine.add(l);
}
idx++;
}

return simplifiedLine;
}

/**
* Returns true if the provided list of points is a closed polygon (i.e., the first and last
* points are the same), and false if it is not
* @param poly polyline or polygon
* @return true if the provided list of points is a closed polygon (i.e., the first and last
* points are the same), and false if it is not
*/
public static boolean isClosedPolygon(List<LatLng> poly) {
LatLng firstPoint = poly.get(0);
LatLng lastPoint = poly.get(poly.size()-1);
if (firstPoint.equals(lastPoint)) {
return true;
} else {
return false;
}
}

/**
* Computes the distance on the sphere between the point p and the line segment start to end.
*
* @param p the point to be measured
* @param start the beginning of the line segment
* @param end the end of the line segment
* @return the distance in meters (assuming spherical earth)
*/
public static double distanceToLine(final LatLng p, final LatLng start, final LatLng end) {
if (start.equals(end)) {
computeDistanceBetween(end, p);
}

final double s0lat = toRadians(p.latitude);
final double s0lng = toRadians(p.longitude);
final double s1lat = toRadians(start.latitude);
final double s1lng = toRadians(start.longitude);
final double s2lat = toRadians(end.latitude);
final double s2lng = toRadians(end.longitude);

double s2s1lat = s2lat - s1lat;
double s2s1lng = s2lng - s1lng;
final double u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng)
/ (s2s1lat * s2s1lat + s2s1lng * s2s1lng);
if (u <= 0) {
return computeDistanceBetween(p, start);
}
if (u >= 1) {
return computeDistanceBetween(p, end);
}
LatLng sa = new LatLng(p.latitude - start.latitude, p.longitude - start.longitude);
LatLng sb = new LatLng(u * (end.latitude - start.latitude), u * (end.longitude - start.longitude));
return computeDistanceBetween(sa, sb);
}

/**
* Decodes an encoded path string into a sequence of LatLngs.
*/
Expand Down
Loading

0 comments on commit 7b1481e

Please sign in to comment.