diff --git a/landsat/landsat.py b/landsat/landsat.py index 01472ac..376ac2f 100755 --- a/landsat/landsat.py +++ b/landsat/landsat.py @@ -32,7 +32,7 @@ Commands: Search: - landsat.py search [-p --pathrow] [--lat] [--lon] [-l LIMIT] [-s START] [-e END] [-c CLOUD] [-h] + landsat.py search [-p --pathrow] [--lat] [--lon] [--address] [-l LIMIT] [-s START] [-e END] [-c CLOUD] [-h] optional arguments: -p, --pathrow Paths and Rows in order separated by comma. Use quotes "001,003". @@ -42,6 +42,8 @@ --lon Longitude + --address Street address + -l LIMIT, --limit LIMIT Search return results limit default is 10 @@ -184,6 +186,7 @@ def args_options(): 'Example: path,row,path,row 001,001,190,204') parser_search.add_argument('--lat', type=float, help='The latitude') parser_search.add_argument('--lon', type=float, help='The longitude') + parser_search.add_argument('--address', type=str, help='The address') parser_search.add_argument('--json', action='store_true', help='Returns a bare JSON response') parser_download = subparsers.add_parser('download', @@ -308,9 +311,14 @@ def main(args): except ValueError: return ["The latitude and longitude values must be valid numbers", 1] + address = args.address + if address and (lat and lon): + return ["Cannot specify both address and latitude-longitude"] + result = s.search(paths_rows=args.pathrow, lat=lat, lon=lon, + address=address, limit=args.limit, start_date=args.start, end_date=args.end, diff --git a/landsat/search.py b/landsat/search.py index 65ebc46..5ee0be8 100644 --- a/landsat/search.py +++ b/landsat/search.py @@ -6,7 +6,7 @@ import requests import settings -from utils import three_digit, create_paired_list +from utils import three_digit, create_paired_list, geocode class Search(object): @@ -15,7 +15,7 @@ class Search(object): def __init__(self): self.api_url = settings.API_URL - def search(self, paths_rows=None, lat=None, lon=None, start_date=None, end_date=None, cloud_min=None, + def search(self, paths_rows=None, lat=None, lon=None, address=None, start_date=None, end_date=None, cloud_min=None, cloud_max=None, limit=1): """ The main method of Search class. It searches Development Seed's Landsat API. @@ -32,6 +32,10 @@ def search(self, paths_rows=None, lat=None, lon=None, start_date=None, end_date= The The longitude :type lon: String, float, integer + :param address: + The address + :type address: + String :param start_date: Date string. format: YYYY-MM-DD :type start_date: @@ -78,7 +82,7 @@ def search(self, paths_rows=None, lat=None, lon=None, start_date=None, end_date= } """ - search_string = self.query_builder(paths_rows, lat, lon, start_date, end_date, cloud_min, cloud_max) + search_string = self.query_builder(paths_rows, lat, lon, address, start_date, end_date, cloud_min, cloud_max) # Have to manually build the URI to bypass requests URI encoding # The api server doesn't accept encoded URIs @@ -109,7 +113,7 @@ def search(self, paths_rows=None, lat=None, lon=None, start_date=None, end_date= return result - def query_builder(self, paths_rows=None, lat=None, lon=None, start_date=None, end_date=None, + def query_builder(self, paths_rows=None, lat=None, lon=None, address=None, start_date=None, end_date=None, cloud_min=None, cloud_max=None): """ Builds the proper search syntax (query) for Landsat API. @@ -125,6 +129,10 @@ def query_builder(self, paths_rows=None, lat=None, lon=None, start_date=None, en The The longitude :type lon: String, float, integer + :param address: + The address + :type address: + String :param start_date: Date string. format: YYYY-MM-DD :type start_date: @@ -171,7 +179,9 @@ def query_builder(self, paths_rows=None, lat=None, lon=None, start_date=None, en elif cloud_max: query.append(self.cloud_cover_prct_range_builder('-1', cloud_max)) - if lat and lon: + if address: + query.append(self.address_builder(address)) + elif lat and lon: query.append(self.lat_lon_builder(lat, lon)) if query: @@ -241,6 +251,20 @@ def cloud_cover_prct_range_builder(self, min=0, max=100): """ return 'cloudCoverFull:[%s+TO+%s]' % (min, max) + def address_builder(self, address): + """ Builds lat and lon query from a geocoded address. + + :param address: + The address + :type address: + String + + :returns: + String + """ + geocoded = geocode(address) + return self.lat_lon_builder(**geocoded) + def lat_lon_builder(self, lat=0, lon=0): """ Builds lat and lon query. diff --git a/landsat/utils.py b/landsat/utils.py index 3107ffc..840ea91 100644 --- a/landsat/utils.py +++ b/landsat/utils.py @@ -7,6 +7,7 @@ import re from cStringIO import StringIO from datetime import datetime +import geocoder from mixins import VerbosityMixin @@ -280,6 +281,52 @@ def convert_to_integer_list(value): return s +# Geocoding confidence scores, from https://github.com/DenisCarriere/geocoder/blob/master/docs/features/Confidence%20Score.md +geocode_confidences = { + 10: 0.25, + 9: 0.5, + 8: 1., + 7: 5., + 6: 7.5, + 5: 10., + 4: 15., + 3: 20., + 2: 25., + 1: 99999., + # 0: unable to locate at all +} + + +def geocode(address, required_precision_km=1.): + """ Identifies the coordinates of an address + + :param address: + the address to be geocoded + :type value: + String + :param required_precision_km: + the maximum permissible geographic uncertainty for the geocoding + :type required_precision_km: + float + + :returns: + dict + + :example: + >>> geocode('1600 Pennsylvania Ave NW, Washington, DC 20500') + {'lat': 38.89767579999999, 'lon': -77.0364827} + + """ + geocoded = geocoder.google(address) + precision_km = geocode_confidences[geocoded.confidence] + + if precision_km <= required_precision_km: + (lon, lat) = geocoded.geometry['coordinates'] + return {'lat': lat, 'lon': lon} + else: + raise ValueError("Address could not be precisely located") + + def convert_to_float_list(value): """ Converts a comma separate string to a list diff --git a/requirements/docker.txt b/requirements/docker.txt index 1df380f..e4a8c18 100644 --- a/requirements/docker.txt +++ b/requirements/docker.txt @@ -6,3 +6,4 @@ six==1.9.0 homura>=0.1.2 boto>=2.38.0 polyline==1.1 +geocoder>=1.5.1 diff --git a/tests/test_utils.py b/tests/test_utils.py index 2b959ab..66c9fdb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -105,6 +105,11 @@ def test_convert_to_integer_list(self): r = utils.convert_to_integer_list('1,11,10,QA') self.assertEqual([1, 11, 10, 'QA'], r) + def test_geocode(self): + self.assertEqual({'lat': 38.89767579999999, 'lon': -77.0364823}, utils.geocode('1600 Pennsylvania Ave NW, Washington, DC 20500')) + self.assertRaises(ValueError, utils.geocode, 'Pennsylvania Ave NW, Washington, DC') + self.assertEqual({'lat': 38.8987352, 'lon': -77.0350902}, utils.geocode('Pennsylvania Ave NW, Washington, DC', 10.)) + def test_convert_to_float_list(self): # correct input r = utils.convert_to_float_list('-1,2,-3') @@ -166,5 +171,6 @@ def test_adjust_bounding_box(self): self.assertEqual(utils.adjust_bounding_box(origin, target), origin) + if __name__ == '__main__': unittest.main()