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 |
)
|