Skip to content

Commit

Permalink
Initial commit to github
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Jennings committed Mar 13, 2012
0 parents commit 2dd7065
Show file tree
Hide file tree
Showing 15 changed files with 1,533 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.pyc
*.swp
*.py~
build
dist
python_omgeo.egg-info
docs
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v1.0, 2012-03-13 -- Initial Release
20 changes: 20 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2012 Azavea, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include README.rst
include LICENSE.txt
18 changes: 18 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
**The Oatmeal Geocoder - Python Edition**

``python-omgeo`` is a geocoding abstraction layer written in python. Currently
supported geocoders:

* Bing
* ESRI's North American locator
* ESRI's European address locator
* Nominatim


See the source for more info. Here's a quick example.

>>> from omgeo import Geocoder
>>> from omgeo.places import PlaceQuery
>>> g = Geocoder()
>>> you_are_here = PlaceQuery('340 N 12th St Philadelphia PA')
>>> candidates = g.geocode(you_are_here)
116 changes: 116 additions & 0 deletions omgeo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import copy
from omgeo.processors.postprocessors import DupePicker

class Geocoder():
"""
The base geocode class. This class can be initialized with settings
for each geocoder and/or settings for the geocoder itself.
Arguments:
==========
sources -- a dictionary of GeocodeServiceConfig() parameters,
keyed by module name for the GeocodeService to use
ex: {'esri_na':{},
'bing': {
'settings': {},
'preprocessors': [],
'postprocessors': []}, ...}
preprocessors -- list of universal preprocessors to use
postprocessors -- list of universal postprocessors to use
"""
_sources = []
"""
A list of classes representing the geocode services that will be used
to find addresses for the given locations
"""
_preprocessors = []
"""
Preprocessor instances to apply to each address requested
"""
_postprocessors = []
"""
Postprocessor instances to apply to each result obtained from the geocoders
"""
_settings = {}
"""
Reserved for future use.
"""

DEFAULT_SOURCES = [['omgeo.services.EsriNA', {}],
['omgeo.services.EsriEU', {}],
['omgeo.services.Nominatim', {}]]
DEFAULT_PREPROCESSORS = []
DEFAULT_POSTPROCESSORS = [
DupePicker('match_addr', 'locator', ['rooftop', 'parcel', 'interpolation_offset', 'interpolation']),
]


def _get_service_by_name(self, service_name):
module, separator, class_name = service_name.rpartition('.')
m = __import__( module )
path = service_name.split('.')[1:]
for p in path:
m = getattr(m, p)
return m

def add_source(self, source):
geocode_service = self._get_service_by_name(source[0])
self._sources.append(geocode_service(**source[1]))

def remove_source(self, source):
geocode_service = self._get_service_by_name(source[0])
self._sources.remove(geocode_service(**source[1]))

def set_sources(self, sources):
"""
Creates GeocodeServiceConfigs from each str source
Argument:
=========
sources -- list of source-settings pairs
ex. "[['EsriNA', {}], ['Nominatim', {}]]"
"""
if len(sources) == 0:
raise Exception('Must declare at least one source for a geocoder')
self._sources = []
for source in sources: # iterate through a list of sources
self.add_source(source)


def __init__(self,
sources=DEFAULT_SOURCES,
preprocessors=DEFAULT_PREPROCESSORS,
postprocessors=DEFAULT_POSTPROCESSORS):

self.set_sources(sources)
self._preprocessors = preprocessors
self._postprocessors = postprocessors

def geocode(self, pq, waterfall=_settings.get('waterfall', False)):
"""
Returns a list of Candidate objects
Arguments:
==========
pq -- A PlaceQuery object (required).
waterfall -- Boolean set to True if all geocoders listed should
be used to find results, instead of stopping after
the first geocoding service with valid candidates
(default False).
"""
processed_pq = copy.copy(pq)
for p in self._preprocessors: # apply universal address preprocessing
processed_pq = p.process(processed_pq)
if processed_pq == False: return []

processed_candidates = []
for gs in self._sources: # iterate through each GeocodeService
candidates = gs.geocode(processed_pq)
processed_candidates += candidates # merge lists
if waterfall is False and len(processed_candidates) > 0:
break # if we have >= 1 good candidate, don't go to next geocoder

for p in self._postprocessors: # apply universal candidate postprocessing
processed_candidates = p.process(processed_candidates)

return processed_candidates
121 changes: 121 additions & 0 deletions omgeo/places.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
class Viewbox():
"""
Class representing a bounding box.
Defaults to maximum bounds for WKID 4326.
Arguments:
==========
left -- Minimum X value (default -180)
top -- Maximum Y value (default 90)
right -- Maximum X value (default 180)
bottom -- Minimum Y value (default -90)
wkid -- Well-known ID for spatial reference system (default 4326)
"""
def _validate(self):
"""
Return True if WKID is found and Viewbox is within maximum bounds.
Return True if WKID is not found.
Otherwise raise error.
"""
return True #TODO: Find max bounds from WKID in PostGIS database

def convert_srs(self, new_wkid):
"""
Return a new Viewbox object with the specified SRS.
"""
return self # for now

def __init__(self, left=-180, top=90, right=180, bottom=-90, wkid=4326):
for k in locals().keys():
if k != 'self': setattr(self, k, locals()[k])
self._validate()

def to_bing_str(self):
"""
Convert Viewbox object to a string that can be used by Bing
as a query parameter.
"""
vb = self.convert_srs(4326)
return '%s,%s,%s,%s' % (vb.bottom, vb.left, vb.top, vb.right)

def to_mapquest_str(self):
"""
Convert Viewbox object to a string that can be used by
MapQuest as a query parameter.
"""
vb = self.convert_srs(4326)
return '%s,%s,%s,%s' % (vb.left, vb.top, vb.right, vb.bottom)

class PlaceQuery():
"""
Class representing an address or place passed to geocoders.
Arguments:
==========
query -- A string containing the query to parse
and match to a coordinate on the map.
*ex: "340 N 12th St Philadelphia PA 19107"
or "Wolf Building, Philadelphia"*
address -- A string for the street line of an address.
*ex: "340 N 12th St"*
city -- A string specifying the populated place for the address.
This commonly refers to a city, but may refer to a suburb
or neighborhood in certain countries.
state -- A string for the state, province, territory, etc.
postal -- A string for the postal / ZIP Code
country -- A string for the country or region. Because the geocoder
uses the country to determine which geocoding service to use,
this is strongly recommended for efficency. ISO alpha-2 is
preferred, and is required by some geocoder services.
viewbox -- A Viewbox object indicating the preferred area
to find search results (default None)
bounded -- Boolean indicating whether or not to only
return candidates within the given Viewbox (default False)
Keyword Arguments:
==================
user_lat -- A float representing the Latitude of the end-user.
user_lon -- A float representing the Longitude of the end-user.
user_ip -- A string representing the IP address of the end-user.
culture -- Culture code to be used for the request (used by Bing).
For example, if set to 'de', the country for a U.S. address
would be returned as "Vereinigte Staaten Von Amerika"
instead of "United States".
"""
def __init__(self, query='', address='', city='', state='', postal='', country='',
viewbox=None, bounded=False, **kwargs):
for k in locals().keys():
if k not in ['self', 'kwargs']: setattr(self, k, locals()[k])
if query == '' and address == '' and city == '' and state == '' and postal == '':
raise Exception('Must provide query or one or more of address, city, state, and postal.')
for k in kwargs:
setattr(self, k, kwargs[k])

class Candidate():
"""
Class representing a candidate address returned from geocoders.
Accepts arguments defined below, plus informal keyword arguments.
Arguments:
==========
locator -- Locator used for geocoding (default '')
score -- Standardized score (default 0)
match_addr -- Address returned by geocoder (default '')
x -- X-coordinate (longitude for lat-lon SRS) (default None)
y -- Y-coordinate (latitude for lat-lon SRS) (default None)
wkid -- Well-known ID for spatial reference system (default 4326)
entity -- Used by Bing (default '')
confidence -- Used by Bing (default '')
geoservice -- GeocodeService used for geocoding (default '')
Usage Example:
==============
c = Candidate('US_RoofTop', 91.5, '340 N 12th St, Philadelphia, PA, 19107',
'-75.16', '39.95', some_extra_data='yellow')
"""
def __init__(self, locator='', score=0, match_addr='', x=None, y=None,
wkid=4326, entity='', confidence='', **kwargs):
for k in locals().keys():
if k not in ['self', 'kwargs']: setattr(self, k, locals()[k])
for k in kwargs:
setattr(self, k, kwargs[k])
32 changes: 32 additions & 0 deletions omgeo/processors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class _Processor():
def _init_helper(self, vars_):
"""Overwrite defaults (if they exist) with arguments passed to constructor"""
for k in vars_:
if k == 'kwargs':
for kwarg in vars_[k]:
setattr(self, kwarg, vars_[k][kwarg])
elif k != 'self':
setattr(self, k, vars_[k])

def __init__(self, **kwargs):
"""
Constructor for Processor.
In a subclass, arguments may be formally defined to avoid the use of keywords
(and to throw errors when bogus keyword arguments are passed):
def __init__(self, arg1='foo', arg2='bar')
"""
self._init_helper(vars())

class PreProcessor(_Processor):
"""Takes, processes, and returns a geocoding.places.PlaceQuery object."""
def process(self, pq):
raise NotImplementedError(
'PreProcessor subclasses must implement process().')

class PostProcessor(_Processor):
"""Takes, processes, and returns list of geocoding.places.Candidate objects."""
def process(self, candidates):
raise NotImplementedError(
'PostProcessor subclasses must implement process().')
Loading

0 comments on commit 2dd7065

Please sign in to comment.