DebasishDhal commited on
Commit
18474d2
·
unverified ·
2 Parent(s): 21ac8b9 f86d829

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

Browse files

- Disable autoZooming if no nearby pages are found
- Returned randomized results if #available nearby pages > #required pages
- Disable re-autoZooming if search parameters (radius, limit) are chaged
- Add a slider to control duration it takes to zoom in on a nearby location cluster

Files changed (3) hide show
  1. backend/utils.py +28 -2
  2. frontend/src/components/Map.js +40 -11
  3. main.py +22 -17
backend/utils.py CHANGED
@@ -1,4 +1,6 @@
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.
@@ -8,7 +10,7 @@ def generate_circle_centers(center_lat, center_lon, radius_km, small_radius_km=1
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),...]
@@ -40,4 +42,28 @@ def generate_circle_centers(center_lat, center_lon, radius_km, small_radius_km=1
40
  lon = center_lon + delta_lon
41
  results.append((lat, lon))
42
 
43
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import math
2
+ import httpx
3
+
4
  def generate_circle_centers(center_lat, center_lon, radius_km, small_radius_km=10):
5
  """
6
  Generate a list of centers of small circles (radius=10km) needed to cover a larger circle.
 
10
  - center_lat: Latitude of the center of the larger circle
11
  - center_lon: Longitude of the center of the larger circle
12
  - radius_km: Radius of the larger circle in kilometers
13
+ - small_radius_km: Radius of the smaller circles in kilometers (default 10km, more than that wiki api cannot accomodate)
14
 
15
  Output:
16
  - A list of tuples, each containing the latitude and longitude of a small circle's center. [(lat1, lon1), (lat2, lon2),...]
 
42
  lon = center_lon + delta_lon
43
  results.append((lat, lon))
44
 
45
+ return results
46
+
47
+
48
+
49
+ async def fetch_url(client: httpx.AsyncClient, url: str):
50
+ """
51
+ Fetch a URL asynchronously using httpx and return the response status and data.
52
+ This function is asynchrounously used to fetch multiple URLs in parallel when search radius > 10km.
53
+ Input:
54
+ - client: httpx.AsyncClient instance
55
+ - url: URL to fetch
56
+ Output:
57
+ - A dictionary with the URL, status code, and data if available.
58
+ - Data includes the JSON format of wiki geosearch response.
59
+ If an error occurs, return a dictionary with the URL and the error message.
60
+ """
61
+ try:
62
+ response = await client.get(url, timeout=10.0)
63
+ return {
64
+ "url": url,
65
+ "status": response.status_code,
66
+ "data": response.json() if response.status_code == 200 else None,
67
+ }
68
+ except Exception as e:
69
+ return {"url": url, "error": str(e)}
frontend/src/components/Map.js CHANGED
@@ -90,9 +90,10 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
90
  const [explorationMarkers, setExplorationMarkers] = useState([]);
91
  const [explorationSidebarOpen, setExplorationSidebarOpen] = useState(false);
92
  const [shouldZoom, setShouldZoom] = useState(false);
 
93
 
94
  // Using CenterMap component to handle centering (for summary/full apis) and for zooming (for wiki/nearby api)
95
- const CenterMap = ({ position, coordinates, shouldZoom }) => {
96
  const map = useMap();
97
  useEffect(() => {
98
  if (position && Array.isArray(position) && position.length === 2) {
@@ -102,15 +103,24 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
102
 
103
 
104
  useEffect(() => {
105
- if (coordinates && Array.isArray(coordinates) && coordinates.length > 0 && shouldZoom) {
106
  const bounds = L.latLngBounds(coordinates);
107
- map.flyToBounds(bounds, {
108
- padding: [50, 50],
109
- maxZoom: 16,
110
- duration: 3
111
- });
 
 
 
 
 
 
 
 
 
112
  }
113
- }, [coordinates, map, shouldZoom]);
114
 
115
  return null;
116
  };
@@ -247,9 +257,7 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
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
@@ -616,6 +624,8 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
616
  position={markerPosition}
617
  coordinates={explorationMarkers.map((marker) => marker.position)}
618
  shouldZoom={shouldZoom}
 
 
619
  />
620
  {baseLayer === "satellite" && (
621
  <>
@@ -1042,6 +1052,25 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
1042
  </div>
1043
  </div>
1044
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1045
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1046
  <input
1047
  type="checkbox"
 
90
  const [explorationMarkers, setExplorationMarkers] = useState([]);
91
  const [explorationSidebarOpen, setExplorationSidebarOpen] = useState(false);
92
  const [shouldZoom, setShouldZoom] = useState(false);
93
+ const [zoomDelaySeconds, setZoomDelaySeconds] = useState(3); // Default zoom delay in seconds
94
 
95
  // Using CenterMap component to handle centering (for summary/full apis) and for zooming (for wiki/nearby api)
96
+ const CenterMap = ({ position, coordinates, shouldZoom, setShouldZoom, zoomDelaySeconds }) => {
97
  const map = useMap();
98
  useEffect(() => {
99
  if (position && Array.isArray(position) && position.length === 2) {
 
103
 
104
 
105
  useEffect(() => {
106
+ if (coordinates && Array.isArray(coordinates) && coordinates.length > 1 && shouldZoom) {
107
  const bounds = L.latLngBounds(coordinates);
108
+ console.log("Delay:", zoomDelaySeconds);
109
+ if (zoomDelaySeconds > 0) {
110
+ map.flyToBounds(bounds, {
111
+ padding: [50, 50],
112
+ maxZoom: 16,
113
+ duration: zoomDelaySeconds
114
+ });
115
+ } else {
116
+ map.fitBounds(bounds, {
117
+ padding: [50, 50],
118
+ maxZoom: 16
119
+ });
120
+ }
121
+ setShouldZoom(false);
122
  }
123
+ }, [coordinates, map, shouldZoom, setShouldZoom, zoomDelaySeconds]);
124
 
125
  return null;
126
  };
 
257
 
258
  if (res.ok) {
259
  const data = await res.json();
260
+ const markers = data.pages.map(page => ({
 
 
261
  position: [page.lat, page.lon],
262
  title: page.title,
263
  distance: page.dist
 
624
  position={markerPosition}
625
  coordinates={explorationMarkers.map((marker) => marker.position)}
626
  shouldZoom={shouldZoom}
627
+ setShouldZoom={setShouldZoom}
628
+ zoomDelaySeconds={zoomDelaySeconds}
629
  />
630
  {baseLayer === "satellite" && (
631
  <>
 
1052
  </div>
1053
  </div>
1054
 
1055
+
1056
+ <div>
1057
+ <label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
1058
+ Zoom Delay (seconds):
1059
+ </label>
1060
+ <input
1061
+ type="range"
1062
+ min="0"
1063
+ max="5"
1064
+ step="1"
1065
+ value={zoomDelaySeconds}
1066
+ onChange={(e) => setZoomDelaySeconds(parseInt(e.target.value))}
1067
+ style={{ width: '100%' }}
1068
+ />
1069
+ <div style={{ textAlign: 'center', marginTop: 4 }}>
1070
+ {zoomDelaySeconds} sec. zoom duration
1071
+ </div>
1072
+ </div>
1073
+
1074
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1075
  <input
1076
  type="checkbox"
main.py CHANGED
@@ -8,7 +8,8 @@ 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
 
@@ -161,7 +162,6 @@ def get_geodistance(payload: Geodistance):
161
  content={"error": str(e)},
162
  status_code=500
163
  )
164
-
165
  return JSONResponse(
166
  content={
167
  "distance": distance,
@@ -175,16 +175,7 @@ def get_geodistance(payload: Geodistance):
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):
@@ -217,7 +208,9 @@ async def get_nearby_wiki_pages(payload: NearbyWikiPage):
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}"
@@ -236,6 +229,9 @@ async def get_nearby_wiki_pages(payload: NearbyWikiPage):
236
 
237
  pages = data.get("query", {}).get("geosearch", [])
238
 
 
 
 
239
  return JSONResponse(
240
  content={
241
  "pages": pages,
@@ -248,11 +244,13 @@ async def get_nearby_wiki_pages(payload: NearbyWikiPage):
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:
@@ -262,19 +260,26 @@ async def get_nearby_wiki_pages(payload: NearbyWikiPage):
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,
 
8
  from cachetools import TTLCache
9
  import os
10
  from dotenv import load_dotenv
11
+ from random import sample
12
+ from backend.utils import generate_circle_centers, fetch_url
13
 
14
  load_dotenv()
15
 
 
162
  content={"error": str(e)},
163
  status_code=500
164
  )
 
165
  return JSONResponse(
166
  content={
167
  "distance": distance,
 
175
  )
176
 
177
 
178
+
 
 
 
 
 
 
 
 
 
179
 
180
  @app.post("/wiki/nearby")
181
  async def get_nearby_wiki_pages(payload: NearbyWikiPage):
 
208
  radius = payload.radius
209
  limit = payload.limit
210
 
211
+ wiki_geosearch_radius_limit_meters = 10000 # Wikipedia API limit for geosearch radius in meters
212
+
213
+ if radius <= wiki_geosearch_radius_limit_meters:
214
  url = ("https://en.wikipedia.org/w/api.php"+"?action=query"
215
  "&list=geosearch"
216
  f"&gscoord={lat_center}|{lon_center}"
 
229
 
230
  pages = data.get("query", {}).get("geosearch", [])
231
 
232
+ if len(pages) > limit:
233
+ pages = sample(pages, limit)
234
+
235
  return JSONResponse(
236
  content={
237
  "pages": pages,
 
244
  content={"error": str(e)},
245
  status_code=500
246
  )
247
+
248
+ elif radius > wiki_geosearch_radius_limit_meters:
249
  all_pages = []
250
+
251
+ small_circle_centers = generate_circle_centers(lat_center, lon_center, radius / 1000, small_radius_km=10)
252
  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"
253
+ urls = [base_url.format(lat=center[0], lon=center[1], small_radius_km=wiki_geosearch_radius_limit_meters, page_limit=100) for center in small_circle_centers]
254
 
255
  print("URL Counts:", len(urls))
256
  try:
 
260
 
261
  # print(results)
262
  for result in results:
263
+
264
  for unit in result.get("data", {}).get("query", {}).get("geosearch", []):
265
+
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
  else:
272
  dist = None
273
 
274
+ if (not dist) or (dist and dist > radius):
275
+ continue
276
+
277
  unit_with_dist = {**unit, "dist": dist}
278
  all_pages.append(unit_with_dist)
279
 
280
+ if len(all_pages) > limit:
281
+ all_pages = sample(all_pages, limit)
282
+
283
  return JSONResponse(
284
  content={
285
  "pages": all_pages,