generated from Grigo/AndroidTemplate
added subprox
This commit is contained in:
@@ -113,6 +113,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/elevation/grid?lat=&lon=&radius_m=200&step_m=0` — сетка высот для хитмапы (100–500 m, step_m=0 авто)
|
||||
- `GET /api/commands/pending?device_id=` — Android, доставка + `delivered_at`
|
||||
- `GET /api/commands?to_device_id=&limit=` — история (веб)
|
||||
|
||||
|
||||
Binary file not shown.
+112
-12
@@ -315,6 +315,117 @@ def _offset_m(lat: float, lon: float, north_m: float, east_m: float) -> tuple[fl
|
||||
return lat + dlat, lon + dlon
|
||||
|
||||
|
||||
_MAX_GRID_POINTS = 2500
|
||||
|
||||
|
||||
def _auto_step_m(radius_m: float) -> float:
|
||||
if radius_m <= 150:
|
||||
return 10.0
|
||||
if radius_m <= 300:
|
||||
return 15.0
|
||||
return 20.0
|
||||
|
||||
|
||||
def _sample_circular_grid(
|
||||
lat: float,
|
||||
lon: float,
|
||||
radius_m: float,
|
||||
step_m: float,
|
||||
) -> list[tuple[int, int, float, float, float]]:
|
||||
steps = int(radius_m / step_m)
|
||||
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)
|
||||
cells.append((i, j, la, lo, dist))
|
||||
return cells
|
||||
|
||||
|
||||
def _resolve_grid_step(lat: float, lon: float, radius_m: float, step_m: float) -> float:
|
||||
if step_m <= 0:
|
||||
step_m = _auto_step_m(radius_m)
|
||||
step_m = max(5.0, min(float(step_m), 100.0))
|
||||
while len(_sample_circular_grid(lat, lon, radius_m, step_m)) > _MAX_GRID_POINTS:
|
||||
step_m = math.ceil(step_m * 1.25)
|
||||
if step_m >= radius_m:
|
||||
break
|
||||
return step_m
|
||||
|
||||
|
||||
def build_elevation_grid(
|
||||
lat: float,
|
||||
lon: float,
|
||||
radius_m: float = 200.0,
|
||||
step_m: float = 0.0,
|
||||
) -> dict[str, Any]:
|
||||
"""Circular elevation grid for heatmap (delta relative to center)."""
|
||||
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(100.0, min(float(radius_m), 500.0))
|
||||
step_m = _resolve_grid_step(lat, lon, radius_m, step_m)
|
||||
|
||||
center_elev = fetch_elevation_m(lat, lon)
|
||||
if center_elev is None:
|
||||
return {"ok": False, "error": "no elevation at center"}
|
||||
|
||||
grid_cells = _sample_circular_grid(lat, lon, radius_m, step_m)
|
||||
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)
|
||||
|
||||
points: list[dict[str, Any]] = []
|
||||
deltas: list[float] = []
|
||||
for (i, j, la, lo, dist), elev in zip(grid_cells, elevations):
|
||||
if elev is None:
|
||||
continue
|
||||
delta = float(elev) - center_elev
|
||||
deltas.append(delta)
|
||||
points.append(
|
||||
{
|
||||
"i": i,
|
||||
"j": j,
|
||||
"lat": round(la, 6),
|
||||
"lon": round(lo, 6),
|
||||
"dist_m": round(dist, 1),
|
||||
"elevation_m": float(elev),
|
||||
"delta_m": round(delta, 1),
|
||||
}
|
||||
)
|
||||
|
||||
if not points:
|
||||
return {"ok": False, "error": "no elevation values in grid"}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"center": {
|
||||
"lat": round(lat, 6),
|
||||
"lon": round(lon, 6),
|
||||
"elevation_m": center_elev,
|
||||
},
|
||||
"radius_m": radius_m,
|
||||
"step_m": step_m,
|
||||
"points": points,
|
||||
"min_delta_m": round(min(deltas), 1),
|
||||
"max_delta_m": round(max(deltas), 1),
|
||||
"api_source": "elevation",
|
||||
"elevation_url": ELEVATION_API_URL,
|
||||
}
|
||||
|
||||
|
||||
def find_nearest_hill(
|
||||
lat: float,
|
||||
lon: float,
|
||||
@@ -339,18 +450,7 @@ def find_nearest_hill(
|
||||
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))
|
||||
|
||||
grid_cells = _sample_circular_grid(lat, lon, radius_m, step_m)
|
||||
if not grid_cells:
|
||||
return {"ok": False, "error": "empty search grid"}
|
||||
|
||||
|
||||
@@ -344,6 +344,18 @@ def elevation_nearest_hill(
|
||||
return find_nearest_hill(lat, lon, radius_m, step_m, min_prominence_m)
|
||||
|
||||
|
||||
@app.get("/api/elevation/grid")
|
||||
def elevation_grid(
|
||||
lat: float = Query(..., ge=-90.0, le=90.0),
|
||||
lon: float = Query(..., ge=-180.0, le=180.0),
|
||||
radius_m: float = Query(200.0, ge=100.0, le=500.0),
|
||||
step_m: float = Query(0.0, ge=0.0, le=100.0),
|
||||
):
|
||||
from core.elevation import build_elevation_grid
|
||||
|
||||
return build_elevation_grid(lat, lon, radius_m, step_m)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
from core.elevation import elevation_status
|
||||
|
||||
@@ -278,6 +278,19 @@ def elevation_nearest_hill():
|
||||
return jsonify(find_nearest_hill(lat, lon, radius_m, step_m, min_prominence_m))
|
||||
|
||||
|
||||
@app.get("/api/elevation/grid")
|
||||
def elevation_grid():
|
||||
from core.elevation import build_elevation_grid
|
||||
|
||||
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", 200, type=float)
|
||||
step_m = request.args.get("step_m", 0, type=float)
|
||||
return jsonify(build_elevation_grid(lat, lon, radius_m, step_m))
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
from core.elevation import elevation_status
|
||||
|
||||
Binary file not shown.
@@ -93,3 +93,36 @@ def test_find_nearest_hill_picks_nearest_peak(monkeypatch):
|
||||
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
|
||||
|
||||
|
||||
def test_build_elevation_grid_delta(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):
|
||||
return [100.0 + (la - 55.75) * 1000.0 for la, lo in zip(lats, lons)]
|
||||
|
||||
monkeypatch.setattr(elev, "fetch_elevation_m", lambda lat, lon: 100.0)
|
||||
monkeypatch.setattr(elev, "fetch_elevations_batch", fake_batch)
|
||||
|
||||
result = elev.build_elevation_grid(55.75, 37.62, radius_m=100, step_m=10)
|
||||
assert result["ok"] is True
|
||||
assert result["step_m"] == 10
|
||||
assert len(result["points"]) > 0
|
||||
assert result["min_delta_m"] <= 0 <= result["max_delta_m"]
|
||||
assert all("delta_m" in p for p in result["points"])
|
||||
|
||||
|
||||
def test_build_elevation_grid_limits_points(monkeypatch):
|
||||
monkeypatch.setattr(elev, "_probe_checked_at", 0.0)
|
||||
monkeypatch.setattr(elev, "probe_elevation_api", lambda force=False: {"ok": True, "error": None})
|
||||
monkeypatch.setattr(elev, "fetch_elevation_m", lambda lat, lon: 50.0)
|
||||
monkeypatch.setattr(
|
||||
elev,
|
||||
"fetch_elevations_batch",
|
||||
lambda lats, lons: [50.0] * len(lats),
|
||||
)
|
||||
|
||||
step = elev._resolve_grid_step(55.75, 37.62, 500.0, 5.0)
|
||||
cells = elev._sample_circular_grid(55.75, 37.62, 500.0, step)
|
||||
assert len(cells) <= elev._MAX_GRID_POINTS
|
||||
|
||||
Reference in New Issue
Block a user