diff --git a/API.md b/API.md new file mode 100644 index 0000000..b33057c --- /dev/null +++ b/API.md @@ -0,0 +1,455 @@ +# RF Propagation API + +Документ описывает текущий HTTP API сервиса. Базовый префикс API: `/api/v1`. + +Примеры ниже предполагают, что сервис опубликован на `http://localhost:5603`. +Если используется дефолтный порт compose, замени его на `8000`. + +## Состояние Реализации + +- Работает: healthcheck, OpenAPI, DEM elevation/profile, OSM buildings query, terrain LOS/Fresnel/diffraction с учётом DEM и buildings, manual link budget, antenna sector/beam helpers. +- Частично работает: coverage/viewshed создают job-stub, но тяжёлый расчёт ещё не реализован. +- Пока не реализовано: landcover/canopy sampling, 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`. + +## 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, + "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, + "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 + }' +``` + +Ключевые поля ответа: + +- `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. + +Поля 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 и заданными потерями по умолчанию `0`. + +```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` + +Модели `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` + +Контракт есть, но raster sampling landcover/canopy пока не реализован. Сейчас endpoint возвращает `501 Not Implemented`. + +```bash +curl -i -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 + }' +``` + +## 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/ | 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' +``` diff --git a/README.md b/README.md index 6becba8..8a8c74a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ connected. - `scripts/` - data bootstrap/import entry points. - `data/` - mounted data volume for DEM, landcover, and canopy rasters. - `docker-compose.yml` - local stack with API, worker, Redis, PostGIS, optional tiler. +- `API.md` - current HTTP endpoints, examples, and implementation status. ## Quick Start