UNPKG

node-geometry-library

Version:

Javascript Geometry Library provides utility functions for the computation of geometric data on the surface of the Earth. Code ported from Google Maps Android API.

362 lines (361 loc) 16.2 kB
"use strict"; /* * Copyright 2013 Google Inc. * * https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/PolyUtil.java * * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); const MathUtil_1 = require("./MathUtil"); const SphericalUtil_1 = require("./SphericalUtil"); const { max, min, tan, cos, sin, sqrt } = Math; const DEFAULT_TOLERANCE = 0.1; // meters. function py2_round(value) { // Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1); } function encode(current, previous, factor) { current = py2_round(current * factor); previous = py2_round(previous * factor); let coordinate = current - previous; coordinate <<= 1; if (current - previous < 0) { coordinate = ~coordinate; } let output = ''; while (coordinate >= 0x20) { output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63); coordinate >>= 5; } output += String.fromCharCode(coordinate + 63); return output; } class PolyUtil { static get DEFAULT_TOLERANCE() { return DEFAULT_TOLERANCE; } /** * Returns tan(latitude-at-lng3) on the great circle (lat1, lng1) to (lat2, lng2). lng1==0. * See http://williams.best.vwh.net/avform.htm . */ static tanLatGC(lat1, lat2, lng2, lng3) { return (tan(lat1) * sin(lng2 - lng3) + tan(lat2) * sin(lng3)) / sin(lng2); } /** * Returns mercator(latitude-at-lng3) on the Rhumb line (lat1, lng1) to (lat2, lng2). lng1==0. */ static mercatorLatRhumb(lat1, lat2, lng2, lng3) { return ((MathUtil_1.default.mercator(lat1) * (lng2 - lng3) + MathUtil_1.default.mercator(lat2) * lng3) / lng2); } /** * Computes whether the vertical segment (lat3, lng3) to South Pole intersects the segment * (lat1, lng1) to (lat2, lng2). * Longitudes are offset by -lng1; the implicit lng1 becomes 0. */ static intersects(lat1, lat2, lng2, lat3, lng3, 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 <= -Math.PI / 2) { return false; } // Any segment end is a pole. if (lat1 <= -Math.PI / 2 || lat2 <= -Math.PI / 2 || lat1 >= Math.PI / 2 || lat2 >= Math.PI / 2) { return false; } if (lng2 <= -Math.PI) { return false; } const 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 >= Math.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) >= PolyUtil.tanLatGC(lat1, lat2, lng2, lng3) : MathUtil_1.default.mercator(lat3) >= PolyUtil.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 * the first or not. * Inside is defined as not containing the South Pole -- the South Pole is always outside. * The polygon is formed of great circle segments if geodesic is true, and of rhumb * (loxodromic) segments otherwise. */ static containsLocation(point, polygon, geodesic = false) { const size = polygon.length; if (size == 0) { return false; } let lat3 = SphericalUtil_1.deg2rad(point["lat"]); let lng3 = SphericalUtil_1.deg2rad(point["lng"]); let prev = polygon[size - 1]; let lat1 = SphericalUtil_1.deg2rad(prev["lat"]); let lng1 = SphericalUtil_1.deg2rad(prev["lng"]); let nIntersect = 0; // @ts-ignore for (let index in polygon) { const val = polygon[index]; let dLng3 = MathUtil_1.default.wrap(lng3 - lng1, -Math.PI, Math.PI); // Special case: point equal to vertex is inside. if (lat3 == lat1 && dLng3 == 0) { return true; } let lat2 = SphericalUtil_1.deg2rad(val["lat"]); let lng2 = SphericalUtil_1.deg2rad(val["lng"]); // Offset longitudes by -lng1. if (PolyUtil.intersects(lat1, lat2, MathUtil_1.default.wrap(lng2 - lng1, -Math.PI, Math.PI), lat3, dLng3, geodesic)) { ++nIntersect; } lat1 = lat2; lng1 = lng2; } return (nIntersect & 1) != 0; } /** * Computes whether the given point lies on or near the edge of a polygon, within a specified * tolerance in meters. The polygon edge is composed of great circle segments if geodesic * is true, and of Rhumb segments otherwise. The polygon edge is implicitly closed -- the * closing segment between the first point and the last point is included. */ static isLocationOnEdge(point, polygon, tolerance = DEFAULT_TOLERANCE, geodesic = true) { return PolyUtil.isLocationOnEdgeOrPath(point, polygon, true, geodesic, tolerance); } /** * Computes whether the given point lies on or near a polyline, within a specified * tolerance in meters. The polyline is composed of great circle segments if geodesic * is true, and of Rhumb segments otherwise. The polyline is not closed -- the closing * segment between the first point and the last point is not included. */ static isLocationOnPath(point, polyline, tolerance = DEFAULT_TOLERANCE, geodesic = true) { return PolyUtil.isLocationOnEdgeOrPath(point, polyline, false, geodesic, tolerance); } static isLocationOnEdgeOrPath(point, poly, closed, geodesic, toleranceEarth) { const size = poly.length; if (size == 0) { return false; } let tolerance = toleranceEarth / MathUtil_1.default.EARTH_RADIUS; let havTolerance = MathUtil_1.default.hav(tolerance); let lat3 = SphericalUtil_1.deg2rad(point["lat"]); let lng3 = SphericalUtil_1.deg2rad(point["lng"]); let prev = closed ? poly[size - 1] : 0; // @ts-ignore let lat1 = SphericalUtil_1.deg2rad(prev ? prev['lat'] : 0); // @ts-ignore let lng1 = SphericalUtil_1.deg2rad(prev ? prev['lng'] : 0); if (geodesic) { for (let i in poly) { const val = poly[i]; let lat2 = SphericalUtil_1.deg2rad(val["lat"]); let lng2 = SphericalUtil_1.deg2rad(val["lng"]); if (PolyUtil.isOnSegmentGC(lat1, lng1, lat2, lng2, lat3, lng3, havTolerance)) { return true; } lat1 = lat2; lng1 = lng2; } } 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. let minAcceptable = lat3 - tolerance; let maxAcceptable = lat3 + tolerance; let y1 = MathUtil_1.default.mercator(lat1); let y3 = MathUtil_1.default.mercator(lat3); let xTry = []; for (let i in poly) { let val = poly[i]; let lat2 = SphericalUtil_1.deg2rad(val["lat"]); let y2 = MathUtil_1.default.mercator(lat2); let lng2 = SphericalUtil_1.deg2rad(val["lng"]); if (max(lat1, lat2) >= minAcceptable && min(lat1, lat2) <= maxAcceptable) { // We offset longitudes by -lng1; the implicit x1 is 0. let x2 = MathUtil_1.default.wrap(lng2 - lng1, -Math.PI, Math.PI); let x3Base = MathUtil_1.default.wrap(lng3 - lng1, -Math.PI, Math.PI); xTry[0] = x3Base; // Also explore wrapping of x3Base around the world in both directions. xTry[1] = x3Base + 2 * Math.PI; xTry[2] = x3Base - 2 * Math.PI; for (let i in xTry) { let x3 = xTry[i]; let dy = y2 - y1; let len2 = x2 * x2 + dy * dy; let t = len2 <= 0 ? 0 : MathUtil_1.default.clamp((x3 * x2 + (y3 - y1) * dy) / len2, 0, 1); let xClosest = t * x2; let yClosest = y1 + t * dy; let latClosest = MathUtil_1.default.inverseMercator(yClosest); let havDist = MathUtil_1.default.havDistance(lat3, latClosest, x3 - xClosest); if (havDist < havTolerance) { return true; } } } lat1 = lat2; lng1 = lng2; y1 = y2; } } return false; } /** * Returns sin(initial bearing from (lat1,lng1) to (lat3,lng3) minus initial bearing * from (lat1, lng1) to (lat2,lng2)). */ static sinDeltaBearing(lat1, lng1, lat2, lng2, lat3, lng3) { const sinLat1 = sin(lat1); const cosLat2 = cos(lat2); const cosLat3 = cos(lat3); const lat31 = lat3 - lat1; const lng31 = lng3 - lng1; const lat21 = lat2 - lat1; const lng21 = lng2 - lng1; const a = sin(lng31) * cosLat3; const c = sin(lng21) * cosLat2; const b = sin(lat31) + 2 * sinLat1 * cosLat3 * MathUtil_1.default.hav(lng31); const d = sin(lat21) + 2 * sinLat1 * cosLat2 * MathUtil_1.default.hav(lng21); const denom = (a * a + b * b) * (c * c + d * d); return denom <= 0 ? 1 : (a * d - b * c) / sqrt(denom); } static isOnSegmentGC(lat1, lng1, lat2, lng2, lat3, lng3, havTolerance) { const havDist13 = MathUtil_1.default.havDistance(lat1, lat3, lng1 - lng3); if (havDist13 <= havTolerance) { return true; } const havDist23 = MathUtil_1.default.havDistance(lat2, lat3, lng2 - lng3); if (havDist23 <= havTolerance) { return true; } const sinBearing = PolyUtil.sinDeltaBearing(lat1, lng1, lat2, lng2, lat3, lng3); const sinDist13 = MathUtil_1.default.sinFromHav(havDist13); const havCrossTrack = MathUtil_1.default.havFromSin(sinDist13 * sinBearing); if (havCrossTrack > havTolerance) { return false; } const havDist12 = MathUtil_1.default.havDistance(lat1, lat2, lng1 - lng2); const term = havDist12 + havCrossTrack * (1 - 2 * havDist12); if (havDist13 > term || havDist23 > term) { return false; } if (havDist12 < 0.74) { return true; } const cosCrossTrack = 1 - 2 * havCrossTrack; const havAlongTrack13 = (havDist13 - havCrossTrack) / cosCrossTrack; const havAlongTrack23 = (havDist23 - havCrossTrack) / cosCrossTrack; const sinSumAlongTrack = MathUtil_1.default.sinSumFromHav(havAlongTrack13, havAlongTrack23); return sinSumAlongTrack > 0; // Compare with half-circle == PI using sign of sin(). } /** * 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) */ static distanceToLine(p, start, end) { if (start == end) { return SphericalUtil_1.default.computeDistanceBetween(end, p); } const s0lat = SphericalUtil_1.deg2rad(p["lat"]); const s0lng = SphericalUtil_1.deg2rad(p["lng"]); const s1lat = SphericalUtil_1.deg2rad(start["lat"]); const s1lng = SphericalUtil_1.deg2rad(start["lng"]); const s2lat = SphericalUtil_1.deg2rad(end["lat"]); const s2lng = SphericalUtil_1.deg2rad(end["lng"]); const s2s1lat = s2lat - s1lat; const s2s1lng = s2lng - s1lng; const u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng) / (s2s1lat * s2s1lat + s2s1lng * s2s1lng); if (u <= 0) { return SphericalUtil_1.default.computeDistanceBetween(p, start); } if (u >= 1) { return SphericalUtil_1.default.computeDistanceBetween(p, end); } const su = { 'lat': start['lat'] + u * (end['lat'] - start['lat']), 'lng': start['lng'] + u * (end['lng'] - start['lng']) }; return SphericalUtil_1.default.computeDistanceBetween(p, su); } /** * Decodes an encoded path string into a sequence of LatLngs. */ static decode(encodedPath, precision = 5) { var index = 0, lat = 0, lng = 0, coordinates = [], shift = 0, result = 0, byte = null, latitude_change, longitude_change, factor = Math.pow(10, Number.isInteger(precision) ? precision : 5); // Coordinates have variable length when encoded, so just keep // track of whether we've hit the end of the string. In each // loop iteration, a single coordinate is decoded. while (index < encodedPath.length) { // Reset shift, result, and byte byte = null; shift = 0; result = 0; do { byte = encodedPath.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); shift = result = 0; do { byte = encodedPath.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); lat += latitude_change; lng += longitude_change; coordinates.push({ lat: lat / factor, lng: lng / factor }); } return coordinates; } /** * Encodes a sequence of LatLngs into an encoded path string. */ static encode(path, precision = 5) { var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5), output = encode(path[0].lat, 0, factor) + encode(path[0].lng, 0, factor); for (var i = 1; i < path.length; i++) { var a = path[i], b = path[i - 1]; output += encode(a.lat, b.lat, factor); output += encode(a.lng, b.lng, factor); } return output; } } exports.default = PolyUtil;