#!/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)