Turfjs / turf

A modular geospatial engine written in JavaScript and TypeScript
https://turfjs.org/
MIT License
9.4k stars 944 forks source link

Why does a buffered point result in a oval? #110

Closed djdmbrwsk closed 9 years ago

djdmbrwsk commented 10 years ago

I know there is probably some geographical/mathematical reason, but why does a buffered point result in a oval polygon? Thanks!

djdmbrwsk commented 10 years ago

Oh duh. So if your Point has coordinates: [0, 0] you get a perfect circle. The reason i'm getting an oval is because I'm in the Northern hemisphere. Great library, thanks for all your hard work!

morganherlocker commented 10 years ago

Thanks! You guessed it though, the issue is that web maps typically use projections that distort shapes the closer they get to the poles. Let me know if you run into any other issues or have any feature requests; feedback is always very helpful.

djdmbrwsk commented 10 years ago

Well I thought I understood this, but I guess not.

When I create a polygon at [0, 0] using turf by buffering it, it is perfectly circular. That makes sense because when projecting a spherical map to a flat surface there is less distortion at the equator. If I create the same polygon at a Boston lon/lat the result is an oval (even if it's just a 5 mile buffer). Because of map projection scaling shouldn't I really only see distortion on very large polygons? And when I do, assuming they're circular, shouldn't they look like an upside-down egg?

morganherlocker commented 10 years ago

I will need to explore this some more, but I think that the issue comes from using 4326 lat,lon data and projecting it over to 3857 (what most web maps use under the hood). One thing to try would be transforming your data over before buffering using something like proj4js. It is my understanding that the distortion in 3857 is actually pretty significant, and that you would see this sort of thing even at 5 miles.

djdmbrwsk commented 10 years ago

This has opened up a can of worms for me. I've been doing a bunch of reading and these different projection standards are quite confusing. But from what I've gathered so far though 4326 is like the standard, and as such most web map APIs take 4326 as input and translate it to whatever they need to display it (mainly 3857). Does that sound right?

If that's the case I wouldn't think I would have to do anything, but there is definitely something funky going on here.

atdrago commented 10 years ago

@morganherlocker I admittedly don't know as much about this stuff as you do, however I was looking at JTS (the parent of JSTS), and it seems like it is built for a 2d, linear plane. From the JTS site:

JTS provides a complete, consistent, robust implementation of fundamental algorithms for processing linear geometry on the 2-dimensional Cartesian plane

I created a little test that takes in @djdmbrwsk's GeoJson file and buffers the reference point (instead of using the feature collection he provided), and then computes the distance to each vertex of the buffered polygon. The points created should seemingly be 3 or close to it, however they fluctuate between 3 -> 2.2 -> 3 -> 2.2 -> 3, which is the oval shape he is talking about.

atdrago commented 10 years ago

I believe I have the buffering of a Point working now, using LatLon.prototype.destinationPoint from this blog to move in a circular direction around the point at a given distance, at a fixed number of points along the circumference of the circle to create a circular shaped polygon:

https://gist.github.com/atdrago/08a42bac9f887243b8d5

I'll try to get Polygon buffering working tomorrow using similar concepts.

morganherlocker commented 10 years ago

This is terrific, and that post is very helpful. Polygon buffers get pretty tricky when you get into complex polygons, but full geodesic buffers would be a huge win. I think you are definitely on the right track.

djdmbrwsk commented 10 years ago

@atdrago Thanks a ton for running some tests to help get to the bottom of this!

@morganherlocker Before we get too deep, you are looking for turf to be a truly geodesic library correct?

morganherlocker commented 10 years ago

@djdmbrwsk Yes. We may want to support planar for some reason in the future, but I think geodesic makes the most sense, and is definitely what people are expecting. Turf is specifically for geospatial data, not geometric data.

djdmbrwsk commented 10 years ago

I saw there was a bunch of organizational/clean up activity today, nice! We should probably mark this issue as a bug...it's a pretty nasty one. I know @atdrago has point buffering working geodesically and he's working on polygons, but I don't know how far he has gotten.

atdrago commented 10 years ago

Sorry for the delay. I went on vacation for about 11 days there.

The Point buffering was very easy, and I made attempts to get Line buffering to work, but they were not successful. Simply getting two lines that are parallel to the initial line at a specified distance turned out to be non-trivial. The road block I hit was trying to get the bearing perpendicular to the initial line's bearing so that I could measure the buffered distance away from the line (at that bearing), and draw a line at that distance with the same bearing as the initial line.

At this point, I'm going to give up on it, as it isn't terribly important to the work I need to do elsewhere, and in the meantime that work has been suffering. I do plan to loop back to this eventually if somebody else hasn't picked it up by the time I'm finished with the project I'm working on. I'm glad to see you plan to remove JSTS from turf, as it really seems bloated and inefficient (not to mention the fact that it is meant for 2d spaces). Good luck... I hope to contribute again soon!

morganherlocker commented 10 years ago

@atdrago No worries. You are not the first, and certainly not the last to attempt solving the issue (including myself). I think we will probably support geodesic points in the short term based on your code, then make a crack at lines and polygons later on.

PostGIS and a few others have solved the problem, but it is clearly non-trivial, even on a 2d plane. Arcgis only started supporting geodesic buffers on polys a couple years ago and JTS has been used for years without it, so I do not feel too far behind. Thanks for your hard work! :+1:

antoniolocandro commented 10 years ago

Hi I don't know if this is the right place to post this but in this document http://www.faa.gov/documentLibrary/media/Order/8260.54A.pdf in Appendix 2 there are lots of calculations and algorithms for solving some useful problems when working with non projected data e.g. EPSG 4326

There is also this github repo https://github.com/pkohut/GeoFormulas and http://wired2code.wordpress.com/2010/07/14/wgs84-ellipsoid-calculations/

For easy references cases covered are here http://www.pkautomation.com/wgs84_geodesic_calc.html Hope this is useful in any way

rowanwins commented 9 years ago

Probably on a similar issue... Running a buffer on a point seems to return a very jagged circle for me at the moment. I've tried on a small buffer (eg 1km) as well as a larger one (20km) and they both seem to generate similar jaggedness. I would've expected a smoother polygon? Any tips? bufferexample

On a more positive note its been really fun playing with turf, it's remarkably simple to use so thank you! Rowan

morganherlocker commented 9 years ago

thanks @rowanwins! Buffers are circles... but geojson only really represents polygons as lists of edges. This means that a "perfect" circle is not actually possible. This is a problem related to the coastline paradox. In a nutshell, a circle has an infinite number of vertices if defined as a polygon, so you have to decide on a resolution for your vertices. This is currently fixed in turf-buffer, and it is unaffected by the size of the buffer.

Also note that Leaflet does some automatic simplification of its own, so the display may be more jagged when zoomed out, even if the data you are using for calculations is more precise.

There is a branch of turf-buffer I will be pushing out in a couple days that will give you perfectly circular point buffers, as well as an optional curve resolution. This means you can specify whatever detail you would like (memory and performance will be affected, but you can tinker with it).

rowanwins commented 9 years ago

thanks @morganherlocker for the description, will await the update!

manelclos commented 9 years ago

Got into this issue today. Easy to catch if you compare it to turf.destination:

    var point = map.getCenter();
    var pt1 = turf.point([point.lng, point.lat]);

    if (geoms) {
        map.removeLayer(geoms);
    }

    var minx = turf.destination(pt1, 0.200, 270, 'kilometers');
    var miny = turf.destination(pt1, 0.200, 180, 'kilometers');
    var maxx = turf.destination(pt1, 0.200, 90, 'kilometers');
    var maxy = turf.destination(pt1, 0.200, 0, 'kilometers');
    var env1 = turf.envelope(turf.featurecollection([minx, miny, maxx, maxy]));

    var buffered = turf.buffer(pt1, 0.200, 'kilometers');
    var env2 = turf.envelope(buffered);
    geoms = L.geoJson([buffered, env2, env1, pt1, minx, miny, maxx, maxy]).addTo(map);
djdmbrwsk commented 9 years ago

FYI, @morganherlocker did make some awesome progress on the issue. In this turf-buffer branch geodesic Point, LineString, and MultiLineString buffering is working.

manelclos commented 9 years ago

Tried the pointBuffer function and it works perfect, same result! Thanks!

floledermann commented 9 years ago

Letting the user specify real-world units for the current buffer implementation is definitely a bug, as these "units" are merely statically "converted" to degrees and the buffer is then calculated using degrees - which, depending on latitude, can give vastly diverging (and wrong!) results in the current implementation, like the ellipses observed by many for the point-buffering case. As far as my understanding goes, the current Turf.js implementation does not calculate a "geodesic buffer" (which would calculate the correct shape on the ellipsoid/geoid), nor an "euclidean buffer", which would have to be calculated in the projection plane (and therefore take the map projection as an argument), but is a naïve implementation treating degrees as cartesian coordinates (so technically I guess it could be described as euclidean buffering using a fixed identity/equirectangular projection). Which is fine, if only it were properly documented and not taking a meaningless and misleading "unit" parameter that suggests geodesic buffering. (QGis does the same thing in their buffer function, only it (correctly) doesn't let you specify a "unit")

This is very unfortunate as creating a buffer from a point is the first thing in practically all the Turf.js tutorials, so you start out with something that is wrong and confusing. :(

morganherlocker commented 9 years ago

Closing in favor of the issue on turf-buffer. TLDR: geodesic buffers are in progress. I expect them to be in the next major turf release, but geodesic polygon buffers are complex, so your patience is appreciated.

bensleveritt commented 8 years ago

Hate to dredge up old issues, but has the geodesic buffer for points been merged in? I'm seeing an oval, and I wanted to check it wasn't just me.

The issue on turf-buffer doesn't go anywhere, has that project been merged into this one since then?

Antenka commented 8 years ago

Hello, I can see the updates in the standalone project here: https://github.com/Turfjs/turf-buffer/blob/artisinal/index.js But the general packages seems like not updated: https://github.com/Turfjs/turf/blob/master/packages/turf-buffer/index.js

Also as the client-side libruary: turf.min.js

Any plans about spreading the change around?

tmcw commented 8 years ago

Hi Antenka,

The file you link to is in the 'artisinal' branch of turf-buffer: the 'master' branch is the one containing finished, deployed work. The other branch is not yet finished - when it is finished, it will be merged into master and the buffers generated by master will follow its approach.

Antenka commented 8 years ago

Hi, thanks for such fast response. Any plans about when it planned to be finished/merged?

tmcw commented 8 years ago

Unfortunately not; you can refer to the JSTS removal ticket - https://github.com/Turfjs/turf/issues/88 for the depth of this task. Many people have tried, none have succeeded in finishing the fundamental geometry algorithms we need to move past JSTS. Your help would be much appreciated if you can offer it.

mclaeysb commented 8 years ago

Hi there! I gave it a try. See my pull request in turf-buffer. All comments welcome!

jaapster commented 7 years ago

Hi, any progress on this issue? I too get the oval and if I measure the radii from the center of the oval, it is clear that this buffer is not calculated correctly. Different distance in NS direction compared to SW direction.

marcosnc08 commented 6 years ago

Hi! I'm having the same issue. Could anyone find a solution?

greetclock commented 5 years ago

I faced the same problem and then I realised that I have turf package instead of @turf/turf.

drio commented 5 years ago

I am running into the same issue described by @djdmbrwsk when running the following code.

I am trying to create a buffered point and then compute programmatically if other locations are within the "area" described by the buffered point. The issue is that the area created by the buffered point is not a circle but an oval.

Details and code here:

  function addBuffer(location, radius) {
    const stopPoint = turf.point(location);
    return turf.buffer(stopPoint, radius, {units: "miles"});
  }

  const ZOOM = 15;
  const map = L.map('mapid').setView(LOCATIONS.charleston, ZOOM);
  const radiusBufferMeters = 100;
  const LOCATIONS = {  
   charleston:                [32.7794759, -79.9340512],
   secondStateCoffee        : [32.779694, -79.937644],
   closeToSecondStateCoffee : [32.779663, -79.937027],
   colonialLake             : [32.778191, -79.941629],
  }

  L.tileLayer(MAP_URL, {
    attribution: ATTR,
    minZoom: ZOOM,
    maxZoom: ZOOM,
    boxZoom: false,
    zoomControl: false,
    dragging: false
  }).addTo(map);

  L.marker(LOCATIONS.secondStateCoffee, {opacity: 0.8}).addTo(map);
  L.marker(LOCATIONS.closeToSecondStateCoffee, {opacity: 0.6}).addTo(map);
  L.marker(LOCATIONS.colonialLake, {opacity: 0.8}).addTo(map);

  const circle = L.circle(LOCATIONS.secondStateCoffee, {radius: radiusBufferMeters});
  circle.addTo(map);

  // leafleat circle
  const tCircle = turf.circle(LOCATIONS.secondStateCoffee, radiusBufferMeters, {units: 'meters'});
  const gjLayer = L.geoJson(tCircle);
  L.polygon(tCircle.geometry.coordinates, {color: "red"}).addTo(map);

  // turf buffered Point
  const bufferedPoint = addBuffer(LOCATIONS.secondStateCoffee, radiusBufferMeters/1000);
  L.polygon(bufferedPoint.geometry.coordinates, {color: "green"}).addTo(map);

Output:

Screen Shot 2019-09-18 at 3 59 31 PM

I have also written a little observable notebook while exploring the issue. Visual aim here:

gis

artuska commented 5 years ago

Faced the same issue — circle method produces ellipsis (oval) geometry. Why? #1793

stebogit commented 5 years ago

@drio you might want to take a look at this

omar-alnayme commented 4 years ago

Hi @morganherlocker is the oval buffer problem solved, I would like to thank you for the fantastic library.

MacaScull commented 3 years ago

Sorry to bring up old issues but did anyone manage to produce geodesic buffers for polygons? Also I was just wondering what algorithm you use to produce the geodesic buffers for all shapes (points, lines, polygons)