UNPKG

@spatial/point-to-line-distance

Version:

turf point-to-line-distance module

213 lines (194 loc) 8.38 kB
'use strict'; var bearing = require('@spatial/bearing'); var distance = require('@spatial/distance'); var rhumbBearing = require('@spatial/rhumb-bearing'); var rhumbDistance = require('@spatial/rhumb-distance'); var projection = require('@spatial/projection'); var invariant = require('@spatial/invariant'); var meta = require('@spatial/meta'); var helpers = require('@spatial/helpers'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var bearing__default = /*#__PURE__*/_interopDefaultLegacy(bearing); var distance__default = /*#__PURE__*/_interopDefaultLegacy(distance); var rhumbBearing__default = /*#__PURE__*/_interopDefaultLegacy(rhumbBearing); var rhumbDistance__default = /*#__PURE__*/_interopDefaultLegacy(rhumbDistance); // (logic of computation inspired by: /** * Returns the minimum distance between a {@link Point} and a {@link LineString}, being the distance from a line the * minimum distance between the point and any segment of the `LineString`. * * @name pointToLineDistance * @param {Coord} pt Feature or Geometry * @param {Feature<LineString>} line GeoJSON Feature or Geometry * @param {Object} [options={}] Optional parameters * @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers * @param {boolean} [options.mercator=false] if distance should be on Mercator or WGS84 projection * @returns {number} distance between point and line * @example * var pt = turf.point([0, 0]); * var line = turf.lineString([[1, 1],[-1, 1]]); * * var distance = turf.pointToLineDistance(pt, line, {units: 'miles'}); * //=69.11854715938406 */ function pointToLineDistance(pt, line, options) { // Optional parameters options = options || {}; if (!helpers.isObject(options)) throw new Error('options is invalid'); // validation if (!pt) throw new Error('pt is required'); if (Array.isArray(pt)) pt = helpers.point(pt); else if (pt.type === 'Point') pt = helpers.feature(pt); else invariant.featureOf(pt, 'Point', 'point'); if (!line) throw new Error('line is required'); if (Array.isArray(line)) line = helpers.lineString(line); else if (line.type === 'LineString') line = helpers.feature(line); else invariant.featureOf(line, 'LineString', 'line'); let distance = Infinity; const p = pt.geometry.coordinates; meta.segmentEach(line, function (segment) { const a = segment.geometry.coordinates[0]; const b = segment.geometry.coordinates[1]; const d = distanceToSegment(p, a, b, options); if (distance > d) distance = d; }); return distance; } /** * Returns the distance between a point P on a segment AB. * * @private * @param {Array<number>} p external point * @param {Array<number>} a first segment point * @param {Array<number>} b second segment point * @param {Object} [options={}] Optional parameters * @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers * @param {boolean} [options.mercator=false] if distance should be on Mercator or WGS84 projection * @returns {number} distance */ function distanceToSegment(p, a, b, options) { const mercator = options.mercator; const distanceAP = (mercator !== true) ? distance__default['default'](a, p, options) : euclideanDistance(a, p, options); const azimuthAP = helpers.bearingToAzimuth((mercator !== true) ? bearing__default['default'](a, p) : rhumbBearing__default['default'](a, p)); const azimuthAB = helpers.bearingToAzimuth((mercator !== true) ? bearing__default['default'](a, b) : rhumbBearing__default['default'](a, b)); const angleA = Math.abs(azimuthAP - azimuthAB); // if (angleA > 180) angleA = Math.abs(angleA - 360); // if the angle PAB is obtuse its projection on the line extending the segment falls outside the segment // thus return distance between P and the start point A /* P__ |\ \____ | \ \____ | \ \____ | \_____________\ H A B */ if (angleA > 90) return distanceAP; const azimuthBA = (azimuthAB + 180) % 360; const azimuthBP = helpers.bearingToAzimuth((mercator !== true) ? bearing__default['default'](b, p) : rhumbBearing__default['default'](b, p)); let angleB = Math.abs(azimuthBP - azimuthBA); if (angleB > 180) angleB = Math.abs(angleB - 360); // also if the angle ABP is acute the projection of P falls outside the segment, on the other side // so return the distance between P and the end point B /* ____P ____/ /| ____/ / | ____/ / | /______________/ | A B H */ if (angleB > 90) return (mercator !== true) ? distance__default['default'](p, b, options) : euclideanDistance(p, b, options); // finally if the projection falls inside the segment // return the distance between P and the segment /* P __/|\ __/ | \ __/ | \ __/ | \ /____________|____\ A H B */ if (mercator !== true) return distanceAP * Math.sin(helpers.degreesToRadians(angleA)); return mercatorPH(a, b, p, options); } /** * Returns the distance between a point P on a segment AB, on Mercator projection * * @private * @param {Array<number>} a first segment point * @param {Array<number>} b second segment point * @param {Array<number>} p external point * @param {Object} [options={}] Optional parameters * @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers * @returns {number} distance */ function mercatorPH(a, b, p, options) { let delta = 0; // translate points if any is crossing the 180th meridian if (Math.abs(a[0]) >= 180 || Math.abs(b[0]) >= 180 || Math.abs(p[0]) >= 180) { delta = (a[0] > 0 || b[0] > 0 || p[0] > 0) ? -180 : 180; } const origin = helpers.point(p); const A = projection.toMercator([a[0] + delta, a[1]]); const B = projection.toMercator([b[0] + delta, b[1]]); const P = projection.toMercator([p[0] + delta, p[1]]); const h = projection.toWgs84(euclideanIntersection(A, B, P)); if (delta !== 0) h[0] -= delta; // translate back to original position const distancePH = rhumbDistance__default['default'](origin, h, options); return distancePH; } /** * Returns the point H projection of a point P on a segment AB, on the euclidean plain * from https://stackoverflow.com/questions/10301001/perpendicular-on-a-line-segment-from-a-given-point#answer-12499474 * P * | * | * _________|____ * A H B * * @private * @param {Array<number>} a first segment point * @param {Array<number>} b second segment point * @param {Array<number>} p external point * @returns {Array<number>} projected point */ function euclideanIntersection(a, b, p) { const x1 = a[0], y1 = a[1], x2 = b[0], y2 = b[1], x3 = p[0], y3 = p[1]; const px = x2 - x1, py = y2 - y1; const dab = px * px + py * py; const u = ((x3 - x1) * px + (y3 - y1) * py) / dab; const x = x1 + u * px, y = y1 + u * py; return [x, y]; // H } /** * Returns euclidean distance between two points * * @private * @param {Object} from first point * @param {Object} to second point * @param {Object} [options={}] Optional parameters * @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers * @returns {number} squared distance */ function euclideanDistance(from, to, options) { const units = options.units; // translate points if any is crossing the 180th meridian let delta = 0; if (Math.abs(from[0]) >= 180) { delta = (from[0] > 0) ? -180 : 180; } if (Math.abs(to[0]) >= 180) { delta = (to[0] > 0) ? -180 : 180; } const p1 = projection.toMercator([from[0] + delta, from[1]]); const p2 = projection.toMercator([to[0] + delta, to[1]]); const sqr = function (n) { return n * n; }; const squareD = sqr(p1[0] - p2[0]) + sqr(p1[1] - p2[1]); const d = Math.sqrt(squareD); return helpers.convertLength(d, 'meters', units); } module.exports = pointToLineDistance;