ENH: add constructors / converters for shapely, WKT and WKB (#995)

This commit is contained in:
Joris Van den Bossche authored and GitHub committed 2019-05-17 11:20:56 +02:00
1 parent 4db3b1e0c8
commit 01f96f6dae
5 files changed
+327 -65

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
from geopandas.geoseries import GeoSeries
from geopandas.geodataframe import GeoDataFrame
from geopandas.geodataframe import points_from_xy
from geopandas.array import _points_from_xy as points_from_xy
from geopandas.io.file import read_file
from geopandas.io.sql import read_postgis
+157 -3
View File
@@ -3,11 +3,161 @@ import warnings
import numpy as np
from shapely.geometry.base import BaseGeometry
from shapely.ops import unary_union
import shapely.geometry
import shapely.ops
import shapely.affinity
# -----------------------------------------------------------------------------
# Constructors / converters to other formats
# -----------------------------------------------------------------------------
def from_shapely(data):
"""
Convert a list or array of shapely objects to a GeometryArray.
Validates the elements.
"""
n = len(data)
out = []
for idx in range(n):
geom = data[idx]
if isinstance(geom, BaseGeometry):
out.append(geom)
elif hasattr(geom, '__geo_interface__'):
geom = shapely.geometry.asShape(geom)
out.append(geom)
elif geom is None or (isinstance(geom, float) and np.isnan(geom)):
out.append(None)
out = np.array(out, dtype=object)
return GeometryArray(out)
def to_shapely(geoms):
"""
Convert GeometryArray to numpy object array of shapely objects.
"""
if not isinstance(geoms, GeometryArray):
raise ValueError("'geoms' must be a GeometryArray")
return geoms.data
def from_wkb(data):
"""
Convert a list or array of WKB objects to a GeometryArray.
"""
import shapely.wkb
n = len(data)
out = []
for idx in range(n):
geom = data[idx]
if geom is not None and len(geom):
geom = shapely.wkb.loads(geom)
else:
geom = None
out.append(geom)
out = np.array(out, dtype=object)
return GeometryArray(out)
def to_wkb(geoms):
"""
Convert GeometryArray to a numpy object array of WKB objects.
"""
if not isinstance(geoms, GeometryArray):
raise ValueError("'geoms' must be a GeometryArray")
out = [geom.wkb if geom is not None else None for geom in geoms]
return np.array(out, dtype=object)
def from_wkt(data):
"""
Convert a list or array of WKT objects to a GeometryArray.
"""
import shapely.wkt
n = len(data)
out = []
for idx in range(n):
geom = data[idx]
if geom is not None and len(geom):
if isinstance(geom, bytes):
geom = geom.decode('utf-8')
geom = shapely.wkt.loads(geom)
else:
geom = None
out.append(geom)
out = np.array(out, dtype=object)
return GeometryArray(out)
def to_wkt(geoms):
"""
Convert GeometryArray to a numpy object array of WKT objects.
"""
if not isinstance(geoms, GeometryArray):
raise ValueError("'geoms' must be a GeometryArray")
out = [geom.wkt if geom is not None else None for geom in geoms]
return np.array(out, dtype=object)
def _points_from_xy(x, y, z=None):
"""
Generate list of shapely Point geometries from x, y(, z) coordinates.
Parameters
----------
x, y, z : array
Examples
--------
>>> geometry = geopandas.points_from_xy(x=[1, 0], y=[0, 1])
>>> geometry = geopandas.points_from_xy(df['x'], df['y'], df['z'])
>>> gdf = geopandas.GeoDataFrame(
df, geometry=geopandas.points_from_xy(df['x'], df['y']))
Returns
-------
list : list
"""
if not len(x) == len(y):
raise ValueError("x and y arrays must be equal length.")
if z is not None:
if not len(z) == len(x):
raise ValueError("z array must be same length as x and y.")
geom = [shapely.geometry.Point(i, j, k) for i, j, k in zip(x, y, z)]
else:
geom = [shapely.geometry.Point(i, j) for i, j in zip(x, y)]
return geom
def points_from_xy(x, y, z=None):
"""Convert arrays of x and y values to a GeometryArray of points."""
x = np.asarray(x, dtype='float64')
y = np.asarray(y, dtype='float64')
if z is not None:
z = np.asarray(z, dtype='float64')
out = _points_from_xy(x, y, z)
out = np.array(out, dtype=object)
return GeometryArray(out)
# -----------------------------------------------------------------------------
# Helper methods for the vectorized operations
# -----------------------------------------------------------------------------
def _binary_geo(op, left, right):
# type: (str, GeometryArray, [GeometryArray/BaseGeometry]) -> GeometryArray
""" Apply geometry-valued operation
@@ -127,6 +277,10 @@ class GeometryArray:
def __len__(self):
return len(self.data)
def __getitem__(self, i):
assert isinstance(i, int)
return self.data[i]
# -------------------------------------------------------------------------
# Geometry related methods
# -------------------------------------------------------------------------
@@ -324,7 +478,7 @@ class GeometryArray:
#
def unary_union(self):
return unary_union(self.data)
return shapely.ops.unary_union(self.data)
#
# Affinity operations
-30
View File
@@ -636,36 +636,6 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
return geo_df
def points_from_xy(x, y, z=None):
"""
Generate list of shapely Point geometries from x, y(, z) coordinates.
Parameters
----------
x, y, z : array
Examples
--------
>>> geometry = geopandas.points_from_xy(x=[1, 0], y=[0, 1])
>>> geometry = geopandas.points_from_xy(df['x'], df['y'], df['z'])
>>> gdf = geopandas.GeoDataFrame(
df, geometry=geopandas.points_from_xy(df['x'], df['y']))
Returns
-------
list : list
"""
if not len(x) == len(y):
raise ValueError("x and y arrays must be equal length.")
if z is not None:
if not len(z) == len(x):
raise ValueError("z array must be same length as x and y.")
geom = [Point(i, j, k) for i, j, k in zip(x, y, z)]
else:
geom = [Point(i, j) for i, j in zip(x, y)]
return geom
def _dataframe_set_geometry(self, col, drop=False, inplace=False, crs=None):
if inplace:
raise ValueError("Can't do inplace setting when converting from"
+169
View File
@@ -0,0 +1,169 @@
import random
import numpy as np
import pandas as pd
import shapely
import shapely.geometry
import shapely.wkb
import geopandas
from geopandas.array import (
GeometryArray, points_from_xy, from_shapely, from_wkb, from_wkt, to_wkb,
to_wkt)
import pytest
import six
triangles = [shapely.geometry.Polygon([(random.random(), random.random())
for i in range(3)])
for _ in range(10)]
T = from_shapely(triangles)
points = [shapely.geometry.Point(random.random(), random.random())
for _ in range(20)]
P = from_shapely(points)
point = points[0]
def test_points():
x = np.arange(10).astype(np.float)
y = np.arange(10).astype(np.float) ** 2
points = points_from_xy(x, y)
assert isinstance(points, GeometryArray)
for i in range(10):
assert isinstance(points[i], shapely.geometry.Point)
assert points[i].x == x[i]
assert points[i].y == y[i]
def test_points_from_xy():
# testing the top-level interface
# using DataFrame column
df = pd.DataFrame([{'x': x, 'y': x, 'z': x} for x in range(10)])
gs = [shapely.geometry.Point(x, x) for x in range(10)]
gsz = [shapely.geometry.Point(x, x, x) for x in range(10)]
geometry1 = geopandas.points_from_xy(df['x'], df['y'])
geometry2 = geopandas.points_from_xy(df['x'], df['y'], df['z'])
assert geometry1 == gs
assert geometry2 == gsz
# using Series or numpy arrays or lists
for s in [pd.Series(range(10)), np.arange(10), list(range(10))]:
geometry1 = geopandas.points_from_xy(s, s)
geometry2 = geopandas.points_from_xy(s, s, s)
assert geometry1 == gs
assert geometry2 == gsz
# using different lengths should throw error
arr_10 = np.arange(10)
arr_20 = np.arange(20)
with pytest.raises(ValueError):
geopandas.points_from_xy(x=arr_10, y=arr_20)
geopandas.points_from_xy(x=arr_10, y=arr_10, z=arr_20)
# Using incomplete arguments should throw error
with pytest.raises(TypeError):
geopandas.points_from_xy(x=s)
geopandas.points_from_xy(y=s)
geopandas.points_from_xy(z=s)
def test_from_shapely():
assert isinstance(T, GeometryArray)
assert [v.equals(t) for v, t in zip(T, triangles)]
def test_from_shapely_geo_interface():
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def __geo_interface__(self):
return {'type': 'Point', 'coordinates': (self.x, self.y)}
result = from_shapely([Point(1.0, 2.0), Point(3.0, 4.0)])
expected = from_shapely([
shapely.geometry.Point(1.0, 2.0), shapely.geometry.Point(3.0, 4.0)])
assert all(v.equals(t) for v, t in zip(result, expected))
def test_from_wkb():
# list
L_wkb = [p.wkb for p in points]
res = from_wkb(L_wkb)
assert isinstance(res, GeometryArray)
assert all(v.equals(t) for v, t in zip(res, points))
# array
res = from_wkb(np.array(L_wkb, dtype=object))
assert isinstance(res, GeometryArray)
assert all(v.equals(t) for v, t in zip(res, points))
# missing values
L_wkb.extend([b'', None])
res = from_wkb(L_wkb)
assert res[-1] is None
assert res[-2] is None
def test_to_wkb():
res = to_wkb(P)
exp = np.array([p.wkb for p in points], dtype=object)
assert isinstance(res, np.ndarray)
np.testing.assert_array_equal(res, exp)
# missing values
a = from_shapely([None, points[0]])
res = to_wkb(a)
assert res[0] is None
@pytest.mark.parametrize('string_type', ['str', 'bytes'])
def test_from_wkt(string_type):
if string_type == 'str':
f = six.text_type
else:
if six.PY3:
def f(x): return bytes(x, 'utf8')
else:
def f(x): return x
# list
L_wkt = [f(p.wkt) for p in points]
res = from_wkt(L_wkt)
assert isinstance(res, GeometryArray)
assert all(v.almost_equals(t) for v, t in zip(res, points))
# array
res = from_wkt(np.array(L_wkt, dtype=object))
assert isinstance(res, GeometryArray)
assert all(v.almost_equals(t) for v, t in zip(res, points))
# missing values
L_wkt.extend([f(''), None])
res = from_wkt(L_wkt)
assert res[-1] is None
assert res[-2] is None
def test_to_wkt():
res = to_wkt(P)
exp = np.array([p.wkt for p in points], dtype=object)
assert isinstance(res, np.ndarray)
np.testing.assert_array_equal(res, exp)
# missing values
a = from_shapely([None, points[0]])
res = to_wkt(a)
assert res[0] is None
-31
View File
@@ -13,7 +13,6 @@ import fiona
import geopandas
from geopandas import GeoDataFrame, read_file, GeoSeries
from geopandas.geodataframe import points_from_xy
import pytest
from pandas.util.testing import (
@@ -540,36 +539,6 @@ class TestDataFrame:
assert_frame_equal(self.df, unpickled)
assert self.df.crs == unpickled.crs
def test_points_from_xy(self):
# using GeoDataFrame column
df = GeoDataFrame([{'x': x, 'y': x, 'z': x} for x in range(10)])
gs = [Point(x, x) for x in range(10)]
gsz = [Point(x, x, x) for x in range(10)]
geometry1 = points_from_xy(df['x'], df['y'])
geometry2 = points_from_xy(df['x'], df['y'], df['z'])
assert geometry1 == gs
assert geometry2 == gsz
# using GeoSeries or numpy arrays or lists
for s in [GeoSeries(range(10)), np.arange(10), list(range(10))]:
geometry1 = points_from_xy(s, s)
geometry2 = points_from_xy(s, s, s)
assert geometry1 == gs
assert geometry2 == gsz
# using different lengths should throw error
arr_10 = np.arange(10)
arr_20 = np.arange(20)
with pytest.raises(ValueError):
points_from_xy(x=arr_10, y=arr_20)
points_from_xy(x=arr_10, y=arr_10, z=arr_20)
# Using incomplete arguments should throw error
with pytest.raises(TypeError):
points_from_xy(x=s)
points_from_xy(y=s)
points_from_xy(z=s)
def check_geodataframe(df, geometry_column='geometry'):
assert isinstance(df, GeoDataFrame)