From 358846fe20ebbbbb0c5adee8f762734267b49987 Mon Sep 17 00:00:00 2001 From: mytec Date: Sat, 31 Jan 2026 02:13:28 +0200 Subject: [PATCH] @mytec: iter1.5.1 ready for testing --- backend/app/api/routes/terrain.py | 17 ++++---- backend/app/main.py | 4 +- backend/app/services/los_service.py | 21 ++++++---- frontend/src/App.tsx | 16 ++++++- .../src/components/map/CoverageBoundary.tsx | 42 +++++++++++++------ 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/backend/app/api/routes/terrain.py b/backend/app/api/routes/terrain.py index 1fab95f..846ffa3 100644 --- a/backend/app/api/routes/terrain.py +++ b/backend/app/api/routes/terrain.py @@ -70,15 +70,18 @@ async def check_fresnel_clearance( rx_lat: float = Query(..., description="Receiver latitude"), rx_lon: float = Query(..., description="Receiver longitude"), rx_height: float = Query(1.5, ge=0, description="Receiver height (m)"), - frequency: float = Query(..., ge=100, le=6000, description="Frequency (MHz)") + frequency: float = Query(1800, ge=100, le=6000, description="Frequency (MHz)") ): """Calculate Fresnel zone clearance""" - result = await los_service.calculate_fresnel_clearance( - tx_lat, tx_lon, tx_height, - rx_lat, rx_lon, rx_height, - frequency - ) - return result + try: + result = await los_service.calculate_fresnel_clearance( + tx_lat, tx_lon, tx_height, + rx_lat, rx_lon, rx_height, + frequency + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=f"Fresnel calculation error: {str(e)}") @router.get("/tiles") diff --git a/backend/app/main.py b/backend/app/main.py index 78b290a..94f6cb8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -17,7 +17,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="RFCP Backend API", description="RF Coverage Planning Backend", - version="1.4.0", + version="1.5.1", lifespan=lifespan, ) @@ -39,7 +39,7 @@ app.include_router(coverage.router, prefix="/api/coverage", tags=["coverage"]) @app.get("/") async def root(): - return {"message": "RFCP Backend API", "version": "1.4.0"} + return {"message": "RFCP Backend API", "version": "1.5.1"} if __name__ == "__main__": diff --git a/backend/app/services/los_service.py b/backend/app/services/los_service.py index 92102b3..0860991 100644 --- a/backend/app/services/los_service.py +++ b/backend/app/services/los_service.py @@ -138,6 +138,14 @@ class LineOfSightService: total_distance = profile[-1]["distance"] + if total_distance <= 0: + return { + "clearance_percent": 100.0, + "has_adequate_clearance": True, + "worst_point_distance": 0, + "fresnel_profile": profile + } + # Wavelength (lambda = c / f) wavelength = 300.0 / frequency_mhz # meters @@ -148,19 +156,16 @@ class LineOfSightService: d = point["distance"] terrain_elev = point["elevation"] - if d == 0 or d == total_distance: + if d <= 0 or d >= total_distance: continue # Skip endpoints # LOS height at this point - if total_distance > 0: - los_height = tx_total + (rx_total - tx_total) * (d / total_distance) - else: - los_height = tx_total + los_height = tx_total + (rx_total - tx_total) * (d / total_distance) # 1st Fresnel zone radius at this point d1 = d d2 = total_distance - d - fresnel_radius = np.sqrt((wavelength * d1 * d2) / total_distance) + fresnel_radius = float(np.sqrt((wavelength * d1 * d2) / total_distance)) # Required clearance (60% of 1st Fresnel zone) required_clearance = 0.6 * fresnel_radius @@ -184,9 +189,9 @@ class LineOfSightService: worst_distance = d return { - "clearance_percent": worst_clearance_pct, + "clearance_percent": float(worst_clearance_pct), "has_adequate_clearance": worst_clearance_pct >= 60.0, - "worst_point_distance": worst_distance, + "worst_point_distance": float(worst_distance), "fresnel_profile": profile } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0673297..a27ff46 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -65,6 +65,20 @@ export default function App() { const [presets, setPresets] = useState>({}); const [showAdvanced, setShowAdvanced] = useState(false); + // Elapsed time counter during calculation + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + if (!isCalculating) { + setElapsed(0); + return; + } + const start = Date.now(); + const interval = setInterval(() => { + setElapsed(Math.floor((Date.now() - start) / 1000)); + }, 1000); + return () => clearInterval(interval); + }, [isCalculating]); + // Load presets on mount useEffect(() => { api.getPresets().then(setPresets).catch((err) => { @@ -415,7 +429,7 @@ export default function App() { > - Cancel + Cancel{elapsed > 0 ? ` (${elapsed}s)` : '...'} ) : ( diff --git a/frontend/src/components/map/CoverageBoundary.tsx b/frontend/src/components/map/CoverageBoundary.tsx index 27e75c4..14aa3a8 100644 --- a/frontend/src/components/map/CoverageBoundary.tsx +++ b/frontend/src/components/map/CoverageBoundary.tsx @@ -52,9 +52,11 @@ export default function CoverageBoundary({ const paths: L.LatLngExpression[][] = []; for (const sitePoints of bySite.values()) { - const path = computeConcaveHull(sitePoints, resolution); - if (path.length >= 3) { - paths.push(path); + const sitePaths = computeConcaveHulls(sitePoints, resolution); + for (const path of sitePaths) { + if (path.length >= 3) { + paths.push(path); + } } } @@ -103,15 +105,16 @@ export default function CoverageBoundary({ // --------------------------------------------------------------------------- /** - * Compute a concave hull boundary for one site's coverage points. + * Compute concave hull boundary path(s) for a set of coverage points. * * maxEdge = resolution * 3 (in km) gives good detail without over-fitting. + * Returns multiple paths if hull is a MultiPolygon (disjoint coverage areas). * Falls back to empty if hull computation fails (e.g., collinear points). */ -function computeConcaveHull( +function computeConcaveHulls( pts: CoveragePoint[], resolutionM: number -): L.LatLngExpression[] { +): L.LatLngExpression[][] { if (pts.length < 3) return []; // Convert to GeoJSON FeatureCollection of Points @@ -124,15 +127,28 @@ function computeConcaveHull( try { const hull = concave(fc, { maxEdge, units: 'kilometers' }); - if (!hull || hull.geometry.type !== 'Polygon') { - return []; + if (!hull) return []; + + // Handle both Polygon and MultiPolygon results + if (hull.geometry.type === 'Polygon') { + const coords = hull.geometry.coordinates[0]; + return [ + coords.map( + ([lon, lat]: number[]) => [lat, lon] as L.LatLngExpression + ), + ]; } - // GeoJSON coordinates are [lon, lat]; Leaflet needs [lat, lon] - const coords = hull.geometry.coordinates[0]; - return coords.map( - ([lon, lat]: number[]) => [lat, lon] as L.LatLngExpression - ); + if (hull.geometry.type === 'MultiPolygon') { + // Return all polygons as separate boundary paths + return hull.geometry.coordinates.map((poly) => + poly[0].map( + ([lon, lat]: number[]) => [lat, lon] as L.LatLngExpression + ) + ); + } + + return []; } catch (error) { logger.error('Coverage hull computation error:', error); return [];