import React, { useState, useEffect, useRef } from 'react'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import apiService from './services/api'; import './styles/App.css'; const APP = () => { const mapContainerRef = useRef(null); const mapRef = useRef(null); const startMarkerRef = useRef(null); const [selectedPoint, setSelectedPoint] = useState({ lat: 51.5074, lon: -0.1278 }); const [direction, setDirection] = useState(45); const [lineOfSightData, setLineOfSightData] = useState(null); const [loading, setLoading] = useState(false); const [mapStyle, setMapStyle] = useState('light'); // 'light' or 'dark' const [tolerance, setTolerance] = useState(50); useEffect(() => { // Initialize MapLibre map mapRef.current = new maplibregl.Map({ container: mapContainerRef.current, style: getMapStyle(mapStyle), center: [-0.1278, 51.5074], // London zoom: 3, pitch: 0 }); mapRef.current.on('load', () => { console.log('Map loaded successfully'); // Initialize start marker startMarkerRef.current = new maplibregl.Marker({ color: '#FF6B6B' }) .setLngLat([selectedPoint.lon, selectedPoint.lat]) .addTo(mapRef.current); // Initialize preview line source and layer mapRef.current.addSource('preview-line', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } } }); mapRef.current.addLayer({ id: 'preview-line', type: 'line', source: 'preview-line', paint: { 'line-color': '#FF6B6B', 'line-width': 2, 'line-dasharray': [2, 2] } }); updatePreviewLine(selectedPoint, direction); }); mapRef.current.on('click', (e) => { const { lng, lat } = e.lngLat; setSelectedPoint({ lat, lon: lng }); if (startMarkerRef.current) { startMarkerRef.current.setLngLat([lng, lat]); } // Clear previous final line when moving start point if (mapRef.current.getSource('line-of-sight')) { mapRef.current.removeLayer('line-of-sight'); mapRef.current.removeSource('line-of-sight'); } }); return () => { mapRef.current.remove(); }; }, []); useEffect(() => { // Update preview whenever point or direction changes if (mapRef.current && mapRef.current.isStyleLoaded()) { updatePreviewLine(selectedPoint, direction); } }, [selectedPoint, direction]); const updatePreviewLine = (point, bearing) => { const map = mapRef.current; if (!map || !map.getSource('preview-line')) return; // Generate 50 points along a 5000km great circle path for a smooth curve const path = []; const steps = 50; const totalDistance = 5000; for (let i = 0; i <= steps; i++) { const dist = (totalDistance * i) / steps; path.push(calculateDestination(point.lat, point.lon, bearing, dist)); } map.getSource('preview-line').setData({ type: 'Feature', geometry: { type: 'LineString', coordinates: path.map(p => [p.lon, p.lat]) } }); }; // Helper to calculate destination point given start, bearing, and distance (km) const calculateDestination = (lat, lon, bearing, distance) => { const R = 6371; // Earth's radius in km const brng = (bearing * Math.PI) / 180; const φ1 = (lat * Math.PI) / 180; const λ1 = (lon * Math.PI) / 180; const δ = distance / R; const φ2 = Math.asin( Math.sin(φ1) * Math.cos(δ) + Math.cos(φ1) * Math.sin(δ) * Math.cos(brng) ); const λ2 = λ1 + Math.atan2( Math.sin(brng) * Math.sin(δ) * Math.cos(φ1), Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2) ); return { lat: (φ2 * 180) / Math.PI, lon: (((λ2 * 180) / Math.PI + 540) % 360) - 180 }; }; useEffect(() => { // Update map style when toggle changes if (mapRef.current) { mapRef.current.setStyle(getMapStyle(mapStyle)); } }, [mapStyle]); const getMapStyle = (style) => { return style === 'dark' ? 'https://demotiles.maplibre.org/style.json' : 'https://demotiles.maplibre.org/style.json'; // Using same for now, can be customized }; const handleShowLineOfSight = async () => { setLoading(true); try { const response = await apiService.getLineOfSight( selectedPoint.lat, selectedPoint.lon, direction, tolerance ); setLineOfSightData(response.data); renderLineOnMap(response.data); } catch (error) { console.error('Error fetching line of sight:', error); } finally { setLoading(false); } }; const renderLineOnMap = (data) => { const map = mapRef.current; if (!map) return; // Clear existing line if (map.getSource('line-of-sight')) { map.removeLayer('line-of-sight'); map.removeSource('line-of-sight'); } // Add line source map.addSource('line-of-sight', { type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: data.line_coordinates.map(coord => [coord.lon, coord.lat]) } } }); // Add line layer map.addLayer({ id: 'line-of-sight', type: 'line', source: 'line-of-sight', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#FF6B6B', 'line-width': 4, 'line-opacity': 0.8 } }); // Add city markers data.conurbations.forEach((city, index) => { const markerId = `city-${index}`; // Create marker element const el = document.createElement('div'); el.className = 'city-marker'; el.innerHTML = `
${city.name}
${(city.population / 1000000).toFixed(1)}M
`; // Add marker to map new maplibregl.Marker(el) .setLngLat([city.lon, city.lat]) .setPopup(new maplibregl.Popup().setHTML( `${city.name}
Population: ${city.population.toLocaleString()}
Distance: ${city.distance_km} km` )) .addTo(map); }); // Fit bounds to show line const bounds = new maplibregl.LngLatBounds(); data.line_coordinates.forEach(coord => { bounds.extend([coord.lon, coord.lat]); }); map.fitBounds(bounds, { padding: 50 }); }; return (

Line of Sight Settings

{selectedPoint.lat.toFixed(4)}, {selectedPoint.lon.toFixed(4)}
setDirection(parseInt(e.target.value))} /> {direction}°
setTolerance(parseInt(e.target.value))} min="10" max="200" /> {tolerance} km
{lineOfSightData && (

Conurbations Found ({lineOfSightData.conurbations.length})

{lineOfSightData.conurbations.slice(0, 10).map((city, index) => ( ))}
# City Population Distance
{index + 1} {city.name} {(city.population / 1000000).toFixed(1)}M {city.distance_km} km
{lineOfSightData.conurbations.length > 10 && (

... and {lineOfSightData.conurbations.length - 10} more cities

)}
)}

How to Use

  1. Click anywhere on the map to select a starting point
  2. Adjust the direction using the slider (0-360°)
  3. Set your fuzziness tolerance (how close cities must be to the line)
  4. Click "Show Line of Sight" to visualize the path and cities
); }; export default APP;