Skip to content

Commit

Permalink
Updates to conform to Geometry interface API (#407)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jan 4, 2020
1 parent 296283a commit 374b095
Show file tree
Hide file tree
Showing 6 changed files with 847 additions and 68 deletions.
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ os:
env:
global:
- PYENV_VERSION=3.7
- PYTHON_VERSION=3.6
- PKG_TEST_PYTHON="--test-python=py36 --test-python=py27"
- CHANS_DEV="-c pyviz/label/dev -c bokeh/label/dev"
- CHANS_REL="-c pyviz -c bokeh"
- PYTHON_VERSION=3.7
- PKG_TEST_PYTHON="--test-python=py37 --test-python=py27"
- CHANS_DEV="-c pyviz/label/dev"
- CHANS_REL="-c pyviz"
- LABELS_DEV="--label dev"
- LABELS_REL="--label dev --label main"
- MPLBACKEND="Agg"
Expand Down
195 changes: 186 additions & 9 deletions geoviews/data/geom_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import numpy as np
from holoviews.core.data import Interface, DictInterface, MultiInterface
from holoviews.core.data.interface import DataError
from holoviews.core.data.spatialpandas import to_geom_dict
from holoviews.core.dimension import OrderedDict as cyODict, dimension_name
from holoviews.core.util import isscalar

Expand Down Expand Up @@ -32,6 +34,24 @@ def init(cls, eltype, data, kdims, vdims):
dimensions = [dimension_name(d) for d in kdims + vdims]
if isinstance(data, geom_types):
data = {'geometry': data}
elif not isinstance(data, dict) or 'geometry' not in data:
xdim, ydim = kdims[:2]
from shapely.geometry import (
Point, LineString, Polygon, MultiPoint, MultiPolygon,
MultiLineString, LinearRing
)
data = to_geom_dict(eltype, data, kdims, vdims, GeomDictInterface)
geom = data.get('geom_type') or MultiInterface.geom_type(eltype)
poly = 'holes' in data or geom == 'Polygon'
if poly:
single_type, multi_type = Polygon, MultiPolygon
elif geom == 'Line':
single_type, multi_type = LineString, MultiLineString
elif geom == 'Ring':
single_type, multi_type = LinearRing, MultiPolygon
else:
single_type, multi_type = Point, MultiPoint
data['geometry'] = geom_from_dict(data, xdim.name, ydim.name, single_type, multi_type)

if not cls.applies(data):
raise ValueError("GeomDictInterface only handles dictionary types "
Expand Down Expand Up @@ -71,15 +91,43 @@ def init(cls, eltype, data, kdims, vdims):

@classmethod
def validate(cls, dataset, validate_vdims):
assert len([d for d in dataset.kdims + dataset.vdims
if d.name not in dataset.data]) == 2
from shapely.geometry.base import BaseGeometry
geom_dims = cls.geom_dims(dataset)
if len(geom_dims) != 2:
raise DataError('Expected %s instance to declare two key '
'dimensions corresponding to the geometry '
'coordinates but %d dimensions were found '
'which did not refer to any columns.'
% (type(dataset).__name__, len(geom_dims)), cls)
elif 'geometry' not in dataset.data:
raise DataError("Could not find a 'geometry' column in the data.")
elif not isinstance(dataset.data['geometry'], BaseGeometry):
raise DataError("The 'geometry' column should be a shapely"
"geometry type, found %s type instead." %
type(dataset.data['geometry']).__name__)

@classmethod
def dtype(cls, dataset, dimension):
name = dataset.get_dimension(dimension, strict=True).name
if name not in dataset.data:
return np.dtype('float') # Geometry dimension
return super(GeomDictInterface, cls).dtype(dataset, dimension)
def shape(cls, dataset):
return (cls.length(dataset), len(dataset.dimensions()))

@classmethod
def geom_type(cls, dataset):
from shapely.geometry import (
Polygon, MultiPolygon, LineString, MultiLineString, LinearRing
)
geom = dataset.data['geometry']
if isinstance(geom, (Polygon, MultiPolygon)):
return 'Polygon'
elif isinstance(geom, LinearRing):
return 'Ring'
elif isinstance(geom, (LineString, MultiLineString)):
return 'Line'
else:
return 'Point'

@classmethod
def geo_column(cls, dataset):
return 'geometry'

@classmethod
def has_holes(cls, dataset):
Expand Down Expand Up @@ -111,6 +159,13 @@ def dimension_type(cls, dataset, dim):
values = dataset.data[name]
return type(values) if isscalar(values) else values.dtype.type

@classmethod
def dtype(cls, dataset, dimension):
name = dataset.get_dimension(dimension, strict=True).name
if name in cls.geom_dims(dataset):
return np.dtype('float')
return Interface.dtype(dataset, dimension)

@classmethod
def range(cls, dataset, dim):
dim = dataset.get_dimension(dim)
Expand Down Expand Up @@ -150,11 +205,82 @@ def values(cls, dataset, dim, expanded=True, flat=True, compute=True, keep_index

@classmethod
def select(cls, dataset, selection_mask=None, **selection):
raise NotImplementedError('select operation not implemented on geometries')
if cls.geom_dims(dataset):
data = cls.shape_mask(dataset, selection)
else:
data = dataset.data
if selection_mask is None:
selection_mask = cls.select_mask(dataset, selection)
empty = not selection_mask.sum()
dimensions = dataset.dimensions()
if empty:
return {d.name: np.array([], dtype=cls.dtype(dataset, d))
for d in dimensions}
indexed = cls.indexed(dataset, selection)
new_data = {}
for k, v in data.items():
if k not in dimensions or isscalar(v):
new_data[k] = v
else:
new_data[k] = v[selection_mask]
if indexed and len(list(new_data.values())[0]) == 1 and len(dataset.vdims) == 1:
value = new_data[dataset.vdims[0].name]
return value if isscalar(value) else value[0]
return new_data

@classmethod
def shape_mask(cls, dataset, selection):
xdim, ydim = cls.geom_dims(dataset)
xsel = selection.pop(xdim.name, None)
ysel = selection.pop(ydim.name, None)
if xsel is None and ysel is None:
return dataset.data

from shapely.geometry import box

if xsel is None:
x0, x1 = cls.range(dataset, xdim)
elif isinstance(xsel, slice):
x0, x1 = xsel.start, xsel.stop
elif isinstance(xsel, tuple):
x0, x1 = xsel
else:
raise ValueError("Only slicing is supported on geometries, %s "
"selection is of type %s."
% (xdim, type(xsel).__name__))

if ysel is None:
y0, y1 = cls.range(dataset, ydim)
elif isinstance(ysel, slice):
y0, y1 = ysel.start, ysel.stop
elif isinstance(ysel, tuple):
y0, y1 = ysel
else:
raise ValueError("Only slicing is supported on geometries, %s "
"selection is of type %s."
% (ydim, type(ysel).__name__))

bounds = box(x0, y0, x1, y1)
geom = dataset.data['geometry']
geom = geom.intersection(bounds)
new_data = dict(dataset.data, geometry=geom)
return new_data

@classmethod
def iloc(cls, dataset, index):
raise NotImplementedError('iloc operation not implemented for geometries.')
from shapely.geometry import MultiPoint
rows, cols = index

data = dict(dataset.data)
geom = data['geometry']

if isinstance(geom, MultiPoint):
if isscalar(rows) or isinstance(rows, slice):
geom = geom[rows]
elif isinstance(rows, (set, list)):
geom = MultiPoint([geom[r] for r in rows])
data['geometry'] = geom
return data

@classmethod
def sample(cls, dataset, samples=[]):
Expand All @@ -169,5 +295,56 @@ def concat(cls, datasets, dimensions, vdims):
raise NotImplementedError('concat operation not implemented for geometries.')


def geom_from_dict(geom, xdim, ydim, single_type, multi_type):
from shapely.geometry import (
Point, LineString, Polygon, MultiPoint, MultiPolygon, MultiLineString
)
if (xdim, ydim) in geom:
xs, ys = np.asarray(geom.pop((xdim, ydim))).T
elif xdim in geom and ydim in geom:
xs, ys = geom.pop(xdim), geom.pop(ydim)
else:
raise ValueError('Could not find geometry dimensions')

xscalar, yscalar = isscalar(xs), isscalar(ys)
if xscalar and yscalar:
xs, ys = np.array([xs]), np.array([ys])
elif xscalar:
xs = np.full_like(ys, xs)
elif yscalar:
ys = np.full_like(xs, ys)
geom_array = np.column_stack([xs, ys])
splits = np.where(np.isnan(geom_array[:, :2].astype('float')).sum(axis=1))[0]
if len(splits):
split_geoms = [g[:-1] if i == (len(splits)-1) else g
for i, g in enumerate(np.split(geom_array, splits+1))]
else:
split_geoms = [geom_array]
split_holes = geom.pop('holes', None)
if split_holes is not None and len(split_holes) != len(split_geoms):
raise DataError('Polygons with holes containing multi-geometries '
'must declare a list of holes for each geometry.')

if single_type is Point:
if len(splits) > 1 or any(len(g) > 1 for g in split_geoms):
geom = MultiPoint(np.concatenate(split_geoms))
else:
geom = Point(*split_geoms[0])
elif len(splits):
if multi_type is MultiPolygon:
if split_holes is None:
split_holes = [[]]*len(split_geoms)
geom = MultiPolygon(list(zip(split_geoms, split_holes)))
else:
geom = MultiLineString(split_geoms)
elif single_type is Polygon:
if split_holes is None or not len(split_holes):
split_holes = [None]
geom = Polygon(split_geoms[0], split_holes[0])
else:
geom = LineString(split_geoms[0])
return geom


MultiInterface.subtypes.insert(0, 'geom_dictionary')
Interface.register(GeomDictInterface)
Loading

0 comments on commit 374b095

Please sign in to comment.