503 lines
15 KiB
Markdown
503 lines
15 KiB
Markdown
# RF Propagation API
|
||
|
||
Документ описывает текущий HTTP API сервиса. Базовый префикс API: `/api/v1`.
|
||
|
||
Примеры ниже предполагают, что сервис опубликован на `http://localhost:5603`.
|
||
Если используется дефолтный порт compose, замени его на `8000`.
|
||
|
||
## Состояние Реализации
|
||
|
||
- Работает: healthcheck, OpenAPI, DEM elevation/profile, OSM buildings query, landcover/canopy path sampling, terrain LOS/Fresnel/diffraction с учётом DEM и buildings, manual link budget, antenna sector/beam helpers.
|
||
- Частично работает: coverage/viewshed создают job-stub, но тяжёлый расчёт ещё не реализован.
|
||
- Пока не реализовано: vegetation attenuation по реальным данным, ITM/P.1812/P.452/P.676, реальные async results для coverage/viewshed.
|
||
|
||
## Общие Правила
|
||
|
||
- Формат запросов и ответов: JSON.
|
||
- Координаты входа/выхода: WGS84, `lat`/`lon` в градусах.
|
||
- GeoJSON coordinates: стандартный порядок `[lon, lat]`.
|
||
- Высота антенн: `height_agl`, метры над землёй.
|
||
- Частота в HTTP API: `frequency_mhz`.
|
||
- DEM берётся из `DEM_PATH`, по умолчанию `/data/dem`.
|
||
- Buildings берутся из PostGIS table `buildings`.
|
||
- Vegetation loss считается по формуле P.833 с коэффициентами из env:
|
||
`P833_GAMMA_DB_PER_M` и `P833_MAX_ATTENUATION_DB`.
|
||
|
||
## Health
|
||
|
||
### `GET /health`
|
||
|
||
Проверка, что API поднят.
|
||
|
||
```bash
|
||
curl -s http://localhost:5603/health | jq
|
||
```
|
||
|
||
Ответ:
|
||
|
||
```json
|
||
{
|
||
"status": "ok"
|
||
}
|
||
```
|
||
|
||
## OpenAPI
|
||
|
||
### `GET /openapi.json`
|
||
|
||
Машиночитаемая схема FastAPI.
|
||
|
||
```bash
|
||
curl -s http://localhost:5603/openapi.json | jq '{title: .info.title, paths: (.paths | keys)}'
|
||
```
|
||
|
||
## Terrain
|
||
|
||
### `GET /api/v1/terrain/elevation`
|
||
|
||
Возвращает высоту DEM в точке.
|
||
|
||
Query parameters:
|
||
|
||
- `lat`: широта.
|
||
- `lon`: долгота.
|
||
- `surface`: `dtm` или `dsm`. Сейчас оба читаются из DEM-растра; отдельный DSM-слой ещё не подключён.
|
||
|
||
```bash
|
||
curl -s "http://localhost:5603/api/v1/terrain/elevation?lat=59.9386&lon=30.3141&surface=dtm" | jq
|
||
```
|
||
|
||
Пример ответа:
|
||
|
||
```json
|
||
{
|
||
"lat": 59.9386,
|
||
"lon": 30.3141,
|
||
"elevation_m": 3.099534749984741,
|
||
"surface": "dtm"
|
||
}
|
||
```
|
||
|
||
### `POST /api/v1/terrain/profile`
|
||
|
||
Строит геодезический профиль между двумя точками. Если `include_buildings=true`, точки профиля, попавшие внутрь OSM building polygon, получают `building_m`.
|
||
|
||
Тело запроса:
|
||
|
||
```json
|
||
{
|
||
"start": {"lat": 59.935, "lon": 30.305},
|
||
"end": {"lat": 59.945, "lon": 30.325},
|
||
"samples": 64,
|
||
"include_buildings": true,
|
||
"include_canopy": false,
|
||
"k_factor": 1.333
|
||
}
|
||
```
|
||
|
||
Пример:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/terrain/profile \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"start": {"lat": 59.935, "lon": 30.305},
|
||
"end": {"lat": 59.945, "lon": 30.325},
|
||
"samples": 64,
|
||
"include_buildings": true,
|
||
"include_canopy": false,
|
||
"k_factor": 1.333
|
||
}' | jq '.distance_m, [.samples[] | select(.building_m > 0)] | length'
|
||
```
|
||
|
||
Ключевые поля ответа:
|
||
|
||
- `distance_m`: длина трассы.
|
||
- `samples`: массив точек профиля.
|
||
- `ground_m`: высота DEM.
|
||
- `building_m`: высота здания над землёй.
|
||
- `canopy_m`: высота кроны, пока всегда `0`.
|
||
- `surface_m`: `ground_m + max(building_m, canopy_m)`.
|
||
- `path_geojson`: GeoJSON LineString.
|
||
|
||
### `POST /api/v1/terrain/los`
|
||
|
||
Проверяет 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": true,
|
||
"include_canopy": false,
|
||
"include_vegetation": true,
|
||
"k_factor": 1.333,
|
||
"samples": 128
|
||
}
|
||
```
|
||
|
||
Пример:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/terrain/los \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"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": true,
|
||
"include_canopy": false,
|
||
"include_vegetation": true,
|
||
"k_factor": 1.333,
|
||
"samples": 128
|
||
}' | jq '{
|
||
los_clear,
|
||
geometric_los,
|
||
fresnel_violations_count,
|
||
geometric_obstructions_count,
|
||
building_obstructions_count,
|
||
worst_obstruction,
|
||
diffraction_loss_db,
|
||
vegetation_loss_db
|
||
}'
|
||
```
|
||
|
||
Ключевые поля ответа:
|
||
|
||
- `los_clear`: `true`, если нет нарушений требуемого Fresnel clearance.
|
||
- `geometric_los`: `true`, если геометрическая линия TX-RX не пересекает поверхность.
|
||
- `first_fresnel_clearance_pct`: минимальный процент доступного просвета относительно требуемого `fresnel_clearance * F1`.
|
||
- `obstructions`: все точки, где `clearance_m < required_clearance_m`.
|
||
- `geometric_obstructions`: только точки, где `clearance_m < 0`.
|
||
- `fresnel_violations_count`: количество нарушений зоны Френеля.
|
||
- `geometric_obstructions_count`: количество геометрических пересечений.
|
||
- `building_obstructions_count`: количество геометрических препятствий типа `building`.
|
||
- `worst_obstruction`: худшая точка по запасу относительно требуемого Fresnel clearance.
|
||
- `diffraction_loss_db`: текущая Bullington-style оценка knife-edge loss.
|
||
- `vegetation_loss_db`: P.833 attenuation по `vegetation_depth_m` из WorldCover, если `include_vegetation=true`.
|
||
|
||
Поля obstruction:
|
||
|
||
```json
|
||
{
|
||
"distance_m": 1031.5257643738905,
|
||
"clearance_m": -11.086004102305118,
|
||
"fresnel_radius_m": 15.730079622922505,
|
||
"required_clearance_m": 9.438047773753503,
|
||
"type": "building"
|
||
}
|
||
```
|
||
|
||
## Buildings
|
||
|
||
### `POST /api/v1/buildings/query`
|
||
|
||
Возвращает OSM buildings из PostGIS как GeoJSON FeatureCollection.
|
||
|
||
Вариант по bbox:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/buildings/query \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"bbox":[30.30,59.93,30.33,59.95]}' | jq '.features | length'
|
||
```
|
||
|
||
Вариант по линии с буфером:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/buildings/query \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"path": {
|
||
"type": "LineString",
|
||
"coordinates": [[30.305, 59.935], [30.325, 59.945]]
|
||
},
|
||
"buffer_m": 50
|
||
}' | jq '.features | length'
|
||
```
|
||
|
||
Feature properties:
|
||
|
||
- `height_m`: высота здания в метрах.
|
||
- `levels`: этажность, если известна.
|
||
- `building_type`: значение OSM tag `building`.
|
||
- `source`: `measured`, если был tag `height`; иначе `estimated`.
|
||
|
||
Высота считается при импорте:
|
||
|
||
- `height`, если есть.
|
||
- иначе `building:levels * 3.0`.
|
||
- иначе дефолт по типу здания.
|
||
|
||
## Link Budget
|
||
|
||
### `POST /api/v1/link/budget`
|
||
|
||
Считает point-to-point budget. Сейчас рабочий режим: `model="manual"`.
|
||
Он учитывает FSPL, terrain/buildings diffraction по DEM/PostGIS profile,
|
||
и vegetation attenuation по WorldCover при `include_vegetation=true`.
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/link/budget \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"tx": {"lat": 60.17, "lon": 24.94, "height_agl": 30, "power_dbm": 37, "gain_dbi": 8},
|
||
"rx": {"lat": 60.25, "lon": 25.10, "height_agl": 2, "gain_dbi": 2, "sensitivity_dbm": -110},
|
||
"frequency_mhz": 433,
|
||
"model": "manual",
|
||
"include_buildings": true,
|
||
"include_vegetation": true,
|
||
"k_factor": 1.333
|
||
}' | jq
|
||
```
|
||
|
||
Ключевые поля ответа:
|
||
|
||
- `distance_km`
|
||
- `fspl_db`
|
||
- `diffraction_db`
|
||
- `vegetation_db`
|
||
- `atmospheric_db`
|
||
- `total_loss_db`
|
||
- `rx_power_dbm`
|
||
- `fade_margin_db`
|
||
- `fresnel_clear`
|
||
- `link_viable`
|
||
|
||
При `include_vegetation=true` API сэмплит WorldCover вдоль трассы и добавляет
|
||
P.833 vegetation attenuation в `vegetation_db`. Если landcover raster отсутствует,
|
||
значение остаётся `0`, чтобы link budget продолжал работать.
|
||
|
||
При `include_buildings=true` API также строит terrain surface profile с DEM и
|
||
OSM buildings, считает Fresnel/LOS и добавляет Bullington-style diffraction loss
|
||
в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа.
|
||
|
||
Модели `p452` и `itm` пока возвращают `501 Not Implemented`.
|
||
|
||
## Antenna
|
||
|
||
### `POST /api/v1/antenna/pattern`
|
||
|
||
Нормализует antenna pattern. Сейчас поддержаны `omni` и простая `sector`-модель.
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/antenna/pattern \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"pattern": "sector",
|
||
"azimuth_deg": 90,
|
||
"tilt_deg": 0,
|
||
"gain_dbi": 8,
|
||
"beamwidth_h": 65,
|
||
"beamwidth_v": 15
|
||
}' | jq '.samples[:5]'
|
||
```
|
||
|
||
`pattern="file"` пока не реализован.
|
||
|
||
### `POST /api/v1/antenna/beam`
|
||
|
||
Возвращает GeoJSON polygon для визуализации сектора/луча.
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/antenna/beam \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"origin": {"lat": 59.935, "lon": 30.305},
|
||
"radius_m": 5000,
|
||
"antenna": {
|
||
"pattern": "sector",
|
||
"azimuth_deg": 90,
|
||
"tilt_deg": 0,
|
||
"gain_dbi": 8,
|
||
"beamwidth_h": 65,
|
||
"beamwidth_v": 15
|
||
}
|
||
}' | jq '.geojson.geometry.type'
|
||
```
|
||
|
||
## Landcover
|
||
|
||
### `POST /api/v1/landcover/path`
|
||
|
||
Сэмплит landcover raster вдоль трассы и сворачивает точки в сегменты.
|
||
Canopy raster используется опционально: если файлов нет, endpoint всё равно
|
||
работает, но `canopy_height_m` будет `null`.
|
||
|
||
Ожидаемые данные:
|
||
|
||
- `LANDCOVER_PATH`: GeoTIFF/COG с кодами ESA WorldCover.
|
||
- `CANOPY_PATH`: GeoTIFF/COG с высотой кроны в метрах.
|
||
|
||
Поддерживаемые классы ESA WorldCover:
|
||
|
||
- `10`: `tree_cover`
|
||
- `20`: `shrubland`
|
||
- `30`: `grassland`
|
||
- `40`: `cropland`
|
||
- `50`: `built_up`
|
||
- `60`: `bare_sparse_vegetation`
|
||
- `70`: `snow_ice`
|
||
- `80`: `water`
|
||
- `90`: `herbaceous_wetland`
|
||
- `95`: `mangroves`
|
||
- `100`: `moss_lichen`
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/landcover/path \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"start": {"lat": 59.935, "lon": 30.305},
|
||
"end": {"lat": 59.945, "lon": 30.325},
|
||
"samples": 256
|
||
}' | jq
|
||
```
|
||
|
||
Ключевые поля ответа:
|
||
|
||
- `segments`: участки с одинаковым landcover class.
|
||
- `segments[].class`: класс покрова.
|
||
- `segments[].forest_type`: пока `unknown` для `tree_cover`/`mangroves`, `null` для остальных классов.
|
||
- `segments[].canopy_height_m`: средняя высота кроны на сегменте, если canopy raster доступен.
|
||
- `vegetation_depth_m`: суммарная длина участков `tree_cover`/`mangroves` по трассе.
|
||
|
||
Если landcover raster отсутствует или не покрывает трассу, endpoint возвращает `501 Not Implemented`.
|
||
|
||
## Viewshed
|
||
|
||
### `POST /api/v1/viewshed`
|
||
|
||
Создаёт job-stub. Реальный WhiteboxTools/GDAL viewshed ещё не подключён.
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/viewshed \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"observer": {"lat": 59.935, "lon": 30.305, "height_agl": 30},
|
||
"radius_m": 15000,
|
||
"target_height_agl": 2,
|
||
"surface": "dsm",
|
||
"k_factor": 1.333,
|
||
"format": "geojson"
|
||
}' | jq
|
||
```
|
||
|
||
Ответ:
|
||
|
||
```json
|
||
{
|
||
"job_id": "..."
|
||
}
|
||
```
|
||
|
||
## Coverage
|
||
|
||
### `POST /api/v1/coverage`
|
||
|
||
Создаёт job-stub. Реальный radial coverage расчёт ещё не подключён.
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:5603/api/v1/coverage \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30, "power_dbm": 40, "frequency_mhz": 433},
|
||
"antenna": {"pattern": "omni", "azimuth_deg": 0, "tilt_deg": 0, "gain_dbi": 8},
|
||
"rx": {"height_agl": 2, "sensitivity_dbm": -110, "gain_dbi": 2},
|
||
"model": "fspl",
|
||
"environment": "urban",
|
||
"radius_m": 20000,
|
||
"azimuth_step_deg": 1,
|
||
"range_step_m": 30,
|
||
"include_buildings": true,
|
||
"include_vegetation": false,
|
||
"levels_dbm": [-90, -100, -110],
|
||
"format": "geojson"
|
||
}' | jq
|
||
```
|
||
|
||
Ответ:
|
||
|
||
```json
|
||
{
|
||
"job_id": "..."
|
||
}
|
||
```
|
||
|
||
## Jobs
|
||
|
||
### `GET /api/v1/jobs/{job_id}`
|
||
|
||
Возвращает статус job, если job был создан текущим процессом API. Сейчас job storage in-memory, поэтому после рестарта API ранее созданные job исчезают.
|
||
|
||
```bash
|
||
curl -s http://localhost:5603/api/v1/jobs/<job_id> | jq
|
||
```
|
||
|
||
Статусы:
|
||
|
||
- `queued`
|
||
- `running`
|
||
- `done`
|
||
- `error`
|
||
|
||
## Данные И Обслуживание
|
||
|
||
### DEM
|
||
|
||
Загрузка DEM для СПб и Ленобласти:
|
||
|
||
```bash
|
||
python scripts/bootstrap_dem.py --bbox 27.3,58.4,35.8,61.4 --output-dir data/dem
|
||
docker compose restart api worker
|
||
```
|
||
|
||
### Buildings
|
||
|
||
Загрузка OSM PBF:
|
||
|
||
```bash
|
||
mkdir -p data/osm
|
||
wget -O data/osm/northwestern-fed-district-latest.osm.pbf \
|
||
https://download.geofabrik.de/russia/northwestern-fed-district-latest.osm.pbf
|
||
sh scripts/load_buildings.sh data/osm/northwestern-fed-district-latest.osm.pbf
|
||
docker compose restart api worker
|
||
```
|
||
|
||
Проверка количества зданий:
|
||
|
||
```bash
|
||
docker compose exec -T postgis psql -U radio -d radio \
|
||
-c "SELECT count(*) FROM buildings;"
|
||
```
|
||
|
||
## Частые Диагностические Команды
|
||
|
||
Проверить, что API видит PostGIS:
|
||
|
||
```bash
|
||
docker compose exec api python -c "
|
||
from sqlalchemy import create_engine, text
|
||
from app.config import get_settings
|
||
e = create_engine(get_settings().database_url)
|
||
with e.connect() as c:
|
||
print(c.execute(text('SELECT count(*) FROM buildings')).scalar())
|
||
"
|
||
```
|
||
|
||
Проверить, что DEM смонтирован в API container:
|
||
|
||
```bash
|
||
docker compose exec api sh -lc 'ls -lh /data/dem | head'
|
||
```
|
||
|
||
Проверить список путей OpenAPI:
|
||
|
||
```bash
|
||
curl -s http://localhost:5603/openapi.json | jq '.paths | keys'
|
||
```
|