Source code for errorgeopy.location

"""A "Location" is a collecion of responses from geocoding services, each one a
distinct attempt to either find a string address given a point (reverse geocode)
or an attempt to find a point that best matches a string address (forward
geocode). A Location is a collection, because fundamentally *ErrorGeoPy* is
oriented to working across providers, and considering all of their results as a
related set of responses.

A "LocationClusters" object, also defined here, is also a collection of
addresses. but is slightly less abstract in that the members of the collection
are organised into clusters, based on some clustering algorithm.

Heavy use is made of shapely in return values of methods for these classes.

.. moduleauthor Richard Law <richard.m.law@gmail.com>
"""

from functools import wraps

import geopy
from shapely.geometry import MultiPoint, GeometryCollection
from shapely.ops import transform

from errorgeopy import utils


def _check_points_exist(func):
    """Decorator for checking that the first argument of a function has a
    points property.
    """

    @wraps(func)
    def inner(*args, **kwargs):
        if not args[0].points:
            return None
        else:
            return func(*args, **kwargs)

    return inner


def _check_polygonisable(func):
    """Decorator for checking that the first argument of a function has a method
    called "_polgonisable" that takes no methods, and returns True.
    """

    @wraps(func)
    def inner(*args, **kwargs):
        if not args[0]._polygonisable():
            return None
        else:
            return func(*args, **kwargs)

    return inner


def _check_concave_hull_calcuable(func):
    """Decorator for checking that there are enough candidates to compute a
    concave hull.
    """

    @wraps(func)
    def inner(*args, **kwargs):
        if len(args[0]) < 4:
            return None
        else:
            return func(*args, **kwargs)

    return inner


def _check_convex_hull_calcuable(func):
    """Decorator for checking that there are enough candidates to compute a
    concave hull.
    """

    @wraps(func)
    def inner(*args, **kwargs):
        if len(args[0]) < 3:
            return None
        else:
            return func(*args, **kwargs)

    return inner


def _check_cluster_calculable(func):
    """Decorator for checking that there are enough locations for calculating
    clusters (mininum of 2).
    """

    @wraps(func)
    def inner(*args, **kwargs):
        if len(args[0]._location) < 3:
            return []
        else:
            return func(*args, **kwargs)

    return inner


[docs]class Location(object): """Represents a collection of parsed geocoder responses, each of which are geopy.Location objects, representing the results of different geocoding services for the same query. """ @utils.check_location_type def __init__(self, locations): self._locations = locations or [] def __unicode__(self): return '\n'.join(self.addresses) def __str__(self): return self.__unicode__() def __repr__(self): return '\n'.join([repr(l) for l in self._locations]) def __getitem__(self, index): return self._locations[index] def __setitem__(self, index, value): if not isinstance(value, geopy.Location): raise TypeError self.locations[index] = value def __eq__(self, other): if not isinstance(other, Location): return False for l, o in zip(self.locations, other): if not l == o: return False return True def __ne__(self, other): return not self.__eq__(other) def __len__(self): return len(self._locations) def _polygonisable(self): if not self._locations or len(self.locations) <= 1: return False return True @property def locations(self): """A sequence of geopy.Location objects. """ if not isinstance(self._locations, list): return [self._locations] else: return self._locations @property def addresses(self): """geopy.Location.address properties for all candidate locations as a sequence of strings. """ return [l.address for l in self.locations] @property def points(self): """An array of geopy.Point objects representing the candidate locations physical positions. These are geodetic points, with latitude, longitude, and altitude (in kilometres, when supported by providers; defaults to 0. """ return [l.point for l in self.locations] @property @_check_points_exist def multipoint(self): """A shapely.geometry.MultiPoint of the Location members. """ return MultiPoint(self._shapely_points()) @property @_check_points_exist def centroid(self): """A shapely.geometry.Point of the centre of all candidate address locations (centre of the multipoint). """ return self.multipoint.centroid @property @_check_points_exist def most_central_location(self): """A shapely.geometry.Point representing the geometry of the candidate location that is nearest to the geometric centre of all of the candidate locations. """ return utils.point_nearest_point(self._shapely_points(), self.centroid) @property @_check_points_exist def mbc(self): """A shapely.geometry.Polygon representing the minimum bounding circle of the candidate locations. """ return utils.minimum_bounding_circle( [p[0:2] for p in self._tuple_points()]) @property @_check_concave_hull_calcuable @_check_polygonisable def concave_hull(self, alpha=0.15): """A concave hull of the Location, as a shapely.geometry.Polygon object. Needs at least four candidates, or else this property is None. Kwargs: alpha (float): The parameter for the alpha shape """ return utils.concave_hull([p[0:2] for p in self._tuple_points()], alpha) @property @_check_convex_hull_calcuable @_check_polygonisable def convex_hull(self): """A convex hull of the Location, as a shapely.geometry.Polygon object. Needs at least three candidates, or else this property is None. """ return utils.convex_hull(self._tuple_points()) @property @_check_points_exist def clusters(self): """Clusters that have been identified in the Location's candidate addresses, as an errorgeopy.location.LocationClusters object. """ return LocationClusters(self) def _shapely_points(self, epsg=None): if epsg: projection = utils.get_proj(epsg) points = [transform(projection, p) for p in self.points] return utils.array_geopy_points_to_shapely_points(self.points) def _tuple_points(self, epsg=None): if epsg: projection = utils.get_proj(epsg) points = [transform(projection, p) for p in self.points] return utils.array_geopy_points_to_xyz_tuples(self.points if not epsg else points)
# TODO it'd be nice to have the names of the geocoder that produced each cluster member; this would require extending geopy.Location to include this information
[docs]class LocationClusters(object): """Represents clusters of addresses identified from an errorgeopy.Location object, which itself is one coherent collection of respones from multiple geocoding services for the same query. """ def __init__(self, location): self._location = location def __len__(self): return len(self.clusters) def __str__(self): return self.__unicode__() def __unicode__(self): return '\n'.join([str(c.location) for c in self.clusters]) def __getitem__(self, index): return self.clusters[index] @property @_check_cluster_calculable def clusters(self): """A sequence of clusters identified from the input. May have length 0 if no clusters can be determined. """ return utils.get_clusters(self._location, Location) @property def geometry_collection(self): """GeometryCollection of clusters as multipoint geometries. """ return GeometryCollection( [c.location.multipoint for c in self.clusters]) @property def cluster_centres(self): """Multipoint of cluster geometric centroids. """ return MultiPoint([c.centroid for c in self.clusters])