lib.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import json
  2. import math
  3. import random
  4. import requests
  5. import haversine
  6. # Google API key, with access to Street View Static API
  7. google_api_key = "AIzaSyAqjCYR6Szph0X0H_iD6O1HenFhL9jySOo"
  8. metadata_url = "https://maps.googleapis.com/maps/api/streetview/metadata"
  9. mapcrunch_url = "http://www.mapcrunch.com/_r/"
  10. rsv_url = "https://randomstreetview.com/data"
  11. urban_centers_usa = []
  12. urban_centers_non_usa = []
  13. with open("./urban-centers-usa.csv") as infile:
  14. for line in infile:
  15. lat, lng = line.split(",")
  16. urban_centers_usa.append((float(lat.strip()), float(lng.strip())))
  17. with open("./urban-centers-non-usa.csv") as infile:
  18. for line in infile:
  19. lat, lng = line.split(",")
  20. urban_centers_non_usa.append((float(lat.strip()), float(lng.strip())))
  21. urban_usa_chance = 0.1
  22. def point_has_streetview(lat, lng):
  23. """
  24. Returns True if the streetview metadata endpoint says a given point has
  25. data available, and False otherwise.
  26. This function calls the streetview metadata endpoint - there is no quota consumed.
  27. """
  28. params = {
  29. "key": google_api_key,
  30. "location": f"{lat},{lng}",
  31. }
  32. js = requests.get(metadata_url, params=params).json()
  33. return js["status"] == "OK"
  34. def generate_coord(max_retries=100, only_america=False):
  35. """
  36. Returns (latitude, longitude) of usable coord (where google has data).
  37. This function will attempt at most max_retries calls to map crunch to fetch
  38. candidate points, and will exit as soon as a suitable candidate is found.
  39. If no suitable candidate is found in this allotted number of retries, None is
  40. returned.
  41. This function calls the streetview metadata endpoint - there is no quota consumed.
  42. """
  43. mc_url = mapcrunch_url + ("?c=21" if only_america else "")
  44. for _ in range(max_retries):
  45. points_res = requests.get(mc_url).text
  46. points_js = json.loads(points_res.strip("while(1); "))
  47. if "c=" not in mc_url:
  48. mc_url += f"?c={points_js['country']}" # lock to the first country randomed
  49. for lat, lng in points_js["points"]:
  50. if point_has_streetview(lat, lng):
  51. return (lat, lng)
  52. def call_random_street_view(only_america=False):
  53. """
  54. Returns an array of (some number of) tuples, each being (latitude, longitude).
  55. All points will be valid streetview coordinates. There is no guarantee as to the
  56. length of this array (it may be empty), but it will never be None.
  57. This function calls the streetview metadata endpoint - there is no quota consumed.
  58. """
  59. rsv_js = requests.post(rsv_url, data={"country": "us" if only_america else "all"}).json()
  60. if not rsv_js["success"]:
  61. return []
  62. return [
  63. (point["lat"], point["lng"])
  64. for point in rsv_js["locations"]
  65. if point_has_streetview(point["lat"], point["lng"])
  66. ]
  67. def random_street_view_generator(only_america=False):
  68. """
  69. Returns a generator which will lazily use call_random_street_view to generate new
  70. street view points.
  71. The returned generator calls the streetview metadata endpoint - there is no quota consumed.
  72. """
  73. points = []
  74. while True:
  75. if len(points) == 0:
  76. points = call_random_street_view(only_america=only_america)
  77. else:
  78. yield points.pop()
  79. def urban_coord(max_retries=10, retries_per_point=30, max_dist_km=25, only_america=False):
  80. """
  81. Returns (latitude, longitude) of usable coord (where google has data) that is near
  82. a known urban center. Points will be at most max_dist_km kilometers away. This function will
  83. generate at most retries_per_point points around an urban center, and will try at most
  84. max_retries urban centers. If none of the generated points have street view data,
  85. this will return None. Otherwise, it will exit as soon as suitable point is found.
  86. This function calls the streetview metadata endpoint - there is no quota consumed.
  87. """
  88. src = urban_centers_usa if only_america or random.random() <= urban_usa_chance else urban_centers_non_usa
  89. for _ in range(max_retries):
  90. # logic adapted from https://stackoverflow.com/a/7835325
  91. # start in a city
  92. (city_lat, city_lng) = random.choice(src)
  93. city_lat_rad = math.radians(city_lat)
  94. sin_lat = math.sin(city_lat_rad)
  95. cos_lat = math.cos(city_lat_rad)
  96. city_lng_rad = math.radians(city_lng)
  97. for _ in range(retries_per_point):
  98. # turn a random direction, and go random distance
  99. dist_km = random.random() * max_dist_km
  100. angle_rad = random.random() * 2 * math.pi
  101. d_over_radius = dist_km / mean_earth_radius_km
  102. sin_dor = math.sin(d_over_radius)
  103. cos_dor = math.cos(d_over_radius)
  104. pt_lat_rad = math.asin(sin_lat * cos_dor + cos_lat * sin_dor * math.cos(angle_rad))
  105. 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))
  106. pt_lat = math.degrees(pt_lat_rad)
  107. pt_lng = math.degrees(pt_lng_rad)
  108. if point_has_streetview(pt_lat, pt_lng):
  109. return (pt_lat, pt_lng)
  110. mean_earth_radius_km = (6378 + 6357) / 2
  111. # if you're more than 1/4 of the Earth's circumfrence away, you get 0
  112. max_dist_km = (math.pi * mean_earth_radius_km) / 2 # this is about 10,000 km
  113. # if you're within 1/16 of the Earth's circumfrence away, you get at least 1000 points
  114. quarter_of_max_km = max_dist_km / 4 # this is about 2,500 km
  115. # https://www.wolframalpha.com/input/?i=sqrt%28%28%28land+mass+of+earth%29+%2F+7%29%29+%2F+pi%29+in+kilometers
  116. # this is the average "radius" of a continent
  117. # within this radius, you get at least 2000 points
  118. avg_continental_rad_km = 1468.0
  119. # somewhat arbitrarily, if you're within 1000 km, you get at least 3000 points
  120. one_thousand = 1000.0
  121. # https://www.wolframalpha.com/input/?i=sqrt%28%28%28land+mass+of+earth%29+%2F+%28number+of+countries+on+earth%29%29+%2F+pi%29+in+kilometers
  122. # this is the average "radius" of a country
  123. # within this radius, you get at least 4000 points
  124. avg_country_rad_km = 479.7
  125. # if you're within 150m, you get a perfect score of 5000
  126. min_dist_km = 0.15
  127. def score_within(raw_dist, min_dist, max_dist):
  128. """
  129. Gives a score between 0 and 1000, with 1000 for the min_dist and 0 for the max_dist
  130. """
  131. # scale the distance down to [0.0, 1.0], then multiply it by 2 for easing
  132. pd2 = 2 * (raw_dist - min_dist) / (max_dist - min_dist)
  133. # perform a quadratic ease-in-out on pd2
  134. r = (pd2 ** 2) / 2 if pd2 < 1 else 1 - (((2 - pd2) ** 2) / 2)
  135. # use this to ease between 1000 and 0
  136. return int(1000 * (1 - r))
  137. def score(target, guess):
  138. """
  139. Takes in two (latitude, longitude) pairs and produces an int score.
  140. Score is in the (inclusive) range [0, 5000]
  141. Higher scores are closer.
  142. Returns (score, distance in km)
  143. """
  144. dist_km = haversine.haversine(target, guess)
  145. if dist_km <= min_dist_km:
  146. point_score = 5000
  147. elif dist_km <= avg_country_rad_km:
  148. point_score = 4000 + score_within(dist_km, min_dist_km, avg_country_rad_km)
  149. elif dist_km <= one_thousand:
  150. point_score = 3000 + score_within(dist_km, avg_country_rad_km, one_thousand)
  151. elif dist_km <= avg_continental_rad_km:
  152. point_score = 2000 + score_within(dist_km, one_thousand, avg_continental_rad_km)
  153. elif dist_km <= quarter_of_max_km:
  154. point_score = 1000 + score_within(dist_km, avg_continental_rad_km, quarter_of_max_km)
  155. elif dist_km <= max_dist_km:
  156. point_score = score_within(dist_km, quarter_of_max_km, max_dist_km)
  157. else: # dist_km > max_dist_km
  158. point_score = 0
  159. return point_score, dist_km