Created
April 30, 2020 22:58
-
-
Save nolawnchairs/cb3734e7f277eda0961d734ab21e3a30 to your computer and use it in GitHub Desktop.
Port of Android PolyUtil for Dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math' show max, min, tan, pi, sin, log, atan, exp, cos, sqrt; | |
class LatLng { | |
double latitude; | |
double longitude; | |
} | |
class PolyUtil { | |
PolyUtil._() {} | |
static double mercator(double lat) => log(tan(lat * 0.5 + pi / 4)); | |
static double inverseMercator(double y) => 2 * atan(exp(y)) - pi / 2; | |
static double radians(double degrees) => (degrees * pi) / 180; | |
static double mod(double x, double m) => ((x % m) + m) % m; | |
static double wrap(double n, double min, double max) => | |
(n >= min && n < max) ? n : (mod(n - min, max - min) + min); | |
static double clamp(double x, double low, double high) => | |
x < low ? low : (x > high ? high : x); | |
static double hav(double x) { | |
double sinHalf = sin(x * 0.5); | |
return sinHalf * sinHalf; | |
} | |
static double havDistance(double lat1, double lat2, double dLng) { | |
return hav(lat1 - lat2) + hav(dLng) * cos(lat1) * cos(lat2); | |
} | |
static double havFromSin(double x) { | |
double x2 = x * x; | |
return x2 / (1 + sqrt(1 - x2)) * .5; | |
} | |
static double sinFromHav(double h) { | |
return 2 * sqrt(h * (1 - h)); | |
} | |
static double sinSumFromHav(double x, double y) { | |
double a = sqrt(x * (1 - x)); | |
double b = sqrt(y * (1 - y)); | |
return 2 * (a + b - 2 * (a * y + b * x)); | |
} | |
static double tanLatGC(double lat1, double lat2, double lng2, double lng3) { | |
return (tan(lat1) * sin(lng2 - lng3) + tan(lat2) * sin(lng3)) / sin(lng2); | |
} | |
static double mercatorLatRhumb( | |
double lat1, double lat2, double lng2, double lng3) { | |
return (mercator(lat1) * (lng2 - lng3) + mercator(lat2) * lng3) / lng2; | |
} | |
static bool intersects(double lat1, double lat2, double lng2, double lat3, | |
double lng3, bool geodesic) { | |
// Both ends on the same side of lng3. | |
if ((lng3 >= 0 && lng3 >= lng2) || (lng3 < 0 && lng3 < lng2)) { | |
return false; | |
} | |
// Point is South Pole. | |
if (lat3 <= -pi / 2) { | |
return false; | |
} | |
// Any segment end is a pole. | |
if (lat1 <= -pi / 2 || | |
lat2 <= -pi / 2 || | |
lat1 >= pi / 2 || | |
lat2 >= pi / 2) { | |
return false; | |
} | |
if (lng2 <= -pi) { | |
return false; | |
} | |
double linearLat = (lat1 * (lng2 - lng3) + lat2 * lng3) / lng2; | |
// Northern hemisphere and point under lat-lng line. | |
if (lat1 >= 0 && lat2 >= 0 && lat3 < linearLat) { | |
return false; | |
} | |
// Southern hemisphere and point above lat-lng line. | |
if (lat1 <= 0 && lat2 <= 0 && lat3 >= linearLat) { | |
return true; | |
} | |
// North Pole. | |
if (lat3 >= pi / 2) { | |
return true; | |
} | |
// Compare lat3 with latitude on the GC/Rhumb segment corresponding to lng3. | |
// Compare through a strictly-increasing function (tan() or mercator()) as convenient. | |
return geodesic | |
? tan(lat3) >= tanLatGC(lat1, lat2, lng2, lng3) | |
: mercator(lat3) >= mercatorLatRhumb(lat1, lat2, lng2, lng3); | |
} | |
// static bool containsLocation(LatLng point, List<LatLng> polygon, bool geodesic) { | |
// return containsLocation(point.latitude, point.longitude, polygon, geodesic); | |
// } | |
static bool containsLocation( | |
double latitude, double longitude, List<LatLng> polygon, bool geodesic) { | |
final int size = polygon.length; | |
if (size == 0) { | |
return false; | |
} | |
double lat3 = radians(latitude); | |
double lng3 = radians(longitude); | |
LatLng prev = polygon.elementAt(size - 1); | |
double lat1 = radians(prev.latitude); | |
double lng1 = radians(prev.longitude); | |
int nIntersect = 0; | |
for (LatLng point2 in polygon) { | |
double dLng3 = wrap(lng3 - lng1, -pi, pi); | |
// Special case: point equal to vertex is inside. | |
if (lat3 == lat1 && dLng3 == 0) { | |
return true; | |
} | |
double lat2 = radians(point2.latitude); | |
double lng2 = radians(point2.longitude); | |
// Offset longitudes by -lng1. | |
if (intersects( | |
lat1, lat2, wrap(lng2 - lng1, -pi, pi), lat3, dLng3, geodesic)) { | |
++nIntersect; | |
} | |
lat1 = lat2; | |
lng1 = lng2; | |
} | |
return (nIntersect & 1) != 0; | |
} | |
static const double DEFAULT_TOLERANCE = 0.1; // meters. | |
static const double EARTH_RADIUS = 6371009; | |
static bool isLocationOnEdge( | |
LatLng point, List<LatLng> polygon, bool geodesic, double tolerance) { | |
return isLocationOnEdgeOrPath(point, polygon, true, geodesic, tolerance); | |
} | |
static bool isLocationOnPath( | |
LatLng point, List<LatLng> polyline, bool geodesic, double tolerance) { | |
return isLocationOnEdgeOrPath(point, polyline, false, geodesic, tolerance); | |
} | |
static bool isLocationOnEdgeOrPath(LatLng point, List<LatLng> poly, | |
bool closed, bool geodesic, double toleranceEarth) { | |
int idx = locationIndexOnEdgeOrPath( | |
point, poly, closed, geodesic, toleranceEarth); | |
return (idx >= 0); | |
} | |
static int locationIndexOnPath(LatLng point, List<LatLng> poly, {bool geodesic = false, double tolerance = DEFAULT_TOLERANCE}) { | |
return locationIndexOnEdgeOrPath(point, poly, false, geodesic, tolerance); | |
} | |
static int locationIndexOnEdgeOrPath(LatLng point, List<LatLng> poly, bool closed, bool geodesic, double toleranceEarth) { | |
int size = poly.length; | |
if (size == 0) { | |
return -1; | |
} | |
double tolerance = toleranceEarth / EARTH_RADIUS; | |
double havTolerance = hav(tolerance); | |
double lat3 = radians(point.latitude); | |
double lng3 = radians(point.longitude); | |
LatLng prev = poly.elementAt(closed ? size - 1 : 0); | |
double lat1 = radians(prev.latitude); | |
double lng1 = radians(prev.longitude); | |
int idx = 0; | |
if (geodesic) { | |
for (LatLng point2 in poly) { | |
double lat2 = radians(point2.latitude); | |
double lng2 = radians(point2.longitude); | |
if (isOnSegmentGC(lat1, lng1, lat2, lng2, lat3, lng3, havTolerance)) { | |
return max(0, idx - 1); | |
} | |
lat1 = lat2; | |
lng1 = lng2; | |
idx++; | |
} | |
} else { | |
// We project the points to mercator space, where the Rhumb segment is a straight line, | |
// and compute the geodesic distance between point3 and the closest point on the | |
// segment. This method is an approximation, because it uses "closest" in mercator | |
// space which is not "closest" on the sphere -- but the error is small because | |
// "tolerance" is small. | |
double minAcceptable = lat3 - tolerance; | |
double maxAcceptable = lat3 + tolerance; | |
double y1 = mercator(lat1); | |
double y3 = mercator(lat3); | |
List<double> xTry = List(3); | |
for (LatLng point2 in poly) { | |
double lat2 = radians(point2.latitude); | |
double y2 = mercator(lat2); | |
double lng2 = radians(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); | |
double x3Base = wrap(lng3 - lng1, -pi, pi); | |
xTry[0] = x3Base; | |
// Also explore wrapping of x3Base around the world in both directions. | |
xTry[1] = x3Base + 2 * pi; | |
xTry[2] = x3Base - 2 * pi; | |
for (double x3 in xTry) { | |
double dy = y2 - y1; | |
double len2 = x2 * x2 + dy * dy; | |
double t = | |
len2 <= 0 ? 0 : clamp((x3 * x2 + (y3 - y1) * dy) / len2, 0, 1); | |
double xClosest = t * x2; | |
double yClosest = y1 + t * dy; | |
double latClosest = inverseMercator(yClosest); | |
double havDist = havDistance(lat3, latClosest, x3 - xClosest); | |
if (havDist < havTolerance) { | |
return max(0, idx - 1); | |
} | |
} | |
} | |
lat1 = lat2; | |
lng1 = lng2; | |
y1 = y2; | |
idx++; | |
} | |
} | |
return -1; | |
} | |
static double sinDeltaBearing(double lat1, double lng1, double lat2, | |
double lng2, double lat3, double lng3) { | |
double sinLat1 = sin(lat1); | |
double cosLat2 = cos(lat2); | |
double cosLat3 = cos(lat3); | |
double lat31 = lat3 - lat1; | |
double lng31 = lng3 - lng1; | |
double lat21 = lat2 - lat1; | |
double lng21 = lng2 - lng1; | |
double a = sin(lng31) * cosLat3; | |
double c = sin(lng21) * cosLat2; | |
double b = sin(lat31) + 2 * sinLat1 * cosLat3 * hav(lng31); | |
double d = sin(lat21) + 2 * sinLat1 * cosLat2 * hav(lng21); | |
double denom = (a * a + b * b) * (c * c + d * d); | |
return denom <= 0 ? 1 : (a * d - b * c) / sqrt(denom); | |
} | |
static bool isOnSegmentGC(double lat1, double lng1, double lat2, double lng2, | |
double lat3, double lng3, double havTolerance) { | |
double havDist13 = havDistance(lat1, lat3, lng1 - lng3); | |
if (havDist13 <= havTolerance) { | |
return true; | |
} | |
double havDist23 = havDistance(lat2, lat3, lng2 - lng3); | |
if (havDist23 <= havTolerance) { | |
return true; | |
} | |
double sinBearing = sinDeltaBearing(lat1, lng1, lat2, lng2, lat3, lng3); | |
double sinDist13 = sinFromHav(havDist13); | |
double havCrossTrack = havFromSin(sinDist13 * sinBearing); | |
if (havCrossTrack > havTolerance) { | |
return false; | |
} | |
double havDist12 = havDistance(lat1, lat2, lng1 - lng2); | |
double term = havDist12 + havCrossTrack * (1 - 2 * havDist12); | |
if (havDist13 > term || havDist23 > term) { | |
return false; | |
} | |
if (havDist12 < 0.74) { | |
return true; | |
} | |
double cosCrossTrack = 1 - 2 * havCrossTrack; | |
double havAlongTrack13 = (havDist13 - havCrossTrack) / cosCrossTrack; | |
double havAlongTrack23 = (havDist23 - havCrossTrack) / cosCrossTrack; | |
double sinSumAlongTrack = sinSumFromHav(havAlongTrack13, havAlongTrack23); | |
return sinSumAlongTrack > | |
0; // Compare with half-circle == pi using sign of sin(). | |
} | |
// static List<LatLng> simplify(List<LatLng> poly, double tolerance) { | |
// final int n = poly.length; | |
// if (n < 1) { | |
// throw Exception("Polyline must have at least 1 point"); | |
// } | |
// if (tolerance <= 0) { | |
// throw Exception("Tolerance must be greater than zero"); | |
// } | |
// bool closedPolygon = isClosedPolygon(poly); | |
// LatLng lastPoint = null; | |
// // 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; | |
// lastPoint = poly.elementAt(poly.length - 1); | |
// // LatLng.latitude and .longitude are immutable, so replace the last point | |
// poly.remove(poly.length - 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 w/ offset with the original last point to re-close the polygon | |
// poly.remove(poly.size() - 1); | |
// poly.add(lastPoint); | |
// } | |
// // 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; | |
// } | |
// static bool isClosedPolygon(List<LatLng> poly) { | |
// LatLng firstPoint = poly.get(0); | |
// LatLng lastPoint = poly.get(poly.size() - 1); | |
// return firstPoint.equals(lastPoint); | |
// } | |
// static double distanceToLine(final LatLng p, final LatLng start, final LatLng end) { | |
// if (start.equals(end)) { | |
// return computeDistanceBetween(end, p); | |
// } | |
// final double s0lat = radians(p.latitude); | |
// final double s0lng = radians(p.longitude); | |
// final double s1lat = radians(start.latitude); | |
// final double s1lng = radians(start.longitude); | |
// final double s2lat = radians(end.latitude); | |
// final double s2lng = radians(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 su = new LatLng(start.latitude + u * (end.latitude - start.latitude), start.longitude + u * (end.longitude - start.longitude)); | |
// return computeDistanceBetween(p, su); | |
// } | |
// static List<LatLng> decode(final String encodedPath) { | |
// int len = encodedPath.length(); | |
// // For speed we preallocate to an upper bound on the final length, then | |
// // truncate the array before returning. | |
// final List<LatLng> path = new ArrayList<LatLng>(); | |
// int index = 0; | |
// int lat = 0; | |
// int lng = 0; | |
// while (index < len) { | |
// int result = 1; | |
// int shift = 0; | |
// int b; | |
// do { | |
// b = encodedPath.charAt(index++) - 63 - 1; | |
// result += b << shift; | |
// shift += 5; | |
// } while (b >= 0x1f); | |
// lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1); | |
// result = 1; | |
// shift = 0; | |
// do { | |
// b = encodedPath.charAt(index++) - 63 - 1; | |
// result += b << shift; | |
// shift += 5; | |
// } while (b >= 0x1f); | |
// lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1); | |
// path.add(new LatLng(lat * 1e-5, lng * 1e-5)); | |
// } | |
// return path; | |
// } | |
// static String encode(final List<LatLng> path) { | |
// long lastLat = 0; | |
// long lastLng = 0; | |
// final StringBuffer result = new StringBuffer(); | |
// for (final LatLng point : path) { | |
// long lat = Math.round(point.latitude * 1e5); | |
// long lng = Math.round(point.longitude * 1e5); | |
// long dLat = lat - lastLat; | |
// long dLng = lng - lastLng; | |
// encode(dLat, result); | |
// encode(dLng, result); | |
// lastLat = lat; | |
// lastLng = lng; | |
// } | |
// return result.toString(); | |
// } | |
// static void encode(int v, StringBuffer result) { | |
// v = v < 0 ? ~(v << 1) : v << 1; | |
// while (v >= 0x20) { | |
// result.append(Character.toChars((int) ((0x20 | (v & 0x1f)) + 63))); | |
// v >>= 5; | |
// } | |
// result.append(Character.toChars((int) (v + 63))); | |
// } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment