diff --git a/packages/turf-nearest-point-on-line/README.md b/packages/turf-nearest-point-on-line/README.md index 30a732d28d..c6aba3c0e8 100644 --- a/packages/turf-nearest-point-on-line/README.md +++ b/packages/turf-nearest-point-on-line/README.md @@ -12,8 +12,8 @@ on the line. ### Parameters -* `lines` **([Geometry][1] | [Feature][2]<([LineString][3] | [MultiLineString][4])>)** lines to snap to -* `pt` **([Geometry][1] | [Feature][2]<[Point][5]> | [Array][6]<[number][7]>)** point to snap from +* `lines` **([Geometry][1] | [Feature][2]<([LineString][3] | [MultiLineString][4])>)** Lines to snap to +* `inputPoint` **([Geometry][1] | [Feature][2]<[Point][5]> | [Array][6]<[number][7]>)** Point to snap from * `options` **[Object][8]** Optional parameters (optional, default `{}`) * `options.units` **Units** Supports all valid Turf [Units][9] (optional, default `'kilometers'`) @@ -29,16 +29,16 @@ var line = turf.lineString([ [-77.021884, 38.889563], [-77.019824, 38.892368] ]); -var pt = turf.point([-77.037076, 38.884017]); +var inputPoint = turf.point([-77.037076, 38.884017]); -var snapped = turf.nearestPointOnLine(line, pt, {units: 'miles'}); +var snapped = turf.nearestPointOnLine(line, inputPoint, {units: 'miles'}); //addToMap -var addToMap = [line, pt, snapped]; +var addToMap = [line, inputPoint, snapped]; snapped.properties['marker-color'] = '#00f'; ``` -Returns **[Feature][2]<[Point][5]>** closest point on the `line` to `point`. The properties object will contain four values: `index`: closest point was found on nth line part, `multiFeatureIndex`: closest point was found on the nth line of the `MultiLineString`, `dist`: distance between pt and the closest point, `location`: distance along the line between start and the closest point. +Returns **[Feature][2]<[Point][5]>** closest point on the `lines` to the `inputPoint`. The point will have the following properties: `lineStringIndex`: closest point was found on the nth LineString (only relevant if input is MultiLineString), `segmentIndex`: closest point was found on nth line segment of the LineString, `totalDistance`: distance along the line from the absolute start of the MultiLineString, `lineDistance`: distance along the line from the start of the LineString where the closest point was found, `segmentDistance`: distance along the line from the start of the line segment where the closest point was found, `pointDistance`: distance to the input point. [1]: https://tools.ietf.org/html/rfc7946#section-3.1 diff --git a/packages/turf-nearest-point-on-line/index.ts b/packages/turf-nearest-point-on-line/index.ts index 231517e71c..45e8c24975 100644 --- a/packages/turf-nearest-point-on-line/index.ts +++ b/packages/turf-nearest-point-on-line/index.ts @@ -18,11 +18,11 @@ import { getCoord, getCoords } from "@turf/invariant"; * on the line. * * @function - * @param {Geometry|Feature} lines lines to snap to - * @param {Geometry|Feature|number[]} pt point to snap from + * @param {Geometry|Feature} lines Lines to snap to + * @param {Geometry|Feature|number[]} inputPoint Point to snap from * @param {Object} [options={}] Optional parameters * @param {Units} [options.units='kilometers'] Supports all valid Turf {@link https://turfjs.org/docs/api/types/Units Units} - * @returns {Feature} closest point on the `line` to `point`. The properties object will contain four values: `index`: closest point was found on nth line part, `multiFeatureIndex`: closest point was found on the nth line of the `MultiLineString`, `dist`: distance between pt and the closest point, `location`: distance along the line between start and the closest point. + * @returns {Feature} closest point on the `lines` to the `inputPoint`. The point will have the following properties: `lineStringIndex`: closest point was found on the nth LineString (only relevant if input is MultiLineString), `segmentIndex`: closest point was found on nth line segment of the LineString, `totalDistance`: distance along the line from the absolute start of the MultiLineString, `lineDistance`: distance along the line from the start of the LineString where the closest point was found, `segmentDistance`: distance along the line from the start of the line segment where the closest point was found, `pointDistance`: distance to the input point. * @example * var line = turf.lineString([ * [-77.031669, 38.878605], @@ -32,48 +32,73 @@ import { getCoord, getCoords } from "@turf/invariant"; * [-77.021884, 38.889563], * [-77.019824, 38.892368] * ]); - * var pt = turf.point([-77.037076, 38.884017]); + * var inputPoint = turf.point([-77.037076, 38.884017]); * - * var snapped = turf.nearestPointOnLine(line, pt, {units: 'miles'}); + * var snapped = turf.nearestPointOnLine(line, inputPoint, {units: 'miles'}); * * //addToMap - * var addToMap = [line, pt, snapped]; + * var addToMap = [line, inputPoint, snapped]; * snapped.properties['marker-color'] = '#00f'; */ function nearestPointOnLine( lines: Feature | G, - pt: Coord, + inputPoint: Coord, options: { units?: Units } = {} ): Feature< Point, { - dist: number; - index: number; + lineStringIndex: number; + segmentIndex: number; + totalDistance: number; + lineDistance: number; + segmentDistance: number; + pointDistance: number; + // deprecated properties START + /** @deprecated use `lineStringIndex` instead */ multiFeatureIndex: number; + /** @deprecated use `segmentIndex` instead */ + index: number; + /** @deprecated use `totalDistance` instead */ location: number; + /** @deprecated use `pointDistance` instead */ + dist: number; + // deprecated properties END [key: string]: any; } > { - if (!lines || !pt) { - throw new Error("lines and pt are required arguments"); + if (!lines || !inputPoint) { + throw new Error("lines and inputPoint are required arguments"); } - const ptPos = getCoord(pt); + const inputPos = getCoord(inputPoint); - let closestPt: Feature< - Point, - { dist: number; index: number; multiFeatureIndex: number; location: number } - > = point([Infinity, Infinity], { - dist: Infinity, - index: -1, + let closestPt = point([Infinity, Infinity], { + lineStringIndex: -1, + segmentIndex: -1, + totalDistance: -1, + lineDistance: -1, + segmentDistance: -1, + pointDistance: Infinity, + // deprecated properties START multiFeatureIndex: -1, + index: -1, location: -1, + dist: Infinity, + // deprecated properties END }); - let length = 0.0; + let totalDistance = 0.0; + let lineDistance = 0.0; + let currentLineStringIndex = -1; flattenEach( lines, - function (line: any, _featureIndex: number, multiFeatureIndex: number) { + function (line: any, _featureIndex: number, lineStringIndex: number) { + //reset lineDistance at each changed lineStringIndex + if (currentLineStringIndex !== lineStringIndex) { + currentLineStringIndex = lineStringIndex; + lineDistance = 0.0; + } + const coords: any = getCoords(line); for (let i = 0; i < coords.length - 1; i++) { @@ -85,47 +110,59 @@ function nearestPointOnLine( const stop: Feature = point(coords[i + 1]); const stopPos = getCoord(stop); - // sectionLength - const sectionLength = distance(start, stop, options); + // segmentLength + const segmentLength = distance(start, stop, options); let intersectPos: Position; let wasEnd: boolean; // Short circuit if snap point is start or end position of the line // Test the end position first for consistency in case they are // coincident - if (stopPos[0] === ptPos[0] && stopPos[1] === ptPos[1]) { + if (stopPos[0] === inputPos[0] && stopPos[1] === inputPos[1]) { [intersectPos, wasEnd] = [stopPos, true]; - } else if (startPos[0] === ptPos[0] && startPos[1] === ptPos[1]) { + } else if (startPos[0] === inputPos[0] && startPos[1] === inputPos[1]) { [intersectPos, wasEnd] = [startPos, false]; } else { // Otherwise, find the nearest point the hard way. [intersectPos, wasEnd] = nearestPointOnSegment( startPos, stopPos, - ptPos + inputPos ); } - const intersectPt = point(intersectPos, { - dist: distance(pt, intersectPos, options), - multiFeatureIndex: multiFeatureIndex, - location: length + distance(start, intersectPos, options), - }); - - if (intersectPt.properties.dist < closestPt.properties.dist) { - closestPt = { - ...intersectPt, - properties: { - ...intersectPt.properties, - // Legacy behaviour where index progresses to next segment # if we - // went with the end point this iteration. - index: wasEnd ? i + 1 : i, - }, + const pointDistance = distance(inputPoint, intersectPos, options); + + if (pointDistance < closestPt.properties.pointDistance) { + const segmentDistance = distance(start, intersectPos, options); + closestPt = point(intersectPos, { + lineStringIndex: lineStringIndex, + // Legacy behaviour where index progresses to next segment + // if we went with the end point this iteration. + segmentIndex: wasEnd ? i + 1 : i, + totalDistance: totalDistance + segmentDistance, + lineDistance: lineDistance + segmentDistance, + segmentDistance: segmentDistance, + pointDistance: pointDistance, + // deprecated properties START + multiFeatureIndex: -1, + index: -1, + location: -1, + dist: Infinity, + }); + closestPt.properties = { + ...closestPt.properties, + multiFeatureIndex: closestPt.properties.lineStringIndex, + index: closestPt.properties.segmentIndex, + location: closestPt.properties.totalDistance, + dist: closestPt.properties.pointDistance, + // deprecated properties END }; } - // update length - length += sectionLength; + // update totalDistance and lineDistance + totalDistance += segmentLength; + lineDistance += segmentLength; } } ); diff --git a/packages/turf-nearest-point-on-line/package.json b/packages/turf-nearest-point-on-line/package.json index 846c7630cc..7856d1b416 100644 --- a/packages/turf-nearest-point-on-line/package.json +++ b/packages/turf-nearest-point-on-line/package.json @@ -5,6 +5,7 @@ "author": "Turf Authors", "contributors": [ "Angel Lacret <@alacret>", + "Emil Junker <@EmilJunker>", "Jon Miles <@jonmiles>", "John Ziebro <@Insighttful>" ], diff --git a/packages/turf-nearest-point-on-line/test.ts b/packages/turf-nearest-point-on-line/test.ts index 9dec50f98b..4bd13e3bd4 100644 --- a/packages/turf-nearest-point-on-line/test.ts +++ b/packages/turf-nearest-point-on-line/test.ts @@ -37,9 +37,20 @@ test("turf-nearest-point-on-line", (t) => { for (const { name, filename, geojson } of fixtures) { const [line, point] = geojson.features; const onLine = nearestPointOnLine(line, point); - onLine.properties["marker-color"] = "#F0F"; + onLine.geometry.coordinates[0] = round(onLine.geometry.coordinates[0], 6); + onLine.geometry.coordinates[1] = round(onLine.geometry.coordinates[1], 6); + onLine.properties.totalDistance = round(onLine.properties.totalDistance, 6); + onLine.properties.lineDistance = round(onLine.properties.lineDistance, 6); + onLine.properties.segmentDistance = round( + onLine.properties.segmentDistance, + 6 + ); + onLine.properties.pointDistance = round(onLine.properties.pointDistance, 6); + // deprecated properties START onLine.properties.dist = round(onLine.properties.dist, 6); onLine.properties.location = round(onLine.properties.location, 6); + // deprecated properties END + onLine.properties["marker-color"] = "#F0F"; const between = lineString( [onLine.geometry.coordinates, point.geometry.coordinates], { stroke: "#F00", "stroke-width": 6 } @@ -75,9 +86,9 @@ test("turf-nearest-point-on-line - first point", (t) => { "pt on start does not move" ); t.equal( - Number(snapped.properties.location.toFixed(6)), + Number(snapped.properties.totalDistance.toFixed(6)), 0, - "properties.location" + "properties.totalDistance" ); t.end(); @@ -104,7 +115,7 @@ test("turf-nearest-point-on-line - points behind first point", (t) => { first.geometry.coordinates, "pt behind start moves to first vertex" ); - expectedLocation.push(Number(snapped.properties.location.toFixed(6))); + expectedLocation.push(Number(snapped.properties.totalDistance.toFixed(6))); }); const filepath = @@ -113,7 +124,7 @@ test("turf-nearest-point-on-line - points behind first point", (t) => { t.deepEqual( loadJsonFileSync(filepath), expectedLocation, - "properties.location" + "properties.totalDistance" ); t.end(); }); @@ -142,7 +153,7 @@ test("turf-nearest-point-on-line - points in front of last point", (t) => { last.geometry.coordinates, "pt behind start moves to last vertex" ); - expectedLocation.push(Number(snapped.properties.location.toFixed(6))); + expectedLocation.push(Number(snapped.properties.totalDistance.toFixed(6))); }); const filepath = @@ -151,7 +162,7 @@ test("turf-nearest-point-on-line - points in front of last point", (t) => { t.deepEqual( loadJsonFileSync(filepath), expectedLocation, - "properties.location" + "properties.totalDistance" ); t.end(); }); @@ -205,7 +216,9 @@ test("turf-nearest-point-on-line - points on joints", (t) => { "pt on joint stayed in place" ); if (!expectedLocation[i]) expectedLocation[i] = []; - expectedLocation[i][j] = Number(snapped.properties.location.toFixed(6)); + expectedLocation[i][j] = Number( + snapped.properties.totalDistance.toFixed(6) + ); }); }); @@ -214,7 +227,7 @@ test("turf-nearest-point-on-line - points on joints", (t) => { t.deepEqual( expectedLocation, loadJsonFileSync(filepath), - "properties.location" + "properties.totalDistance" ); t.end(); }); @@ -245,7 +258,7 @@ test("turf-nearest-point-on-line - points on top of line", (t) => { const snapped = nearestPointOnLine(line, pt, { units: "miles" }); const shift = distance(pt, snapped, { units: "miles" }); t.true(shift < 0.000001, "pt did not shift far"); - expectedLocation.push(Number(snapped.properties.location.toFixed(6))); + expectedLocation.push(Number(snapped.properties.totalDistance.toFixed(6))); } const filepath = @@ -254,7 +267,7 @@ test("turf-nearest-point-on-line - points on top of line", (t) => { t.deepEqual( expectedLocation, loadJsonFileSync(filepath), - "properties.location" + "properties.totalDistance" ); t.end(); }); @@ -305,7 +318,7 @@ test("turf-nearest-point-on-line - points on sides of lines", (t) => { t.end(); }); -test("turf-nearest-point-on-line - check dist and index", (t) => { +test("turf-nearest-point-on-line - segmentIndex and pointDistance", (t) => { const line = lineString([ [-92.090492, 41.102897], [-92.191085, 41.079868], @@ -320,11 +333,11 @@ test("turf-nearest-point-on-line - check dist and index", (t) => { const pt = point([-92.110576, 41.040649]); const snapped = truncate(nearestPointOnLine(line, pt)); - t.equal(snapped.properties.index, 8, "properties.index"); + t.equal(snapped.properties.segmentIndex, 8, "properties.segmentIndex"); t.equal( - Number(snapped.properties.dist.toFixed(6)), + Number(snapped.properties.pointDistance.toFixed(6)), 0.823802, - "properties.dist" + "properties.pointDistance" ); t.deepEqual( snapped.geometry.coordinates, @@ -342,9 +355,9 @@ test("turf-nearest-point-on-line -- Issue #691", (t) => { [9, 50], ]); const pointAlong = along(line1, 10); - const { location } = nearestPointOnLine(line1, pointAlong).properties; + const { totalDistance } = nearestPointOnLine(line1, pointAlong).properties; - t.false(isNaN(location)); + t.false(isNaN(totalDistance)); t.end(); }); @@ -377,7 +390,7 @@ test("turf-nearest-point-on-line -- Geometry Support", (t) => { t.end(); }); -test("turf-nearest-point-on-line -- multifeature index", (t) => { +test("turf-nearest-point-on-line -- lineStringIndex", (t) => { const pt = point([4, 30]); const multiLine = multiLineString([ [ @@ -392,9 +405,40 @@ test("turf-nearest-point-on-line -- multifeature index", (t) => { ], ]); t.equal( - nearestPointOnLine(multiLine.geometry, pt).properties.multiFeatureIndex, + nearestPointOnLine(multiLine.geometry, pt).properties.lineStringIndex, 1, - "multiFeatureIndex" + "properties.lineStringIndex" + ); + t.end(); +}); + +test("turf-nearest-point-on-line -- issue 2753", (t) => { + const multiLine = multiLineString([ + [ + [-122.3125, 47.6632], + [-122.3102, 47.6646], + ], + [ + [-122.3116, 47.6623], + [-122.3091, 47.6636], + ], + ]); + + const ptA = point([-122.3106, 47.6638], { name: "A" }); + const ptB = point([-122.3102, 47.6634], { name: "B" }); + + const nearestToA = nearestPointOnLine(multiLine, ptA, { units: "meters" }); + const nearestToB = nearestPointOnLine(multiLine, ptB, { units: "meters" }); + + t.equal( + Number(nearestToA.properties.lineDistance.toFixed(6)), + 150.296465, + "nearestToA lineDistance" + ); + t.equal( + Number(nearestToB.properties.lineDistance.toFixed(6)), + 157.738215, + "nearestToB lineDistance" ); t.end(); }); @@ -514,7 +558,7 @@ test("turf-nearest-point-on-line -- issue 2808 redundant point support", (t) => const thePoint = point([10.57846, 49.8468386]); const nearest = nearestPointOnLine(line1, thePoint); // should not throw - t.equal(nearest.properties.dist, 0, "redundant point should not throw"); + t.equal(nearest.properties.pointDistance, 0, "redundant point support"); t.end(); }); @@ -528,8 +572,8 @@ test("turf-nearest-point-on-line -- duplicate points on line string shouldn't br ]); const userPoint = point([-80.191762, 25.885587]); const nearest = nearestPointOnLine(line, userPoint, { units: "meters" }); - t.equal(nearest.properties.dist > 4, true, "dist should be greater than 4"); - t.equal(nearest.properties.location, 0, "location should be 0"); + t.equal(nearest.properties.pointDistance > 4, true, "pointDistance be > 4"); + t.equal(nearest.properties.totalDistance, 0, "totalDistance be 0"); t.end(); }); diff --git a/packages/turf-nearest-point-on-line/test/out/line-northern-latitude-#344.geojson b/packages/turf-nearest-point-on-line/test/out/line-northern-latitude-#344.geojson index 4c16f72487..10518c5c43 100644 --- a/packages/turf-nearest-point-on-line/test/out/line-northern-latitude-#344.geojson +++ b/packages/turf-nearest-point-on-line/test/out/line-northern-latitude-#344.geojson @@ -39,6 +39,12 @@ { "type": "Feature", "properties": { + "lineStringIndex": 0, + "segmentIndex": 1, + "totalDistance": 19.748879, + "lineDistance": 19.748879, + "segmentDistance": 1.771897, + "pointDistance": 5.959562, "dist": 5.959562, "multiFeatureIndex": 0, "location": 19.748879, diff --git a/packages/turf-nearest-point-on-line/test/out/line1.geojson b/packages/turf-nearest-point-on-line/test/out/line1.geojson index d896f49105..0d99764126 100644 --- a/packages/turf-nearest-point-on-line/test/out/line1.geojson +++ b/packages/turf-nearest-point-on-line/test/out/line1.geojson @@ -39,6 +39,12 @@ { "type": "Feature", "properties": { + "lineStringIndex": 0, + "segmentIndex": 1, + "totalDistance": 22.137494, + "lineDistance": 22.137494, + "segmentDistance": 3.445915, + "pointDistance": 2.556271, "dist": 2.556271, "multiFeatureIndex": 0, "location": 22.137494, diff --git a/packages/turf-nearest-point-on-line/test/out/multiLine1.geojson b/packages/turf-nearest-point-on-line/test/out/multiLine1.geojson index b6765f1fe4..ad4ceee464 100644 --- a/packages/turf-nearest-point-on-line/test/out/multiLine1.geojson +++ b/packages/turf-nearest-point-on-line/test/out/multiLine1.geojson @@ -67,10 +67,16 @@ { "type": "Feature", "properties": { + "lineStringIndex": 1, + "segmentIndex": 21, + "totalDistance": 9479.011715, + "lineDistance": 4800.716022, + "segmentDistance": 173.221741, + "pointDistance": 114.725451, "dist": 114.725451, - "index": 21, "multiFeatureIndex": 1, "location": 9479.011715, + "index": 21, "marker-color": "#F0F" }, "geometry": { diff --git a/packages/turf-nearest-point-on-line/test/out/multiLine2.geojson b/packages/turf-nearest-point-on-line/test/out/multiLine2.geojson index 6c55987cc8..33d69b0377 100644 --- a/packages/turf-nearest-point-on-line/test/out/multiLine2.geojson +++ b/packages/turf-nearest-point-on-line/test/out/multiLine2.geojson @@ -48,10 +48,16 @@ { "type": "Feature", "properties": { + "lineStringIndex": 1, + "segmentIndex": 0, + "totalDistance": 1656.139708, + "lineDistance": 0, + "segmentDistance": 0, + "pointDistance": 390.942725, "dist": 390.942725, - "index": 0, "multiFeatureIndex": 1, "location": 1656.139708, + "index": 0, "marker-color": "#F0F" }, "geometry": { diff --git a/packages/turf-nearest-point-on-line/test/out/multiLine3.geojson b/packages/turf-nearest-point-on-line/test/out/multiLine3.geojson index d858ad925f..60de5cb7b1 100644 --- a/packages/turf-nearest-point-on-line/test/out/multiLine3.geojson +++ b/packages/turf-nearest-point-on-line/test/out/multiLine3.geojson @@ -57,6 +57,12 @@ { "type": "Feature", "properties": { + "lineStringIndex": 0, + "segmentIndex": 0, + "totalDistance": 214.735285, + "lineDistance": 214.735285, + "segmentDistance": 214.735285, + "pointDistance": 121.937841, "dist": 121.937841, "multiFeatureIndex": 0, "location": 214.735285, diff --git a/packages/turf-nearest-point-on-line/test/out/route1.geojson b/packages/turf-nearest-point-on-line/test/out/route1.geojson index 71003b12d5..cafeca72c1 100644 --- a/packages/turf-nearest-point-on-line/test/out/route1.geojson +++ b/packages/turf-nearest-point-on-line/test/out/route1.geojson @@ -4792,6 +4792,12 @@ { "type": "Feature", "properties": { + "lineStringIndex": 0, + "segmentIndex": 3104, + "totalDistance": 183.46844, + "lineDistance": 183.46844, + "segmentDistance": 0.044741, + "pointDistance": 7.876557, "dist": 7.876557, "multiFeatureIndex": 0, "location": 183.46844, diff --git a/packages/turf-nearest-point-on-line/test/out/route2.geojson b/packages/turf-nearest-point-on-line/test/out/route2.geojson index 59b51f99d4..c2cc1bb967 100644 --- a/packages/turf-nearest-point-on-line/test/out/route2.geojson +++ b/packages/turf-nearest-point-on-line/test/out/route2.geojson @@ -3796,6 +3796,12 @@ { "type": "Feature", "properties": { + "lineStringIndex": 0, + "segmentIndex": 1185, + "totalDistance": 303.639629, + "lineDistance": 303.639629, + "segmentDistance": 0.867249, + "pointDistance": 19.22738, "dist": 19.22738, "multiFeatureIndex": 0, "location": 303.639629,