Skip to content

Add Douglas-Peucker poly simplification algorithm as PolyUtil.simplify() #201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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