145 lines
4.5 KiB
Python
145 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from app.models.schemas import (
|
|
GeoAntenna,
|
|
LinkBudget,
|
|
LinkResponse,
|
|
Obstacle,
|
|
PathProfilePoint,
|
|
)
|
|
from app.services.buildings import BuildingService
|
|
from app.services.canopy import CanopyService
|
|
from app.services.fresnel import clearance_m, fresnel_radius_m
|
|
from app.services.geodesy import line_distance_m, los_height_at, sample_line
|
|
from app.services.path_loss import compute_link_budget, fspl_db
|
|
from app.services.terrain import TerrainService
|
|
|
|
|
|
@dataclass
|
|
class LinkCalculator:
|
|
terrain: TerrainService
|
|
canopy: CanopyService
|
|
buildings: BuildingService
|
|
|
|
async def calculate_link(
|
|
self,
|
|
from_pt: GeoAntenna,
|
|
to_pt: GeoAntenna,
|
|
frequency_mhz: float,
|
|
fresnel_clearance: float,
|
|
tx_power_dbm: float,
|
|
rx_sensitivity_dbm: float,
|
|
sample_step_m: float,
|
|
) -> LinkResponse:
|
|
total_distance = line_distance_m(from_pt.lat, from_pt.lng, to_pt.lat, to_pt.lng)
|
|
path_points = sample_line(
|
|
from_pt.lat,
|
|
from_pt.lng,
|
|
to_pt.lat,
|
|
to_pt.lng,
|
|
sample_step_m,
|
|
)
|
|
|
|
coordinates = [(lat, lng) for lat, lng, _ in path_points]
|
|
terrains = self.terrain.get_elevations(coordinates)
|
|
canopies = self.canopy.get_canopy_heights(coordinates)
|
|
building_heights = await self.buildings.get_max_heights(coordinates)
|
|
|
|
from_terrain = self.terrain.get_elevation(from_pt.lat, from_pt.lng)
|
|
to_terrain = self.terrain.get_elevation(to_pt.lat, to_pt.lng)
|
|
from_antenna_asl = from_terrain + from_pt.antenna_height_m
|
|
to_antenna_asl = to_terrain + to_pt.antenna_height_m
|
|
|
|
profile: list[PathProfilePoint] = []
|
|
obstacles: list[Obstacle] = []
|
|
los_clear = True
|
|
fresnel_clear = True
|
|
|
|
for index, (lat, lng, distance) in enumerate(path_points):
|
|
terrain_m = terrains[index]
|
|
canopy_m = canopies[index]
|
|
building_m = building_heights[index]
|
|
obstacle_height = max(canopy_m, building_m)
|
|
total_m = terrain_m + obstacle_height
|
|
los_height = los_height_at(distance, from_antenna_asl, to_antenna_asl, total_distance)
|
|
d1 = distance
|
|
d2 = max(total_distance - distance, 0.0)
|
|
fresnel_r = fresnel_radius_m(d1, d2, total_distance, frequency_mhz)
|
|
point_clearance = clearance_m(los_height, total_m, fresnel_r, fresnel_clearance)
|
|
|
|
if los_height < total_m:
|
|
los_clear = False
|
|
if point_clearance < 0:
|
|
fresnel_clear = False
|
|
obstacles.append(
|
|
Obstacle(
|
|
lat=round(lat, 6),
|
|
lng=round(lng, 6),
|
|
distance_from_start_m=round(distance, 1),
|
|
terrain_height_m=round(terrain_m, 1),
|
|
canopy_height_m=round(canopy_m, 1),
|
|
building_height_m=round(building_m, 1),
|
|
total_height_m=round(total_m, 1),
|
|
fresnel_radius_m=round(fresnel_r, 1),
|
|
clearance_m=round(point_clearance, 1),
|
|
)
|
|
)
|
|
|
|
profile.append(
|
|
PathProfilePoint(
|
|
distance_m=round(distance, 1),
|
|
lat=round(lat, 6),
|
|
lng=round(lng, 6),
|
|
terrain_m=round(terrain_m, 1),
|
|
canopy_m=round(canopy_m, 1),
|
|
building_m=round(building_m, 1),
|
|
total_m=round(total_m, 1),
|
|
los_height_m=round(los_height, 1),
|
|
fresnel_radius_m=round(fresnel_r, 1),
|
|
clearance_m=round(point_clearance, 1),
|
|
)
|
|
)
|
|
|
|
link_budget: LinkBudget = compute_link_budget(
|
|
tx_power_dbm,
|
|
rx_sensitivity_dbm,
|
|
total_distance,
|
|
frequency_mhz,
|
|
)
|
|
|
|
return LinkResponse(
|
|
distance_m=round(total_distance, 1),
|
|
los=los_clear,
|
|
fresnel_clear=fresnel_clear,
|
|
free_space_loss_db=round(fspl_db(total_distance, frequency_mhz), 1),
|
|
link_budget=link_budget,
|
|
obstacles=obstacles,
|
|
path_profile=profile,
|
|
)
|
|
|
|
async def elevation_profile(
|
|
self,
|
|
from_lat: float,
|
|
from_lng: float,
|
|
to_lat: float,
|
|
to_lng: float,
|
|
step_m: float,
|
|
) -> tuple[float, list[dict[str, float]]]:
|
|
total_distance = line_distance_m(from_lat, from_lng, to_lat, to_lng)
|
|
path_points = sample_line(from_lat, from_lng, to_lat, to_lng, step_m)
|
|
coordinates = [(lat, lng) for lat, lng, _ in path_points]
|
|
terrains = self.terrain.get_elevations(coordinates)
|
|
|
|
profile = [
|
|
{
|
|
"distance_m": round(distance, 1),
|
|
"lat": round(lat, 6),
|
|
"lng": round(lng, 6),
|
|
"terrain_m": round(terrains[index], 1),
|
|
}
|
|
for index, (lat, lng, distance) in enumerate(path_points)
|
|
]
|
|
return round(total_distance, 1), profile
|