So far there's only one code snippet in the answer to this question and I couldn't get it work. I have a similar problem - I want to find every Ethiopian restaurant in the lower 48 of the USA. To do that I want to use the Google Places API "Nearby Search" feature and step across the lower 48, one 100km circle (the max search area for the API) at a time without missing any spots. I ended up using baskax's approach but instead of polygon's I used squares since they were just easier.
I wanted to make the code as polite as possible so that reviewers could more easily understand and improve or tweak it. Because of that, it's not terribly compact or fast.
from pyproj import Geod
import geopandas as gpd
from shapely.geometry import Point
from shapely.ops import nearest_points
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Rectangle
from math import sqrt
from decimal import Decimal
from types import SimpleNamespace
class RadarCoverage():
def __init__(self, map_data_4326: gpd.GeoDataFrame, radar_radius_meters):
# Setup our graph info so that we can visiualize the reesult
self.fig, self.ax1 = plt.subplots(nrows=1, ncols=1, figsize=(12, 12))
self.ax1.set_title("WGS84 Grid System")
self.ax1.set_aspect(aspect=1)
plt.tight_layout()
# Map data
self.geod = Geod(ellps="WGS84")
self.map_data = map_data_4326
self.map_data.plot(ax=self.ax1, facecolor="gray")
self.ploygons_of_areas_of_interest = [i for i in self.map_data.geometry]
# Bounding box, based on map data
raw_bounds = self.map_data.bounds
self.min_x = raw_bounds['minx'].min()
self.min_y = raw_bounds['miny'].min()
self.max_x = raw_bounds['maxx'].max()
self.max_y = raw_bounds['maxy'].max()
bounding_box_width = self.max_x-self.min_x
bounding_box_height = self.max_y-self.min_y
self.ax1.add_patch(plt.Rectangle((self.min_x, self.min_y), bounding_box_width, bounding_box_height, fill=False))
# Base radar info
self.radar_radius = Decimal(radar_radius_meters)
half_of_square_side = sqrt((self.radar_radius**2)/2)
self.square_side = 2*half_of_square_side
self.initial_longitude_of_scan = self.min_x
self.initial_latitude_of_scan = self.max_y
def step(self, starting_long, starting_lat, direction, distance_meters):
if direction == "up":
azimuth = Decimal(0)
if direction == "right":
azimuth = Decimal(90)
if direction == "down":
azimuth = Decimal(180)
if direction == "left":
azimuth = Decimal(270)
ending_long, ending_lat, back_azimuth = self.geod.fwd(lons=starting_long, lats=starting_lat, az=azimuth, dist=distance_meters)
step_polite = {"long": ending_long, "lat": ending_lat, "long_delta": abs(ending_long-starting_long), "lat_delta": abs(ending_lat-starting_lat)}
return SimpleNamespace(**step_polite)
def scan(self):
current_longitude = self.min_x
current_latitude = self.max_y
list_of_centers = []
while current_latitude > self.min_y:
while current_longitude < self.max_x:
# Step down and over to define the area of the sqaure
step_down_coord = self.step(current_longitude, current_latitude, "down", self.square_side)
step_over_coord = self.step(step_down_coord.long, step_down_coord.lat, "right", self.square_side)
# Create that square and find it's center
square = Rectangle((step_down_coord.long, step_down_coord.lat), step_over_coord.long_delta, step_down_coord.lat_delta, fill=False, color='red')
square_center = square.get_center()
shapely_square_center = Point(square_center)
# Determine if the radar ping from the center of that square would cover an area of interet
radar_touches_area_of_interest = False
for poly in self.ploygons_of_areas_of_interest:
p1, p2 = nearest_points(poly, shapely_square_center)
azimuth1, azimuth2, distance = self.geod.inv(square_center[0], square_center[1], p1.x, p1.y)
if distance <= self.radar_radius:
radar_touches_area_of_interest = True
# If it does, log the radar centerpoint, build the square tile, and build the radar coverage
if radar_touches_area_of_interest:
list_of_centers.append(square_center)
plt.plot(square_center[0], square_center[1], 'r+')
self.ax1.add_patch(square)
# Note that the radar coverage will often be elliptical in shape due to projection distotion
# To account for this we build each arm of the ellipse idenpendently
top_step = self.step(square_center[0], square_center[1], "up", self.radar_radius)
bottom_step = self.step(square_center[0], square_center[1], "down", self.radar_radius)
right_step = self.step(square_center[0], square_center[1], "right", self.radar_radius)
left_step = self.step(square_center[0], square_center[1], "left", self.radar_radius)
ellipsis_width = right_step.long_delta + left_step.long_delta
ellipsis_height = top_step.lat_delta + bottom_step.lat_delta
radar_coverage = Ellipse(square_center, ellipsis_width, ellipsis_height, fill=False, color='red')
self.ax1.add_patch(radar_coverage)
current_longitude = current_longitude + step_over_coord.long_delta
current_latitude = current_latitude - step_down_coord.lat_delta
current_longitude = self.initial_longitude_of_scan
plt.show()
return list_of_centers
if __name__ == "__main__":
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
sweden = world[world.name == "Sweden"]
radius_meters = 50000
scanner = RadarCoverage(sweden, radius_meters)
result = scanner.scan()
print(len(result))