@mytec: iter1.5.1 ready for testing
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,20 @@ export default function App() {
|
||||
const [presets, setPresets] = useState<Record<string, Preset>>({});
|
||||
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() {
|
||||
>
|
||||
<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" />
|
||||
Cancel
|
||||
Cancel{elapsed > 0 ? ` (${elapsed}s)` : '...'}
|
||||
</span>
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user