DebasishDhal99's picture
Make distance feature an option within the GeoTools sidebar
ab8ba8e
raw
history blame
25.2 kB
import React, { useState, useEffect, useRef,
useCallback
} from 'react';
import { MapContainer, TileLayer,
useMapEvents,
Marker,
Popup ,
useMap,
Polyline,
Tooltip
} from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import generateGeodesicPoints 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 ClickHandler = ({ onClick }) => {
useMapEvents({
click(e) {
const { lat, lng } = e.latlng;
onClick(lat, lng);
},
});
return null;
};
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8004';
console.log(BACKEND_URL);
const ResizeHandler = ({ trigger }) => {
const map = useMap();
useEffect(() => {
map.invalidateSize();
}, [trigger, map]);
return null;
};
const Map = ( { onMapClick, searchQuery, contentType } ) => {
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"
const [geoUnit, setGeoUnit] = useState('km');
const [isGeoMarkerDragging, setIsGeoMarkerDragging] = useState(false);
const distanceCache = useRef({});
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/${pageName}`;
}
else if (contentType === 'full') {
endpoint = `${BACKEND_URL}/wiki/search/${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 && data.latitude && data.longitude) {
setMarkerPosition([data.latitude, data.longitude]);
}
} else if (contentType === 'full') {
setWikiContent({
title: data.title,
content: data.content
});
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
img { max-width: 100%; }
</style>
<base href="https://en.wikipedia.org">"
<!-- The upper line is added so that relative links in the Wikipedia content work correctly. -->
</head>
<body>
${data.content}
</body>
</html>
`;
const blob = new Blob([htmlContent], { type: 'text/html' });
const blobUrl = URL.createObjectURL(blob);
setIframeSrc(blobUrl);
// const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent);
// setIframeSrc(dataUrl);;
if (data && 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]);
// const markerPosition = [21.2514, 81.6296];
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 handleGeoClick = 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]);
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]);
return (
<div ref={containerRef} style={{ display: 'flex', height: '100vh', width: '100%', overflow: 'hidden' }}>
{panelSize !== 'closed' && (
<>
<div style={{
width: `${wikiWidth}%`,
height: '100%',
overflow: 'auto',
padding: '20px',
backgroundColor: 'white',
boxShadow: '2px 0 5px rgba(0,0,0,0.1)',
zIndex: 1000,
flexShrink: 0
}}>
<div style={{ marginBottom: '20px' }}>
<h2>{wikiContent?.title || 'Search for a location'}</h2>
</div>
{wikiContent ? (
<div>
{contentType === 'full' ? (
<iframe
src={iframeSrc}
style={{
width: '100%',
height: 'calc(100vh - 100px)',
border: 'none'
}}
title="Wikipedia Page"
/>
) : (
<p>{wikiContent.content}</p>
)}
</div>
) : (
<p>Search for a location to see Wikipedia content</p>
)}
</div>
<div
onMouseDown={handleMouseDown}
style={{
width: '8px',
height: '100%',
backgroundColor: '#f0f0f0',
cursor: 'col-resize',
position: 'relative',
zIndex: 1001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}
>
<div style={{
width: '2px',
height: '40px',
backgroundColor: '#ccc',
borderRadius: '1px'
}} />
</div>
</>
)}
<div style={{
flex: 1,
height: '100%',
position: 'relative',
minWidth: 0,
overflow: 'hidden'
}}>
<MapContainer
center={markerPosition || [0, 0]} // Default center if no marker position
zoom={2}
style={{ height: '100%', width: '100%' }}
>
<ResizeHandler trigger={wikiWidth} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<ClickHandler onClick={handleGeoClick} />
{markerPosition && (
<Marker position={markerPosition}>
{contentType === 'summary' && (
<Popup minWidth={250}>
{wikiContent ? (
<>
<strong>{wikiContent.title}</strong><br />
<p style={{ fontSize: '12px' }}>{wikiContent.content}</p>
</>
) : (
"Search for a location to see information"
)}
</Popup>
)}
</Marker>
)}
{/* Only show geodistance markers/polyline if sidebar is open */}
{geoSidebarOpen && geoToolMode === "distance" && geoPoints.map((pt, index) => (
<Marker key={`geo-${index}`}
position={[pt.lat, pt.lon]}
draggable={true}
eventHandlers={{
dragstart: () => {
setIsGeoMarkerDragging(true);
},
drag: (e) => {
const { lat, lng } = e.target.getLatLng();
const updated = [...geoPoints];
updated[index] = { lat, lon: lng };
setGeoPoints(updated); // The distance function will be continioust triggered throughout the dragging journey
} ,
dragend: (e) => {
const { lat, lng } = e.target.getLatLng();
const updated = [...geoPoints];
updated[index] = { lat, lon: lng };
setGeoPoints(updated); // Triggering the distance fetch via useEffect
setIsGeoMarkerDragging(false);
}
}}
>
<Popup>
Point {index + 1}: {pt.lat.toFixed(4)}, {pt.lon.toFixed(4)}
</Popup>
</Marker>
))}
{/* Polyline if 2 points are selected and sidebar is open */}
{geoSidebarOpen && geoToolMode === "distance" && geoPoints.length === 2 && (
<Polyline
key={geoPoints.map(pt => `${pt.lat},${pt.lon}`).join('-')}
positions={generateGeodesicPoints(
geoPoints[0].lat, geoPoints[0].lon,
geoPoints[1].lat, geoPoints[1].lon
)}
pathOptions={{ color: '#1976d2', weight: 4 }}
>
{geoDistance !== null && (
<Tooltip
direction="center"
permanent
offset={[0, 0]}
opacity={1}
className="distance-tooltip"
>
<span style={{
color: '#1976d2',
fontWeight: 600,
fontSize: '15px',
background: 'none',
border: 'none',
boxShadow: 'none',
padding: 0
}}>
{geoDistance.toFixed(2)} {geoUnit}
</span>
</Tooltip>
)}
</Polyline>
)}
</MapContainer>
{/* Geo Tools Button */}
{!geoSidebarOpen && (
<button
onClick={() => setGeoSidebarOpen(true)}
style={{
position: 'absolute',
top: 12,
right: 12,
zIndex: 1000,
padding: '6px 12px',
backgroundColor: '#1976d2',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}
>
Geo Tools
</button>
)}
{/* Geo Sidebar */}
{geoSidebarOpen && (
<div style={{
position: 'fixed',
top: 24,
right: 24,
width: 280,
background: 'white',
borderRadius: 10,
boxShadow: '0 2px 12px rgba(0,0,0,0.12)',
zIndex: 2000,
padding: 20,
display: 'flex',
flexDirection: 'column',
gap: 16,
border: '1px solid #eee'
}}>
{geoToolMode === "menu" && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<strong>Geo Tools</strong>
<button
onClick={() => {
setGeoSidebarOpen(false);
setGeoPoints([]);
setGeoDistance(null);
setGeoToolMode("menu");
}}
style={{
background: 'none',
border: 'none',
fontSize: 18,
cursor: 'pointer',
color: '#888'
}}
title="Close"
</button>
</div>
<button
style={{
marginTop: 16,
padding: '10px 0',
borderRadius: 4,
border: '1px solid #1976d2',
background: '#1976d2',
color: 'white',
fontWeight: 500,
cursor: 'pointer'
}}
onClick={() => setGeoToolMode("distance")}
>
Measure distance
</button>
{/* Add more tool buttons here in the future */}
</>
)}
{geoToolMode === "distance" && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<strong>Geodistance</strong>
<button
onClick={() => {
setGeoToolMode("menu");
setGeoPoints([]);
setGeoDistance(null);
}}
style={{
background: 'none',
border: 'none',
fontSize: 18,
cursor: 'pointer',
color: '#888'
}}
title="Back"
>←</button>
</div>
<div>
<label style={{ fontWeight: 500, marginRight: 8 }}>Unit:</label>
<select
value={geoUnit}
onChange={e => setGeoUnit(e.target.value)}
style={{ padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc' }}
>
<option value="km">Kilometers</option>
<option value="mi">Miles</option>
</select>
</div>
{geoDistance !== null && (
<div style={{ fontSize: 20, fontWeight: 600, color: '#1976d2' }}>
{geoDistance.toFixed(2)} {geoUnit}
</div>
)}
<button
onClick={() => {
setGeoToolMode("menu");
setGeoPoints([]);
setGeoDistance(null);
}}
style={{
marginTop: 8,
padding: '6px 0',
borderRadius: 4,
border: '1px solid #1976d2',
background: '#1976d2',
color: 'white',
fontWeight: 500,
cursor: 'pointer'
}}
>
Clear & Back
</button>
</>
)}
</div>
)}
{panelSize === 'closed' && (
<button
onClick={togglePanel}
style={{
position: 'absolute',
top: '10px',
left: '10px',
zIndex: 1000,
padding: '5px 10px',
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Show Wikipedia
</button>
)}
</div>
</div>
);
};
export default Map;