import React, { useState, useEffect, useRef, useCallback } from 'react'; import { MapContainer, TileLayer, useMapEvents, Marker, Popup , useMap, Polyline, Tooltip, Polygon, GeoJSON, ScaleControl } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { generateGeodesicPoints, calculatePolygonArea, getPolygonCentroid, formatArea, formatPerimeter } from '../utils/mapUtils'; delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', }); const maxExplorationLimit = 50; // kilometers, the maximum amount user can select to explore. const ClickHandler = ({ onClick }) => { useMapEvents({ click(e) { const { lat, lng } = e.latlng; onClick(lat, lng); }, }); return null; }; const rawUrl = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8004'; const BACKEND_URL = rawUrl.replace(/\/$/, ''); // console.log(BACKEND_URL); const ResizeHandler = ({ trigger }) => { const map = useMap(); useEffect(() => { map.invalidateSize(); }, [trigger, map]); return null; }; const WikiMap = ( { onMapClick, searchQuery, contentType, setSearchQuery, setSubmittedQuery } ) => { const [baseLayer, setBaseLayer] = useState("base"); // "base" | "satellite" const [markerPosition, setMarkerPosition] = useState(null); const [wikiContent, setWikiContent] = useState(null); const [panelSize, setPanelSize] = useState('half'); const [wikiWidth, setWikiWidth] = useState(20); const [iframeSrc, setIframeSrc] = useState(''); const isDragging = useRef(false); const startX = useRef(0); const startWidth = useRef(0); const containerRef = useRef(null); const [geoPoints, setGeoPoints] = useState([]); const [geoDistance, setGeoDistance] = useState(null); const [geoSidebarOpen, setGeoSidebarOpen] = useState(false); const [geoToolMode, setGeoToolMode] = useState("menu"); // "menu" | "distance" | "area" const [geoUnit, setGeoUnit] = useState('km'); const [isGeoMarkerDragging, setIsGeoMarkerDragging] = useState(false); const distanceCache = useRef({}); const [areaPoints, setAreaPoints] = useState([]); const [polygonArea, setPolygonArea] = useState(null); const [areaUnit, setAreaUnit] = useState('sqm'); // 'sqm', 'sqkm', 'ha', 'acres', 'sqmi' const [numberFormat, setNumberFormat] = useState('normal'); // 'normal' | 'scientific' const [polygonPerimeter, setPolygonPerimeter] = useState(null); const [viewPanelOpen, setViewPanelOpen] = useState(true); const [countryBorders, setCountryBorders] = useState(null); const [explorationMode, setExplorationMode] = useState(false); const [explorationRadius, setExplorationRadius] = useState(10); const [explorationLimit, setExplorationLimit] = useState(10); const [explorationMarkers, setExplorationMarkers] = useState([]); const [explorationSidebarOpen, setExplorationSidebarOpen] = useState(false); const [shouldZoom, setShouldZoom] = useState(false); const [zoomDelaySeconds, setZoomDelaySeconds] = useState(3); // Default zoom delay in seconds // Using CenterMap component to handle centering (for summary/full apis) and for zooming (for wiki/nearby api) const CenterMap = ({ position, coordinates, shouldZoom, setShouldZoom, zoomDelaySeconds }) => { const map = useMap(); useEffect(() => { if (position && Array.isArray(position) && position.length === 2) { map.setView(position, map.getZoom()); } }, [map, position]); useEffect(() => { if (coordinates && Array.isArray(coordinates) && coordinates.length > 1 && shouldZoom) { const bounds = L.latLngBounds(coordinates); console.log("Delay:", zoomDelaySeconds); if (zoomDelaySeconds > 0) { map.flyToBounds(bounds, { padding: [50, 50], maxZoom: 16, duration: zoomDelaySeconds }); } else { map.fitBounds(bounds, { padding: [50, 50], maxZoom: 16 }); } setShouldZoom(false); } }, [coordinates, map, shouldZoom, setShouldZoom, zoomDelaySeconds]); return null; }; const handleMouseDown = (e) => { isDragging.current = true; startX.current = e.clientX; startWidth.current = wikiWidth; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; const handleMouseMove = (e) => { if (!isDragging.current || !containerRef.current) return; const containerWidth = containerRef.current.offsetWidth; const deltaX = e.clientX - startX.current; const newWidth = Math.max(20, Math.min(80, startWidth.current + (deltaX / containerWidth * 100))); setWikiWidth(newWidth); }; const handleMouseUp = () => { isDragging.current = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; } useEffect(() => { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, []); const fetchWiki = useCallback(async (pageName) => { try{ let endpoint; if (contentType === 'summary') { endpoint = `${BACKEND_URL}/wiki/search/summary/${pageName}`; } else if (contentType === 'full') { endpoint = `${BACKEND_URL}/wiki/search/full/${pageName}`; } else { console.log("Invalid content type:", contentType); setWikiContent(null); return; } const res = await fetch(endpoint); const data = await res.json(); if (contentType === 'summary') { setWikiContent(data); if (data?.latitude && data?.longitude) { setMarkerPosition([data.latitude, data.longitude]); } } else if (contentType === 'full') { setWikiContent({ title: data.title, content: data.content }); const htmlContent = ` " ${data.content} `; const blob = new Blob([htmlContent], { type: 'text/html' }); const blobUrl = URL.createObjectURL(blob); setIframeSrc(blobUrl); if (data?.latitude && data?.longitude) { setMarkerPosition([data.latitude, data.longitude]); } } else { console.log("Invalid content type:", contentType); setWikiContent(null); } } catch (error) { console.error("Error fetching Wikipedia content:", error); } }, [contentType]); useEffect(() => { if (searchQuery) { fetchWiki(searchQuery); } }, [searchQuery, fetchWiki]); const togglePanel = () => { setPanelSize(prev => { if (prev === 'half') return 'half'; if (prev === 'full') return 'half'; return 'half'; }); setWikiWidth(20); }; const handleExplorationClick = useCallback(async (lat, lon) => { try{ const res = await fetch(`${BACKEND_URL}/wiki/nearby`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: lat, lon: lon, radius: explorationRadius*1000, limit: explorationLimit }), }); if (res.ok) { const data = await res.json(); const markers = data.pages.map(page => ({ position: [page.lat, page.lon], title: page.title, distance: page.dist })); // setExplorationMarkers(markers); // Now adding the main clicked point setExplorationMarkers([ { position: [lat, lon], title: 'Clicked Location', distance: 0, isClickedPoint: true }, ...markers ]); setShouldZoom(true); console.log(`Found ${markers.length} nearby pages`); // Only backend results. } else { console.error('Failed to fetch nearby pages'); } } catch (err) { console.error('Error fetching nearby pages:', err); } }, [explorationRadius, explorationLimit, setExplorationMarkers, setShouldZoom]); const handleDistanceClick = useCallback(async (lat, lon) => { const updatedPoints = [...geoPoints, { lat, lon }]; if (updatedPoints.length > 2) { updatedPoints.shift(); // keep only two } setGeoPoints(updatedPoints); if (updatedPoints.length === 2) { console.log("Fetching distance"); try { const res = await fetch(`${BACKEND_URL}/geodistance`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat1: updatedPoints[0].lat, lon1: updatedPoints[0].lon, lat2: updatedPoints[1].lat, lon2: updatedPoints[1].lon, unit: geoUnit, }), }); const data = await res.json(); setGeoDistance(data.distance); setGeoSidebarOpen(true); console.log("Distance fetched:", data.distance); } catch (err) { console.error('Failed to fetch distance:', err); setGeoDistance(null); } } }, [geoPoints, geoUnit, setGeoPoints, setGeoDistance, setGeoSidebarOpen]); const handleAreaClick = useCallback((lat, lon) => { const updated = [...areaPoints, [lat, lon]]; setAreaPoints(updated); }, [areaPoints, setAreaPoints]); const handleMapClick = useCallback(async (lat, lon) => { if (explorationMode) { await handleExplorationClick(lat, lon); } else if (geoToolMode === "distance") { await handleDistanceClick(lat, lon); } else if (geoToolMode === "area") { handleAreaClick(lat, lon); } else { console.log("Invalid tool mode:", geoToolMode); } }, [explorationMode, geoToolMode, handleExplorationClick, handleDistanceClick, handleAreaClick]); useEffect(() => { if (geoPoints.length === 2) { const cacheKey = `${geoPoints[0].lat},${geoPoints[0].lon}-${geoPoints[1].lat},${geoPoints[1].lon}-${geoUnit}`; if (distanceCache.current[cacheKey]) { setGeoDistance(distanceCache.current[cacheKey]); console.log("Using cached distance:", distanceCache.current[cacheKey].toFixed(3)); return; } const fetchDistance = async () => { try{ const res = await fetch(`${BACKEND_URL}/geodistance`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat1: geoPoints[0].lat, lon1: geoPoints[0].lon, lat2: geoPoints[1].lat, lon2: geoPoints[1].lon, unit: geoUnit }), }); const data = await res.json(); setGeoDistance(data.distance); distanceCache.current[cacheKey] = data.distance; // Setting up the cache here, forgot it in first attempt. console.log("Using normal distance method:", data.distance.toFixed(3)); } catch (err) { console.error('Failed to fetch distance:', err); setGeoDistance(null); } }; if (isGeoMarkerDragging){ const timeoutId = setTimeout(() => { fetchDistance(); }, 100); // 100ms timeout, before every backend call during dragging return () => clearTimeout(timeoutId); } fetchDistance(); } }, [geoPoints, geoUnit, isGeoMarkerDragging]); useEffect(() => { if (geoToolMode === "area" && areaPoints.length >= 3) { // Just ensuring that the polygon is closed (first == last) const closed = [...areaPoints, areaPoints[0]]; const {area, perimeter} = calculatePolygonArea(closed); // This took me a while to figure out, it should be just (lat, lon), not (lon, lat) setPolygonArea(area); setPolygonPerimeter(perimeter); } else { setPolygonArea(null); setPolygonPerimeter(null); } }, [geoToolMode, areaPoints]); useEffect(() => { if (!countryBorders) { fetch('/data/countryBordersCondensed.json') .then(res => res.json()) .then(data => setCountryBorders(data)) .catch(err => console.error("Failed to load country borders:", err)); } }, [countryBorders]); const wrapCount = 3; const equatorLines = []; for (let i = -wrapCount; i <= wrapCount; i++) { const offset = i * 360; equatorLines.push([ [0, -180 + offset], [0, 180 + offset], ]); } const cancerLat = 23.4366; const capricornLat = -23.4366; const generateWrappedLine = (latitude) => { const lines = []; for (let i = -wrapCount; i <= wrapCount; i++) { const offset = i * 360; lines.push([ [latitude, -180 + offset], [latitude, 180 + offset], ]); } return lines; }; const tropicOfCancerLines = generateWrappedLine(cancerLat); const tropicOfCapricornLines = generateWrappedLine(capricornLat); const generateLongitudeLines = (interval = 30, wraps = 1) => { const lines = []; for (let lon = -180; lon <= 180; lon += interval) { for (let w = -wraps; w <= wraps; w++) { const wrappedLon = lon + (360 * w); lines.push([ [-90, wrappedLon], [90, wrappedLon] ]); } } return lines; }; return (
{panelSize !== 'closed' && ( <>

{wikiContent?.title || 'Search for a location'}

{markerPosition && ( )}
{wikiContent ? (
{contentType === 'full' ? (