DebasishDhal commited on
Commit
21ac8b9
·
unverified ·
2 Parent(s): 9f476fb 024db11

Merge pull request #9 from DebasishDhal/feat/wiki-geosearch

Browse files

- Wikipedia api can handle search area of radius <= 1 km
- Used multiple hexagonally arranged 10km circles to fill the larger circle, fetch wiki responses for all of them and use in frontend
- Made Search Radius max limit (both slider and keyboard dynamic)
- Enabled nearby mode map exploration possible with exploration sidebar closed

Render the html content below to see hexagonal circular packing of a 50km circle centered at Bangalore, with 10km circles.

Disadvantage - Assumes spherical earth. And it misses out some points in the gaps.
```
<!DOCTYPE html>
<html>
<head>

<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/leaflet.awesome.rotate.min.css"/>

<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
#map_0d8b86cbc8c3d9e67b61e4080646ece2 {
position: relative;
width: 100.0%;
height: 100.0%;
left: 0.0%;
top: 0.0%;
}
.leaflet-container { font-size: 1rem; }
</style>

<style>html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>

<style>#map {
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
}
</style>

<script>
L_NO_TOUCH = false;
L_DISABLE_3D = false;
</script>


</head>
<body>


<div class="folium-map" id="map_0d8b86cbc8c3d9e67b61e4080646ece2" ></div>

</body>
<script>


var map_0d8b86cbc8c3d9e67b61e4080646ece2 = L.map(
"map_0d8b86cbc8c3d9e67b61e4080646ece2",
{
center: [12.9716, 77.5946],
crs: L.CRS.EPSG3857,
...{
"zoom": 10,
"zoomControl": true,
"preferCanvas": false,
}

}
);





var tile_layer_5539c953595e8644a534907b01c9b9dd = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
"minZoom": 0,
"maxZoom": 19,
"maxNativeZoom": 19,
"noWrap": false,
"attribution": "\u0026copy; \u003ca href=\"https://www.openstreetmap.org/copyright\"\u003eOpenStreetMap\u003c/a\u003e contributors",
"subdomains": "abc",
"detectRetina": false,
"tms": false,
"opacity": 1,
}

);


tile_layer_5539c953595e8644a534907b01c9b9dd.addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_f1c8f9ae3fe1a67cf46e8338270315f5 = L.circle(
[12.9716, 77.5946],
{"bubblingMouseEvents": true, "color": "blue", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "blue", "fillOpacity": 0.1, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 50000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var popup_52aa2bb8a1b914eaf507be92fd68695e = L.popup({
"maxWidth": "100%",
});



var html_7447c4f769cea93e6ae52ce15cb34a8b = $(`<div id="html_7447c4f769cea93e6ae52ce15cb34a8b" style="width: 100.0%; height: 100.0%;">Target Area</div>`)[0];
popup_52aa2bb8a1b914eaf507be92fd68695e.setContent(html_7447c4f769cea93e6ae52ce15cb34a8b);



circle_f1c8f9ae3fe1a67cf46e8338270315f5.bindPopup(popup_52aa2bb8a1b914eaf507be92fd68695e)
;




var circle_52a6246c4981f1366d92201def7f9cbe = L.circle(
[12.504298785861298, 77.31773841119686],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_e78eb9cfb4635c517d3e43cf7bd7233a = L.circle(
[12.504298785861298, 77.50231280373228],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_e4ec44b83736eca64c05417624a37ed8 = L.circle(
[12.504298785861298, 77.68688719626772],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_caf16f07b56c6f6fd3dc92a0957efc0d = L.circle(
[12.504298785861298, 77.87146158880314],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_4b75e906a045a46964343af78f5d0829 = L.circle(
[12.660065857240864, 77.22545121492914],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_dc1305c877ce55a877e1577d4eb6c1ec = L.circle(
[12.660065857240864, 77.41002560746458],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_c6273fc19f0d6c7c53e11797678a26d7 = L.circle(
[12.660065857240864, 77.5946],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_72c14238321f382b75d7e7252e099771 = L.circle(
[12.660065857240864, 77.77917439253542],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_87d8d6d7da9e3dadc2f449b41d2c3283 = L.circle(
[12.660065857240864, 77.96374878507086],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_73b1316540dab26935b6628bc29e8373 = L.circle(
[12.815832928620432, 77.13316401866143],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_abbf0ccd94dc3585f29e6097cf31816c = L.circle(
[12.815832928620432, 77.31773841119686],
{"bubblingMouseEvents": true, "color": "red", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "red", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 10000, "stroke": true, "weight": 3}
).addTo(map_0d8b86cbc8c3d9e67b61e4080646ece2);


var circle_123feae420dca7db7f16836de

Files changed (3) hide show
  1. backend/utils.py +43 -0
  2. frontend/src/components/Map.js +17 -12
  3. main.py +82 -29
backend/utils.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ def generate_circle_centers(center_lat, center_lon, radius_km, small_radius_km=10):
3
+ """
4
+ Generate a list of centers of small circles (radius=10km) needed to cover a larger circle.
5
+ Circles are arranged in hexagonal pattern to minimize # of small circles.
6
+ No overlapping among small circles, but some small circles may be outside the larger circle.
7
+ Input:
8
+ - center_lat: Latitude of the center of the larger circle
9
+ - center_lon: Longitude of the center of the larger circle
10
+ - radius_km: Radius of the larger circle in kilometers
11
+ - small_radius_km: Radius of the smaller circles in kilometers (default 15km)
12
+
13
+ Output:
14
+ - A list of tuples, each containing the latitude and longitude of a small circle's center. [(lat1, lon1), (lat2, lon2),...]
15
+ """
16
+ R = 6371 # Earth radius
17
+
18
+ dx = 2 * small_radius_km
19
+ dy = math.sqrt(3) * small_radius_km
20
+ max_dist = radius_km + small_radius_km
21
+
22
+ results = []
23
+ lat_rad = math.radians(center_lat)
24
+ n_y = int(max_dist // dy) + 2
25
+
26
+ for row in range(-n_y, n_y + 1):
27
+ y = row * dy
28
+ offset = 0 if row % 2 == 0 else dx / 2
29
+ n_x = int((max_dist + dx) // dx) + 2
30
+
31
+ for col in range(-n_x, n_x + 1):
32
+ x = col * dx + offset
33
+ distance = math.sqrt(x ** 2 + y ** 2)
34
+
35
+ if distance <= max_dist:
36
+ delta_lat = (y / R) * (180 / math.pi)
37
+ delta_lon = (x / (R * math.cos(lat_rad))) * (180 / math.pi)
38
+
39
+ lat = center_lat + delta_lat
40
+ lon = center_lon + delta_lon
41
+ results.append((lat, lon))
42
+
43
+ return results
frontend/src/components/Map.js CHANGED
@@ -26,6 +26,8 @@ L.Icon.Default.mergeOptions({
26
  // Add scale
27
  // L.control.scale().addTo(window.Map);
28
 
 
 
29
  const ClickHandler = ({ onClick }) => {
30
  useMapEvents({
31
  click(e) {
@@ -83,7 +85,7 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
83
  const [countryBorders, setCountryBorders] = useState(null);
84
 
85
  const [explorationMode, setExplorationMode] = useState(false);
86
- const [explorationRadius, setExplorationRadius] = useState(10000);
87
  const [explorationLimit, setExplorationLimit] = useState(10);
88
  const [explorationMarkers, setExplorationMarkers] = useState([]);
89
  const [explorationSidebarOpen, setExplorationSidebarOpen] = useState(false);
@@ -238,14 +240,16 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
238
  body: JSON.stringify({
239
  lat: lat,
240
  lon: lon,
241
- radius: explorationRadius,
242
  limit: explorationLimit
243
  }),
244
  });
245
 
246
  if (res.ok) {
247
  const data = await res.json();
248
- const markers = data.pages.map(page => ({
 
 
249
  position: [page.lat, page.lon],
250
  title: page.title,
251
  distance: page.dist
@@ -963,8 +967,9 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
963
  <button
964
  onClick={() => {
965
  setExplorationSidebarOpen(false);
966
- setExplorationMode(false);
967
- setExplorationMarkers([]);
 
968
  }}
969
  style={{
970
  background: 'none',
@@ -979,12 +984,12 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
979
 
980
  <div>
981
  <label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
982
- Search Radius (meters):
983
  </label>
984
  <input
985
  type="range"
986
- min="10"
987
- max="10000"
988
  value={explorationRadius}
989
  onChange={(e) => setExplorationRadius(parseInt(e.target.value))}
990
  style={{ width: '100%' }}
@@ -998,12 +1003,12 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
998
  }}>
999
  <input
1000
  type="number"
1001
- min="10"
1002
- max="10000"
1003
  value={explorationRadius}
1004
  onChange={(e) => {
1005
  const value = parseInt(e.target.value);
1006
- if (value >= 10 && value <= 10000) {
1007
  setExplorationRadius(value);
1008
  }
1009
  }}
@@ -1015,7 +1020,7 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
1015
  textAlign: 'center'
1016
  }}
1017
  />
1018
- <span>m</span>
1019
  </div>
1020
  </div>
1021
 
 
26
  // Add scale
27
  // L.control.scale().addTo(window.Map);
28
 
29
+ const maxExplorationLimit = 50; // kilometers, the maximum amount user can select to explore.
30
+
31
  const ClickHandler = ({ onClick }) => {
32
  useMapEvents({
33
  click(e) {
 
85
  const [countryBorders, setCountryBorders] = useState(null);
86
 
87
  const [explorationMode, setExplorationMode] = useState(false);
88
+ const [explorationRadius, setExplorationRadius] = useState(10);
89
  const [explorationLimit, setExplorationLimit] = useState(10);
90
  const [explorationMarkers, setExplorationMarkers] = useState([]);
91
  const [explorationSidebarOpen, setExplorationSidebarOpen] = useState(false);
 
240
  body: JSON.stringify({
241
  lat: lat,
242
  lon: lon,
243
+ radius: explorationRadius*1000,
244
  limit: explorationLimit
245
  }),
246
  });
247
 
248
  if (res.ok) {
249
  const data = await res.json();
250
+ const markers = data.pages.filter(
251
+ page => typeof page.dist === "number" && page.dist <= explorationRadius * 1000
252
+ ).map(page => ({
253
  position: [page.lat, page.lon],
254
  title: page.title,
255
  distance: page.dist
 
967
  <button
968
  onClick={() => {
969
  setExplorationSidebarOpen(false);
970
+ // setExplorationMode(false); // even with exp. sidebar closed, you can do exploration normally.
971
+ // setExplorationMarkers([]);
972
+ setShouldZoom(false); // If this line is removed, it map automatically zooms after re-opening/closing the exp. sidebar
973
  }}
974
  style={{
975
  background: 'none',
 
984
 
985
  <div>
986
  <label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
987
+ Search Radius (km):
988
  </label>
989
  <input
990
  type="range"
991
+ min="1"
992
+ max={maxExplorationLimit}
993
  value={explorationRadius}
994
  onChange={(e) => setExplorationRadius(parseInt(e.target.value))}
995
  style={{ width: '100%' }}
 
1003
  }}>
1004
  <input
1005
  type="number"
1006
+ min="1"
1007
+ max={maxExplorationLimit}
1008
  value={explorationRadius}
1009
  onChange={(e) => {
1010
  const value = parseInt(e.target.value);
1011
+ if (value >= 1 && value <= maxExplorationLimit) {
1012
  setExplorationRadius(value);
1013
  }
1014
  }}
 
1020
  textAlign: 'center'
1021
  }}
1022
  />
1023
+ <span>km</span>
1024
  </div>
1025
  </div>
1026
 
main.py CHANGED
@@ -2,12 +2,13 @@ from fastapi import FastAPI, BackgroundTasks
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import JSONResponse
4
  from pydantic import BaseModel, Field
5
- import requests
6
  from geopy.geocoders import Nominatim
7
  import geopy.distance
8
  from cachetools import TTLCache
9
  import os
10
  from dotenv import load_dotenv
 
11
 
12
  load_dotenv()
13
 
@@ -25,7 +26,7 @@ class Geodistance(BaseModel):
25
  class NearbyWikiPage(BaseModel):
26
  lat: float = Field(default=54.163337, ge=-90, le=90)
27
  lon: float = Field(default=37.561109, ge=-180, le=180)
28
- radius: int = Field(default=1000, ge=10, le=10000,description="Distance in meters from the reference point")
29
  limit: int = Field(10, ge=1, description="Number of pages to return")
30
 
31
  app.add_middleware(
@@ -173,6 +174,18 @@ def get_geodistance(payload: Geodistance):
173
  status_code=200
174
  )
175
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  @app.post("/wiki/nearby")
177
  async def get_nearby_wiki_pages(payload: NearbyWikiPage):
178
  """
@@ -198,42 +211,82 @@ async def get_nearby_wiki_pages(payload: NearbyWikiPage):
198
  ],
199
  "count": 10 #Total no. of such pages
200
  }
 
201
  """
202
- lat, lon = payload.lat, payload.lon
203
  radius = payload.radius
204
  limit = payload.limit
205
 
206
- url = ("https://en.wikipedia.org/w/api.php"+"?action=query"
207
- "&list=geosearch"
208
- f"&gscoord={lat}|{lon}"
209
- f"&gsradius={radius}"
210
- f"&gslimit={limit}"
211
- "&format=json")
212
- print(url)
213
- try:
214
- response = requests.get(url, timeout=10)
215
- if response.status_code != 200:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  return JSONResponse(
217
- content={"error": "Failed to fetch nearby pages"},
218
  status_code=500
219
  )
220
- data = response.json()
 
 
 
 
221
 
222
- pages = data.get("query", {}).get("geosearch", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
- return JSONResponse(
225
- content={
226
- "pages": pages,
227
- "count": len(pages)
228
- },
229
- status_code=200
230
- )
231
- except Exception as e:
232
- return JSONResponse(
233
- content={"error": str(e)},
234
- status_code=500
235
- )
236
-
 
 
237
 
238
 
239
 
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import JSONResponse
4
  from pydantic import BaseModel, Field
5
+ import requests, httpx, asyncio
6
  from geopy.geocoders import Nominatim
7
  import geopy.distance
8
  from cachetools import TTLCache
9
  import os
10
  from dotenv import load_dotenv
11
+ from backend.utils import generate_circle_centers
12
 
13
  load_dotenv()
14
 
 
26
  class NearbyWikiPage(BaseModel):
27
  lat: float = Field(default=54.163337, ge=-90, le=90)
28
  lon: float = Field(default=37.561109, ge=-180, le=180)
29
+ radius: int = Field(default=1000, ge=10, le=100_000,description="Distance in meters from the reference point")
30
  limit: int = Field(10, ge=1, description="Number of pages to return")
31
 
32
  app.add_middleware(
 
174
  status_code=200
175
  )
176
 
177
+
178
+ async def fetch_url(client: httpx.AsyncClient, url: str):
179
+ try:
180
+ response = await client.get(url, timeout=10.0)
181
+ return {
182
+ "url": url,
183
+ "status": response.status_code,
184
+ "data": response.json() if response.status_code == 200 else None,
185
+ }
186
+ except Exception as e:
187
+ return {"url": url, "error": str(e)}
188
+
189
  @app.post("/wiki/nearby")
190
  async def get_nearby_wiki_pages(payload: NearbyWikiPage):
191
  """
 
211
  ],
212
  "count": 10 #Total no. of such pages
213
  }
214
+ Example raw respone from Wikipedia API: https://en.wikipedia.org/w/api.php?action=query&list=geosearch&gscoord=40.7128%7C-74.0060&gsradius=10000&gslimit=1&format=json
215
  """
216
+ lat_center, lon_center = payload.lat, payload.lon
217
  radius = payload.radius
218
  limit = payload.limit
219
 
220
+ if radius <= 10000:
221
+ url = ("https://en.wikipedia.org/w/api.php"+"?action=query"
222
+ "&list=geosearch"
223
+ f"&gscoord={lat_center}|{lon_center}"
224
+ f"&gsradius={radius}"
225
+ f"&gslimit={limit}"
226
+ "&format=json")
227
+ # print(url)
228
+ try:
229
+ response = requests.get(url, timeout=10)
230
+ if response.status_code != 200:
231
+ return JSONResponse(
232
+ content={"error": "Failed to fetch nearby pages"},
233
+ status_code=500
234
+ )
235
+ data = response.json()
236
+
237
+ pages = data.get("query", {}).get("geosearch", [])
238
+
239
+ return JSONResponse(
240
+ content={
241
+ "pages": pages,
242
+ "count": len(pages)
243
+ },
244
+ status_code=200
245
+ )
246
+ except Exception as e:
247
  return JSONResponse(
248
+ content={"error": str(e)},
249
  status_code=500
250
  )
251
+ elif radius > 10000:
252
+ small_circle_centers = generate_circle_centers(lat_center, lon_center, radius / 1000, small_radius_km=10)
253
+ all_pages = []
254
+ base_url = "https://en.wikipedia.org/w/api.php?action=query&list=geosearch&gscoord={lat}|{lon}&gsradius={small_radius_km}&gslimit={page_limit}&format=json"
255
+ urls = [base_url.format(lat=center[0], lon=center[1], small_radius_km=10*1000, page_limit=100) for center in small_circle_centers]
256
 
257
+ print("URL Counts:", len(urls))
258
+ try:
259
+ async with httpx.AsyncClient() as client:
260
+ tasks = [fetch_url(client, url) for url in urls]
261
+ results = await asyncio.gather(*tasks)
262
+
263
+ # print(results)
264
+ for result in results:
265
+ for unit in result.get("data", {}).get("query", {}).get("geosearch", []):
266
+ lat, lon = unit.get("lat"), unit.get("lon")
267
+ if lat is not None and lon is not None:
268
+ dist = int(geopy.distance.distance(
269
+ (lat_center, lon_center), (lat, lon)
270
+ ).m)
271
+ print(dist)
272
+ else:
273
+ dist = None
274
 
275
+ unit_with_dist = {**unit, "dist": dist}
276
+ all_pages.append(unit_with_dist)
277
+
278
+ return JSONResponse(
279
+ content={
280
+ "pages": all_pages,
281
+ "count": len(all_pages)
282
+ }
283
+ )
284
+
285
+ except Exception as e:
286
+ return JSONResponse(
287
+ content={"error": str(e)},
288
+ status_code=500
289
+ )
290
 
291
 
292