Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New module @turf/linestring-to-polygon #672

Merged
merged 5 commits into from
Apr 21, 2017
Merged

Conversation

DenisCarriere
Copy link
Member

@DenisCarriere DenisCarriere commented Apr 16, 2017

New module 🎉 @turf/linestring-to-polygon

Converts (Multi)LineString(s) to Polygon(s).

Note: Polygon with holes get converted to MultiLineString using @turf/polygon-to-linestring. This module is meant to work in harmony with polygon-to-linestring.

To-Do

  • Feature => Polygon
  • FeatureCollection => MultiPolygon
  • Polygon outer ring must be in the first position of the Array

JSDocs

/**
 * Converts a {@link LineString} or {@link MultiLineString} to a {@link Polygon}.
 *
 * @name lineStringToPolygon
 * @param {FeatureCollection|Feature<LineString|MultiLineString>} lines Features to convert
 * @param {boolean} [autoComplete=true] auto complete linestrings
 * @returns {FeatureCollection|Feature<Polygon>} converted to Polygons
 * @example
 * var line = {
 *   'type': 'Feature',
 *   'properties': {},
 *   'geometry': {
 *     'type': 'LineString',
 *     'coordinates': [[125, -30], [145, -30], [145, -20], [125, -20], [125, -30]]
 *   }
 * }
 * var polygon = turf.lineStringToPolygon(line);
 *
 * //addToMap
 * var addToMap = [polygon];
 */

Dependencies

"dependencies": {
  "@turf/helpers": "^4.1.0",
  "@turf/invariant": "^4.1.0"
}

Benchmark Results

collection-linestring x 2,337,816 ops/sec ±3.08% (86 runs sampled)
geometry-linestring x 6,574,088 ops/sec ±3.62% (83 runs sampled)
linestring-incomplete x 6,768,527 ops/sec ±3.60% (84 runs sampled)
linestring x 6,752,969 ops/sec ±1.94% (84 runs sampled)
multi-linestring-incomplete x 3,263,278 ops/sec ±2.56% (88 runs sampled)
multi-linestring-with-hole x 3,381,728 ops/sec ±1.75% (88 runs sampled)
multi-linestrings-with-holes x 1,084,043 ops/sec ±2.28% (83 runs sampled)

Example 1 (with holes)

Before: Feature Collection of (Multi)LineString

image

After: Feature Collection of Polygon (hole is preserved).

image

Example 2 (incomplete lines)

Before: Incomplete (Multi)LineString

image

After: Completed Polygons

image

CC: @Turfjs/ownership

@DenisCarriere DenisCarriere added this to the 4.2.0 milestone Apr 16, 2017
@DenisCarriere DenisCarriere self-assigned this Apr 16, 2017
@rowanwins rowanwins mentioned this pull request Apr 18, 2017
@stebogit
Copy link
Collaborator

stebogit commented Apr 19, 2017

@DenisCarriere @rowanwins Man this is definitely gonna help for isobands! Yay!! 👍
Just a note: in my experience and understanding of GeoJSON standards a multi-linestrings-with-holes would be a MultiPolygon, not just a Polygon, and the outermost Polygon needs to be the first in the coordinates array.
If I recall correctly Google Maps gave me a hard time with just Polygons since it wasn't rendering the output correctly. geojson.io instead seems to be less strict, but still not always perfect output.
What do you think, any experience that suggests differently?
I'd be happy to create some test so we can find out how it behaves.

@stebogit
Copy link
Collaborator

stebogit commented Apr 19, 2017

@DenisCarriere @rowanwins check this out: https://jsfiddle.net/jStefano/5xwxxco2/
if you render the same geojson in geojson.io instead it renders fine

@DenisCarriere
Copy link
Member Author

DenisCarriere commented Apr 19, 2017

Man this is definitely gonna help for isobands 👍 !

😄 Indeed! This will help a lot of the modules that only handle LineStrings operations and need to easily convert them back into Polygons.

in my experience and understanding of GeoJSON standards a multi-linestrings-with-holes would be a MultiPolygon

There's two very different tests going on here:

Current geometry transformations

  • MultiLineString => Polygon with Hole
  • LineString => Polygon
  • FeatureCollection of MultiLineString => FeatureCollection of Polygons with holes
  • FeatureCollection of LineString => FeatureCollection of Polygons

Proposed geometry transformations (@stebogit's suggestion)

  • MultiLineString => Polygon with Hole
  • LineString => Polygon
  • FeatureCollection of MultiLineString => MultiPolygon with holes **(changed)
  • FeatureCollection of LineString => MultiPolygon **(changed)

I'm 👍 on this change, I prefer this proposed transformation.

AutoComplete

the outermost Polygon needs to be the first in the coordinates array.

Correct, however I've added a autoComplete flag (default true) that completes the polygon if the first & last does not match (via autoCompleteCoords(coords)).

Sometimes a LineString will be "pretty close" but not exact, therefore it's pretty handy to have the autoComplete used by default instead of throwing an error.

CC: @stebogit

Changes based on #672 (comment)

- Convert FeatureCollection & GeometryCollection to MultiPolygon
- Colorize test fixtures (easier to visualize)
- Update Typescript defintion & tests
CC: @stebogit
@DenisCarriere
Copy link
Member Author

I've updated the repo with the proposed changes ( 👍 it's a lot better now).

Key take aways

Output type changes based on input type

  • Feature => Polygon
  • FeatureCollection => MultiPolygon

The index.d.ts is a good place to understand the input/output flow.

@stebogit Feel free to include more test fixtures

@DenisCarriere
Copy link
Member Author

@stebogit check this out: https://jsfiddle.net/jStefano/5xwxxco2/
if you render the same geojson in geojson.io instead it renders fine

Open an issue ticket to Google Maps API 🤓, that Polygon's geometry is perfectly valid.

@stebogit
Copy link
Collaborator

@DenisCarriere you know what, I did already at the time 🤓, and they told me the right geometry is a MultiPolygon (that's why I new about that, I'm not an expert! 😅)...
And actually it make sense, otherwise what's the need of an additional feature type? They would all be just Polygons no?
Is there any other geojson mapping tools we could use to see if that geometry is rendered as expected?

@DenisCarriere
Copy link
Member Author

DenisCarriere commented Apr 19, 2017

There's QGIS that you can open a GeoJSON, just loaded your Polygon and it works just fine.

image

@stebogit
Copy link
Collaborator

@DenisCarriere you are right, no need for MultiPolygons (although I wonder what's their use then...): a Polygon can contain multiple LinearRings coordinates, but as said the outermost ring MUST (according to the specs) be in the first position of the coords array.
Here I modified that way the geojson passed to Google Maps and in fact it works fine also in there 🎉
Google evidently is just more strict with geojson specs than others; however I believe it's the right way in order to avoid possible rendering issues.

My current approach (in @turf/isobands) to properly group the rings is first to order them by area; then, starting from the larger not grouped one, I collect all the rings (not yet grouped) inside that ring (using @turf/inside on each point of the ring in exam).
It is probably not the most efficient way, but it has been proved effective.

Any thought?

cc: @rowanwins

@DenisCarriere
Copy link
Member Author

a Polygon can contain multiple LinearRings coordinates, but as said the outermost ring MUST be in the first position of the coords array.

👍 Agreed, I'll add that in my next commit, a solution that won't be so computational heavy would be to retrieve all the bbox from each linestring ring (use GeoJSON bbox property if exists). Then using only bbox as polygons we can quickly calculate the largest extent and define it as the outer ring (first position of array).

Just recently, I've improved @turf/inside with inBBox which quickly returns false if point feature is not inside bbox, which was inspired by rbush. We can easily modify this function to reflect inBBox(bbox, bbox) => true/false.

Example:

Red Lines: LineString input
Blue Polys: Polygon generated from bbox of input LineString

image

CC: @stebogit @rowanwins

@stebogit
Copy link
Collaborator

stebogit commented Apr 21, 2017

@DenisCarriere that is a brilliant idea! 👍
However, what about this example (sorry, just to play devil's advocate):

screen shot 2017-04-21 at 8 38 20 am

cc: @rowanwins

@stebogit
Copy link
Collaborator

stebogit commented Apr 21, 2017

@DenisCarriere what if you simply "remove" the larger polygon (that you identify calculating the area or larger bbox) from the smaller one, you should be returned null, if the larger one includes the smaller, right? If you instead are left with any polygon you know you have only a partial overlapping/inclusion.
I don't know though how @turf/difference would respond to this kind of clipping.

cc: @rowanwins

@DenisCarriere
Copy link
Member Author

DenisCarriere commented Apr 21, 2017

@stebogit Well that would mean the user is trying to make an invalid geometry (still somewhat works...).

The largest linestring would be the first polygon and the smaller polygon would be the 2nd in the Array.

The result would look something like this.

https://gist.github.com/a7375bcb2b46c9eb35dde17346dfdc49

image

You would then need to use @turf/unkink-polygon to untangle that polygon (currently not very stable as a solution).

The goal of this library isn't to perform any clipping, difference, inside, it simply needs to repackage arrays of linestrings into polygons, it assumes the user is entering a valid input. Currently the benchmark results are between 2M-6M ops/sec. Adding any difference will drop it down to 20K ops/sec, which would decrease the performance by 100x-300x.

- Add Test fixture with outer ring in middle position (must be placed in first position)
- Update Typescript definition
- Update benchmark results (2-3x peformance loss, not significant enough to complain)
CC: @stebogit
@DenisCarriere
Copy link
Member Author

@stebogit Added a new param orderCoords=true to place the most outer ring in the first position.

No significant performance loss (2x-3x slower and changes only apply to MultiPolygons).

Added a new test Fixture for this test case multi-linestring-outer-ring-middle-position.geojson:

  • Before (Largest LineString is in the 2nd position)
  • After (Outer ring is now moved to the first position)

@DenisCarriere
Copy link
Member Author

Going to merge this PR now, @stebogit don't hesitate to send more issues/concern afterwards.

@DenisCarriere DenisCarriere merged commit 4ad8852 into master Apr 21, 2017
@DenisCarriere DenisCarriere deleted the linestring-to-polygon branch April 21, 2017 23:47
@stebogit
Copy link
Collaborator

@DenisCarriere I added a failing test, which fails because currently the method considers all the LinearRings as a single Polygon, with the larger LinearRing as the outermost ring of the whole set (see the rendering issue).
Here instead there are actually four Polygons, which have to be separated and collected into a MultiPolygon to be sure the rendering (and thus the compliance with GeoJSON standards) is consistently correct.

This the correct output:
screen shot 2017-04-22 at 1 14 46 am

The method should be able to detect which rings are part of a Polygon (collection of LinearRings) and collect the Polyogns in a MultiPolygon (collection of Polygons).
From the specs:

For Polygons with more than one of these rings, the first MUST be
the exterior ring, and any others MUST be interior rings. The
exterior ring bounds the surface, and the interior rings (if
present) bound holes within the surface.

and

For type "MultiPolygon", the "coordinates" member is an array of
Polygon coordinate arrays.

It might become computationally expensive, but this is definitely a complex problem! 😕

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants