Compare commits
6 Commits
cda2195419
...
42ee45acc2
Author | SHA1 | Date | |
---|---|---|---|
|
42ee45acc2 | ||
|
85f18d2518 | ||
|
5b0a0768cb | ||
|
43b85d5214 | ||
|
5aafac2025 | ||
|
70a33ff00d |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
geoapi.ini
|
||||||
|
.idea/
|
21
LICENSE.md
Executable file
21
LICENSE.md
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
### MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Erbosoft Metaverse Design Solutions
|
||||||
|
|
||||||
|
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.
|
21
README.md
21
README.md
@ -1,3 +1,22 @@
|
|||||||
# distance-calc
|
# distance-calc
|
||||||
|
|
||||||
Python-based distance calculator to help determine how far new apartment complexes are from the office.
|
A Python-based distance calculator to help determine how far new apartment complexes are from the office.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
* **gcoder.py** - Takes an address on the command line and returns its latitude and longitude.
|
||||||
|
May use one of four different geocoding engines.
|
||||||
|
* **dcalc.py** - Takes an address on the command line and returns its latitude and longitude,
|
||||||
|
distance from the VMware Carbon Black Boulder office, and projected drive time to the
|
||||||
|
Boulder office. Uses Google APIs.
|
||||||
|
* **batch_distance.py** - Same as dcalc, but uses CSV files for input and output.
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
All scripts make use of a configuration file, called `geoapi.ini` by default, that contains the
|
||||||
|
API keys. Copy the file `geoapi.ini.template` and add your API keys. You can also specify a different
|
||||||
|
configuration file with the `-C` or `--config` option to each script.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Thanks to VMware Carbon Black for sponsoring Hackathon, which provided the time to pursue this research.
|
||||||
|
9
distance-calc.iml
Executable file
9
distance-calc.iml
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
12
geoapi.ini.template
Executable file
12
geoapi.ini.template
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
[google]
|
||||||
|
apikey=<<API KEY HERE>>
|
||||||
|
|
||||||
|
[mapquest]
|
||||||
|
apikey=<<CONSUMER KEY HERE>>
|
||||||
|
secret=<<CONSUMER SECRET HERE>>
|
||||||
|
|
||||||
|
[geocodio]
|
||||||
|
apikey=<<API KEY HERE>>
|
||||||
|
|
||||||
|
[locationiq]
|
||||||
|
token=<<TOKEN HERE>>
|
161
src/batch_distance.py
Executable file
161
src/batch_distance.py
Executable file
@ -0,0 +1,161 @@
|
|||||||
|
# Batch_Distance.py: Batch process a bunch of distance measurements.
|
||||||
|
# *****************************************************************************
|
||||||
|
# Copyright (c) Erbosoft Metaverse Design Solutions 2020. All Rights Reserved.
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# *****************************************************************************
|
||||||
|
# *
|
||||||
|
# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT
|
||||||
|
# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN,
|
||||||
|
# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED
|
||||||
|
# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY,
|
||||||
|
# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import configparser
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
class GeocodingError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_google(config, address):
|
||||||
|
apikey = config['google']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("Google API key not specified")
|
||||||
|
query = { 'key': apikey, 'address': address, 'region': 'us'}
|
||||||
|
url = 'https://maps.googleapis.com/maps/api/geocode/json?' + \
|
||||||
|
urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
stat = apireturn['status']
|
||||||
|
if stat == 'OK':
|
||||||
|
results = apireturn['results']
|
||||||
|
if len(results) > 1:
|
||||||
|
raise GeocodingError(f"Google API returned ambiguous results (total count {len(results)})")
|
||||||
|
coords = results[0]['geometry']['location']
|
||||||
|
return coords['lat'], coords['lng']
|
||||||
|
elif stat == 'ZERO_RESULTS':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns status of {stat}")
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
OFFICE_LOCATION = (40.0187905, -105.2764775) # office location - 1433 Pearl Street, Boulder, CO
|
||||||
|
RADIUS = 6371.0 * 1000.0 # Earth radius in meters
|
||||||
|
METERS_PER_MILE = 1852.0 # number of meters per mile
|
||||||
|
|
||||||
|
|
||||||
|
def distance_miles(point1, point2):
|
||||||
|
Φ1 = math.radians(point1[0])
|
||||||
|
Φ2 = math.radians(point2[0])
|
||||||
|
ΔΦ = math.radians(point2[0] - point1[0])
|
||||||
|
Δλ = math.radians(point2[1] - point1[1])
|
||||||
|
a = math.sin(ΔΦ / 2.0) * math.sin(ΔΦ / 2.0) + math.cos(Φ1) * math.cos(Φ2) * math.sin(Δλ / 2.0) * math.sin(Δλ / 2.0)
|
||||||
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
|
||||||
|
return RADIUS * c / METERS_PER_MILE
|
||||||
|
|
||||||
|
|
||||||
|
THRESHOLD_RADIUS = 15.0 # number of miles in "acceptable" threshold
|
||||||
|
ERROR_BAR_WIDTH = 0.5 # number of miles in "error bar" surrounding threshold
|
||||||
|
|
||||||
|
|
||||||
|
def classify_distance(dist):
|
||||||
|
if dist > (THRESHOLD_RADIUS + ERROR_BAR_WIDTH / 2):
|
||||||
|
return 'RED'
|
||||||
|
if dist >= (THRESHOLD_RADIUS - ERROR_BAR_WIDTH / 2):
|
||||||
|
return 'YELLOW'
|
||||||
|
return 'GREEN'
|
||||||
|
|
||||||
|
|
||||||
|
def transit_time_google(config, org, dest):
|
||||||
|
apikey = config['google']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("Google API key not specified")
|
||||||
|
str_origin = f"{org[0]},{org[1]}"
|
||||||
|
str_dest = f"{dest[0]},{dest[1]}"
|
||||||
|
query = {'key': apikey, 'origin': str_origin, 'destination': str_dest, 'mode': 'driving', 'region': 'us'}
|
||||||
|
url = 'https://maps.googleapis.com/maps/api/directions/json?' + \
|
||||||
|
urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
stat = apireturn['status']
|
||||||
|
if stat == 'OK':
|
||||||
|
best_time = None
|
||||||
|
best_summary = None
|
||||||
|
for route in apireturn['routes']:
|
||||||
|
summary = route['summary']
|
||||||
|
current_time = 0
|
||||||
|
for leg in route['legs']:
|
||||||
|
current_time += leg['duration']['value']
|
||||||
|
if best_time is None or current_time < best_time:
|
||||||
|
best_time = current_time
|
||||||
|
best_summary = summary
|
||||||
|
if best_time is None:
|
||||||
|
return None
|
||||||
|
return best_time, best_summary
|
||||||
|
elif stat == 'ZERO_RESULTS':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns status of {stat}")
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
def stringize_seconds(total_secs):
|
||||||
|
secs = total_secs % 60
|
||||||
|
remainder = total_secs // 60
|
||||||
|
if remainder == 0:
|
||||||
|
return f"{secs}s"
|
||||||
|
mins = remainder % 60
|
||||||
|
hours = remainder // 60
|
||||||
|
if hours == 0:
|
||||||
|
return f"{mins}m{secs}s"
|
||||||
|
return f"{hours}h{mins}m{secs}s"
|
||||||
|
|
||||||
|
|
||||||
|
cmdline_parser = argparse.ArgumentParser()
|
||||||
|
cmdline_parser.add_argument('input', help='The input file containing locations to be checked.')
|
||||||
|
cmdline_parser.add_argument('output', nargs='?', help='The output file name that will receive the processed data.')
|
||||||
|
cmdline_parser.add_argument('-C', '--config', default='geoapi.ini', help='The geocoding API configuration file')
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
opts = cmdline_parser.parse_args(args)
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(opts.config)
|
||||||
|
output_name = opts.output
|
||||||
|
if not output_name:
|
||||||
|
output_name = opts.input + '.out'
|
||||||
|
with open(opts.input, newline='') as input_file:
|
||||||
|
dialect = csv.Sniffer().sniff(input_file.read(1024))
|
||||||
|
input_file.seek(0)
|
||||||
|
reader = csv.reader(input_file, dialect)
|
||||||
|
with open(output_name, mode='w') as output_file:
|
||||||
|
writer = csv.writer(output_file, dialect)
|
||||||
|
for in_row in reader:
|
||||||
|
coords = geocode_google(config, in_row[1])
|
||||||
|
if coords:
|
||||||
|
dist = distance_miles(OFFICE_LOCATION, coords)
|
||||||
|
out_row = [in_row[0], in_row[1], coords[0], coords[1], dist, classify_distance(dist)]
|
||||||
|
transit_time = transit_time_google(config, coords, OFFICE_LOCATION)
|
||||||
|
if transit_time:
|
||||||
|
out_row += [transit_time[0], stringize_seconds(transit_time[0]), transit_time[1]]
|
||||||
|
else:
|
||||||
|
out_row += ['*** Unable to calculate transit time']
|
||||||
|
else:
|
||||||
|
out_row = [in_row[0], in_row[1], '*** Unable to locate']
|
||||||
|
writer.writerow(out_row)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
140
src/dcalc.py
Executable file
140
src/dcalc.py
Executable file
@ -0,0 +1,140 @@
|
|||||||
|
# DCalc.py: basic test of distance calculation from the Boulder office
|
||||||
|
# *****************************************************************************
|
||||||
|
# Copyright (c) Erbosoft Metaverse Design Solutions 2020. All Rights Reserved.
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# *****************************************************************************
|
||||||
|
# *
|
||||||
|
# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT
|
||||||
|
# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN,
|
||||||
|
# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED
|
||||||
|
# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY,
|
||||||
|
# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import configparser
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
class GeocodingError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_google(config, address):
|
||||||
|
apikey = config['google']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("Google API key not specified")
|
||||||
|
query = { 'key': apikey, 'address': address, 'region': 'us'}
|
||||||
|
url = 'https://maps.googleapis.com/maps/api/geocode/json?' + \
|
||||||
|
urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
stat = apireturn['status']
|
||||||
|
if stat == 'OK':
|
||||||
|
results = apireturn['results']
|
||||||
|
if len(results) > 1:
|
||||||
|
raise GeocodingError(f"Google API returned ambiguous results (total count {len(results)})")
|
||||||
|
coords = results[0]['geometry']['location']
|
||||||
|
return coords['lat'], coords['lng']
|
||||||
|
elif stat == 'ZERO_RESULTS':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns status of {stat}")
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
OFFICE_LOCATION = (40.0187905, -105.2764775) # office location - 1433 Pearl Street, Boulder, CO
|
||||||
|
RADIUS = 6371.0 * 1000.0 # Earth radius in meters
|
||||||
|
METERS_PER_MILE = 1852.0 # number of meters per mile
|
||||||
|
|
||||||
|
|
||||||
|
def distance_miles(point1, point2):
|
||||||
|
Φ1 = math.radians(point1[0])
|
||||||
|
Φ2 = math.radians(point2[0])
|
||||||
|
ΔΦ = math.radians(point2[0] - point1[0])
|
||||||
|
Δλ = math.radians(point2[1] - point1[1])
|
||||||
|
a = math.sin(ΔΦ / 2.0) * math.sin(ΔΦ / 2.0) + math.cos(Φ1) * math.cos(Φ2) * math.sin(Δλ / 2.0) * math.sin(Δλ / 2.0)
|
||||||
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
|
||||||
|
return RADIUS * c / METERS_PER_MILE
|
||||||
|
|
||||||
|
|
||||||
|
def transit_time_google(config, org, dest):
|
||||||
|
apikey = config['google']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("Google API key not specified")
|
||||||
|
str_origin = f"{org[0]},{org[1]}"
|
||||||
|
str_dest = f"{dest[0]},{dest[1]}"
|
||||||
|
query = {'key': apikey, 'origin': str_origin, 'destination': str_dest, 'mode': 'driving', 'region': 'us'}
|
||||||
|
url = 'https://maps.googleapis.com/maps/api/directions/json?' + \
|
||||||
|
urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
stat = apireturn['status']
|
||||||
|
if stat == 'OK':
|
||||||
|
best_time = None
|
||||||
|
best_summary = None
|
||||||
|
for route in apireturn['routes']:
|
||||||
|
summary = route['summary']
|
||||||
|
current_time = 0
|
||||||
|
for leg in route['legs']:
|
||||||
|
current_time += leg['duration']['value']
|
||||||
|
if best_time is None or current_time < best_time:
|
||||||
|
best_time = current_time
|
||||||
|
best_summary = summary
|
||||||
|
if best_time is None:
|
||||||
|
return None
|
||||||
|
return best_time, best_summary
|
||||||
|
elif stat == 'ZERO_RESULTS':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns status of {stat}")
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
def stringize_seconds(total_secs):
|
||||||
|
secs = total_secs % 60
|
||||||
|
remainder = total_secs // 60
|
||||||
|
if remainder == 0:
|
||||||
|
return f"{secs}s"
|
||||||
|
mins = remainder % 60
|
||||||
|
hours = remainder // 60
|
||||||
|
if hours == 0:
|
||||||
|
return f"{mins}m{secs}s"
|
||||||
|
return f"{hours}h{mins}m{secs}s"
|
||||||
|
|
||||||
|
|
||||||
|
cmdline_parser = argparse.ArgumentParser()
|
||||||
|
cmdline_parser.add_argument('address', nargs='+', help='The address to be calculated')
|
||||||
|
cmdline_parser.add_argument('-C', '--config', default='geoapi.ini', help='The geocoding API configuration file')
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
opts = cmdline_parser.parse_args(args)
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(opts.config)
|
||||||
|
my_address = ' '.join(opts.address)
|
||||||
|
print(f"Address: '{my_address}'")
|
||||||
|
coords = geocode_google(config, my_address)
|
||||||
|
if coords:
|
||||||
|
print(f"Coordinates: Latitude {coords[0]}, longitude {coords[1]}")
|
||||||
|
dist = distance_miles(OFFICE_LOCATION, coords)
|
||||||
|
print(f"Distance from Boulder office: {dist} miles")
|
||||||
|
transit_time = transit_time_google(config, coords, OFFICE_LOCATION)
|
||||||
|
if transit_time:
|
||||||
|
str_time = stringize_seconds(transit_time[0])
|
||||||
|
print(f"Estimated transit time: {str_time} - route: {transit_time[1]} ({transit_time[0]} seconds)")
|
||||||
|
else:
|
||||||
|
print("Unable to calculate transit time.")
|
||||||
|
else:
|
||||||
|
print("Location was not found.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
134
src/gcoder.py
Executable file
134
src/gcoder.py
Executable file
@ -0,0 +1,134 @@
|
|||||||
|
# GCoder.py: basic geocoding test
|
||||||
|
# *****************************************************************************
|
||||||
|
# Copyright (c) Erbosoft Metaverse Design Solutions 2020. All Rights Reserved.
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# *****************************************************************************
|
||||||
|
# *
|
||||||
|
# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT
|
||||||
|
# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN,
|
||||||
|
# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED
|
||||||
|
# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY,
|
||||||
|
# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import configparser
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
class GeocodingError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_google(config, address):
|
||||||
|
apikey = config['google']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("Google API key not specified")
|
||||||
|
query = { 'key': apikey, 'address': address, 'region': 'us'}
|
||||||
|
url = 'https://maps.googleapis.com/maps/api/geocode/json?' + \
|
||||||
|
urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
stat = apireturn['status']
|
||||||
|
if stat == 'OK':
|
||||||
|
results = apireturn['results']
|
||||||
|
if len(results) > 1:
|
||||||
|
raise GeocodingError(f"Google API returned ambiguous results (total count {len(results)})")
|
||||||
|
coords = results[0]['geometry']['location']
|
||||||
|
return coords['lat'], coords['lng']
|
||||||
|
elif stat == 'ZERO_RESULTS':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns status of {stat}")
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Google API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_mapquest(config, address):
|
||||||
|
apikey = config['mapquest']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("MapQuest API key not specified")
|
||||||
|
query = { 'key': apikey, 'inFormat': 'kvp', 'outFormat': 'json', 'location': address,
|
||||||
|
'thumbMaps': 'false', 'maxResults': 1}
|
||||||
|
url = 'https://www.mapquestapi.com/geocoding/v1/address?' + urllib.parse.urlencode(query)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
stat = apireturn['info']['statuscode']
|
||||||
|
if stat == 0:
|
||||||
|
results = apireturn['results']
|
||||||
|
if len(results) > 1:
|
||||||
|
raise GeocodingError(f"MapQuest API returned ambiguous results (total count {len(results)})")
|
||||||
|
locations = results[0]['locations']
|
||||||
|
if len(locations) > 1:
|
||||||
|
raise GeocodingError(f"MapQuest API returned ambiguous locations (total count {len(locations)})")
|
||||||
|
coords = locations[0]['latLng']
|
||||||
|
return coords['lat'], coords['lng']
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"MapQuest API returns status of {stat}")
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"MapQuest API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_geocodio(config, address):
|
||||||
|
apikey = config['geocodio']['apikey']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("Geocodio API key not specified")
|
||||||
|
query = {'q': address, 'api_key': apikey}
|
||||||
|
url = 'https://api.geocod.io/v1.6/geocode?' + urllib.parse.urlencode(query)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
coords = apireturn['results'][0]['location']
|
||||||
|
return coords['lat'], coords['lng']
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"Geocodio API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_locationiq(config, address):
|
||||||
|
apikey = config['locationiq']['token']
|
||||||
|
if not apikey:
|
||||||
|
raise GeocodingError("LocationIQ API key not specified")
|
||||||
|
query = {'q': address, 'key': apikey, 'format': 'json', 'limit': 1}
|
||||||
|
url = 'https://us1.locationiq.com/v1/search.php?' + urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
apireturn = json.loads(response.read())
|
||||||
|
place = apireturn[0]
|
||||||
|
return place['lat'], place['lon']
|
||||||
|
else:
|
||||||
|
raise GeocodingError(f"LocationIQ API returns {response.status} HTTP status code")
|
||||||
|
|
||||||
|
|
||||||
|
geocoding_procs = {
|
||||||
|
'google': geocode_google,
|
||||||
|
'mapquest': geocode_mapquest,
|
||||||
|
'geocodio': geocode_geocodio,
|
||||||
|
'locationiq': geocode_locationiq
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdline_parser = argparse.ArgumentParser()
|
||||||
|
cmdline_parser.add_argument('address', nargs='+', help='The address to be geocoded')
|
||||||
|
cmdline_parser.add_argument('-C', '--config', default='geoapi.ini', help='The geocoding API configuration file')
|
||||||
|
cmdline_parser.add_argument('-g', '--geocoder', default='google', choices=geocoding_procs.keys(),
|
||||||
|
help='Geocoding processor to use to get coordinates (default: google)')
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
opts = cmdline_parser.parse_args(args)
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(opts.config)
|
||||||
|
my_address = ' '.join(opts.address)
|
||||||
|
print(f"Address: '{my_address}'")
|
||||||
|
coords = geocoding_procs[opts.geocoder](config, my_address)
|
||||||
|
if coords:
|
||||||
|
print(f"Coordinates: Latitude {coords[0]}, longitude {coords[1]}")
|
||||||
|
else:
|
||||||
|
print("Location was not found.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
Loading…
x
Reference in New Issue
Block a user