Turfjs / Turfjs/turf

@turf/buffer gives invalid inner buffer around closed LineString

Open
#2,929 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

@turf/buffer
Dominant language
TypeScript
Stars
10.5k
Forks
1k
Avg merge
1h 11m
Merged PRs (30d)
4

Description

Hi,

when creating a buffer around a LineString which has the same start vertex and the same end vertex the calculated inner buffer is wrong under special circumstances.

Here is a minimal code example which reproduces the wrong buffer. The first test case is a closed LineString which has a hole when creating a buffer of 500 meters. The second test case is a slightly different closed LineString which has no hole when creating a buffer of 500 meters around it. The third test case is a different example with a buffer of 2000 meters and an even bigger hole in the which shouldnt belong there.

import { buffer } from "@turf/turf";
import { LineString } from "geojson";
import { expect, it } from "vitest";

const bufferOptions: Parameters<typeof buffer>[2] = {
  units: "meters",
  steps: 0,
};

it.each<[string, LineString, number, number]>([
  [
    "Corridor with buffer should have hole",
    {
      type: "LineString",
      coordinates: [
        [11.9864449, 52.9652597],
        [12.0249011, 52.9771682],
        [12.0622777, 52.9652798],
        [11.9864449, 52.9652597],
      ],
    },
    500,
    2,
  ],
  [
    "Corridor with buffer shouldn't have hole",
    {
      type: "LineString",
      coordinates: [
        [11.9864449, 52.9652597],
        [12.0243343, 52.9740445],
        [12.0622777, 52.9652798],
        [11.9864449, 52.9652597],
      ],
    },
    500,
    1,
  ],
  [
    "Corridor with buffer shouldn't have hole",
    {
      type: "LineString",
      coordinates: [
        [12.4102822, 53.6462527],
        [12.3735781, 53.6041575],
        [12.4513151, 53.615602],
        [12.4102822, 53.6462527],
      ],
    },
    2000,
    1,
  ],
])("$1", (_name, lineString, radius, numRings) => {
  const corridor = buffer(lineString, radius, bufferOptions)!;
  expect(corridor.geometry.coordinates.length).toEqual(numRings);
});

The first test case where a hole is expected.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [11.9864449, 52.9652597],
          [12.0249011, 52.9771682],
          [12.0622777, 52.9652798],
          [11.9864449, 52.9652597]
        ]
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [11.986452128660401, 52.960763100408464],
            [12.062277041615708, 52.9607831983179],
            [12.063684519759718, 52.960863761914545],
            [12.065041550468617, 52.96110271332033],
            [12.066299470082624, 52.96149148385769],
            [12.067413166027833, 52.96201613211102],
            [12.068342693716165, 52.96265784346482],
            [12.069054708826204, 52.96339360433853],
            [12.069523663664947, 52.964197027024746],
            [12.069732724654596, 52.965039295637716],
            [12.069674377879451, 52.96589019932362],
            [12.069350700726773, 52.966719215730365],
            [12.068773289577013, 52.96749660590088],
            [12.067962845823466, 52.96819448130805],
            [12.066948434788726, 52.96878780471482],
            [12.065766443910471, 52.969255288887574],
            [12.028387364746067, 52.981144782741886],
            [12.02706246923208, 52.98147236900115],
            [12.025658060755003, 52.98164164428673],
            [12.02422580470309, 52.9816463812231],
            [12.022818391870915, 52.98148640554473],
            [12.021487598272255, 52.98116760251265],
            [11.983028781062549, 52.96925800051954],
            [11.98183337337055, 52.96879600437097],
            [11.980804897063242, 52.96820602234372],
            [11.979980573410897, 52.96750941060336],
            [11.979390229493612, 52.96673138398435],
            [11.979055219500575, 52.96590010282219],
            [11.978987653405312, 52.965045653317176],
            [11.979189960718271, 52.96419895840421],
            [11.979654804815691, 52.96339065858894],
            [11.980365350622026, 52.96265000326192],
            [11.98129587564726, 52.96200379258628],
            [11.982412702011077, 52.96147540819164],
            [11.983675415562319, 52.96108396766854],
            [11.985038327928422, 52.960843633365506],
            [11.986452128660401, 52.960763100408464]
          ],
          [
            [12.017333025212631, 52.96977031465849],
            [12.024854857987961, 52.972098339869596],
            [12.032165528799508, 52.96977419741604],
            [12.017333025212631, 52.96977031465849]
          ]
        ]
      }
    }
  ]
}

The second test case where no hole is expected. Also noteworthy is that the calculated hole is kind of mirrored/inverted.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [11.9864449, 52.9652597],
          [12.0243343, 52.9740445],
          [12.0622777, 52.9652798],
          [11.9864449, 52.9652597]
        ]
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [11.986452128633372, 52.96076310040846],
            [12.062277041642751, 52.96078319831792],
            [12.063747133387176, 52.96087116589199],
            [12.065159692119654, 52.961131780607424],
            [12.066459409642427, 52.961554838473894],
            [12.067595392214091, 52.96212377488877],
            [12.068523152296521, 52.96281631269754],
            [12.06920635040717, 52.9636053338648],
            [12.069618218935268, 52.964459940742934],
            [12.069742612068522, 52.96534666549963],
            [12.069574640466282, 52.966230780442125],
            [12.069120865462224, 52.96707765798852],
            [12.068399044770057, 52.96785412704424],
            [12.067437439235782, 52.96852977263269],
            [12.066273707440773, 52.9690781278045],
            [12.064953431224962, 52.96947771103499],
            [12.027006887787016, 52.978243262558884],
            [12.025691007582523, 52.978466269277284],
            [12.024329654362797, 52.978541100948505],
            [12.022968456852539, 52.97846524941931],
            [12.02165303855271, 52.9782412570257],
            [11.983760505127004, 52.96945560395751],
            [11.98244121918252, 52.9690550948594],
            [11.98127873731212, 52.96850596750384],
            [11.980318580480697, 52.967829728048024],
            [11.979598340679749, 52.96705285978615],
            [11.979146209448798, 52.96620578538327],
            [11.978979875485422, 52.965321674977176],
            [11.979105834283425, 52.9644351469261],
            [11.97951913648318, 52.963580912149034],
            [11.980203584371624, 52.9627924151678],
            [11.981132368418002, 52.9621005250386],
            [11.98226911856279, 52.961532327360345],
            [11.983569328854063, 52.961110064552784],
            [11.984982099561302, 52.960850265772564],
            [11.986452128633372, 52.96076310040846]
          ],
          [
            [12.02669255276277, 52.96977297957135],
            [12.024339275254995, 52.96922781769787],
            [12.021982645144059, 52.96977173037168],
            [12.02669255276277, 52.96977297957135]
          ]
        ]
      }
    }
  ]
}

The third test case where no hole is expected. Here the wrong inner hole is better visible.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [12.4102822, 53.6462527],
          [12.3735781, 53.6041575],
          [12.4513151, 53.615602],
          [12.4102822, 53.6462527]
        ]
      }
    },
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [12.434048190429264, 53.65743746814345],
            [12.429841883642712, 53.66000497984985],
            [12.424860610473045, 53.66202802983935],
            [12.419301649861998, 53.66342645795963],
            [12.413385247348295, 53.66414484704941],
            [12.407345864483649, 53.664154726539344],
            [12.401422854912267, 53.66345570488502],
            [12.39585094299714, 53.6620754851424],
            [12.390850889294208, 53.6600687630567],
            [12.386620719209533, 53.657515052124324],
            [12.383327867230836, 53.654515523282534],
            [12.346643469754003, 53.612412084962976],
            [12.344493313764195, 53.60922861559079],
            [12.343400950658962, 53.605860882939936],
            [12.34340584950627, 53.602431354553005],
            [12.344507568178525, 53.59906471542497],
            [12.346665807473236, 53.595883334947686],
            [12.349801911479396, 53.593002822243626],
            [12.353801754574436, 53.5905278308667],
            [12.358519906166206, 53.58854826426606],
            [12.363784919542637, 53.58713601848681],
            [12.369405552340158, 53.586342378911134],
            [12.37517769431382, 53.58619616415882],
            [12.380891754152904, 53.58670268341412],
            [12.45859860683452, 53.59814245377081],
            [12.464295404451546, 53.59934778948034],
            [12.469475927693365, 53.601200132664594],
            [12.473934034403996, 53.60362580403688],
            [12.477492233375447, 53.60652830501132],
            [12.480008747692269, 53.60979214611756],
            [12.481383174732379, 53.61328743223641],
            [12.481560515172976, 53.616875024194066],
            [12.480533405695052, 53.62041207287481],
            [12.478342459767518, 53.62375770648294],
            [12.475074695439954, 53.6267786446326],
            [12.434048190429264, 53.65743746814345]
          ],
          [
            [12.415680155344077, 53.61330297239273],
            [12.398237388931744, 53.62632681525116],
            [12.431292590222798, 53.63119037277658],
            [12.415680155344077, 53.61330297239273]
          ]
        ]
      }
    }
  ]
}

Latest Turf version is used which was 7.2.0 at the time of filing this bug report.
It may be related to #2920.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the supplied TypeScript reproduction against @turf/buffer in Turf 7.2.0, then trace the buffer implementation and compare its output rings with the three expected cases. Done means the first case retains its expected hole while the other two do not produce inverted or spurious inner holes; issue #2920 may provide related context.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
computer-graphics
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.