DebasishDhal commited on
Commit
a5fc0f9
·
unverified ·
2 Parent(s): 1bfa936 0b5acb5

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

Browse files

- click on a location (with exploration mode on), a number of wikipages which are close to the clicked location will pop up
- added doc string to backend routes
- Both a small and significant merge. It has the first core functionality that I had wanted this project to have.
Next target

- Add auto zoom, i.e. after every click, a no. of markers will pop up, i want the map to zoom to an adequate level so that the markers would be properly usable (and not just visible).

Files changed (2) hide show
  1. frontend/src/components/Map.js +252 -3
  2. main.py +105 -2
frontend/src/components/Map.js CHANGED
@@ -82,6 +82,12 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
82
 
83
  const [countryBorders, setCountryBorders] = useState(null);
84
 
 
 
 
 
 
 
85
  const CenterMap = ({ position }) => {
86
  const map = useMap();
87
  useEffect(() => {
@@ -208,7 +214,46 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
208
  };
209
 
210
  const handleMapClick = useCallback(async (lat, lon) => {
211
- if (geoToolMode === "distance") {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  const updatedPoints = [...geoPoints, { lat, lon }];
213
  if (updatedPoints.length > 2) {
214
  updatedPoints.shift(); // keep only two
@@ -249,7 +294,7 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
249
  console.log("Invalid tool mode:", geoToolMode);
250
  }
251
 
252
- }, [geoToolMode, geoPoints, geoUnit, areaPoints]);
253
 
254
  useEffect(() => {
255
  if (geoPoints.length === 2) {
@@ -655,6 +700,41 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
655
  </Marker>
656
  )}
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  {/* Only show geodistance markers/polyline if sidebar is open */}
659
  {geoSidebarOpen && geoToolMode === "distance" && geoPoints.map((pt, index) => (
660
  <Marker key={`geo-${index}`}
@@ -799,7 +879,176 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
799
  </button>
800
  )}
801
 
802
- {/* Geo Sidebar */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  {geoSidebarOpen && (
804
  <div style={{
805
  position: 'fixed',
 
82
 
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);
90
+
91
  const CenterMap = ({ position }) => {
92
  const map = useMap();
93
  useEffect(() => {
 
214
  };
215
 
216
  const handleMapClick = useCallback(async (lat, lon) => {
217
+ if (explorationMode) {
218
+ // Handle exploration mode click
219
+ try {
220
+ const res = await fetch(`${BACKEND_URL}/wiki/nearby`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ lat: lat,
225
+ lon: lon,
226
+ radius: explorationRadius,
227
+ limit: explorationLimit
228
+ }),
229
+ });
230
+
231
+ if (res.ok) {
232
+ const data = await res.json();
233
+ const markers = data.pages.map(page => ({
234
+ position: [page.lat, page.lon],
235
+ title: page.title,
236
+ distance: page.dist
237
+ }));
238
+ // setExplorationMarkers(markers);
239
+ // Now adding the main clicked point
240
+ setExplorationMarkers([
241
+ {
242
+ position: [lat, lon],
243
+ title: 'Clicked Location',
244
+ distance: 0,
245
+ isClickedPoint: true
246
+ },
247
+ ...markers
248
+ ]);
249
+ console.log(`Found ${markers.length} nearby pages`);
250
+ } else {
251
+ console.error('Failed to fetch nearby pages');
252
+ }
253
+ } catch (err) {
254
+ console.error('Error fetching nearby pages:', err);
255
+ }
256
+ } else if (geoToolMode === "distance") {
257
  const updatedPoints = [...geoPoints, { lat, lon }];
258
  if (updatedPoints.length > 2) {
259
  updatedPoints.shift(); // keep only two
 
294
  console.log("Invalid tool mode:", geoToolMode);
295
  }
296
 
297
+ }, [explorationMode, explorationRadius, explorationLimit, geoToolMode, geoPoints, geoUnit, areaPoints]);
298
 
299
  useEffect(() => {
300
  if (geoPoints.length === 2) {
 
700
  </Marker>
701
  )}
702
 
703
+ {/* Exploration Mode Markers */}
704
+ {explorationMode && explorationMarkers.map((marker, index) => (
705
+ <Marker
706
+ key={`exploration-${index}`}
707
+ position={marker.position}
708
+ icon={marker.isClickedPoint ? new L.Icon({
709
+ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
710
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
711
+ iconSize: [25, 41],
712
+ iconAnchor: [12, 41],
713
+ popupAnchor: [1, -34],
714
+ shadowSize: [41, 41]
715
+ }) : new L.Icon({
716
+ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
717
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
718
+ iconSize: [25, 41],
719
+ iconAnchor: [12, 41],
720
+ popupAnchor: [1, -34],
721
+ shadowSize: [41, 41]
722
+ })}
723
+ >
724
+ <Popup>
725
+ <div>
726
+ <strong>{marker.title}</strong><br />
727
+ {!marker.isClickedPoint && (
728
+ <small>Distance: {marker.distance.toFixed(1)}m</small>
729
+ )}
730
+ {marker.isClickedPoint && (
731
+ <small>Pos: {marker.position[0].toFixed(4)}, {marker.position[1].toFixed(4)}</small>
732
+ )}
733
+ </div>
734
+ </Popup>
735
+ </Marker>
736
+ ))}
737
+
738
  {/* Only show geodistance markers/polyline if sidebar is open */}
739
  {geoSidebarOpen && geoToolMode === "distance" && geoPoints.map((pt, index) => (
740
  <Marker key={`geo-${index}`}
 
879
  </button>
880
  )}
881
 
882
+ {/* Exploration Mode Button */}
883
+ {!explorationSidebarOpen && !geoSidebarOpen && (
884
+ <button
885
+ onClick={() => setExplorationSidebarOpen(true)}
886
+ style={{
887
+ position: 'absolute',
888
+ top: 50, // Position below Geo Tools button
889
+ right: 12,
890
+ zIndex: 1000,
891
+ padding: '6px 12px',
892
+ backgroundColor: '#4caf50',
893
+ color: 'white',
894
+ border: 'none',
895
+ borderRadius: 4,
896
+ cursor: 'pointer'
897
+ }}
898
+ >
899
+ Exploration
900
+ </button>
901
+ )}
902
+
903
+ {/* Exploration Mode Button - when Geo Tools sidebar is open */}
904
+ {!explorationSidebarOpen && geoSidebarOpen && (
905
+ <button
906
+ onClick={() => setExplorationSidebarOpen(true)}
907
+ style={{
908
+ position: 'fixed',
909
+ top: 320, // Position below Geo Tools sidebar
910
+ right: 24,
911
+ zIndex: 2000,
912
+ padding: '6px 12px',
913
+ backgroundColor: '#4caf50',
914
+ color: 'white',
915
+ border: 'none',
916
+ borderRadius: 4,
917
+ cursor: 'pointer'
918
+ }}
919
+ >
920
+ Exploration
921
+ </button>
922
+ )}
923
+
924
+ {/* Exploration Sidebar */}
925
+ {explorationSidebarOpen && (
926
+ <div style={{
927
+ position: 'fixed',
928
+ top: geoSidebarOpen ? 320 : 24, // Position below Geo Tools sidebar if open
929
+ right: 24,
930
+ width: 280,
931
+ background: 'white',
932
+ borderRadius: 10,
933
+ boxShadow: '0 2px 12px rgba(0,0,0,0.12)',
934
+ zIndex: 2000,
935
+ padding: 20,
936
+ display: 'flex',
937
+ flexDirection: 'column',
938
+ gap: 16,
939
+ border: '1px solid #eee'
940
+ }}>
941
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
942
+ <strong>Exploration Mode</strong>
943
+ <button
944
+ onClick={() => {
945
+ setExplorationSidebarOpen(false);
946
+ setExplorationMode(false);
947
+ setExplorationMarkers([]);
948
+ }}
949
+ style={{
950
+ background: 'none',
951
+ border: 'none',
952
+ fontSize: 18,
953
+ cursor: 'pointer',
954
+ color: '#888'
955
+ }}
956
+ title="Close"
957
+ >×</button>
958
+ </div>
959
+
960
+ <div>
961
+ <label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
962
+ Search Radius (meters):
963
+ </label>
964
+ <input
965
+ type="range"
966
+ min="10"
967
+ max="10000"
968
+ step="1000"
969
+ value={explorationRadius}
970
+ onChange={(e) => setExplorationRadius(parseInt(e.target.value))}
971
+ style={{ width: '100%' }}
972
+ />
973
+ <div style={{ textAlign: 'center', marginTop: 4 }}>
974
+ {explorationRadius.toLocaleString()}m
975
+ </div>
976
+ </div>
977
+
978
+ <div>
979
+ <label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
980
+ Number of Results:
981
+ </label>
982
+ <input
983
+ type="range"
984
+ min="1"
985
+ max="50"
986
+ step="1"
987
+ value={explorationLimit}
988
+ onChange={(e) => setExplorationLimit(parseInt(e.target.value))}
989
+ style={{ width: '100%' }}
990
+ />
991
+ <div style={{ textAlign: 'center', marginTop: 4 }}>
992
+ {explorationLimit} results
993
+ </div>
994
+ </div>
995
+
996
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
997
+ <input
998
+ type="checkbox"
999
+ id="explorationMode"
1000
+ checked={explorationMode}
1001
+ onChange={(e) => setExplorationMode(e.target.checked)}
1002
+ />
1003
+ <label htmlFor="explorationMode" style={{ fontWeight: 500 }}>
1004
+ Enable Exploration Mode
1005
+ </label>
1006
+ </div>
1007
+
1008
+ {explorationMode && (
1009
+ <div style={{
1010
+ padding: '8px 12px',
1011
+ backgroundColor: '#e8f5e8',
1012
+ borderRadius: 4,
1013
+ fontSize: '14px',
1014
+ color: '#2e7d32'
1015
+ }}>
1016
+ ✓ Click anywhere on the map to find nearby Wikipedia pages
1017
+ </div>
1018
+ )}
1019
+
1020
+ {explorationMarkers.length > 0 && (
1021
+ <div style={{
1022
+ padding: '8px 12px',
1023
+ backgroundColor: '#e3f2fd',
1024
+ borderRadius: 4,
1025
+ fontSize: '14px',
1026
+ color: '#1976d2'
1027
+ }}>
1028
+ Found {explorationMarkers.length} nearby pages
1029
+ </div>
1030
+ )}
1031
+
1032
+ <button
1033
+ onClick={() => {
1034
+ setExplorationMarkers([]);
1035
+ }}
1036
+ style={{
1037
+ padding: '6px 0',
1038
+ borderRadius: 4,
1039
+ border: '1px solid #f44336',
1040
+ background: '#f44336',
1041
+ color: 'white',
1042
+ fontWeight: 500,
1043
+ cursor: 'pointer'
1044
+ }}
1045
+ >
1046
+ Clear Markers
1047
+ </button>
1048
+ </div>
1049
+ )}
1050
+
1051
+ {/* Geo Sidebar - Keep as is */}
1052
  {geoSidebarOpen && (
1053
  <div style={{
1054
  position: 'fixed',
main.py CHANGED
@@ -22,6 +22,12 @@ class Geodistance(BaseModel):
22
  lon2: float = Field(..., ge=-180, le=180)
23
  unit: str = "km"
24
 
 
 
 
 
 
 
25
  app.add_middleware(
26
  CORSMiddleware,
27
  allow_origins=["*"], # Replace with your frontend domain in prod
@@ -40,6 +46,12 @@ def health_check():
40
 
41
  @app.get("/wiki/search/summary/{summary_page_name}")
42
  async def get_wiki_summary(summary_page_name: str, background_tasks: BackgroundTasks):
 
 
 
 
 
 
43
  if summary_page_name in summary_cache:
44
  # print("Cache hit for summary:", page_name) #Working
45
  return JSONResponse(content=summary_cache[summary_page_name], status_code=200)
@@ -77,7 +89,13 @@ async def get_wiki_summary(summary_page_name: str, background_tasks: BackgroundT
77
  )
78
 
79
  @app.get("/wiki/search/full/{full_page}")
80
- def search_wiki_full_page(full_page: str, background_tasks: BackgroundTasks):
 
 
 
 
 
 
81
  if full_page in full_page_cache:
82
  # print("Cache hit for full_page:", full_page) #Working
83
  return JSONResponse(content=full_page_cache[full_page], status_code=200)
@@ -117,6 +135,10 @@ def search_wiki_full_page(full_page: str, background_tasks: BackgroundTasks):
117
 
118
  @app.post("/geodistance")
119
  def get_geodistance(payload: Geodistance):
 
 
 
 
120
  lat1, lon1 = payload.lat1, payload.lon1
121
  lat2, lon2 = payload.lat2, payload.lon2
122
  unit = payload.unit
@@ -151,11 +173,92 @@ def get_geodistance(payload: Geodistance):
151
  status_code=200
152
  )
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  @app.get("/random")
155
  def random():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  return JSONResponse(
157
  content={
158
- "message": "Spare endpoint to test."
 
159
  },
160
  status_code=200
161
  )
 
22
  lon2: float = Field(..., ge=-180, le=180)
23
  unit: str = "km"
24
 
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(
32
  CORSMiddleware,
33
  allow_origins=["*"], # Replace with your frontend domain in prod
 
46
 
47
  @app.get("/wiki/search/summary/{summary_page_name}")
48
  async def get_wiki_summary(summary_page_name: str, background_tasks: BackgroundTasks):
49
+ """
50
+ This function fetches the summary of a Wikipedia page along with its geographical coordinates.
51
+ It also caches the result in ephemeral in-memory cache in the background.
52
+ Input: summary_page_name: str - Name of the Wikipedia page to fetch summary for.
53
+ Output: {"title": "Page Title", "content": "Summary content here", "latitude": float, "longitude": float9}
54
+ """
55
  if summary_page_name in summary_cache:
56
  # print("Cache hit for summary:", page_name) #Working
57
  return JSONResponse(content=summary_cache[summary_page_name], status_code=200)
 
89
  )
90
 
91
  @app.get("/wiki/search/full/{full_page}")
92
+ async def search_wiki_full_page(full_page: str, background_tasks: BackgroundTasks):
93
+ """
94
+ This function fetches the full content of a Wikipedia page along with its geographical coordinates.
95
+ It also caches the result in ephemeral in-memory cache in the background.
96
+ Input: full_page: str - Name of the Wikipedia page to fetch full content for.
97
+ Output: {"title": "Page Title", "content": "Full content here", "latitude": float, "longitude": float}
98
+ """
99
  if full_page in full_page_cache:
100
  # print("Cache hit for full_page:", full_page) #Working
101
  return JSONResponse(content=full_page_cache[full_page], status_code=200)
 
135
 
136
  @app.post("/geodistance")
137
  def get_geodistance(payload: Geodistance):
138
+ """
139
+ Input: "lat1", "lon1", "lat2", "lon2", "unit (km/mi)"
140
+ Output: {"distance": float, "unit": str, "lat1": float, "lon1": float, "lat2": float, "lon2": float}
141
+ """
142
  lat1, lon1 = payload.lat1, payload.lon1
143
  lat2, lon2 = payload.lat2, payload.lon2
144
  unit = payload.unit
 
173
  status_code=200
174
  )
175
 
176
+ @app.post("/wiki/nearby")
177
+ async def get_nearby_wiki_pages(payload: NearbyWikiPage):
178
+ """
179
+ Returns a list of wikipedia pages whose geographical coordinates are within a specified radius from a given location.
180
+ Input:
181
+ - lat: Latitude of the reference point
182
+ - lon: Longitude of the reference point
183
+ - radius: Radius in meters within which to search for pages
184
+ - limit: Maximum number of pages to return
185
+
186
+ Output:
187
+ {
188
+ "pages": [
189
+ {
190
+ "pageid": 123456,
191
+ "title": "Page Title",
192
+ "lat": 54.163337,
193
+ "lon": 37.561109,
194
+ "dist": 123.45 # Dist. in meters from the reference point
195
+ ...
196
+ },
197
+ ...
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
+
240
  @app.get("/random")
241
  def random():
242
+ url = "https://en.wikipedia.org/w/api.php?action=query&list=geosearch&gscoord=54.163337|37.561109&gsradius=10000&gslimit=10&format=json"
243
+ response = requests.get(url, timeout=10)
244
+
245
+ if response.status_code != 200:
246
+ return JSONResponse(
247
+ content={"error": "Failed to fetch random page"},
248
+ status_code=500
249
+ )
250
+ data = response.json()
251
+ pages = data.get("query", {}).get("geosearch", [])
252
+ if not pages:
253
+ return JSONResponse(
254
+ content={"error": "No pages found"},
255
+ status_code=404
256
+ )
257
+
258
  return JSONResponse(
259
  content={
260
+ "pages": pages,
261
+ "count": len(pages)
262
  },
263
  status_code=200
264
  )