Spaces:
Runtime error
Runtime error
Commit
·
d6ae08e
1
Parent(s):
1bfa936
Add basic nearby wikisearch, both backend and frontend
Browse files- Used wikipedia api to look for nearby wiki pages.
- Get coordinates from backend and render markers over them
- Search radius range = [10, 10,000] meters, from server side
- I had to deal with a very strange case, where GET request didn't work at all, no matter what. Rebuilt the docker image,
- frontend/src/components/Map.js +222 -3
- main.py +64 -1
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,36 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
|
|
208 |
};
|
209 |
|
210 |
const handleMapClick = useCallback(async (lat, lon) => {
|
211 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
const updatedPoints = [...geoPoints, { lat, lon }];
|
213 |
if (updatedPoints.length > 2) {
|
214 |
updatedPoints.shift(); // keep only two
|
@@ -249,7 +284,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 +690,21 @@ 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 +849,176 @@ const Map = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmitt
|
|
799 |
</button>
|
800 |
)}
|
801 |
|
802 |
-
{/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
console.log(`Found ${markers.length} nearby pages`);
|
240 |
+
} else {
|
241 |
+
console.error('Failed to fetch nearby pages');
|
242 |
+
}
|
243 |
+
} catch (err) {
|
244 |
+
console.error('Error fetching nearby pages:', err);
|
245 |
+
}
|
246 |
+
} else if (geoToolMode === "distance") {
|
247 |
const updatedPoints = [...geoPoints, { lat, lon }];
|
248 |
if (updatedPoints.length > 2) {
|
249 |
updatedPoints.shift(); // keep only two
|
|
|
284 |
console.log("Invalid tool mode:", geoToolMode);
|
285 |
}
|
286 |
|
287 |
+
}, [explorationMode, explorationRadius, explorationLimit, geoToolMode, geoPoints, geoUnit, areaPoints]);
|
288 |
|
289 |
useEffect(() => {
|
290 |
if (geoPoints.length === 2) {
|
|
|
690 |
</Marker>
|
691 |
)}
|
692 |
|
693 |
+
{/* Exploration Mode Markers */}
|
694 |
+
{explorationMode && explorationMarkers.map((marker, index) => (
|
695 |
+
<Marker
|
696 |
+
key={`exploration-${index}`}
|
697 |
+
position={marker.position}
|
698 |
+
>
|
699 |
+
<Popup>
|
700 |
+
<div>
|
701 |
+
<strong>{marker.title}</strong><br />
|
702 |
+
<small>Distance: {marker.distance.toFixed(1)}m</small>
|
703 |
+
</div>
|
704 |
+
</Popup>
|
705 |
+
</Marker>
|
706 |
+
))}
|
707 |
+
|
708 |
{/* Only show geodistance markers/polyline if sidebar is open */}
|
709 |
{geoSidebarOpen && geoToolMode === "distance" && geoPoints.map((pt, index) => (
|
710 |
<Marker key={`geo-${index}`}
|
|
|
849 |
</button>
|
850 |
)}
|
851 |
|
852 |
+
{/* Exploration Mode Button */}
|
853 |
+
{!explorationSidebarOpen && !geoSidebarOpen && (
|
854 |
+
<button
|
855 |
+
onClick={() => setExplorationSidebarOpen(true)}
|
856 |
+
style={{
|
857 |
+
position: 'absolute',
|
858 |
+
top: 50, // Position below Geo Tools button
|
859 |
+
right: 12,
|
860 |
+
zIndex: 1000,
|
861 |
+
padding: '6px 12px',
|
862 |
+
backgroundColor: '#4caf50',
|
863 |
+
color: 'white',
|
864 |
+
border: 'none',
|
865 |
+
borderRadius: 4,
|
866 |
+
cursor: 'pointer'
|
867 |
+
}}
|
868 |
+
>
|
869 |
+
Exploration
|
870 |
+
</button>
|
871 |
+
)}
|
872 |
+
|
873 |
+
{/* Exploration Mode Button - when Geo Tools sidebar is open */}
|
874 |
+
{!explorationSidebarOpen && geoSidebarOpen && (
|
875 |
+
<button
|
876 |
+
onClick={() => setExplorationSidebarOpen(true)}
|
877 |
+
style={{
|
878 |
+
position: 'fixed',
|
879 |
+
top: 320, // Position below Geo Tools sidebar
|
880 |
+
right: 24,
|
881 |
+
zIndex: 2000,
|
882 |
+
padding: '6px 12px',
|
883 |
+
backgroundColor: '#4caf50',
|
884 |
+
color: 'white',
|
885 |
+
border: 'none',
|
886 |
+
borderRadius: 4,
|
887 |
+
cursor: 'pointer'
|
888 |
+
}}
|
889 |
+
>
|
890 |
+
Exploration
|
891 |
+
</button>
|
892 |
+
)}
|
893 |
+
|
894 |
+
{/* Exploration Sidebar */}
|
895 |
+
{explorationSidebarOpen && (
|
896 |
+
<div style={{
|
897 |
+
position: 'fixed',
|
898 |
+
top: geoSidebarOpen ? 320 : 24, // Position below Geo Tools sidebar if open
|
899 |
+
right: 24,
|
900 |
+
width: 280,
|
901 |
+
background: 'white',
|
902 |
+
borderRadius: 10,
|
903 |
+
boxShadow: '0 2px 12px rgba(0,0,0,0.12)',
|
904 |
+
zIndex: 2000,
|
905 |
+
padding: 20,
|
906 |
+
display: 'flex',
|
907 |
+
flexDirection: 'column',
|
908 |
+
gap: 16,
|
909 |
+
border: '1px solid #eee'
|
910 |
+
}}>
|
911 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
912 |
+
<strong>Exploration Mode</strong>
|
913 |
+
<button
|
914 |
+
onClick={() => {
|
915 |
+
setExplorationSidebarOpen(false);
|
916 |
+
setExplorationMode(false);
|
917 |
+
setExplorationMarkers([]);
|
918 |
+
}}
|
919 |
+
style={{
|
920 |
+
background: 'none',
|
921 |
+
border: 'none',
|
922 |
+
fontSize: 18,
|
923 |
+
cursor: 'pointer',
|
924 |
+
color: '#888'
|
925 |
+
}}
|
926 |
+
title="Close"
|
927 |
+
>×</button>
|
928 |
+
</div>
|
929 |
+
|
930 |
+
<div>
|
931 |
+
<label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
|
932 |
+
Search Radius (meters):
|
933 |
+
</label>
|
934 |
+
<input
|
935 |
+
type="range"
|
936 |
+
min="10"
|
937 |
+
max="10000"
|
938 |
+
step="1000"
|
939 |
+
value={explorationRadius}
|
940 |
+
onChange={(e) => setExplorationRadius(parseInt(e.target.value))}
|
941 |
+
style={{ width: '100%' }}
|
942 |
+
/>
|
943 |
+
<div style={{ textAlign: 'center', marginTop: 4 }}>
|
944 |
+
{explorationRadius.toLocaleString()}m
|
945 |
+
</div>
|
946 |
+
</div>
|
947 |
+
|
948 |
+
<div>
|
949 |
+
<label style={{ fontWeight: 500, marginBottom: 8, display: 'block' }}>
|
950 |
+
Number of Results:
|
951 |
+
</label>
|
952 |
+
<input
|
953 |
+
type="range"
|
954 |
+
min="1"
|
955 |
+
max="50"
|
956 |
+
step="1"
|
957 |
+
value={explorationLimit}
|
958 |
+
onChange={(e) => setExplorationLimit(parseInt(e.target.value))}
|
959 |
+
style={{ width: '100%' }}
|
960 |
+
/>
|
961 |
+
<div style={{ textAlign: 'center', marginTop: 4 }}>
|
962 |
+
{explorationLimit} results
|
963 |
+
</div>
|
964 |
+
</div>
|
965 |
+
|
966 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
967 |
+
<input
|
968 |
+
type="checkbox"
|
969 |
+
id="explorationMode"
|
970 |
+
checked={explorationMode}
|
971 |
+
onChange={(e) => setExplorationMode(e.target.checked)}
|
972 |
+
/>
|
973 |
+
<label htmlFor="explorationMode" style={{ fontWeight: 500 }}>
|
974 |
+
Enable Exploration Mode
|
975 |
+
</label>
|
976 |
+
</div>
|
977 |
+
|
978 |
+
{explorationMode && (
|
979 |
+
<div style={{
|
980 |
+
padding: '8px 12px',
|
981 |
+
backgroundColor: '#e8f5e8',
|
982 |
+
borderRadius: 4,
|
983 |
+
fontSize: '14px',
|
984 |
+
color: '#2e7d32'
|
985 |
+
}}>
|
986 |
+
✓ Click anywhere on the map to find nearby Wikipedia pages
|
987 |
+
</div>
|
988 |
+
)}
|
989 |
+
|
990 |
+
{explorationMarkers.length > 0 && (
|
991 |
+
<div style={{
|
992 |
+
padding: '8px 12px',
|
993 |
+
backgroundColor: '#e3f2fd',
|
994 |
+
borderRadius: 4,
|
995 |
+
fontSize: '14px',
|
996 |
+
color: '#1976d2'
|
997 |
+
}}>
|
998 |
+
Found {explorationMarkers.length} nearby pages
|
999 |
+
</div>
|
1000 |
+
)}
|
1001 |
+
|
1002 |
+
<button
|
1003 |
+
onClick={() => {
|
1004 |
+
setExplorationMarkers([]);
|
1005 |
+
}}
|
1006 |
+
style={{
|
1007 |
+
padding: '6px 0',
|
1008 |
+
borderRadius: 4,
|
1009 |
+
border: '1px solid #f44336',
|
1010 |
+
background: '#f44336',
|
1011 |
+
color: 'white',
|
1012 |
+
fontWeight: 500,
|
1013 |
+
cursor: 'pointer'
|
1014 |
+
}}
|
1015 |
+
>
|
1016 |
+
Clear Markers
|
1017 |
+
</button>
|
1018 |
+
</div>
|
1019 |
+
)}
|
1020 |
+
|
1021 |
+
{/* Geo Sidebar - Keep as is */}
|
1022 |
{geoSidebarOpen && (
|
1023 |
<div style={{
|
1024 |
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
|
@@ -151,11 +157,68 @@ def get_geodistance(payload: Geodistance):
|
|
151 |
status_code=200
|
152 |
)
|
153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
@app.get("/random")
|
155 |
def random():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
return JSONResponse(
|
157 |
content={
|
158 |
-
"
|
|
|
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
|
|
|
157 |
status_code=200
|
158 |
)
|
159 |
|
160 |
+
@app.post("/wiki/nearby")
|
161 |
+
async def get_nearby_wiki_pages(payload: NearbyWikiPage):
|
162 |
+
lat, lon = payload.lat, payload.lon
|
163 |
+
radius = payload.radius
|
164 |
+
limit = payload.limit
|
165 |
+
|
166 |
+
url = ("https://en.wikipedia.org/w/api.php"+"?action=query"
|
167 |
+
"&list=geosearch"
|
168 |
+
f"&gscoord={lat}|{lon}"
|
169 |
+
f"&gsradius={radius}"
|
170 |
+
f"&gslimit={limit}"
|
171 |
+
"&format=json")
|
172 |
+
print(url)
|
173 |
+
try:
|
174 |
+
response = requests.get(url, timeout=10)
|
175 |
+
if response.status_code != 200:
|
176 |
+
return JSONResponse(
|
177 |
+
content={"error": "Failed to fetch nearby pages"},
|
178 |
+
status_code=500
|
179 |
+
)
|
180 |
+
data = response.json()
|
181 |
+
|
182 |
+
pages = data.get("query", {}).get("geosearch", [])
|
183 |
+
|
184 |
+
return JSONResponse(
|
185 |
+
content={
|
186 |
+
"pages": pages,
|
187 |
+
"count": len(pages)
|
188 |
+
},
|
189 |
+
status_code=200
|
190 |
+
)
|
191 |
+
except Exception as e:
|
192 |
+
return JSONResponse(
|
193 |
+
content={"error": str(e)},
|
194 |
+
status_code=500
|
195 |
+
)
|
196 |
+
|
197 |
+
|
198 |
+
|
199 |
+
|
200 |
@app.get("/random")
|
201 |
def random():
|
202 |
+
url = "https://en.wikipedia.org/w/api.php?action=query&list=geosearch&gscoord=54.163337|37.561109&gsradius=10000&gslimit=10&format=json"
|
203 |
+
response = requests.get(url, timeout=10)
|
204 |
+
|
205 |
+
if response.status_code != 200:
|
206 |
+
return JSONResponse(
|
207 |
+
content={"error": "Failed to fetch random page"},
|
208 |
+
status_code=500
|
209 |
+
)
|
210 |
+
data = response.json()
|
211 |
+
pages = data.get("query", {}).get("geosearch", [])
|
212 |
+
if not pages:
|
213 |
+
return JSONResponse(
|
214 |
+
content={"error": "No pages found"},
|
215 |
+
status_code=404
|
216 |
+
)
|
217 |
+
|
218 |
return JSONResponse(
|
219 |
content={
|
220 |
+
"pages": pages,
|
221 |
+
"count": len(pages)
|
222 |
},
|
223 |
status_code=200
|
224 |
)
|