@mytec: iter1.5.1 ready for testing

This commit is contained in:
2026-01-31 02:13:28 +02:00
parent 7595ba430d
commit 358846fe20
5 changed files with 69 additions and 31 deletions

View File

@@ -70,15 +70,18 @@ async def check_fresnel_clearance(
rx_lat: float = Query(..., description="Receiver latitude"), rx_lat: float = Query(..., description="Receiver latitude"),
rx_lon: float = Query(..., description="Receiver longitude"), rx_lon: float = Query(..., description="Receiver longitude"),
rx_height: float = Query(1.5, ge=0, description="Receiver height (m)"), 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""" """Calculate Fresnel zone clearance"""
try:
result = await los_service.calculate_fresnel_clearance( result = await los_service.calculate_fresnel_clearance(
tx_lat, tx_lon, tx_height, tx_lat, tx_lon, tx_height,
rx_lat, rx_lon, rx_height, rx_lat, rx_lon, rx_height,
frequency frequency
) )
return result return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Fresnel calculation error: {str(e)}")
@router.get("/tiles") @router.get("/tiles")

View File

@@ -17,7 +17,7 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title="RFCP Backend API", title="RFCP Backend API",
description="RF Coverage Planning Backend", description="RF Coverage Planning Backend",
version="1.4.0", version="1.5.1",
lifespan=lifespan, lifespan=lifespan,
) )
@@ -39,7 +39,7 @@ app.include_router(coverage.router, prefix="/api/coverage", tags=["coverage"])
@app.get("/") @app.get("/")
async def root(): async def root():
return {"message": "RFCP Backend API", "version": "1.4.0"} return {"message": "RFCP Backend API", "version": "1.5.1"}
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -138,6 +138,14 @@ class LineOfSightService:
total_distance = profile[-1]["distance"] 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 (lambda = c / f)
wavelength = 300.0 / frequency_mhz # meters wavelength = 300.0 / frequency_mhz # meters
@@ -148,19 +156,16 @@ class LineOfSightService:
d = point["distance"] d = point["distance"]
terrain_elev = point["elevation"] terrain_elev = point["elevation"]
if d == 0 or d == total_distance: if d <= 0 or d >= total_distance:
continue # Skip endpoints continue # Skip endpoints
# LOS height at this point # LOS height at this point
if total_distance > 0:
los_height = tx_total + (rx_total - tx_total) * (d / total_distance) los_height = tx_total + (rx_total - tx_total) * (d / total_distance)
else:
los_height = tx_total
# 1st Fresnel zone radius at this point # 1st Fresnel zone radius at this point
d1 = d d1 = d
d2 = total_distance - 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 (60% of 1st Fresnel zone)
required_clearance = 0.6 * fresnel_radius required_clearance = 0.6 * fresnel_radius
@@ -184,9 +189,9 @@ class LineOfSightService:
worst_distance = d worst_distance = d
return { return {
"clearance_percent": worst_clearance_pct, "clearance_percent": float(worst_clearance_pct),
"has_adequate_clearance": worst_clearance_pct >= 60.0, "has_adequate_clearance": worst_clearance_pct >= 60.0,
"worst_point_distance": worst_distance, "worst_point_distance": float(worst_distance),
"fresnel_profile": profile "fresnel_profile": profile
} }

View File

@@ -65,6 +65,20 @@ export default function App() {
const [presets, setPresets] = useState<Record<string, Preset>>({}); const [presets, setPresets] = useState<Record<string, Preset>>({});
const [showAdvanced, setShowAdvanced] = useState(false); 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 // Load presets on mount
useEffect(() => { useEffect(() => {
api.getPresets().then(setPresets).catch((err) => { api.getPresets().then(setPresets).catch((err) => {
@@ -415,7 +429,7 @@ export default function App() {
> >
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="animate-spin inline-block w-3 h-3 border-2 border-white border-t-transparent rounded-full" /> <span className="animate-spin inline-block w-3 h-3 border-2 border-white border-t-transparent rounded-full" />
Cancel Cancel{elapsed > 0 ? ` (${elapsed}s)` : '...'}
</span> </span>
</Button> </Button>
) : ( ) : (

View File

@@ -52,11 +52,13 @@ export default function CoverageBoundary({
const paths: L.LatLngExpression[][] = []; const paths: L.LatLngExpression[][] = [];
for (const sitePoints of bySite.values()) { for (const sitePoints of bySite.values()) {
const path = computeConcaveHull(sitePoints, resolution); const sitePaths = computeConcaveHulls(sitePoints, resolution);
for (const path of sitePaths) {
if (path.length >= 3) { if (path.length >= 3) {
paths.push(path); paths.push(path);
} }
} }
}
return paths; return paths;
}, [points, visible, resolution]); }, [points, visible, resolution]);
@@ -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. * 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). * Falls back to empty if hull computation fails (e.g., collinear points).
*/ */
function computeConcaveHull( function computeConcaveHulls(
pts: CoveragePoint[], pts: CoveragePoint[],
resolutionM: number resolutionM: number
): L.LatLngExpression[] { ): L.LatLngExpression[][] {
if (pts.length < 3) return []; if (pts.length < 3) return [];
// Convert to GeoJSON FeatureCollection of Points // Convert to GeoJSON FeatureCollection of Points
@@ -124,15 +127,28 @@ function computeConcaveHull(
try { try {
const hull = concave(fc, { maxEdge, units: 'kilometers' }); const hull = concave(fc, { maxEdge, units: 'kilometers' });
if (!hull || hull.geometry.type !== 'Polygon') { if (!hull) return [];
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] if (hull.geometry.type === 'MultiPolygon') {
const coords = hull.geometry.coordinates[0]; // Return all polygons as separate boundary paths
return coords.map( return hull.geometry.coordinates.map((poly) =>
poly[0].map(
([lon, lat]: number[]) => [lat, lon] as L.LatLngExpression ([lon, lat]: number[]) => [lat, lon] as L.LatLngExpression
)
); );
}
return [];
} catch (error) { } catch (error) {
logger.error('Coverage hull computation error:', error); logger.error('Coverage hull computation error:', error);
return []; return [];