turf icon indicating copy to clipboard operation
turf copied to clipboard

Problem with line offset

Open Armindou opened this issue 8 years ago • 14 comments

Hi guys, im having some issues with the line offset function, it was working ok with a test dataset but then I changed the data and it doesn't work anymore.

The function is returning an array with the first and last coordinate but a bunch of [NaN, NaN] in between. I'm not sure if I have to do something to the data before calling the function, it was working correctly but maybe im missing something.

https://codepen.io/Armindou/pen/dzLgRz?editors=1111

Thanks in advance!

This may be off-topic but the reason im using the line offset is because I need to create a polygon around the route, so what im trying to do is to get 2 offsets (a positive and a negative one), then merge them into a single line and making a polygon of it.

Armindou avatar Sep 05 '17 22:09 Armindou

@Armindou it seems your "notWorking" coordinates need to be cleaned with @turf/clean-coords (I noticed there are few duplicated points); this the result.

var line = turf.cleanCoords(turf.lineString([...coordinates...]);
var offsetLine = turf.lineOffset(line, -0.04, "kilometers");

However @rowanwins @DenisCarriere it seems the output of @turf/line-offset is still off in some places:

screen shot 2017-09-05 at 9 48 09 pm

screen shot 2017-09-05 at 9 49 51 pm

@Armindou for your application you might want to consider also @turf/buffer:

var line = turf.cleanCoords(turf.lineString([...coordinates...]);
var offsetLine = turf.buffer(line, 0.04, "kilometers");

Here the result with one of your lines as an example. Please be aware though that this module uses jsts as dependency, an external library that sometimes can generate unexpected results.

stebogit avatar Sep 06 '17 05:09 stebogit

What he ( @stebogit ) said :)

I was 90% of the way through the exact same response.

rowanwins avatar Sep 06 '17 05:09 rowanwins

👍 @stebogit Good response, I've noticed this type of behavior a few times as well, no clue how to fix it though... 🤔

DenisCarriere avatar Sep 07 '17 16:09 DenisCarriere

I've started work on it and found a partial fix but still doesn't get rid of all kinks. I've got a feeling a better answer lies in a complicated sweepline algorithm...

rowanwins avatar Sep 08 '17 01:09 rowanwins

In this picture I created a lineOffset of 10 meters and a buffer of 10 meters based on Tenison Ave.

It seems that the lineOffset is incorrect (skewed)

screen shot 2017-11-07 at 09 12 14

(The offset is the blue line)

jaapster avatar Nov 07 '17 08:11 jaapster

Hi @jaapster

Unfortunately buffer and offset algorithms aren't as simple as they seem. If you run the same buffer using a variety of tools (eg QGIS vs shapely vs ArcMap) all the outputs will differ slightly.

That said the lineOffset module was just my hack together so there may well be better ways to do it, we've already identified a few shortfalls that Im trying to rectify.

Cheers

rowanwins avatar Nov 07 '17 21:11 rowanwins

@jaapster , @rowanwins -- Yes, there's a definite skew in the line offset algorithm. On the equator, it's fine. At higher latitudes, the offset contracts east-west and expands north-south. If it were doing non-geodesic calculation then one would expect to see the opposite of this effect; instead it appears to be doing geodesic calculations incorrectly.

nkoren avatar Dec 01 '17 01:12 nkoren

Hi @nkoren, @rowanwins, I have done some tests and it also looks wrong to me (around 10% error at 40deg latitude). @nkoren: have you been able to work on the fix of the identfied issues? Thanks in advance

pyarza avatar Sep 18 '18 17:09 pyarza

Why not use the buffer to generate the offset? If we create the buffer like shown by @jaapster, we simply need to cut the buffer perpendicular to the last and first coordinates. Then we select the line on the left or on the right. Correct me if i'm wrong or if I forget specific cases.

kaligrafy avatar Jan 29 '19 15:01 kaligrafy

@pyarza Unfortunately, in our case it was easier roll our own geometry library rather than to patch Turf. Sorry!

nkoren avatar Jan 29 '19 16:01 nkoren

Hi @nkoren ! If you can give me a guide or a workarround I would be very grateful!

I have problems when the multistring becomes concave!

Thanks!

AntonyFagundezStk avatar Mar 11 '20 20:03 AntonyFagundezStk

I've made this pull request #1949 that gives a better result on small (at least several kms) #offsets

PereUbu7 avatar Sep 24 '20 07:09 PereUbu7

function dist2d(coord1, coord2) {
  let dx = coord1[0] - coord2[0];
  let dy = coord1[1] - coord2[1];
  return Math.sqrt(dx * dx + dy * dy)
}
function equals(coord1, coord2) {
  let equals = true;
  for (let i = coord1.length - 1; i >= 0; --i){
    if (coord1[i] != coord2[i]) {
      equals = false;
      break
    }
  }
  return equals
}
function offsetCoords(coords, offset) {
    var path = [];
    var N = coords.length-1;
    var max = N;
    var mi, mi1, li, li1, ri, ri1, si, si1, Xi1, Yi1;
    var p0, p1, p2;
    var isClosed = equals(coords[0],coords[N]);
    if (!isClosed) {
            p0 = coords[0];
            p1 = coords[1];
            p2 = [
                    p0[0] + (p1[1] - p0[1]) / dist2d(p0,p1) *offset,
                    p0[1] - (p1[0] - p0[0]) / dist2d(p0,p1) *offset
            ];
            path.push(p2);
            coords.push(coords[N])
            N++;
            max--;
    }
    for (var i = 0; i < max; i++) {
            p0 = coords[i];
            p1 = coords[(i+1) % N];
            p2 = coords[(i+2) % N];
            mi = (p1[1] - p0[1])/(p1[0] - p0[0]);
            mi1 = (p2[1] - p1[1])/(p2[0] - p1[0]);
            // Prevent alignements
            if (Math.abs(mi-mi1) > 1e-10) {
                    li = Math.sqrt((p1[0] - p0[0])*(p1[0] - p0[0])+(p1[1] - p0[1])*(p1[1] - p0[1]));
                    li1 = Math.sqrt((p2[0] - p1[0])*(p2[0] - p1[0])+(p2[1] - p1[1])*(p2[1] - p1[1]));
                    ri = p0[0] + offset*(p1[1] - p0[1])/li;
                    ri1 = p1[0] + offset*(p2[1] - p1[1])/li1;
                    si = p0[1] - offset*(p1[0] - p0[0])/li;
                    si1 = p1[1] - offset*(p2[0] - p1[0])/li1;
                    Xi1 = (mi1*ri1-mi*ri+si-si1) / (mi1-mi);
                    Yi1 = (mi*mi1*(ri1-ri)+mi1*si-mi*si1) / (mi1-mi);
                    // Correction for vertical lines
                    if(p1[0] - p0[0] == 0) {
                            Xi1 = p1[0] + offset*(p1[1] - p0[1])/Math.abs(p1[1] - p0[1]);
                            Yi1 = mi1*Xi1 - mi1*ri1 + si1;
                    }
                    if (p2[0] - p1[0] == 0 ) {
                            Xi1 = p2[0] + offset*(p2[1] - p1[1])/Math.abs(p2[1] - p1[1]);
                            Yi1 = mi*Xi1 - mi*ri + si;
                    }
                    path.push([Xi1, Yi1]);
            }
    }
    if (isClosed) {
            path.push(path[0]);
    } else {
            coords.pop();
            p0 = coords[coords.length-1];
            p1 = coords[coords.length-2];
            p2 = [
                    p0[0] - (p1[1] - p0[1]) / dist2d(p0,p1) *offset,
                    p0[1] + (p1[0] - p0[0]) / dist2d(p0,p1) *offset
            ];
            path.push(p2);
    }
    return path;
}

redrockhorse avatar Sep 16 '22 08:09 redrockhorse

function dist2d(coord1, coord2) {
  let dx = coord1[0] - coord2[0];
  let dy = coord1[1] - coord2[1];
  return Math.sqrt(dx * dx + dy * dy)
}
function equals(coord1, coord2) {
  let equals = true;
  for (let i = coord1.length - 1; i >= 0; --i){
    if (coord1[i] != coord2[i]) {
      equals = false;
      break
    }
  }
  return equals
}
function offsetCoords(coords, offset) {
    var path = [];
    var N = coords.length-1;
    var max = N;
    var mi, mi1, li, li1, ri, ri1, si, si1, Xi1, Yi1;
    var p0, p1, p2;
    var isClosed = equals(coords[0],coords[N]);
    if (!isClosed) {
            p0 = coords[0];
            p1 = coords[1];
            p2 = [
                    p0[0] + (p1[1] - p0[1]) / dist2d(p0,p1) *offset,
                    p0[1] - (p1[0] - p0[0]) / dist2d(p0,p1) *offset
            ];
            path.push(p2);
            coords.push(coords[N])
            N++;
            max--;
    }
    for (var i = 0; i < max; i++) {
            p0 = coords[i];
            p1 = coords[(i+1) % N];
            p2 = coords[(i+2) % N];
            mi = (p1[1] - p0[1])/(p1[0] - p0[0]);
            mi1 = (p2[1] - p1[1])/(p2[0] - p1[0]);
            // Prevent alignements
            if (Math.abs(mi-mi1) > 1e-10) {
                    li = Math.sqrt((p1[0] - p0[0])*(p1[0] - p0[0])+(p1[1] - p0[1])*(p1[1] - p0[1]));
                    li1 = Math.sqrt((p2[0] - p1[0])*(p2[0] - p1[0])+(p2[1] - p1[1])*(p2[1] - p1[1]));
                    ri = p0[0] + offset*(p1[1] - p0[1])/li;
                    ri1 = p1[0] + offset*(p2[1] - p1[1])/li1;
                    si = p0[1] - offset*(p1[0] - p0[0])/li;
                    si1 = p1[1] - offset*(p2[0] - p1[0])/li1;
                    Xi1 = (mi1*ri1-mi*ri+si-si1) / (mi1-mi);
                    Yi1 = (mi*mi1*(ri1-ri)+mi1*si-mi*si1) / (mi1-mi);
                    // Correction for vertical lines
                    if(p1[0] - p0[0] == 0) {
                            Xi1 = p1[0] + offset*(p1[1] - p0[1])/Math.abs(p1[1] - p0[1]);
                            Yi1 = mi1*Xi1 - mi1*ri1 + si1;
                    }
                    if (p2[0] - p1[0] == 0 ) {
                            Xi1 = p2[0] + offset*(p2[1] - p1[1])/Math.abs(p2[1] - p1[1]);
                            Yi1 = mi*Xi1 - mi*ri + si;
                    }
                    path.push([Xi1, Yi1]);
            }
    }
    if (isClosed) {
            path.push(path[0]);
    } else {
            coords.pop();
            p0 = coords[coords.length-1];
            p1 = coords[coords.length-2];
            p2 = [
                    p0[0] - (p1[1] - p0[1]) / dist2d(p0,p1) *offset,
                    p0[1] + (p1[0] - p0[0]) / dist2d(p0,p1) *offset
            ];
            path.push(p2);
    }
    return path;
}

use this code to solve the problem

redrockhorse avatar Sep 16 '22 08:09 redrockhorse