shared.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. from typing import List, Tuple, Union, Dict
  2. import collections
  3. import logging
  4. import requests
  5. # Google API key, with access to Street View Static API
  6. # this can be safely committed due to permission restriction
  7. google_api_key = "AIzaSyAqjCYR6Szph0X0H_iD6O1HenFhL9jySOo"
  8. metadata_url = "https://maps.googleapis.com/maps/api/streetview/metadata"
  9. logger = logging.getLogger(__name__)
  10. def point_has_streetview(lat, lng):
  11. """
  12. Returns True if the streetview metadata endpoint says a given point has
  13. data available, and False otherwise.
  14. This function calls the streetview metadata endpoint - there is no quota consumed.
  15. """
  16. return requests.get(metadata_url, params={
  17. "key": google_api_key,
  18. "location": f"{lat},{lng}",
  19. }).json()["status"] == "OK"
  20. class ExhaustedSourceError(Exception):
  21. pass
  22. class GeoPointSource:
  23. """
  24. Abstract base class for a source of geo points
  25. """
  26. def get_name(self) -> str:
  27. """
  28. Return a human-readable name for this point source, for debugging purposes.
  29. """
  30. raise NotImplemented("Must be implemented by subclasses")
  31. def get_points(self, n: int) -> List[Tuple[float, float]]:
  32. """
  33. Return a list of at least n valid geo points, as (latitude, longitude) pairs.
  34. In the event that the GeoPointSource cannot reasonably supply enough points,
  35. most likely due to time constraints, it should raise an ExhaustedSourceError.
  36. """
  37. raise NotImplemented("Must be implemented by subclasses")
  38. class CachedGeoPointSource(GeoPointSource):
  39. """
  40. Wrapper tool for maintaing a cache of points from a GeoPointSource to
  41. make get_points faster, at the exchange of needing to restock those
  42. points after the fact. This can be done in another thread, however, to
  43. hide this cost from the user.
  44. """
  45. def __init__(self, source: GeoPointSource, stock_target: int):
  46. self.source = source
  47. self.stock = collections.deque()
  48. self.stock_target = stock_target
  49. def get_name(self):
  50. return f"Cached({self.source.get_name()}, {self.stock_target})"
  51. def restock(self, n: Union[int, None] = None):
  52. """
  53. Restock at least n points into this source.
  54. If n is not provided, it will default to stock_target, as set during the
  55. construction of this point source.
  56. """
  57. n = n if n is not None else self.stock_target - len(self.stock)
  58. if n > 0:
  59. logger.info(f"Restocking {type(self).__name__} with {n} points")
  60. pts = self.source.get_points(n)
  61. self.stock.extend(pts)
  62. diff = n - len(pts)
  63. if diff > 0:
  64. # if implementations of source.get_points are well behaved, this will
  65. # never actually need to recurse to finish the job.
  66. self.restock(n=diff)
  67. logger.info(f"Finished restocking {type(self).__name__}")
  68. def get_points(self, n: int) -> List[Tuple[float, float]]:
  69. """
  70. Pull n points from the current stock.
  71. It is recommended to call CachedGeoPointSource.restock after this, to ensure
  72. the stock is not depleted. If possible, calling restock in another thread is
  73. recommended, as it can be a long operation depending on implementation.
  74. """
  75. if len(self.stock) >= n:
  76. pts = []
  77. for _ in range(n):
  78. pts.append(self.stock.popleft())
  79. return pts
  80. self.restock(n=n)
  81. # this is safe as long as restock does actually add enough new points.
  82. # unless this object is being rapidly drained by another thread,
  83. # this will recur at most once.
  84. return self.get_points(n=n)
  85. class GeoPointSourceGroup:
  86. """
  87. Container of multiple GeoPointSources, each with some key.
  88. """
  89. def __init__(self, sources: Dict[str, GeoPointSource], default: GeoPointSource):
  90. self.sources = sources
  91. self.default = default
  92. self.cached = [s for s in sources.values() if isinstance(s, CachedGeoPointSource)]
  93. if isinstance(default, CachedGeoPointSource):
  94. self.cached.append(default)
  95. def restock_all(self):
  96. """
  97. Restock any and all CachedGeoPointSources managed by this group.
  98. """
  99. for s in self.cached:
  100. s.restock()
  101. def get_points_from(self, n: int, key: Union[str, None] = None) -> List[Tuple[float, float]]:
  102. """
  103. Return a list of at least n valid geo points, as (latitude, longitude) pairs,
  104. for a given key. If no key is provided, or no matching GeoPointSource is found,
  105. the default GeoPointSource will be used.
  106. """
  107. return self.sources.get(key, self.default).get_points(n)