diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbc790f..d4330ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ In particular, when submitting a pull request: - All existing tests should pass. Please make sure that the test suite passes, both locally and on - [Travis CI](https://travis-ci.org/kjordahl/geopandas). Status on + [Travis CI](https://travis-ci.org/geopandas/geopandas). Status on Travis will be visible on a pull request. If you want to enable Travis CI on your own fork, please read the pandas guidelines link above or the diff --git a/README.md b/README.md index 5f006c1..6c2bc82 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -GeoPandas [![build status](https://secure.travis-ci.org/kjordahl/geopandas.png?branch=master)](https://travis-ci.org/kjordahl/geopandas) [![Coverage Status](https://coveralls.io/repos/kjordahl/geopandas/badge.png)](https://coveralls.io/r/kjordahl/geopandas) +GeoPandas [![build status](https://secure.travis-ci.org/geopandas/geopandas.png?branch=master)](https://travis-ci.org/geopandas/geopandas) [![Coverage Status](https://coveralls.io/repos/geopandas/geopandas/badge.png)](https://coveralls.io/r/geopandas/geopandas) ========= Python tools for geographic data diff --git a/doc/source/conf.py b/doc/source/conf.py index d1524ee..653eb9d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -41,7 +41,7 @@ master_doc = 'index' # General information about the project. project = u'GeoPandas' -copyright = u'2013, GeoPandas developers' +copyright = u'2013-2014, GeoPandas developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -52,7 +52,7 @@ try: version = release = d['version'] except: # FIXME: This shouldn't be hardwired, but should be set one place only - version = release = '0.1.0.dev' + version = release = '0.2.0.dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/source/install.rst b/doc/source/install.rst index 7b62299..9b3ddcc 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -1,14 +1,23 @@ Installation ============ -GeoPandas is continuous-release software. You may install the latest -source from `GitHub`_ and use the setup script:: +The released version of GeoPandas is 0.1. To install the released +version, use ``pip install geopandas``. +You may install the latest development version by cloning the +`GitHub`_ repository and using the setup script:: + + git clone https://github.com/geopandas/geopandas.git + cd geopandas python setup.py install -GeoPandas is also available on `PyPI`_, so ``pip install geopandas`` -should work as well. You will have to add the ``--pre`` flag -for pip 1.4 and later. +It is also possible to install the latest development version +available on PyPI with `pip` by adding the ``--pre`` flag for pip 1.4 +and later, or to use `pip` to install directly from the GitHub +repository with:: + + pip install git+git://github.com/geopandas/geopandas.git + Dependencies ------------ @@ -20,7 +29,7 @@ Supports Python versions 2.6, 2.7, and 3.2+. - `shapely`_ - `fiona`_ - `six`_ -- `geopy`_ 0.96.3 (optional; for geocoding) +- `geopy`_ 0.99 (optional; for geocoding) - `psycopg2`_ (optional; for PostGIS connection) For plotting, these additional packages may be used: @@ -42,7 +51,7 @@ Tests are automatically run on all commits on the GitHub repository, including pull requests, on `Travis CI`_. .. _PyPI: https://pypi.python.org/pypi/geopandas -.. _GitHub: https://github.com/kjordahl/geopandas +.. _GitHub: https://github.com/geopandas/geopandas .. _numpy: http://www.numpy.org .. _pandas: http://pandas.pydata.org .. _shapely: http://toblerity.github.io/shapely @@ -53,7 +62,7 @@ including pull requests, on `Travis CI`_. .. _six: https://pythonhosted.org/six .. _psycopg2: https://pypi.python.org/pypi/psycopg2 .. _pysal: http://pysal.org -.. _Travis CI: https://travis-ci.org/kjordahl/geopandas +.. _Travis CI: https://travis-ci.org/geopandas/geopandas .. toctree:: :maxdepth: 2 diff --git a/doc/source/user.rst b/doc/source/user.rst index e5c27e2..05c8e3c 100644 --- a/doc/source/user.rst +++ b/doc/source/user.rst @@ -254,6 +254,12 @@ Additionally, the following methods are implemented: See ``GeoSeries.bounds`` for the bounds of the geometries contained in the series. +.. attribute:: GeoSeries.__geo_interface__ + + Implements the `geo_interface`_. Returns a python data structure + to represent the ``GeoSeries`` as a GeoJSON-like ``FeatureCollection``. + Note that the features will have an empty ``properties`` dict as they don't + have associated attributes (geometry only). Methods of pandas ``Series`` objects are also available, although not all are applicable to geometric objects and some may return a @@ -312,6 +318,11 @@ Currently, the following methods are implemented for a ``GeoDataFrame``: that column, otherwise calls ``GeoSeries.plot()`` on the ``geometry`` column. Wraps the ``plot_dataframe()`` function. +.. attribute:: GeoDataFrame.__geo_interface__ + + Implements the `geo_interface`_. Returns a python data structure + to represent the ``GeoDataFrame`` as a GeoJSON-like ``FeatureCollection``. + All pandas ``DataFrame`` methods are also available, although they may not operate in a meaningful way on the ``geometry`` column and may not return a ``GeoDataFrame`` result even when it would be appropriate to @@ -480,6 +491,7 @@ borough that are in the holes: .. _matplotlib: http://matplotlib.org .. _fiona: http://toblerity.github.io/fiona .. _geopy: https://github.com/geopy/geopy +.. _geo_interface: https://gist.github.com/sgillies/2217756 .. _file containing the boroughs of New York City: http://www.nyc.gov/html/dcp/download/bytes/nybb_14aav.zip .. toctree:: diff --git a/geopandas/__init__.py b/geopandas/__init__.py index 3f2a276..2780f17 100644 --- a/geopandas/__init__.py +++ b/geopandas/__init__.py @@ -1,7 +1,7 @@ try: from geopandas.version import version as __version__ except ImportError: - __version__ = '0.1.0.dev-unknown' + __version__ = '0.2.0.dev-unknown' from geopandas.geoseries import GeoSeries from geopandas.geodataframe import GeoDataFrame diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index a3809ec..8976aab 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -202,7 +202,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame): def to_json(self, na='null', **kwargs): - """Returns a GeoJSON representation of the GeoDataFrame. + """Returns a GeoJSON string representation of the GeoDataFrame. Parameters ---------- @@ -216,6 +216,33 @@ class GeoDataFrame(GeoPandasBase, DataFrame): The remaining *kwargs* are passed to json.dumps(). """ + return json.dumps(self._to_geo(na), **kwargs) + + @property + def __geo_interface__(self): + """Returns a python feature collection (i.e. the geointerface) + representation of the GeoDataFrame. + + This differs from `_to_geo()` only in that it is a property + with a default `na` arg instead of a method + """ + return self._to_geo(na='null') + + def _to_geo(self, na='null'): + """Returns a python feature collection (i.e. the geointerface) + representation of the GeoDataFrame. + + Parameters + ---------- + na : {'null', 'drop', 'keep'}, default 'null' + Indicates how to output missing (NaN) values in the GeoDataFrame + * null: ouput the missing entries as JSON null + * drop: remove the property from the feature. This applies to + each feature individually so that features may have + different properties + * keep: output the missing entries as NaN + """ + def fill_none(row): """ Takes in a Series, converts to a dictionary with null values @@ -246,10 +273,8 @@ class GeoDataFrame(GeoPandasBase, DataFrame): dict((k, v) for k, v in iteritems(row) if k != self._geometry_column_name), 'geometry': mapping(row[self._geometry_column_name]) } - return json.dumps( - {'type': 'FeatureCollection', - 'features': [feature(i, row) for i, row in self.iterrows()]}, - **kwargs ) + return {'type': 'FeatureCollection', + 'features': [feature(i, row) for i, row in self.iterrows()]} def to_file(self, filename, driver="ESRI Shapefile", **kwargs): """ diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py index 56d2ae0..6bc586b 100644 --- a/geopandas/geoseries.py +++ b/geopandas/geoseries.py @@ -106,6 +106,13 @@ class GeoSeries(GeoPandasBase, Series): g.crs = crs return g + @property + def __geo_interface__(self): + """Returns a GeoSeries as a python feature collection + """ + from geopandas import GeoDataFrame + return GeoDataFrame({'geometry': self}).__geo_interface__ + def to_file(self, filename, driver="ESRI Shapefile", **kwargs): from geopandas import GeoDataFrame data = GeoDataFrame({"geometry": self, diff --git a/geopandas/io/sql.py b/geopandas/io/sql.py index 1bf5d16..b88f412 100644 --- a/geopandas/io/sql.py +++ b/geopandas/io/sql.py @@ -3,9 +3,9 @@ import binascii from pandas import read_sql import shapely.wkb - from geopandas import GeoSeries, GeoDataFrame + def read_postgis(sql, con, geom_col='geom', crs=None, index_col=None, coerce_float=True, params=None): """ @@ -19,7 +19,7 @@ def read_postgis(sql, con, geom_col='geom', crs=None, index_col=None, Parameters ---------- sql: string - con: DB connection object + con: DB connection object or SQLAlchemy engine geom_col: string, default 'geom' column name to convert to shapely geometries crs: optional @@ -30,8 +30,9 @@ def read_postgis(sql, con, geom_col='geom', crs=None, index_col=None, index_col, coerce_float, params """ - df = read_sql(sql, con, index_col=index_col, coerce_float=coerce_float, + df = read_sql(sql, con, index_col=index_col, coerce_float=coerce_float, params=params) + if geom_col not in df: raise ValueError("Query missing geometry column '{0}'".format( geom_col)) diff --git a/requirements.test.txt b/requirements.test.txt index b61fea4..69b4255 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -1,4 +1,5 @@ psycopg2>=2.5.1 +SQLAlchemy>=0.8.3 geopy==0.99 matplotlib>=1.2.1 descartes>=1.0 diff --git a/tests/test_geodataframe.py b/tests/test_geodataframe.py index db2b9f8..7a4536d 100644 --- a/tests/test_geodataframe.py +++ b/tests/test_geodataframe.py @@ -367,8 +367,6 @@ class TestDataFrame(unittest.TestCase): {'a': 2, 'b': np.nan}]) assert_frame_equal(expected, result) - @unittest.skipIf(PANDAS_NEW_SQL_API, 'Development version of pandas ' - 'not yet supported in SQL API.') def test_from_postgis_default(self): con = connect('test_geopandas') if con is None or not create_db(self.df): @@ -378,12 +376,13 @@ class TestDataFrame(unittest.TestCase): sql = "SELECT * FROM nybb;" df = GeoDataFrame.from_postgis(sql, con) finally: + if PANDAS_NEW_SQL_API: + # It's not really a connection, it's an engine + con = con.connect() con.close() validate_boro_df(self, df) - @unittest.skipIf(PANDAS_NEW_SQL_API, 'Development version of pandas ' - 'not yet supported in SQL API.') def test_from_postgis_custom_geom_col(self): con = connect('test_geopandas') if con is None or not create_db(self.df): @@ -396,6 +395,9 @@ class TestDataFrame(unittest.TestCase): FROM nybb;""" df = GeoDataFrame.from_postgis(sql, con, geom_col='__geometry__') finally: + if PANDAS_NEW_SQL_API: + # It's not really a connection, it's an engine + con = con.connect() con.close() validate_boro_df(self, df) @@ -425,3 +427,9 @@ class TestDataFrame(unittest.TestCase): with self.assertRaises(ValueError): df.set_geometry('location', inplace=True) + + def test_geodataframe_geointerface(self): + self.assertEqual(self.df.__geo_interface__['type'], 'FeatureCollection') + self.assertEqual(len(self.df.__geo_interface__['features']), + self.df.shape[0]) + diff --git a/tests/test_geoseries.py b/tests/test_geoseries.py index 22ed080..91c11f5 100644 --- a/tests/test_geoseries.py +++ b/tests/test_geoseries.py @@ -146,5 +146,10 @@ class TestSeries(unittest.TestCase): self.assertTrue(geom_equals(self.g3[[True, False]], self.g3.cx[0.9:, :0.1])) self.assertTrue(geom_equals(self.g3[[False, True]], self.g3.cx[0:0.1, 0.9:1.0])) + def test_geoseries_geointerface(self): + self.assertEqual(self.g1.__geo_interface__['type'], 'FeatureCollection') + self.assertEqual(len(self.g1.__geo_interface__['features']), + self.g1.shape[0]) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_io.py b/tests/test_io.py index 5b3eb8e..ec04bd7 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -16,8 +16,6 @@ class TestIO(unittest.TestCase): with fiona.open(path, vfs=vfs) as f: self.crs = f.crs - @unittest.skipIf(PANDAS_NEW_SQL_API, 'Development version of pandas ' - 'not yet supported in SQL API.') def test_read_postgis_default(self): con = tests.util.connect('test_geopandas') if con is None or not tests.util.create_db(self.df): @@ -27,12 +25,13 @@ class TestIO(unittest.TestCase): sql = "SELECT * FROM nybb;" df = read_postgis(sql, con) finally: + if PANDAS_NEW_SQL_API: + # It's not really a connection, it's an engine + con = con.connect() con.close() tests.util.validate_boro_df(self, df) - @unittest.skipIf(PANDAS_NEW_SQL_API, 'Development version of pandas ' - 'not yet supported in SQL API.') def test_read_postgis_custom_geom_col(self): con = tests.util.connect('test_geopandas') if con is None or not tests.util.create_db(self.df): @@ -45,6 +44,9 @@ class TestIO(unittest.TestCase): FROM nybb;""" df = read_postgis(sql, con, geom_col='__geometry__') finally: + if PANDAS_NEW_SQL_API: + # It's not really a connection, it's an engine + con = con.connect() con.close() tests.util.validate_boro_df(self, df)