diff --git a/frontend/src/App.css b/frontend/src/App.css index a47c26cd72..0e020e85ef 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1795,6 +1795,94 @@ body { font-weight: 500; } +.map-button-controls { + position: absolute; + z-index: 1200; + top: 10px; + right: 10px; + display: flex; + gap: 0.4rem; + flex-wrap: wrap; +} + +.map-button-controls button { + border: 1px solid #ef6c00; + background: rgba(255, 243, 224, 0.95); + color: #5d4037; + border-radius: 8px; + padding: 0.45rem 0.6rem; + font-weight: 700; + cursor: pointer; +} + +.map-button-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.downloads-panel { + background: #fff8e1; + border: 1px solid #ffcc80; + border-radius: 10px; + padding: 0.75rem; + margin-top: 0.75rem; +} + +.downloads-subtitle { + margin: 0.25rem 0 0.75rem 0; + color: #6d4c41; + font-size: 0.88rem; +} + +.download-item { + border: 1px solid #ffe0b2; + background: #fffdf8; + border-radius: 8px; + padding: 0.5rem; + margin-bottom: 0.5rem; +} + +.download-main { + display: flex; + justify-content: space-between; + margin-bottom: 0.35rem; +} + +.download-progress { + width: 100%; + height: 8px; + border-radius: 999px; + background: #ffe0b2; + overflow: hidden; +} + +.download-progress-fill { + height: 100%; + background: linear-gradient(90deg, #ff8a65, #ffb74d); +} + +.download-actions { + margin-top: 0.45rem; + display: flex; + align-items: center; + gap: 0.45rem; +} + +.download-actions button { + border: 1px solid #ffb74d; + background: #fff3e0; + border-radius: 6px; + padding: 0.3rem 0.5rem; + cursor: pointer; +} + +.download-status { + text-transform: uppercase; + font-size: 0.72rem; + letter-spacing: 0.02em; + color: #5d4037; +} + @media (max-width: 1024px) { .sidebar { width: 100%; diff --git a/frontend/src/App.js b/frontend/src/App.js index e0e77c376b..5793fe8e38 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -684,6 +684,9 @@ const FlightConditionsPanel = ({ conditions, sensorStatus, motionData }) => {
Recomendación: {recommendationLabel[conditions.recommendation] || conditions.recommendation}
Despegue óptimo: {Math.round(conditions.takeoff_heading_deg)}° (contra viento)
Aterrizaje óptimo: {Math.round(conditions.landing_heading_deg)}° (contra viento)
+ + ); +}; const NetworkStatusBanner = ({ isOnline, weatherFromCache, weatherUpdatedAt }) => { if (isOnline) return null; @@ -697,6 +700,71 @@ const NetworkStatusBanner = ({ isOnline, weatherFromCache, weatherUpdatedAt }) = ); }; +const MAP_DATASETS = [ + { id: 'terrain-iberia', type: 'terrain', name: 'Terreno Iberia (ES/PT)', sizeMb: 480 }, + { id: 'terrain-alps', type: 'terrain', name: 'Terreno Alpes (FR/IT/CH/AT)', sizeMb: 620 }, + { id: 'airspace-spain', type: 'airspace', name: 'Airspace España', sizeMb: 24 }, + { id: 'airspace-france', type: 'airspace', name: 'Airspace Francia', sizeMb: 28 } +]; + +const DownloadsPanel = ({ isOnline }) => { + const [downloads, setDownloads] = useState(() => MAP_DATASETS.map((item) => ({ ...item, status: 'idle', progress: 0 }))); + + useEffect(() => { + const timers = []; + downloads.forEach((item) => { + if (item.status !== 'downloading') return; + const timer = setInterval(() => { + setDownloads((prev) => prev.map((row) => { + if (row.id !== item.id || row.status !== 'downloading') return row; + const next = Math.min(100, row.progress + 10); + return { ...row, progress: next, status: next >= 100 ? 'done' : 'downloading' }; + })); + }, 600); + timers.push(timer); + }); + return () => timers.forEach(clearInterval); + }, [downloads]); + + const startDownload = (id) => { + if (!isOnline) return; + setDownloads((prev) => prev.map((row) => row.id === id ? { ...row, status: 'downloading', progress: row.progress || 1 } : row)); + }; + + const pauseDownload = (id) => { + setDownloads((prev) => prev.map((row) => row.id === id ? { ...row, status: 'paused' } : row)); + }; + + const resetDownload = (id) => { + setDownloads((prev) => prev.map((row) => row.id === id ? { ...row, status: 'idle', progress: 0 } : row)); + }; + + return ( +Mapas de terreno y airspace sin salir de la app.
+ {downloads.map((item) => ( +Sin conexión: vuelve online para iniciar descargas.
} +Cargando condiciones meteorológicas...
; @@ -1128,7 +1205,6 @@ const App = () => { const [sensorStatus, setSensorStatus] = useState({ gps: false, barometer: false, accelerometer: false, compass: false }); const [trackingEnabled, setTrackingEnabled] = useState(false); const [flightConditions, setFlightConditions] = useState(null); - const [motionData, setMotionData] = useState({ compassHeading: null, acceleration: null, speedMs: null }); const [motionData, setMotionData] = useState({ compassHeading: null, acceleration: null, speedMs: null, barometricAltitude: null, pressureHpa: null }); const [pages, setPages] = useState(UI_DEFAULT_PAGES); const [activePage, setActivePage] = useState(UI_DEFAULT_PAGES[0]); @@ -1271,6 +1347,12 @@ const App = () => { setTrackingEnabled(false); }; + const handleCenterPosition = () => { + if (currentPosition) { + setMapCenter([currentPosition.lat, currentPosition.lng]); + } + }; + const handlePositionUpdate = (position) => { setCurrentPosition(position); setMotionData(prev => ({ ...prev, speedMs: position.speed || prev.speedMs })); @@ -1284,8 +1366,6 @@ const App = () => { setSensorStatus(status); }; - const handleMotionUpdate = (data) => { - setMotionData(prev => ({ ...prev, ...data })); const motionUpdateRaf = useRef(null); const handleMotionUpdate = (data) => { @@ -1335,8 +1415,6 @@ const App = () => { params: { lat: currentPosition.lat, lng: currentPosition.lng } }); setFlightConditions(response.data); - } catch (error) { - console.error('Error loading flight conditions:', error); const minimizedData = { weather_description: response.data.weather_description, temperature_c: response.data.temperature_c, @@ -1360,8 +1438,7 @@ const App = () => { }; fetchConditions(); - }, [currentPosition]); - }, [currentPosition, isOnline]); + }, [currentPosition, isOnline, lastWeatherSnapshot]); useEffect(() => { @@ -1495,6 +1572,7 @@ const App = () => {