From 6be98104044626cae8441f518ff91dd7031f5c8b Mon Sep 17 00:00:00 2001 From: grigo Date: Tue, 23 Jun 2026 11:49:32 +0300 Subject: [PATCH] added obstacles --- .../__pycache__/terrain.cpython-313.pyc | Bin 3355 -> 3616 bytes api/app/models/terrain.py | 6 +++++ .../__pycache__/terrain.cpython-313.pyc | Bin 6386 -> 7196 bytes api/app/services/terrain.py | 19 +++++++++++-- .../test_api.cpython-313-pytest-9.0.3.pyc | Bin 10804 -> 15251 bytes api/tests/test_api.py | 25 ++++++++++++++++++ 6 files changed, 48 insertions(+), 2 deletions(-) diff --git a/api/app/models/__pycache__/terrain.cpython-313.pyc b/api/app/models/__pycache__/terrain.cpython-313.pyc index 903934e3cec7f244a665991ac325de9c51b97d81..9208ca554c38e91461602504c2e506c49c396e2a 100644 GIT binary patch delta 510 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 diff --git a/api/app/services/terrain.py b/api/app/services/terrain.py index 3431d1a..779c53f 100644 --- a/api/app/services/terrain.py +++ b/api/app/services/terrain.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from app.config import get_settings from app.core import dem from app.core.diffraction import deygout -from app.core.fresnel import los_analysis +from app.core.fresnel import LosSample, los_analysis from app.core.geo import GeoPoint, PathPoint, linestring_geojson, sample_path from app.core.surface import build_surface_profile from app.models.terrain import ( @@ -102,19 +102,34 @@ def los( request.frequency_mhz * 1_000_000, ) - def convert(sample: object) -> Obstruction: + def convert(sample: LosSample) -> Obstruction: return Obstruction( distance_m=sample.distance_m, clearance_m=sample.clearance_m, + fresnel_radius_m=sample.fresnel_radius_m, + required_clearance_m=sample.required_clearance_m, type=sample.obstruction_type, ) + geometric_obstructions = [ + sample for sample in result.obstructions if sample.clearance_m < 0 + ] + building_obstructions = [ + sample + for sample in geometric_obstructions + if sample.obstruction_type == "building" + ] + return LosResponse( los_clear=result.los_clear, geometric_los=result.geometric_los, first_fresnel_clearance_pct=result.first_fresnel_clearance_pct, worst_obstruction=convert(result.worst_sample) if result.worst_sample else None, obstructions=[convert(sample) for sample in result.obstructions], + geometric_obstructions=[convert(sample) for sample in geometric_obstructions], + fresnel_violations_count=len(result.obstructions), + geometric_obstructions_count=len(geometric_obstructions), + building_obstructions_count=len(building_obstructions), diffraction_loss_db=diffraction_loss, profile_ref=f"inline:{profile_request.samples}", ) diff --git a/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc b/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc index 141eb890043bf0d5632d2d967c78cac3b1f8a063..b5c6efaf1f6df55d034b87d953fd5fdb22aca5e2 100644 GIT binary patch delta 2705 zcmcguPiz!b7@wJ)ot@p;zqH+!EiJpWv>giD$`%$1Ed>=81r(>d#egnLJ6#61v*pch z2@tYiOvHFVd>0ZgBx1A)F&Jv17h}BW!I+&E(#^($#)~}|i-0H>{NCHLZ6k>ujMLfQ z{J!7&-uHg*W%^~N2JtEPU6;$jz;!g%dqQ(wb~muEUv7`>a439$Il&`IaVS>GoWKOi zT)=FUxq*q4d4Sm|^QKu@`sCUh9@`{a<}`g0cX{hvlU&&{e)=f_k%#!8x0Zc~N4;OO z{WrtDsGZ%6M+42`^WZ~f&vfH1YdwBHFw9P3TjLI4h{$eyGT7`Y^CPKT3T*=gZO5k? z^X$=^zcjjeX@Dp@3GBi`@H=)G-ws}voFG`>ch@iBF0mEg2#L*Miy|mCMO3W7>|k4h zKd0hS++k7i;Hx!FtQW6lqjkQxrH55k99q%CZQ`K_TuKf0IS=9+7CXMWW-D8RO*Vvs zT#u_ZY}p_%jLs`{iXW%AICeW4d0kNI@q1F#)({t=)JlBP5oH5V0($~VW0<2;?8+*o z2|q8rz&8Ku+ymWlL0N4DN7)Ibo;-E#@Jiy4Tybs_TVdG`_E52u8{$p&0sKLMM#U|)x2E`*bQdxNqj%u{PtrA29tTf12rNqD@qla=rV$=|En z`dD4;W5?Hlk+lm(n*V2{gwx=<7=x=}Vjeh!Z*^fVwe$* zVfMS=w45(;`+W=}F+s@Go&R)_9hIYHMrIl2F#gmZ{O^u+$!^l%rQ&Clk>vwuE3BH1 z{dlJK;|Kc((jV^ucA$S?t{G(6f}Vy4dIq5EIIe2x0hi??AhOTCk!x=S&Qu?rp_Q5FHPv8In@^6Y> zz`j6x!I?c~DZh*2st!Bs69MXo9OJ|iE9xphdWiu&S zDLI_Y>p9X2)^>XAY{-sth;|DeZ6@nRA1{u` z22mXWKR|ialf6{dEjJ#^Ync&EPcJiC8ALCW`C=u*uBXvqh-DY)PKC<0E6s^4)ki=k z&6_yF2m>^$?mr|#0(x6YQtrXK>qkNlq@bH%O_Whc!QJ4;Q$QmC~QYAZFj zlp2~!^(*iA>~6=rmGL&tGfuB?J~kg!5a#kA3DgBYWU^-6%%(%yh+o7Q14m6r1H2?rBhn4HDj3!e{xyLI$YU zjcUHR7~O{S$ezl`3@u#EsuIYde!`~IRpb-J?Sp1C4rD4)jK*go38F<3rj(fF7w&0L zq%H_y|A||VgX(ZaoQhNhlN75&w;?+kukaaKxSCZZkV6e^Fs1N3!&!w=pt-m@Ivv(QAD>TkjM^bf(^Tq<)>wq9 z4Z0xJX{kTU;*deU63ng6guqYR1;Am5>2>p5XfI#r1&#&?(C-Ts*xXtIB7kgNY`$m| zgtC1+drC#gkz-?JF`d$MU>bTBM$m2oX#nlrM~@!;Z6-QHrh5!kH?*8Kq2*4KB53__ zA`4?FJ)g=9(snJ|&xy3eWjAx(>BAp){B%lx& zBoHT%z(2S3ut)HYU^gCUwBncALv^%!FSfB)NnR3PY~N;m9q8Vh-?YEUvOfHxJh8Q( zj2$IFPL1{>M3N>M6ea@gZQ^#Ym(QsE=xxx*w}}zO59MypJ%^ta?r-$7?KJx@SBun= delta 586 zcmYk2PiqrF7>6_KCQY_xlNKtnOG^rtx*jAZ#%>zhD&k*6tqEF-C2X5zxoIHqljPJXGxQB;lf4`S^XI311s_8$|X)OlN*5c~^ z+vJgcn)HvBZoR!O^RLu>xRkCX>nk3-~Vf z4S>u9)C*Y1dhBU9z%A^UK%8hhUe{^W_%#@^rlb<~rNa9%{fyJ+UFz)(>h0!G#@n3+ zZSkA9X_k3=t!{(Xd(@?&6S9uSZ{v6F0K|{X8DZG^++9rWA?_m{AU4ICeNlY`D(20C zcyBMxJ;wa+-$UC%JP}{*O56v{kLJs-2+4|ftG#R)yKTe{!Wa39p$$Oc9 None: assert response.status_code == 501 +def test_los_endpoint_returns_obstruction_summary_fields() -> None: + response = client.post( + "/api/v1/terrain/los", + json={ + "tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30}, + "rx": {"lat": 59.945, "lon": 30.325, "height_agl": 2}, + "frequency_mhz": 433, + "fresnel_clearance": 0.6, + "include_buildings": False, + "include_canopy": False, + "k_factor": 1.333, + "samples": 16, + }, + ) + assert response.status_code == 200 + data = response.json() + assert "obstructions" in data + assert "geometric_obstructions" in data + assert data["fresnel_violations_count"] == len(data["obstructions"]) + assert data["geometric_obstructions_count"] == len(data["geometric_obstructions"]) + assert data["building_obstructions_count"] == len( + [item for item in data["geometric_obstructions"] if item["type"] == "building"] + ) + + def test_manual_link_budget_endpoint() -> None: response = client.post( "/api/v1/link/budget",