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

Add support for Polygons with holes #3092

Merged
merged 37 commits into from
Oct 23, 2018
Merged

Add support for Polygons with holes #3092

merged 37 commits into from
Oct 23, 2018

Conversation

philippjfr
Copy link
Member

@philippjfr philippjfr commented Oct 19, 2018

In conjunction with the corresponding geoviews PR this represents a full protocol for round-tripping data between the dictionary format in holoviews and shapely geometries. Therefore this formally defines the conventions of our format relative to GEOS geometry definitions.

  • A MultiInterface can hold a list of standard columnar data structures, the columnar data structures may be, numpy arrays, dictionaries of columns, (pandas/dask) dataframes and dictionaries of a geometry and other data.
  • Each item in a MultiInterface list represents an individual geometry, where a geometry can be a LinearString, LinearRing, Polygon, MultiLineString or MultiPolygon.
  • If an item in the list represents a MultiLineString or MultiPolygon then each polygon/line string should be separated by nans.
  • In order to store holes for MultiPolygons the holes are deeply nested to unambiguously assign each hole to each polygon:
    • 1st. The first level of nesting corresponds to the list of geometries
    • 2nd. The second level corresponds to each Polygon in a MultiPolygon
    • 3rd. The third level of nesting allows for multiple holes per Polygon

To demonstrate this let's take a few examples of polygon geometries with holes. In the simplest case we have a Polygon of a few coordinates with holes defined as a list of list of arrays:

coords = [(1, 2), (2, 0), (3, 7)]
holes = [[[(1.5, 2), (2, 3), (1.6, 1.6)], [(2.1, 4.5), (2.5, 5), (2.3, 3.5)]]]
hv.Polygons([{('x', 'y'): coords, 'holes': holes}])

bokeh_plot

In the case of a MultiPolygon which contains the Polygon from above it becomes clearer. The coords now contain a nan separator and there are two separate lists of holes, one the same as above the second empty:

coords = [(1, 2), (2, 0), (3, 7), (np.nan, np.nan), (6, 7), (7, 5), (3, 2)]
holes = [[[(1.5, 2), (2, 3), (1.6, 1.6)], [(2.1, 4.5), (2.5, 5), (2.3, 3.5)]], []]
hv.Polygons([{('x', 'y'): coords, 'holes': holes}])

bokeh_plot

To further illustrate this, we can split the MultiPolygon into two separate Polygons like this:

coords = [(1, 2), (2, 0), (3, 7)]
holes = [[[(1.5, 2), (2, 3), (1.6, 1.6)], [(2.1, 4.5), (2.5, 5), (2.3, 3.5)]]]
coords2 = [(6, 7), (7, 5), (3, 2)]
hv.Polygons([{('x', 'y'): coords, 'holes': holes, 'value': 1},
             {('x', 'y'): coords2, 'value': 2}], vdims='value')

bokeh_plot

producing almost the same plot but allowing us to set two distinct values for each polygon, when in the MultiPolygon case they would have had to share the same value.

This scheme can also be used to faithfully round-trip matplotlib geometries (e.g. Patches) to this format, which means we can finally handle the contours operation correctly.

@jsignell
Copy link
Member

Are you losing the mapping from polygon (section of multipolygon) to hole in that transformation?

@jsignell
Copy link
Member

nevermind I misread. I think that seems like a reasonable representation. It might be worth looking at geopandas though and trying to make that work well

@philippjfr
Copy link
Member Author

nevermind I misread. I think that seems like a reasonable representation. It might be worth looking at geopandas though and trying to make that work well

Geopandas support will follow in geoviews, but there we don't have to worry about the representation since geopandas already represents holes.

@jsignell
Copy link
Member

there we don't have to worry about the representation since geopandas already represents holes.

Oh I see, so we won't transform it to this style?

@philippjfr
Copy link
Member Author

Oh I see, so we won't transform it to this style?

No, it should generally stay in its native format, i.e. inside shapely objects.

@philippjfr
Copy link
Member Author

Slight correction now that I've started on the geoviews representation, in geoviews paths and polygons are converted to lists of dictionaries containing shapely geometries, which simplifies projections massively.

for i, sd in enumerate(d):
unpacked.append((sd, vals[:, i]))

unpacked = []
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I simply unindented this section when I thought I'd have to complicate it further, there are no changes to DictInterface.init that need reviewing.

unique = set(values)
else:
unique = np.unique(values)
if (~util.isfinite(unique)).all():
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A column of NaNs was not being detected as being scalar.

]
}
],
"metadata": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to clear out this metadata before this PR can be merged...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

"\n",
"The ``Path`` element represents a collection of path geometries with associated values. Each path geometry may be split into sub-geometries on NaN-values and may be associated with scalar values or array values varying along its length. In analogy to GEOS geometry types a Path is a collection of LineString and MultiLineString geometries with associated values.\n",
"\n",
"While many different formats are accepted in theory, natively HoloViews provides the ``MultiInterface`` which allows representing paths as lists of regular columnar data objects including arrays, dataframes and dictionaries of column arrays and scalars. A simple path geometry may therefore be drawn using:"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are interfaces discussed elsewhere in the user guide? If not, I wouldn't mention MultiInterface explicitly, if so, I might want to point to where interfaces are discussed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay removing this.

"cell_type": "markdown",
"metadata": {},
"source": [
"If a polygon has no holes at all the 'holes' key may be ommitted entirely:"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth doing this first (no holes) before introducing holes? I.e a reminded of the Polygon constructor before holes were supported...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think saying that the format is exactly the same as the paths one is enough, otherwise it gets a bit repetitive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

" {'x': [4, 6, 6], 'y': [0, 2, 1], 'value': 1}\n",
"], vdims='value')\n",
"\n",
"polys = poly.split()\n",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think it is worth introducing the polys handle here (just inline it to the Layout). Looks to me like the handle that is reused is just poly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

@@ -185,3 +186,33 @@ def get_raster_array(image):
else:
data = np.flipud(data)
return data

def ring_coding(array):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this called ring because it is closed (or at least looks closed)? If so the docstring should mention this, if not, I'm not sure what 'ring' is referring to. If I remember right, matplotlib has explicit codes for declaring genuinely closed paths that I assume you've considered using...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It generates the path codes for each of the exterior and interior paths (which are always rings in a polygon). Using CLOSEPOLY path codes is not necessary since PathPatch will automatically close any patches.

key = Polygons._hole_key
if key in dataset.data:
return [[[np.asarray(h) for h in hs] for hs in dataset.data[key]]]
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't you use the super definition of holes if holes aren't defined/supported?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless the default implementation works for all interfaces except this one specifically?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can do that.

@jlstevens
Copy link
Contributor

I've made a few comments but overall this PR looks good. This feature has been a long time coming!

@philippjfr philippjfr added this to the v1.11.0 milestone Oct 22, 2018
@jlstevens
Copy link
Contributor

The tests have now passed (finally!). All looks good to me.

@jlstevens jlstevens merged commit cfb4b62 into master Oct 23, 2018
@philippjfr philippjfr deleted the poly_holes branch November 12, 2018 18:00
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.

None yet

3 participants