shared.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. def __init__(self, partial=[]):
  22. self.partial = partial
  23. class GeoPointSource:
  24. """
  25. Abstract base class for a source of geo points
  26. """
  27. def get_name(self) -> str:
  28. """
  29. Return a human-readable name for this point source, for debugging purposes.
  30. """
  31. raise NotImplemented("Must be implemented by subclasses")
  32. def get_points(self, n: int) -> List[Tuple[float, float]]:
  33. """
  34. Return a list of at least n valid geo points, as (latitude, longitude) pairs.
  35. In the event that the GeoPointSource cannot reasonably supply enough points,
  36. most likely due to time constraints, it should raise an ExhaustedSourceError.
  37. """
  38. raise NotImplemented("Must be implemented by subclasses")
  39. class CachedGeoPointSource(GeoPointSource):
  40. """
  41. Wrapper tool for maintaing a cache of points from a GeoPointSource to
  42. make get_points faster, at the exchange of needing to restock those
  43. points after the fact. This can be done in another thread, however, to
  44. hide this cost from the user.
  45. """
  46. def __init__(self, source: GeoPointSource, stock_target: int):
  47. self.source = source
  48. self.stock = collections.deque()
  49. self.stock_target = stock_target
  50. def get_name(self):
  51. return f"Cached({self.source.get_name()}, {self.stock_target})"
  52. def restock(self, n: Union[int, None] = None):
  53. """
  54. Restock at least n points into this source.
  55. If n is not provided, it will default to stock_target, as set during the
  56. construction of this point source.
  57. """
  58. n = n if n is not None else self.stock_target - len(self.stock)
  59. if n > 0:
  60. logger.info(f"Restocking {type(self).__name__} with {n} points")
  61. try:
  62. pts = self.source.get_points(n)
  63. except ExhaustedSourceError as e:
  64. pts = e.partial # take what we can get
  65. self.stock.extend(pts)
  66. diff = n - len(pts)
  67. if diff > 0:
  68. # if implementations of source.get_points are well behaved, this will
  69. # never actually need to recurse to finish the job.
  70. self.restock(n=diff)
  71. logger.info(f"Finished restocking {type(self).__name__}")
  72. def get_points(self, n: int) -> List[Tuple[float, float]]:
  73. """
  74. Pull n points from the current stock.
  75. It is recommended to call CachedGeoPointSource.restock after this, to ensure
  76. the stock is not depleted. If possible, calling restock in another thread is
  77. recommended, as it can be a long operation depending on implementation.
  78. """
  79. if len(self.stock) >= n:
  80. pts = []
  81. for _ in range(n):
  82. pts.append(self.stock.popleft())
  83. return pts
  84. self.restock(n=n)
  85. # this is safe as long as restock does actually add enough new points.
  86. # unless this object is being rapidly drained by another thread,
  87. # this will recur at most once.
  88. return self.get_points(n=n)
  89. class GeoPointSourceGroup:
  90. """
  91. Container of multiple GeoPointSources, each with some key.
  92. """
  93. def __init__(self, sources: Dict[str, GeoPointSource], default: GeoPointSource):
  94. self.sources = sources
  95. self.default = default
  96. def restock(self, key: Union[str, None] = None):
  97. """
  98. Restock a CachedGeoPointSources managed by this group.
  99. If the targeted GeoPointSource is uncached, this method does nothing.
  100. """
  101. src = self.sources.get(key, self.default)
  102. if isinstance(src, CachedGeoPointSource):
  103. src.restock()
  104. def get_points_from(self, n: int, key: Union[str, None] = None) -> List[Tuple[float, float]]:
  105. """
  106. Return a list of at least n valid geo points, as (latitude, longitude) pairs,
  107. for a given key. If no key is provided, or no matching GeoPointSource is found,
  108. the default GeoPointSource will be used.
  109. """
  110. return self.sources.get(key, self.default).get_points(n)