[python-geopandas] 01/12: Imported Upstream version 0.2

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Fri Jun 10 22:54:33 UTC 2016


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-geopandas.

commit b536ca7e0601aa9f5b9803f418a89b7f74cbbaae
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Fri Jun 10 20:59:35 2016 +0200

    Imported Upstream version 0.2
---
 .gitattributes                                     |    1 +
 .gitignore                                         |    2 +
 .requirements-2.6.txt                              |    2 -
 .travis.yml                                        |   96 +-
 CHANGELOG                                          |   37 +
 LICENSE.txt                                        |    2 +-
 MANIFEST.in                                        |    2 +
 README.md                                          |   40 +-
 constraints.txt                                    |    1 +
 doc/environment.yml                                |   18 +
 doc/source/_static/overlay_operations.png          |  Bin 0 -> 19251 bytes
 doc/source/about.rst                               |    6 +-
 doc/source/aggregation_with_dissolve.rst           |   51 +
 doc/source/conf.py                                 |   22 +-
 doc/source/contributing.rst                        |  281 ++++
 doc/source/data_structures.rst                     |  135 ++
 doc/source/geocoding.rst                           |   17 +
 doc/source/geometric_manipulations.rst             |  222 +++
 doc/source/index.rst                               |   15 +-
 doc/source/install.rst                             |   64 +-
 doc/source/io.rst                                  |   28 +
 doc/source/mapping.rst                             |  120 ++
 doc/source/mergingdata.rst                         |   77 +
 doc/source/projections.rst                         |   71 +
 doc/source/{user.rst => reference.rst}             |  220 +--
 doc/source/set_operations.rst                      |  195 +++
 examples/null_geom.geojson                         |    9 +
 examples/nyc_boros.py                              |    2 +-
 examples/overlays.ipynb                            |  601 +++++++
 examples/spatial_joins.ipynb                       |  929 ++++++++++
 geopandas/__init__.py                              |   13 +-
 geopandas/_version.py                              |  484 ++++++
 geopandas/base.py                                  |  183 +-
 geopandas/datasets/__init__.py                     |   27 +
 .../naturalearth_cities.README.html                |  336 ++++
 .../naturalearth_cities.VERSION.txt                |    1 +
 .../naturalearth_cities/naturalearth_cities.cpg    |    1 +
 .../naturalearth_cities/naturalearth_cities.dbf    |  Bin 0 -> 16427 bytes
 .../naturalearth_cities/naturalearth_cities.prj    |    1 +
 .../naturalearth_cities/naturalearth_cities.shp    |  Bin 0 -> 5756 bytes
 .../naturalearth_cities/naturalearth_cities.shx    |  Bin 0 -> 1716 bytes
 .../naturalearth_lowres/naturalearth_lowres.cpg    |    1 +
 .../naturalearth_lowres/naturalearth_lowres.dbf    |  Bin 0 -> 51346 bytes
 .../naturalearth_lowres/naturalearth_lowres.prj    |    1 +
 .../naturalearth_lowres/naturalearth_lowres.shp    |  Bin 0 -> 179828 bytes
 .../naturalearth_lowres/naturalearth_lowres.shx    |  Bin 0 -> 1516 bytes
 geopandas/geodataframe.py                          |  262 ++-
 geopandas/geoseries.py                             |   65 +-
 geopandas/io/file.py                               |   92 +-
 {tests => geopandas/io/tests}/__init__.py          |    0
 {tests => geopandas/io/tests}/test_io.py           |   27 +-
 geopandas/plotting.py                              |  225 ++-
 geopandas/sindex.py                                |   26 +
 {tests => geopandas/tests}/__init__.py             |    0
 .../baseline_images/test_plotting/lines_plot.png   |  Bin
 .../baseline_images/test_plotting/points_plot.png  |  Bin
 .../baseline_images/test_plotting/poly_plot.png    |  Bin
 .../test_plotting/poly_plot_with_kwargs.png        |  Bin 0 -> 30619 bytes
 geopandas/tests/test_dissolve.py                   |   83 +
 geopandas/tests/test_geocode.py                    |  143 ++
 {tests => geopandas/tests}/test_geodataframe.py    |  127 +-
 {tests => geopandas/tests}/test_geom_methods.py    |   59 +-
 {tests => geopandas/tests}/test_geoseries.py       |   45 +-
 geopandas/tests/test_merge.py                      |   62 +
 geopandas/tests/test_overlay.py                    |  114 ++
 geopandas/tests/test_plotting.py                   |  311 ++++
 geopandas/tests/test_sindex.py                     |  117 ++
 {tests => geopandas/tests}/test_types.py           |    6 +-
 {tests => geopandas/tests}/util.py                 |   41 +-
 geopandas/tools/__init__.py                        |   14 +
 geopandas/{geocode.py => tools/geocoding.py}       |   78 +-
 geopandas/tools/overlay.py                         |  183 ++
 geopandas/tools/sjoin.py                           |  125 ++
 {tests => geopandas/tools/tests}/__init__.py       |    0
 geopandas/tools/tests/test_sjoin.py                |   88 +
 geopandas/tools/tests/test_tools.py                |   51 +
 geopandas/tools/util.py                            |   52 +
 readthedocs.yml                                    |    7 +
 requirements.test.txt                              |    4 +-
 requirements.txt                                   |    3 +
 setup.cfg                                          |   10 +
 setup.py                                           |   73 +-
 tests/test_geocode.py                              |   91 -
 tests/test_plotting.py                             |   78 -
 versioneer.py                                      | 1774 ++++++++++++++++++++
 85 files changed, 7956 insertions(+), 794 deletions(-)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8340bd6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+geopandas/_version.py export-subst
diff --git a/.gitignore b/.gitignore
index 3d5a618..a8d7cd8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ doc/_build
 geopandas.egg-info
 geopandas/version.py
 *.py~
+doc/_static/world_*
+examples/nybb_*.zip
\ No newline at end of file
diff --git a/.requirements-2.6.txt b/.requirements-2.6.txt
deleted file mode 100644
index 062b6c5..0000000
--- a/.requirements-2.6.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-unittest2
-ordereddict
diff --git a/.travis.yml b/.travis.yml
index 6fd2689..3980a09 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,42 +1,84 @@
 language: python
 
-python:
-  - 2.6
-  - 2.7
-  - 3.2
-  - 3.3
-  - 3.4
+sudo: false
 
-env:
-  - PANDAS_VERSION=v0.13.1
-  - PANDAS_VERSION=v0.14.0
-  - PANDAS_VERSION=master
+cache:
+  directories:
+    - ~/.cache/pip
+
+addons:
+  apt:
+    packages:
+    - libgdal1h
+    - gdal-bin
+    - libgdal-dev
+    - libspatialindex-dev
 
+env:
+  global:
+    - PIP_WHEEL_DIR=$HOME/.cache/pip/wheels
+    - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels
+  
+# matrix creates 3+5x2 = 13 tests
 matrix:
-  exclude:
+  include:
+    # Only one test for these Python versions
+    # Pandas >= 0.18.0, is not Python 2.6 compatible
     - python: 2.6
-      env: PANDAS_VERSION=v0.14.0
+      env: PANDAS=0.16.2 MATPLOTLIB=1.2.1
+    - python: 3.3
+      env: PANDAS=0.17.1 MATPLOTLIB=1.3.1
+    - python: 3.4
+      env: PANDAS=0.18.0 MATPLOTLIB=1.5.1
+
+    # Python 2.7 and 3.5 test all supported Pandas versions
+    - python: 2.7
+      env: PANDAS=0.15.2  MATPLOTLIB=1.2.1
+    - python: 2.7
+      env: PANDAS=0.16.2  MATPLOTLIB=1.3.1
+    - python: 2.7
+      env: PANDAS=0.17.1  MATPLOTLIB=1.4.3
+    - python: 2.7
+      env: PANDAS=0.18.0  MATPLOTLIB=1.5.1
+    - python: 2.7
+      env: PANDAS=master  MATPLOTLIB=master
+
+    # Note: Python 3.5 and matplotlib versions before 1.4.3 support is hit or miss.
+    - python: 3.5
+      env: PANDAS=0.15.2  MATPLOTLIB=1.4.3
+    - python: 3.5
+      env: PANDAS=0.16.2  MATPLOTLIB=1.4.3
+    - python: 3.5
+      env: PANDAS=0.17.1  MATPLOTLIB=1.4.3
+    - python: 3.5
+      env: PANDAS=0.18.0  MATPLOTLIB=1.5.1
+    - python: 3.5
+      env: PANDAS=master  MATPLOTLIB=master
+
+  # matplotlib 2.x (development version) causes a few tests to fail
+  allow_failures:
+    - env: PANDAS=master  MATPLOTLIB=master
+  
 
 before_install:
-  - sudo add-apt-repository -y ppa:ubuntugis/ppa
-  - sudo apt-get update
-  - sudo apt-get install gdal-bin libgdal-dev
-#  - sudo -u postgres psql -c "drop database if exists test_geopandas"
-#  - sudo -u postgres psql -c "create database test_geopandas"
-#  - sudo -u postgres psql -c "create extension postgis" -d test_geopandas
+  - pip install -U pip
+  - pip install wheel
 
 install:
-  - pip install -r requirements.txt --use-mirrors
-  - pip install -r requirements.test.txt --use-mirrors
-  - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r .requirements-2.6.txt --use-mirrors; fi
-  - git clone git://github.com/pydata/pandas.git
-  - cd pandas
-  - git checkout $PANDAS_VERSION
-  - python setup.py install
-  - cd ..
+  - pip wheel numpy
+  - pip wheel -r requirements.txt
+  - pip wheel -r requirements.test.txt
+
+  - pip install numpy
+  - if [[ $MATPLOTLIB == 'master' ]]; then pip install git+https://github.com/matplotlib/matplotlib.git; else pip wheel matplotlib==$MATPLOTLIB; pip install matplotlib==$MATPLOTLIB; fi
+
+  - pip install -r requirements.txt
+  - pip install -r requirements.test.txt
+
+  - if [[ $PANDAS == 'master' ]]; then pip install git+https://github.com/pydata/pandas.git; else pip wheel pandas==$PANDAS; pip install pandas==$PANDAS; fi
 
 script:
-  - py.test tests --cov geopandas -v --cov-report term-missing
+  - py.test geopandas --cov geopandas -v --cov-report term-missing
 
 after_success:
   - coveralls
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..58d7c8a
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,37 @@
+Version 0.2.0
+-------------
+
+Improvements:
+
+* Complete overhaul of the documentation
+* Addition of ``overlay`` to perform spatial overlays with polygons (#142)
+* Addition of ``sjoin`` to perform spatial joins (#115, #145, #188)
+* Addition of ``__geo_interface__`` that returns a python data structure
+  to represent the ``GeoSeries`` as a GeoJSON-like ``FeatureCollection`` (#116)
+  and ``iterfeatures`` method (#178)
+* Addition of the ``explode`` (#146) and ``dissolve`` (#310, #311) methods.
+* Addition of the ``sindex`` attribute, a Spatial Index using the optional
+  dependency ``rtree`` (``libspatialindex``) that can be used to speed up
+  certain operations such as overlays (#140, #141).
+* Addition of the ``GeoSeries.ix`` coordinate indexer to slice a GeoSeries based
+  on a bounding box of the coordinates (#55).
+* Improvements to plotting: ability to specify edge colors (#173), support for
+  the ``vmin``, ``vmax``, ``figsize``, ``linewidth`` keywords (#207), legends
+  for chloropleth plots (#210), color points by specifying a colormap (#186) or
+  a single color (#238).
+* Larger flexibility of ``to_crs``, accepting both dicts and proj strings (#289)
+* Addition of embedded example data, accessible through
+  ``geopandas.datasets.get_path``.
+
+API changes:
+
+* In the ``plot`` method, the ``axes`` keyword is renamed to ``ax`` for
+  consistency with pandas, and the ``colormap`` keyword is renamed to ``cmap``
+  for consistency with matplotlib (#208, #228, #240).
+
+Bug fixes:
+
+* Properly handle rows with missing geometries (#139, #193).
+* Fix ``GeoSeries.to_json`` (#263).
+* Correctly serialize metadata when pickling (#199, #206).
+* Fix ``merge`` and ``concat`` to return correct GeoDataFrame (#247, #320, #322).
diff --git a/LICENSE.txt b/LICENSE.txt
index 15d5aed..7eb8134 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2013, GeoPandas developers.
+Copyright (c) 2013-2016, GeoPandas developers.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..d7934e2
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include versioneer.py
+include geopandas/_version.py
diff --git a/README.md b/README.md
index 6c2bc82..a1f04fb 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,33 @@ transformed to new coordinate systems with the `to_crs()` method.
 There is currently no enforcement of like coordinates for operations,
 but that may change in the future.
 
+Documentation is available at [geopandas.org](http://geopandas.org)
+(current release) and
+[Read the Docs](http://geopandas.readthedocs.io/en/master/)
+(release and development versions).
+
+Install
+--------
+
+**Requirements**
+
+For the installation of GeoPandas, the following packages are required:
+
+- ``pandas``
+- ``shapely``
+- ``fiona``
+- ``descartes``
+- ``pyproj``
+
+Further, [``rtree``](https://github.com/Toblerity/rtree) is an optional
+dependency. ``rtree`` requires the C library [``libspatialindex``](https://github.com/libspatialindex/libspatialindex). If using brew, you can install using ``brew install Spatialindex``.
+
+
+**Install**
+
+Then, installation works as normal: ``pip install geopandas``
+
+
 Examples
 --------
 
@@ -58,7 +85,7 @@ GeoPandas objects also know how to plot themselves.  GeoPandas uses [descartes](
 
     >>> g.plot()
 
-GeoPandas also implements alternate constructors that can read any data format recognized by [fiona](http://toblerity.github.io/fiona).  To read a [file containing the boroughs of New York City](http://www.nyc.gov/html/dcp/download/bytes/nybb_14aav.zip):
+GeoPandas also implements alternate constructors that can read any data format recognized by [fiona](http://toblerity.github.io/fiona).  To read a [file containing the boroughs of New York City](http://www1.nyc.gov/assets/planning/download/zip/data-maps/open-data/nybb_16a.zip):
 
     >>> boros = GeoDataFrame.from_file('nybb.shp')
     >>> boros.set_index('BoroCode', inplace=True)
@@ -71,7 +98,7 @@ GeoPandas also implements alternate constructors that can read any data format r
     3              Brooklyn  1.959432e+09  726568.946340
     4                Queens  3.049947e+09  861038.479299
     5         Staten Island  1.623853e+09  330385.036974
-    
+
                                                        geometry
     BoroCode
     1         (POLYGON ((981219.0557861328125000 188655.3157...
@@ -81,7 +108,7 @@ GeoPandas also implements alternate constructors that can read any data format r
     5         (POLYGON ((970217.0223999023437500 145643.3322...
 
 ![New York City boroughs](examples/nyc.png)
- 
+
     >>> boros['geometry'].convex_hull
     0    POLYGON ((915517.6877458114176989 120121.88125...
     1    POLYGON ((1000721.5317993164062500 136681.7761...
@@ -91,10 +118,3 @@ GeoPandas also implements alternate constructors that can read any data format r
     dtype: object
 
 ![Convex hulls of New York City boroughs](examples/nyc_hull.png)
-
-TODO
-----
-
-- Finish implementing and testing pandas methods on GeoPandas objects
-- The current GeoDataFrame does not do very much.
-- spatial joins, grouping and more...
diff --git a/constraints.txt b/constraints.txt
new file mode 100644
index 0000000..cf3d761
--- /dev/null
+++ b/constraints.txt
@@ -0,0 +1 @@
+rtree>=0.8
diff --git a/doc/environment.yml b/doc/environment.yml
new file mode 100644
index 0000000..0daf4ef
--- /dev/null
+++ b/doc/environment.yml
@@ -0,0 +1,18 @@
+name: geopandas_docs
+channels:
+- conda-forge
+dependencies:
+- python=3.4
+- pandas
+- shapely
+- fiona
+- pyproj
+- rtree
+- six
+- geopy
+- matplotlib
+- descartes
+- pysal
+- sphinx
+- sphinx_rtd_theme
+- ipython=4.0.1
diff --git a/doc/source/_static/overlay_operations.png b/doc/source/_static/overlay_operations.png
new file mode 100644
index 0000000..38beb8f
Binary files /dev/null and b/doc/source/_static/overlay_operations.png differ
diff --git a/doc/source/about.rst b/doc/source/about.rst
index 1efef84..e5ca2d9 100644
--- a/doc/source/about.rst
+++ b/doc/source/about.rst
@@ -4,10 +4,8 @@ About
 Known issues
 ------------
 
-- The ``geopy`` API has changed significantly over recent versions.
-  ``geopy 0.99`` is currently supported (though it is known to fail
-  with Python 3.2, it should work with other supported python
-  versions).
+- The ``geopy`` API has changed significantly over recent versions,
+  ``geopy 1.10.0`` is currently supported.
 
 .. toctree::
    :maxdepth: 2
diff --git a/doc/source/aggregation_with_dissolve.rst b/doc/source/aggregation_with_dissolve.rst
new file mode 100644
index 0000000..a191fb5
--- /dev/null
+++ b/doc/source/aggregation_with_dissolve.rst
@@ -0,0 +1,51 @@
+.. ipython:: python
+   :suppress:
+
+   import geopandas as gpd
+
+
+Aggregation with dissolve
+=============================
+
+It is often the case that we find ourselves working with spatial data that is more granular than we need. For example, we might have data on sub-national units, but we're actually interested in studying patterns at the level of countries.
+
+In a non-spatial setting, we aggregate our data using the ``groupby`` function. But when working with spatial data, we need a special tool that can also aggregate geometric features. In the *geopandas* library, that functionality is provided by the ``dissolve`` function.
+
+``dissolve`` can be thought of as doing three things: (a) it dissolves all the geometries within a given group together into a single geometric feature (using the ``unary_union`` method), and (b) it aggregates all the rows of data in a group using ``groupby.aggregate()``, and (c) it combines those two results.
+
+``dissolve`` Example
+~~~~~~~~~~~~~~~~~~~~~
+
+Suppose we are interested in studying continents, but we only have country-level data like the country dataset included in *geopandas*. We can easily convert this to a continent-level dataset.
+
+
+First, let's look at the most simple case where we just want continent shapes and names. By default, ``dissolve`` will pass ``'first'`` to ``groupby.aggregate``.
+
+.. ipython:: python
+
+    world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+    world = world[['continent', 'geometry']]
+    continents = world.dissolve(by='continent')
+
+    @savefig continents.png width=5in
+    continents.plot();
+
+    continents.head()
+
+If we are interested in aggregate populations, however, we can pass different functions to the ``dissolve`` method to aggregate populations:
+
+.. ipython:: python
+
+   world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+   world = world[['continent', 'geometry', 'pop_est']]
+   continents = world.dissolve(by='continent', aggfunc='sum')
+
+   @savefig continents.png width=5in
+   continents.plot(column = 'pop_est', scheme='quantiles', cmap='YlOrRd');
+
+   continents.head()
+
+
+
+.. toctree::
+   :maxdepth: 2
diff --git a/doc/source/conf.py b/doc/source/conf.py
index da2f27c..d563ab3 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -25,9 +25,11 @@ import sys, os
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = []
+extensions = ['IPython.sphinxext.ipython_console_highlighting',
+              'IPython.sphinxext.ipython_directive']
 
 # Add any paths that contain templates here, relative to this directory.
+
 templates_path = ['_templates']
 
 # The suffix of source filenames.
@@ -41,18 +43,13 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'GeoPandas'
-copyright = u'2013-2014, GeoPandas developers'
+copyright = u'2013-2016, 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
 # built documents.
-d = {}
-try:
-    execfile(os.path.join('..', '..', 'geopandas', 'version.py'), d)
-    version = release = d['version']
-except:
-    # FIXME: This shouldn't be hardwired, but should be set one place only
-    version = release = '0.1.1'
+import geopandas
+version = release = geopandas.__version__
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -93,10 +90,9 @@ pygments_style = 'sphinx'
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-if os.environ.get('READTHEDOCS', None) == 'True':
-    html_theme = 'default'
-else:
-    html_theme = 'nature'
+import sphinx_rtd_theme
+html_theme = "sphinx_rtd_theme"
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst
new file mode 100644
index 0000000..a384ab2
--- /dev/null
+++ b/doc/source/contributing.rst
@@ -0,0 +1,281 @@
+Contributing to GeoPandas
+=========================
+
+(Contribution guidelines largely copied from `pandas <http://pandas.pydata.org/pandas-docs/stable/contributing.html>`_)
+
+Overview
+--------
+
+Contributions to GeoPandas are very welcome.  They are likely to
+be accepted more quickly if they follow these guidelines.
+
+At this stage of GeoPandas development, the priorities are to define a
+simple, usable, and stable API and to have clean, maintainable,
+readable code.  Performance matters, but not at the expense of those
+goals.
+
+In general, GeoPandas follows the conventions of the pandas project
+where applicable.
+
+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/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
+  `getting started docs <http://about.travis-ci.org/docs/user/getting-started/>`_.
+
+- New functionality should include tests.  Please write reasonable
+  tests for your code and make sure that they pass on your pull request.
+
+- Classes, methods, functions, etc. should have docstrings.  The first
+  line of a docstring should be a standalone summary.  Parameters and
+  return values should be ducumented explicitly.
+
+- GeoPandas supports python 2 (2.6+) and python 3 (3.2+) with a single
+  code base.  Use modern python idioms when possible that are
+  compatibile with both major versions, and use the
+  `six <https://pythonhosted.org/six>`_ library where helpful to smooth
+  over the differences.  Use ``from __future__ import`` statements where
+  appropriate.  Test code locally in both python 2 and python 3 when
+  possible (all supported versions will be automatically tested on
+  Travis CI).
+
+- Follow PEP 8 when possible.
+
+- Imports should be grouped with standard library imports first,
+  3rd-party libraries next, and geopandas imports third.  Within each
+  grouping, imports should be alphabetized.  Always use absolute
+  imports when possible, and explicit relative imports for local
+  imports when necessary in tests.
+
+
+Seven Steps for Contributing
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are seven basic steps to contributing to *geopandas*:
+
+1) Fork the *geopandas* git repository
+2) Create a development environment
+3) Install *geopandas* dependencies
+4) Make a ``development`` build of *geopandas*
+5) Make changes to code and add tests
+6) Update the documentation
+7) Submit a Pull Request
+
+Each of these 7 steps is detailed below.
+
+
+1) Forking the *geopandas* repository using Git
+------------------------------------------------
+
+To the new user, working with Git is one of the more daunting aspects of contributing to *geopandas**.
+It can very quickly become overwhelming, but sticking to the guidelines below will help keep the process
+straightforward and mostly trouble free.  As always, if you are having difficulties please
+feel free to ask for help.
+
+The code is hosted on `GitHub <https://github.com/geopandas/geopandas>`_. To
+contribute you will need to sign up for a `free GitHub account
+<https://github.com/signup/free>`_. We use `Git <http://git-scm.com/>`_ for
+version control to allow many people to work together on the project.
+
+Some great resources for learning Git:
+
+* Software Carpentry's `Git Tutorial <http://swcarpentry.github.io/git-novice/>`_
+* `Atlassian <https://www.atlassian.com/git/tutorials/what-is-version-control>`_
+* the `GitHub help pages <http://help.github.com/>`_.
+* Matthew Brett's `Pydagogue <http://matthew-brett.github.com/pydagogue/>`_.
+
+Getting started with Git
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+`GitHub has instructions <http://help.github.com/set-up-git-redirect>`__ for installing git,
+setting up your SSH key, and configuring git.  All these steps need to be completed before
+you can work seamlessly between your local repository and GitHub.
+
+.. _contributing.forking:
+
+Forking
+~~~~~~~~
+
+You will need your own fork to work on the code. Go to the `geopandas project
+page <https://github.com/geopandas/geopandas>`_ and hit the ``Fork`` button. You will
+want to clone your fork to your machine::
+
+    git clone git at github.com:your-user-name/geopandas.git geopandas-yourname
+    cd geopandas-yourname
+    git remote add upstream git://github.com/geopandas/geopandas.git
+
+This creates the directory `geopandas-yourname` and connects your repository to
+the upstream (main project) *geopandas* repository.
+
+The testing suite will run automatically on Travis-CI once your pull request is
+submitted.  However, if you wish to run the test suite on a branch prior to
+submitting the pull request, then Travis-CI needs to be hooked up to your
+GitHub repository.  Instructions for doing so are `here
+<http://about.travis-ci.org/docs/user/getting-started/>`__.
+
+Creating a branch
+~~~~~~~~~~~~~~~~~~
+
+You want your master branch to reflect only production-ready code, so create a
+feature branch for making your changes. For example::
+
+    git branch shiny-new-feature
+    git checkout shiny-new-feature
+
+The above can be simplified to::
+
+    git checkout -b shiny-new-feature
+
+This changes your working directory to the shiny-new-feature branch.  Keep any
+changes in this branch specific to one bug or feature so it is clear
+what the branch brings to *geopandas*. You can have many shiny-new-features
+and switch in between them using the git checkout command.
+
+To update this branch, you need to retrieve the changes from the master branch::
+
+    git fetch upstream
+    git rebase upstream/master
+
+This will replay your commits on top of the latest geopandas git master.  If this
+leads to merge conflicts, you must resolve these before submitting your pull
+request.  If you have uncommitted changes, you will need to ``stash`` them prior
+to updating.  This will effectively store your changes and they can be reapplied
+after updating.
+
+.. _contributing.dev_env:
+
+2) Creating a development environment
+---------------------------------------
+A development environment is a virtual space where you can keep an independent installation of *geopandas*.
+This makes it easy to keep both a stable version of python in one place you use for work, and a development
+version (which you may break while playing with code) in another.
+
+An easy way to create a *geopandas* development environment is as follows:
+
+- Install either `Anaconda <http://docs.continuum.io/anaconda/>`_ or
+  `miniconda <http://conda.pydata.org/miniconda.html>`_
+- Make sure that you have :ref:`cloned the repository <contributing.forking>`
+- ``cd`` to the *geopandas** source directory
+
+Tell conda to create a new environment, named ``geopandas_dev``, or any other name you would like
+for this environment, by running::
+
+      conda create -n geopandas_dev
+
+For a python 3 environment::
+
+      conda create -n geopandas_dev python=3.4
+
+This will create the new environment, and not touch any of your existing environments,
+nor any existing python installation.
+
+To work in this environment, Windows users should ``activate`` it as follows::
+
+      activate geopandas_dev
+
+Mac OSX and Linux users should use::
+
+      source activate geopandas_dev
+
+You will then see a confirmation message to indicate you are in the new development environment.
+
+To view your environments::
+
+      conda info -e
+
+To return to you home root environment::
+
+      deactivate
+
+See the full conda docs `here <http://conda.pydata.org/docs>`__.
+
+At this point you can easily do a *development* install, as detailed in the next sections.
+
+3) Installing Dependencies
+--------------------------
+
+To run *geopandas* in an development environment, you must first install
+*geopandas*'s dependencies. We suggest doing so using the following commands
+(executed after your development environment has been activated)::
+
+    conda install -c conda-forge fiona shapely pyproj rtree
+    conda install pandas
+
+
+This should install all necessary dependencies.
+
+4) Making a development build
+-----------------------------
+
+Once dependencies are in place, make an in-place build by navigating to the git
+clone of the *geopandas* repository and running::
+
+    python setup.py develop
+
+
+5) Making changes and writing tests
+-------------------------------------
+
+*geopandas* is serious about testing and strongly encourages contributors to embrace
+`test-driven development (TDD) <http://en.wikipedia.org/wiki/Test-driven_development>`_.
+This development process "relies on the repetition of a very short development cycle:
+first the developer writes an (initially failing) automated test case that defines a desired
+improvement or new function, then produces the minimum amount of code to pass that test."
+So, before actually writing any code, you should write your tests.  Often the test can be
+taken from the original GitHub issue.  However, it is always worth considering additional
+use cases and writing corresponding tests.
+
+Adding tests is one of the most common requests after code is pushed to *geopandas*.  Therefore,
+it is worth getting in the habit of writing tests ahead of time so this is never an issue.
+
+Like many packages, *geopandas* uses the `Nose testing system
+<http://nose.readthedocs.org/en/latest/index.html>`_ and the convenient
+extensions in `numpy.testing
+<http://docs.scipy.org/doc/numpy/reference/routines.testing.html>`_.
+
+Writing tests
+~~~~~~~~~~~~~
+
+All tests should go into the ``tests`` directory. This folder contains many
+current examples of tests, and we suggest looking to these for inspiration.
+
+The ``.util`` module has some special ``assert`` functions that
+make it easier to make statements about whether GeoSeries or GeoDataFrame
+objects are equivalent. The easiest way to verify that your code is correct is to
+explicitly construct the result you expect, then compare the actual result to
+the expected correct result, using eg the function ``assert_geoseries_equal``.
+
+Running the test suite
+~~~~~~~~~~~~~~~~~~~~~~
+
+The tests can then be run directly inside your Git clone (without having to
+install *geopandas*) by typing::
+
+    nosetests -v
+
+6) Updating the Documentation
+-----------------------------
+
+*geopandas* documentation resides in the `doc` folder. Changes to the docs are
+make by modifying the appropriate file in the `source` folder within `doc`.
+*geopandas* docs us reStructuredText syntax, `which is explained here <http://www.sphinx-doc.org/en/stable/rest.html#rst-primer>`_
+and the docstrings follow the `Numpy Docstring standard <https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt>`_.
+
+Once you have made your changes, you can build the docs by navigating to the `doc` folder and typing::
+
+    make html
+
+The resulting html pages will be located in `doc/build/html`.
+
+
+7) Submitting a Pull Request
+------------------------------
+
+Once you've made changes and pushed them to your forked repository, you then
+submit a pull request to have them integrated into the *geopandas* code base.
+
+You can find a pull request (or PR) tutorial in the `GitHub's Help Docs <https://help.github.com/articles/using-pull-requests/>`_.
diff --git a/doc/source/data_structures.rst b/doc/source/data_structures.rst
new file mode 100644
index 0000000..c80361d
--- /dev/null
+++ b/doc/source/data_structures.rst
@@ -0,0 +1,135 @@
+.. currentmodule:: geopandas
+
+.. ipython:: python
+   :suppress:
+
+   import geopandas as gpd
+
+
+Data Structures
+=========================================
+
+GeoPandas implements two main data structures, a ``GeoSeries`` and a
+``GeoDataFrame``.  These are subclasses of pandas ``Series`` and
+``DataFrame``, respectively.
+
+GeoSeries
+---------
+
+A ``GeoSeries`` is essentially a vector where each entry in the vector
+is a set of shapes corresponding to one observation. An entry may consist
+of only one shape (like a single polygon) or multiple shapes that are
+meant to be thought of as one observation (like the many polygons that
+make up the State of Hawaii or a country like Indonesia).
+
+*geopandas* has three basic classes of geometric objects (which are actually *shapely* objects):
+
+* Points / Multi-Points
+* Lines / Multi-Lines
+* Polygons / Multi-Polygons
+
+Note that all entries in  a ``GeoSeries`` need not be of the same geometric type, although certain export operations will fail if this is not the case.
+
+Overview of Attributes and Methods
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``GeoSeries`` class implements nearly all of the attributes and
+methods of Shapely objects.  When applied to a ``GeoSeries``, they
+will apply elementwise to all geometries in the series.  Binary
+operations can be applied between two ``GeoSeries``, in which case the
+operation is carried out elementwise.  The two series will be aligned
+by matching indices.  Binary operations can also be applied to a
+single geometry, in which case the operation is carried out for each
+element of the series with that geometry.  In either case, a
+``Series`` or a ``GeoSeries`` will be returned, as appropriate.
+
+A short summary of a few attributes and methods for GeoSeries is
+presented here, and a full list can be found in the :doc:`all attributes and methods page <reference>`.
+There is also a family of methods for creating new shapes by expanding
+existing shapes or applying set-theoretic operations like "union" described
+in :doc:`geometric manipulations <geometric_manipulations>`.
+
+Attributes
+^^^^^^^^^^^^^^^
+* ``area``: shape area (units of projection -- see :doc:`projections <projections>`)
+* ``bounds``: tuple of max and min coordinates on each axis for each shape
+* ``total_bounds``: tuple of max and min coordinates on each axis for entire GeoSeries
+* ``geom_type``: type of geometry.
+* ``is_valid``: tests if coordinates make a shape that is reasonable geometric shape (`according to this <http://www.opengeospatial.org/standards/sfa>`_).
+
+Basic Methods
+^^^^^^^^^^^^^^
+
+* ``distance(other)``: returns ``Series`` with minimum distance from each entry to ``other``
+* ``centroid``: returns ``GeoSeries`` of centroids
+* ``representative_point()``:  returns ``GeoSeries`` of points that are guaranteed to be within each geometry. It does **NOT** return centroids.
+* ``to_crs()``: change coordinate reference system. See :doc:`projections <projections>`
+* ``plot()``: plot ``GeoSeries``. See :doc:`mapping <mapping>`.
+
+Relationship Tests
+^^^^^^^^^^^^^^^^^^^
+
+* ``geom_almost_equals(other)``: is shape almost the same as ``other`` (good when floating point precision issues make shapes slightly different)
+* ``contains(other)``: is shape contained within ``other``
+* ``intersects(other)``: does shape intersect ``other``
+
+
+GeoDataFrame
+------------
+
+A ``GeoDataFrame`` is a tabular data structure that contains a ``GeoSeries``.
+
+The most important property of a ``GeoDataFrame`` is that it always has one ``GeoSeries`` column that holds a special status. This ``GeoSeries`` is referred to as the ``GeoDataFrame``'s "geometry". When a spatial method is applied to a ``GeoDataFrame`` (or a spatial attribute like ``area`` is called), this commands will always act on the "geometry" column.
+
+The "geometry" column -- no matter its name -- can be accessed through the ``geometry`` attribute (``gdf.geometry``), and the name of the ``geometry`` column can be found by typing ``gdf.geometry.name``.
+
+A ``GeoDataFrame`` may also contain other columns with geometrical (shapely) objects, but only one column can be the active geometry at a time. To change which column is the active geometry column, use the ``set_geometry`` method.
+
+An example using the ``worlds`` GeoDataFrame:
+
+.. ipython:: python
+
+    world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+
+    world.head()
+    #Plot countries
+    @savefig world_borders.png width=3in
+    world.plot();
+
+Currently, the column named "geometry" with country borders is the active
+geometry column:
+
+.. ipython:: python
+
+    world.geometry.name
+
+We can also rename this column to "borders":
+
+.. ipython:: python
+
+    world = world.rename(columns={'geometry': 'borders'}).set_geometry('borders')
+    world.geometry.name
+
+Now, we create centroids and make it the geometry:
+
+.. ipython:: python
+
+    world['centroid_column'] = world.centroid
+    world = world.set_geometry('centroid_column')
+
+    @savefig world_centroids.png width=3in
+    world.plot();
+
+
+**Note:** A ``GeoDataFrame`` keeps track of the active column by name, so if you rename the active geometry column, you must also reset the geometry::
+
+    gdf = gdf.rename(columns={'old_name': 'new_name'}).set_geometry('new_name')
+
+**Note 2:** Somewhat confusingly, by default when you use the ``read_file`` command, the column containing spatial objects from the file is named "geometry" by default, and will be set as the active geometry column. However, despite using the same term for the name of the column and the name of the special attribute that keeps track of the active column, they are distinct. You can easily shift the active geometry column to a different ``GeoSeries`` with the ``set_geometry`` command. Furt [...]
+
+Attributes and Methods
+~~~~~~~~~~~~~~~~~~~~~~
+
+Any of the attributes calls or methods described for a ``GeoSeries`` will work on a ``GeoDataFrame`` -- effectively, they are just applied to the "geometry" ``GeoSeries``.
+
+However, ``GeoDataFrames`` also have a few extra methods for input and output which are described on the :doc:`Input and Output <io>` page and for geocoding with are described in :doc:`Geocoding <geocoding>`.
diff --git a/doc/source/geocoding.rst b/doc/source/geocoding.rst
new file mode 100644
index 0000000..f6cb026
--- /dev/null
+++ b/doc/source/geocoding.rst
@@ -0,0 +1,17 @@
+
+Geocoding
+==========
+
+[TO BE COMPLETED]
+
+
+.. function:: geopandas.geocode.geocode(strings, provider='googlev3', **kwargs)
+
+  Geocode a list of strings and return a GeoDataFrame containing the
+  resulting points in its ``geometry`` column.  Available
+  ``provider``s include ``googlev3``, ``bing``, ``google``, ``yahoo``,
+  ``mapquest``, and ``openmapquest``.  ``**kwargs`` will be passed as
+  parameters to the appropriate geocoder.
+
+  Requires `geopy`_.  Please consult the Terms of Service for the
+  chosen provider.
diff --git a/doc/source/geometric_manipulations.rst b/doc/source/geometric_manipulations.rst
new file mode 100644
index 0000000..b1b256c
--- /dev/null
+++ b/doc/source/geometric_manipulations.rst
@@ -0,0 +1,222 @@
+Geometric Manipulations
+========================
+
+*geopandas* makes available all the tools for geometric manipulations in the `*shapely* library <http://toblerity.org/shapely/manual.html>`_.
+
+Note that documentation for all set-theoretic tools for creating new shapes using the relationship between two different spatial datasets -- like creating intersections, or differences -- can be found on the :doc:`set operations <set_operations>` page.
+
+Constructive Methods
+~~~~~~~~~~~~~~~~~~~~
+
+.. method:: GeoSeries.buffer(distance, resolution=16)
+
+  Returns a ``GeoSeries`` of geometries representing all points within a given `distance`
+  of each geometric object.
+
+.. attribute:: GeoSeries.boundary
+
+  Returns a ``GeoSeries`` of lower dimensional objects representing
+  each geometries's set-theoretic `boundary`.
+
+.. attribute:: GeoSeries.centroid
+
+  Returns a ``GeoSeries`` of points for each geometric centroid.
+
+.. attribute:: GeoSeries.convex_hull
+
+  Returns a ``GeoSeries`` of geometries representing the smallest
+  convex `Polygon` containing all the points in each object unless the
+  number of points in the object is less than three. For two points,
+  the convex hull collapses to a `LineString`; for 1, a `Point`.
+
+.. attribute:: GeoSeries.envelope
+
+  Returns a ``GeoSeries`` of geometries representing the point or
+  smallest rectangular polygon (with sides parallel to the coordinate
+  axes) that contains each object.
+
+.. method:: GeoSeries.simplify(tolerance, preserve_topology=True)
+
+  Returns a ``GeoSeries`` containing a simplified representation of
+  each object.
+
+.. attribute:: GeoSeries.unary_union
+
+  Return a geometry containing the union of all geometries in the ``GeoSeries``.
+
+
+Affine transformations
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. method:: GeoSeries.rotate(self, angle, origin='center', use_radians=False)
+
+  Rotate the coordinates of the GeoSeries.
+
+.. method:: GeoSeries.scale(self, xfact=1.0, yfact=1.0, zfact=1.0, origin='center')
+
+ Scale the geometries of the GeoSeries along each (x, y, z) dimensio.
+
+.. method:: GeoSeries.skew(self, angle, origin='center', use_radians=False)
+
+  Shear/Skew the geometries of the GeoSeries by angles along x and y dimensions.
+
+.. method:: GeoSeries.translate(self, angle, origin='center', use_radians=False)
+
+  Shift the coordinates of the GeoSeries.
+
+
+
+Examples of Geometric Manipulations
+------------------------------------
+
+.. sourcecode:: python
+
+    >>> p1 = Polygon([(0, 0), (1, 0), (1, 1)])
+    >>> p2 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
+    >>> p3 = Polygon([(2, 0), (3, 0), (3, 1), (2, 1)])
+    >>> g = GeoSeries([p1, p2, p3])
+    >>> g
+    0    POLYGON ((0.0000000000000000 0.000000000000000...
+    1    POLYGON ((0.0000000000000000 0.000000000000000...
+    2    POLYGON ((2.0000000000000000 0.000000000000000...
+    dtype: object
+
+.. image:: _static/test.png
+
+Some geographic operations return normal pandas object.  The ``area`` property of a ``GeoSeries`` will return a ``pandas.Series`` containing the area of each item in the ``GeoSeries``:
+
+.. sourcecode:: python
+
+    >>> print g.area
+    0    0.5
+    1    1.0
+    2    1.0
+    dtype: float64
+
+Other operations return GeoPandas objects:
+
+.. sourcecode:: python
+
+    >>> g.buffer(0.5)
+    Out[15]:
+    0    POLYGON ((-0.3535533905932737 0.35355339059327...
+    1    POLYGON ((-0.5000000000000000 0.00000000000000...
+    2    POLYGON ((1.5000000000000000 0.000000000000000...
+    dtype: object
+
+.. image:: _static/test_buffer.png
+
+GeoPandas objects also know how to plot themselves.  GeoPandas uses `descartes`_ to generate a `matplotlib`_ plot. To generate a plot of our GeoSeries, use:
+
+.. sourcecode:: python
+
+    >>> g.plot()
+
+GeoPandas also implements alternate constructors that can read any data format recognized by `fiona`_.  To read a `file containing the boroughs of New York City`_:
+
+.. sourcecode:: python
+
+    >>> boros = GeoDataFrame.from_file('nybb.shp')
+    >>> boros.set_index('BoroCode', inplace=True)
+    >>> boros.sort()
+    >>> boros
+                   BoroName    Shape_Area     Shape_Leng  \
+    BoroCode
+    1             Manhattan  6.364422e+08  358532.956418
+    2                 Bronx  1.186804e+09  464517.890553
+    3              Brooklyn  1.959432e+09  726568.946340
+    4                Queens  3.049947e+09  861038.479299
+    5         Staten Island  1.623853e+09  330385.036974
+
+                                                       geometry
+    BoroCode
+    1         (POLYGON ((981219.0557861328125000 188655.3157...
+    2         (POLYGON ((1012821.8057861328125000 229228.264...
+    3         (POLYGON ((1021176.4790039062500000 151374.796...
+    4         (POLYGON ((1029606.0765991210937500 156073.814...
+    5         (POLYGON ((970217.0223999023437500 145643.3322...
+
+.. image:: _static/nyc.png
+
+.. sourcecode:: python
+
+    >>> boros['geometry'].convex_hull
+    0    POLYGON ((915517.6877458114176989 120121.88125...
+    1    POLYGON ((1000721.5317993164062500 136681.7761...
+    2    POLYGON ((988872.8212280273437500 146772.03179...
+    3    POLYGON ((977855.4451904296875000 188082.32238...
+    4    POLYGON ((1017949.9776000976562500 225426.8845...
+    dtype: object
+
+.. image:: _static/nyc_hull.png
+
+To demonstrate a more complex operation, we'll generate a
+``GeoSeries`` containing 2000 random points:
+
+.. sourcecode:: python
+
+    >>> from shapely.geometry import Point
+    >>> xmin, xmax, ymin, ymax = 900000, 1080000, 120000, 280000
+    >>> xc = (xmax - xmin) * np.random.random(2000) + xmin
+    >>> yc = (ymax - ymin) * np.random.random(2000) + ymin
+    >>> pts = GeoSeries([Point(x, y) for x, y in zip(xc, yc)])
+
+Now draw a circle with fixed radius around each point:
+
+.. sourcecode:: python
+
+    >>> circles = pts.buffer(2000)
+
+We can collapse these circles into a single shapely MultiPolygon
+geometry with
+
+.. sourcecode:: python
+
+    >>> mp = circles.unary_union
+
+To extract the part of this geometry contained in each borough, we can
+just use:
+
+.. sourcecode:: python
+
+    >>> holes = boros['geometry'].intersection(mp)
+
+.. image:: _static/holes.png
+
+and to get the area outside of the holes:
+
+.. sourcecode:: python
+
+    >>> boros_with_holes = boros['geometry'].difference(mp)
+
+.. image:: _static/boros_with_holes.png
+
+Note that this can be simplified a bit, since ``geometry`` is
+available as an attribute on a ``GeoDataFrame``, and the
+``intersection`` and ``difference`` methods are implemented with the
+"&" and "-" operators, respectively.  For example, the latter could
+have been expressed simply as ``boros.geometry - mp``.
+
+It's easy to do things like calculate the fractional area in each
+borough that are in the holes:
+
+.. sourcecode:: python
+
+    >>> holes.area / boros.geometry.area
+    BoroCode
+    1           0.602015
+    2           0.523457
+    3           0.585901
+    4           0.577020
+    5           0.559507
+    dtype: float64
+
+.. _Descartes: https://pypi.python.org/pypi/descartes
+.. _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::
+   :maxdepth: 2
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 6ea251b..0395b88 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -24,10 +24,20 @@ operations in python that would otherwise require a spatial database
 such as PostGIS.
 
 .. toctree::
-   :maxdepth: 2
+  :maxdepth: 2
 
   Installation <install>
-  User Guide <user>
+  Data Structures <data_structures>
+  Reading and Writing Files <io>
+  Making Maps <mapping>
+  Managing Projections <projections>
+  Geometric Manipulations <geometric_manipulations>
+  Set Operations with overlay <set_operations>
+  Aggregation with dissolve <aggregation_with_dissolve>
+  Merging Data <mergingdata>
+  Geocoding <geocoding>
+  Reference to All Attributes and Methods <reference>
+  Contributing to GeoPandas <contributing>
   About <about>
 
 Indices and tables
@@ -36,4 +46,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/doc/source/install.rst b/doc/source/install.rst
index 9b3ddcc..37d80e7 100644
--- a/doc/source/install.rst
+++ b/doc/source/install.rst
@@ -1,36 +1,46 @@
 Installation
 ============
 
-The released version of GeoPandas is 0.1.  To install the released
-version, use ``pip install geopandas``.
+Installing GeoPandas
+---------------------
+
+To install the released version, you can use pip::
+
+    pip install geopandas
+
+or you can install the conda package from the conda-forge channel::
+
+    conda install -c conda-forge geopandas
 
 You may install the latest development version by cloning the
-`GitHub`_ repository and using the setup script::
+`GitHub` repository and using the setup script::
 
     git clone https://github.com/geopandas/geopandas.git
     cd geopandas
-    python setup.py install
+    pip install .
 
 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::
+directly from the GitHub repository with::
 
     pip install git+git://github.com/geopandas/geopandas.git
 
-
 Dependencies
-------------
+--------------
 
-Supports Python versions 2.6, 2.7, and 3.2+.
+Installation via `conda` should also install all dependencies, but a complete list is as follows:
 
 - `numpy`_
-- `pandas`_ (version 0.13 or later)
+- `pandas`_ (version 0.15.2 or later)
 - `shapely`_
 - `fiona`_
 - `six`_
+- `pyproj`_
+
+Further, optional dependencies are:
+
 - `geopy`_ 0.99 (optional; for geocoding)
 - `psycopg2`_ (optional; for PostGIS connection)
+- `rtree`_ (optional; spatial index to improve performance)
 
 For plotting, these additional packages may be used:
 
@@ -38,32 +48,40 @@ For plotting, these additional packages may be used:
 - `descartes`_
 - `pysal`_
 
-Testing
--------
-
-To run the current set of tests from the source directory, run::
-
-    nosetests -v
+These can be installed independently via the following set of commands::
 
-from a command line.
+    conda install -c conda-forge fiona shapely pyproj rtree
+    conda install pandas
 
-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/geopandas/geopandas
+
 .. _numpy: http://www.numpy.org
+
 .. _pandas: http://pandas.pydata.org
+
 .. _shapely: http://toblerity.github.io/shapely
+
 .. _fiona: http://toblerity.github.io/fiona
+
 .. _Descartes: https://pypi.python.org/pypi/descartes
+
 .. _matplotlib: http://matplotlib.org
+
 .. _geopy: https://github.com/geopy/geopy
+
 .. _six: https://pythonhosted.org/six
+
 .. _psycopg2: https://pypi.python.org/pypi/psycopg2
+
 .. _pysal: http://pysal.org
-.. _Travis CI: https://travis-ci.org/geopandas/geopandas
 
-.. toctree::
-   :maxdepth: 2
+.. _pyproj: https://github.com/jswhit/pyproj
 
+.. _rtree: https://github.com/Toblerity/rtree
+
+.. _libspatialindex: https://github.com/libspatialindex/libspatialindex
+
+.. _Travis CI: https://travis-ci.org/geopandas/geopandas
diff --git a/doc/source/io.rst b/doc/source/io.rst
new file mode 100644
index 0000000..65f33f0
--- /dev/null
+++ b/doc/source/io.rst
@@ -0,0 +1,28 @@
+
+Reading and Writing Files
+=========================================
+
+
+
+Reading Spatial Data
+---------------------
+
+*geopandas* can read almost any vector-based spatial data format including ESRI shapefile, GeoJSON files and more using the command::
+
+    gpd.read_file()
+
+which returns a GeoDataFrame object. (This is possible because *geopandas* makes use of the great `fiona <http://toblerity.org/fiona/manual.html>`_ library, which in turn makes use of a massive open-source program called `GDAL/OGR <http://www.gdal.org/>`_ designed to facilitate spatial data transformations).
+
+Any arguments passed to ``read_file()`` after the file name will be passed directly to ``fiona.open``, which does the actual data importation. In general, ``read_file`` is pretty smart and should do what you want without extra arguments, but for more help, type::
+
+    import fiona; help(fiona.open)
+
+Among other things, one can explicitly set the driver (shapefile, GeoJSON) with the ``driver`` keyword, or pick a single layer from a multi-layered file with the ``layer`` keyword.
+
+*geopandas* can also get data from a PostGIS database using the ``read_postgis()`` command.
+
+
+Writing Spatial Data
+---------------------
+
+GeoDataFrames can be exported to many different standard formats using the ``GeoDataFrame.to_file()`` method. For a full list of supported formats, type ``import fiona; fiona.supported_drivers``.
diff --git a/doc/source/mapping.rst b/doc/source/mapping.rst
new file mode 100644
index 0000000..ff75960
--- /dev/null
+++ b/doc/source/mapping.rst
@@ -0,0 +1,120 @@
+.. currentmodule:: geopandas
+
+.. ipython:: python
+   :suppress:
+
+   import geopandas as gpd
+
+
+Mapping Tools
+=========================================
+
+
+*geopandas* provides a high-level interface to the ``matplotlib`` library for making maps. Mapping shapes is as easy as using the ``plot()`` method on a ``GeoSeries`` or ``GeoDataFrame``.
+
+Loading some example data:
+
+.. ipython:: python
+
+    world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+    cities = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))
+
+We can now plot those GeoDataFrames:
+
+.. ipython:: python
+
+    # Examine country GeoDataFrame
+    world.head()
+
+    # Basic plot, random colors
+    @savefig world_randomcolors.png width=5in
+    world.plot();
+
+Note that in general, any options one can pass to `pyplot <http://matplotlib.org/api/pyplot_api.html>`_ in ``matplotlib`` (or `style options that work for lines <http://matplotlib.org/api/lines_api.html>`_) can be passed to the ``plot()`` method.
+
+
+Chloropleth Maps
+-----------------
+
+*geopandas* makes it easy to create Chloropleth maps (maps where the color of each shape is based on the value of an associated variable). Simply use the plot command with the ``column`` argument set to the column whose values you want used to assign colors.
+
+.. ipython:: python
+
+    # Plot by GDP per capta
+    world = world[(world.pop_est>0) & (world.name!="Antarctica")]
+    world['gdp_per_cap'] = world.gdp_md_est / world.pop_est
+    @savefig world_gdp_per_cap.png width=5in
+    world.plot(column='gdp_per_cap');
+
+
+Choosing colors
+~~~~~~~~~~~~~~~~
+
+One can also modify the colors used by ``plot`` with the ``cmap`` option (for a full list of colormaps, see the `matplotlib website <http://matplotlib.org/users/colormaps.html>`_):
+
+.. ipython:: python
+
+    @savefig world_gdp_per_cap_red.png width=5in
+    world.plot(column='gdp_per_cap', cmap='OrRd');
+
+
+The way color maps are scaled can also be manipulated with the ``scheme`` option (if you have ``pysal`` installed, which can be accomplished via ``conda install pysal``). By default, ``scheme`` is set to 'equal_intervals', but it can also be adjusted to any other `pysal option <http://pysal.org/1.2/library/esda/mapclassify.html>`_, like 'quantiles', 'percentiles', etc.
+
+.. ipython:: python
+
+    @savefig world_gdp_per_cap_quantiles.png width=5in
+    world.plot(column='gdp_per_cap', cmap='OrRd', scheme='quantiles');
+
+
+Maps with Layers
+-----------------
+
+There are two strategies for making a map with multiple layers -- one more succinct, and one that is a littel more flexible.
+
+Before combining maps, however, remember to always ensure they share a common CRS (so they will align).
+
+.. ipython:: python
+
+    # Look at capitals
+    # Note use of standard `pyplot` line style options
+    @savefig capitals.png width=5in
+    cities.plot(marker='*', color='green', markersize=5);
+
+    # Check crs
+    cities = cities.to_crs(world.crs)
+
+    # Now we can overlay over country outlines
+    # And yes, there are lots of island capitals
+    # apparently in the middle of the ocean!
+
+**Method 1**
+
+.. ipython:: python
+
+    base = world.plot(color='white')
+    @savefig capitals_over_countries_1.png width=5in
+    cities.plot(ax=base, marker='o', color='red', markersize=5);
+
+**Method 2: Using matplotlib objects**
+
+.. ipython:: python
+
+    import matplotlib.pyplot as plt
+    fig, ax = plt.subplots()
+
+    # set aspect to equal. This is done automatically
+    # when using *geopandas* plot on it's own, but not when
+    # working with pyplot directly.
+    ax.set_aspect('equal')
+
+    world.plot(ax=ax, color='white')
+    cities.plot(ax=ax, marker='o', color='red', markersize=5)
+    @savefig capitals_over_countries_2.png width=5in
+    plt.show();
+
+
+Other Resources
+-----------------
+Links to jupyter Notebooks for different mapping tasks:
+
+`Making Heat Maps <http://nbviewer.jupyter.org/gist/perrygeo/c426355e40037c452434>`_
diff --git a/doc/source/mergingdata.rst b/doc/source/mergingdata.rst
new file mode 100644
index 0000000..3ebd69c
--- /dev/null
+++ b/doc/source/mergingdata.rst
@@ -0,0 +1,77 @@
+.. currentmodule:: geopandas
+
+.. ipython:: python
+   :suppress:
+
+   import geopandas as gpd
+
+
+Merging Data
+=========================================
+
+There are two ways to combine datasets in *geopandas* -- attribute joins and spatial joins.
+
+In an attribute join, a ``GeoSeries`` or ``GeoDataFrame`` is combined with a regular *pandas* ``Series`` or ``DataFrame`` based on a common variable. This is analogous to normal merging or joining in *pandas*.
+
+In a Spatial Join, observations from to ``GeoSeries`` or ``GeoDataFrames`` are combined based on their spatial relationship to one another.
+
+In the following examples, we use these datasets:
+
+.. ipython:: python
+
+   world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+   cities = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))
+
+   # For attribute join
+   country_shapes = world[['geometry', 'iso_a3']]
+   country_names = world[['name', 'iso_a3']]
+
+   # For spatial join
+   countries = world[['geometry', 'name']]
+   countries = countries.rename(columns={'name':'country'})
+
+
+Attribute Joins
+----------------
+
+Attribute joins are accomplished using the ``merge`` method. In general, it is recommended to use the ``merge`` method called from the spatial dataset. With that said, the stand-alone ``merge`` function will work if the GeoDataFrame is in the ``left`` argument; if a DataFrame is in the ``left`` argument and a GeoDataFrame is in the ``right`` position, the result will no longer be a GeoDataFrame.
+
+
+For example, consider the following merge that adds full names to a ``GeoDataFrame`` that initially has only ISO codes for each country by merging it with a *pandas* ``DataFrame``.
+
+.. ipython:: python
+
+   # `country_shapes` is GeoDataFrame with country shapes and iso codes
+   country_shapes.head()
+
+   # `country_names` is DataFrame with country names and iso codes
+   country_names.head()
+
+   # Merge with `merge` method on shared variable (iso codes):
+   country_shapes = country_shapes.merge(country_names, on='iso_a3')
+   country_shapes.head()
+
+
+
+Spatial Joins
+----------------
+
+In a Spatial Join, two geometry objects are merged based on their spatial relationship to one another.
+
+.. ipython:: python
+
+
+   # One GeoDataFrame of countries, one of Cities.
+   # Want to merge so we can get each city's country.
+   countries.head()
+   cities.head()
+
+   # Execute spatial join
+
+   cities_with_country = gpd.sjoin(cities, countries, how="inner", op='intersects')
+   cities_with_country.head()
+
+
+The ``op`` options determines the type of join operation to apply. ``op`` can be set to "intersects", "within" or "contains" (these are all equivalent when joining points to polygons, but differ when joining polygons to other polygons or lines).
+
+Note more complicated spatial relationships can be studied by combining geometric operations with spatial join. To find all polygons within a given distance of a point, for example, one can first use the ``buffer`` method to expand each point into a circle of appropriate radius, then intersect those buffered circles with the polygons in question.
diff --git a/doc/source/projections.rst b/doc/source/projections.rst
new file mode 100644
index 0000000..bfca83c
--- /dev/null
+++ b/doc/source/projections.rst
@@ -0,0 +1,71 @@
+.. currentmodule:: geopandas
+
+.. ipython:: python
+   :suppress:
+
+   import geopandas as gpd
+
+
+Managing Projections
+=========================================
+
+
+
+Coordinate Reference Systems
+-----------------------------
+
+CRS are important because the geometric shapes in a GeoSeries or GeoDataFrame object are simply a collection of coordinates in an arbitrary space. A CRS tells Python how those coordinates related to places on the Earth.
+
+CRS are referred to using codes called `proj4 strings <https://en.wikipedia.org/wiki/PROJ.4>`_. You can find the codes for most commonly used projections from `www.spatialreference.org <http://spatialreference.org/>`_ or `remotesensing.org <http://www.remotesensing.org/geotiff/proj_list/>`_.
+
+The same CRS can often be referred to in many ways. For example, one of the most commonly used CRS is the WGS84 latitude-longitude projection. One `proj4` representation of this projection is: ``"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"``. But common projections can also be referred to by `EPSG` codes, so this same projection can also called using the `proj4` string ``"+init=epsg:4326"``.
+
+*geopandas* can accept lots of representations of CRS, including the `proj4` string itself (``"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"``) or parameters broken out in a dictionary: ``{'proj': 'latlong', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}``). In addition, some functions will take `EPSG` codes directly.
+
+For reference, a few very common projections and their proj4 strings:
+
+* WGS84 Latitude/Longitude: ``"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"`` or ``"+init=epsg:4326"``
+* UTM Zones (North): ``"+proj=utm +zone=33 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"``
+* UTM Zones (South): ``"+proj=utm +zone=33 +ellps=WGS84 +datum=WGS84 +units=m +no_defs +south"``
+
+Setting a Projection
+----------------------
+
+There are two relevant operations for projections: setting a projection and re-projecting.
+
+Setting a projection may be necessary when for some reason *geopandas* has coordinate data (x-y values), but no information about how those coordinates refer to locations in the real world. Setting a projection is how one tells *geopandas* how to interpret coordinates. If no CRS is set, *geopandas* geometry operations will still work, but coordinate transformations will not be possible and exported files may not be interpreted correctly by other software.
+
+Be aware that **most of the time** you don't have to set a projection. Data loaded from a reputable source (using the ``from_file()`` command) *should* always include projection information. You can see an objects current CRS through the ``crs`` attribute: ``my_geoseries.crs``.
+
+From time to time, however, you may get data that does not include a projection. In this situation, you have to set the CRS so *geopandas* knows how to interpret the coordinates.
+
+For example, if you convert a spreadsheet of latitudes and longitudes into a GeoSeries by hand, you would set the projection by assigning the WGS84 latitude-longitude CRS to the ``crs`` attribute:
+
+.. sourcecode:: python
+
+   my_geoseries.crs = {'init' :'epsg:4326'}
+
+
+Re-Projecting
+----------------
+
+Re-projecting is the process of changing the representation of locations from one coordinate system to another. All projections of locations on the Earth into a two-dimensional plane `are distortions <https://en.wikipedia.org/wiki/Map_projection#Which_projection_is_best.3F>`_, the projection that is best for your application may be different from the projection associated with the data you import. In these cases, data can be re-projected using the ``to_crs`` command:
+
+.. ipython:: python
+
+    # load example data
+    world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+
+    # Check original projection
+    # (it's Platte Carre! x-y are long and lat)
+    world.crs
+
+    # Visualize
+    @savefig world_starting.png width=3in
+    world.plot();
+
+    # Reproject to Mercator (after dropping Antartica)
+    world = world[(world.name != "Antarctica") & (world.name != "Fr. S. Antarctic Lands")]
+    world = world.to_crs({'init': 'epsg:3395'}) # world.to_crs(epsg=3395) would also work
+    @savefig world_reproj.png width=3in
+    world.plot();
diff --git a/doc/source/user.rst b/doc/source/reference.rst
similarity index 62%
rename from doc/source/user.rst
rename to doc/source/reference.rst
index e5c27e2..2b95ab7 100644
--- a/doc/source/user.rst
+++ b/doc/source/reference.rst
@@ -1,24 +1,7 @@
-GeoPandas User Guide
-====================
 
-GeoPandas implements two main data structures, a ``GeoSeries`` and a
-``GeoDataFrame``.  These are subclasses of pandas ``Series`` and
-``DataFrame``, respectively.
 
-GeoSeries
----------
-
-A ``GeoSeries`` contains a sequence of geometries.
-
-The ``GeoSeries`` class implements nearly all of the attributes and
-methods of Shapely objects.  When applied to a ``GeoSeries``, they
-will apply elementwise to all geometries in the series.  Binary
-operations can be applied between two ``GeoSeries``, in which case the
-operation is carried out elementwise.  The two series will be aligned
-by matching indices.  Binary operations can also be applied to a
-single geometry, in which case the operation is carried out for each
-element of the series with that geometry.  In either case, a
-``Series`` or a ``GeoSeries`` will be returned, as appropriate.
+Reference
+===========================
 
 The following Shapely methods and attributes are available on
 ``GeoSeries`` objects:
@@ -141,15 +124,6 @@ The following Shapely methods and attributes are available on
 
 `Set-theoretic Methods`
 
-.. attribute:: GeoSeries.boundary
-
-  Returns a ``GeoSeries`` of lower dimensional objects representing
-  each geometries's set-theoretic `boundary`.
-
-.. attribute:: GeoSeries.centroid
-
-  Returns a ``GeoSeries`` of points for each geometric centroid.
-
 .. method:: GeoSeries.difference(other)
 
   Returns a ``GeoSeries`` of the points in each geometry that
@@ -177,6 +151,15 @@ The following Shapely methods and attributes are available on
   Returns a ``GeoSeries`` of geometries representing all points within a given `distance`
   of each geometric object.
 
+.. attribute:: GeoSeries.boundary
+
+  Returns a ``GeoSeries`` of lower dimensional objects representing
+  each geometries's set-theoretic `boundary`.
+
+.. attribute:: GeoSeries.centroid
+
+  Returns a ``GeoSeries`` of points for each geometric centroid.
+
 .. attribute:: GeoSeries.convex_hull
 
   Returns a ``GeoSeries`` of geometries representing the smallest
@@ -254,6 +237,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,177 +301,12 @@ 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
 do so.
-
-Geopandas functions
--------------------
-
-.. function:: geopandas.geocode.geocode(strings, provider='googlev3', **kwargs)
-
-  Geocode a list of strings and return a GeoDataFrame containing the
-  resulting points in its ``geometry`` column.  Available
-  ``provider``s include ``googlev3``, ``bing``, ``google``, ``yahoo``,
-  ``mapquest``, and ``openmapquest``.  ``**kwargs`` will be passed as
-  parameters to the appropriate geocoder.
-
-  Requires `geopy`_.  Please consult the Terms of Service for the
-  chosen provider.
-
-Examples
---------
-
-.. sourcecode:: python
-
-    >>> p1 = Polygon([(0, 0), (1, 0), (1, 1)])
-    >>> p2 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
-    >>> p3 = Polygon([(2, 0), (3, 0), (3, 1), (2, 1)])
-    >>> g = GeoSeries([p1, p2, p3])
-    >>> g
-    0    POLYGON ((0.0000000000000000 0.000000000000000...
-    1    POLYGON ((0.0000000000000000 0.000000000000000...
-    2    POLYGON ((2.0000000000000000 0.000000000000000...
-    dtype: object
-
-.. image:: _static/test.png
-
-Some geographic operations return normal pandas object.  The ``area`` property of a ``GeoSeries`` will return a ``pandas.Series`` containing the area of each item in the ``GeoSeries``:
-
-.. sourcecode:: python
-
-    >>> print g.area
-    0    0.5
-    1    1.0
-    2    1.0
-    dtype: float64
-
-Other operations return GeoPandas objects:
-
-.. sourcecode:: python
-
-    >>> g.buffer(0.5)
-    Out[15]:
-    0    POLYGON ((-0.3535533905932737 0.35355339059327...
-    1    POLYGON ((-0.5000000000000000 0.00000000000000...
-    2    POLYGON ((1.5000000000000000 0.000000000000000...
-    dtype: object
-
-.. image:: _static/test_buffer.png
-
-GeoPandas objects also know how to plot themselves.  GeoPandas uses `descartes`_ to generate a `matplotlib`_ plot. To generate a plot of our GeoSeries, use:
-
-.. sourcecode:: python
-
-    >>> g.plot()
-
-GeoPandas also implements alternate constructors that can read any data format recognized by `fiona`_.  To read a `file containing the boroughs of New York City`_:
-
-.. sourcecode:: python
-
-    >>> boros = GeoDataFrame.from_file('nybb.shp')
-    >>> boros.set_index('BoroCode', inplace=True)
-    >>> boros.sort()
-    >>> boros
-                   BoroName    Shape_Area     Shape_Leng  \
-    BoroCode
-    1             Manhattan  6.364422e+08  358532.956418
-    2                 Bronx  1.186804e+09  464517.890553
-    3              Brooklyn  1.959432e+09  726568.946340
-    4                Queens  3.049947e+09  861038.479299
-    5         Staten Island  1.623853e+09  330385.036974
-    
-                                                       geometry
-    BoroCode
-    1         (POLYGON ((981219.0557861328125000 188655.3157...
-    2         (POLYGON ((1012821.8057861328125000 229228.264...
-    3         (POLYGON ((1021176.4790039062500000 151374.796...
-    4         (POLYGON ((1029606.0765991210937500 156073.814...
-    5         (POLYGON ((970217.0223999023437500 145643.3322...
-
-.. image:: _static/nyc.png
- 
-.. sourcecode:: python
-
-    >>> boros['geometry'].convex_hull
-    0    POLYGON ((915517.6877458114176989 120121.88125...
-    1    POLYGON ((1000721.5317993164062500 136681.7761...
-    2    POLYGON ((988872.8212280273437500 146772.03179...
-    3    POLYGON ((977855.4451904296875000 188082.32238...
-    4    POLYGON ((1017949.9776000976562500 225426.8845...
-    dtype: object
-
-.. image:: _static/nyc_hull.png
-
-To demonstrate a more complex operation, we'll generate a
-``GeoSeries`` containing 2000 random points:
-
-.. sourcecode:: python
-
-    >>> from shapely.geometry import Point
-    >>> xmin, xmax, ymin, ymax = 900000, 1080000, 120000, 280000
-    >>> xc = (xmax - xmin) * np.random.random(2000) + xmin
-    >>> yc = (ymax - ymin) * np.random.random(2000) + ymin
-    >>> pts = GeoSeries([Point(x, y) for x, y in zip(xc, yc)])
-
-Now draw a circle with fixed radius around each point:
-
-.. sourcecode:: python
-
-    >>> circles = pts.buffer(2000)
-
-We can collapse these circles into a single shapely MultiPolygon
-geometry with
-
-.. sourcecode:: python
-
-    >>> mp = circles.unary_union
-
-To extract the part of this geometry contained in each borough, we can
-just use:
-
-.. sourcecode:: python
-
-    >>> holes = boros['geometry'].intersection(mp)
-
-.. image:: _static/holes.png
- 
-and to get the area outside of the holes:
-
-.. sourcecode:: python
-
-    >>> boros_with_holes = boros['geometry'].difference(mp)
-
-.. image:: _static/boros_with_holes.png
- 
-Note that this can be simplified a bit, since ``geometry`` is
-available as an attribute on a ``GeoDataFrame``, and the
-``intersection`` and ``difference`` methods are implemented with the
-"&" and "-" operators, respectively.  For example, the latter could
-have been expressed simply as ``boros.geometry - mp``.
-
-It's easy to do things like calculate the fractional area in each
-borough that are in the holes:
-
-.. sourcecode:: python
-
-    >>> holes.area / boros.geometry.area
-    BoroCode
-    1           0.602015
-    2           0.523457
-    3           0.585901
-    4           0.577020
-    5           0.559507
-    dtype: float64
-
-.. _Descartes: https://pypi.python.org/pypi/descartes
-.. _matplotlib: http://matplotlib.org
-.. _fiona: http://toblerity.github.io/fiona
-.. _geopy: https://github.com/geopy/geopy
-.. _file containing the boroughs of New York City: http://www.nyc.gov/html/dcp/download/bytes/nybb_14aav.zip
-
-.. toctree::
-   :maxdepth: 2
-
-
diff --git a/doc/source/set_operations.rst b/doc/source/set_operations.rst
new file mode 100644
index 0000000..2db553b
--- /dev/null
+++ b/doc/source/set_operations.rst
@@ -0,0 +1,195 @@
+.. ipython:: python
+   :suppress:
+
+   import geopandas as gpd
+
+
+Set-Operations with Overlay
+============================
+
+When working with multiple spatial datasets -- especially multiple *polygon* or
+*line* datasets -- users often wish to create new shapes based on places where
+those datasets overlap (or don't overlap). These manipulations are often
+referred using the language of sets -- intersections, unions, and differences.
+These types of operations are made available in the *geopandas* library through
+the ``overlay`` function.
+
+The basic idea is demonstrated by the graphic below but keep in mind that
+overlays operate at the DataFrame level, not on individual geometries, and the
+properties from both are retained. In effect, for every shape in the first
+GeoDataFrame, this operation is executed against every other shape in the other
+GeoDataFrame:
+
+.. image:: _static/overlay_operations.png
+
+**Source: QGIS Documentation**
+
+(Note to users familiar with the *shapely* library: ``overlay`` can be thought
+of as offering versions of the standard *shapely* set-operations that deal with
+the complexities of applying set operations to two *GeoSeries*. The standard
+*shapely* set-operations are also available as ``GeoSeries`` methods.)
+
+
+The different Overlay operations
+--------------------------------
+
+First, we create some example data:
+
+.. ipython:: python
+
+    from shapely.geometry import Polygon
+    polys1 = gpd.GeoSeries([Polygon([(0,0), (2,0), (2,2), (0,2)]),
+                            Polygon([(2,2), (4,2), (4,4), (2,4)])])
+    polys2 = gpd.GeoSeries([Polygon([(1,1), (3,1), (3,3), (1,3)]),
+                            Polygon([(3,3), (5,3), (5,5), (3,5)])])
+
+    df1 = gpd.GeoDataFrame({'geometry': polys1, 'df1':[1,2]})
+    df2 = gpd.GeoDataFrame({'geometry': polys2, 'df2':[1,2]})
+
+These two GeoDataFrames have some overlapping areas:
+
+.. ipython:: python
+
+    ax = df1.plot(color='red');
+    @savefig overlay_example.png width=5in
+    df2.plot(ax=ax, color='green');
+
+We illustrate the different overlay modes with the above example.
+The ``overlay`` function will determine the set of all individual geometries
+from overlaying the two input GeoDataFrames. This result covers the area covered
+by the two input GeoDataFrames, and also preserves all unique regions defined by
+the combined boundaries of the two GeoDataFrames.
+
+When using ``how='union'``, all those possible geometries are returned:
+
+.. ipython:: python
+
+    res_union = gpd.overlay(df1, df2, how='union')
+    res_union
+
+    ax = res_union.plot()
+    df1.plot(ax=ax, facecolor='none');
+    @savefig overlay_example_union.png width=5in
+    df2.plot(ax=ax, facecolor='none');
+
+The other ``how`` operations will return different subsets of those geometries.
+With ``how='intersection'``, it returns only those geometries that are contained
+by both GeoDataFrames:
+
+.. ipython:: python
+
+    res_intersection = gpd.overlay(df1, df2, how='intersection')
+    res_intersection
+
+    ax = res_intersection.plot()
+    df1.plot(ax=ax, facecolor='none');
+    @savefig overlay_example_intersection.png width=5in
+    df2.plot(ax=ax, facecolor='none');
+
+``how='symmetric_difference'`` is the opposite of ``'intersection'`` and returns
+the geometries that are only part of one of the GeoDataFrames but not of both:
+
+.. ipython:: python
+
+    res_symdiff = gpd.overlay(df1, df2, how='symmetric_difference')
+    res_symdiff
+
+    ax = res_symdiff.plot()
+    df1.plot(ax=ax, facecolor='none');
+    @savefig overlay_example_symdiff.png width=5in
+    df2.plot(ax=ax, facecolor='none');
+
+To obtain the geometries that are part of ``df1`` but are not contained in
+``df2``, you can use ``how='difference'``:
+
+.. ipython:: python
+
+    res_difference = gpd.overlay(df1, df2, how='difference')
+    res_difference
+
+    ax = res_difference.plot()
+    df1.plot(ax=ax, facecolor='none');
+    @savefig overlay_example_difference.png width=5in
+    df2.plot(ax=ax, facecolor='none');
+
+Finally, with ``how='identity'``, the result consists of the surface of ``df1``,
+but with the geometries obtained from overlaying ``df1`` with ``df2``:
+
+.. ipython:: python
+
+    res_identity = gpd.overlay(df1, df2, how='identity')
+    res_identity
+
+    ax = res_identity.plot()
+    df1.plot(ax=ax, facecolor='none');
+    @savefig overlay_example_identity.png width=5in
+    df2.plot(ax=ax, facecolor='none');
+
+
+Overlay Countries Example
+-------------------------
+
+First, we load the countries and cities example datasets and select :
+
+.. ipython:: python
+
+    world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
+    capitals = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))
+
+    # Select South Amarica and some columns
+    countries = world[world['continent'] == "South America"]
+    countries = countries[['geometry', 'name']]
+
+    # Project to crs that uses meters as distance measure
+    countries = countries.to_crs('+init=epsg:3395')
+    capitals = capitals.to_crs('+init=epsg:3395')
+
+To illustrate the ``overlay`` function, consider the following case in which one
+wishes to identify the "core" portion of each country -- defined as areas within
+500km of a capital -- using a ``GeoDataFrame`` of countries and a
+``GeoDataFrame`` of capitals.
+
+.. ipython:: python
+
+    # Look at countries:
+    @savefig world_basic.png width=5in
+    countries.plot();
+
+    # Now buffer cities to find area within 500km.
+    # Check CRS -- World Mercator, units of meters.
+    capitals.crs
+
+    # make 500km buffer
+    capitals['geometry']= capitals.buffer(500000)
+    @savefig capital_buffers.png width=5in
+    capitals.plot();
+
+
+To select only the portion of countries within 500km of a capital, we specify the ``how`` option to be "intersect", which creates a new set of polygons where these two layers overlap:
+
+.. ipython:: python
+
+   country_cores = gpd.overlay(countries, capitals, how='intersection')
+   @savefig country_cores.png width=5in
+   country_cores.plot();
+
+Changing the "how" option allows for different types of overlay operations. For example, if we were interested in the portions of countries *far* from capitals (the peripheries), we would compute the difference of the two.
+
+.. ipython:: python
+
+   country_peripheries = gpd.overlay(countries, capitals, how='difference')
+   @savefig country_peripheries.png width=5in
+   country_peripheries.plot();
+
+
+
+
+More Examples
+-------------
+
+A larger set of examples of the use of ``overlay`` can be found `here <http://nbviewer.jupyter.org/github/geopandas/geopandas/blob/master/examples/overlays.ipynb>`_
+
+
+
+.. toctree::
+   :maxdepth: 2
diff --git a/examples/null_geom.geojson b/examples/null_geom.geojson
new file mode 100644
index 0000000..9bbbadc
--- /dev/null
+++ b/examples/null_geom.geojson
@@ -0,0 +1,9 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
+                                                                                
+"features": [
+{ "type": "Feature", "properties": { "Name": "Null Geometry" }, "geometry": null },
+{ "type": "Feature", "properties": { "Name": "SF to NY" }, "geometry": { "type": "LineString", "coordinates": [ [ -122.4051293283311, 37.786780113640894 ], [ -73.859832357849271, 40.487594916296196 ] ] } }
+]
+}
diff --git a/examples/nyc_boros.py b/examples/nyc_boros.py
index 6db6ec5..3e70210 100644
--- a/examples/nyc_boros.py
+++ b/examples/nyc_boros.py
@@ -14,7 +14,7 @@ from geopandas import GeoSeries, GeoDataFrame
 np.random.seed(1)
 DPI = 100
 
-# http://www.nyc.gov/html/dcp/download/bytes/nybb_14aav.zip
+# http://www1.nyc.gov/assets/planning/download/zip/data-maps/open-data/nybb_16a.zip
 boros = GeoDataFrame.from_file('nybb.shp')
 boros.set_index('BoroCode', inplace=True)
 boros.sort()
diff --git a/examples/overlays.ipynb b/examples/overlays.ipynb
new file mode 100644
index 0000000..d0db629
--- /dev/null
+++ b/examples/overlays.ipynb
@@ -0,0 +1,601 @@
+{
+ "metadata": {
+  "name": "",
+  "signature": "sha256:f2cfcb83c09747f8bcf7a08abe582a16207e6bcfb0547e4cb69755830bcae172"
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+  {
+   "cells": [
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "Spatial overlays allow you to compare two GeoDataFrames containing polygon or multipolygon geometries \n",
+      "and create a new GeoDataFrame with the new geometries representing the spatial combination *and*\n",
+      "merged properties. This allows you to answer questions like\n",
+      "\n",
+      "> What are the demographics of the census tracts within 1000 ft of the highway?\n",
+      "\n",
+      "The basic idea is demonstrated by the graphic below but keep in mind that overlays operate at the dataframe level, \n",
+      "not on individual geometries, and the properties from both are retained"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "from IPython.core.display import Image \n",
+      "Image(url=\"http://docs.qgis.org/testing/en/_images/overlay_operations.png\") "
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<img src=\"http://docs.qgis.org/testing/en/_images/overlay_operations.png\"/>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 1,
+       "text": [
+        "<IPython.core.display.Image at 0x1038a8f10>"
+       ]
+      }
+     ],
+     "prompt_number": 1
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "Now we can load up two GeoDataFrames containing (multi)polygon geometries..."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "%matplotlib inline\n",
+      "import os\n",
+      "from shapely.geometry import Point\n",
+      "from geopandas import GeoDataFrame, read_file\n",
+      "from geopandas.tools import overlay\n",
+      "\n",
+      "# NYC Boros\n",
+      "zippath = os.path.abspath('../examples/nybb_14aav.zip')\n",
+      "polydf = read_file('/nybb_14a_av/nybb.shp', vfs='zip://' + zippath)\n",
+      "\n",
+      "# Generate some circles\n",
+      "b = [int(x) for x in polydf.total_bounds]\n",
+      "N = 10\n",
+      "polydf2 = GeoDataFrame([\n",
+      "    {'geometry' : Point(x, y).buffer(10000), 'value1': x + y, 'value2': x - y}\n",
+      "    for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),\n",
+      "                    range(b[1], b[3], int((b[3]-b[1])/N)))])"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 2
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "The first dataframe contains multipolygons of the NYC boros"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "polydf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 3,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x1038b1550>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAAUcAAAEACAYAAAAgFLS/AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXVUVVkbh5976VAJSVtBsXBsrJExsWtsjDFH1LEm7BjH\nnrG7YxxzxvgMFB0xxg6wAxUDA6WUjnu/P84BkZAQuID7Weusu+8+e+/zHtfl5873BYFAIBAIBAKB\nQCAQCAQCgUAgEAgEAoFAIBAIBAKBQJAHKAacAG4BN4Ef5PxawEXgGnAJqJmozjjgAXAXaJYovzpw\nQ763KFG+HrBDzj8PlEh0rw9wX756Z8ULCQQCQVZgDXwlp42Be0B5wBNoLue3QBJQgAqAF6ADlAR8\nAIV87yKSqAIcAlzktBuwXE53BbbLaTPgIWAiX/FpgUAgyHaUadx/hSR2AKHAHaAI8BIoJOebAH5y\nuh2wDYgBf [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x1038ab350>"
+       ]
+      }
+     ],
+     "prompt_number": 3
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "And the second GeoDataFrame is a randomly generated set of circles in the same geographic space (TODO ...use real data)"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "polydf2.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 4,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x10fb4ad90>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAARUAAAEACAYAAACd9eLKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJztnXlYFdX/x19eQFBRBHcQ3MV9w91UclfMpVwzNbM0zVyy\nxexbapllZqk/0yzXyl0zzV1T3DdcElwCQWRRUQQkcAG5/v44c+OqKFwYWT+v55nnzj0z8579Ped8\nzpkzIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAjpwg44BpwBzgNfaelOwC7AH9gJFDVb5mMgALgIdDBL\n9wB8tWmzzdJtgdVa+lGgnNm0wdo6/IFBeuyQIAhZT0Ht1xp1078AfAN8qKV/BHytjddAGZANUB64\nBOTTph0HGmvjW4FO2vhIYJ423hdYpY07AYEowypqNi4IQi6hIHACqInKhZTS0ktr/0HlUj4yW2Y7\n0BQoA1wwS [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x10fb7da50>"
+       ]
+      }
+     ],
+     "prompt_number": 4
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "The `geopandas.tools.overlay` function takes three arguments:\n",
+      "\n",
+      "* df1\n",
+      "* df2\n",
+      "* how\n",
+      "\n",
+      "Where `how` can be one of:\n",
+      "\n",
+      "    ['intersection',\n",
+      "    'union',\n",
+      "    'identity',\n",
+      "    'symmetric_difference',\n",
+      "    'difference']\n",
+      "\n",
+      "So let's identify the areas (and attributes) where both dataframes intersect using the `overlay` tool. "
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "from geopandas.tools import overlay\n",
+      "newdf = overlay(polydf, polydf2, how=\"intersection\")\n",
+      "newdf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 5,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x10ce02510>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAASsAAAEACAYAAADrz1BBAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXdYVEcXh19AmiAi2BUEjaioWFDRz6hoLFiiJCaxd40R\nNbEk1kRjjC1FE2PvLaLGrlFEE0k0dhQ7oggWEBXsSt/9/pi7sBSl7dKc93nus7NzZ+bMhd3fzpw7\ndw5IJBKJRCKRSCQSiUQikUgkEolEIpFIJBKJRCKR5FvsgMPAZeAS8LmS3wg4BZwDTgMNtepMBK4D\ngUBbrXxX4KJy7letfFNgs5J/Aqikda4fEKQcfXVxQRKJpHBSFqirpC2Ba0ANwA9op+S3RwgagDMQ\nABgDDsANwEA5dwohcgD7AA8l7QUsUtLdgE1K2gYIBqyVQ5OWSCRvIYYZnI9AiA/AC+AqUAG4BxRX\n8q2BMCXdB [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x10ce02250>"
+       ]
+      }
+     ],
+     "prompt_number": 5
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "And take a look at the attributes; we see that the attributes from both of the original GeoDataFrames are retained. "
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "polydf.head()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "      <th>geometry</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> (POLYGON ((970217.0223999023 145643.3322143555...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td> 3</td>\n",
+        "      <td>      Brooklyn</td>\n",
+        "      <td> 1.937810e+09</td>\n",
+        "      <td> 741227.337073</td>\n",
+        "      <td> (POLYGON ((1021176.479003906 151374.7969970703...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td> 4</td>\n",
+        "      <td>        Queens</td>\n",
+        "      <td> 3.045079e+09</td>\n",
+        "      <td> 896875.396449</td>\n",
+        "      <td> (POLYGON ((1029606.076599121 156073.8142089844...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td> 1</td>\n",
+        "      <td>     Manhattan</td>\n",
+        "      <td> 6.364308e+08</td>\n",
+        "      <td> 358400.912836</td>\n",
+        "      <td> (POLYGON ((981219.0557861328 188655.3157958984...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td> 2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "      <td> (POLYGON ((1012821.805786133 229228.2645874023...</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 6,
+       "text": [
+        "   BoroCode       BoroName    Shape_Area     Shape_Leng  \\\n",
+        "0         5  Staten Island  1.623847e+09  330454.175933   \n",
+        "1         3       Brooklyn  1.937810e+09  741227.337073   \n",
+        "2         4         Queens  3.045079e+09  896875.396449   \n",
+        "3         1      Manhattan  6.364308e+08  358400.912836   \n",
+        "4         2          Bronx  1.186822e+09  464475.145651   \n",
+        "\n",
+        "                                            geometry  \n",
+        "0  (POLYGON ((970217.0223999023 145643.3322143555...  \n",
+        "1  (POLYGON ((1021176.479003906 151374.7969970703...  \n",
+        "2  (POLYGON ((1029606.076599121 156073.8142089844...  \n",
+        "3  (POLYGON ((981219.0557861328 188655.3157958984...  \n",
+        "4  (POLYGON ((1012821.805786133 229228.2645874023...  "
+       ]
+      }
+     ],
+     "prompt_number": 6
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "polydf2.head()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>geometry</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td> POLYGON ((923175 120121, 923126.847266722 1191...</td>\n",
+        "      <td> 1033296</td>\n",
+        "      <td> 793054</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td> POLYGON ((938595 135393, 938546.847266722 1344...</td>\n",
+        "      <td> 1063988</td>\n",
+        "      <td> 793202</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td> POLYGON ((954015 150665, 953966.847266722 1496...</td>\n",
+        "      <td> 1094680</td>\n",
+        "      <td> 793350</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td> POLYGON ((969435 165937, 969386.847266722 1649...</td>\n",
+        "      <td> 1125372</td>\n",
+        "      <td> 793498</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td> POLYGON ((984855 181209, 984806.847266722 1802...</td>\n",
+        "      <td> 1156064</td>\n",
+        "      <td> 793646</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 7,
+       "text": [
+        "                                            geometry   value1  value2\n",
+        "0  POLYGON ((923175 120121, 923126.847266722 1191...  1033296  793054\n",
+        "1  POLYGON ((938595 135393, 938546.847266722 1344...  1063988  793202\n",
+        "2  POLYGON ((954015 150665, 953966.847266722 1496...  1094680  793350\n",
+        "3  POLYGON ((969435 165937, 969386.847266722 1649...  1125372  793498\n",
+        "4  POLYGON ((984855 181209, 984806.847266722 1802...  1156064  793646"
+       ]
+      }
+     ],
+     "prompt_number": 7
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "newdf.head()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "      <th>geometry</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> 1033296</td>\n",
+        "      <td> 793054</td>\n",
+        "      <td> POLYGON ((916755.4256330276 129447.9617643995,...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> 1063988</td>\n",
+        "      <td> 793202</td>\n",
+        "      <td> POLYGON ((938595 135393, 938546.847266722 1344...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> 1125372</td>\n",
+        "      <td> 793498</td>\n",
+        "      <td> POLYGON ((961436.3049926758 175473.0296020508,...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> 1094680</td>\n",
+        "      <td> 793350</td>\n",
+        "      <td> POLYGON ((954015 150665, 953966.847266722 1496...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td> 2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "      <td> 1309524</td>\n",
+        "      <td> 794386</td>\n",
+        "      <td> POLYGON ((1043287.193237305 260300.0289916992,...</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 8,
+       "text": [
+        "   BoroCode       BoroName    Shape_Area     Shape_Leng   value1  value2  \\\n",
+        "0         5  Staten Island  1.623847e+09  330454.175933  1033296  793054   \n",
+        "1         5  Staten Island  1.623847e+09  330454.175933  1063988  793202   \n",
+        "2         5  Staten Island  1.623847e+09  330454.175933  1125372  793498   \n",
+        "3         5  Staten Island  1.623847e+09  330454.175933  1094680  793350   \n",
+        "4         2          Bronx  1.186822e+09  464475.145651  1309524  794386   \n",
+        "\n",
+        "                                            geometry  \n",
+        "0  POLYGON ((916755.4256330276 129447.9617643995,...  \n",
+        "1  POLYGON ((938595 135393, 938546.847266722 1344...  \n",
+        "2  POLYGON ((961436.3049926758 175473.0296020508,...  \n",
+        "3  POLYGON ((954015 150665, 953966.847266722 1496...  \n",
+        "4  POLYGON ((1043287.193237305 260300.0289916992,...  "
+       ]
+      }
+     ],
+     "prompt_number": 8
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "Now let's look at the other `how` operations:"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "newdf = overlay(polydf, polydf2, how=\"union\")\n",
+      "newdf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 9,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x10ce3a610>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAARUAAAEACAYAAACd9eLKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXdUVEcbh58tNKUrKqKAiL13DRpRscaWiF1jjy2WJJ+9\nRY0ajV0Txd57jV2x12BXQKSJjV6ls8vu98ddESIKizTNfc7Zw71T3pm77P526jsgIiIiIiIiIiIi\nIiIiIiIiIiIiIiIiIiIiIiIiIiKSI/SBf4AHgCewQBNuDpwDvIGzgGm6PFMAH8ALaJMuvB7wWBO3\nIl24HrBXE34LsEkXN0BThjfwfW48kIiISMFTRPNXjvClbwosAiZqwicBv2uuqyIIkA5gC/gCEk2c\nG9BQc30SaKe5HgX8pbnuCezRXJsDfgiCZZruWkRE5AuhCHAbqIbQCimpCS+luQehlTIpXZ7TQGPA\nEniSLrwXs [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x10cdaf190>"
+       ]
+      }
+     ],
+     "prompt_number": 9
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "newdf = overlay(polydf, polydf2, how=\"identity\")\n",
+      "newdf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 10,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x113aeda90>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAAUcAAAEACAYAAAAgFLS/AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXVYVckbxz9cussABBW7sFDB2BUTXbtWbNdaa1ddN1zb\nDWN/7urahYUdaxcWdhcWKiIKKKKEdNz4/XEOJSDtFTyf57nPHabOexS/zsw7My9ISEhISEhISEhI\nSEhISEhISEhISEhISEhISEhISEhISEhISBQB7IDTwH3gHvC9mN8IuArcAq4BDdO0+RV4AvgAbdPk\nOwJ3xbJ/0+TrAtvF/MtAuTRlg4DH4mdgQbyQhISEREFgBdQV00bAI6A64AW4ivntEQQUoAZwG9AG\nygO+gIZYdhVBVAEOA+3E9GhgmZjuDWwT0xbAU8BM/CSnJSQkJAodWTblwQhiBxANPATKAK8AUzHf\nDAgS012Ar [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x10cda3b10>"
+       ]
+      }
+     ],
+     "prompt_number": 10
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "newdf = overlay(polydf, polydf2, how=\"symmetric_difference\")\n",
+      "newdf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 11,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x113473b50>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAARUAAAEACAYAAACd9eLKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXdYVEcXh196EaWJiAiiYMPeu6KixvhZYq/BGlvE3mLU\nqLFrYjQW7Igdey/EXkAsiIAoghRBRHqvu98fu6ygKKzSve/z7MPduTNzZ1f5MXPmzDkgICAgICAg\nICAgICAgICAgICAgICAgICAgICAgICDwVagDLoAb4AWslJbrAVeBl8AVQCdLm/mAD+ANdM1S3gR4\nJr33T5ZyNeCItNwZqJLlno30GS+Bn/PjAwkICBQ9mtKfykh+6dsCa4A50vK5wCrptSUSAVIBzIBX\ngIL03gOgufT6AvCD9HoSsEV6PQg4LL3WA3yRCJZOlmsBAYFSgibgCtRBMgsxlJZXlL4HySxlbpY2\nl4CWgBHwP [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x113c66e10>"
+       ]
+      }
+     ],
+     "prompt_number": 11
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "newdf = overlay(polydf, polydf2, how=\"difference\")\n",
+      "newdf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 12,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x113c7cc10>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAAUcAAAEACAYAAAAgFLS/AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXVYlecbxz8cGkQkBERRbCzEDgxs51TUGdhuxpTZ09mE\ngbrf1Nktxux2tjixwMAWOzDAohSQPvz+eA+IgtIcwOdzXe91nvPk/SJ+efoGgUAgEAgEAoFAIBAI\nBAKBQCAQCAQCgUAgEAgEAoFAkA+wAE4BvsBtYKQivi5wCbgGXAbqJCszCXgI3ANaJ4uvBdxSpC1M\nFq8JbFfEXwBKJUvrDzxQPP2y44UEAoEgOzADbBThQsB9oBLgCbRRxP+AJKAAlYHrgDpgCTwCVBRp\nl5BEFeAw0FYRdgSWKcI9gG2KsCHwGCiieBLDAoFAkOPI0kh/jSR2AOHAXaA48ArQV8QXAfwVYXtg\nKxAL+CGJY [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x113464590>"
+       ]
+      }
+     ],
+     "prompt_number": 12
+    }
+   ],
+   "metadata": {}
+  }
+ ]
+}
\ No newline at end of file
diff --git a/examples/spatial_joins.ipynb b/examples/spatial_joins.ipynb
new file mode 100644
index 0000000..dda756d
--- /dev/null
+++ b/examples/spatial_joins.ipynb
@@ -0,0 +1,929 @@
+{
+ "metadata": {
+  "name": "",
+  "signature": "sha256:48efe66abdde37459952c96981b795279e6c34f5d4ebbec82ba5614f7e90d199"
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+  {
+   "cells": [
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "#Spatial Joins\n",
+      "\n",
+      "A *spatial join* uses [binary predicates](http://toblerity.org/shapely/manual.html#binary-predicates) \n",
+      "such as `intersects` and `crosses` to combine two `GeoDataFrames` based on the spatial relationship \n",
+      "between their geometries.\n",
+      "\n",
+      "A common use case might be a spatial join between a point layer and a polygon layer where you want to retain the point geometries and grab the attributes of the intersecting polygons.\n"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "from IPython.core.display import Image \n",
+      "Image(url=\"https://dl.dropboxusercontent.com/u/6769420/sjoin_test.png\") "
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<img src=\"https://dl.dropboxusercontent.com/u/6769420/sjoin_test.png\"/>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 2,
+       "text": [
+        "<IPython.core.display.Image at 0x7fe191420650>"
+       ]
+      }
+     ],
+     "prompt_number": 2
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "\n",
+      "## Types of spatial joins\n",
+      "\n",
+      "We currently support the following methods of spatial joins. We refer to the *left_df* and *right_df* which are the correspond to the two dataframes passed in as args.\n",
+      "\n",
+      "### Left outer join\n",
+      "\n",
+      "In a LEFT OUTER JOIN (`how='left'`), we keep *all* rows from the left and duplicate them if necessary to represent multiple hits between the two dataframes. We retain attributes of the right if they intersect and lose right rows that don't intersect. A left outer join implies that we are interested in retaining the geometries of the left. \n",
+      "\n",
+      "This is equivalent to the PostGIS query:\n",
+      "```\n",
+      "SELECT pts.geom, pts.id as ptid, polys.id as polyid  \n",
+      "FROM pts\n",
+      "LEFT OUTER JOIN polys\n",
+      "ON ST_Intersects(pts.geom, polys.geom);\n",
+      "\n",
+      "                    geom                    | ptid | polyid \n",
+      "--------------------------------------------+------+--------\n",
+      " 010100000040A9FBF2D88AD03F349CD47D796CE9BF |    4 |     10\n",
+      " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     10\n",
+      " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     20\n",
+      " 0101000000F0D88AA0E1A4EEBF7052F7E5B115E9BF |    2 |     20\n",
+      " 0101000000818693BA2F8FF7BF4ADD97C75604E9BF |    1 |       \n",
+      "(5 rows)\n",
+      "```\n",
+      "\n",
+      "### Right outer join\n",
+      "\n",
+      "In a RIGHT OUTER JOIN (`how='right'`), we keep *all* rows from the right and duplicate them if necessary to represent multiple hits between the two dataframes. We retain attributes of the left if they intersect and lose left rows that don't intersect. A right outer join implies that we are interested in retaining the geometries of the right. \n",
+      "\n",
+      "This is equivalent to the PostGIS query:\n",
+      "```\n",
+      "SELECT polys.geom, pts.id as ptid, polys.id as polyid  \n",
+      "FROM pts\n",
+      "RIGHT OUTER JOIN polys\n",
+      "ON ST_Intersects(pts.geom, polys.geom);\n",
+      "\n",
+      "  geom    | ptid | polyid \n",
+      "----------+------+--------\n",
+      " 01...9BF |    4 |     10\n",
+      " 01...9BF |    3 |     10\n",
+      " 02...7BF |    3 |     20\n",
+      " 02...7BF |    2 |     20\n",
+      " 00...5BF |      |     30\n",
+      "(5 rows)\n",
+      "```\n",
+      "\n",
+      "### Inner join\n",
+      "\n",
+      "In an INNER JOIN (`how='inner'`), we keep rows from the right and left only where their binary predicate is `True`. We duplicate them if necessary to represent multiple hits between the two dataframes. We retain attributes of the right and left only if they intersect and lose all rows that do not. An inner join implies that we are interested in retaining the geometries of the left. \n",
+      "\n",
+      "This is equivalent to the PostGIS query:\n",
+      "```\n",
+      "SELECT pts.geom, pts.id as ptid, polys.id as polyid  \n",
+      "FROM pts\n",
+      "INNER JOIN polys\n",
+      "ON ST_Intersects(pts.geom, polys.geom);\n",
+      "\n",
+      "                    geom                    | ptid | polyid \n",
+      "--------------------------------------------+------+--------\n",
+      " 010100000040A9FBF2D88AD03F349CD47D796CE9BF |    4 |     10\n",
+      " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     10\n",
+      " 010100000048EABE3CB622D8BFA8FBF2D88AA0E9BF |    3 |     20\n",
+      " 0101000000F0D88AA0E1A4EEBF7052F7E5B115E9BF |    2 |     20\n",
+      "(4 rows) \n",
+      "```"
+     ]
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "## Spatial Joins between two GeoDataFrames\n",
+      "\n",
+      "Let's take a look at how we'd implement these using `GeoPandas`. First, load up the NYC test data into `GeoDataFrames`:"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "import os\n",
+      "from shapely.geometry import Point\n",
+      "from geopandas import GeoDataFrame, read_file\n",
+      "from geopandas.tools import overlay\n",
+      "\n",
+      "# NYC Boros\n",
+      "zippath = os.path.abspath('../examples/nybb_14aav.zip')\n",
+      "polydf = read_file('/nybb_14a_av/nybb.shp', vfs='zip://' + zippath)\n",
+      "\n",
+      "# Generate some points\n",
+      "b = [int(x) for x in polydf.total_bounds]\n",
+      "N = 8\n",
+      "pointdf = GeoDataFrame([\n",
+      "    {'geometry' : Point(x, y), 'value1': x + y, 'value2': x - y}\n",
+      "    for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),\n",
+      "                    range(b[1], b[3], int((b[3]-b[1])/N)))])"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 19
+    },
+    {
+     "cell_type": "code",
+     "collapsed": true,
+     "input": [
+      "pointdf"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>geometry</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td>  POINT (913175 120121)</td>\n",
+        "      <td> 1033296</td>\n",
+        "      <td> 793054</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td>  POINT (932450 139211)</td>\n",
+        "      <td> 1071661</td>\n",
+        "      <td> 793239</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td>  POINT (951725 158301)</td>\n",
+        "      <td> 1110026</td>\n",
+        "      <td> 793424</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td>  POINT (971000 177391)</td>\n",
+        "      <td> 1148391</td>\n",
+        "      <td> 793609</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td>  POINT (990275 196481)</td>\n",
+        "      <td> 1186756</td>\n",
+        "      <td> 793794</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>5</th>\n",
+        "      <td> POINT (1009550 215571)</td>\n",
+        "      <td> 1225121</td>\n",
+        "      <td> 793979</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>6</th>\n",
+        "      <td> POINT (1028825 234661)</td>\n",
+        "      <td> 1263486</td>\n",
+        "      <td> 794164</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>7</th>\n",
+        "      <td> POINT (1048100 253751)</td>\n",
+        "      <td> 1301851</td>\n",
+        "      <td> 794349</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>8</th>\n",
+        "      <td> POINT (1067375 272841)</td>\n",
+        "      <td> 1340216</td>\n",
+        "      <td> 794534</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 20,
+       "text": [
+        "                 geometry   value1  value2\n",
+        "0   POINT (913175 120121)  1033296  793054\n",
+        "1   POINT (932450 139211)  1071661  793239\n",
+        "2   POINT (951725 158301)  1110026  793424\n",
+        "3   POINT (971000 177391)  1148391  793609\n",
+        "4   POINT (990275 196481)  1186756  793794\n",
+        "5  POINT (1009550 215571)  1225121  793979\n",
+        "6  POINT (1028825 234661)  1263486  794164\n",
+        "7  POINT (1048100 253751)  1301851  794349\n",
+        "8  POINT (1067375 272841)  1340216  794534"
+       ]
+      }
+     ],
+     "prompt_number": 20
+    },
+    {
+     "cell_type": "code",
+     "collapsed": true,
+     "input": [
+      "polydf"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "      <th>geometry</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> (POLYGON ((970217.0223999023 145643.3322143555...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td> 3</td>\n",
+        "      <td>      Brooklyn</td>\n",
+        "      <td> 1.937810e+09</td>\n",
+        "      <td> 741227.337073</td>\n",
+        "      <td> (POLYGON ((1021176.479003906 151374.7969970703...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td> 4</td>\n",
+        "      <td>        Queens</td>\n",
+        "      <td> 3.045079e+09</td>\n",
+        "      <td> 896875.396449</td>\n",
+        "      <td> (POLYGON ((1029606.076599121 156073.8142089844...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td> 1</td>\n",
+        "      <td>     Manhattan</td>\n",
+        "      <td> 6.364308e+08</td>\n",
+        "      <td> 358400.912836</td>\n",
+        "      <td> (POLYGON ((981219.0557861328 188655.3157958984...</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td> 2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "      <td> (POLYGON ((1012821.805786133 229228.2645874023...</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 21,
+       "text": [
+        "   BoroCode       BoroName    Shape_Area     Shape_Leng  \\\n",
+        "0         5  Staten Island  1.623847e+09  330454.175933   \n",
+        "1         3       Brooklyn  1.937810e+09  741227.337073   \n",
+        "2         4         Queens  3.045079e+09  896875.396449   \n",
+        "3         1      Manhattan  6.364308e+08  358400.912836   \n",
+        "4         2          Bronx  1.186822e+09  464475.145651   \n",
+        "\n",
+        "                                            geometry  \n",
+        "0  (POLYGON ((970217.0223999023 145643.3322143555...  \n",
+        "1  (POLYGON ((1021176.479003906 151374.7969970703...  \n",
+        "2  (POLYGON ((1029606.076599121 156073.8142089844...  \n",
+        "3  (POLYGON ((981219.0557861328 188655.3157958984...  \n",
+        "4  (POLYGON ((1012821.805786133 229228.2645874023...  "
+       ]
+      }
+     ],
+     "prompt_number": 21
+    },
+    {
+     "cell_type": "code",
+     "collapsed": true,
+     "input": [
+      "pointdf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 22,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x7fe17c79de90>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAAUEAAAD/CAYAAABvuWSAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAGQFJREFUeJzt3X+QpVV5J/DPDDBAQg/iIBizGEbcrFguiygM0qJtHEQk\n1AZdF0U3a6XEFdCSkFqtQsM0Zl0jKgobAlVqVhAWKzBYolMZhwjX+cHvFVBX10i2KUkZYwocZiAK\nI7J/PKfpa9sz3dP07Wnm/X6quub2ue99z317bn/7nPe89z5ERERERERERERERERERERERHTaXvgC\n1uN2nIIXYSM24HNY1LY9A3fiVpzc2vbF6vb4NTiwtR+L29p+zu/rb1XrZxOOHsQBRUTsjHfgonb7\nAPwQ1+D1re0q/D6ei2+p0Fzabi/BuSZC7jR8ut2+B8vb7TU4Ekfh663tENwx1wcTETHZ4mnuv9ZE\niC3GNvwMy [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x7fe17c8344d0>"
+       ]
+      }
+     ],
+     "prompt_number": 22
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "polydf.plot()"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 23,
+       "text": [
+        "<matplotlib.axes.AxesSubplot at 0x7fe17c48f8d0>"
+       ]
+      },
+      {
+       "metadata": {},
+       "output_type": "display_data",
+       "png": "iVBORw0KGgoAAAANSUhEUgAAAUEAAAD/CAYAAABvuWSAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzsnXVYlFkbh+8ZhgEkFASxUMRWVOzAQMWOtXvtXDvXz3bt\nWl3XjnXtxlhcW1k7sTDAAMFGEEElZ+b74x0QZZSQ5tzXNZczz3vOeZ8X4TcnnwcEAoFAIBAIBAKB\nQCAQCAQCgUAgEAgEAoFAIBAIBIIsjT6wGTgDXAZaACWAc8BZYD0g05btB1wFLgLNtDYjYK+2/iHA\nUmuvBlzStjMl1v2mau9zHqicEg8kEAgEiaEn8Lv2vTngC2wHGmttW4DmQG7gNpJommnfK4FRfBa5\njsAS7fubQCHt+0OAA1ABOKm12QBXkvthBAKB4Gvk8VzfzWcRkwORQCiQE6kHaApEAFWQem+RQDDw\nCCgLOAJHt [...]
+       "text": [
+        "<matplotlib.figure.Figure at 0x7fe17c7a93d0>"
+       ]
+      }
+     ],
+     "prompt_number": 23
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "## Joins"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "from geopandas.tools import sjoin\n",
+      "join_left_df = sjoin(pointdf, polydf, how=\"left\")\n",
+      "join_left_df\n",
+      "# Note the NaNs where the point did not intersect a boro"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>geometry</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td>  POINT (913175 120121)</td>\n",
+        "      <td> 1033296</td>\n",
+        "      <td> 793054</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td>  POINT (932450 139211)</td>\n",
+        "      <td> 1071661</td>\n",
+        "      <td> 793239</td>\n",
+        "      <td>  5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td>  POINT (951725 158301)</td>\n",
+        "      <td> 1110026</td>\n",
+        "      <td> 793424</td>\n",
+        "      <td>  5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td>  POINT (971000 177391)</td>\n",
+        "      <td> 1148391</td>\n",
+        "      <td> 793609</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td>  POINT (990275 196481)</td>\n",
+        "      <td> 1186756</td>\n",
+        "      <td> 793794</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>5</th>\n",
+        "      <td> POINT (1009550 215571)</td>\n",
+        "      <td> 1225121</td>\n",
+        "      <td> 793979</td>\n",
+        "      <td>  4</td>\n",
+        "      <td>        Queens</td>\n",
+        "      <td> 3.045079e+09</td>\n",
+        "      <td> 896875.396449</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>6</th>\n",
+        "      <td> POINT (1028825 234661)</td>\n",
+        "      <td> 1263486</td>\n",
+        "      <td> 794164</td>\n",
+        "      <td>  2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>7</th>\n",
+        "      <td> POINT (1048100 253751)</td>\n",
+        "      <td> 1301851</td>\n",
+        "      <td> 794349</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>8</th>\n",
+        "      <td> POINT (1067375 272841)</td>\n",
+        "      <td> 1340216</td>\n",
+        "      <td> 794534</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 24,
+       "text": [
+        "                 geometry   value1  value2  BoroCode       BoroName  \\\n",
+        "0   POINT (913175 120121)  1033296  793054       NaN           None   \n",
+        "1   POINT (932450 139211)  1071661  793239         5  Staten Island   \n",
+        "2   POINT (951725 158301)  1110026  793424         5  Staten Island   \n",
+        "3   POINT (971000 177391)  1148391  793609       NaN           None   \n",
+        "4   POINT (990275 196481)  1186756  793794       NaN           None   \n",
+        "5  POINT (1009550 215571)  1225121  793979         4         Queens   \n",
+        "6  POINT (1028825 234661)  1263486  794164         2          Bronx   \n",
+        "7  POINT (1048100 253751)  1301851  794349       NaN           None   \n",
+        "8  POINT (1067375 272841)  1340216  794534       NaN           None   \n",
+        "\n",
+        "     Shape_Area     Shape_Leng  \n",
+        "0           NaN            NaN  \n",
+        "1  1.623847e+09  330454.175933  \n",
+        "2  1.623847e+09  330454.175933  \n",
+        "3           NaN            NaN  \n",
+        "4           NaN            NaN  \n",
+        "5  3.045079e+09  896875.396449  \n",
+        "6  1.186822e+09  464475.145651  \n",
+        "7           NaN            NaN  \n",
+        "8           NaN            NaN  "
+       ]
+      }
+     ],
+     "prompt_number": 24
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "join_right_df = sjoin(pointdf, polydf, how=\"right\")\n",
+      "join_right_df\n",
+      "# Note Staten Island is repeated"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "      <th>geometry</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> (POLYGON ((970217.0223999023 145643.3322143555...</td>\n",
+        "      <td> 1071661</td>\n",
+        "      <td> 793239</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "      <td> (POLYGON ((970217.0223999023 145643.3322143555...</td>\n",
+        "      <td> 1110026</td>\n",
+        "      <td> 793424</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td> 3</td>\n",
+        "      <td>      Brooklyn</td>\n",
+        "      <td> 1.937810e+09</td>\n",
+        "      <td> 741227.337073</td>\n",
+        "      <td> (POLYGON ((1021176.479003906 151374.7969970703...</td>\n",
+        "      <td>     NaN</td>\n",
+        "      <td>    NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td> 4</td>\n",
+        "      <td>        Queens</td>\n",
+        "      <td> 3.045079e+09</td>\n",
+        "      <td> 896875.396449</td>\n",
+        "      <td> (POLYGON ((1029606.076599121 156073.8142089844...</td>\n",
+        "      <td> 1225121</td>\n",
+        "      <td> 793979</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td> 1</td>\n",
+        "      <td>     Manhattan</td>\n",
+        "      <td> 6.364308e+08</td>\n",
+        "      <td> 358400.912836</td>\n",
+        "      <td> (POLYGON ((981219.0557861328 188655.3157958984...</td>\n",
+        "      <td>     NaN</td>\n",
+        "      <td>    NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>5</th>\n",
+        "      <td> 2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "      <td> (POLYGON ((1012821.805786133 229228.2645874023...</td>\n",
+        "      <td> 1263486</td>\n",
+        "      <td> 794164</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 25,
+       "text": [
+        "   BoroCode       BoroName    Shape_Area     Shape_Leng  \\\n",
+        "0         5  Staten Island  1.623847e+09  330454.175933   \n",
+        "1         5  Staten Island  1.623847e+09  330454.175933   \n",
+        "2         3       Brooklyn  1.937810e+09  741227.337073   \n",
+        "3         4         Queens  3.045079e+09  896875.396449   \n",
+        "4         1      Manhattan  6.364308e+08  358400.912836   \n",
+        "5         2          Bronx  1.186822e+09  464475.145651   \n",
+        "\n",
+        "                                            geometry   value1  value2  \n",
+        "0  (POLYGON ((970217.0223999023 145643.3322143555...  1071661  793239  \n",
+        "1  (POLYGON ((970217.0223999023 145643.3322143555...  1110026  793424  \n",
+        "2  (POLYGON ((1021176.479003906 151374.7969970703...      NaN     NaN  \n",
+        "3  (POLYGON ((1029606.076599121 156073.8142089844...  1225121  793979  \n",
+        "4  (POLYGON ((981219.0557861328 188655.3157958984...      NaN     NaN  \n",
+        "5  (POLYGON ((1012821.805786133 229228.2645874023...  1263486  794164  "
+       ]
+      }
+     ],
+     "prompt_number": 25
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "join_inner_df = sjoin(pointdf, polydf, how=\"inner\")\n",
+      "join_inner_df\n",
+      "# Note the lack of NaNs; dropped anything that didn't intersect"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>geometry</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td>  POINT (932450 139211)</td>\n",
+        "      <td> 1071661</td>\n",
+        "      <td> 793239</td>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td>  POINT (951725 158301)</td>\n",
+        "      <td> 1110026</td>\n",
+        "      <td> 793424</td>\n",
+        "      <td> 5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td> POINT (1009550 215571)</td>\n",
+        "      <td> 1225121</td>\n",
+        "      <td> 793979</td>\n",
+        "      <td> 4</td>\n",
+        "      <td>        Queens</td>\n",
+        "      <td> 3.045079e+09</td>\n",
+        "      <td> 896875.396449</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td> POINT (1028825 234661)</td>\n",
+        "      <td> 1263486</td>\n",
+        "      <td> 794164</td>\n",
+        "      <td> 2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 27,
+       "text": [
+        "                 geometry   value1  value2  BoroCode       BoroName  \\\n",
+        "0   POINT (932450 139211)  1071661  793239         5  Staten Island   \n",
+        "1   POINT (951725 158301)  1110026  793424         5  Staten Island   \n",
+        "2  POINT (1009550 215571)  1225121  793979         4         Queens   \n",
+        "3  POINT (1028825 234661)  1263486  794164         2          Bronx   \n",
+        "\n",
+        "     Shape_Area     Shape_Leng  \n",
+        "0  1.623847e+09  330454.175933  \n",
+        "1  1.623847e+09  330454.175933  \n",
+        "2  3.045079e+09  896875.396449  \n",
+        "3  1.186822e+09  464475.145651  "
+       ]
+      }
+     ],
+     "prompt_number": 27
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "We're not limited to using the `intersection` binary predicate. Any of the `Shapely` geometry methods that return a Boolean can be used by specifying the `op` kwarg."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "sjoin(pointdf, polydf, how=\"left\", op=\"within\")"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "html": [
+        "<div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+        "<table border=\"1\" class=\"dataframe\">\n",
+        "  <thead>\n",
+        "    <tr style=\"text-align: right;\">\n",
+        "      <th></th>\n",
+        "      <th>geometry</th>\n",
+        "      <th>value1</th>\n",
+        "      <th>value2</th>\n",
+        "      <th>BoroCode</th>\n",
+        "      <th>BoroName</th>\n",
+        "      <th>Shape_Area</th>\n",
+        "      <th>Shape_Leng</th>\n",
+        "    </tr>\n",
+        "  </thead>\n",
+        "  <tbody>\n",
+        "    <tr>\n",
+        "      <th>0</th>\n",
+        "      <td>  POINT (913175 120121)</td>\n",
+        "      <td> 1033296</td>\n",
+        "      <td> 793054</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>1</th>\n",
+        "      <td>  POINT (932450 139211)</td>\n",
+        "      <td> 1071661</td>\n",
+        "      <td> 793239</td>\n",
+        "      <td>  5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>2</th>\n",
+        "      <td>  POINT (951725 158301)</td>\n",
+        "      <td> 1110026</td>\n",
+        "      <td> 793424</td>\n",
+        "      <td>  5</td>\n",
+        "      <td> Staten Island</td>\n",
+        "      <td> 1.623847e+09</td>\n",
+        "      <td> 330454.175933</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>3</th>\n",
+        "      <td>  POINT (971000 177391)</td>\n",
+        "      <td> 1148391</td>\n",
+        "      <td> 793609</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>4</th>\n",
+        "      <td>  POINT (990275 196481)</td>\n",
+        "      <td> 1186756</td>\n",
+        "      <td> 793794</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>5</th>\n",
+        "      <td> POINT (1009550 215571)</td>\n",
+        "      <td> 1225121</td>\n",
+        "      <td> 793979</td>\n",
+        "      <td>  4</td>\n",
+        "      <td>        Queens</td>\n",
+        "      <td> 3.045079e+09</td>\n",
+        "      <td> 896875.396449</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>6</th>\n",
+        "      <td> POINT (1028825 234661)</td>\n",
+        "      <td> 1263486</td>\n",
+        "      <td> 794164</td>\n",
+        "      <td>  2</td>\n",
+        "      <td>         Bronx</td>\n",
+        "      <td> 1.186822e+09</td>\n",
+        "      <td> 464475.145651</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>7</th>\n",
+        "      <td> POINT (1048100 253751)</td>\n",
+        "      <td> 1301851</td>\n",
+        "      <td> 794349</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "    <tr>\n",
+        "      <th>8</th>\n",
+        "      <td> POINT (1067375 272841)</td>\n",
+        "      <td> 1340216</td>\n",
+        "      <td> 794534</td>\n",
+        "      <td>NaN</td>\n",
+        "      <td>          None</td>\n",
+        "      <td>          NaN</td>\n",
+        "      <td>           NaN</td>\n",
+        "    </tr>\n",
+        "  </tbody>\n",
+        "</table>\n",
+        "</div>"
+       ],
+       "metadata": {},
+       "output_type": "pyout",
+       "prompt_number": 32,
+       "text": [
+        "                 geometry   value1  value2  BoroCode       BoroName  \\\n",
+        "0   POINT (913175 120121)  1033296  793054       NaN           None   \n",
+        "1   POINT (932450 139211)  1071661  793239         5  Staten Island   \n",
+        "2   POINT (951725 158301)  1110026  793424         5  Staten Island   \n",
+        "3   POINT (971000 177391)  1148391  793609       NaN           None   \n",
+        "4   POINT (990275 196481)  1186756  793794       NaN           None   \n",
+        "5  POINT (1009550 215571)  1225121  793979         4         Queens   \n",
+        "6  POINT (1028825 234661)  1263486  794164         2          Bronx   \n",
+        "7  POINT (1048100 253751)  1301851  794349       NaN           None   \n",
+        "8  POINT (1067375 272841)  1340216  794534       NaN           None   \n",
+        "\n",
+        "     Shape_Area     Shape_Leng  \n",
+        "0           NaN            NaN  \n",
+        "1  1.623847e+09  330454.175933  \n",
+        "2  1.623847e+09  330454.175933  \n",
+        "3           NaN            NaN  \n",
+        "4           NaN            NaN  \n",
+        "5  3.045079e+09  896875.396449  \n",
+        "6  1.186822e+09  464475.145651  \n",
+        "7           NaN            NaN  \n",
+        "8           NaN            NaN  "
+       ]
+      }
+     ],
+     "prompt_number": 32
+    }
+   ],
+   "metadata": {}
+  }
+ ]
+}
\ No newline at end of file
diff --git a/geopandas/__init__.py b/geopandas/__init__.py
index 4c0d5f8..77eb04b 100644
--- a/geopandas/__init__.py
+++ b/geopandas/__init__.py
@@ -1,16 +1,19 @@
-try:
-    from geopandas.version import version as __version__
-except ImportError:
-    __version__ = '0.1.1'
-
 from geopandas.geoseries import GeoSeries
 from geopandas.geodataframe import GeoDataFrame
 
 from geopandas.io.file import read_file
 from geopandas.io.sql import read_postgis
+from geopandas.tools import sjoin
+from geopandas.tools import overlay
+
+import geopandas.datasets
 
 # make the interactive namespace easier to use
 # for `from geopandas import *` demos.
 import geopandas as gpd
 import pandas as pd
 import numpy as np
+
+from ._version import get_versions
+__version__ = get_versions()['version']
+del get_versions
diff --git a/geopandas/_version.py b/geopandas/_version.py
new file mode 100644
index 0000000..9b20bc0
--- /dev/null
+++ b/geopandas/_version.py
@@ -0,0 +1,484 @@
+
+# This file helps to compute a version number in source trees obtained from
+# git-archive tarball (such as those provided by githubs download-from-tag
+# feature). Distribution tarballs (built by setup.py sdist) and build
+# directories (produced by setup.py build) will contain a much shorter file
+# that just contains the computed version number.
+
+# This file is released into the public domain. Generated by
+# versioneer-0.16 (https://github.com/warner/python-versioneer)
+
+"""Git implementation of _version.py."""
+
+import errno
+import os
+import re
+import subprocess
+import sys
+
+
+def get_keywords():
+    """Get the keywords needed to look up the version information."""
+    # these strings will be replaced by git during git-archive.
+    # setup.py/versioneer.py will grep for the variable names, so they must
+    # each be defined on a line of their own. _version.py will just call
+    # get_keywords().
+    git_refnames = " (HEAD -> master, tag: v0.2)"
+    git_full = "99a0c31270906864d8f86d423aa4a8bde00b1fd5"
+    keywords = {"refnames": git_refnames, "full": git_full}
+    return keywords
+
+
+class VersioneerConfig:
+    """Container for Versioneer configuration parameters."""
+
+
+def get_config():
+    """Create, populate and return the VersioneerConfig() object."""
+    # these strings are filled in when 'setup.py versioneer' creates
+    # _version.py
+    cfg = VersioneerConfig()
+    cfg.VCS = "git"
+    cfg.style = "pep440"
+    cfg.tag_prefix = "v"
+    cfg.parentdir_prefix = "geopandas-"
+    cfg.versionfile_source = "geopandas/_version.py"
+    cfg.verbose = False
+    return cfg
+
+
+class NotThisMethod(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method):  # decorator
+    """Decorator to mark a method as the handler for a particular VCS."""
+    def decorate(f):
+        """Store f in HANDLERS[vcs][method]."""
+        if vcs not in HANDLERS:
+            HANDLERS[vcs] = {}
+        HANDLERS[vcs][method] = f
+        return f
+    return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
+    """Call the given command(s)."""
+    assert isinstance(commands, list)
+    p = None
+    for c in commands:
+        try:
+            dispcmd = str([c] + args)
+            # remember shell=False, so use git.cmd on windows, not just git
+            p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
+                                 stderr=(subprocess.PIPE if hide_stderr
+                                         else None))
+            break
+        except EnvironmentError:
+            e = sys.exc_info()[1]
+            if e.errno == errno.ENOENT:
+                continue
+            if verbose:
+                print("unable to run %s" % dispcmd)
+                print(e)
+            return None
+    else:
+        if verbose:
+            print("unable to find command, tried %s" % (commands,))
+        return None
+    stdout = p.communicate()[0].strip()
+    if sys.version_info[0] >= 3:
+        stdout = stdout.decode()
+    if p.returncode != 0:
+        if verbose:
+            print("unable to run %s (error)" % dispcmd)
+        return None
+    return stdout
+
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+    """Try to determine the version from the parent directory name.
+
+    Source tarballs conventionally unpack into a directory that includes
+    both the project name and a version string.
+    """
+    dirname = os.path.basename(root)
+    if not dirname.startswith(parentdir_prefix):
+        if verbose:
+            print("guessing rootdir is '%s', but '%s' doesn't start with "
+                  "prefix '%s'" % (root, dirname, parentdir_prefix))
+        raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+    return {"version": dirname[len(parentdir_prefix):],
+            "full-revisionid": None,
+            "dirty": False, "error": None}
+
+
+ at register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+    """Extract version information from the given file."""
+    # the code embedded in _version.py can just fetch the value of these
+    # keywords. When used from setup.py, we don't want to import _version.py,
+    # so we do it with a regexp instead. This function is not used from
+    # _version.py.
+    keywords = {}
+    try:
+        f = open(versionfile_abs, "r")
+        for line in f.readlines():
+            if line.strip().startswith("git_refnames ="):
+                mo = re.search(r'=\s*"(.*)"', line)
+                if mo:
+                    keywords["refnames"] = mo.group(1)
+            if line.strip().startswith("git_full ="):
+                mo = re.search(r'=\s*"(.*)"', line)
+                if mo:
+                    keywords["full"] = mo.group(1)
+        f.close()
+    except EnvironmentError:
+        pass
+    return keywords
+
+
+ at register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+    """Get version information from git keywords."""
+    if not keywords:
+        raise NotThisMethod("no keywords at all, weird")
+    refnames = keywords["refnames"].strip()
+    if refnames.startswith("$Format"):
+        if verbose:
+            print("keywords are unexpanded, not using")
+        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+    refs = set([r.strip() for r in refnames.strip("()").split(",")])
+    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+    TAG = "tag: "
+    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
+    if not tags:
+        # Either we're using git < 1.8.3, or there really are no tags. We use
+        # a heuristic: assume all version tags have a digit. The old git %d
+        # expansion behaves like git log --decorate=short and strips out the
+        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+        # between branches and tags. By ignoring refnames without digits, we
+        # filter out many common branch names like "release" and
+        # "stabilization", as well as "HEAD" and "master".
+        tags = set([r for r in refs if re.search(r'\d', r)])
+        if verbose:
+            print("discarding '%s', no digits" % ",".join(refs-tags))
+    if verbose:
+        print("likely tags: %s" % ",".join(sorted(tags)))
+    for ref in sorted(tags):
+        # sorting will prefer e.g. "2.0" over "2.0rc1"
+        if ref.startswith(tag_prefix):
+            r = ref[len(tag_prefix):]
+            if verbose:
+                print("picking %s" % r)
+            return {"version": r,
+                    "full-revisionid": keywords["full"].strip(),
+                    "dirty": False, "error": None
+                    }
+    # no suitable tags, so version is "0+unknown", but full hex is still there
+    if verbose:
+        print("no suitable tags, using unknown + full revision id")
+    return {"version": "0+unknown",
+            "full-revisionid": keywords["full"].strip(),
+            "dirty": False, "error": "no suitable tags"}
+
+
+ at register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+    """Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if the git-archive 'subst' keywords were *not*
+    expanded, and _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+    if not os.path.exists(os.path.join(root, ".git")):
+        if verbose:
+            print("no .git in %s" % root)
+        raise NotThisMethod("no .git directory")
+
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+    # if there isn't one, this yields HEX[-dirty] (no NUM)
+    describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+                                      "--always", "--long",
+                                      "--match", "%s*" % tag_prefix],
+                               cwd=root)
+    # --long was added in git-1.5.5
+    if describe_out is None:
+        raise NotThisMethod("'git describe' failed")
+    describe_out = describe_out.strip()
+    full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+    if full_out is None:
+        raise NotThisMethod("'git rev-parse' failed")
+    full_out = full_out.strip()
+
+    pieces = {}
+    pieces["long"] = full_out
+    pieces["short"] = full_out[:7]  # maybe improved later
+    pieces["error"] = None
+
+    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+    # TAG might have hyphens.
+    git_describe = describe_out
+
+    # look for -dirty suffix
+    dirty = git_describe.endswith("-dirty")
+    pieces["dirty"] = dirty
+    if dirty:
+        git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+    # now we have TAG-NUM-gHEX or HEX
+
+    if "-" in git_describe:
+        # TAG-NUM-gHEX
+        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+        if not mo:
+            # unparseable. Maybe git-describe is misbehaving?
+            pieces["error"] = ("unable to parse git-describe output: '%s'"
+                               % describe_out)
+            return pieces
+
+        # tag
+        full_tag = mo.group(1)
+        if not full_tag.startswith(tag_prefix):
+            if verbose:
+                fmt = "tag '%s' doesn't start with prefix '%s'"
+                print(fmt % (full_tag, tag_prefix))
+            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+                               % (full_tag, tag_prefix))
+            return pieces
+        pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+        # distance: number of commits since tag
+        pieces["distance"] = int(mo.group(2))
+
+        # commit: short hex revision ID
+        pieces["short"] = mo.group(3)
+
+    else:
+        # HEX: no tags
+        pieces["closest-tag"] = None
+        count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+                                cwd=root)
+        pieces["distance"] = int(count_out)  # total number of commits
+
+    return pieces
+
+
+def plus_or_dot(pieces):
+    """Return a + if we don't already have one, else return a ."""
+    if "+" in pieces.get("closest-tag", ""):
+        return "."
+    return "+"
+
+
+def render_pep440(pieces):
+    """Build up version string, with post-release "local version identifier".
+
+    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+    Exceptions:
+    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += plus_or_dot(pieces)
+            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_pre(pieces):
+    """TAG[.post.devDISTANCE] -- No -dirty.
+
+    Exceptions:
+    1: no tags. 0.post.devDISTANCE
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += ".post.dev%d" % pieces["distance"]
+    else:
+        # exception #1
+        rendered = "0.post.dev%d" % pieces["distance"]
+    return rendered
+
+
+def render_pep440_post(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+    The ".dev0" means dirty. Note that .dev0 sorts backwards
+    (a dirty tree will appear "older" than the corresponding clean one),
+    but you shouldn't be releasing software with -dirty anyways.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%s" % pieces["short"]
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+        rendered += "+g%s" % pieces["short"]
+    return rendered
+
+
+def render_pep440_old(pieces):
+    """TAG[.postDISTANCE[.dev0]] .
+
+    The ".dev0" means dirty.
+
+    Eexceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+    return rendered
+
+
+def render_git_describe(pieces):
+    """TAG[-DISTANCE-gHEX][-dirty].
+
+    Like 'git describe --tags --dirty --always'.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render_git_describe_long(pieces):
+    """TAG-DISTANCE-gHEX[-dirty].
+
+    Like 'git describe --tags --dirty --always -long'.
+    The distance/hash is unconditional.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render(pieces, style):
+    """Render the given version pieces into the requested style."""
+    if pieces["error"]:
+        return {"version": "unknown",
+                "full-revisionid": pieces.get("long"),
+                "dirty": None,
+                "error": pieces["error"]}
+
+    if not style or style == "default":
+        style = "pep440"  # the default
+
+    if style == "pep440":
+        rendered = render_pep440(pieces)
+    elif style == "pep440-pre":
+        rendered = render_pep440_pre(pieces)
+    elif style == "pep440-post":
+        rendered = render_pep440_post(pieces)
+    elif style == "pep440-old":
+        rendered = render_pep440_old(pieces)
+    elif style == "git-describe":
+        rendered = render_git_describe(pieces)
+    elif style == "git-describe-long":
+        rendered = render_git_describe_long(pieces)
+    else:
+        raise ValueError("unknown style '%s'" % style)
+
+    return {"version": rendered, "full-revisionid": pieces["long"],
+            "dirty": pieces["dirty"], "error": None}
+
+
+def get_versions():
+    """Get version information or return default if unable to do so."""
+    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+    # __file__, we can work backwards from there to the root. Some
+    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
+    # case we can only use expanded keywords.
+
+    cfg = get_config()
+    verbose = cfg.verbose
+
+    try:
+        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+                                          verbose)
+    except NotThisMethod:
+        pass
+
+    try:
+        root = os.path.realpath(__file__)
+        # versionfile_source is the relative path from the top of the source
+        # tree (where the .git directory might live) to this file. Invert
+        # this to find the root from __file__.
+        for i in cfg.versionfile_source.split('/'):
+            root = os.path.dirname(root)
+    except NameError:
+        return {"version": "0+unknown", "full-revisionid": None,
+                "dirty": None,
+                "error": "unable to find root of source tree"}
+
+    try:
+        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+        return render(pieces, cfg.style)
+    except NotThisMethod:
+        pass
+
+    try:
+        if cfg.parentdir_prefix:
+            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+    except NotThisMethod:
+        pass
+
+    return {"version": "0+unknown", "full-revisionid": None,
+            "dirty": None,
+            "error": "unable to compute version"}
diff --git a/geopandas/base.py b/geopandas/base.py
index cc27e3e..ec475ce 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -6,10 +6,19 @@ from shapely.ops import cascaded_union, unary_union
 import shapely.affinity as affinity
 
 import numpy as np
-from pandas import Series, DataFrame
+import pandas as pd
+from pandas import Series, DataFrame, MultiIndex
 
 import geopandas as gpd
 
+try:
+    from rtree.core import RTreeError
+    HAS_SINDEX = True
+except ImportError:
+    class RTreeError(Exception):
+        pass
+    HAS_SINDEX = False
+
 
 def _geo_op(this, other, op):
     """Operation that returns a GeoSeries"""
@@ -32,14 +41,17 @@ def _geo_op(this, other, op):
 # TODO: think about merging with _geo_op
 def _series_op(this, other, op, **kwargs):
     """Geometric operation that returns a pandas Series"""
+    null_val = False if op != 'distance' else np.nan
+
     if isinstance(other, GeoPandasBase):
         this = this.geometry
         this, other = this.align(other.geometry)
         return Series([getattr(this_elem, op)(other_elem, **kwargs)
-                      for this_elem, other_elem in zip(this, other)],
-                      index=this.index)
+                    if not this_elem.is_empty | other_elem.is_empty else null_val
+                    for this_elem, other_elem in zip(this, other)],
+                    index=this.index)
     else:
-        return Series([getattr(s, op)(other, **kwargs)
+        return Series([getattr(s, op)(other, **kwargs) if s else null_val
                       for s in this.geometry], index=this.index)
 
 def _geo_unary_op(this, op):
@@ -47,22 +59,49 @@ def _geo_unary_op(this, op):
     return gpd.GeoSeries([getattr(geom, op) for geom in this.geometry],
                      index=this.index, crs=this.crs)
 
-def _series_unary_op(this, op):
+def _series_unary_op(this, op, null_value=False):
     """Unary operation that returns a Series"""
-    return Series([getattr(geom, op) for geom in this.geometry],
+    return Series([getattr(geom, op, null_value) for geom in this.geometry],
                      index=this.index)
 
 
 class GeoPandasBase(object):
+
+    def _generate_sindex(self):
+        self._sindex = None
+        if not HAS_SINDEX:
+            warn("Cannot generate spatial index: Missing package `rtree`.")
+        else:
+            from geopandas.sindex import SpatialIndex
+            stream = ((i, item.bounds, idx) for i, (idx, item) in
+                   enumerate(self.geometry.iteritems()) if
+                   pd.notnull(item) and not item.is_empty)
+            try:
+                self._sindex = SpatialIndex(stream)
+            # What we really want here is an empty generator error, or
+            # for the bulk loader to log that the generator was empty
+            # and move on. See https://github.com/Toblerity/rtree/issues/20.
+            except RTreeError:
+                pass
+
+    def _invalidate_sindex(self):
+        """
+        Indicates that the spatial index should be re-built next
+        time it's requested.
+
+        """
+        self._sindex = None
+        self._sindex_valid = False
+
     @property
     def area(self):
         """Return the area of each geometry in the GeoSeries"""
-        return _series_unary_op(self, 'area')
+        return _series_unary_op(self, 'area', null_value=np.nan)
 
     @property
     def geom_type(self):
         """Return the geometry type of each geometry in the GeoSeries"""
-        return _series_unary_op(self, 'geom_type')
+        return _series_unary_op(self, 'geom_type', null_value=None)
 
     @property
     def type(self):
@@ -72,22 +111,22 @@ class GeoPandasBase(object):
     @property
     def length(self):
         """Return the length of each geometry in the GeoSeries"""
-        return _series_unary_op(self, 'length')
+        return _series_unary_op(self, 'length', null_value=np.nan)
 
     @property
     def is_valid(self):
         """Return True for each valid geometry, else False"""
-        return _series_unary_op(self, 'is_valid')
+        return _series_unary_op(self, 'is_valid', null_value=False)
 
     @property
     def is_empty(self):
         """Return True for each empty geometry, False for non-empty"""
-        return _series_unary_op(self, 'is_empty')
+        return _series_unary_op(self, 'is_empty', null_value=False)
 
     @property
     def is_simple(self):
         """Return True for each simple geometry, else False"""
-        return _series_unary_op(self, 'is_simple')
+        return _series_unary_op(self, 'is_simple', null_value=False)
 
     @property
     def is_ring(self):
@@ -130,7 +169,7 @@ class GeoPandasBase(object):
     def interiors(self):
         """Return the interior rings of each polygon"""
         # TODO: return empty list or None for non-polygons
-        return _geo_unary_op(self, 'interiors')
+        return _series_unary_op(self, 'interiors', null_value=False)
 
     def representative_point(self):
         """Return a GeoSeries of points guaranteed to be in each geometry"""
@@ -150,7 +189,7 @@ class GeoPandasBase(object):
     @property
     def unary_union(self):
         """Return the union of all geometries"""
-        return unary_union(self.values)
+        return unary_union(self.geometry.values)
 
     #
     # Binary operations that return a pandas Series
@@ -233,7 +272,7 @@ class GeoPandasBase(object):
         return DataFrame(bounds,
                          columns=['minx', 'miny', 'maxx', 'maxy'],
                          index=self.index)
-                         
+
     @property
     def total_bounds(self):
         """Return a single bounding box (minx, miny, maxx, maxy) for all geometries
@@ -247,8 +286,15 @@ class GeoPandasBase(object):
                 b['maxx'].max(),
                 b['maxy'].max())
 
+    @property
+    def sindex(self):
+        if not self._sindex_valid:
+            self._generate_sindex()
+            self._sindex_valid = True
+        return self._sindex
+
     def buffer(self, distance, resolution=16):
-        return gpd.GeoSeries([geom.buffer(distance, resolution) 
+        return gpd.GeoSeries([geom.buffer(distance, resolution)
                              for geom in self.geometry],
                          index=self.index, crs=self.crs)
 
@@ -263,7 +309,7 @@ class GeoPandasBase(object):
     def project(self, other, normalized=False):
         """
         Return the distance along each geometry nearest to *other*
-        
+
         Parameters
         ----------
         other : BaseGeometry or GeoSeries
@@ -271,29 +317,29 @@ class GeoPandasBase(object):
         normalized : boolean
             If normalized is True, return the distance normalized to
             the length of the object.
-        
+
         The project method is the inverse of interpolate.
         """
-        
+
         return _series_op(self, other, 'project', normalized=normalized)
 
     def interpolate(self, distance, normalized=False):
         """
         Return a point at the specified distance along each geometry
-        
+
         Parameters
         ----------
         distance : float or Series of floats
             Distance(s) along the geometries at which a point should be returned
         normalized : boolean
-            If normalized is True, distance will be interpreted as a fraction 
+            If normalized is True, distance will be interpreted as a fraction
             of the geometric object's length.
         """
-        
-        return gpd.GeoSeries([s.interpolate(distance, normalized) 
+
+        return gpd.GeoSeries([s.interpolate(distance, normalized)
                              for s in self.geometry],
             index=self.index, crs=self.crs)
-        
+
     def translate(self, xoff=0.0, yoff=0.0, zoff=0.0):
         """
         Shift the coordinates of the GeoSeries.
@@ -302,39 +348,39 @@ class GeoPandasBase(object):
         ----------
         xoff, yoff, zoff : float, float, float
             Amount of offset along each dimension.
-            xoff, yoff, and zoff for translation along the x, y, and z 
+            xoff, yoff, and zoff for translation along the x, y, and z
             dimensions respectively.
 
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
 
-        return gpd.GeoSeries([affinity.translate(s, xoff, yoff, zoff) 
-                             for s in self.geometry], 
+        return gpd.GeoSeries([affinity.translate(s, xoff, yoff, zoff)
+                             for s in self.geometry],
             index=self.index, crs=self.crs)
 
     def rotate(self, angle, origin='center', use_radians=False):
         """
         Rotate the coordinates of the GeoSeries.
-        
+
         Parameters
         ----------
         angle : float
-            The angle of rotation can be specified in either degrees (default) 
-            or radians by setting use_radians=True. Positive angles are 
+            The angle of rotation can be specified in either degrees (default)
+            or radians by setting use_radians=True. Positive angles are
             counter-clockwise and negative are clockwise rotations.
         origin : string, Point, or tuple (x, y)
-            The point of origin can be a keyword 'center' for the bounding box 
-            center (default), 'centroid' for the geometry's centroid, a Point 
+            The point of origin can be a keyword 'center' for the bounding box
+            center (default), 'centroid' for the geometry's centroid, a Point
             object or a coordinate tuple (x, y).
         use_radians : boolean
             Whether to interpret the angle of rotation as degrees or radians
-            
+
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
 
-        return gpd.GeoSeries([affinity.rotate(s, angle, origin=origin, 
+        return gpd.GeoSeries([affinity.rotate(s, angle, origin=origin,
             use_radians=use_radians) for s in self.geometry],
             index=self.index, crs=self.crs)
 
@@ -347,8 +393,8 @@ class GeoPandasBase(object):
         xfact, yfact, zfact : float, float, float
             Scaling factors for the x, y, and z dimensions respectively.
         origin : string, Point, or tuple
-            The point of origin can be a keyword 'center' for the 2D bounding 
-            box center (default), 'centroid' for the geometry's 2D centroid, a 
+            The point of origin can be a keyword 'center' for the 2D bounding
+            box center (default), 'centroid' for the geometry's 2D centroid, a
             Point object or a coordinate tuple (x, y, z).
 
         Note: Negative scale factors will mirror or reflect coordinates.
@@ -357,35 +403,76 @@ class GeoPandasBase(object):
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
 
-        return gpd.GeoSeries([affinity.scale(s, xfact, yfact, zfact, 
-            origin=origin) for s in self.geometry], index=self.index, 
+        return gpd.GeoSeries([affinity.scale(s, xfact, yfact, zfact,
+            origin=origin) for s in self.geometry], index=self.index,
             crs=self.crs)
-                           
+
     def skew(self, xs=0.0, ys=0.0, origin='center', use_radians=False):
         """
         Shear/Skew the geometries of the GeoSeries by angles along x and y dimensions.
-        
+
         Parameters
         ----------
         xs, ys : float, float
-            The shear angle(s) for the x and y axes respectively. These can be 
-            specified in either degrees (default) or radians by setting 
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
             use_radians=True.
         origin : string, Point, or tuple (x, y)
-            The point of origin can be a keyword 'center' for the bounding box 
-            center (default), 'centroid' for the geometry's centroid, a Point 
+            The point of origin can be a keyword 'center' for the bounding box
+            center (default), 'centroid' for the geometry's centroid, a Point
             object or a coordinate tuple (x, y).
         use_radians : boolean
             Whether to interpret the shear angle(s) as degrees or radians
-            
+
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
-        
-        return gpd.GeoSeries([affinity.skew(s, xs, ys, origin=origin, 
+
+        return gpd.GeoSeries([affinity.skew(s, xs, ys, origin=origin,
             use_radians=use_radians) for s in self.geometry],
             index=self.index, crs=self.crs)
 
+    def explode(self):
+        """
+        Explode multi-part geometries into multiple single geometries.
+
+        Single rows can become multiple rows.
+        This is analogous to PostGIS's ST_Dump(). The 'path' index is the
+        second level of the returned MultiIndex
+
+        Returns
+        ------
+        A GeoSeries with a MultiIndex. The levels of the MultiIndex are the
+        original index and an integer.
+
+        Example
+        -------
+        >>> gdf  # gdf is GeoSeries of MultiPoints
+        0                 (POINT (0 0), POINT (1 1))
+        1    (POINT (2 2), POINT (3 3), POINT (4 4))
+
+        >>> gdf.explode()
+        0  0    POINT (0 0)
+           1    POINT (1 1)
+        1  0    POINT (2 2)
+           1    POINT (3 3)
+           2    POINT (4 4)
+        dtype: object
+
+        """
+        index = []
+        geometries = []
+        for idx, s in self.geometry.iteritems():
+            if s.type.startswith('Multi') or s.type == 'GeometryCollection':
+                geoms = s.geoms
+                idxs = [(idx, i) for i in range(len(geoms))]
+            else:
+                geoms = [s]
+                idxs = [(idx, 0)]
+            index.extend(idxs)
+            geometries.extend(geoms)
+        return gpd.GeoSeries(geometries,
+            index=MultiIndex.from_tuples(index)).__finalize__(self)
 
 def _array_input(arr):
     if isinstance(arr, (MultiPoint, MultiLineString, MultiPolygon)):
@@ -396,5 +483,3 @@ def _array_input(arr):
         arr[0] = geom
 
     return arr
-
-
diff --git a/geopandas/datasets/__init__.py b/geopandas/datasets/__init__.py
new file mode 100644
index 0000000..35b24e0
--- /dev/null
+++ b/geopandas/datasets/__init__.py
@@ -0,0 +1,27 @@
+import os
+
+
+__all__ = ['available', 'get_path']
+
+module_path = os.path.dirname(__file__)
+available = [p for p in next(os.walk(module_path))[1]
+             if not p.startswith('__')]
+
+
+def get_path(dataset):
+    """
+    Get the path to the data file.
+
+    Parameters
+    ----------
+    dataset : str
+        The name of the dataset. See ``geopandas.datasets.available`` for
+        all options.
+
+    """
+    if dataset in available:
+        return os.path.abspath(
+            os.path.join(module_path, dataset, dataset + '.shp'))
+    else:
+        msg = "The dataset '{data}' is not available".format(data=dataset)
+        raise ValueError(msg)
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.README.html b/geopandas/datasets/naturalearth_cities/naturalearth_cities.README.html
new file mode 100644
index 0000000..2f5786f
--- /dev/null
+++ b/geopandas/datasets/naturalearth_cities/naturalearth_cities.README.html
@@ -0,0 +1,336 @@
+
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
+
+<head profile="http://gmpg.org/xfn/11">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
+<title>Populated Places | Natural Earth</title>
+
+<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
+<link rel="alternate" type="application/rss+xml" title="Natural Earth RSS Feed" href="http://www.naturalearthdata.com/feed/" />
+<link rel="pingback" href="http://www.naturalearthdata.com/xmlrpc.php" />
+<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/themes/NEV/includes/js/suckerfish.js"></script>
+<!--[if lt IE 7]>
+    <script src="http://ie7-js.googlecode.com/svn/version/2.0(beta3)/IE7.js" type="text/javascript"></script>
+    <script defer="defer" type="text/javascript" src="http://www.naturalearthdata.com/wp-content/themes/NEV/includes/js/pngfix.js"></script>
+<![endif]--> 
+<link rel="stylesheet" href="http://www.naturalearthdata.com/wp-content/themes/NEV/style.css" type="text/css" media="screen" />
+
+<meta name='Admin Management Xtended WordPress plugin' content='2.1.1' />
+<link rel="alternate" type="application/rss+xml" title="Natural Earth » Populated Places Comments Feed" href="http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-populated-places/feed/" />
+<link rel='stylesheet' id='sociable-front-css-css'  href='http://www.naturalearthdata.com/wp-content/plugins/sociable/sociable.css?ver=2.9.2' type='text/css' media='' />
+<link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://www.naturalearthdata.com/xmlrpc.php?rsd" />
+<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://www.naturalearthdata.com/wp-includes/wlwmanifest.xml" /> 
+<link rel='index' title='Natural Earth' href='http://www.naturalearthdata.com' />
+<link rel='start' title='Welcome to the Natural Earth Blog' href='http://www.naturalearthdata.com/blog/miscellaneous/test/' />
+<link rel='prev' title='Antarctic Ice Shelves' href='http://www.naturalearthdata.com/downloads/10m-physical-vectors/10m-antarctic-ice-shelves/' />
+<link rel='next' title='Admin 1 – States, Provinces' href='http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-admin-1-states-provinces/' />
+<meta name="generator" content="WordPress 2.9.2" />
+
+<!-- All in One SEO Pack 1.6.10.2 by Michael Torbert of Semper Fi Web Design[309,448] -->
+<meta name="description" content="City and town points, from Tokyo to Wasilla, Cairo to Kandahar About Point symbols with name attributes. Includes all admin-0 and many" />
+<link rel="canonical" href="http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-populated-places/" />
+<!-- /all in one seo pack -->
+
+	<!-- begin gallery scripts -->
+    <link rel="stylesheet" href="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/css/jd.gallery.css.php" type="text/css" media="screen" charset="utf-8"/>
+	<link rel="stylesheet" href="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/css/jd.gallery.css" type="text/css" media="screen" charset="utf-8"/>
+	<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/scripts/mootools.v1.11.js"></script>
+	<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/scripts/jd.gallery.js.php"></script>
+	<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/scripts/jd.gallery.transitions.js"></script>
+	<!-- end gallery scripts -->
+<style type="text/css">.broken_link, a.broken_link {
+	text-decoration: line-through;
+}</style><link href="http://www.naturalearthdata.com/wp-content/themes/NEV/css/default.css" rel="stylesheet" type="text/css" />
+	<style type="text/css">.recentcomments a{display:inline !important;padding:0 !important;margin:0 !important;}</style>
+<!--[if lte IE 7]>
+<link rel="stylesheet" type="text/css" href="http://www.naturalearthdata.com/wp-content/themes/NEV/ie.css" />
+<![endif]-->
+<script src="http://www.naturalearthdata.com/wp-content/themes/NEV/js/jquery-1.2.6.min.js" type="text/javascript" charset="utf-8"></script>
+<script>
+     jQuery.noConflict();
+</script>
+<script type="text/javascript" charset="utf-8">
+	$(function(){
+		var tabContainers = $('div#maintabdiv > div');
+		tabContainers.hide().filter('#comments').show();
+		
+		$('div#maintabdiv ul#tabnav a').click(function () {
+				tabContainers.hide();
+				tabContainers.filter(this.hash).show();
+				$('div#maintabdiv ul#tabnav a').removeClass('current');
+				$(this).addClass('current');
+				return false;
+			}).filter('#comments').click();
+		
+		
+	});
+</script>
+
+		<script type="text/javascript" language="javascript" src="http://www.naturalearthdata.com/dataTables/media/js/jquery.dataTables.js"></script>
+		<script type="text/javascript" charset="utf-8">
+			$(document).ready(function() {
+				$('#ne_table').dataTable();
+			} );
+		</script>
+
+</head>
+<body>
+<div id="page">
+<div id="header">
+	<div id="headerimg">		
+        <h1><a href="http://www.naturalearthdata.com/"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/nev_logo.png" alt="Natural Earth title="Natural Earth" /></a></h1> 
+        <div class="description">Free vector and raster map data at 1:10m, 1:50m, and 1:110m scales</div> 
+        <div class="header_search"><form method="get" id="searchform" action="http://www.naturalearthdata.com/">
+<label class="hidden" for="s">Search for:</label>
+<div><input type="text" value="" name="s" id="s" />
+<input type="submit" id="searchsubmit" value="Search" />
+</div>
+</form>
+</div>
+<!--<div class="translate_panel" style="align:top; margin-left:650px; top:50px;">
+<div id="google_translate_element" style="float:left;"></div>
+<script>
+function googleTranslateElementInit() {
+ new google.translate.TranslateElement({
+   pageLanguage: 'en'
+ }, 'google_translate_element');
+}
+</script>
+<script src="http://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
+</div>-->
+	</div>
+    
+</div>
+
+<div id="pagemenu" style="align:bottom;">
+    <ul id="page-list" class="clearfix"><li class="page_item page-item-4"><a href="http://www.naturalearthdata.com" title="Home">Home</a></li>
+<li class="page_item page-item-10"><a href="http://www.naturalearthdata.com/features/" title="Features">Features</a></li>
+<li class="page_item page-item-12"><a href="http://www.naturalearthdata.com/downloads/" title="Downloads">Downloads</a></li>
+<li class="page_item page-item-6 current_page_parent"><a href="http://www.naturalearthdata.com/blog/" title="Blog">Blog</a></li>
+<li class="page_item page-item-14"><a href="http://www.naturalearthdata.com/forums" title="Forums">Forums</a></li>
+<li class="page_item page-item-366"><a href="http://www.naturalearthdata.com/corrections" title="Corrections">Corrections</a></li>
+<li class="page_item page-item-16"><a href="http://www.naturalearthdata.com/about/" title="About">About</a></li>
+</ul>    
+</div>
+
+<hr />	<div id="main">
+	<div id="content" class="narrowcolumn">
+
+				
+									« <a href="http://www.naturalearthdata.com/downloads/10m-cultural-vectors/">1:10m Cultural Vectors</a> 
+						   <div class="post" id="post-472">
+       		<h2>Populated Places</h2>
+			<div class="entry">
+				<div class="downloadPromoBlock" style="float: left;">
+<div style="float: left; width: 170px;"><img class="alignnone size-full wp-image-1918" title="pop_thumb" src="http://www.naturalearthdata.com/wp-content/uploads/2009/09/pop_thumb.png" alt="pop_thumb" width="150" height="97" /></div>
+<div style="float: left; width: 410px;"><em>City and town points, from Tokyo to Wasilla, Cairo to Kandahar</em>
+<div class="download-link-div">
+	<a class="download-link" rel="nofollow" title="Downloaded 26754 times (Shapefile, geoDB, or TIFF format)" onclick="if (window.urchinTracker) urchinTracker ('http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places.zip');" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_10m_po [...]
+</div> <div class="download-link-div">
+	<a class="download-link" rel="nofollow" title="Downloaded 2515 times (Shapefile, geoDB, or TIFF format)" onclick="if (window.urchinTracker) urchinTracker ('http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places_simple.zip');" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places_simple.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultu [...]
+</div>
+<span id="more-472"></span></div>
+</div>
+<div class="downloadMainBlock" style="float: left;">
+<p><strong>About</strong></p>
+<p>Point symbols with name attributes. Includes all admin-0 and many admin-1 capitals, major cities and towns, plus a sampling of smaller towns in sparsely inhabited regions. We favor regional significance over population census in determining our selection of places. Use the scale rankings to filter the number of towns that appear on your map.</p>
+<p><img class="alignnone size-full wp-image-1920" title="pop_banner" src="http://www.naturalearthdata.com/wp-content/uploads/2009/09/pop_banner.png" alt="pop_banner" width="580" height="150" /></p>
+<p><a href="http://www.ornl.gov/sci/landscan/" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.ornl.gov/sci/landscan/');">LandScan</a> derived population estimates are provided for 90% of our cities. Those lacking population estimates are often in sparsely inhabited areas. We provide a range of population values that account for the total “metropolitan” population rather than it’s administrative boundary population. Use the PopMax column to  [...]
+<p>Population estimates were derived from the LANDSCAN dataset maintained and distributed by the Oak Ridge National Laboratory. These data were converted from raster to vector and pixels with fewer than 200 persons per square kilometer were removed from the dataset as they were classified as rural. Once urban pixels were selected, these pixels were aggregated into contiguous units. Concurrently Thiessen polygons were created based on the selected city points. The Thiessen polygons were u [...]
+<p>Once intersected, the contiguous polygons were recalculated, using aerial interpolation assuming uniform population distribution within each pixel, to determine the population total. This process was conducted multiple times, for each scale level, to produce population estimates for each city at nested scales of 1:300 million, 1:110 million, 1:50 million, 1:20 million, and 1:10 million. </p>
+<div class="download-link-div">
+	<a class="download-link" rel="nofollow" title="Downloaded 481 times (Shapefile, geoDB, or TIFF format)" onclick="if (window.urchinTracker) urchinTracker ('http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_urban_areas_landscan.zip');" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_urban_areas_landscan.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_ [...]
+</div>
+<p><strong>Population ranks</strong></p>
+<p>Are calculated as rank_max and rank_min using this general VB formula that can be pasted into ArcMap Field Calculator advanced area (set your output to x):</p>
+<blockquote><p>
+a = [pop_max]</p>
+<p>if( a > 10000000 ) then
+x = 14
+elseif( a > 5000000 ) then
+x = 13
+elseif( a > 1000000 ) then
+x = 12
+elseif( a > 500000 ) then
+x = 11
+elseif( a > 200000 ) then
+x = 10
+elseif( a > 100000 ) then
+x = 9
+elseif( a > 50000 ) then
+x = 8
+elseif( a > 20000 ) then
+x = 7
+elseif( a > 10000 ) then
+x = 6
+elseif( a > 5000 ) then
+x = 5
+elseif( a > 2000 ) then
+x = 4
+elseif( a > 1000 ) then
+x = 3
+elseif( a > 200 ) then
+x = 2
+elseif( a > 0 ) then
+x = 1
+else
+x = 0
+end if</p></blockquote>
+<p><strong>Issues</strong></p>
+<p>While we don’t want to show every admin-1 capital, for those countries where we show most admin-1 capitals, we should have a complete set. If you find we are missing one, please log it in the Cx tool at right.</p>
+<p><strong>Version History</strong></p>
+	<ul>
+					<li>
+									<a rel="nofollow" title="Download version 2.0.0 of ne_10m_populated_places.zip" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_10m_populated_places.zip');">2.0.0</a>								
+							</li>
+					<li>
+									1.4.0								
+							</li>
+					<li>
+									1.3.0								
+							</li>
+					<li>
+									1.1.0								
+							</li>
+					<li>
+									0.9.0								
+							</li>
+			</ul>
+
+<p><a href="https://github.com/nvkelso/natural-earth-vector/blob/master/CHANGELOG" onclick="javascript:pageTracker._trackPageview('/outbound/article/https://github.com/nvkelso/natural-earth-vector/blob/master/CHANGELOG');">The master changelog is available on Github »</a>
+</div>
+
+<div class="sociable">
+<div class="sociable_tagline">
+<strong>Share and Enjoy:</strong>
+</div>
+<ul>
+	<li class="sociablefirst"><a rel="nofollow"  target="_blank" href="http://twitter.com/home?status=Populated%20Places%20-%20http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://twitter.com/home?status=Populated%20Places%20-%20http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F');" title="Twitter"><img src="http://www.natura [...]
+	<li><a rel="nofollow"  target="_blank" href="http://www.facebook.com/share.php?u=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&t=Populated%20Places" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.facebook.com/share.php?u=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&t=Populated%20Places');" title="Facebook"><img src="http://www.naturalearthdata. [...]
+	<li><a rel="nofollow"  target="_blank" href="http://digg.com/submit?phase=2&url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places&bodytext=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20wit [...]
+	<li><a rel="nofollow"  target="_blank" href="http://delicious.com/post?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places&notes=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20a [...]
+	<li><a rel="nofollow"  target="_blank" href="http://www.google.com/bookmarks/mark?op=edit&bkmk=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places&annotation=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoin [...]
+	<li><a rel="nofollow"  target="_blank" href="http://slashdot.org/bookmark.pl?title=Populated%20Places&url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://slashdot.org/bookmark.pl?title=Populated%20Places&url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F');" title="Slashdot"><img src="http://www.naturalea [...]
+	<li><a rel="nofollow"  target="_blank" href="http://www.stumbleupon.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.stumbleupon.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places');" title="StumbleUpon"><img src="http://www.na [...]
+	<li><a rel="nofollow"  target="_blank" href="mailto:?subject=Populated%20Places&body=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F" title="email"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif"  title="email" alt="email" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; ba [...]
+	<li><a rel="nofollow"  target="_blank" href="http://www.linkedin.com/shareArticle?mini=true&url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places&source=Natural+Earth+Free+vector+and+raster+map+data+at+1%3A10m%2C+1%3A50m%2C+and+1%3A110m+scales&summary=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D [...]
+	<li class="sociablelast"><a rel="nofollow"  target="_blank" href="http://reddit.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://reddit.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&title=Populated%20Places');" title="Reddit"><img src="http://www.natu [...]
+</ul>
+</div>
+
+				
+			</div>
+
+		</div>
+		
+
+		</div>
+
+
+	<div id="sidebar">
+    	<ul><li id='text-5' class='widget widget_text'><h2 class="widgettitle">Stay up to Date</h2>
+			<div class="textwidget"> Know when a new version of Natural Earth is released by subscribing to our <a href="http://www.naturalearthdata.com/updates/" class="up-to-date-link" >announcement list</a>.</div>
+		</li></ul><ul><li id='text-2' class='widget widget_text'><h2 class="widgettitle">Find a Problem?</h2>
+			<div class="textwidget"><div>
+<div style="float:left; width:65px;"><a href="/corrections/index.php?a=add"><img class="alignleft" title="New Ticket" src="http://www.naturalearthdata.com/corrections/img/newticket.png" alt="" width="60" height="60" /></a></div><div class="textwidget" style="float:left;width:120px; font-size:1.2em; font-size-adjust:none; font-style:normal;
+font-variant:normal; font-weight:normal; line-height:normal;">Submit suggestions and bug reports via our <a href="/corrections/index.php?a=add">correction system</a> and track the progress of your edits.</div>
+</div></div>
+		</li></ul><ul><li id='text-3' class='widget widget_text'><h2 class="widgettitle">Join Our Community</h2>
+			<div class="textwidget"><div>
+<div style="float:left; width:65px;"><a href="/forums/"><img src="http://www.naturalearthdata.com/wp-content/uploads/2009/08/green_globe_chat_bubble_562e.png" alt="forums" title="Chat in the forum!" width="50" height="50" /></a></div><div class="textwidget" style="float:left;width:120px; font-size:1.2em; font-size-adjust:none; font-style:normal;
+font-variant:normal; font-weight:normal; line-height:normal;">Talk back and discuss Natural Earth in the <a href="/forums/">Forums</a>.</div>
+</div></div>
+		</li></ul><ul><li id='text-4' class='widget widget_text'><h2 class="widgettitle">Thank You</h2>
+			<div class="textwidget">Our data downloads are generously hosted by Florida State University.</div>
+		</li></ul>	</div>
+
+</div>
+
+<hr />
+<div id="footer">
+<div id="footerarea">
+	<div id="footerlogos">
+    	<p>Supported by:</p>
+        <div class="footer-ad-box">
+        	<a href="http://www.nacis.org" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/nacis.png" alt="NACIS" /></a>
+        </div>
+    	<div class="footer-ad-box">
+        	<a href="http://www.cartotalk.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/cartotalk_ad.png" alt="Cartotalk" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.mapgiving.org" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/mapgiving.png" alt="Mapgiving" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.geography.wisc.edu/cartography/" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/wisconsin.png" alt="University of Wisconsin Madison - Cartography Dept." /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.shadedrelief.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/shaded_relief.png" alt="Shaded Relief" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.xnrproductions.com " target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/xnr.png" alt="XNR Productions" /></a>
+        </div>
+        
+        <p style="clear:both;"></p>
+        
+       <div class="footer-ad-box">
+        	<a href="http://www.freac.fsu.edu" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/fsu.png" alt="Florida State University - FREAC" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.springercartographics.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/scllc.png" alt="Springer Cartographics LLC" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.washingtonpost.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/wpost.png" alt="Washington Post" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.redgeographics.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/redgeo.png" alt="Red Geographics" /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://kelsocartography.com/blog " target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/kelso.png" alt="Kelso Cartography" /></a>
+        </div>
+        
+        <p style="clear:both;"></p>
+        <div class="footer-ad-box">
+        	<a href="http://www.avenza.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/avenza.png" alt="Avenza Systems Inc." /></a>
+        </div>
+        <div class="footer-ad-box">
+        	<a href="http://www.stamen.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/stamen_ne_logo.png" alt="Stamen Design" /></a>
+        </div>
+
+
+    </div>
+    <p style="clear:both;"></p>
+	<span id="footerleft">
+		© 2012. Natural Earth. All rights reserved.
+	</span>
+    <span id="footerright"> 
+    	<!-- Please help promote WordPress and simpleX. Do not remove -->   
+		<div>Powered by <a href="http://wordpress.org/">WordPress</a></div>
+        <div><a href="http://www.naturalearthdata.com/wp-admin">Staff Login »</a></div>
+    </span>
+</div>
+</div>
+		
+<!-- Google Analytics for WordPress | http://yoast.com/wordpress/google-analytics/ -->
+<script type="text/javascript">
+	var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+	document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+	try {
+		var pageTracker = _gat._getTracker("UA-10168306-1");
+	} catch(err) {}
+</script>
+<script src="http://www.naturalearthdata.com/wp-content/plugins/google-analytics-for-wordpress/custom_se.js" type="text/javascript"></script>
+<script type="text/javascript">
+	try {
+		// Cookied already: 
+		pageTracker._trackPageview();
+	} catch(err) {}
+</script>
+<!-- End of Google Analytics code -->
+
+</body>
+</html>
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.VERSION.txt b/geopandas/datasets/naturalearth_cities/naturalearth_cities.VERSION.txt
new file mode 100644
index 0000000..359a5b9
--- /dev/null
+++ b/geopandas/datasets/naturalearth_cities/naturalearth_cities.VERSION.txt
@@ -0,0 +1 @@
+2.0.0
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.cpg b/geopandas/datasets/naturalearth_cities/naturalearth_cities.cpg
new file mode 100644
index 0000000..cd89cb9
--- /dev/null
+++ b/geopandas/datasets/naturalearth_cities/naturalearth_cities.cpg
@@ -0,0 +1 @@
+ISO-8859-1
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.dbf b/geopandas/datasets/naturalearth_cities/naturalearth_cities.dbf
new file mode 100644
index 0000000..d9b9726
Binary files /dev/null and b/geopandas/datasets/naturalearth_cities/naturalearth_cities.dbf differ
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.prj b/geopandas/datasets/naturalearth_cities/naturalearth_cities.prj
new file mode 100644
index 0000000..a30c00a
--- /dev/null
+++ b/geopandas/datasets/naturalearth_cities/naturalearth_cities.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.shp b/geopandas/datasets/naturalearth_cities/naturalearth_cities.shp
new file mode 100644
index 0000000..d180259
Binary files /dev/null and b/geopandas/datasets/naturalearth_cities/naturalearth_cities.shp differ
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.shx b/geopandas/datasets/naturalearth_cities/naturalearth_cities.shx
new file mode 100644
index 0000000..e7eca86
Binary files /dev/null and b/geopandas/datasets/naturalearth_cities/naturalearth_cities.shx differ
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.cpg b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.cpg
new file mode 100644
index 0000000..cd89cb9
--- /dev/null
+++ b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.cpg
@@ -0,0 +1 @@
+ISO-8859-1
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.dbf b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.dbf
new file mode 100644
index 0000000..bd20a04
Binary files /dev/null and b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.dbf differ
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.prj b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.prj
new file mode 100644
index 0000000..a30c00a
--- /dev/null
+++ b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shp b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shp
new file mode 100644
index 0000000..32a78cd
Binary files /dev/null and b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shp differ
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shx b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shx
new file mode 100644
index 0000000..507f2c2
Binary files /dev/null and b/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shx differ
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index a3809ec..3f0817d 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -3,16 +3,15 @@ try:
 except ImportError:
     # Python 2.6
     from ordereddict import OrderedDict
-from collections import defaultdict
 import json
 import os
 import sys
 
 import numpy as np
-from pandas import DataFrame, Series
+from pandas import DataFrame, Series, Index
 from shapely.geometry import mapping, shape
 from shapely.geometry.base import BaseGeometry
-from six import string_types, iteritems
+from six import string_types
 
 from geopandas import GeoSeries
 from geopandas.base import GeoPandasBase
@@ -38,7 +37,15 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         If str, column to use as geometry. If array, will be set as 'geometry'
         column on GeoDataFrame.
     """
+
+    # XXX: This will no longer be necessary in pandas 0.17
+    _internal_names = ['_data', '_cacher', '_item_cache', '_cache',
+                       'is_copy', '_subtyp', '_index',
+                       '_default_kind', '_default_fill_value', '_metadata',
+                       '__array_struct__', '__array_interface__']
+
     _metadata = ['crs', '_geometry_column_name']
+
     _geometry_column_name = DEFAULT_GEO_COLUMN_NAME
 
     def __init__(self, *args, **kwargs):
@@ -48,6 +55,14 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         self.crs = crs
         if geometry is not None:
             self.set_geometry(geometry, inplace=True)
+        self._invalidate_sindex()
+
+    # Serialize metadata (will no longer be necessary in pandas 0.17+)
+    # See https://github.com/pydata/pandas/pull/10557
+    def __getstate__(self):
+        meta = dict((k, getattr(self, k, None)) for k in self._metadata)
+        return dict(_data=self._data, _typ=self._typ,
+                    _metadata=self._metadata, **meta)
 
     def __setattr__(self, attr, val):
         # have to special case geometry b/c pandas tries to use as column...
@@ -67,7 +82,6 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         if not isinstance(col, (list, np.ndarray, Series)):
             raise ValueError("Must use a list-like to set the geometry"
                              " property")
-
         self.set_geometry(col, inplace=True)
 
     geometry = property(fget=_get_geometry, fset=_set_geometry,
@@ -111,7 +125,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             crs = getattr(col, 'crs', self.crs)
 
         to_remove = None
-        geo_column_name = DEFAULT_GEO_COLUMN_NAME
+        geo_column_name = self._geometry_column_name
         if isinstance(col, (Series, list, np.ndarray)):
             level = col
         elif hasattr(col, 'ndim') and col.ndim != 1:
@@ -125,7 +139,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                 raise
             if drop:
                 to_remove = col
-                geo_column_name = DEFAULT_GEO_COLUMN_NAME
+                geo_column_name = self._geometry_column_name
             else:
                 geo_column_name = col
 
@@ -138,12 +152,12 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             level.crs = crs
 
         # Check that we are using a listlike of geometries
-        if not all(isinstance(item, BaseGeometry) for item in level):
+        if not all(isinstance(item, BaseGeometry) or not item for item in level):
             raise TypeError("Input geometry column must contain valid geometry objects.")
         frame[geo_column_name] = level
         frame._geometry_column_name = geo_column_name
         frame.crs = crs
-
+        frame._invalidate_sindex()
         if not inplace:
             return frame
 
@@ -151,7 +165,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
     def from_file(cls, filename, **kwargs):
         """
         Alternate constructor to create a GeoDataFrame from a file.
-        
+
         Example:
             df = geopandas.GeoDataFrame.from_file('nybb.shp')
 
@@ -176,7 +190,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             else:
                 f = f
 
-            d = {'geometry': shape(f['geometry'])}
+            d = {'geometry': shape(f['geometry']) if f['geometry'] else None}
             d.update(f['properties'])
             rows.append(d)
         df = GeoDataFrame.from_dict(rows)
@@ -197,12 +211,46 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         Wraps geopandas.read_postgis(). For additional help, see read_postgis()
 
         """
-        return geopandas.io.sql.read_postgis(sql, con, geom_col, crs, index_col, 
+        return geopandas.io.sql.read_postgis(sql, con, geom_col, crs, index_col,
                      coerce_float, params)
 
+    def to_json(self, na='null', show_bbox=False, **kwargs):
+        """
+        Returns a GeoJSON string representation of the GeoDataFrame.
+
+        Parameters
+        ----------
+        na : {'null', 'drop', 'keep'}, default 'null'
+            Indicates how to output missing (NaN) values in the GeoDataFrame
+            * null: output 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
+
+        show_bbox : include bbox (bounds) in the geojson
 
-    def to_json(self, na='null', **kwargs):
-        """Returns a GeoJSON representation of the GeoDataFrame.
+        The remaining *kwargs* are passed to json.dumps().
+
+        """
+        return json.dumps(self._to_geo(na=na, show_bbox=show_bbox), **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
+        default args instead of a method
+
+        """
+        return self._to_geo(na='null', show_bbox=True)
+
+    def iterfeatures(self, na='null', show_bbox=False):
+        """
+        Returns an iterator that yields feature dictionaries that comply with
+        __geo_interface__
 
         Parameters
         ----------
@@ -213,8 +261,9 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                     each feature individually so that features may have
                     different properties
             * keep: output the missing entries as NaN
-        
-        The remaining *kwargs* are passed to json.dumps().
+
+        show_bbox : include bbox (bounds) in the geojson. default False
+
         """
         def fill_none(row):
             """
@@ -228,81 +277,70 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                 d[k] = None
             return d
 
-        # na_methods must take in a Series and return dict-like
+        # na_methods must take in a Series and return dict
         na_methods = {'null': fill_none,
-                      'drop': lambda row: row.dropna(),
-                      'keep': lambda row: row}
+                      'drop': lambda row: row.dropna().to_dict(),
+                      'keep': lambda row: row.to_dict()}
 
         if na not in na_methods:
             raise ValueError('Unknown na method {0}'.format(na))
         f = na_methods[na]
 
-        def feature(i, row):
-            row = f(row)
-            return {
+        for i, row in self.iterrows():
+            properties = f(row)
+            del properties[self._geometry_column_name]
+
+            feature = {
                 'id': str(i),
                 'type': 'Feature',
-                'properties':
-                    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 )
-            
-    def to_file(self, filename, driver="ESRI Shapefile", **kwargs):
+                'properties': properties,
+                'geometry': mapping(row[self._geometry_column_name])
+                            if row[self._geometry_column_name] else None
+            }
+
+            if show_bbox:
+                feature['bbox'] = row.geometry.bounds
+
+            yield feature
+
+    def _to_geo(self, **kwargs):
+        """
+        Returns a python feature collection (i.e. the geointerface)
+        representation of the GeoDataFrame.
+
+        """
+        geo = {'type': 'FeatureCollection',
+               'features': list(self.iterfeatures(**kwargs))}
+
+        if kwargs.get('show_bbox', False):
+            geo['bbox'] = self.total_bounds
+
+        return geo
+
+    def to_file(self, filename, driver="ESRI Shapefile", schema=None,
+                **kwargs):
         """
         Write this GeoDataFrame to an OGR data source
-        
+
         A dictionary of supported OGR providers is available via:
         >>> import fiona
         >>> fiona.supported_drivers
 
         Parameters
         ----------
-        filename : string 
+        filename : string
             File path or file handle to write to.
         driver : string, default 'ESRI Shapefile'
             The OGR format driver used to write the vector file.
+        schema : dict, default None
+            If specified, the schema dictionary is passed to Fiona to
+            better control how the file is written.
 
-        The *kwargs* are passed to fiona.open and can be used to write 
+        The *kwargs* are passed to fiona.open and can be used to write
         to multi-layer data, store data within archives (zip files), etc.
         """
-        import fiona
-        def convert_type(in_type):
-            if in_type == object:
-                return 'str'
-            out_type = type(np.asscalar(np.zeros(1, in_type))).__name__
-            if out_type == 'long':
-                out_type = 'int'
-            return out_type
-            
-        def feature(i, row):
-            return {
-                'id': str(i),
-                'type': 'Feature',
-                'properties':
-                    dict((k, v) for k, v in iteritems(row) if k != 'geometry'),
-                'geometry': mapping(row['geometry']) }
-        
-        properties = OrderedDict([(col, convert_type(_type)) for col, _type 
-            in zip(self.columns, self.dtypes) if col!='geometry'])
-        # Need to check geom_types before we write to file... 
-        # Some (most?) providers expect a single geometry type: 
-        # Point, LineString, or Polygon
-        geom_types = self['geometry'].geom_type.unique()
-        from os.path import commonprefix # To find longest common prefix
-        geom_type = commonprefix([g[::-1] for g in geom_types])[::-1]  # Reverse
-        if geom_type == '': # No common suffix = mixed geometry types
-            raise ValueError("Geometry column cannot contains mutiple "
-                             "geometry types when writing to file.")
-        schema = {'geometry': geom_type, 'properties': properties}
-        filename = os.path.abspath(os.path.expanduser(filename))
-        with fiona.open(filename, 'w', driver=driver, crs=self.crs, 
-                        schema=schema, **kwargs) as c:
-            for i, row in self.iterrows():
-                c.write(feature(i, row))
+        from geopandas.io.file import to_file
+        to_file(self, filename, driver, schema, **kwargs)
 
     def to_crs(self, crs=None, epsg=None, inplace=False):
         """Transform geometries to a new coordinate reference system
@@ -312,6 +350,12 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         joining points are assumed to be lines in the current
         projection, not geodesics.  Objects crossing the dateline (or
         other projection boundary) will have undesirable behavior.
+
+        `to_crs` passes the `crs` argument to the `Proj` function from the
+        `pyproj` library (with the option `preserve_units=True`). It can
+        therefore accept proj4 projections in any format
+        supported by `Proj`, including dictionaries, or proj4 strings.
+
         """
         if inplace:
             df = self
@@ -334,28 +378,49 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         if isinstance(key, string_types) and key == geo_col:
             result.__class__ = GeoSeries
             result.crs = self.crs
+            result._invalidate_sindex()
         elif isinstance(result, DataFrame) and geo_col in result:
             result.__class__ = GeoDataFrame
             result.crs = self.crs
             result._geometry_column_name = geo_col
+            result._invalidate_sindex()
         elif isinstance(result, DataFrame) and geo_col not in result:
             result.__class__ = DataFrame
-            result.crs = self.crs
         return result
 
     #
     # Implement pandas methods
     #
 
+    def merge(self, *args, **kwargs):
+        result = DataFrame.merge(self, *args, **kwargs)
+        geo_col = self._geometry_column_name
+        if isinstance(result, DataFrame) and geo_col in result:
+            result.__class__ = GeoDataFrame
+            result.crs = self.crs
+            result._geometry_column_name = geo_col
+            result._invalidate_sindex()
+        elif isinstance(result, DataFrame) and geo_col not in result:
+            result.__class__ = DataFrame
+        return result
+
     @property
     def _constructor(self):
         return GeoDataFrame
 
     def __finalize__(self, other, method=None, **kwargs):
-        """ propagate metadata from other to self """
-        # NOTE: backported from pandas master (upcoming v0.13)
-        for name in self._metadata:
-            object.__setattr__(self, name, getattr(other, name, None))
+        """propagate metadata from other to self """
+        # merge operation: using metadata of the left object
+        if method == 'merge':
+            for name in self._metadata:
+                object.__setattr__(self, name, getattr(other.left, name, None))
+        # concat operation: using metadata of the first object
+        elif method == 'concat':
+            for name in self._metadata:
+                object.__setattr__(self, name, getattr(other.objs[0], name, None))
+        else:
+            for name in self._metadata:
+                object.__setattr__(self, name, getattr(other, name, None))
         return self
 
     def copy(self, deep=True):
@@ -378,8 +443,59 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         return GeoDataFrame(data).__finalize__(self)
 
     def plot(self, *args, **kwargs):
+
         return plot_dataframe(self, *args, **kwargs)
 
+    plot.__doc__ = plot_dataframe.__doc__
+
+
+    def dissolve(self, by=None, aggfunc='first', as_index=True):
+        """
+        Dissolve geometries within `groupby` into single observation.
+        This is accomplished by applying the `unary_union` method
+        to all geometries within a groupself.
+
+        Observations associated with each `groupby` group will be aggregated
+        using the `aggfunc`.
+
+        Parameters
+        ----------
+        by : string, default None
+            Column whose values define groups to be dissolved
+        aggfunc : function or string, default "first"
+            Aggregation function for manipulation of data associated
+            with each group. Passed to pandas `groupby.agg` method.
+        as_index : boolean, default True
+            If true, groupby columns become index of result.
+
+        Returns
+        -------
+        GeoDataFrame
+        """
+
+        # Process non-spatial component
+        data = self.drop(labels=self.geometry.name, axis=1)
+        aggregated_data = data.groupby(by=by).agg(aggfunc)
+
+
+        # Process spatial component
+        def merge_geometries(block):
+            merged_geom = block.unary_union
+            return merged_geom
+
+        g = self.groupby(by=by, group_keys=False)[self.geometry.name].agg(merge_geometries)
+
+        # Aggregate
+        aggregated_geometry = GeoDataFrame(g, geometry=self.geometry.name)
+        # Recombine
+        aggregated = aggregated_geometry.join(aggregated_data)
+
+        # Reset if requested
+        if not as_index:
+            aggregated = aggregated.reset_index()
+
+        return aggregated
+
 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/geoseries.py b/geopandas/geoseries.py
index 9ac72b7..e4c3529 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -1,10 +1,13 @@
 from functools import partial
+import json
 from warnings import warn
 
 import numpy as np
 from pandas import Series, DataFrame
+from pandas.core.indexing import _NDFrameIndexer
+from pandas.util.decorators import cache_readonly
 import pyproj
-from shapely.geometry import shape, Polygon, Point
+from shapely.geometry import box, shape, Polygon, Point
 from shapely.geometry.collection import GeometryCollection
 from shapely.geometry.base import BaseGeometry
 from shapely.ops import transform
@@ -25,6 +28,28 @@ def _convert_array_args(args):
         args = ([args[0]],)
     return args
 
+class _CoordinateIndexer(_NDFrameIndexer):
+    """ Indexing by coordinate slices """
+    def _getitem_tuple(self, tup):
+        obj = self.obj
+        xs, ys = tup
+        # handle numeric values as x and/or y coordinate index
+        if type(xs) is not slice:
+            xs = slice(xs, xs)
+        if type(ys) is not slice:
+            ys = slice(ys, ys)
+        # don't know how to handle step; should this raise?
+        if xs.step is not None or ys.step is not None:
+            warn("Ignoring step - full interval is used.")
+        xmin, ymin, xmax, ymax = obj.total_bounds
+        bbox = box(xs.start or xmin,
+                   ys.start or ymin,
+                   xs.stop or xmax,
+                   ys.stop or ymax)
+        idx = obj.intersects(bbox)
+        return obj[idx]
+
+
 class GeoSeries(GeoPandasBase, Series):
     """A Series object designed to store shapely geometry objects."""
     _metadata = ['name', 'crs']
@@ -48,6 +73,10 @@ class GeoSeries(GeoPandasBase, Series):
 
         super(GeoSeries, self).__init__(*args, **kwargs)
         self.crs = crs
+        self._invalidate_sindex()
+
+    def append(self, *args, **kwargs):
+        return self._wrapped_pandas_method('append', *args, **kwargs)
 
     @property
     def geometry(self):
@@ -82,6 +111,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,
@@ -104,6 +140,7 @@ class GeoSeries(GeoPandasBase, Series):
         if type(val) == Series:
             val.__class__ = GeoSeries
             val.crs = self.crs
+            val._invalidate_sindex()
         return val
 
     def __getitem__(self, key):
@@ -202,9 +239,10 @@ class GeoSeries(GeoPandasBase, Series):
         else:
             return False
 
-
     def plot(self, *args, **kwargs):
         return plot_series(self, *args, **kwargs)
+    
+    plot.__doc__ = plot_series.__doc__
 
     #
     # Additional methods
@@ -218,6 +256,12 @@ class GeoSeries(GeoPandasBase, Series):
         joining points are assumed to be lines in the current
         projection, not geodesics.  Objects crossing the dateline (or
         other projection boundary) will have undesirable behavior.
+
+        `to_crs` passes the `crs` argument to the `Proj` function from the
+        `pyproj` library (with the option `preserve_units=True`). It can
+        therefore accept proj4 projections in any format
+        supported by `Proj`, including dictionaries, or proj4 strings.
+
         """
         from fiona.crs import from_epsg
         if self.crs is None:
@@ -228,14 +272,25 @@ class GeoSeries(GeoPandasBase, Series):
                 crs = from_epsg(epsg)
             except TypeError:
                 raise TypeError('Must set either crs or epsg for output.')
-        proj_in = pyproj.Proj(preserve_units=True, **self.crs)
-        proj_out = pyproj.Proj(preserve_units=True, **crs)
+        proj_in = pyproj.Proj(self.crs, preserve_units=True)
+        proj_out = pyproj.Proj(crs, preserve_units=True)
         project = partial(pyproj.transform, proj_in, proj_out)
         result = self.apply(lambda geom: transform(project, geom))
         result.__class__ = GeoSeries
         result.crs = crs
+        result._invalidate_sindex()
         return result
 
+    def to_json(self, **kwargs):
+        """
+        Returns a GeoJSON string representation of the GeoSeries.
+
+        Parameters
+        ----------
+        *kwargs* that will be passed to json.dumps().
+        """
+        return json.dumps(self.__geo_interface__, **kwargs)
+
     #
     # Implement standard operators for GeoSeries
     #
@@ -255,3 +310,5 @@ class GeoSeries(GeoPandasBase, Series):
     def __sub__(self, other):
         """Implement - operator as for builtin set type"""
         return self.difference(other)
+
+GeoSeries._create_indexer('cx', _CoordinateIndexer)
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 05f35b5..d407615 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -1,18 +1,26 @@
+import os
+
+import fiona
+import numpy as np
+from shapely.geometry import mapping
+
+from six import iteritems
 from geopandas import GeoDataFrame
 
+
 def read_file(filename, **kwargs):
     """
     Returns a GeoDataFrame from a file.
 
     *filename* is either the absolute or relative path to the file to be
-    opened and *kwargs* are keyword args to be passed to the method when
-    opening the file.
+    opened and *kwargs* are keyword args to be passed to the `open` method
+    in the fiona library when opening the file. For more information on 
+    possible keywords, type: ``import fiona; help(fiona.open)``
     """
-    import fiona
     bbox = kwargs.pop('bbox', None)
     with fiona.open(filename, **kwargs) as f:
         crs = f.crs
-        if bbox != None:
+        if bbox is not None:
             assert len(bbox)==4
             f_filt = f.filter(bbox=bbox)
         else:
@@ -20,3 +28,79 @@ def read_file(filename, **kwargs):
         gdf = GeoDataFrame.from_features(f, crs=crs)
 
     return gdf
+
+
+def to_file(df, filename, driver="ESRI Shapefile", schema=None,
+            **kwargs):
+    """
+    Write this GeoDataFrame to an OGR data source
+
+    A dictionary of supported OGR providers is available via:
+    >>> import fiona
+    >>> fiona.supported_drivers
+
+    Parameters
+    ----------
+    df : GeoDataFrame to be written
+    filename : string
+        File path or file handle to write to.
+    driver : string, default 'ESRI Shapefile'
+        The OGR format driver used to write the vector file.
+    schema : dict, default None
+        If specified, the schema dictionary is passed to Fiona to
+        better control how the file is written. If None, GeoPandas
+        will determine the schema based on each column's dtype
+
+    The *kwargs* are passed to fiona.open and can be used to write
+    to multi-layer data, store data within archives (zip files), etc.
+    """
+    if schema is None:
+        schema = infer_schema(df)
+    filename = os.path.abspath(os.path.expanduser(filename))
+    with fiona.open(filename, 'w', driver=driver, crs=df.crs,
+                    schema=schema, **kwargs) as c:
+        for feature in df.iterfeatures():
+            c.write(feature)
+
+
+def infer_schema(df):
+    try:
+        from collections import OrderedDict
+    except ImportError:
+        from ordereddict import OrderedDict
+
+    def convert_type(in_type):
+        if in_type == object:
+            return 'str'
+        out_type = type(np.asscalar(np.zeros(1, in_type))).__name__
+        if out_type == 'long':
+            out_type = 'int'
+        return out_type
+
+    properties = OrderedDict([
+        (col, convert_type(_type)) for col, _type in
+        zip(df.columns, df.dtypes) if col != df._geometry_column_name
+    ])
+
+    geom_type = _common_geom_type(df)
+    if not geom_type:
+        raise ValueError("Geometry column cannot contain mutiple "
+                         "geometry types when writing to file.")
+
+    schema = {'geometry': geom_type, 'properties': properties}
+
+    return schema
+
+
+def _common_geom_type(df):
+    # Need to check geom_types before we write to file...
+    # Some (most?) providers expect a single geometry type:
+    # Point, LineString, or Polygon
+    geom_types = df.geometry.geom_type.unique()
+
+    from os.path import commonprefix   # To find longest common prefix
+    geom_type = commonprefix([g[::-1] for g in geom_types if g])[::-1]  # Reverse
+    if not geom_type:
+        geom_type = None
+
+    return geom_type
diff --git a/tests/__init__.py b/geopandas/io/tests/__init__.py
similarity index 100%
copy from tests/__init__.py
copy to geopandas/io/tests/__init__.py
diff --git a/tests/test_io.py b/geopandas/io/tests/test_io.py
similarity index 63%
rename from tests/test_io.py
rename to geopandas/io/tests/test_io.py
index ec04bd7..e8e87f9 100644
--- a/tests/test_io.py
+++ b/geopandas/io/tests/test_io.py
@@ -2,23 +2,22 @@ from __future__ import absolute_import
 
 import fiona
 
-from geopandas import GeoDataFrame, read_postgis, read_file
-import tests.util
-from .util import PANDAS_NEW_SQL_API, unittest
+from geopandas import read_postgis, read_file
+from geopandas.tests.util import download_nybb, connect, create_db, \
+     PANDAS_NEW_SQL_API, unittest, validate_boro_df
 
 
 class TestIO(unittest.TestCase):
     def setUp(self):
-        nybb_filename = tests.util.download_nybb()
-        path = '/nybb_14a_av/nybb.shp'
+        nybb_filename, nybb_zip_path = download_nybb()
         vfs = 'zip://' + nybb_filename
-        self.df = read_file(path, vfs=vfs)
-        with fiona.open(path, vfs=vfs) as f:
+        self.df = read_file(nybb_zip_path, vfs=vfs)
+        with fiona.open(nybb_zip_path, vfs=vfs) as f:
             self.crs = f.crs
 
     def test_read_postgis_default(self):
-        con = tests.util.connect('test_geopandas')
-        if con is None or not tests.util.create_db(self.df):
+        con = connect('test_geopandas')
+        if con is None or not create_db(self.df):
             raise unittest.case.SkipTest()
 
         try:
@@ -30,11 +29,11 @@ class TestIO(unittest.TestCase):
                 con = con.connect()
             con.close()
 
-        tests.util.validate_boro_df(self, df)
+        validate_boro_df(self, df)
 
     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):
+        con = connect('test_geopandas')
+        if con is None or not create_db(self.df):
             raise unittest.case.SkipTest()
 
         try:
@@ -49,9 +48,9 @@ class TestIO(unittest.TestCase):
                 con = con.connect()
             con.close()
 
-        tests.util.validate_boro_df(self, df)
+        validate_boro_df(self, df)
 
     def test_read_file(self):
         df = self.df.rename(columns=lambda x: x.lower())
-        tests.util.validate_boro_df(self, df)
+        validate_boro_df(self, df)
         self.assert_(df.crs == self.crs)
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 69557de..38d404e 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -1,51 +1,57 @@
 from __future__ import print_function
 
+import warnings
+
 import numpy as np
 from six import next
 from six.moves import xrange
+from shapely.geometry import Polygon
 
 
-def plot_polygon(ax, poly, facecolor='red', edgecolor='black', alpha=0.5):
+def plot_polygon(ax, poly, facecolor='red', edgecolor='black', alpha=0.5, linewidth=1.0, **kwargs):
     """ Plot a single Polygon geometry """
     from descartes.patch import PolygonPatch
     a = np.asarray(poly.exterior)
+    if poly.has_z:
+        poly = Polygon(zip(*poly.exterior.xy))
+
     # without Descartes, we could make a Patch of exterior
-    ax.add_patch(PolygonPatch(poly, facecolor=facecolor, alpha=alpha))
-    ax.plot(a[:, 0], a[:, 1], color=edgecolor)
+    ax.add_patch(PolygonPatch(poly, facecolor=facecolor, linewidth=0, alpha=alpha))  # linewidth=0 because boundaries are drawn separately
+    ax.plot(a[:, 0], a[:, 1], color=edgecolor, linewidth=linewidth, **kwargs)
     for p in poly.interiors:
         x, y = zip(*p.coords)
-        ax.plot(x, y, color=edgecolor)
+        ax.plot(x, y, color=edgecolor, linewidth=linewidth)
 
 
-def plot_multipolygon(ax, geom, facecolor='red', alpha=0.5):
+def plot_multipolygon(ax, geom, facecolor='red', edgecolor='black', alpha=0.5, linewidth=1.0, **kwargs):
     """ Can safely call with either Polygon or Multipolygon geometry
     """
     if geom.type == 'Polygon':
-        plot_polygon(ax, geom, facecolor=facecolor, alpha=alpha)
+        plot_polygon(ax, geom, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, linewidth=linewidth, **kwargs)
     elif geom.type == 'MultiPolygon':
         for poly in geom.geoms:
-            plot_polygon(ax, poly, facecolor=facecolor, alpha=alpha)
+            plot_polygon(ax, poly, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, linewidth=linewidth, **kwargs)
 
 
-def plot_linestring(ax, geom, color='black', linewidth=1):
+def plot_linestring(ax, geom, color='black', linewidth=1.0, **kwargs):
     """ Plot a single LineString geometry """
     a = np.array(geom)
-    ax.plot(a[:,0], a[:,1], color=color, linewidth=linewidth)
+    ax.plot(a[:, 0], a[:, 1], color=color, linewidth=linewidth, **kwargs)
 
 
-def plot_multilinestring(ax, geom, color='red', linewidth=1):
+def plot_multilinestring(ax, geom, color='red', linewidth=1.0, **kwargs):
     """ Can safely call with either LineString or MultiLineString geometry
     """
     if geom.type == 'LineString':
-        plot_linestring(ax, geom, color=color, linewidth=linewidth)
+        plot_linestring(ax, geom, color=color, linewidth=linewidth, **kwargs)
     elif geom.type == 'MultiLineString':
         for line in geom.geoms:
-            plot_linestring(ax, line, color=color, linewidth=linewidth)
+            plot_linestring(ax, line, color=color, linewidth=linewidth, **kwargs)
 
 
-def plot_point(ax, pt, marker='o', markersize=2):
+def plot_point(ax, pt, marker='o', markersize=2, color='black', **kwargs):
     """ Plot a single Point geometry """
-    ax.plot(pt.x, pt.y, marker=marker, markersize=markersize, linewidth=0)
+    ax.plot(pt.x, pt.y, marker=marker, markersize=markersize, color=color, **kwargs)
 
 
 def gencolor(N, colormap='Set1'):
@@ -67,7 +73,9 @@ def gencolor(N, colormap='Set1'):
     for i in xrange(N):
         yield colors[i % n_colors]
 
-def plot_series(s, colormap='Set1', alpha=0.5, axes=None):
+
+def plot_series(s, cmap='Set1', color=None, ax=None, linewidth=1.0,
+                figsize=None, **color_kwds):
     """ Plot a GeoSeries
 
         Generate a plot of a GeoSeries geometry with matplotlib.
@@ -80,7 +88,7 @@ def plot_series(s, colormap='Set1', alpha=0.5, axes=None):
             MultiPolygon, LineString, MultiLineString and Point
             geometries can be plotted.
 
-        colormap : str (default 'Set1')
+        cmap : str (default 'Set1')
             The name of a colormap recognized by matplotlib.  Any
             colormap will work, but categorical colormaps are
             generally recommended.  Examples of useful discrete
@@ -88,40 +96,63 @@ def plot_series(s, colormap='Set1', alpha=0.5, axes=None):
 
                 Accent, Dark2, Paired, Pastel1, Pastel2, Set1, Set2, Set3
 
-        alpha : float (default 0.5)
-            Alpha value for polygon fill regions.  Has no effect for
-            lines or points.
+        color : str (default None)
+            If specified, all objects will be colored uniformly.
 
-        axes : matplotlib.pyplot.Artist (default None)
+        ax : matplotlib.pyplot.Artist (default None)
             axes on which to draw the plot
 
+        linewidth : float (default 1.0)
+            Line width for geometries.
+
+        figsize : pair of floats (default None)
+            Size of the resulting matplotlib.figure.Figure. If the argument
+            ax is given explicitly, figsize is ignored.
+
+        **color_kwds : dict
+            Color options to be passed on to the actual plot function
+
         Returns
         -------
 
         matplotlib axes instance
     """
+    if 'colormap' in color_kwds:
+        warnings.warn("'colormap' is deprecated, please use 'cmap' instead "
+                      "(for consistency with matplotlib)", FutureWarning)
+        cmap = color_kwds.pop('colormap')
+    if 'axes' in color_kwds:
+        warnings.warn("'axes' is deprecated, please use 'ax' instead "
+                      "(for consistency with pandas)", FutureWarning)
+        ax = color_kwds.pop('axes')
+
     import matplotlib.pyplot as plt
-    if axes == None:
-        fig = plt.gcf()
-        fig.add_subplot(111, aspect='equal')
-        ax = plt.gca()
-    else:
-        ax = axes
-    color = gencolor(len(s), colormap=colormap)
+    if ax is None:
+        fig, ax = plt.subplots(figsize=figsize)
+        ax.set_aspect('equal')
+    color_generator = gencolor(len(s), colormap=cmap)
     for geom in s:
+        if color is None:
+            col = next(color_generator)
+        else:
+            col = color
         if geom.type == 'Polygon' or geom.type == 'MultiPolygon':
-            plot_multipolygon(ax, geom, facecolor=next(color), alpha=alpha)
+            if 'facecolor' in color_kwds:
+                plot_multipolygon(ax, geom, linewidth=linewidth, **color_kwds)
+            else:
+                plot_multipolygon(ax, geom, facecolor=col, linewidth=linewidth, **color_kwds)
         elif geom.type == 'LineString' or geom.type == 'MultiLineString':
-            plot_multilinestring(ax, geom, color=next(color))
+            plot_multilinestring(ax, geom, color=col, linewidth=linewidth, **color_kwds)
         elif geom.type == 'Point':
-            plot_point(ax, geom)
+            plot_point(ax, geom, color=col, **color_kwds)
     plt.draw()
     return ax
 
 
-def plot_dataframe(s, column=None, colormap=None, alpha=0.5,
-                   categorical=False, legend=False, axes=None, scheme=None,
-                   k=5):
+def plot_dataframe(s, column=None, cmap=None, color=None, linewidth=1.0,
+                   categorical=False, legend=False, ax=None,
+                   scheme=None, k=5, vmin=None, vmax=None, figsize=None,
+                   **color_kwds):
     """ Plot a GeoDataFrame
 
         Generate a plot of a GeoDataFrame with matplotlib.  If a
@@ -141,49 +172,78 @@ def plot_dataframe(s, column=None, colormap=None, alpha=0.5,
             The name of the column to be plotted.
 
         categorical : bool (default False)
-            If False, colormap will reflect numerical values of the
+            If False, cmap will reflect numerical values of the
             column being plotted.  For non-numerical columns (or if
             column=None), this will be set to True.
 
-        colormap : str (default 'Set1')
+        cmap : str (default 'Set1')
             The name of a colormap recognized by matplotlib.
 
-        alpha : float (default 0.5)
-            Alpha value for polygon fill regions.  Has no effect for
-            lines or points.
+        color : str (default None)
+            If specified, all objects will be colored uniformly.
+
+        linewidth : float (default 1.0)
+            Line width for geometries.
 
         legend : bool (default False)
             Plot a legend (Experimental; currently for categorical
             plots only)
 
-        axes : matplotlib.pyplot.Artist (default None)
+        ax : matplotlib.pyplot.Artist (default None)
             axes on which to draw the plot
 
         scheme : pysal.esda.mapclassify.Map_Classifier
-            Choropleth classification schemes
+            Choropleth classification schemes (requires PySAL)
 
         k   : int (default 5)
             Number of classes (ignored if scheme is None)
 
+        vmin : None or float (default None)
+
+            Minimum value of cmap. If None, the minimum data value
+            in the column to be plotted is used.
+
+        vmax : None or float (default None)
+
+            Maximum value of cmap. If None, the maximum data value
+            in the column to be plotted is used.
+
+        figsize
+            Size of the resulting matplotlib.figure.Figure. If the argument
+            axes is given explicitly, figsize is ignored.
+
+        **color_kwds : dict
+            Color options to be passed on to the actual plot function
 
         Returns
         -------
 
         matplotlib axes instance
     """
+    if 'colormap' in color_kwds:
+        warnings.warn("'colormap' is deprecated, please use 'cmap' instead "
+                      "(for consistency with matplotlib)", FutureWarning)
+        cmap = color_kwds.pop('colormap')
+    if 'axes' in color_kwds:
+        warnings.warn("'axes' is deprecated, please use 'ax' instead "
+                      "(for consistency with pandas)", FutureWarning)
+        ax = color_kwds.pop('axes')
+
     import matplotlib.pyplot as plt
     from matplotlib.lines import Line2D
     from matplotlib.colors import Normalize
     from matplotlib import cm
 
     if column is None:
-        return plot_series(s.geometry, colormap=colormap, alpha=alpha, axes=axes)
+        return plot_series(s.geometry, cmap=cmap, color=color,
+                           ax=ax, linewidth=linewidth, figsize=figsize,
+                           **color_kwds)
     else:
         if s[column].dtype is np.dtype('O'):
             categorical = True
         if categorical:
-            if colormap is None:
-                colormap = 'Set1'
+            if cmap is None:
+                cmap = 'Set1'
             categories = list(set(s[column].values))
             categories.sort()
             valuemap = dict([(k, v) for (v, k) in enumerate(categories)])
@@ -191,28 +251,34 @@ def plot_dataframe(s, column=None, colormap=None, alpha=0.5,
         else:
             values = s[column]
         if scheme is not None:
-            values = __pysal_choro(values, scheme, k=k)
-        cmap = norm_cmap(values, colormap, Normalize, cm)
-        if axes == None:
-            fig = plt.gcf()
-            fig.add_subplot(111, aspect='equal')
-            ax = plt.gca()
-        else:
-            ax = axes
+            binning = __pysal_choro(values, scheme, k=k)
+            values = binning.yb
+            # set categorical to True for creating the legend
+            categorical = True
+            binedges = [binning.yb.min()] + binning.bins.tolist()
+            categories = ['{0:.2f} - {1:.2f}'.format(binedges[i], binedges[i+1])
+                          for i in range(len(binedges)-1)]
+        cmap = norm_cmap(values, cmap, Normalize, cm, vmin=vmin, vmax=vmax)
+        if ax is None:
+            fig, ax = plt.subplots(figsize=figsize)
+            ax.set_aspect('equal')
         for geom, value in zip(s.geometry, values):
+            if color is None:
+                col = cmap.to_rgba(value)
+            else:
+                col = color
             if geom.type == 'Polygon' or geom.type == 'MultiPolygon':
-                plot_multipolygon(ax, geom, facecolor=cmap.to_rgba(value), alpha=alpha)
+                plot_multipolygon(ax, geom, facecolor=col, linewidth=linewidth, **color_kwds)
             elif geom.type == 'LineString' or geom.type == 'MultiLineString':
-                plot_multilinestring(ax, geom, color=cmap.to_rgba(value))
-            # TODO: color point geometries
+                plot_multilinestring(ax, geom, color=col, linewidth=linewidth, **color_kwds)
             elif geom.type == 'Point':
-                plot_point(ax, geom)
+                plot_point(ax, geom, color=col, **color_kwds)
         if legend:
             if categorical:
                 patches = []
                 for value, cat in enumerate(categories):
                     patches.append(Line2D([0], [0], linestyle="none",
-                                          marker="o", alpha=alpha,
+                                          marker="o", alpha=color_kwds.get('alpha', 0.5),
                                           markersize=10, markerfacecolor=cmap.to_rgba(value)))
                 ax.legend(patches, categories, numpoints=1, loc='best')
             else:
@@ -232,7 +298,8 @@ def __pysal_choro(values, scheme, k=5):
             Series to be plotted
 
         scheme
-            pysal.esda.mapclassify classificatin scheme ['Equal_interval'|'Quantiles'|'Fisher_Jenks']
+            pysal.esda.mapclassify classificatin scheme
+            ['Equal_interval'|'Quantiles'|'Fisher_Jenks']
 
         k
             number of classes (2 <= k <=9)
@@ -240,11 +307,12 @@ def __pysal_choro(values, scheme, k=5):
         Returns
         -------
 
-        values
-            Series with values replaced with class identifier if PySAL is available, otherwise the original values are used
+        binning
+            Binning objects that holds the Series with values replaced with
+            class identifier and the bins.
     """
 
-    try: 
+    try:
         from pysal.esda.mapclassify import Quantiles, Equal_Interval, Fisher_Jenks
         schemes = {}
         schemes['equal_interval'] = Equal_Interval
@@ -254,20 +322,19 @@ def __pysal_choro(values, scheme, k=5):
         scheme = scheme.lower()
         if scheme not in schemes:
             scheme = 'quantiles'
-            print('Unrecognized scheme: ', s0)
-            print('Using Quantiles instead')
-        if k<2 or k>9:
-            print('Invalid k: ', k)
-            print('2<=k<=9, setting k=5 (default)')
+            warnings.warn('Unrecognized scheme "{0}". Using "Quantiles" '
+                          'instead'.format(s0), UserWarning, stacklevel=3)
+        if k < 2 or k > 9:
+            warnings.warn('Invalid k: {0} (2 <= k <= 9), setting k=5 '
+                          '(default)'.format(k), UserWarning, stacklevel=3)
             k = 5
         binning = schemes[scheme](values, k)
-        values = binning.yb
-    except ImportError: 
-        print('PySAL not installed, setting map to default')
+        return binning
+    except ImportError:
+        raise ImportError("PySAL is required to use the 'scheme' keyword")
 
-    return values
 
-def norm_cmap(values, cmap, normalize, cm):
+def norm_cmap(values, cmap, normalize, cm, vmin=None, vmax=None):
 
     """ Normalize and set colormap
 
@@ -286,19 +353,21 @@ def norm_cmap(values, cmap, normalize, cm):
         cm
             matplotlib.cm
 
+        vmin
+            Minimum value of colormap. If None, uses min(values).
+
+        vmax
+            Maximum value of colormap. If None, uses max(values).
+
         Returns
         -------
         n_cmap
             mapping of normalized values to colormap (cmap)
-            
+
     """
 
-    mn, mx = min(values), max(values)
+    mn = min(values) if vmin is None else vmin
+    mx = max(values) if vmax is None else vmax
     norm = normalize(vmin=mn, vmax=mx)
-    n_cmap  = cm.ScalarMappable(norm=norm, cmap=cmap)
+    n_cmap = cm.ScalarMappable(norm=norm, cmap=cmap)
     return n_cmap
-
-
-
-
-
diff --git a/geopandas/sindex.py b/geopandas/sindex.py
new file mode 100644
index 0000000..669582a
--- /dev/null
+++ b/geopandas/sindex.py
@@ -0,0 +1,26 @@
+from geopandas import base
+
+if base.HAS_SINDEX:
+    from rtree.core import RTreeError
+    from rtree.index import Index as RTreeIndex
+
+
+class SpatialIndex(RTreeIndex):
+    """
+    A simple wrapper around rtree's RTree Index
+    """
+
+    def __init__(self, *args):
+        if not base.HAS_SINDEX:
+            raise ImportError("SpatialIndex needs `rtree`")
+        RTreeIndex.__init__(self, *args)
+
+    @property
+    def size(self):
+        return len(self.leaves()[0][1])
+
+    @property
+    def is_empty(self):
+        if len(self.leaves()) > 1:
+            return False
+        return self.size < 1
diff --git a/tests/__init__.py b/geopandas/tests/__init__.py
similarity index 100%
copy from tests/__init__.py
copy to geopandas/tests/__init__.py
diff --git a/tests/baseline_images/test_plotting/lines_plot.png b/geopandas/tests/baseline_images/test_plotting/lines_plot.png
similarity index 100%
rename from tests/baseline_images/test_plotting/lines_plot.png
rename to geopandas/tests/baseline_images/test_plotting/lines_plot.png
diff --git a/tests/baseline_images/test_plotting/points_plot.png b/geopandas/tests/baseline_images/test_plotting/points_plot.png
similarity index 100%
rename from tests/baseline_images/test_plotting/points_plot.png
rename to geopandas/tests/baseline_images/test_plotting/points_plot.png
diff --git a/tests/baseline_images/test_plotting/poly_plot.png b/geopandas/tests/baseline_images/test_plotting/poly_plot.png
similarity index 100%
rename from tests/baseline_images/test_plotting/poly_plot.png
rename to geopandas/tests/baseline_images/test_plotting/poly_plot.png
diff --git a/geopandas/tests/baseline_images/test_plotting/poly_plot_with_kwargs.png b/geopandas/tests/baseline_images/test_plotting/poly_plot_with_kwargs.png
new file mode 100644
index 0000000..a2f88f3
Binary files /dev/null and b/geopandas/tests/baseline_images/test_plotting/poly_plot_with_kwargs.png differ
diff --git a/geopandas/tests/test_dissolve.py b/geopandas/tests/test_dissolve.py
new file mode 100644
index 0000000..45cc877
--- /dev/null
+++ b/geopandas/tests/test_dissolve.py
@@ -0,0 +1,83 @@
+from __future__ import absolute_import
+import tempfile
+import shutil
+import numpy as np
+from shapely.geometry import Point
+from geopandas import GeoDataFrame, read_file
+from geopandas.tools import overlay
+from .util import unittest, download_nybb
+from pandas.util.testing import assert_frame_equal
+from pandas import Index
+from distutils.version import LooseVersion
+import pandas as pd
+
+pandas_0_15_problem = 'fails under pandas < 0.16 due to issue 324,'\
+                      'not problem with dissolve.'
+
+class TestDataFrame(unittest.TestCase):
+
+    def setUp(self):
+
+        nybb_filename, nybb_zip_path = download_nybb()
+        self.polydf = read_file(nybb_zip_path, vfs='zip://' + nybb_filename)
+        self.polydf = self.polydf[['geometry', 'BoroName', 'BoroCode']]
+
+        self.polydf = self.polydf.rename(columns={'geometry':'myshapes'})
+        self.polydf = self.polydf.set_geometry('myshapes')
+
+        self.polydf['manhattan_bronx'] = 5
+        self.polydf.loc[3:4,'manhattan_bronx']=6
+
+        # Merged geometry
+        manhattan_bronx = self.polydf.loc[3:4,]
+        others = self.polydf.loc[0:2,]
+
+        collapsed = [others.geometry.unary_union, manhattan_bronx.geometry.unary_union]
+        merged_shapes = GeoDataFrame({'myshapes': collapsed}, geometry='myshapes',
+                             index=Index([5,6], name='manhattan_bronx'))
+
+        # Different expected results
+        self.first = merged_shapes.copy()
+        self.first['BoroName'] = ['Staten Island', 'Manhattan']
+        self.first['BoroCode'] = [5, 1]
+
+        self.mean = merged_shapes.copy()
+        self.mean['BoroCode'] = [4,1.5]
+
+
+    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.16'), pandas_0_15_problem)
+    def test_geom_dissolve(self):
+        test = self.polydf.dissolve('manhattan_bronx')
+        self.assertTrue(test.geometry.name == 'myshapes')
+        self.assertTrue(test.geom_almost_equals(self.first).all())
+
+    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.16'), pandas_0_15_problem)
+    def test_first_dissolve(self):
+        test = self.polydf.dissolve('manhattan_bronx')
+        assert_frame_equal(self.first, test, check_column_type=False)
+
+    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.16'), pandas_0_15_problem)
+    def test_mean_dissolve(self):
+        test = self.polydf.dissolve('manhattan_bronx', aggfunc='mean')
+        assert_frame_equal(self.mean, test, check_column_type=False)
+
+        test = self.polydf.dissolve('manhattan_bronx', aggfunc=np.mean)
+        assert_frame_equal(self.mean, test, check_column_type=False)
+
+    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.16'), pandas_0_15_problem)
+    def test_multicolumn_dissolve(self):
+        multi = self.polydf.copy()
+        multi['dup_col'] = multi.manhattan_bronx
+        multi_test = multi.dissolve(['manhattan_bronx', 'dup_col'], aggfunc='first')
+
+        first = self.first.copy()
+        first['dup_col'] = first.index
+        first = first.set_index([first.index, 'dup_col'])
+
+        assert_frame_equal(multi_test, first, check_column_type=False)
+
+    @unittest.skipIf(str(pd.__version__) < LooseVersion('0.16'), pandas_0_15_problem)
+    def test_reset_index(self):
+        test = self.polydf.dissolve('manhattan_bronx', as_index=False)
+        comparison = self.first.reset_index()
+        assert_frame_equal(comparison, test, check_column_type=False)
diff --git a/geopandas/tests/test_geocode.py b/geopandas/tests/test_geocode.py
new file mode 100644
index 0000000..5f7b23a
--- /dev/null
+++ b/geopandas/tests/test_geocode.py
@@ -0,0 +1,143 @@
+from __future__ import absolute_import
+
+from fiona.crs import from_epsg
+import pandas as pd
+import pandas.util.testing as tm
+from shapely.geometry import Point
+import geopandas as gpd
+import nose
+
+from geopandas import GeoSeries
+from geopandas.tools import geocode, reverse_geocode
+from geopandas.tools.geocoding import _prepare_geocode_result
+
+from geopandas.tests.util import unittest, mock, assert_geoseries_equal
+
+
+def _skip_if_no_geopy():
+    try:
+        import geopy
+    except ImportError:
+        raise nose.SkipTest("Geopy not installed. Skipping tests.")
+    except SyntaxError:
+        raise nose.SkipTest("Geopy is known to be broken on Python 3.2. "
+                            "Skipping tests.")
+
+
+class ForwardMock(mock.MagicMock):
+    """
+    Mock the forward geocoding function.
+    Returns the passed in address and (p, p+.5) where p increases
+    at each call
+
+    """
+    def __init__(self, *args, **kwargs):
+        super(ForwardMock, self).__init__(*args, **kwargs)
+        self._n = 0.0
+
+    def __call__(self, *args, **kwargs):
+        self.return_value = args[0], (self._n, self._n + 0.5)
+        self._n += 1
+        return super(ForwardMock, self).__call__(*args, **kwargs)
+
+
+class ReverseMock(mock.MagicMock):
+    """
+    Mock the reverse geocoding function.
+    Returns the passed in point and 'address{p}' where p increases
+    at each call
+
+    """
+    def __init__(self, *args, **kwargs):
+        super(ReverseMock, self).__init__(*args, **kwargs)
+        self._n = 0
+
+    def __call__(self, *args, **kwargs):
+        self.return_value = 'address{0}'.format(self._n), args[0]
+        self._n += 1
+        return super(ReverseMock, self).__call__(*args, **kwargs)
+
+
+class TestGeocode(unittest.TestCase):
+    def setUp(self):
+        _skip_if_no_geopy()
+        self.locations = ['260 Broadway, New York, NY',
+                          '77 Massachusetts Ave, Cambridge, MA']
+        self.points = [Point(-71.0597732, 42.3584308),
+                       Point(-77.0365305, 38.8977332)]
+
+    def test_prepare_result(self):
+        # Calls _prepare_result with sample results from the geocoder call
+        # loop
+        p0 = Point(12.3, -45.6) # Treat these as lat/lon
+        p1 = Point(-23.4, 56.7)
+        d = {'a': ('address0', p0.coords[0]),
+             'b': ('address1', p1.coords[0])}
+
+        df = _prepare_geocode_result(d)
+        assert type(df) is gpd.GeoDataFrame
+        self.assertEqual(from_epsg(4326), df.crs)
+        self.assertEqual(len(df), 2)
+        self.assert_('address' in df)
+
+        coords = df.loc['a']['geometry'].coords[0]
+        test = p0.coords[0]
+        # Output from the df should be lon/lat
+        self.assertAlmostEqual(coords[0], test[1])
+        self.assertAlmostEqual(coords[1], test[0])
+
+        coords = df.loc['b']['geometry'].coords[0]
+        test = p1.coords[0]
+        self.assertAlmostEqual(coords[0], test[1])
+        self.assertAlmostEqual(coords[1], test[0])
+
+    def test_prepare_result_none(self):
+        p0 = Point(12.3, -45.6) # Treat these as lat/lon
+        d = {'a': ('address0', p0.coords[0]),
+             'b': (None, None)}
+
+        df = _prepare_geocode_result(d)
+        assert type(df) is gpd.GeoDataFrame
+        self.assertEqual(from_epsg(4326), df.crs)
+        self.assertEqual(len(df), 2)
+        self.assert_('address' in df)
+
+        row = df.loc['b']
+        self.assertEqual(len(row['geometry'].coords), 0)
+        self.assert_(pd.np.isnan(row['address']))
+    
+    def test_bad_provider_forward(self):
+        with self.assertRaises(ValueError):
+            geocode(['cambridge, ma'], 'badprovider')
+
+    def test_bad_provider_reverse(self):
+        with self.assertRaises(ValueError):
+            reverse_geocode(['cambridge, ma'], 'badprovider')
+
+    def test_forward(self):
+        with mock.patch('geopy.geocoders.googlev3.GoogleV3.geocode',
+                        ForwardMock()) as m:
+            g = geocode(self.locations, provider='googlev3', timeout=2)
+            self.assertEqual(len(self.locations), m.call_count)
+
+        n = len(self.locations)
+        self.assertIsInstance(g, gpd.GeoDataFrame)
+        expected = GeoSeries([Point(float(x) + 0.5, float(x)) for x in range(n)],
+                             crs=from_epsg(4326))
+        assert_geoseries_equal(expected, g['geometry'])
+        tm.assert_series_equal(g['address'],
+                               pd.Series(self.locations, name='address'))
+
+    def test_reverse(self):
+        with mock.patch('geopy.geocoders.googlev3.GoogleV3.reverse',
+                        ReverseMock()) as m:
+            g = reverse_geocode(self.points, provider='googlev3', timeout=2)
+            self.assertEqual(len(self.points), m.call_count)
+
+        self.assertIsInstance(g, gpd.GeoDataFrame)
+
+        expected = GeoSeries(self.points, crs=from_epsg(4326))
+        assert_geoseries_equal(expected, g['geometry'])
+        address = pd.Series(['address' + str(x) for x in range(len(self.points))],
+                            name='address')
+        tm.assert_series_equal(g['address'], address)
diff --git a/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
similarity index 77%
rename from tests/test_geodataframe.py
rename to geopandas/tests/test_geodataframe.py
index e595b49..6739cd2 100644
--- a/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -12,8 +12,8 @@ from shapely.geometry import Point, Polygon
 
 import fiona
 from geopandas import GeoDataFrame, read_file, GeoSeries
-from .util import unittest, download_nybb, assert_geoseries_equal, connect, \
-                  create_db, validate_boro_df, PANDAS_NEW_SQL_API
+from geopandas.tests.util import assert_geoseries_equal, connect, create_db, \
+    download_nybb, PACKAGE_DIR, PANDAS_NEW_SQL_API, unittest, validate_boro_df
 
 
 class TestDataFrame(unittest.TestCase):
@@ -21,15 +21,19 @@ class TestDataFrame(unittest.TestCase):
     def setUp(self):
         N = 10
 
-        nybb_filename = download_nybb()
+        nybb_filename, nybb_zip_path = download_nybb()
 
-        self.df = read_file('/nybb_14a_av/nybb.shp', vfs='zip://' + nybb_filename)
+        self.df = read_file(nybb_zip_path, vfs='zip://' + nybb_filename)
+        with fiona.open(nybb_zip_path, vfs='zip://' + nybb_filename) as f:
+            self.schema = f.schema
         self.tempdir = tempfile.mkdtemp()
         self.boros = self.df['BoroName']
         self.crs = {'init': 'epsg:4326'}
         self.df2 = GeoDataFrame([
-            {'geometry' : Point(x, y), 'value1': x + y, 'value2': x * y}
+            {'geometry': Point(x, y), 'value1': x + y, 'value2': x * y}
             for x, y in zip(range(N), range(N))], crs=self.crs)
+        self.df3 = read_file(os.path.join(PACKAGE_DIR, 'examples', 'null_geom.geojson'))
+        self.line_paths = self.df3['Name']
 
     def tearDown(self):
         shutil.rmtree(self.tempdir)
@@ -51,16 +55,12 @@ class TestDataFrame(unittest.TestCase):
 
         geom2 = [Point(x, y) for x, y in zip(range(5, 10), range(5))]
         df2 = df.set_geometry(geom2, crs='dummy_crs')
-        self.assert_('geometry' in df2)
         self.assert_('location' in df2)
         self.assertEqual(df2.crs, 'dummy_crs')
         self.assertEqual(df2.geometry.crs, 'dummy_crs')
         # reset so it outputs okay
         df2.crs = df.crs
         assert_geoseries_equal(df2.geometry, GeoSeries(geom2, crs=df2.crs))
-        # for right now, non-geometry comes back as series
-        assert_geoseries_equal(df2['location'], df['location'],
-                                  check_series_type=False, check_dtype=False)
 
     def test_geo_getitem(self):
         data = {"A": range(5), "B": range(-5, 0),
@@ -86,7 +86,7 @@ class TestDataFrame(unittest.TestCase):
                                   check_dtype=True, check_index_type=True)
 
         df = self.df.copy()
-        new_geom = [Point(x,y) for x, y in zip(range(len(self.df)),
+        new_geom = [Point(x, y) for x, y in zip(range(len(self.df)),
                                                range(len(self.df)))]
         df.geometry = new_geom
 
@@ -131,7 +131,7 @@ class TestDataFrame(unittest.TestCase):
             df.geometry = df
 
     def test_set_geometry(self):
-        geom = GeoSeries([Point(x,y) for x,y in zip(range(5), range(5))])
+        geom = GeoSeries([Point(x, y) for x, y in zip(range(5), range(5))])
         original_geom = self.df.geometry
 
         df2 = self.df.set_geometry(geom)
@@ -178,7 +178,7 @@ class TestDataFrame(unittest.TestCase):
         assert_geoseries_equal(df3.geometry, g_simplified)
 
     def test_set_geometry_inplace(self):
-        geom = [Point(x,y) for x,y in zip(range(5), range(5))]
+        geom = [Point(x, y) for x, y in zip(range(5), range(5))]
         ret = self.df.set_geometry(geom, inplace=True)
         self.assert_(ret is None)
         geom = GeoSeries(geom, index=self.df.index, crs=self.df.crs)
@@ -224,7 +224,7 @@ class TestDataFrame(unittest.TestCase):
 
     def test_to_json_na(self):
         # Set a value as nan and make sure it's written
-        self.df['Shape_Area'][self.df['BoroName']=='Queens'] = np.nan
+        self.df.loc[self.df['BoroName']=='Queens', 'Shape_Area'] = np.nan
 
         text = self.df.to_json()
         data = json.loads(text)
@@ -241,8 +241,8 @@ class TestDataFrame(unittest.TestCase):
             text = self.df.to_json(na='garbage')
 
     def test_to_json_dropna(self):
-        self.df['Shape_Area'][self.df['BoroName']=='Queens'] = np.nan
-        self.df['Shape_Leng'][self.df['BoroName']=='Bronx'] = np.nan
+        self.df.loc[self.df['BoroName']=='Queens', 'Shape_Area'] = np.nan
+        self.df.loc[self.df['BoroName']=='Bronx', 'Shape_Leng'] = np.nan
 
         text = self.df.to_json(na='drop')
         data = json.loads(text)
@@ -263,8 +263,8 @@ class TestDataFrame(unittest.TestCase):
                 self.assertEqual(len(props), 4)
 
     def test_to_json_keepna(self):
-        self.df['Shape_Area'][self.df['BoroName']=='Queens'] = np.nan
-        self.df['Shape_Leng'][self.df['BoroName']=='Bronx'] = np.nan
+        self.df.loc[self.df['BoroName']=='Queens', 'Shape_Area'] = np.nan
+        self.df.loc[self.df['BoroName']=='Bronx', 'Shape_Leng'] = np.nan
 
         text = self.df.to_json(na='keep')
         data = json.loads(text)
@@ -290,12 +290,21 @@ class TestDataFrame(unittest.TestCase):
         """ Test to_file and from_file """
         tempfilename = os.path.join(self.tempdir, 'boros.shp')
         self.df.to_file(tempfilename)
-        # Read layer back in?
+        # Read layer back in
         df = GeoDataFrame.from_file(tempfilename)
         self.assertTrue('geometry' in df)
         self.assertTrue(len(df) == 5)
         self.assertTrue(np.alltrue(df['BoroName'].values == self.boros))
 
+        # Write layer with null geometry out to file
+        tempfilename = os.path.join(self.tempdir, 'null_geom.shp')
+        self.df3.to_file(tempfilename)
+        # Read layer back in
+        df3 = GeoDataFrame.from_file(tempfilename)
+        self.assertTrue('geometry' in df3)
+        self.assertTrue(len(df3) == 2)
+        self.assertTrue(np.alltrue(df3['Name'].values == self.line_paths))
+
     def test_to_file_types(self):
         """ Test various integer type columns (GH#93) """
         tempfilename = os.path.join(self.tempdir, 'int.shp')
@@ -310,11 +319,39 @@ class TestDataFrame(unittest.TestCase):
     def test_mixed_types_to_file(self):
         """ Test that mixed geometry types raise error when writing to file """
         tempfilename = os.path.join(self.tempdir, 'test.shp')
-        s = GeoDataFrame({'geometry' : [Point(0, 0),
+        s = GeoDataFrame({'geometry': [Point(0, 0),
                                         Polygon([(0, 0), (1, 0), (1, 1)])]})
         with self.assertRaises(ValueError):
             s.to_file(tempfilename)
 
+    def test_to_file_schema(self):
+        """
+        Ensure that the file is written according to the schema
+        if it is specified
+        
+        """
+        try:
+            from collections import OrderedDict
+        except ImportError:
+            from ordereddict import OrderedDict
+
+        tempfilename = os.path.join(self.tempdir, 'test.shp')
+        properties = OrderedDict([
+            ('Shape_Leng', 'float:19.11'),
+            ('BoroName', 'str:40'),
+            ('BoroCode', 'int:10'),
+            ('Shape_Area', 'float:19.11'),
+        ])
+        schema = {'geometry': 'Polygon', 'properties': properties}
+
+        # Take the first 2 features to speed things up a bit
+        self.df.iloc[:2].to_file(tempfilename, schema=schema)
+
+        with fiona.open(tempfilename) as f:
+            result_schema = f.schema
+
+        self.assertEqual(result_schema, schema)
+
     def test_bool_index(self):
         # Find boros with 'B' in their name
         df = self.df[self.df['BoroName'].str.contains('B')]
@@ -331,9 +368,21 @@ class TestDataFrame(unittest.TestCase):
         utm = lonlat.to_crs(epsg=26918)
         self.assertTrue(all(df2['geometry'].geom_almost_equals(utm['geometry'], decimal=2)))
 
+    def test_to_crs_geo_column_name(self):
+        # Test to_crs() with different geometry column name (GH#339)
+        df2 = self.df2.copy()
+        df2.crs = {'init': 'epsg:26918', 'no_defs': True}
+        df2 = df2.rename(columns={'geometry': 'geom'})
+        df2.set_geometry('geom', inplace=True)
+        lonlat = df2.to_crs(epsg=4326)
+        utm = lonlat.to_crs(epsg=26918)
+        self.assertEqual(lonlat.geometry.name, 'geom')
+        self.assertEqual(utm.geometry.name, 'geom')
+        self.assertTrue(all(df2.geometry.geom_almost_equals(utm.geometry, decimal=2)))
+
     def test_from_features(self):
-        nybb_filename = download_nybb()
-        with fiona.open('/nybb_14a_av/nybb.shp',
+        nybb_filename, nybb_zip_path = download_nybb()
+        with fiona.open(nybb_zip_path,
                         vfs='zip://' + nybb_filename) as f:
             features = list(f)
             crs = f.crs
@@ -344,17 +393,17 @@ class TestDataFrame(unittest.TestCase):
         self.assert_(df.crs == crs)
 
     def test_from_features_unaligned_properties(self):
-        p1 = Point(1,1)
-        f1 = {'type': 'Feature', 
-                'properties': {'a': 0}, 
+        p1 = Point(1, 1)
+        f1 = {'type': 'Feature',
+                'properties': {'a': 0},
                 'geometry': p1.__geo_interface__}
 
-        p2 = Point(2,2)
+        p2 = Point(2, 2)
         f2 = {'type': 'Feature',
                 'properties': {'b': 1},
                 'geometry': p2.__geo_interface__}
 
-        p3 = Point(3,3)
+        p3 = Point(3, 3)
         f3 = {'type': 'Feature',
                 'properties': {'a': 2},
                 'geometry': p3.__geo_interface__}
@@ -427,3 +476,29 @@ 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])
+
+    def test_geodataframe_geojson_no_bbox(self):
+        geo = self.df._to_geo(na="null", show_bbox=False)
+        self.assertFalse('bbox' in geo.keys())
+        for feature in geo['features']:
+            self.assertFalse('bbox' in feature.keys())
+
+    def test_geodataframe_geojson_bbox(self):
+        geo = self.df._to_geo(na="null", show_bbox=True)
+        self.assertTrue('bbox' in geo.keys())
+        self.assertEqual(len(geo['bbox']), 4)
+        self.assertTrue(isinstance(geo['bbox'], tuple))
+        for feature in geo['features']:
+            self.assertTrue('bbox' in feature.keys())
+
+    def test_pickle(self):
+        filename = os.path.join(self.tempdir, 'df.pkl')
+        self.df.to_pickle(filename)
+        unpickled = pd.read_pickle(filename)
+        assert_frame_equal(self.df, unpickled)
+        self.assertEqual(self.df.crs, unpickled.crs)
diff --git a/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
similarity index 88%
rename from tests/test_geom_methods.py
rename to geopandas/tests/test_geom_methods.py
index 9e87965..0bccec3 100644
--- a/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -5,21 +5,26 @@ import string
 import numpy as np
 from numpy.testing import assert_array_equal
 from pandas.util.testing import assert_series_equal, assert_frame_equal
-from pandas import Series, DataFrame
-from shapely.geometry import Point, LinearRing, LineString, Polygon
+from pandas import Series, DataFrame, MultiIndex
+from shapely.geometry import (
+    Point, LinearRing, LineString, Polygon, MultiPoint
+)
 from shapely.geometry.collection import GeometryCollection
+from shapely.ops import unary_union
 
 from geopandas import GeoSeries, GeoDataFrame
 from geopandas.base import GeoPandasBase
-from .util import (
+from geopandas.tests.util import (
     unittest, geom_equals, geom_almost_equals, assert_geoseries_equal
 )
 
+
 class TestGeomMethods(unittest.TestCase):
 
     def setUp(self):
         self.t1 = Polygon([(0, 0), (1, 0), (1, 1)])
         self.t2 = Polygon([(0, 0), (1, 1), (0, 1)])
+        self.t3 = Polygon([(2, 0), (3, 0), (3, 1)])
         self.sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
         self.inner_sq = Polygon([(0.25, 0.25), (0.75, 0.25), (0.75, 0.75),
                             (0.25, 0.75)])
@@ -34,7 +39,7 @@ class TestGeomMethods(unittest.TestCase):
         self.g3.crs = {'init': 'epsg:4326', 'no_defs': True}
         self.g4 = GeoSeries([self.t2, self.t1])
         self.na = GeoSeries([self.t1, self.t2, Polygon()])
-        self.na_none = GeoSeries([self.t1, self.t2, None])
+        self.na_none = GeoSeries([self.t1, None])
         self.a1 = self.g1.copy()
         self.a1.index = ['A', 'B']
         self.a2 = self.g2.copy()
@@ -46,6 +51,7 @@ class TestGeomMethods(unittest.TestCase):
         self.l1 = LineString([(0, 0), (0, 1), (1, 1)])
         self.l2 = LineString([(0, 0), (1, 0), (1, 1), (0, 1)])
         self.g5 = GeoSeries([self.l1, self.l2])
+        self.g6 = GeoSeries([self.p0, self.t3])
 
         # Crossed lines
         self.l3 = LineString([(0, 0), (1, 1)])
@@ -71,7 +77,7 @@ class TestGeomMethods(unittest.TestCase):
         if isinstance(expected, GeoPandasBase):
             fcmp = assert_geoseries_equal
         else:
-            fcmp = lambda a, b: self.assert_(geom_equals(a, b))
+            fcmp = lambda a, b: self.assert_(a.equals(b))
         self._test_unary(op, expected, a, fcmp)
 
     def _test_binary_topological(self, op, expected, a, b, *args, **kwargs):
@@ -219,6 +225,9 @@ class TestGeomMethods(unittest.TestCase):
         expected = Series(np.array([0.5, 1.0]), index=self.g1.index)
         self._test_unary_real('area', expected, self.g1)
 
+        expected = Series(np.array([0.5, np.nan]), index=self.na_none.index)
+        self._test_unary_real('area', expected, self.na_none)
+
     def test_bounds(self):
         # Set columns to get the order right
         expected = DataFrame({'minx': [0.0, 0.0], 'miny': [0.0, 0.0],
@@ -233,6 +242,14 @@ class TestGeomMethods(unittest.TestCase):
         result = gdf.bounds
         assert_frame_equal(expected, result)
 
+    def test_unary_union(self):
+        p1 = self.t1
+        p2 = Polygon([(2, 0), (3, 0), (3, 1)])
+        expected = unary_union([p1, p2])
+        g = GeoSeries([p1, p2])
+
+        self._test_unary_topological('unary_union', expected, g)
+
     def test_contains(self):
         expected = [True, False, True, False, False, False]
         assert_array_equal(expected, self.g0.contains(self.t1))
@@ -241,6 +258,11 @@ class TestGeomMethods(unittest.TestCase):
         expected = Series(np.array([2 + np.sqrt(2), 4]), index=self.g1.index)
         self._test_unary_real('length', expected, self.g1)
 
+        expected = Series(
+                        np.array([2 + np.sqrt(2), np.nan]),
+                        index=self.na_none.index)
+        self._test_unary_real('length', expected, self.na_none)
+
     def test_crosses(self):
         expected = [False, False, False, False, False, False]
         assert_array_equal(expected, self.g0.crosses(self.t1))
@@ -252,10 +274,24 @@ class TestGeomMethods(unittest.TestCase):
         expected = [False, False, False, False, False, True]
         assert_array_equal(expected, self.g0.disjoint(self.t1))
 
+    def test_distance(self):
+        expected = Series(np.array([
+                          np.sqrt((5 - 1)**2 + (5 - 1)**2),
+                          np.nan]),
+                    self.na_none.index)
+        assert_array_equal(expected, self.na_none.distance(self.p0))
+
+        expected = Series(np.array([np.sqrt(4**2 + 4**2), np.nan]),
+                          self.g6.index)
+        assert_array_equal(expected, self.g6.distance(self.na_none))
+
     def test_intersects(self):
         expected = [True, True, True, True, True, False]
         assert_array_equal(expected, self.g0.intersects(self.t1))
 
+        expected = [True, False]
+        assert_array_equal(expected, self.na_none.intersects(self.t2))
+
     def test_overlaps(self):
         expected = [True, True, False, False, False, False]
         assert_array_equal(expected, self.g0.overlaps(self.inner_sq))
@@ -389,6 +425,19 @@ class TestGeomMethods(unittest.TestCase):
                            'col1': range(len(self.landmarks))})
         self.assert_(df.total_bounds, bbox)
 
+    def test_explode(self):
+        s = GeoSeries([MultiPoint([(0,0), (1,1)]),
+                      MultiPoint([(2,2), (3,3), (4,4)])])
+
+        index = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 2)]
+        expected = GeoSeries([Point(0,0), Point(1,1), Point(2,2), Point(3,3),
+                              Point(4,4)], index=MultiIndex.from_tuples(index))
+
+        assert_geoseries_equal(expected, s.explode())
+
+        df = self.gdf1[:2].set_geometry(s)
+        assert_geoseries_equal(expected, df.explode())
+
     #
     # Test '&', '|', '^', and '-'
     # The left can only be a GeoSeries. The right hand side can be a
diff --git a/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
similarity index 73%
rename from tests/test_geoseries.py
rename to geopandas/tests/test_geoseries.py
index daa21d4..42b585b 100644
--- a/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -1,16 +1,16 @@
 from __future__ import absolute_import
 
 import os
+import json
 import shutil
 import tempfile
 import numpy as np
 from numpy.testing import assert_array_equal
-from pandas import Series
 from shapely.geometry import (Polygon, Point, LineString,
                               MultiPoint, MultiLineString, MultiPolygon)
 from shapely.geometry.base import BaseGeometry
 from geopandas import GeoSeries
-from .util import unittest, geom_equals, geom_almost_equals
+from geopandas.tests.util import unittest, geom_equals
 
 
 class TestSeries(unittest.TestCase):
@@ -116,6 +116,12 @@ class TestSeries(unittest.TestCase):
         self.assertTrue(all(self.g3.geom_equals(s)))
         # TODO: compare crs
 
+    def test_to_json(self):
+        """Test whether GeoSeries.to_json works and returns an actual json file."""
+        json_str = self.g3.to_json()
+        json_dict = json.loads(json_str)
+        # TODO : verify the output is a valid GeoJSON.
+
     def test_representative_point(self):
         self.assertTrue(np.alltrue(self.g1.contains(self.g1.representative_point())))
         self.assertTrue(np.alltrue(self.g2.contains(self.g2.representative_point())))
@@ -139,5 +145,40 @@ class TestSeries(unittest.TestCase):
         # XXX: method works inconsistently for different pandas versions
         #self.na_none.fillna(method='backfill')
 
+    def test_coord_slice(self):
+        """ Test CoordinateSlicer """
+        # need some better test cases
+        self.assertTrue(geom_equals(self.g3, self.g3.cx[:, :]))
+        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])
+
+    def test_proj4strings(self):
+        # As string
+        reprojected = self.g3.to_crs('+proj=utm +zone=30N')
+        reprojected_back = reprojected.to_crs(epsg=4326)
+        self.assertTrue(np.alltrue(self.g3.geom_almost_equals(reprojected_back)))
+
+        # As dict
+        reprojected = self.g3.to_crs({'proj': 'utm', 'zone': '30N'})
+        reprojected_back = reprojected.to_crs(epsg=4326)
+        self.assertTrue(np.alltrue(self.g3.geom_almost_equals(reprojected_back)))
+
+        # Set to equivalent string, convert, compare to original
+        copy = self.g3.copy()
+        copy.crs = '+init=epsg:4326'
+        reprojected = copy.to_crs({'proj': 'utm', 'zone': '30N'})
+        reprojected_back = reprojected.to_crs(epsg=4326)
+        self.assertTrue(np.alltrue(self.g3.geom_almost_equals(reprojected_back)))
+
+        # Conversions by different format
+        reprojected_string = self.g3.to_crs('+proj=utm +zone=30N')
+        reprojected_dict = self.g3.to_crs({'proj': 'utm', 'zone': '30N'})
+        self.assertTrue(np.alltrue(reprojected_string.geom_almost_equals(reprojected_dict)))
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
new file mode 100644
index 0000000..17cd238
--- /dev/null
+++ b/geopandas/tests/test_merge.py
@@ -0,0 +1,62 @@
+from __future__ import absolute_import
+
+import pandas as pd
+from shapely.geometry import Point
+
+from geopandas import GeoDataFrame, GeoSeries
+from geopandas.tests.util import unittest
+
+
+class TestMerging(unittest.TestCase):
+
+    def setUp(self):
+
+        self.gseries = GeoSeries([Point(i, i) for i in range(3)])
+        self.series = pd.Series([1, 2, 3])
+        self.gdf = GeoDataFrame({'geometry': self.gseries, 'values': range(3)})
+        self.df = pd.DataFrame({'col1': [1, 2, 3], 'col2': [0.1, 0.2, 0.3]})
+
+    def _check_metadata(self, gdf, geometry_column_name='geometry', crs=None):
+
+        self.assertEqual(gdf._geometry_column_name, geometry_column_name)
+        self.assertEqual(gdf.crs, crs)
+
+    def test_merge(self):
+
+        res = self.gdf.merge(self.df, left_on='values', right_on='col1')
+
+        # check result is a GeoDataFrame
+        self.assert_(isinstance(res, GeoDataFrame))
+
+        # check geometry property gives GeoSeries
+        self.assert_(isinstance(res.geometry, GeoSeries))
+
+        # check metadata
+        self._check_metadata(res)
+
+        ## test that crs and other geometry name are preserved
+        self.gdf.crs = {'init' :'epsg:4326'}
+        self.gdf = (self.gdf.rename(columns={'geometry': 'points'})
+                            .set_geometry('points'))
+        res = self.gdf.merge(self.df, left_on='values', right_on='col1')
+        self.assert_(isinstance(res, GeoDataFrame))
+        self.assert_(isinstance(res.geometry, GeoSeries))
+        self._check_metadata(res, 'points', self.gdf.crs)
+
+    def test_concat_axis0(self):
+
+        res = pd.concat([self.gdf, self.gdf])
+
+        self.assertEqual(res.shape, (6, 2))
+        self.assert_(isinstance(res, GeoDataFrame))
+        self.assert_(isinstance(res.geometry, GeoSeries))
+        self._check_metadata(res)
+
+    def test_concat_axis1(self):
+
+        res = pd.concat([self.gdf, self.df], axis=1)
+
+        self.assertEqual(res.shape, (3, 4))
+        self.assert_(isinstance(res, GeoDataFrame))
+        self.assert_(isinstance(res.geometry, GeoSeries))
+        self._check_metadata(res)
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
new file mode 100644
index 0000000..051618a
--- /dev/null
+++ b/geopandas/tests/test_overlay.py
@@ -0,0 +1,114 @@
+from __future__ import absolute_import
+
+import tempfile
+import shutil
+
+from shapely.geometry import Point
+
+from geopandas import GeoDataFrame, read_file
+from geopandas.tests.util import unittest, download_nybb
+from geopandas import overlay
+
+
+class TestDataFrame(unittest.TestCase):
+
+    def setUp(self):
+        N = 10
+
+        nybb_filename, nybb_zip_path = download_nybb()
+
+        self.polydf = read_file(nybb_zip_path, vfs='zip://' + nybb_filename)
+        self.tempdir = tempfile.mkdtemp()
+        self.crs = {'init': 'epsg:4326'}
+        b = [int(x) for x in self.polydf.total_bounds]
+        self.polydf2 = GeoDataFrame([
+            {'geometry' : Point(x, y).buffer(10000), 'value1': x + y, 'value2': x - y}
+            for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),
+                            range(b[1], b[3], int((b[3]-b[1])/N)))], crs=self.crs)
+        self.pointdf = GeoDataFrame([
+            {'geometry' : Point(x, y), 'value1': x + y, 'value2': x - y}
+            for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),
+                            range(b[1], b[3], int((b[3]-b[1])/N)))], crs=self.crs)
+
+        # TODO this appears to be necessary;
+        # why is the sindex not generated automatically?
+        self.polydf2._generate_sindex()
+
+        self.union_shape = (180, 7)
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+
+    def test_union(self):
+        df = overlay(self.polydf, self.polydf2, how="union")
+        self.assertTrue(type(df) is GeoDataFrame)
+        self.assertEquals(df.shape, self.union_shape)
+        self.assertTrue('value1' in df.columns and 'Shape_Area' in df.columns)
+
+    def test_union_no_index(self):
+        # explicitly ignore indicies
+        dfB = overlay(self.polydf, self.polydf2, how="union", use_sindex=False)
+        self.assertEquals(dfB.shape, self.union_shape)
+
+        # remove indicies from df
+        self.polydf._sindex = None
+        self.polydf2._sindex = None
+        dfC = overlay(self.polydf, self.polydf2, how="union")
+        self.assertEquals(dfC.shape, self.union_shape)
+
+    def test_intersection(self):
+        df = overlay(self.polydf, self.polydf2, how="intersection")
+        self.assertIsNotNone(df['BoroName'][0])
+        self.assertEquals(df.shape, (68, 7))
+
+    def test_identity(self):
+        df = overlay(self.polydf, self.polydf2, how="identity")
+        self.assertEquals(df.shape, (154, 7))
+
+    def test_symmetric_difference(self):
+        df = overlay(self.polydf, self.polydf2, how="symmetric_difference")
+        self.assertEquals(df.shape, (122, 7))
+
+    def test_difference(self):
+        df = overlay(self.polydf, self.polydf2, how="difference")
+        self.assertEquals(df.shape, (86, 7))
+
+    def test_bad_how(self):
+        self.assertRaises(ValueError,
+                          overlay, self.polydf, self.polydf, how="spandex")
+
+    def test_nonpoly(self):
+        self.assertRaises(TypeError,
+                          overlay, self.pointdf, self.polydf, how="union")
+
+    def test_duplicate_column_name(self):
+        polydf2r = self.polydf2.rename(columns={'value2': 'Shape_Area'})
+        df = overlay(self.polydf, polydf2r, how="union")
+        self.assertTrue('Shape_Area_2' in df.columns and 'Shape_Area' in df.columns)
+
+    def test_geometry_not_named_geometry(self):
+        # Issue #306
+        # Add points and flip names
+        polydf3 = self.polydf.copy()
+        polydf3 = polydf3.rename(columns={'geometry':'polygons'})
+        polydf3 = polydf3.set_geometry('polygons')
+        polydf3['geometry'] = self.pointdf.geometry.loc[0:4]
+        self.assertTrue(polydf3.geometry.name == 'polygons')
+
+        df = overlay(polydf3, self.polydf2, how="union")
+        self.assertTrue(type(df) is GeoDataFrame)
+        
+        df2 = overlay(self.polydf, self.polydf2, how="union")
+        self.assertTrue(df.geom_almost_equals(df2).all())
+
+    def test_geoseries_warning(self):
+        # Issue #305
+
+        def f():
+            overlay(self.polydf, self.polydf2.geometry, how="union")
+        self.assertRaises(NotImplementedError, f)
+
+
+
+
+
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
new file mode 100644
index 0000000..ac6a468
--- /dev/null
+++ b/geopandas/tests/test_plotting.py
@@ -0,0 +1,311 @@
+from __future__ import absolute_import, division
+
+import numpy as np
+import os
+import shutil
+import tempfile
+from distutils.version import LooseVersion
+
+import matplotlib
+matplotlib.use('Agg', warn=False)
+from matplotlib.pyplot import Artist, savefig, clf, cm, get_cmap
+from matplotlib.testing.noseclasses import ImageComparisonFailure
+from matplotlib.testing.compare import compare_images
+from numpy import cos, sin, pi
+from shapely.geometry import Polygon, LineString, Point
+from six.moves import xrange
+from .util import unittest
+
+from geopandas import GeoSeries, GeoDataFrame, read_file
+
+
+# If set to True, generate images rather than perform tests (all tests will pass!)
+GENERATE_BASELINE = False
+
+BASELINE_DIR = os.path.join(os.path.dirname(__file__), 'baseline_images', 'test_plotting')
+
+TRAVIS = bool(os.environ.get('TRAVIS', False))
+MPL_DEV = matplotlib.__version__ > LooseVersion('1.5.1')
+
+class TestImageComparisons(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.tempdir = tempfile.mkdtemp()
+        return
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.tempdir)
+        return
+
+    def _compare_images(self, ax, filename, tol=10):
+        """ Helper method to do the comparisons """
+        assert isinstance(ax, Artist)
+        if GENERATE_BASELINE:
+            savefig(os.path.join(BASELINE_DIR, filename))
+        savefig(os.path.join(self.tempdir, filename))
+        err = compare_images(os.path.join(BASELINE_DIR, filename),
+                             os.path.join(self.tempdir, filename),
+                             tol, in_decorator=True)
+        if err:
+            raise ImageComparisonFailure('images not close: %(actual)s '
+                                         'vs. %(expected)s '
+                                         '(RMS %(rms).3f)' % err)
+
+    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
+    def test_poly_plot(self):
+        """ Test plotting a simple series of polygons """
+        clf()
+        filename = 'poly_plot.png'
+        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
+        t2 = Polygon([(1, 0), (2, 0), (2, 1)])
+        polys = GeoSeries([t1, t2])
+        ax = polys.plot()
+        self._compare_images(ax=ax, filename=filename)
+
+    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
+    def test_point_plot(self):
+        """ Test plotting a simple series of points """
+        clf()
+        filename = 'points_plot.png'
+        N = 10
+        points = GeoSeries(Point(i, i) for i in xrange(N))
+        ax = points.plot()
+        self._compare_images(ax=ax, filename=filename)
+
+    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
+    def test_line_plot(self):
+        """ Test plotting a simple series of lines """
+        clf()
+        filename = 'lines_plot.png'
+        N = 10
+        lines = GeoSeries([LineString([(0, i), (9, i)]) for i in xrange(N)])
+        ax = lines.plot()
+        self._compare_images(ax=ax, filename=filename)
+
+    @unittest.skipIf(TRAVIS, 'Skip on Travis (fails even though it passes locally)')
+    def test_plot_GeoDataFrame_with_kwargs(self):
+        """
+        Test plotting a simple GeoDataFrame consisting of a series of polygons
+        with increasing values using various extra kwargs.
+        """
+        clf()
+        filename = 'poly_plot_with_kwargs.png'
+        ts = np.linspace(0, 2*pi, 10, endpoint=False)
+
+        # Build GeoDataFrame from a series of triangles wrapping around in a ring
+        # and a second column containing a list of increasing values.
+        r1 = 1.0  # radius of inner ring boundary
+        r2 = 1.5  # radius of outer ring boundary
+
+        def make_triangle(t0, t1):
+            return Polygon([(r1*cos(t0), r1*sin(t0)),
+                            (r2*cos(t0), r2*sin(t0)),
+                            (r1*cos(t1), r1*sin(t1))])
+
+        polys = GeoSeries([make_triangle(t0, t1) for t0, t1 in zip(ts, ts[1:])])
+        values = np.arange(len(polys))
+        df = GeoDataFrame({'geometry': polys, 'values': values})
+
+        # Plot the GeoDataFrame using various keyword arguments to see if they are honoured
+        ax = df.plot(column='values', cmap=cm.RdBu, vmin=+2, vmax=None, figsize=(8, 4))
+        self._compare_images(ax=ax, filename=filename)
+
+
+
+class TestPointPlotting(unittest.TestCase):
+
+    def setUp(self):
+
+        self.N = 10
+        self.points = GeoSeries(Point(i, i) for i in range(self.N))
+        values = np.arange(self.N)
+        self.df = GeoDataFrame({'geometry': self.points, 'values': values})
+
+    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
+    def test_default_colors(self):
+
+        ## without specifying values -> max 9 different colors
+
+        # GeoSeries
+        ax = self.points.plot()
+        cmap = get_cmap('Set1', 9)
+        expected_colors = cmap(list(range(9))*2)
+        _check_colors(ax.get_lines(), expected_colors)
+
+        # GeoDataFrame -> uses 'jet' instead of 'Set1'
+        ax = self.df.plot()
+        cmap = get_cmap('jet', 9)
+        expected_colors = cmap(list(range(9))*2)
+        _check_colors(ax.get_lines(), expected_colors)
+
+        ## with specifying values
+
+        ax = self.df.plot(column='values')
+        cmap = get_cmap('jet')
+        expected_colors = cmap(np.arange(self.N)/(self.N-1))
+
+        _check_colors(ax.get_lines(), expected_colors)
+
+    def test_colormap(self):
+
+        ## without specifying values -> max 9 different colors
+
+        # GeoSeries
+        ax = self.points.plot(cmap='RdYlGn')
+        cmap = get_cmap('RdYlGn', 9)
+        expected_colors = cmap(list(range(9))*2)
+        _check_colors(ax.get_lines(), expected_colors)
+
+        # GeoDataFrame -> same as GeoSeries in this case
+        ax = self.df.plot(cmap='RdYlGn')
+        _check_colors(ax.get_lines(), expected_colors)
+
+        ## with specifying values
+
+        ax = self.df.plot(column='values', cmap='RdYlGn')
+        cmap = get_cmap('RdYlGn')
+        expected_colors = cmap(np.arange(self.N)/(self.N-1))
+        _check_colors(ax.get_lines(), expected_colors)
+
+    def test_single_color(self):
+
+        ax = self.points.plot(color='green')
+        _check_colors(ax.get_lines(), ['green']*self.N)
+
+        ax = self.df.plot(color='green')
+        _check_colors(ax.get_lines(), ['green']*self.N)
+
+        ax = self.df.plot(column='values', color='green')
+        _check_colors(ax.get_lines(), ['green']*self.N)
+
+    def test_style_kwargs(self):
+
+        # markersize
+        ax = self.points.plot(markersize=10)
+        ms = [l.get_markersize() for l in ax.get_lines()]
+        assert ms == [10] * self.N
+
+        ax = self.df.plot(markersize=10)
+        ms = [l.get_markersize() for l in ax.get_lines()]
+        assert ms == [10] * self.N
+
+        ax = self.df.plot(column='values', markersize=10)
+        ms = [l.get_markersize() for l in ax.get_lines()]
+        assert ms == [10] * self.N
+
+
+class TestLineStringPlotting(unittest.TestCase):
+
+    def setUp(self):
+
+        self.N = 10
+        values = np.arange(self.N)
+        self.lines = GeoSeries([LineString([(0, i), (9, i)]) for i in xrange(self.N)])
+        self.df = GeoDataFrame({'geometry': self.lines, 'values': values})
+
+    def test_single_color(self):
+
+        ax = self.lines.plot(color='green')
+        _check_colors(ax.get_lines(), ['green']*self.N)
+
+        ax = self.df.plot(color='green')
+        _check_colors(ax.get_lines(), ['green']*self.N)
+
+        ax = self.df.plot(column='values', color='green')
+        _check_colors(ax.get_lines(), ['green']*self.N)
+
+    def test_style_kwargs(self):
+
+        # linestyle
+        ax = self.lines.plot(linestyle='dashed')
+        ls = [l.get_linestyle() for l in ax.get_lines()]
+        assert ls == ['--'] * self.N
+
+        ax = self.df.plot(linestyle='dashed')
+        ls = [l.get_linestyle() for l in ax.get_lines()]
+        assert ls == ['--'] * self.N
+
+        ax = self.df.plot(column='values', linestyle='dashed')
+        ls = [l.get_linestyle() for l in ax.get_lines()]
+        assert ls == ['--'] * self.N
+
+
+class TestPolygonPlotting(unittest.TestCase):
+
+    def setUp(self):
+
+        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
+        t2 = Polygon([(1, 0), (2, 0), (2, 1)])
+        self.polys = GeoSeries([t1, t2])
+        self.df = GeoDataFrame({'geometry': self.polys, 'values': [0, 1]})
+        return
+
+    def test_single_color(self):
+
+        ax = self.polys.plot(color='green')
+        _check_colors(ax.patches, ['green']*2, alpha=0.5)
+
+        ax = self.df.plot(color='green')
+        _check_colors(ax.patches, ['green']*2, alpha=0.5)
+
+        ax = self.df.plot(column='values', color='green')
+        _check_colors(ax.patches, ['green']*2, alpha=0.5)
+
+    def test_vmin_vmax(self):
+
+        # when vmin == vmax, all polygons should be the same color
+        ax = self.df.plot(column='values', categorical=True, vmin=0, vmax=0)
+        cmap = get_cmap('Set1', 2)
+        self.assertEqual(ax.patches[0].get_facecolor(), ax.patches[1].get_facecolor())
+
+    def test_facecolor(self):
+        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
+        t2 = Polygon([(1, 0), (2, 0), (2, 1)])
+        polys = GeoSeries([t1, t2])
+        df = GeoDataFrame({'geometry': polys, 'values': [0, 1]})
+
+        ax = polys.plot(facecolor='k')
+        _check_colors(ax.patches, ['k']*2, alpha=0.5)
+
+
+class TestPySALPlotting(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        try:
+            import pysal as ps
+        except ImportError:
+            raise unittest.SkipTest("PySAL is not installed")
+
+        pth = ps.examples.get_path("columbus.shp")
+        cls.tracts = read_file(pth)
+
+    def test_legend(self):
+        ax = self.tracts.plot(column='CRIME', scheme='QUANTILES', k=3,
+                         cmap='OrRd', legend=True)
+
+        labels = [t.get_text() for t in ax.get_legend().get_texts()]
+        expected = [u'0.00 - 26.07', u'26.07 - 41.97', u'41.97 - 68.89']
+        self.assertEqual(labels, expected)
+
+
+def _check_colors(collection, expected_colors, alpha=None):
+
+    from matplotlib.lines import Line2D
+    import matplotlib.colors as colors
+    conv = colors.colorConverter
+
+    for patch, color in zip(collection, expected_colors):
+        if isinstance(patch, Line2D):
+            # points/lines
+            result = patch.get_color()
+        else:
+            # polygons
+            result = patch.get_facecolor()
+        assert conv.to_rgba(result) == conv.to_rgba(color, alpha=alpha)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/geopandas/tests/test_sindex.py b/geopandas/tests/test_sindex.py
new file mode 100644
index 0000000..d978d20
--- /dev/null
+++ b/geopandas/tests/test_sindex.py
@@ -0,0 +1,117 @@
+from shapely.geometry import Polygon, Point
+
+from geopandas import GeoSeries, GeoDataFrame, base, read_file
+from geopandas.tests.util import unittest, download_nybb
+
+
+ at unittest.skipIf(not base.HAS_SINDEX, 'Rtree absent, skipping')
+class TestSeriesSindex(unittest.TestCase):
+
+    def test_empty_index(self):
+        self.assert_(GeoSeries().sindex is None)
+
+    def test_point(self):
+        s = GeoSeries([Point(0, 0)])
+        self.assertEqual(s.sindex.size, 1)
+        hits = s.sindex.intersection((-1, -1, 1, 1))
+        self.assertEqual(len(list(hits)), 1)
+        hits = s.sindex.intersection((-2, -2, -1, -1))
+        self.assertEqual(len(list(hits)), 0)
+
+    def test_empty_point(self):
+        s = GeoSeries([Point()])
+        self.assert_(GeoSeries().sindex is None)
+
+    def test_polygons(self):
+        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
+        t2 = Polygon([(0, 0), (1, 1), (0, 1)])
+        sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
+        s = GeoSeries([t1, t2, sq])
+        self.assertEqual(s.sindex.size, 3)
+
+    def test_polygons_append(self):
+        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
+        t2 = Polygon([(0, 0), (1, 1), (0, 1)])
+        sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
+        s = GeoSeries([t1, t2, sq])
+        t = GeoSeries([t1, t2, sq], [3,4,5])
+        s = s.append(t)
+        self.assertEqual(len(s), 6)
+        self.assertEqual(s.sindex.size, 6)
+
+    def test_lazy_build(self):
+        s = GeoSeries([Point(0, 0)])
+        self.assert_(s._sindex is None)
+        self.assertEqual(s.sindex.size, 1)
+        self.assert_(s._sindex is not None)
+
+
+ at unittest.skipIf(not base.HAS_SINDEX, 'Rtree absent, skipping')
+class TestFrameSindex(unittest.TestCase):
+    def setUp(self):
+        data = {"A": range(5), "B": range(-5, 0),
+                "location": [Point(x, y) for x, y in zip(range(5), range(5))]}
+        self.df = GeoDataFrame(data, geometry='location')
+
+    def test_sindex(self):
+        self.df.crs = {'init': 'epsg:4326'}
+        self.assertEqual(self.df.sindex.size, 5)
+        hits = list(self.df.sindex.intersection((2.5, 2.5, 4, 4), objects=True))
+        self.assertEqual(len(hits), 2)
+        self.assertEqual(hits[0].object, 3)
+
+    def test_lazy_build(self):
+        self.assert_(self.df._sindex is None)
+        self.assertEqual(self.df.sindex.size, 5)
+        self.assert_(self.df._sindex is not None)
+
+    def test_sindex_rebuild_on_set_geometry(self):
+        # First build the sindex
+        self.assert_(self.df.sindex is not None)
+        self.df.set_geometry(
+            [Point(x, y) for x, y in zip(range(5, 10), range(5, 10))],
+            inplace=True)
+        self.assert_(self.df._sindex_valid == False)
+
+
+# Skip to accommodate Shapely geometries being unhashable
+ at unittest.skip
+class TestJoinSindex(unittest.TestCase):
+
+    def setUp(self):
+        nybb_filename, nybb_zip_path = download_nybb()
+        self.boros = read_file(nybb_zip_path, vfs='zip://' + nybb_filename)
+
+    def test_merge_geo(self):
+        # First check that we gets hits from the boros frame.
+        tree = self.boros.sindex
+        hits = tree.intersection((1012821.80, 229228.26), objects=True)
+        self.assertEqual(
+            [self.boros.ix[hit.object]['BoroName'] for hit in hits],
+            ['Bronx', 'Queens'])
+
+        # Check that we only get the Bronx from this view.
+        first = self.boros[self.boros['BoroCode'] < 3]
+        tree = first.sindex
+        hits = tree.intersection((1012821.80, 229228.26), objects=True)
+        self.assertEqual(
+            [first.ix[hit.object]['BoroName'] for hit in hits],
+            ['Bronx'])
+
+        # Check that we only get Queens from this view.
+        second = self.boros[self.boros['BoroCode'] >= 3]
+        tree = second.sindex
+        hits = tree.intersection((1012821.80, 229228.26), objects=True)
+        self.assertEqual(
+            [second.ix[hit.object]['BoroName'] for hit in hits],
+            ['Queens'])
+
+        # Get both the Bronx and Queens again.
+        merged = first.merge(second, how='outer')
+        self.assertEqual(len(merged), 5)
+        self.assertEqual(merged.sindex.size, 5)
+        tree = merged.sindex
+        hits = tree.intersection((1012821.80, 229228.26), objects=True)
+        self.assertEqual(
+            [merged.ix[hit.object]['BoroName'] for hit in hits],
+            ['Bronx', 'Queens'])
diff --git a/tests/test_types.py b/geopandas/tests/test_types.py
similarity index 94%
rename from tests/test_types.py
rename to geopandas/tests/test_types.py
index fdd14f5..f5f1d59 100644
--- a/tests/test_types.py
+++ b/geopandas/tests/test_types.py
@@ -5,7 +5,7 @@ from shapely.geometry import Point
 from pandas import Series, DataFrame
 
 from geopandas import GeoSeries, GeoDataFrame
-from .util import unittest
+from geopandas.tests.util import unittest
 
 OLD_PANDAS = issubclass(Series, np.ndarray)
 
@@ -39,7 +39,7 @@ class TestSeries(unittest.TestCase):
         assert type(self.pts.iloc[5:]) is GeoSeries
 
     def test_fancy(self):
-        idx = (self.pts.index % 2).astype(bool)
+        idx = (self.pts.index.to_series() % 2).astype(bool)
         assert type(self.pts[idx]) is GeoSeries
 
     def test_take(self):
@@ -85,5 +85,5 @@ class TestDataFrame(unittest.TestCase):
         assert type(self.df[::2]) is GeoDataFrame
 
     def test_fancy(self):
-        idx = (self.df.index % 2).astype(bool)
+        idx = (self.df.index.to_series() % 2).astype(bool)
         assert type(self.df[idx]) is GeoDataFrame
diff --git a/tests/util.py b/geopandas/tests/util.py
similarity index 88%
rename from tests/util.py
rename to geopandas/tests/util.py
index 9751c31..51f2f94 100644
--- a/tests/util.py
+++ b/geopandas/tests/util.py
@@ -1,17 +1,22 @@
 import io
 import os.path
+import sys
+import zipfile
+
 from six.moves.urllib.request import urlopen
+from pandas.util.testing import assert_isinstance
 
 from geopandas import GeoDataFrame, GeoSeries
 
+HERE = os.path.abspath(os.path.dirname(__file__))
+PACKAGE_DIR = os.path.dirname(os.path.dirname(HERE))
+
 # Compatibility layer for Python 2.6: try loading unittest2
-import sys
 if sys.version_info[:2] == (2, 6):
     try:
         import unittest2 as unittest
     except ImportError:
         import unittest
-
 else:
     import unittest
 
@@ -23,6 +28,11 @@ except ImportError:
         pass
 
 try:
+    import unittest.mock as mock
+except ImportError:
+    import mock
+
+try:
     from pandas import read_sql_table
 except ImportError:
     PANDAS_NEW_SQL_API = False
@@ -31,16 +41,26 @@ else:
 
 
 def download_nybb():
-    """ Returns the path to the NYC boroughs file. Downloads if necessary. """
+    """ Returns the path to the NYC boroughs file. Downloads if necessary.
+
+    returns tuple (zip file name, shapefile's name and path within zip file)"""
     # Data from http://www.nyc.gov/html/dcp/download/bytes/nybb_14aav.zip
     # saved as geopandas/examples/nybb_14aav.zip.
-    filename = 'nybb_14aav.zip'
-    full_path_name = os.path.join('examples', filename)
+    filename = 'nybb_16a.zip'
+    full_path_name = os.path.join(PACKAGE_DIR, 'examples', filename)
     if not os.path.exists(full_path_name):
         with io.open(full_path_name, 'wb') as f:
-            response = urlopen('http://www.nyc.gov/html/dcp/download/bytes/{0}'.format(filename))
+            response = urlopen('http://www1.nyc.gov/assets/planning/download/zip/data-maps/open-data/{0}'.format(filename))
             f.write(response.read())
-    return full_path_name
+
+    shp_zip_path = None
+    zf = zipfile.ZipFile(full_path_name, 'r')
+    # finds path name in zip file
+    for zip_filename_path in zf.namelist():
+        if zip_filename_path.endswith('nybb.shp'):
+            break
+
+    return full_path_name, ('/' + zip_filename_path)
 
 
 def validate_boro_df(test, df):
@@ -144,13 +164,6 @@ def geom_almost_equals(this, that):
     return (this.geom_almost_equals(that) |
             (this.is_empty & that.is_empty)).all()
 
-# TODO: Remove me when standardizing on pandas 0.13, which already includes
-#       this test util.
-def assert_isinstance(obj, klass_or_tuple):
-    assert isinstance(obj, klass_or_tuple), "type: %r != %r" % (
-                                           type(obj).__name__,
-                                           getattr(klass_or_tuple, '__name__',
-                                                   klass_or_tuple))
 
 def assert_geoseries_equal(left, right, check_dtype=False,
                            check_index_type=False,
diff --git a/geopandas/tools/__init__.py b/geopandas/tools/__init__.py
new file mode 100644
index 0000000..daabcad
--- /dev/null
+++ b/geopandas/tools/__init__.py
@@ -0,0 +1,14 @@
+from __future__ import absolute_import
+
+from .geocoding import geocode, reverse_geocode
+from .overlay import overlay
+from .sjoin import sjoin
+from .util import collect
+
+__all__ = [
+    'overlay',
+    'sjoin',
+    'geocode',
+    'reverse_geocode',
+    'collect',
+]
diff --git a/geopandas/geocode.py b/geopandas/tools/geocoding.py
similarity index 58%
rename from geopandas/geocode.py
rename to geopandas/tools/geocoding.py
index c154f3c..c4b96df 100644
--- a/geopandas/geocode.py
+++ b/geopandas/tools/geocoding.py
@@ -39,7 +39,7 @@ def geocode(strings, provider='googlev3', **kwargs):
         * yahoo
         * mapquest
         * openmapquest
-    
+
     Ensure proper use of the results by consulting the Terms of Service for
     your provider.
 
@@ -49,16 +49,71 @@ def geocode(strings, provider='googlev3', **kwargs):
     Example
     -------
     >>> df = geocode(['boston, ma', '1600 pennsylvania ave. washington, dc'])
-    address                                               geometry
-    0                                    Boston, MA, USA  POINT (-71.0597731999999951 42.3584308000000007)
-    1  1600 Pennsylvania Avenue Northwest, President'...  POINT (-77.0365122999999983 38.8978377999999978)
+
+                                                 address  \
+    0                                    Boston, MA, USA
+    1  1600 Pennsylvania Avenue Northwest, President'...
+
+                             geometry
+    0  POINT (-71.0597732 42.3584308)
+    1  POINT (-77.0365305 38.8977332)
 
     """
+    return _query(strings, True, provider, **kwargs)
+
+
+def reverse_geocode(points, provider='googlev3', **kwargs):
+    """
+    Reverse geocode a set of points and get a GeoDataFrame of the resulting
+    addresses.
+
+    The points
+
+    Parameters
+    ----------
+    points : list or Series of Shapely Point objects.
+        x coordinate is longitude
+        y coordinate is latitude
+    provider : geopy geocoder to use, default 'googlev3'
+        These are the same options as the geocode() function
+        Some providers require additional arguments such as access keys
+        See each geocoder's specific parameters in geopy.geocoders
+        * googlev3, default
+        * bing
+        * google
+        * yahoo
+        * mapquest
+        * openmapquest
+
+    Ensure proper use of the results by consulting the Terms of Service for
+    your provider.
+
+    Reverse geocoding requires geopy. Install it using 'pip install geopy'.
+    See also https://github.com/geopy/geopy
+
+    Example
+    -------
+    >>> df = reverse_geocode([Point(-71.0594869, 42.3584697),
+                              Point(-77.0365305, 38.8977332)])
+
+                                             address  \
+    0             29 Court Square, Boston, MA 02108, USA
+    1  1600 Pennsylvania Avenue Northwest, President'...
+
+                             geometry
+    0  POINT (-71.0594869 42.3584697)
+    1  POINT (-77.0365305 38.8977332)
+
+    """
+    return _query(points, False, provider, **kwargs)
+
+
+def _query(data, forward, provider, **kwargs):
     import geopy
     from geopy.geocoders.base import GeocoderQueryError
 
-    if not isinstance(strings, pd.Series):
-        strings = pd.Series(strings)
+    if not isinstance(data, pd.Series):
+        data = pd.Series(data)
 
     # workaround changed name in 0.96
     try:
@@ -69,18 +124,20 @@ def geocode(strings, provider='googlev3', **kwargs):
     coders = {'googlev3': geopy.geocoders.GoogleV3,
               'bing': geopy.geocoders.Bing,
               'yahoo': Yahoo,
-              'mapquest': geopy.geocoders.MapQuest,
               'openmapquest': geopy.geocoders.OpenMapQuest,
-              'nominatim' : geopy.geocoders.Nominatim}
+              'nominatim': geopy.geocoders.Nominatim}
 
     if provider not in coders:
         raise ValueError('Unknown geocoding provider: {0}'.format(provider))
 
     coder = coders[provider](**kwargs)
     results = {}
-    for i, s in iteritems(strings):
+    for i, s in iteritems(data):
         try:
-            results[i] = coder.geocode(s)
+            if forward:
+                results[i] = coder.geocode(s)
+            else:
+                results[i] = coder.reverse((s.y, s.x), exactly_one=True)
         except (GeocoderQueryError, ValueError):
             results[i] = (None, None)
         time.sleep(_throttle_time(provider))
@@ -88,6 +145,7 @@ def geocode(strings, provider='googlev3', **kwargs):
     df = _prepare_geocode_result(results)
     return df
 
+
 def _prepare_geocode_result(results):
     """
     Helper function for the geocode function
diff --git a/geopandas/tools/overlay.py b/geopandas/tools/overlay.py
new file mode 100644
index 0000000..8abbca7
--- /dev/null
+++ b/geopandas/tools/overlay.py
@@ -0,0 +1,183 @@
+from shapely.ops import unary_union, polygonize
+from shapely.geometry import MultiLineString
+import pandas as pd
+from geopandas import GeoDataFrame, GeoSeries
+
+
+def _uniquify(columns):
+    ucols = []
+    for col in columns:
+        inc = 1
+        newcol = col
+        while newcol in ucols:
+            inc += 1
+            newcol = "{0}_{1}".format(col, inc)
+        ucols.append(newcol)
+    return ucols
+
+
+def _extract_rings(df):
+    """Collects all inner and outer linear rings from a GeoDataFrame
+    with (multi)Polygon geometeries
+
+    Parameters
+    ----------
+    df: GeoDataFrame with MultiPolygon or Polygon geometry column
+
+    Returns
+    -------
+    rings: list of LinearRings
+    """
+    poly_msg = "overlay only takes GeoDataFrames with (multi)polygon geometries"
+    rings = []
+    geometry_column = df.geometry.name
+
+    for i, feat in df.iterrows():
+        geom = feat[geometry_column]
+
+        if geom.type not in ['Polygon', 'MultiPolygon']:
+            raise TypeError(poly_msg)
+
+        if hasattr(geom, 'geoms'):
+            for poly in geom.geoms:  # if it's a multipolygon
+                if not poly.is_valid:
+                    # geom from layer is not valid attempting fix by buffer 0"
+                    poly = poly.buffer(0)
+                rings.append(poly.exterior)
+                rings.extend(poly.interiors)
+        else:
+            if not geom.is_valid:
+                # geom from layer is not valid attempting fix by buffer 0"
+                geom = geom.buffer(0)
+            rings.append(geom.exterior)
+            rings.extend(geom.interiors)
+
+    return rings
+
+def overlay(df1, df2, how, use_sindex=True):
+    """Perform spatial overlay between two polygons.
+
+    Currently only supports data GeoDataFrames with polygons.
+    Implements several methods that are all effectively subsets of
+    the union.
+
+    Parameters
+    ----------
+    df1 : GeoDataFrame with MultiPolygon or Polygon geometry column
+    df2 : GeoDataFrame with MultiPolygon or Polygon geometry column
+    how : string
+        Method of spatial overlay: 'intersection', 'union',
+        'identity', 'symmetric_difference' or 'difference'.
+    use_sindex : boolean, default True
+        Use the spatial index to speed up operation if available.
+
+    Returns
+    -------
+    df : GeoDataFrame
+        GeoDataFrame with new set of polygons and attributes
+        resulting from the overlay
+
+    """
+    allowed_hows = [
+        'intersection',
+        'union',
+        'identity',
+        'symmetric_difference',
+        'difference',  # aka erase
+    ]
+
+    if how not in allowed_hows:
+        raise ValueError("`how` was \"%s\" but is expected to be in %s" % \
+            (how, allowed_hows))
+
+    if isinstance(df1, GeoSeries) or isinstance(df2, GeoSeries):
+        raise NotImplementedError("overlay currently only implemented for GeoDataFrames")
+
+    # Collect the interior and exterior rings
+    rings1 = _extract_rings(df1)
+    rings2 = _extract_rings(df2)
+    mls1 = MultiLineString(rings1)
+    mls2 = MultiLineString(rings2)
+
+    # Union and polygonize
+    try:
+        # calculating union (try the fast unary_union)
+        mm = unary_union([mls1, mls2])
+    except:
+        # unary_union FAILED
+        # see https://github.com/Toblerity/Shapely/issues/47#issuecomment-18506767
+        # calculating union again (using the slow a.union(b))
+        mm = mls1.union(mls2)
+    newpolys = polygonize(mm)
+
+    # determine spatial relationship
+    collection = []
+    for fid, newpoly in enumerate(newpolys):
+        cent = newpoly.representative_point()
+
+        # Test intersection with original polys
+        # FIXME there should be a higher-level abstraction to search by bounds
+        # and fall back in the case of no index?
+        if use_sindex and df1.sindex is not None:
+            candidates1 = [x.object for x in
+                           df1.sindex.intersection(newpoly.bounds, objects=True)]
+        else:
+            candidates1 = [i for i, x in df1.iterrows()]
+
+        if use_sindex and df2.sindex is not None:
+            candidates2 = [x.object for x in
+                           df2.sindex.intersection(newpoly.bounds, objects=True)]
+        else:
+            candidates2 = [i for i, x in df2.iterrows()]
+
+        df1_hit = False
+        df2_hit = False
+        prop1 = None
+        prop2 = None
+        for cand_id in candidates1:
+            cand = df1.ix[cand_id]
+            if cent.intersects(cand[df1.geometry.name]):
+                df1_hit = True
+                prop1 = cand
+                break  # Take the first hit
+        for cand_id in candidates2:
+            cand = df2.ix[cand_id]
+            if cent.intersects(cand[df2.geometry.name]):
+                df2_hit = True
+                prop2 = cand
+                break  # Take the first hit
+
+        # determine spatial relationship based on type of overlay
+        hit = False
+        if how == "intersection" and (df1_hit and df2_hit):
+            hit = True
+        elif how == "union" and (df1_hit or df2_hit):
+            hit = True
+        elif how == "identity" and df1_hit:
+            hit = True
+        elif how == "symmetric_difference" and not (df1_hit and df2_hit):
+            hit = True
+        elif how == "difference" and (df1_hit and not df2_hit):
+            hit = True
+
+        if not hit:
+            continue
+
+        # gather properties
+        if prop1 is None:
+            prop1 = pd.Series(dict.fromkeys(df1.columns, None))
+        if prop2 is None:
+            prop2 = pd.Series(dict.fromkeys(df2.columns, None))
+
+        # Concat but don't retain the original geometries
+        out_series = pd.concat([prop1.drop(df1._geometry_column_name),
+                                prop2.drop(df2._geometry_column_name)])
+
+        out_series.index = _uniquify(out_series.index)
+
+        # Create a geoseries and add it to the collection
+        out_series['geometry'] = newpoly
+        collection.append(out_series)
+
+    # Return geodataframe with new indicies
+    return GeoDataFrame(collection, index=range(len(collection)))
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
new file mode 100644
index 0000000..206f4cb
--- /dev/null
+++ b/geopandas/tools/sjoin.py
@@ -0,0 +1,125 @@
+import numpy as np
+import pandas as pd
+from shapely import prepared
+
+
+def sjoin(left_df, right_df, how='inner', op='intersects',
+          lsuffix='left', rsuffix='right'):
+    """Spatial join of two GeoDataFrames.
+
+    Parameters
+    ----------
+    left_df, right_df : GeoDataFrames
+    how : string, default 'inner'
+        The type of join:
+
+        * 'left': use keys from left_df; retain only left_df geometry column
+        * 'right': use keys from right_df; retain only right_df geometry column
+        * 'inner': use intersection of keys from both dfs; retain only
+          left_df geometry column
+    op : string, default 'intersection'
+        Binary predicate, one of {'intersects', 'contains', 'within'}.
+        See http://toblerity.org/shapely/manual.html#binary-predicates.
+    lsuffix : string, default 'left'
+        Suffix to apply to overlapping column names (left GeoDataFrame).
+    rsuffix : string, default 'right'
+        Suffix to apply to overlapping column names (right GeoDataFrame).
+
+    """
+    import rtree
+
+    allowed_hows = ['left', 'right', 'inner']
+    if how not in allowed_hows:
+        raise ValueError("`how` was \"%s\" but is expected to be in %s" % \
+            (how, allowed_hows))
+
+    allowed_ops = ['contains', 'within', 'intersects']
+    if op not in allowed_ops:
+        raise ValueError("`op` was \"%s\" but is expected to be in %s" % \
+            (op, allowed_ops))
+
+    if op == "within":
+        # within implemented as the inverse of contains; swap names
+        left_df, right_df = right_df, left_df
+
+    if left_df.crs != right_df.crs:
+        print('Warning: CRS does not match!')
+
+    tree_idx = rtree.index.Index()
+    right_df_bounds = right_df['geometry'].apply(lambda x: x.bounds)
+    for i in right_df_bounds.index:
+        tree_idx.insert(i, right_df_bounds[i])
+
+    idxmatch = (left_df['geometry'].apply(lambda x: x.bounds)
+                .apply(lambda x: list(tree_idx.intersection(x))))
+    idxmatch = idxmatch[idxmatch.apply(len) > 0]
+
+    r_idx = np.concatenate(idxmatch.values)
+    l_idx = np.concatenate([[i] * len(v) for i, v in idxmatch.iteritems()])
+
+    # Vectorize predicate operations
+    def find_intersects(a1, a2):
+        return a1.intersects(a2)
+
+    def find_contains(a1, a2):
+        return a1.contains(a2)
+
+    predicate_d = {'intersects': find_intersects,
+                   'contains': find_contains,
+                   'within': find_contains}
+
+    check_predicates = np.vectorize(predicate_d[op])
+
+    result = (
+              pd.DataFrame(
+                  np.column_stack(
+                      [l_idx,
+                       r_idx,
+                       check_predicates(
+                           left_df['geometry']
+                           .apply(lambda x: prepared.prep(x))[l_idx],
+                           right_df['geometry'][r_idx])
+                       ]))
+               )
+
+    result.columns = ['index_%s' % lsuffix, 'index_%s' % rsuffix, 'match_bool']
+    result = (
+              pd.DataFrame(result[result['match_bool']==1])
+              .drop('match_bool', axis=1)
+              )
+
+    if op == "within":
+        # within implemented as the inverse of contains; swap names
+        left_df, right_df = right_df, left_df
+        result = result.rename(columns={
+                    'index_%s' % (lsuffix): 'index_%s' % (rsuffix),
+                    'index_%s' % (rsuffix): 'index_%s' % (lsuffix)})
+
+    if how == 'inner':
+        result = result.set_index('index_%s' % lsuffix)
+        return (
+                left_df
+                .merge(result, left_index=True, right_index=True)
+                .merge(right_df.drop('geometry', axis=1),
+                    left_on='index_%s' % rsuffix, right_index=True,
+                    suffixes=('_%s' % lsuffix, '_%s' % rsuffix))
+                )
+    elif how == 'left':
+        result = result.set_index('index_%s' % lsuffix)
+        return (
+                left_df
+                .merge(result, left_index=True, right_index=True, how='left')
+                .merge(right_df.drop('geometry', axis=1),
+                    how='left', left_on='index_%s' % rsuffix, right_index=True,
+                    suffixes=('_%s' % lsuffix, '_%s' % rsuffix))
+                )
+    elif how == 'right':
+        return (
+                left_df
+                .drop('geometry', axis=1)
+                .merge(result.merge(right_df,
+                    left_on='index_%s' % rsuffix, right_index=True,
+                    how='right'), left_index=True,
+                    right_on='index_%s' % lsuffix, how='right')
+                .set_index('index_%s' % rsuffix)
+                )
diff --git a/tests/__init__.py b/geopandas/tools/tests/__init__.py
similarity index 100%
rename from tests/__init__.py
rename to geopandas/tools/tests/__init__.py
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
new file mode 100644
index 0000000..cfaaf37
--- /dev/null
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -0,0 +1,88 @@
+from __future__ import absolute_import
+
+import tempfile
+import shutil
+
+import numpy as np
+from shapely.geometry import Point
+
+from geopandas import GeoDataFrame, read_file, base
+from geopandas.tests.util import unittest, download_nybb
+from geopandas import sjoin
+
+
+ at unittest.skipIf(not base.HAS_SINDEX, 'Rtree absent, skipping')
+class TestSpatialJoin(unittest.TestCase):
+
+    def setUp(self):
+        nybb_filename, nybb_zip_path = download_nybb()
+        self.polydf = read_file(nybb_zip_path, vfs='zip://' + nybb_filename)
+        self.tempdir = tempfile.mkdtemp()
+        self.crs = {'init': 'epsg:4326'}
+        N = 20
+        b = [int(x) for x in self.polydf.total_bounds]
+        self.pointdf = GeoDataFrame([
+            {'geometry' : Point(x, y), 'pointattr1': x + y, 'pointattr2': x - y}
+            for x, y in zip(range(b[0], b[2], int((b[2]-b[0])/N)),
+                            range(b[1], b[3], int((b[3]-b[1])/N)))], crs=self.crs)
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+
+    def test_sjoin_left(self):
+        df = sjoin(self.pointdf, self.polydf, how='left')
+        self.assertEquals(df.shape, (21,8))
+        for i, row in df.iterrows():
+            self.assertEquals(row.geometry.type, 'Point')
+        self.assertTrue('pointattr1' in df.columns)
+        self.assertTrue('BoroCode' in df.columns)
+
+    def test_sjoin_right(self):
+        # the inverse of left
+        df = sjoin(self.pointdf, self.polydf, how="right")
+        df2 = sjoin(self.polydf, self.pointdf, how="left")
+        self.assertEquals(df.shape, (12, 8))
+        self.assertEquals(df.shape, df2.shape)
+        for i, row in df.iterrows():
+            self.assertEquals(row.geometry.type, 'MultiPolygon')
+        for i, row in df2.iterrows():
+            self.assertEquals(row.geometry.type, 'MultiPolygon')
+
+    def test_sjoin_inner(self):
+        df = sjoin(self.pointdf, self.polydf, how="inner")
+        self.assertEquals(df.shape, (11, 8))
+
+    def test_sjoin_op(self):
+        # points within polygons
+        df = sjoin(self.pointdf, self.polydf, how="left", op="within")
+        self.assertEquals(df.shape, (21,8))
+        self.assertEquals(df.ix[1]['BoroName'], 'Staten Island')
+
+        # points contain polygons? never happens so we should have nulls
+        df = sjoin(self.pointdf, self.polydf, how="left", op="contains")
+        self.assertEquals(df.shape, (21, 8))
+        self.assertTrue(np.isnan(df.ix[1]['Shape_Area']))
+
+    def test_sjoin_bad_op(self):
+        # AttributeError: 'Point' object has no attribute 'spandex'
+        self.assertRaises(ValueError, sjoin,
+            self.pointdf, self.polydf, how="left", op="spandex")
+
+    def test_sjoin_duplicate_column_name(self):
+        pointdf2 = self.pointdf.rename(columns={'pointattr1': 'Shape_Area'})
+        df = sjoin(pointdf2, self.polydf, how="left")
+        self.assertTrue('Shape_Area_left' in df.columns)
+        self.assertTrue('Shape_Area_right' in df.columns)
+
+    def test_sjoin_values(self):
+        # GH190
+        self.polydf.index = [1, 3, 4, 5, 6]
+        df = sjoin(self.pointdf, self.polydf, how='left')
+        self.assertEquals(df.shape, (21,8))
+        df = sjoin(self.polydf, self.pointdf, how='left')
+        self.assertEquals(df.shape, (12,8))
+
+    @unittest.skip("Not implemented")
+    def test_sjoin_outer(self):
+        df = sjoin(self.pointdf, self.polydf, how="outer")
+        self.assertEquals(df.shape, (21,8))
diff --git a/geopandas/tools/tests/test_tools.py b/geopandas/tools/tests/test_tools.py
new file mode 100644
index 0000000..6c73bf9
--- /dev/null
+++ b/geopandas/tools/tests/test_tools.py
@@ -0,0 +1,51 @@
+from __future__ import absolute_import
+
+from shapely.geometry import Point, MultiPoint, LineString
+from geopandas import GeoSeries
+from geopandas.tools import collect
+from geopandas.tests.util import unittest
+
+
+class TestTools(unittest.TestCase):
+    def setUp(self):
+        self.p1 = Point(0, 0)
+        self.p2 = Point(1, 1)
+        self.p3 = Point(2, 2)
+        self.mpc = MultiPoint([self.p1, self.p2, self.p3])
+
+        self.mp1 = MultiPoint([self.p1, self.p2])
+        self.line1 = LineString([(3, 3), (4, 4)])
+
+    def test_collect_single(self):
+        result = collect(self.p1)
+        self.assert_(self.p1.equals(result))
+
+    def test_collect_single_force_multi(self):
+        result = collect(self.p1, multi=True)
+        expected = MultiPoint([self.p1])
+        self.assert_(expected.equals(result))
+
+    def test_collect_multi(self):
+        result = collect(self.mp1)
+        self.assert_(self.mp1.equals(result))
+
+    def test_collect_multi_force_multi(self):
+        result = collect(self.mp1)
+        self.assert_(self.mp1.equals(result))
+
+    def test_collect_list(self):
+        result = collect([self.p1, self.p2, self.p3])
+        self.assert_(self.mpc.equals(result))
+
+    def test_collect_GeoSeries(self):
+        s = GeoSeries([self.p1, self.p2, self.p3])
+        result = collect(s)
+        self.assert_(self.mpc.equals(result))
+
+    def test_collect_mixed_types(self):
+        with self.assertRaises(ValueError):
+            collect([self.p1, self.line1])
+
+    def test_collect_mixed_multi(self):
+        with self.assertRaises(ValueError):
+            collect([self.mpc, self.mp1])
diff --git a/geopandas/tools/util.py b/geopandas/tools/util.py
new file mode 100644
index 0000000..c057eba
--- /dev/null
+++ b/geopandas/tools/util.py
@@ -0,0 +1,52 @@
+import pandas as pd
+import geopandas as gpd
+from shapely.geometry import (
+    Point,
+    LineString,
+    Polygon,
+    MultiPoint,
+    MultiLineString,
+    MultiPolygon
+)
+from shapely.geometry.base import BaseGeometry
+
+_multi_type_map = {
+    'Point': MultiPoint,
+    'LineString': MultiLineString,
+    'Polygon': MultiPolygon
+}
+
+def collect(x, multi=False):
+    """
+    Collect single part geometries into their Multi* counterpart
+
+    Parameters
+    ----------
+    x : an iterable or Series of Shapely geometries, a GeoSeries, or
+        a single Shapely geometry        
+    multi : boolean, default False
+        if True, force returned geometries to be Multi* even if they
+        only have one component.
+
+    """
+    if isinstance(x, BaseGeometry):
+        x = [x]
+    elif isinstance(x, pd.Series):
+        x = list(x)
+
+    # We cannot create GeometryCollection here so all types
+    # must be the same. If there is more than one element,
+    # they cannot be Multi*, i.e., can't pass in combination of
+    # Point and MultiPoint... or even just MultiPoint
+    t = x[0].type
+    if not all(g.type == t for g in x):
+        raise ValueError('Geometry type must be homogenous')
+    if len(x) > 1 and t.startswith('Multi'):
+        raise ValueError(
+            'Cannot collect {0}. Must have single geometries'.format(t))
+
+    if len(x) == 1 and (t.startswith('Multi') or not multi):
+        # If there's only one single part geom and we're not forcing to
+        # multi, then just return it
+        return x[0]
+    return _multi_type_map[t](x)
diff --git a/readthedocs.yml b/readthedocs.yml
new file mode 100644
index 0000000..50b41dd
--- /dev/null
+++ b/readthedocs.yml
@@ -0,0 +1,7 @@
+formats:
+    - none
+conda:
+    file: doc/environment.yml
+python:
+    version: 3
+    setup_py_install: true
diff --git a/requirements.test.txt b/requirements.test.txt
index 69b4255..02991bd 100644
--- a/requirements.test.txt
+++ b/requirements.test.txt
@@ -1,7 +1,9 @@
 psycopg2>=2.5.1
 SQLAlchemy>=0.8.3
-geopy==0.99
+geopy==1.10.0
 matplotlib>=1.2.1
 descartes>=1.0
+mock>=1.0.1  # technically not need for python >= 3.3
 pytest-cov
 coveralls
+rtree>=0.8
diff --git a/requirements.txt b/requirements.txt
index 3f2efce..7c19f99 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,6 @@ shapely>=1.2.18
 fiona>=1.0.1
 pyproj>=1.9.3
 six>=1.3.0
+# for Python 2.6 (environment marker support as of pip 6.0)
+unittest2 ; python_version < '2.7'
+ordereddict ; python_version < '2.7'
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..96b08c0
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,10 @@
+[bdist_wheel]
+universal = 1
+
+[versioneer]
+VCS = git
+style = pep440
+versionfile_source = geopandas/_version.py
+versionfile_build = geopandas/_version.py
+tag_prefix = v
+parentdir_prefix = geopandas-
diff --git a/setup.py b/setup.py
index 1eaf90e..3d2bed0 100644
--- a/setup.py
+++ b/setup.py
@@ -1,18 +1,17 @@
 #!/usr/bin/env/python
 """Installation script
 
-Version handling borrowed from pandas project.
 """
 
-import sys
 import os
-import warnings
 
 try:
     from setuptools import setup
 except ImportError:
     from distutils.core import setup
 
+import versioneer
+
 LONG_DESCRIPTION = """GeoPandas is a project to add support for geographic data to
 `pandas`_ objects.
 
@@ -27,64 +26,28 @@ such as PostGIS.
 .. _shapely: http://toblerity.github.io/shapely
 """
 
-MAJOR = 0
-MINOR = 1
-MICRO = 1
-ISRELEASED = True
-VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO)
-QUALIFIER = ''
-
-FULLVERSION = VERSION
-if not ISRELEASED:
-    FULLVERSION += '.dev'
-    try:
-        import subprocess
-        try:
-            pipe = subprocess.Popen(["git", "rev-parse", "--short", "HEAD"],
-                                    stdout=subprocess.PIPE).stdout
-        except OSError:
-            # msysgit compatibility
-            pipe = subprocess.Popen(
-                ["git.cmd", "describe", "HEAD"],
-                stdout=subprocess.PIPE).stdout
-        rev = pipe.read().strip()
-        # makes distutils blow up on Python 2.7
-        if sys.version_info[0] >= 3:
-            rev = rev.decode('ascii')
-
-        FULLVERSION = '%d.%d.%d.dev-%s' % (MAJOR, MINOR, MICRO, rev)
-
-    except:
-        warnings.warn("WARNING: Couldn't get git revision")
+if os.environ.get('READTHEDOCS', False) == 'True':
+    INSTALL_REQUIRES = []
 else:
-    FULLVERSION += QUALIFIER
-
-
-def write_version_py(filename=None):
-    cnt = """\
-version = '%s'
-short_version = '%s'
-"""
-    if not filename:
-        filename = os.path.join(
-            os.path.dirname(__file__), 'geopandas', 'version.py')
+    INSTALL_REQUIRES = ['pandas', 'shapely', 'fiona', 'descartes', 'pyproj']
 
-    a = open(filename, 'w')
-    try:
-        a.write(cnt % (FULLVERSION, VERSION))
-    finally:
-        a.close()
+# get all data dirs in the datasets module
+data_files = []
 
-write_version_py()
+for item in os.listdir("geopandas/datasets"):
+    if os.path.isdir(os.path.join("geopandas/datasets/", item)) \
+            and not item.startswith('__'):
+        data_files.append(os.path.join("datasets", item, '*'))
 
 setup(name='geopandas',
-      version=FULLVERSION,
+      version=versioneer.get_version(),
       description='Geographic pandas extensions',
       license='BSD',
-      author='Kelsey Jordahl',
-      author_email='kjordahl at enthought.com',
+      author='GeoPandas contributors',
+      author_email='kjordahl at alum.mit.edu',
       url='http://geopandas.org',
       long_description=LONG_DESCRIPTION,
-      packages=['geopandas', 'geopandas.io'],
-      install_requires=['pandas', 'shapely', 'fiona', 'descartes', 'pyproj'],
-)
+      packages=['geopandas', 'geopandas.io', 'geopandas.tools',
+                'geopandas.datasets'],
+      package_data={'geopandas': data_files},
+      install_requires=INSTALL_REQUIRES)
diff --git a/tests/test_geocode.py b/tests/test_geocode.py
deleted file mode 100644
index f108454..0000000
--- a/tests/test_geocode.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from __future__ import absolute_import
-
-import sys
-
-from fiona.crs import from_epsg
-import pandas as pd
-from shapely.geometry import Point
-import geopandas as gpd
-import nose
-
-from geopandas.geocode import geocode, _prepare_geocode_result
-from .util import unittest
-
-
-def _skip_if_no_geopy():
-    try:
-        import geopy
-    except ImportError:
-        raise nose.SkipTest("Geopy not installed. Skipping tests.")
-    except SyntaxError:
-        raise nose.SkipTest("Geopy is known to be broken on Python 3.2. "
-                            "Skipping tests.")
-
-class TestGeocode(unittest.TestCase):
-    def setUp(self):
-        _skip_if_no_geopy()
-        self.locations = ['260 Broadway, New York, NY',
-                          '77 Massachusetts Ave, Cambridge, MA']
-
-    def test_prepare_result(self):
-        # Calls _prepare_result with sample results from the geocoder call
-        # loop
-        from geopandas.geocode import _prepare_geocode_result
-        p0 = Point(12.3, -45.6) # Treat these as lat/lon
-        p1 = Point(-23.4, 56.7)
-        d = {'a': ('address0', p0.coords[0]),
-             'b': ('address1', p1.coords[0])}
-
-        df = _prepare_geocode_result(d)
-        assert type(df) is gpd.GeoDataFrame
-        self.assertEqual(from_epsg(4326), df.crs)
-        self.assertEqual(len(df), 2)
-        self.assert_('address' in df)
-
-        coords = df.loc['a']['geometry'].coords[0]
-        test = p0.coords[0]
-        # Output from the df should be lon/lat
-        self.assertAlmostEqual(coords[0], test[1])
-        self.assertAlmostEqual(coords[1], test[0])
-
-        coords = df.loc['b']['geometry'].coords[0]
-        test = p1.coords[0]
-        self.assertAlmostEqual(coords[0], test[1])
-        self.assertAlmostEqual(coords[1], test[0])
-
-    def test_prepare_result_none(self):
-        from geopandas.geocode import _prepare_geocode_result
-        p0 = Point(12.3, -45.6) # Treat these as lat/lon
-        d = {'a': ('address0', p0.coords[0]),
-             'b': (None, None)}
-
-        df = _prepare_geocode_result(d)
-        assert type(df) is gpd.GeoDataFrame
-        self.assertEqual(from_epsg(4326), df.crs)
-        self.assertEqual(len(df), 2)
-        self.assert_('address' in df)
-
-        row = df.loc['b']
-        self.assertEqual(len(row['geometry'].coords), 0)
-        self.assert_(pd.np.isnan(row['address']))
-    
-    def test_bad_provider(self):
-        from geopandas.geocode import geocode
-        with self.assertRaises(ValueError):
-            geocode(['cambridge, ma'], 'badprovider')
-
-    def test_googlev3(self):
-        from geopandas.geocode import geocode
-        g = geocode(self.locations, provider='googlev3', timeout=2)
-        self.assertIsInstance(g, gpd.GeoDataFrame)
-
-    def test_openmapquest(self):
-        from geopandas.geocode import geocode
-        g = geocode(self.locations, provider='openmapquest', timeout=2)
-        self.assertIsInstance(g, gpd.GeoDataFrame)
-
-    @unittest.skip('Nominatim server is unreliable for tests.')
-    def test_nominatim(self):
-        from geopandas.geocode import geocode
-        g = geocode(self.locations, provider='nominatim', timeout=2)
-        self.assertIsInstance(g, gpd.GeoDataFrame)
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
deleted file mode 100644
index 5bc3ef6..0000000
--- a/tests/test_plotting.py
+++ /dev/null
@@ -1,78 +0,0 @@
-from __future__ import absolute_import
-
-import os
-import shutil
-import tempfile
-import unittest
-
-import matplotlib
-matplotlib.use('Agg', warn=False)
-from matplotlib.pyplot import Artist, savefig, clf
-from matplotlib.testing.noseclasses import ImageComparisonFailure
-from matplotlib.testing.compare import compare_images
-from shapely.geometry import Polygon, LineString, Point
-from six.moves import xrange
-
-from geopandas import GeoSeries
-
-
-# If set to True, generate images rather than perform tests (all tests will pass!)
-GENERATE_BASELINE = False
-
-BASELINE_DIR = os.path.join(os.path.dirname(__file__), 'baseline_images', 'test_plotting')
-
-
-class PlotTests(unittest.TestCase):
-    
-    def setUp(self):
-        self.tempdir = tempfile.mkdtemp()
-        return
-
-    def tearDown(self):
-        shutil.rmtree(self.tempdir)
-        return
-
-    def _compare_images(self, ax, filename, tol=8):
-        """ Helper method to do the comparisons """
-        assert isinstance(ax, Artist)
-        if GENERATE_BASELINE:
-            savefig(os.path.join(BASELINE_DIR, filename))
-        savefig(os.path.join(self.tempdir, filename))
-        err = compare_images(os.path.join(BASELINE_DIR, filename),
-                             os.path.join(self.tempdir, filename),
-                             tol, in_decorator=True)
-        if err:
-            raise ImageComparisonFailure('images not close: %(actual)s '
-                                         'vs. %(expected)s '
-                                         '(RMS %(rms).3f)' % err)
-
-    def test_poly_plot(self):
-        """ Test plotting a simple series of polygons """
-        clf()
-        filename = 'poly_plot.png'
-        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
-        t2 = Polygon([(1, 0), (2, 0), (2, 1)])
-        polys = GeoSeries([t1, t2])
-        ax = polys.plot()
-        self._compare_images(ax=ax, filename=filename)
-
-    def test_point_plot(self):
-        """ Test plotting a simple series of points """
-        clf()
-        filename = 'points_plot.png'
-        N = 10
-        points = GeoSeries(Point(i, i) for i in xrange(N))
-        ax = points.plot()
-        self._compare_images(ax=ax, filename=filename)
-
-    def test_line_plot(self):
-        """ Test plotting a simple series of lines """
-        clf()
-        filename = 'lines_plot.png'
-        N = 10
-        lines = GeoSeries([LineString([(0, i), (9, i)]) for i in xrange(N)])
-        ax = lines.plot()
-        self._compare_images(ax=ax, filename=filename)
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/versioneer.py b/versioneer.py
new file mode 100644
index 0000000..7ed2a21
--- /dev/null
+++ b/versioneer.py
@@ -0,0 +1,1774 @@
+
+# Version: 0.16
+
+"""The Versioneer - like a rocketeer, but for versions.
+
+The Versioneer
+==============
+
+* like a rocketeer, but for versions!
+* https://github.com/warner/python-versioneer
+* Brian Warner
+* License: Public Domain
+* Compatible With: python2.6, 2.7, 3.3, 3.4, 3.5, and pypy
+* [![Latest Version]
+(https://pypip.in/version/versioneer/badge.svg?style=flat)
+](https://pypi.python.org/pypi/versioneer/)
+* [![Build Status]
+(https://travis-ci.org/warner/python-versioneer.png?branch=master)
+](https://travis-ci.org/warner/python-versioneer)
+
+This is a tool for managing a recorded version number in distutils-based
+python projects. The goal is to remove the tedious and error-prone "update
+the embedded version string" step from your release process. Making a new
+release should be as easy as recording a new tag in your version-control
+system, and maybe making new tarballs.
+
+
+## Quick Install
+
+* `pip install versioneer` to somewhere to your $PATH
+* add a `[versioneer]` section to your setup.cfg (see below)
+* run `versioneer install` in your source tree, commit the results
+
+## Version Identifiers
+
+Source trees come from a variety of places:
+
+* a version-control system checkout (mostly used by developers)
+* a nightly tarball, produced by build automation
+* a snapshot tarball, produced by a web-based VCS browser, like github's
+  "tarball from tag" feature
+* a release tarball, produced by "setup.py sdist", distributed through PyPI
+
+Within each source tree, the version identifier (either a string or a number,
+this tool is format-agnostic) can come from a variety of places:
+
+* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows
+  about recent "tags" and an absolute revision-id
+* the name of the directory into which the tarball was unpacked
+* an expanded VCS keyword ($Id$, etc)
+* a `_version.py` created by some earlier build step
+
+For released software, the version identifier is closely related to a VCS
+tag. Some projects use tag names that include more than just the version
+string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool
+needs to strip the tag prefix to extract the version identifier. For
+unreleased software (between tags), the version identifier should provide
+enough information to help developers recreate the same tree, while also
+giving them an idea of roughly how old the tree is (after version 1.2, before
+version 1.3). Many VCS systems can report a description that captures this,
+for example `git describe --tags --dirty --always` reports things like
+"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the
+0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has
+uncommitted changes.
+
+The version identifier is used for multiple purposes:
+
+* to allow the module to self-identify its version: `myproject.__version__`
+* to choose a name and prefix for a 'setup.py sdist' tarball
+
+## Theory of Operation
+
+Versioneer works by adding a special `_version.py` file into your source
+tree, where your `__init__.py` can import it. This `_version.py` knows how to
+dynamically ask the VCS tool for version information at import time.
+
+`_version.py` also contains `$Revision$` markers, and the installation
+process marks `_version.py` to have this marker rewritten with a tag name
+during the `git archive` command. As a result, generated tarballs will
+contain enough information to get the proper version.
+
+To allow `setup.py` to compute a version too, a `versioneer.py` is added to
+the top level of your source tree, next to `setup.py` and the `setup.cfg`
+that configures it. This overrides several distutils/setuptools commands to
+compute the version when invoked, and changes `setup.py build` and `setup.py
+sdist` to replace `_version.py` with a small static file that contains just
+the generated version data.
+
+## Installation
+
+First, decide on values for the following configuration variables:
+
+* `VCS`: the version control system you use. Currently accepts "git".
+
+* `style`: the style of version string to be produced. See "Styles" below for
+  details. Defaults to "pep440", which looks like
+  `TAG[+DISTANCE.gSHORTHASH[.dirty]]`.
+
+* `versionfile_source`:
+
+  A project-relative pathname into which the generated version strings should
+  be written. This is usually a `_version.py` next to your project's main
+  `__init__.py` file, so it can be imported at runtime. If your project uses
+  `src/myproject/__init__.py`, this should be `src/myproject/_version.py`.
+  This file should be checked in to your VCS as usual: the copy created below
+  by `setup.py setup_versioneer` will include code that parses expanded VCS
+  keywords in generated tarballs. The 'build' and 'sdist' commands will
+  replace it with a copy that has just the calculated version string.
+
+  This must be set even if your project does not have any modules (and will
+  therefore never import `_version.py`), since "setup.py sdist" -based trees
+  still need somewhere to record the pre-calculated version strings. Anywhere
+  in the source tree should do. If there is a `__init__.py` next to your
+  `_version.py`, the `setup.py setup_versioneer` command (described below)
+  will append some `__version__`-setting assignments, if they aren't already
+  present.
+
+* `versionfile_build`:
+
+  Like `versionfile_source`, but relative to the build directory instead of
+  the source directory. These will differ when your setup.py uses
+  'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`,
+  then you will probably have `versionfile_build='myproject/_version.py'` and
+  `versionfile_source='src/myproject/_version.py'`.
+
+  If this is set to None, then `setup.py build` will not attempt to rewrite
+  any `_version.py` in the built tree. If your project does not have any
+  libraries (e.g. if it only builds a script), then you should use
+  `versionfile_build = None`. To actually use the computed version string,
+  your `setup.py` will need to override `distutils.command.build_scripts`
+  with a subclass that explicitly inserts a copy of
+  `versioneer.get_version()` into your script file. See
+  `test/demoapp-script-only/setup.py` for an example.
+
+* `tag_prefix`:
+
+  a string, like 'PROJECTNAME-', which appears at the start of all VCS tags.
+  If your tags look like 'myproject-1.2.0', then you should use
+  tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this
+  should be an empty string, using either `tag_prefix=` or `tag_prefix=''`.
+
+* `parentdir_prefix`:
+
+  a optional string, frequently the same as tag_prefix, which appears at the
+  start of all unpacked tarball filenames. If your tarball unpacks into
+  'myproject-1.2.0', this should be 'myproject-'. To disable this feature,
+  just omit the field from your `setup.cfg`.
+
+This tool provides one script, named `versioneer`. That script has one mode,
+"install", which writes a copy of `versioneer.py` into the current directory
+and runs `versioneer.py setup` to finish the installation.
+
+To versioneer-enable your project:
+
+* 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and
+  populating it with the configuration values you decided earlier (note that
+  the option names are not case-sensitive):
+
+  ````
+  [versioneer]
+  VCS = git
+  style = pep440
+  versionfile_source = src/myproject/_version.py
+  versionfile_build = myproject/_version.py
+  tag_prefix =
+  parentdir_prefix = myproject-
+  ````
+
+* 2: Run `versioneer install`. This will do the following:
+
+  * copy `versioneer.py` into the top of your source tree
+  * create `_version.py` in the right place (`versionfile_source`)
+  * modify your `__init__.py` (if one exists next to `_version.py`) to define
+    `__version__` (by calling a function from `_version.py`)
+  * modify your `MANIFEST.in` to include both `versioneer.py` and the
+    generated `_version.py` in sdist tarballs
+
+  `versioneer install` will complain about any problems it finds with your
+  `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all
+  the problems.
+
+* 3: add a `import versioneer` to your setup.py, and add the following
+  arguments to the setup() call:
+
+        version=versioneer.get_version(),
+        cmdclass=versioneer.get_cmdclass(),
+
+* 4: commit these changes to your VCS. To make sure you won't forget,
+  `versioneer install` will mark everything it touched for addition using
+  `git add`. Don't forget to add `setup.py` and `setup.cfg` too.
+
+## Post-Installation Usage
+
+Once established, all uses of your tree from a VCS checkout should get the
+current version string. All generated tarballs should include an embedded
+version string (so users who unpack them will not need a VCS tool installed).
+
+If you distribute your project through PyPI, then the release process should
+boil down to two steps:
+
+* 1: git tag 1.0
+* 2: python setup.py register sdist upload
+
+If you distribute it through github (i.e. users use github to generate
+tarballs with `git archive`), the process is:
+
+* 1: git tag 1.0
+* 2: git push; git push --tags
+
+Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at
+least one tag in its history.
+
+## Version-String Flavors
+
+Code which uses Versioneer can learn about its version string at runtime by
+importing `_version` from your main `__init__.py` file and running the
+`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can
+import the top-level `versioneer.py` and run `get_versions()`.
+
+Both functions return a dictionary with different flavors of version
+information:
+
+* `['version']`: A condensed version string, rendered using the selected
+  style. This is the most commonly used value for the project's version
+  string. The default "pep440" style yields strings like `0.11`,
+  `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section
+  below for alternative styles.
+
+* `['full-revisionid']`: detailed revision identifier. For Git, this is the
+  full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac".
+
+* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that
+  this is only accurate if run in a VCS checkout, otherwise it is likely to
+  be False or None
+
+* `['error']`: if the version string could not be computed, this will be set
+  to a string describing the problem, otherwise it will be None. It may be
+  useful to throw an exception in setup.py if this is set, to avoid e.g.
+  creating tarballs with a version string of "unknown".
+
+Some variants are more useful than others. Including `full-revisionid` in a
+bug report should allow developers to reconstruct the exact code being tested
+(or indicate the presence of local changes that should be shared with the
+developers). `version` is suitable for display in an "about" box or a CLI
+`--version` output: it can be easily compared against release notes and lists
+of bugs fixed in various releases.
+
+The installer adds the following text to your `__init__.py` to place a basic
+version in `YOURPROJECT.__version__`:
+
+    from ._version import get_versions
+    __version__ = get_versions()['version']
+    del get_versions
+
+## Styles
+
+The setup.cfg `style=` configuration controls how the VCS information is
+rendered into a version string.
+
+The default style, "pep440", produces a PEP440-compliant string, equal to the
+un-prefixed tag name for actual releases, and containing an additional "local
+version" section with more detail for in-between builds. For Git, this is
+TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags
+--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the
+tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and
+that this commit is two revisions ("+2") beyond the "0.11" tag. For released
+software (exactly equal to a known tag), the identifier will only contain the
+stripped tag, e.g. "0.11".
+
+Other styles are available. See details.md in the Versioneer source tree for
+descriptions.
+
+## Debugging
+
+Versioneer tries to avoid fatal errors: if something goes wrong, it will tend
+to return a version of "0+unknown". To investigate the problem, run `setup.py
+version`, which will run the version-lookup code in a verbose mode, and will
+display the full contents of `get_versions()` (including the `error` string,
+which may help identify what went wrong).
+
+## Updating Versioneer
+
+To upgrade your project to a new release of Versioneer, do the following:
+
+* install the new Versioneer (`pip install -U versioneer` or equivalent)
+* edit `setup.cfg`, if necessary, to include any new configuration settings
+  indicated by the release notes
+* re-run `versioneer install` in your source tree, to replace
+  `SRC/_version.py`
+* commit any changed files
+
+### Upgrading to 0.16
+
+Nothing special.
+
+### Upgrading to 0.15
+
+Starting with this version, Versioneer is configured with a `[versioneer]`
+section in your `setup.cfg` file. Earlier versions required the `setup.py` to
+set attributes on the `versioneer` module immediately after import. The new
+version will refuse to run (raising an exception during import) until you
+have provided the necessary `setup.cfg` section.
+
+In addition, the Versioneer package provides an executable named
+`versioneer`, and the installation process is driven by running `versioneer
+install`. In 0.14 and earlier, the executable was named
+`versioneer-installer` and was run without an argument.
+
+### Upgrading to 0.14
+
+0.14 changes the format of the version string. 0.13 and earlier used
+hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a
+plus-separated "local version" section strings, with dot-separated
+components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old
+format, but should be ok with the new one.
+
+### Upgrading from 0.11 to 0.12
+
+Nothing special.
+
+### Upgrading from 0.10 to 0.11
+
+You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running
+`setup.py setup_versioneer`. This will enable the use of additional
+version-control systems (SVN, etc) in the future.
+
+## Future Directions
+
+This tool is designed to make it easily extended to other version-control
+systems: all VCS-specific components are in separate directories like
+src/git/ . The top-level `versioneer.py` script is assembled from these
+components by running make-versioneer.py . In the future, make-versioneer.py
+will take a VCS name as an argument, and will construct a version of
+`versioneer.py` that is specific to the given VCS. It might also take the
+configuration arguments that are currently provided manually during
+installation by editing setup.py . Alternatively, it might go the other
+direction and include code from all supported VCS systems, reducing the
+number of intermediate scripts.
+
+
+## License
+
+To make Versioneer easier to embed, all its code is dedicated to the public
+domain. The `_version.py` that it creates is also in the public domain.
+Specifically, both are released under the Creative Commons "Public Domain
+Dedication" license (CC0-1.0), as described in
+https://creativecommons.org/publicdomain/zero/1.0/ .
+
+"""
+
+from __future__ import print_function
+try:
+    import configparser
+except ImportError:
+    import ConfigParser as configparser
+import errno
+import json
+import os
+import re
+import subprocess
+import sys
+
+
+class VersioneerConfig:
+    """Container for Versioneer configuration parameters."""
+
+
+def get_root():
+    """Get the project root directory.
+
+    We require that all commands are run from the project root, i.e. the
+    directory that contains setup.py, setup.cfg, and versioneer.py .
+    """
+    root = os.path.realpath(os.path.abspath(os.getcwd()))
+    setup_py = os.path.join(root, "setup.py")
+    versioneer_py = os.path.join(root, "versioneer.py")
+    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+        # allow 'python path/to/setup.py COMMAND'
+        root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
+        setup_py = os.path.join(root, "setup.py")
+        versioneer_py = os.path.join(root, "versioneer.py")
+    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+        err = ("Versioneer was unable to run the project root directory. "
+               "Versioneer requires setup.py to be executed from "
+               "its immediate directory (like 'python setup.py COMMAND'), "
+               "or in a way that lets it use sys.argv[0] to find the root "
+               "(like 'python path/to/setup.py COMMAND').")
+        raise VersioneerBadRootError(err)
+    try:
+        # Certain runtime workflows (setup.py install/develop in a setuptools
+        # tree) execute all dependencies in a single python process, so
+        # "versioneer" may be imported multiple times, and python's shared
+        # module-import table will cache the first one. So we can't use
+        # os.path.dirname(__file__), as that will find whichever
+        # versioneer.py was first imported, even in later projects.
+        me = os.path.realpath(os.path.abspath(__file__))
+        if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]:
+            print("Warning: build in %s is using versioneer.py from %s"
+                  % (os.path.dirname(me), versioneer_py))
+    except NameError:
+        pass
+    return root
+
+
+def get_config_from_root(root):
+    """Read the project setup.cfg file to determine Versioneer config."""
+    # This might raise EnvironmentError (if setup.cfg is missing), or
+    # configparser.NoSectionError (if it lacks a [versioneer] section), or
+    # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
+    # the top of versioneer.py for instructions on writing your setup.cfg .
+    setup_cfg = os.path.join(root, "setup.cfg")
+    parser = configparser.SafeConfigParser()
+    with open(setup_cfg, "r") as f:
+        parser.readfp(f)
+    VCS = parser.get("versioneer", "VCS")  # mandatory
+
+    def get(parser, name):
+        if parser.has_option("versioneer", name):
+            return parser.get("versioneer", name)
+        return None
+    cfg = VersioneerConfig()
+    cfg.VCS = VCS
+    cfg.style = get(parser, "style") or ""
+    cfg.versionfile_source = get(parser, "versionfile_source")
+    cfg.versionfile_build = get(parser, "versionfile_build")
+    cfg.tag_prefix = get(parser, "tag_prefix")
+    if cfg.tag_prefix in ("''", '""'):
+        cfg.tag_prefix = ""
+    cfg.parentdir_prefix = get(parser, "parentdir_prefix")
+    cfg.verbose = get(parser, "verbose")
+    return cfg
+
+
+class NotThisMethod(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+# these dictionaries contain VCS-specific tools
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method):  # decorator
+    """Decorator to mark a method as the handler for a particular VCS."""
+    def decorate(f):
+        """Store f in HANDLERS[vcs][method]."""
+        if vcs not in HANDLERS:
+            HANDLERS[vcs] = {}
+        HANDLERS[vcs][method] = f
+        return f
+    return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
+    """Call the given command(s)."""
+    assert isinstance(commands, list)
+    p = None
+    for c in commands:
+        try:
+            dispcmd = str([c] + args)
+            # remember shell=False, so use git.cmd on windows, not just git
+            p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
+                                 stderr=(subprocess.PIPE if hide_stderr
+                                         else None))
+            break
+        except EnvironmentError:
+            e = sys.exc_info()[1]
+            if e.errno == errno.ENOENT:
+                continue
+            if verbose:
+                print("unable to run %s" % dispcmd)
+                print(e)
+            return None
+    else:
+        if verbose:
+            print("unable to find command, tried %s" % (commands,))
+        return None
+    stdout = p.communicate()[0].strip()
+    if sys.version_info[0] >= 3:
+        stdout = stdout.decode()
+    if p.returncode != 0:
+        if verbose:
+            print("unable to run %s (error)" % dispcmd)
+        return None
+    return stdout
+LONG_VERSION_PY['git'] = '''
+# This file helps to compute a version number in source trees obtained from
+# git-archive tarball (such as those provided by githubs download-from-tag
+# feature). Distribution tarballs (built by setup.py sdist) and build
+# directories (produced by setup.py build) will contain a much shorter file
+# that just contains the computed version number.
+
+# This file is released into the public domain. Generated by
+# versioneer-0.16 (https://github.com/warner/python-versioneer)
+
+"""Git implementation of _version.py."""
+
+import errno
+import os
+import re
+import subprocess
+import sys
+
+
+def get_keywords():
+    """Get the keywords needed to look up the version information."""
+    # these strings will be replaced by git during git-archive.
+    # setup.py/versioneer.py will grep for the variable names, so they must
+    # each be defined on a line of their own. _version.py will just call
+    # get_keywords().
+    git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
+    git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
+    keywords = {"refnames": git_refnames, "full": git_full}
+    return keywords
+
+
+class VersioneerConfig:
+    """Container for Versioneer configuration parameters."""
+
+
+def get_config():
+    """Create, populate and return the VersioneerConfig() object."""
+    # these strings are filled in when 'setup.py versioneer' creates
+    # _version.py
+    cfg = VersioneerConfig()
+    cfg.VCS = "git"
+    cfg.style = "%(STYLE)s"
+    cfg.tag_prefix = "%(TAG_PREFIX)s"
+    cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s"
+    cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s"
+    cfg.verbose = False
+    return cfg
+
+
+class NotThisMethod(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method):  # decorator
+    """Decorator to mark a method as the handler for a particular VCS."""
+    def decorate(f):
+        """Store f in HANDLERS[vcs][method]."""
+        if vcs not in HANDLERS:
+            HANDLERS[vcs] = {}
+        HANDLERS[vcs][method] = f
+        return f
+    return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
+    """Call the given command(s)."""
+    assert isinstance(commands, list)
+    p = None
+    for c in commands:
+        try:
+            dispcmd = str([c] + args)
+            # remember shell=False, so use git.cmd on windows, not just git
+            p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
+                                 stderr=(subprocess.PIPE if hide_stderr
+                                         else None))
+            break
+        except EnvironmentError:
+            e = sys.exc_info()[1]
+            if e.errno == errno.ENOENT:
+                continue
+            if verbose:
+                print("unable to run %%s" %% dispcmd)
+                print(e)
+            return None
+    else:
+        if verbose:
+            print("unable to find command, tried %%s" %% (commands,))
+        return None
+    stdout = p.communicate()[0].strip()
+    if sys.version_info[0] >= 3:
+        stdout = stdout.decode()
+    if p.returncode != 0:
+        if verbose:
+            print("unable to run %%s (error)" %% dispcmd)
+        return None
+    return stdout
+
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+    """Try to determine the version from the parent directory name.
+
+    Source tarballs conventionally unpack into a directory that includes
+    both the project name and a version string.
+    """
+    dirname = os.path.basename(root)
+    if not dirname.startswith(parentdir_prefix):
+        if verbose:
+            print("guessing rootdir is '%%s', but '%%s' doesn't start with "
+                  "prefix '%%s'" %% (root, dirname, parentdir_prefix))
+        raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+    return {"version": dirname[len(parentdir_prefix):],
+            "full-revisionid": None,
+            "dirty": False, "error": None}
+
+
+ at register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+    """Extract version information from the given file."""
+    # the code embedded in _version.py can just fetch the value of these
+    # keywords. When used from setup.py, we don't want to import _version.py,
+    # so we do it with a regexp instead. This function is not used from
+    # _version.py.
+    keywords = {}
+    try:
+        f = open(versionfile_abs, "r")
+        for line in f.readlines():
+            if line.strip().startswith("git_refnames ="):
+                mo = re.search(r'=\s*"(.*)"', line)
+                if mo:
+                    keywords["refnames"] = mo.group(1)
+            if line.strip().startswith("git_full ="):
+                mo = re.search(r'=\s*"(.*)"', line)
+                if mo:
+                    keywords["full"] = mo.group(1)
+        f.close()
+    except EnvironmentError:
+        pass
+    return keywords
+
+
+ at register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+    """Get version information from git keywords."""
+    if not keywords:
+        raise NotThisMethod("no keywords at all, weird")
+    refnames = keywords["refnames"].strip()
+    if refnames.startswith("$Format"):
+        if verbose:
+            print("keywords are unexpanded, not using")
+        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+    refs = set([r.strip() for r in refnames.strip("()").split(",")])
+    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+    TAG = "tag: "
+    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
+    if not tags:
+        # Either we're using git < 1.8.3, or there really are no tags. We use
+        # a heuristic: assume all version tags have a digit. The old git %%d
+        # expansion behaves like git log --decorate=short and strips out the
+        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+        # between branches and tags. By ignoring refnames without digits, we
+        # filter out many common branch names like "release" and
+        # "stabilization", as well as "HEAD" and "master".
+        tags = set([r for r in refs if re.search(r'\d', r)])
+        if verbose:
+            print("discarding '%%s', no digits" %% ",".join(refs-tags))
+    if verbose:
+        print("likely tags: %%s" %% ",".join(sorted(tags)))
+    for ref in sorted(tags):
+        # sorting will prefer e.g. "2.0" over "2.0rc1"
+        if ref.startswith(tag_prefix):
+            r = ref[len(tag_prefix):]
+            if verbose:
+                print("picking %%s" %% r)
+            return {"version": r,
+                    "full-revisionid": keywords["full"].strip(),
+                    "dirty": False, "error": None
+                    }
+    # no suitable tags, so version is "0+unknown", but full hex is still there
+    if verbose:
+        print("no suitable tags, using unknown + full revision id")
+    return {"version": "0+unknown",
+            "full-revisionid": keywords["full"].strip(),
+            "dirty": False, "error": "no suitable tags"}
+
+
+ at register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+    """Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if the git-archive 'subst' keywords were *not*
+    expanded, and _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+    if not os.path.exists(os.path.join(root, ".git")):
+        if verbose:
+            print("no .git in %%s" %% root)
+        raise NotThisMethod("no .git directory")
+
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+    # if there isn't one, this yields HEX[-dirty] (no NUM)
+    describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+                                      "--always", "--long",
+                                      "--match", "%%s*" %% tag_prefix],
+                               cwd=root)
+    # --long was added in git-1.5.5
+    if describe_out is None:
+        raise NotThisMethod("'git describe' failed")
+    describe_out = describe_out.strip()
+    full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+    if full_out is None:
+        raise NotThisMethod("'git rev-parse' failed")
+    full_out = full_out.strip()
+
+    pieces = {}
+    pieces["long"] = full_out
+    pieces["short"] = full_out[:7]  # maybe improved later
+    pieces["error"] = None
+
+    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+    # TAG might have hyphens.
+    git_describe = describe_out
+
+    # look for -dirty suffix
+    dirty = git_describe.endswith("-dirty")
+    pieces["dirty"] = dirty
+    if dirty:
+        git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+    # now we have TAG-NUM-gHEX or HEX
+
+    if "-" in git_describe:
+        # TAG-NUM-gHEX
+        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+        if not mo:
+            # unparseable. Maybe git-describe is misbehaving?
+            pieces["error"] = ("unable to parse git-describe output: '%%s'"
+                               %% describe_out)
+            return pieces
+
+        # tag
+        full_tag = mo.group(1)
+        if not full_tag.startswith(tag_prefix):
+            if verbose:
+                fmt = "tag '%%s' doesn't start with prefix '%%s'"
+                print(fmt %% (full_tag, tag_prefix))
+            pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'"
+                               %% (full_tag, tag_prefix))
+            return pieces
+        pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+        # distance: number of commits since tag
+        pieces["distance"] = int(mo.group(2))
+
+        # commit: short hex revision ID
+        pieces["short"] = mo.group(3)
+
+    else:
+        # HEX: no tags
+        pieces["closest-tag"] = None
+        count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+                                cwd=root)
+        pieces["distance"] = int(count_out)  # total number of commits
+
+    return pieces
+
+
+def plus_or_dot(pieces):
+    """Return a + if we don't already have one, else return a ."""
+    if "+" in pieces.get("closest-tag", ""):
+        return "."
+    return "+"
+
+
+def render_pep440(pieces):
+    """Build up version string, with post-release "local version identifier".
+
+    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+    Exceptions:
+    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += plus_or_dot(pieces)
+            rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_pre(pieces):
+    """TAG[.post.devDISTANCE] -- No -dirty.
+
+    Exceptions:
+    1: no tags. 0.post.devDISTANCE
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += ".post.dev%%d" %% pieces["distance"]
+    else:
+        # exception #1
+        rendered = "0.post.dev%%d" %% pieces["distance"]
+    return rendered
+
+
+def render_pep440_post(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+    The ".dev0" means dirty. Note that .dev0 sorts backwards
+    (a dirty tree will appear "older" than the corresponding clean one),
+    but you shouldn't be releasing software with -dirty anyways.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%%d" %% pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%%s" %% pieces["short"]
+    else:
+        # exception #1
+        rendered = "0.post%%d" %% pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+        rendered += "+g%%s" %% pieces["short"]
+    return rendered
+
+
+def render_pep440_old(pieces):
+    """TAG[.postDISTANCE[.dev0]] .
+
+    The ".dev0" means dirty.
+
+    Eexceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%%d" %% pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+    else:
+        # exception #1
+        rendered = "0.post%%d" %% pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+    return rendered
+
+
+def render_git_describe(pieces):
+    """TAG[-DISTANCE-gHEX][-dirty].
+
+    Like 'git describe --tags --dirty --always'.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render_git_describe_long(pieces):
+    """TAG-DISTANCE-gHEX[-dirty].
+
+    Like 'git describe --tags --dirty --always -long'.
+    The distance/hash is unconditional.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render(pieces, style):
+    """Render the given version pieces into the requested style."""
+    if pieces["error"]:
+        return {"version": "unknown",
+                "full-revisionid": pieces.get("long"),
+                "dirty": None,
+                "error": pieces["error"]}
+
+    if not style or style == "default":
+        style = "pep440"  # the default
+
+    if style == "pep440":
+        rendered = render_pep440(pieces)
+    elif style == "pep440-pre":
+        rendered = render_pep440_pre(pieces)
+    elif style == "pep440-post":
+        rendered = render_pep440_post(pieces)
+    elif style == "pep440-old":
+        rendered = render_pep440_old(pieces)
+    elif style == "git-describe":
+        rendered = render_git_describe(pieces)
+    elif style == "git-describe-long":
+        rendered = render_git_describe_long(pieces)
+    else:
+        raise ValueError("unknown style '%%s'" %% style)
+
+    return {"version": rendered, "full-revisionid": pieces["long"],
+            "dirty": pieces["dirty"], "error": None}
+
+
+def get_versions():
+    """Get version information or return default if unable to do so."""
+    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+    # __file__, we can work backwards from there to the root. Some
+    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
+    # case we can only use expanded keywords.
+
+    cfg = get_config()
+    verbose = cfg.verbose
+
+    try:
+        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+                                          verbose)
+    except NotThisMethod:
+        pass
+
+    try:
+        root = os.path.realpath(__file__)
+        # versionfile_source is the relative path from the top of the source
+        # tree (where the .git directory might live) to this file. Invert
+        # this to find the root from __file__.
+        for i in cfg.versionfile_source.split('/'):
+            root = os.path.dirname(root)
+    except NameError:
+        return {"version": "0+unknown", "full-revisionid": None,
+                "dirty": None,
+                "error": "unable to find root of source tree"}
+
+    try:
+        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+        return render(pieces, cfg.style)
+    except NotThisMethod:
+        pass
+
+    try:
+        if cfg.parentdir_prefix:
+            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+    except NotThisMethod:
+        pass
+
+    return {"version": "0+unknown", "full-revisionid": None,
+            "dirty": None,
+            "error": "unable to compute version"}
+'''
+
+
+ at register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+    """Extract version information from the given file."""
+    # the code embedded in _version.py can just fetch the value of these
+    # keywords. When used from setup.py, we don't want to import _version.py,
+    # so we do it with a regexp instead. This function is not used from
+    # _version.py.
+    keywords = {}
+    try:
+        f = open(versionfile_abs, "r")
+        for line in f.readlines():
+            if line.strip().startswith("git_refnames ="):
+                mo = re.search(r'=\s*"(.*)"', line)
+                if mo:
+                    keywords["refnames"] = mo.group(1)
+            if line.strip().startswith("git_full ="):
+                mo = re.search(r'=\s*"(.*)"', line)
+                if mo:
+                    keywords["full"] = mo.group(1)
+        f.close()
+    except EnvironmentError:
+        pass
+    return keywords
+
+
+ at register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+    """Get version information from git keywords."""
+    if not keywords:
+        raise NotThisMethod("no keywords at all, weird")
+    refnames = keywords["refnames"].strip()
+    if refnames.startswith("$Format"):
+        if verbose:
+            print("keywords are unexpanded, not using")
+        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+    refs = set([r.strip() for r in refnames.strip("()").split(",")])
+    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+    TAG = "tag: "
+    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
+    if not tags:
+        # Either we're using git < 1.8.3, or there really are no tags. We use
+        # a heuristic: assume all version tags have a digit. The old git %d
+        # expansion behaves like git log --decorate=short and strips out the
+        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+        # between branches and tags. By ignoring refnames without digits, we
+        # filter out many common branch names like "release" and
+        # "stabilization", as well as "HEAD" and "master".
+        tags = set([r for r in refs if re.search(r'\d', r)])
+        if verbose:
+            print("discarding '%s', no digits" % ",".join(refs-tags))
+    if verbose:
+        print("likely tags: %s" % ",".join(sorted(tags)))
+    for ref in sorted(tags):
+        # sorting will prefer e.g. "2.0" over "2.0rc1"
+        if ref.startswith(tag_prefix):
+            r = ref[len(tag_prefix):]
+            if verbose:
+                print("picking %s" % r)
+            return {"version": r,
+                    "full-revisionid": keywords["full"].strip(),
+                    "dirty": False, "error": None
+                    }
+    # no suitable tags, so version is "0+unknown", but full hex is still there
+    if verbose:
+        print("no suitable tags, using unknown + full revision id")
+    return {"version": "0+unknown",
+            "full-revisionid": keywords["full"].strip(),
+            "dirty": False, "error": "no suitable tags"}
+
+
+ at register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+    """Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if the git-archive 'subst' keywords were *not*
+    expanded, and _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+    if not os.path.exists(os.path.join(root, ".git")):
+        if verbose:
+            print("no .git in %s" % root)
+        raise NotThisMethod("no .git directory")
+
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+    # if there isn't one, this yields HEX[-dirty] (no NUM)
+    describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+                                      "--always", "--long",
+                                      "--match", "%s*" % tag_prefix],
+                               cwd=root)
+    # --long was added in git-1.5.5
+    if describe_out is None:
+        raise NotThisMethod("'git describe' failed")
+    describe_out = describe_out.strip()
+    full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+    if full_out is None:
+        raise NotThisMethod("'git rev-parse' failed")
+    full_out = full_out.strip()
+
+    pieces = {}
+    pieces["long"] = full_out
+    pieces["short"] = full_out[:7]  # maybe improved later
+    pieces["error"] = None
+
+    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+    # TAG might have hyphens.
+    git_describe = describe_out
+
+    # look for -dirty suffix
+    dirty = git_describe.endswith("-dirty")
+    pieces["dirty"] = dirty
+    if dirty:
+        git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+    # now we have TAG-NUM-gHEX or HEX
+
+    if "-" in git_describe:
+        # TAG-NUM-gHEX
+        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+        if not mo:
+            # unparseable. Maybe git-describe is misbehaving?
+            pieces["error"] = ("unable to parse git-describe output: '%s'"
+                               % describe_out)
+            return pieces
+
+        # tag
+        full_tag = mo.group(1)
+        if not full_tag.startswith(tag_prefix):
+            if verbose:
+                fmt = "tag '%s' doesn't start with prefix '%s'"
+                print(fmt % (full_tag, tag_prefix))
+            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+                               % (full_tag, tag_prefix))
+            return pieces
+        pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+        # distance: number of commits since tag
+        pieces["distance"] = int(mo.group(2))
+
+        # commit: short hex revision ID
+        pieces["short"] = mo.group(3)
+
+    else:
+        # HEX: no tags
+        pieces["closest-tag"] = None
+        count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+                                cwd=root)
+        pieces["distance"] = int(count_out)  # total number of commits
+
+    return pieces
+
+
+def do_vcs_install(manifest_in, versionfile_source, ipy):
+    """Git-specific installation logic for Versioneer.
+
+    For Git, this means creating/changing .gitattributes to mark _version.py
+    for export-time keyword substitution.
+    """
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+    files = [manifest_in, versionfile_source]
+    if ipy:
+        files.append(ipy)
+    try:
+        me = __file__
+        if me.endswith(".pyc") or me.endswith(".pyo"):
+            me = os.path.splitext(me)[0] + ".py"
+        versioneer_file = os.path.relpath(me)
+    except NameError:
+        versioneer_file = "versioneer.py"
+    files.append(versioneer_file)
+    present = False
+    try:
+        f = open(".gitattributes", "r")
+        for line in f.readlines():
+            if line.strip().startswith(versionfile_source):
+                if "export-subst" in line.strip().split()[1:]:
+                    present = True
+        f.close()
+    except EnvironmentError:
+        pass
+    if not present:
+        f = open(".gitattributes", "a+")
+        f.write("%s export-subst\n" % versionfile_source)
+        f.close()
+        files.append(".gitattributes")
+    run_command(GITS, ["add", "--"] + files)
+
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+    """Try to determine the version from the parent directory name.
+
+    Source tarballs conventionally unpack into a directory that includes
+    both the project name and a version string.
+    """
+    dirname = os.path.basename(root)
+    if not dirname.startswith(parentdir_prefix):
+        if verbose:
+            print("guessing rootdir is '%s', but '%s' doesn't start with "
+                  "prefix '%s'" % (root, dirname, parentdir_prefix))
+        raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+    return {"version": dirname[len(parentdir_prefix):],
+            "full-revisionid": None,
+            "dirty": False, "error": None}
+
+SHORT_VERSION_PY = """
+# This file was generated by 'versioneer.py' (0.16) from
+# revision-control system data, or from the parent directory name of an
+# unpacked source archive. Distribution tarballs contain a pre-generated copy
+# of this file.
+
+import json
+import sys
+
+version_json = '''
+%s
+'''  # END VERSION_JSON
+
+
+def get_versions():
+    return json.loads(version_json)
+"""
+
+
+def versions_from_file(filename):
+    """Try to determine the version from _version.py if present."""
+    try:
+        with open(filename) as f:
+            contents = f.read()
+    except EnvironmentError:
+        raise NotThisMethod("unable to read _version.py")
+    mo = re.search(r"version_json = '''\n(.*)'''  # END VERSION_JSON",
+                   contents, re.M | re.S)
+    if not mo:
+        raise NotThisMethod("no version_json in _version.py")
+    return json.loads(mo.group(1))
+
+
+def write_to_version_file(filename, versions):
+    """Write the given version number to the given _version.py file."""
+    os.unlink(filename)
+    contents = json.dumps(versions, sort_keys=True,
+                          indent=1, separators=(",", ": "))
+    with open(filename, "w") as f:
+        f.write(SHORT_VERSION_PY % contents)
+
+    print("set %s to '%s'" % (filename, versions["version"]))
+
+
+def plus_or_dot(pieces):
+    """Return a + if we don't already have one, else return a ."""
+    if "+" in pieces.get("closest-tag", ""):
+        return "."
+    return "+"
+
+
+def render_pep440(pieces):
+    """Build up version string, with post-release "local version identifier".
+
+    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+    Exceptions:
+    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += plus_or_dot(pieces)
+            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_pre(pieces):
+    """TAG[.post.devDISTANCE] -- No -dirty.
+
+    Exceptions:
+    1: no tags. 0.post.devDISTANCE
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += ".post.dev%d" % pieces["distance"]
+    else:
+        # exception #1
+        rendered = "0.post.dev%d" % pieces["distance"]
+    return rendered
+
+
+def render_pep440_post(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+    The ".dev0" means dirty. Note that .dev0 sorts backwards
+    (a dirty tree will appear "older" than the corresponding clean one),
+    but you shouldn't be releasing software with -dirty anyways.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%s" % pieces["short"]
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+        rendered += "+g%s" % pieces["short"]
+    return rendered
+
+
+def render_pep440_old(pieces):
+    """TAG[.postDISTANCE[.dev0]] .
+
+    The ".dev0" means dirty.
+
+    Eexceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+    return rendered
+
+
+def render_git_describe(pieces):
+    """TAG[-DISTANCE-gHEX][-dirty].
+
+    Like 'git describe --tags --dirty --always'.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render_git_describe_long(pieces):
+    """TAG-DISTANCE-gHEX[-dirty].
+
+    Like 'git describe --tags --dirty --always -long'.
+    The distance/hash is unconditional.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render(pieces, style):
+    """Render the given version pieces into the requested style."""
+    if pieces["error"]:
+        return {"version": "unknown",
+                "full-revisionid": pieces.get("long"),
+                "dirty": None,
+                "error": pieces["error"]}
+
+    if not style or style == "default":
+        style = "pep440"  # the default
+
+    if style == "pep440":
+        rendered = render_pep440(pieces)
+    elif style == "pep440-pre":
+        rendered = render_pep440_pre(pieces)
+    elif style == "pep440-post":
+        rendered = render_pep440_post(pieces)
+    elif style == "pep440-old":
+        rendered = render_pep440_old(pieces)
+    elif style == "git-describe":
+        rendered = render_git_describe(pieces)
+    elif style == "git-describe-long":
+        rendered = render_git_describe_long(pieces)
+    else:
+        raise ValueError("unknown style '%s'" % style)
+
+    return {"version": rendered, "full-revisionid": pieces["long"],
+            "dirty": pieces["dirty"], "error": None}
+
+
+class VersioneerBadRootError(Exception):
+    """The project root directory is unknown or missing key files."""
+
+
+def get_versions(verbose=False):
+    """Get the project version from whatever source is available.
+
+    Returns dict with two keys: 'version' and 'full'.
+    """
+    if "versioneer" in sys.modules:
+        # see the discussion in cmdclass.py:get_cmdclass()
+        del sys.modules["versioneer"]
+
+    root = get_root()
+    cfg = get_config_from_root(root)
+
+    assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg"
+    handlers = HANDLERS.get(cfg.VCS)
+    assert handlers, "unrecognized VCS '%s'" % cfg.VCS
+    verbose = verbose or cfg.verbose
+    assert cfg.versionfile_source is not None, \
+        "please set versioneer.versionfile_source"
+    assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
+
+    versionfile_abs = os.path.join(root, cfg.versionfile_source)
+
+    # extract version from first of: _version.py, VCS command (e.g. 'git
+    # describe'), parentdir. This is meant to work for developers using a
+    # source checkout, for users of a tarball created by 'setup.py sdist',
+    # and for users of a tarball/zipball created by 'git archive' or github's
+    # download-from-tag feature or the equivalent in other VCSes.
+
+    get_keywords_f = handlers.get("get_keywords")
+    from_keywords_f = handlers.get("keywords")
+    if get_keywords_f and from_keywords_f:
+        try:
+            keywords = get_keywords_f(versionfile_abs)
+            ver = from_keywords_f(keywords, cfg.tag_prefix, verbose)
+            if verbose:
+                print("got version from expanded keyword %s" % ver)
+            return ver
+        except NotThisMethod:
+            pass
+
+    try:
+        ver = versions_from_file(versionfile_abs)
+        if verbose:
+            print("got version from file %s %s" % (versionfile_abs, ver))
+        return ver
+    except NotThisMethod:
+        pass
+
+    from_vcs_f = handlers.get("pieces_from_vcs")
+    if from_vcs_f:
+        try:
+            pieces = from_vcs_f(cfg.tag_prefix, root, verbose)
+            ver = render(pieces, cfg.style)
+            if verbose:
+                print("got version from VCS %s" % ver)
+            return ver
+        except NotThisMethod:
+            pass
+
+    try:
+        if cfg.parentdir_prefix:
+            ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+            if verbose:
+                print("got version from parentdir %s" % ver)
+            return ver
+    except NotThisMethod:
+        pass
+
+    if verbose:
+        print("unable to compute version")
+
+    return {"version": "0+unknown", "full-revisionid": None,
+            "dirty": None, "error": "unable to compute version"}
+
+
+def get_version():
+    """Get the short version string for this project."""
+    return get_versions()["version"]
+
+
+def get_cmdclass():
+    """Get the custom setuptools/distutils subclasses used by Versioneer."""
+    if "versioneer" in sys.modules:
+        del sys.modules["versioneer"]
+        # this fixes the "python setup.py develop" case (also 'install' and
+        # 'easy_install .'), in which subdependencies of the main project are
+        # built (using setup.py bdist_egg) in the same python process. Assume
+        # a main project A and a dependency B, which use different versions
+        # of Versioneer. A's setup.py imports A's Versioneer, leaving it in
+        # sys.modules by the time B's setup.py is executed, causing B to run
+        # with the wrong versioneer. Setuptools wraps the sub-dep builds in a
+        # sandbox that restores sys.modules to it's pre-build state, so the
+        # parent is protected against the child's "import versioneer". By
+        # removing ourselves from sys.modules here, before the child build
+        # happens, we protect the child from the parent's versioneer too.
+        # Also see https://github.com/warner/python-versioneer/issues/52
+
+    cmds = {}
+
+    # we add "version" to both distutils and setuptools
+    from distutils.core import Command
+
+    class cmd_version(Command):
+        description = "report generated version string"
+        user_options = []
+        boolean_options = []
+
+        def initialize_options(self):
+            pass
+
+        def finalize_options(self):
+            pass
+
+        def run(self):
+            vers = get_versions(verbose=True)
+            print("Version: %s" % vers["version"])
+            print(" full-revisionid: %s" % vers.get("full-revisionid"))
+            print(" dirty: %s" % vers.get("dirty"))
+            if vers["error"]:
+                print(" error: %s" % vers["error"])
+    cmds["version"] = cmd_version
+
+    # we override "build_py" in both distutils and setuptools
+    #
+    # most invocation pathways end up running build_py:
+    #  distutils/build -> build_py
+    #  distutils/install -> distutils/build ->..
+    #  setuptools/bdist_wheel -> distutils/install ->..
+    #  setuptools/bdist_egg -> distutils/install_lib -> build_py
+    #  setuptools/install -> bdist_egg ->..
+    #  setuptools/develop -> ?
+
+    # we override different "build_py" commands for both environments
+    if "setuptools" in sys.modules:
+        from setuptools.command.build_py import build_py as _build_py
+    else:
+        from distutils.command.build_py import build_py as _build_py
+
+    class cmd_build_py(_build_py):
+        def run(self):
+            root = get_root()
+            cfg = get_config_from_root(root)
+            versions = get_versions()
+            _build_py.run(self)
+            # now locate _version.py in the new build/ directory and replace
+            # it with an updated value
+            if cfg.versionfile_build:
+                target_versionfile = os.path.join(self.build_lib,
+                                                  cfg.versionfile_build)
+                print("UPDATING %s" % target_versionfile)
+                write_to_version_file(target_versionfile, versions)
+    cmds["build_py"] = cmd_build_py
+
+    if "cx_Freeze" in sys.modules:  # cx_freeze enabled?
+        from cx_Freeze.dist import build_exe as _build_exe
+
+        class cmd_build_exe(_build_exe):
+            def run(self):
+                root = get_root()
+                cfg = get_config_from_root(root)
+                versions = get_versions()
+                target_versionfile = cfg.versionfile_source
+                print("UPDATING %s" % target_versionfile)
+                write_to_version_file(target_versionfile, versions)
+
+                _build_exe.run(self)
+                os.unlink(target_versionfile)
+                with open(cfg.versionfile_source, "w") as f:
+                    LONG = LONG_VERSION_PY[cfg.VCS]
+                    f.write(LONG %
+                            {"DOLLAR": "$",
+                             "STYLE": cfg.style,
+                             "TAG_PREFIX": cfg.tag_prefix,
+                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
+                             })
+        cmds["build_exe"] = cmd_build_exe
+        del cmds["build_py"]
+
+    # we override different "sdist" commands for both environments
+    if "setuptools" in sys.modules:
+        from setuptools.command.sdist import sdist as _sdist
+    else:
+        from distutils.command.sdist import sdist as _sdist
+
+    class cmd_sdist(_sdist):
+        def run(self):
+            versions = get_versions()
+            self._versioneer_generated_versions = versions
+            # unless we update this, the command will keep using the old
+            # version
+            self.distribution.metadata.version = versions["version"]
+            return _sdist.run(self)
+
+        def make_release_tree(self, base_dir, files):
+            root = get_root()
+            cfg = get_config_from_root(root)
+            _sdist.make_release_tree(self, base_dir, files)
+            # now locate _version.py in the new base_dir directory
+            # (remembering that it may be a hardlink) and replace it with an
+            # updated value
+            target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
+            print("UPDATING %s" % target_versionfile)
+            write_to_version_file(target_versionfile,
+                                  self._versioneer_generated_versions)
+    cmds["sdist"] = cmd_sdist
+
+    return cmds
+
+
+CONFIG_ERROR = """
+setup.cfg is missing the necessary Versioneer configuration. You need
+a section like:
+
+ [versioneer]
+ VCS = git
+ style = pep440
+ versionfile_source = src/myproject/_version.py
+ versionfile_build = myproject/_version.py
+ tag_prefix =
+ parentdir_prefix = myproject-
+
+You will also need to edit your setup.py to use the results:
+
+ import versioneer
+ setup(version=versioneer.get_version(),
+       cmdclass=versioneer.get_cmdclass(), ...)
+
+Please read the docstring in ./versioneer.py for configuration instructions,
+edit setup.cfg, and re-run the installer or 'python versioneer.py setup'.
+"""
+
+SAMPLE_CONFIG = """
+# See the docstring in versioneer.py for instructions. Note that you must
+# re-run 'versioneer.py setup' after changing this section, and commit the
+# resulting files.
+
+[versioneer]
+#VCS = git
+#style = pep440
+#versionfile_source =
+#versionfile_build =
+#tag_prefix =
+#parentdir_prefix =
+
+"""
+
+INIT_PY_SNIPPET = """
+from ._version import get_versions
+__version__ = get_versions()['version']
+del get_versions
+"""
+
+
+def do_setup():
+    """Main VCS-independent setup function for installing Versioneer."""
+    root = get_root()
+    try:
+        cfg = get_config_from_root(root)
+    except (EnvironmentError, configparser.NoSectionError,
+            configparser.NoOptionError) as e:
+        if isinstance(e, (EnvironmentError, configparser.NoSectionError)):
+            print("Adding sample versioneer config to setup.cfg",
+                  file=sys.stderr)
+            with open(os.path.join(root, "setup.cfg"), "a") as f:
+                f.write(SAMPLE_CONFIG)
+        print(CONFIG_ERROR, file=sys.stderr)
+        return 1
+
+    print(" creating %s" % cfg.versionfile_source)
+    with open(cfg.versionfile_source, "w") as f:
+        LONG = LONG_VERSION_PY[cfg.VCS]
+        f.write(LONG % {"DOLLAR": "$",
+                        "STYLE": cfg.style,
+                        "TAG_PREFIX": cfg.tag_prefix,
+                        "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+                        "VERSIONFILE_SOURCE": cfg.versionfile_source,
+                        })
+
+    ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
+                       "__init__.py")
+    if os.path.exists(ipy):
+        try:
+            with open(ipy, "r") as f:
+                old = f.read()
+        except EnvironmentError:
+            old = ""
+        if INIT_PY_SNIPPET not in old:
+            print(" appending to %s" % ipy)
+            with open(ipy, "a") as f:
+                f.write(INIT_PY_SNIPPET)
+        else:
+            print(" %s unmodified" % ipy)
+    else:
+        print(" %s doesn't exist, ok" % ipy)
+        ipy = None
+
+    # Make sure both the top-level "versioneer.py" and versionfile_source
+    # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so
+    # they'll be copied into source distributions. Pip won't be able to
+    # install the package without this.
+    manifest_in = os.path.join(root, "MANIFEST.in")
+    simple_includes = set()
+    try:
+        with open(manifest_in, "r") as f:
+            for line in f:
+                if line.startswith("include "):
+                    for include in line.split()[1:]:
+                        simple_includes.add(include)
+    except EnvironmentError:
+        pass
+    # That doesn't cover everything MANIFEST.in can do
+    # (http://docs.python.org/2/distutils/sourcedist.html#commands), so
+    # it might give some false negatives. Appending redundant 'include'
+    # lines is safe, though.
+    if "versioneer.py" not in simple_includes:
+        print(" appending 'versioneer.py' to MANIFEST.in")
+        with open(manifest_in, "a") as f:
+            f.write("include versioneer.py\n")
+    else:
+        print(" 'versioneer.py' already in MANIFEST.in")
+    if cfg.versionfile_source not in simple_includes:
+        print(" appending versionfile_source ('%s') to MANIFEST.in" %
+              cfg.versionfile_source)
+        with open(manifest_in, "a") as f:
+            f.write("include %s\n" % cfg.versionfile_source)
+    else:
+        print(" versionfile_source already in MANIFEST.in")
+
+    # Make VCS-specific changes. For git, this means creating/changing
+    # .gitattributes to mark _version.py for export-time keyword
+    # substitution.
+    do_vcs_install(manifest_in, cfg.versionfile_source, ipy)
+    return 0
+
+
+def scan_setup_py():
+    """Validate the contents of setup.py against Versioneer's expectations."""
+    found = set()
+    setters = False
+    errors = 0
+    with open("setup.py", "r") as f:
+        for line in f.readlines():
+            if "import versioneer" in line:
+                found.add("import")
+            if "versioneer.get_cmdclass()" in line:
+                found.add("cmdclass")
+            if "versioneer.get_version()" in line:
+                found.add("get_version")
+            if "versioneer.VCS" in line:
+                setters = True
+            if "versioneer.versionfile_source" in line:
+                setters = True
+    if len(found) != 3:
+        print("")
+        print("Your setup.py appears to be missing some important items")
+        print("(but I might be wrong). Please make sure it has something")
+        print("roughly like the following:")
+        print("")
+        print(" import versioneer")
+        print(" setup( version=versioneer.get_version(),")
+        print("        cmdclass=versioneer.get_cmdclass(),  ...)")
+        print("")
+        errors += 1
+    if setters:
+        print("You should remove lines like 'versioneer.VCS = ' and")
+        print("'versioneer.versionfile_source = ' . This configuration")
+        print("now lives in setup.cfg, and should be removed from setup.py")
+        print("")
+        errors += 1
+    return errors
+
+if __name__ == "__main__":
+    cmd = sys.argv[1]
+    if cmd == "setup":
+        errors = do_setup()
+        errors += scan_setup_py()
+        if errors:
+            sys.exit(1)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-geopandas.git



More information about the Pkg-grass-devel mailing list