urban_centers.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import math
  2. import random
  3. import csv
  4. import logging
  5. import asyncio
  6. from collections import defaultdict
  7. from .shared import point_has_streetview, ExhaustedSourceError
  8. from ..scoring import mean_earth_radius_km
  9. logger = logging.getLogger(__name__)
  10. URBAN_CENTERS = defaultdict(list)
  11. _found_countries = set()
  12. _urban_center_count = 0
  13. with open("./data/urban-centers.csv") as infile:
  14. for code, name, lat, lng in csv.reader(infile, delimiter=",", quotechar='"'):
  15. URBAN_CENTERS[code].append((name, float(lat), float(lng)))
  16. _found_countries.add(code)
  17. _urban_center_count += 1
  18. logger.info(f"Read {_urban_center_count} urban centers from {len(_found_countries)} countries.")
  19. VALID_COUNTRIES = tuple(_found_countries)
  20. async def urban_coord(country_lock, city_retries=10, point_retries=10, max_dist_km=25):
  21. """
  22. Returns (latitude, longitude) of usable coord (where google has data) that is near
  23. a known urban center. Points will be at most max_dist_km kilometers away. This function
  24. will use country_lock to determine the country from which to pull a known urban center,
  25. generate at most point_retries points around that urban center, and try at most
  26. city_retries urban centers in that country. If none of the generated points have street
  27. view data, this will return None. Otherwise, it will exit as soon as suitable point is
  28. found.
  29. This function calls the streetview metadata endpoint - there is no quota consumed.
  30. """
  31. country_lock = country_lock.lower()
  32. cities = URBAN_CENTERS[country_lock]
  33. src = random.sample(cities, k=min(city_retries, len(cities)))
  34. logger.info(f"Trying {len(src)} centers in {country_lock}")
  35. for (name, city_lat, city_lng) in src:
  36. # logic adapted from https://stackoverflow.com/a/7835325
  37. # start in a city
  38. logger.info(f"Trying at most {point_retries} points around {name}")
  39. city_lat_rad = math.radians(city_lat)
  40. sin_lat = math.sin(city_lat_rad)
  41. cos_lat = math.cos(city_lat_rad)
  42. city_lng_rad = math.radians(city_lng)
  43. for _ in range(point_retries):
  44. # turn a random direction, and go random distance
  45. dist_km = random.random() * max_dist_km
  46. angle_rad = random.random() * 2 * math.pi
  47. d_over_radius = dist_km / mean_earth_radius_km
  48. sin_dor = math.sin(d_over_radius)
  49. cos_dor = math.cos(d_over_radius)
  50. pt_lat_rad = math.asin(sin_lat * cos_dor + cos_lat * sin_dor * math.cos(angle_rad))
  51. pt_lng_rad = city_lng_rad + math.atan2(math.sin(angle_rad) * sin_dor * cos_lat, cos_dor - sin_lat * math.sin(pt_lat_rad))
  52. pt_lat = math.degrees(pt_lat_rad)
  53. pt_lng = math.degrees(pt_lng_rad)
  54. if await point_has_streetview(pt_lat, pt_lng):
  55. logger.info("Point found!")
  56. return (country_lock, pt_lat, pt_lng)
  57. async def urban_coord_unlocked(country_retries=30, city_retries=10, point_retries=10, max_dist_km=25):
  58. """
  59. The same behavior as urban_coord, but for a randomly chosen country. Will attempt at most
  60. country_retries countries, calling urban_coord for each, with the provided settings.
  61. Will never return None, instead opting to raise ExhaustedSourceError on failure.
  62. This function calls the streetview metadata endpoint - there is no quota consumed.
  63. """
  64. countries = random.sample(URBAN_CENTERS.keys(), k=min(country_retries, len(URBAN_CENTERS)))
  65. for country in countries:
  66. logger.info(f"Selecting urban centers from {country}")
  67. pt = await urban_coord(country, city_retries=city_retries, point_retries=point_retries, max_dist_km=max_dist_km)
  68. if pt is not None:
  69. return pt
  70. else:
  71. raise ExhaustedSourceError
  72. async def urban_coord_ensured(country_lock, max_attempts=30, city_retries=10, point_retries=10, max_dist_km=25):
  73. """
  74. The same behavior as urban_coord, but will make at most max_attempts cycles through the
  75. behavior of urban_coord, trying to ensure a valid point is found.
  76. Will never return None, instead opting to raise ExhaustedSourceError on failure.
  77. This function calls the streetview metadata endpoint - there is no quota consumed.
  78. """
  79. for i in range(max_attempts):
  80. logger.info(f"Attempt #{i + 1} to select urban centers from {country_lock}")
  81. pt = await urban_coord(country_lock, city_retries=city_retries, point_retries=point_retries, max_dist_km=max_dist_km)
  82. if pt is not None:
  83. return pt
  84. else:
  85. raise ExhaustedSourceError