#!/usr/bin/env python3
# nearest_city.py
# 2024-08-27
# by Gernot WALZL
# Given GPS coordinates, or given an image with GPSInfo in EXIF tags,
# this script prints the closest city nearby.
# The data is loaded from local dumps of https://www.geonames.org/
#
# As an alternative, GPS coordinates can be extracted from images with exiv2:
# exiv2 --grep Exif.GPSInfo image.jpg
import argparse
import csv
import datetime
import os
import sys
import zipfile
import requests
from PIL import Image, ExifTags
from scipy.spatial import KDTree
class NearestCity:
_url_geonames_dump = "https://download.geonames.org/export/dump/"
def __init__(self):
self._filename_cities_zip = "cities1000.zip"
self._filename_cities_txt = "cities1000.txt"
self._coords = []
self._cities = []
@staticmethod
def _download(filename, url):
result = False
response = requests.get(url, timeout=10)
if response.status_code == 200:
with open(filename, mode='wb') as file:
bytes_written = file.write(response.content)
result = (len(response.content) == bytes_written)
return result
@staticmethod
def _unzip(filename_zip, filename_extract):
result = False
with zipfile.ZipFile(filename_zip, 'r') as myzip:
path_extracted = myzip.extract(filename_extract)
result = path_extracted.endswith(filename_extract)
return result
def _get_db(self):
if self._download(
self._filename_cities_zip,
self._url_geonames_dump+self._filename_cities_zip):
if self._unzip(
self._filename_cities_zip,
self._filename_cities_txt):
os.remove(self._filename_cities_zip)
return os.path.isfile(self._filename_cities_txt)
def load_cities(self, population=1000):
if population not in [500, 1000, 5000, 15000]:
return False
self._filename_cities_zip = f"cities{population}.zip"
self._filename_cities_txt = f"cities{population}.txt"
if not os.path.isfile(self._filename_cities_txt):
if not self._get_db():
return False
self._coords = []
self._cities = []
with open(self._filename_cities_txt, encoding='utf-8') as csvfile:
reader = csv.reader(csvfile, delimiter="\t")
for row in reader:
self._coords.append([float(row[4]), float(row[5])])
self._cities.append(str(row[2]))
return True
def query(self, coord_query):
_, index = KDTree(self._coords).query(coord_query)
return self._cities[index]
class ExifGPSInfo:
def __init__(self):
self._gpsinfo = None
def read_gpsinfo(self, filename):
result = 0
self._gpsinfo = None
with Image.open(filename) as img:
exif = img.getexif()
if exif:
ifd = exif.get_ifd(0x8825) # GPSInfo
if ifd:
self._gpsinfo = {}
for key, val in ifd.items():
self._gpsinfo[ExifTags.GPSTAGS[key]] = val
result = len(self._gpsinfo)
return result
def get_latitude_longitude(self):
gps_lat = self._gpsinfo['GPSLatitude']
gps_lon = self._gpsinfo['GPSLongitude']
lat = gps_lat[0] + gps_lat[1]/60.0 + gps_lat[2]/3600.0
lon = gps_lon[0] + gps_lon[1]/60.0 + gps_lon[2]/3600.0
return (lat, lon)
def get_timestamp(self):
gps_date = self._gpsinfo['GPSDateStamp']
gps_time = self._gpsinfo['GPSTimeStamp']
date = gps_date.split(':')
return datetime.datetime(
int(date[0]),
int(date[1]),
int(date[2]),
int(float(gps_time[0])),
int(float(gps_time[1])),
int(float(gps_time[2]))
)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-lat', type=float, help='latitude')
parser.add_argument('-lon', type=float, help='longitude')
parser.add_argument('-img', type=str, help='image')
parser.add_argument('-pop', default=1000, choices=[500, 1000, 5000, 15000],
type=int, help='population of city')
args = parser.parse_args()
if args.img:
exifgpsinfo = ExifGPSInfo()
if exifgpsinfo.read_gpsinfo(args.img) > 0:
coord_lat_lon = exifgpsinfo.get_latitude_longitude()
else:
sys.exit(1)
else:
coord_lat_lon = (float(args.lat), float(args.lon))
nearest_city = NearestCity()
if nearest_city.load_cities(args.pop):
print(nearest_city.query(coord_lat_lon))
else:
sys.exit(1)