diff --git a/geopandas/__init__.py b/geopandas/__init__.py index 52b48b6..ba1f050 100644 --- a/geopandas/__init__.py +++ b/geopandas/__init__.py @@ -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 diff --git a/geopandas/array.py b/geopandas/array.py index 07d9ef2..8088d93 100644 --- a/geopandas/array.py +++ b/geopandas/array.py @@ -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 diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index f7d7e53..25b1a43 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -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" diff --git a/geopandas/tests/test_array.py b/geopandas/tests/test_array.py new file mode 100644 index 0000000..083256d --- /dev/null +++ b/geopandas/tests/test_array.py @@ -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 diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py index 36de93b..83ca0ff 100644 --- a/geopandas/tests/test_geodataframe.py +++ b/geopandas/tests/test_geodataframe.py @@ -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)