Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
118 changes: 94 additions & 24 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,9 @@ const FlightConditionsPanel = ({ conditions, sensorStatus, motionData }) => {
<p><strong>Recomendación:</strong> {recommendationLabel[conditions.recommendation] || conditions.recommendation}</p>
<p><strong>Despegue óptimo:</strong> {Math.round(conditions.takeoff_heading_deg)}° (contra viento)</p>
<p><strong>Aterrizaje óptimo:</strong> {Math.round(conditions.landing_heading_deg)}° (contra viento)</p>
</div>
);
};

const NetworkStatusBanner = ({ isOnline, weatherFromCache, weatherUpdatedAt }) => {
if (isOnline) return null;
Expand All @@ -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 (
<div className="downloads-panel">
<h3>📦 Descargas in-app</h3>
<p className="downloads-subtitle">Mapas de terreno y airspace sin salir de la app.</p>
{downloads.map((item) => (
<div key={item.id} className="download-item">
<div className="download-main">
<strong>{item.name}</strong>
<span>{item.sizeMb} MB</span>
</div>
<div className="download-progress">
<div className="download-progress-fill" style={{ width: `${item.progress}%` }} />
</div>
<div className="download-actions">
{item.status !== 'downloading' && item.status !== 'done' && <button onClick={() => startDownload(item.id)}>⬇️ Descargar</button>}
{item.status === 'downloading' && <button onClick={() => pauseDownload(item.id)}>⏸️ Pausar</button>}
{(item.status === 'paused' || item.status === 'done') && <button onClick={() => resetDownload(item.id)}>🔄 Reiniciar</button>}
<span className={`download-status ${item.status}`}>{item.status}</span>
</div>
</div>
))}
{!isOnline && <p className="downloads-offline">Sin conexión: vuelve online para iniciar descargas.</p>}
</div>
);
};

const recommendationLabel = {
recommended: '✅ Recomendado volar',
caution: '⚠️ Volar con precaución',
Expand Down Expand Up @@ -748,6 +816,15 @@ const DraggableWidget = ({ id, title, children, config, onUpdate }) => {
);
};

const MapButtonControls = ({ onCenterPosition, onToggleSidebar, onStartNavigation, onEndNavigation, canStartNavigation, navigationMode }) => (
<div className="map-button-controls">
<button onClick={onCenterPosition}>🎯 Centrar GPS</button>
<button onClick={onToggleSidebar}>🗂️ Panel</button>
{!navigationMode && <button disabled={!canStartNavigation} onClick={onStartNavigation}>🧭 Navegar</button>}
{navigationMode && <button onClick={onEndNavigation}>🛑 Fin navegación</button>}
</div>
);

const WeatherWidget = ({ conditions, sensorStatus, motionData }) => {
if (!conditions) {
return <p>Cargando condiciones meteorológicas...</p>;
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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 }));
Expand All @@ -1284,8 +1366,6 @@ const App = () => {
setSensorStatus(status);
};

const handleMotionUpdate = (data) => {
setMotionData(prev => ({ ...prev, ...data }));
const motionUpdateRaf = useRef(null);

const handleMotionUpdate = (data) => {
Expand Down Expand Up @@ -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,
Expand All @@ -1360,8 +1438,7 @@ const App = () => {
};

fetchConditions();
}, [currentPosition]);
}, [currentPosition, isOnline]);
}, [currentPosition, isOnline, lastWeatherSnapshot]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove self-updating weather snapshot dependency

Including lastWeatherSnapshot in this effect’s dependency list causes a request loop whenever a position is available and the app is online: the effect fetches weather, then calls setLastWeatherSnapshot(...) with a new object (updatedAt: Date.now()), which retriggers the same effect immediately and repeats indefinitely. In practice this will spam /conditions, increase battery/network usage, and can make the UI feel unstable under normal online navigation.

Useful? React with 👍 / 👎.



useEffect(() => {
Expand Down Expand Up @@ -1495,6 +1572,7 @@ const App = () => {
<button onClick={() => setSelectedAirspaceTypes([])} className="clear-filters-btn">Clear All Filters</button>
</div>
<OpenSourcePanel />
<DownloadsPanel isOnline={isOnline} />
<RouteDisplay routes={routes} selectedRoute={selectedRoute} onRouteSelect={setSelectedRoute} />
</div>
)}
Expand All @@ -1503,6 +1581,14 @@ const App = () => {
{visibleOnPage('map') && (
<DraggableWidget id="map" title="🗺️ Mapa de vuelo" config={widgetConfig.map} onUpdate={updateWidgetConfig}>
<div className="map-widget-inner">
<MapButtonControls
onCenterPosition={handleCenterPosition}
onToggleSidebar={() => setShowSidebar(!showSidebar)}
onStartNavigation={handleStartNavigation}
onEndNavigation={handleEndNavigation}
canStartNavigation={Boolean(selectedRoute && sensorStatus.gps)}
navigationMode={navigationMode}
/>
<SensorWarnings sensorStatus={sensorStatus} />
{navigationMode && selectedRoute && (
<NavigationMode route={selectedRoute} currentPosition={currentPosition} onNavigationEnd={handleEndNavigation} />
Expand Down Expand Up @@ -1559,22 +1645,6 @@ const App = () => {
</DraggableWidget>
)}

<MapContainer
center={mapCenter}
zoom={navigationMode ? 15 : 5}
className="leaflet-map"
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>

{/* GPS Tracker - always active */}
<GPSTracker
onPositionUpdate={handlePositionUpdate}
onSensorStatus={handleSensorStatus}
onMotionUpdate={handleMotionUpdate}
/>
{visibleOnPage('weather') && (
<DraggableWidget id="weather" title="🌤️ Tiempo y recomendación" config={widgetConfig.weather} onUpdate={updateWidgetConfig}>
<WeatherWidget conditions={flightConditions} sensorStatus={sensorStatus} motionData={motionData} />
Expand All @@ -1592,4 +1662,4 @@ const App = () => {
);
};

export default App;
export default App;