added subproxy

This commit is contained in:
2026-06-11 09:09:28 +03:00
parent 17d383ddc6
commit 94e2b772e8
17 changed files with 573 additions and 1 deletions
+1
View File
@@ -112,6 +112,7 @@ curl http://127.0.0.1:7634/api/health
- `POST /api/elevation/profile``{points: [{lat, lon}], step_m?: 10}` → срез рельефа (локальный Open-Meteo)
- `GET /api/tracks/{id}/elevation-profile?step_m=10` — то же по сохранённому треку
- `GET /api/elevation/nearest-hill?lat=&lon=&radius_m=5000` — ближайшая возвышенность (прокси Open-Meteo)
- `GET /api/commands/pending?device_id=` — Android, доставка + `delivered_at`
- `GET /api/commands?to_device_id=&limit=` — история (веб)
Binary file not shown.
+126
View File
@@ -307,3 +307,129 @@ def build_elevation_profile(
if not elev_vals:
result["api_error"] = "elevation API returned no values"
return result
def _offset_m(lat: float, lon: float, north_m: float, east_m: float) -> tuple[float, float]:
dlat = north_m / 111_320.0
dlon = east_m / (111_320.0 * max(math.cos(math.radians(lat)), 1e-6))
return lat + dlat, lon + dlon
def find_nearest_hill(
lat: float,
lon: float,
radius_m: float = 5000.0,
step_m: float = 300.0,
min_prominence_m: float = 8.0,
) -> dict[str, Any]:
"""Find nearest local elevation maximum around a point."""
probe = probe_elevation_api()
if not probe["ok"]:
return {
"ok": False,
"error": f"elevation API unreachable: {probe['error']}",
"elevation_url": ELEVATION_API_URL,
}
radius_m = max(500.0, min(float(radius_m), 15_000.0))
step_m = max(100.0, min(float(step_m), 500.0))
min_prominence_m = max(3.0, min(float(min_prominence_m), 100.0))
center_elev = fetch_elevation_m(lat, lon)
if center_elev is None:
return {"ok": False, "error": "no elevation at center"}
steps = int(radius_m / step_m)
grid_cells: list[tuple[int, int, float, float, float]] = []
for i in range(-steps, steps + 1):
for j in range(-steps, steps + 1):
north = i * step_m
east = j * step_m
dist = math.hypot(north, east)
if dist > radius_m:
continue
la, lo = _offset_m(lat, lon, north, east)
grid_cells.append((i, j, la, lo, dist))
if not grid_cells:
return {"ok": False, "error": "empty search grid"}
lats = [c[2] for c in grid_cells]
lons = [c[3] for c in grid_cells]
elevations = fetch_elevations_batch(lats, lons)
grid: dict[tuple[int, int], dict[str, Any]] = {}
for (i, j, la, lo, dist), elev in zip(grid_cells, elevations):
grid[(i, j)] = {
"lat": round(la, 6),
"lon": round(lo, 6),
"dist_m": round(dist, 1),
"elevation_m": elev,
}
def is_local_max(i: int, j: int, elev: float) -> bool:
for di in (-1, 0, 1):
for dj in (-1, 0, 1):
if di == 0 and dj == 0:
continue
n = grid.get((i + di, j + dj))
if n and n["elevation_m"] is not None and n["elevation_m"] >= elev:
return False
return True
candidates: list[dict[str, Any]] = []
for (i, j), cell in grid.items():
elev = cell.get("elevation_m")
if elev is None:
continue
prominence = float(elev) - center_elev
if prominence < min_prominence_m:
continue
if is_local_max(i, j, float(elev)):
candidates.append({**cell, "prominence_m": round(prominence, 1)})
if not candidates:
best = None
for cell in grid.values():
elev = cell.get("elevation_m")
if elev is None:
continue
prominence = float(elev) - center_elev
if prominence < min_prominence_m * 0.5:
continue
if best is None or cell["dist_m"] < best["dist_m"]:
best = {
**cell,
"prominence_m": round(prominence, 1),
"is_local_max": False,
}
if best is None:
return {
"ok": False,
"error": "no hill found in radius",
"center": {
"lat": round(lat, 6),
"lon": round(lon, 6),
"elevation_m": center_elev,
},
"radius_m": radius_m,
}
hill = best
else:
candidates.sort(key=lambda c: c["dist_m"])
hill = {**candidates[0], "is_local_max": True}
return {
"ok": True,
"center": {
"lat": round(lat, 6),
"lon": round(lon, 6),
"elevation_m": center_elev,
},
"hill": hill,
"candidates": len(candidates),
"radius_m": radius_m,
"step_m": step_m,
"api_source": "elevation",
"elevation_url": ELEVATION_API_URL,
}
+13
View File
@@ -331,6 +331,19 @@ def track_elevation_profile(
return build_elevation_profile(track.get("points") or [], step_m)
@app.get("/api/elevation/nearest-hill")
def elevation_nearest_hill(
lat: float = Query(..., ge=-90.0, le=90.0),
lon: float = Query(..., ge=-180.0, le=180.0),
radius_m: float = Query(5000.0, ge=500.0, le=15000.0),
step_m: float = Query(300.0, ge=100.0, le=500.0),
min_prominence_m: float = Query(8.0, ge=3.0, le=100.0),
):
from core.elevation import find_nearest_hill
return find_nearest_hill(lat, lon, radius_m, step_m, min_prominence_m)
@app.get("/api/health")
def health():
from core.elevation import elevation_status
+14
View File
@@ -264,6 +264,20 @@ def track_elevation_profile(track_id: int):
return jsonify(build_elevation_profile(points, step_m or 10.0))
@app.get("/api/elevation/nearest-hill")
def elevation_nearest_hill():
from core.elevation import find_nearest_hill
lat = request.args.get("lat", type=float)
lon = request.args.get("lon", type=float)
if lat is None or lon is None:
return jsonify({"ok": False, "error": "lat and lon required"}), 400
radius_m = request.args.get("radius_m", 5000, type=float)
step_m = request.args.get("step_m", 300, type=float)
min_prominence_m = request.args.get("min_prominence_m", 8, type=float)
return jsonify(find_nearest_hill(lat, lon, radius_m, step_m, min_prominence_m))
@app.get("/api/health")
def health():
from core.elevation import elevation_status
+31
View File
@@ -62,3 +62,34 @@ def test_build_profile_reports_unreachable(monkeypatch):
assert profile["points"] == []
assert "unreachable" in profile["api_error"]
def test_find_nearest_hill_unreachable(monkeypatch):
monkeypatch.setattr(
elev,
"probe_elevation_api",
lambda force=False: {"ok": False, "url": elev.ELEVATION_API_URL, "error": "down"},
)
result = elev.find_nearest_hill(55.75, 37.62)
assert result["ok"] is False
def test_find_nearest_hill_picks_nearest_peak(monkeypatch):
monkeypatch.setattr(elev, "_probe_checked_at", 0.0)
monkeypatch.setattr(elev, "probe_elevation_api", lambda force=False: {"ok": True, "error": None})
def fake_batch(lats, lons):
out = []
for la, lo in zip(lats, lons):
if abs(la - 55.75) < 1e-4 and abs(lo - 37.62) < 1e-4:
out.append(100.0)
elif la > 55.75:
out.append(130.0)
else:
out.append(95.0)
return out
monkeypatch.setattr(elev, "fetch_elevations_batch", fake_batch)
result = elev.find_nearest_hill(55.75, 37.62, radius_m=2000, step_m=300, min_prominence_m=8)
assert result["ok"] is True
assert result["hill"]["elevation_m"] >= 120.0