DebasishDhal99 commited on
Commit
f88eda5
·
1 Parent(s): 2258af3

Expand max exploration radius to 50km or above

Browse files

- Made hexagonal arrangement of circles to cover a larger circular area of exploration
- Fetch responses from wiki api for all of them, process the response and pass them to frontend.
- Make frontend Exploration Radius dynamic.

Files changed (3) hide show
  1. backend/utils.py +43 -0
  2. frontend/src/components/Map.js +7 -3
  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) {
@@ -245,7 +247,9 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
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
@@ -985,7 +989,7 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
985
  <input
986
  type="range"
987
  min="1"
988
- max="10"
989
  value={explorationRadius}
990
  onChange={(e) => setExplorationRadius(parseInt(e.target.value))}
991
  style={{ width: '100%' }}
@@ -1004,7 +1008,7 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
1004
  value={explorationRadius}
1005
  onChange={(e) => {
1006
  const value = parseInt(e.target.value);
1007
- if (value >= 1 && value <= 10) {
1008
  setExplorationRadius(value);
1009
  }
1010
  }}
 
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) {
 
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
 
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%' }}
 
1008
  value={explorationRadius}
1009
  onChange={(e) => {
1010
  const value = parseInt(e.target.value);
1011
+ if (value >= 1 && value <= maxExplorationLimit) {
1012
  setExplorationRadius(value);
1013
  }
1014
  }}
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