Compare commits

..

No commits in common. "42ee45acc296e496e69e4f9b3350429c99838cfa" and "cda2195419d7e28b5dc6336a11b05239040cfb5a" have entirely different histories.

8 changed files with 1 additions and 499 deletions

2
.gitignore vendored
View File

@ -1,2 +0,0 @@
geoapi.ini
.idea/

View File

@ -1,21 +0,0 @@
### 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.

View File

@ -1,22 +1,3 @@
# distance-calc # distance-calc
A Python-based distance calculator to help determine how far new apartment complexes are from the office. 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.

View File

@ -1,9 +0,0 @@
<?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>

View File

@ -1,12 +0,0 @@
[google]
apikey=<<API KEY HERE>>
[mapquest]
apikey=<<CONSUMER KEY HERE>>
secret=<<CONSUMER SECRET HERE>>
[geocodio]
apikey=<<API KEY HERE>>
[locationiq]
token=<<TOKEN HERE>>

View File

@ -1,161 +0,0 @@
# 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:]))

View File

@ -1,140 +0,0 @@
# 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:]))

View File

@ -1,134 +0,0 @@
# 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:]))