turf icon indicating copy to clipboard operation
turf copied to clipboard

turf.lineOffset precision

Open devleaks opened this issue 5 years ago • 6 comments

Hello, I'm using line offset to build parallels to short segments (a few kilometers long) but I found an issue with the precision of the function:

const turf = require('@turf/turf')
const line = turf.lineString([ [4.35,50.845], [4.347,50.85] ])
const parallel = turf.lineOffset(line, 1) // kilometer
const distance0 = turf.distance(line.geometry.coordinates[0], parallel.geometry.coordinates[0])
const distance1 = turf.distance(line.geometry.coordinates[1], parallel.geometry.coordinates[1])
console.log(distance0, distance1)
// output: 0.7468818837096454 0.7468398187109684

This can be reproduced at other places in the world. Any idea on how to improve precision? Thanks.

devleaks avatar Feb 19 '20 09:02 devleaks

Hi @devleaks

turf/lineOffset is a bit hacky and has some known issues although that one is a bit surprising. The module probably needs a thorough overhaul.

rowanwins avatar Mar 07 '20 12:03 rowanwins

Hi! It seems that the offset algorithm is directly copied from this stackoverflow thread https://stackoverflow.com/questions/2825412/draw-a-parallel-line This calculation is valid for a Cartesian coordinate system whereas in turf it handles a spherical coord system. This means that the longitude offset is not compensated by the latitude - It gets more squashed the longer from the equator you get.

PereUbu7 avatar Sep 22 '20 09:09 PereUbu7

Can confirm this is what @PereUbu7 says it is. We're using lineOffset to draw rectangles in London and the descrepancy between north/south rectangles and east/west is pretty close to the haversine fudge factor we use for correcting for the latitude of London.

Looks like I'll need to brush up on my maths...

andybak avatar Apr 07 '21 14:04 andybak

To be honnest, I redid the function I needed, it is quite easy: For line (p1, p2), compute the bearing p1->p2. Add or substract 90° to the bearing. Use "destination" function to compute a point with calculated bearing and supplied distance from p1 and from p2. I understand this might not be a "parallel" in spherical geometry, but for short segments (a dozen kilometer long), it is acceptable. I will not work for larger segments. Regards. P.

devleaks avatar Apr 07 '21 14:04 devleaks

I made a PR for this: https://github.com/Turfjs/turf/pull/1949 But couldn't get my env going, so it's untested.

PereUbu7 avatar Apr 08 '21 06:04 PereUbu7

Here's my code to get correct parallel line with more than 2 points :

const getLineOffset = (line, distance, { units = 'kilometers' } = {}) => {
    const lineCoords = line.geometry.coordinates;
    const transformAngle = distance < 0 ? -90 : 90;
    if (distance < 0) distance = -distance;

    const offsetLines = [];
    for (let i = 0; i < lineCoords.length - 1; i++) { // Translating each segment of the line to correct position
        const angle = bearing(lineCoords[i], lineCoords[i + 1]) + transformAngle;
        const firstPoint = transformTranslate(point(lineCoords[i]), distance, angle, { units })?.geometry.coordinates;
        const secondPoint = transformTranslate(point(lineCoords[i + 1]), distance, angle, { units })?.geometry.coordinates;
        offsetLines.push([firstPoint, secondPoint]);
    }

    const offsetCoords = [offsetLines[0][0]]; // First point inserted
    for (let i = 0; i < offsetLines.length; i++) { // For each translated segment of the initial line
        if (offsetLines[i + 1]) { // If there's another segment after this one
            const firstLine = transformScale(lineString(offsetLines[i]), 2); // transformScale is useful in case the two segment don't have an intersection point
            const secondLine = transformScale(lineString(offsetLines[i + 1]), 2); // Which happen when the resulting offset line is bigger than the initial one
            // We're calculating the intersection point between the two translated & scaled segments
            offsetCoords.push(lineIntersect(firstLine, secondLine).features[0].geometry.coordinates);
        } else offsetCoords.push(offsetLines[i][1]); // If there's no other segment after this one, we simply push the last point of the line
    }

    return lineString(offsetCoords);
};

Uolc avatar Sep 27 '21 12:09 Uolc