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 | Population | Distance |
|---|---|---|---|
| {index + 1} | {city.name} | {(city.population / 1000000).toFixed(1)}M | {city.distance_km} km |
... and {lineOfSightData.conurbations.length - 10} more cities
)}