Stahovani elevace
This commit is contained in:
@@ -1,30 +1,25 @@
|
||||
from aiohttp import ClientResponseError
|
||||
from sqlalchemy import select
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from decorators.retryable import retryable
|
||||
from external.elevation import elevation_api
|
||||
from external.gpx_parser import GPXParser
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from external.elevation import elevation_api, update_track_points_elevation
|
||||
|
||||
|
||||
@retryable
|
||||
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
coordinates = await gpx_parser.get_coordinates_all()
|
||||
|
||||
try:
|
||||
elevation = await elevation_api.get_elevation_for_points(coordinates)
|
||||
tree_with_elevation = gpx_parser.add_terrain_elevation(elevation)
|
||||
output_name = f"terrain_{gpx_filename}"
|
||||
gpx_parser.write(tree_with_elevation, f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
||||
|
||||
async with get_session() as db:
|
||||
await models.Flight.update(
|
||||
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True},
|
||||
id=flight_id
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
print(e)
|
||||
async def add_terrain_elevation_to_flight(flight_id: int):
|
||||
async with get_session() as db:
|
||||
track_points = (
|
||||
await db.scalars(
|
||||
select(models.TrackPoint)
|
||||
.join(models.TrackPoint.track)
|
||||
.join(models.Track.flight)
|
||||
.filter(models.Flight.id == flight_id))
|
||||
).all()
|
||||
try:
|
||||
await update_track_points_elevation(db, track_points)
|
||||
except ClientResponseError as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@retryable
|
||||
|
||||
@@ -133,6 +133,7 @@ class Track(BaseModel):
|
||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
flight: Mapped['Flight'] = relationship()
|
||||
# created_by: Mapped['User'] = relationship()
|
||||
|
||||
|
||||
@@ -149,6 +150,8 @@ class TrackPoint(BaseModel):
|
||||
altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
|
||||
track: Mapped['Track'] = relationship()
|
||||
|
||||
|
||||
class FlightPlanMarker(BaseModel):
|
||||
__tablename__ = "flight_plan_marker"
|
||||
@@ -427,7 +430,7 @@ class Flight(BaseModel):
|
||||
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
|
||||
created_by: Mapped['User'] = relationship()
|
||||
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||
|
||||
track: Mapped['Track'] = relationship()
|
||||
|
||||
class Copilot(BaseModel):
|
||||
__tablename__ = "copilot"
|
||||
|
||||
Vendored
+26
-17
@@ -1,13 +1,15 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
from typing import List, Dict
|
||||
from typing import Optional
|
||||
import aiohttp
|
||||
|
||||
from database import models
|
||||
from decorators.retryable import retryable
|
||||
from utils.list_utils import chunks
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ElevationInfo:
|
||||
id: Optional[int]
|
||||
lat: float
|
||||
lng: float
|
||||
elevation: float
|
||||
@@ -16,11 +18,11 @@ class ElevationInfo:
|
||||
class ElevationAPI:
|
||||
ELEVATION_ENDPOINT: str
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: List[Dict[str, float]]):
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
...
|
||||
|
||||
@retryable
|
||||
async def call_api(self, points: List[Dict[str, float]]):
|
||||
async def call_api(self, points: list[dict[str, float]]):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with self.get_request(session, points) as resp:
|
||||
resp.raise_for_status()
|
||||
@@ -30,44 +32,44 @@ class ElevationAPI:
|
||||
class OpenElevationAPI(ElevationAPI):
|
||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: List[Dict[str, float]]):
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
data = {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||
return session.post(self.ELEVATION_ENDPOINT, json=data)
|
||||
|
||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[ElevationInfo]:
|
||||
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||
response = await self.call_api(points)
|
||||
|
||||
return [
|
||||
ElevationInfo(lat=loc['latitude'], lng=loc['longitude'], elevation=loc['elevation'])
|
||||
for loc in response['results']
|
||||
]
|
||||
elevation = []
|
||||
for i, point in enumerate(points):
|
||||
response_point = response['results'][i]
|
||||
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
|
||||
return elevation
|
||||
|
||||
|
||||
class OpenMeteoElevationAPI(ElevationAPI):
|
||||
ELEVATION_ENDPOINT = "https://api.open-meteo.com/v1/elevation"
|
||||
|
||||
def get_request(self, session: aiohttp.ClientSession, points: List[Dict[str, float]]):
|
||||
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||
latitudes = ",".join(str(point['lat']) for point in points)
|
||||
longitudes = ",".join(str(point['lng']) for point in points)
|
||||
url = f"{self.ELEVATION_ENDPOINT}?latitude={latitudes}&longitude={longitudes}"
|
||||
|
||||
return session.get(url)
|
||||
|
||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[ElevationInfo]:
|
||||
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||
elevation = []
|
||||
for chunk in chunks(points, n=100):
|
||||
response = await self.call_api(chunk)
|
||||
|
||||
chunk_elevation = [
|
||||
ElevationInfo(**point, elevation=response['elevation'][i]) for i, point in enumerate(points)
|
||||
]
|
||||
chunk_elevation = []
|
||||
for i, point in enumerate(chunk):
|
||||
chunk_elevation.append(ElevationInfo(**point, elevation=response['elevation'][i]))
|
||||
|
||||
elevation += chunk_elevation
|
||||
return elevation
|
||||
|
||||
|
||||
class CombinedElevationAPI:
|
||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[ElevationInfo]:
|
||||
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||
for api in (OpenMeteoElevationAPI(), OpenElevationAPI()):
|
||||
elevation = await api.get_elevation_for_points(points)
|
||||
|
||||
@@ -75,4 +77,11 @@ class CombinedElevationAPI:
|
||||
return elevation
|
||||
|
||||
|
||||
async def update_track_points_elevation(db, track_points: list[models.TrackPoint]):
|
||||
points = [{"id": p.id, "lat": p.gps_latitude, "lng": p.gps_longitude} for p in track_points]
|
||||
elevation = await elevation_api.get_elevation_for_points(points)
|
||||
for point in elevation:
|
||||
await models.TrackPoint.update(db, {"terrain_elevation": point.elevation}, id=point.id)
|
||||
|
||||
|
||||
elevation_api = CombinedElevationAPI()
|
||||
|
||||
Vendored
-28
@@ -3,7 +3,6 @@ from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any
|
||||
from aiocache import cached
|
||||
from lxml import etree
|
||||
from lxml.etree import _ElementTree
|
||||
|
||||
|
||||
class GPXParser:
|
||||
@@ -136,30 +135,3 @@ class GPXParser:
|
||||
async def get_avg_altitude(self):
|
||||
altitudes = await self.get_altitude()
|
||||
return round(sum(altitudes) / len(altitudes), 2)
|
||||
|
||||
def add_terrain_elevation(self, points_with_elevation: List[Dict[str, float]]):
|
||||
track_points = self.run_xpath("//gpx:trkpt")
|
||||
# TODO: open elevation API umi jen presnost na 6 desetinnych mist!
|
||||
track_points_index = {(float(n.attrib['lat']), float(n.attrib['lon'])): n for n in track_points}
|
||||
|
||||
for point_with_elevation in points_with_elevation:
|
||||
lat = point_with_elevation['lat']
|
||||
lng = point_with_elevation['lng']
|
||||
elevation = point_with_elevation['elevation']
|
||||
target_node = track_points_index.get((lat, lng))
|
||||
|
||||
if target_node is None:
|
||||
continue
|
||||
|
||||
extensions = target_node.find("./extensions", self.gpx.getroot().nsmap)
|
||||
if extensions is not None:
|
||||
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>"))
|
||||
else:
|
||||
target_node.append(
|
||||
etree.XML(f"<extensions><terrain_elevation>{elevation}</terrain_elevation></extensions>")
|
||||
)
|
||||
|
||||
return self.gpx
|
||||
|
||||
def write(self, tree: _ElementTree, output: str):
|
||||
tree.write(output)
|
||||
|
||||
+13
-32
@@ -1,11 +1,13 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
from paths import FLIGHT_GPX_TRACK_PATH # noqa
|
||||
from database import async_session, models # noqa
|
||||
from external.elevation import elevation_api # noqa
|
||||
from external.elevation import elevation_api, update_track_points_elevation # noqa
|
||||
from external.gpx_parser import GPXParser # noqa
|
||||
from database.transaction import get_session
|
||||
|
||||
|
||||
async def add_elevation_to_photos():
|
||||
@@ -32,44 +34,23 @@ async def add_elevation_to_photos():
|
||||
|
||||
|
||||
async def add_elevation_to_tracks():
|
||||
async with async_session() as session:
|
||||
flights = (await session.scalars(
|
||||
select(models.Flight)
|
||||
.filter(models.Flight.has_terrain_elevation.is_(False))
|
||||
.filter(models.Flight.gpx_track_filename.isnot(None))
|
||||
async with get_session() as db:
|
||||
track_points = (await db.session.scalars(
|
||||
select(models.TrackPoint)
|
||||
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||
)).all()
|
||||
|
||||
if not flights:
|
||||
if not track_points:
|
||||
print("all done")
|
||||
return
|
||||
|
||||
for flight in flights:
|
||||
gpx_file = f"{FLIGHT_GPX_TRACK_PATH}/{flight.gpx_track_filename}"
|
||||
try:
|
||||
gpx = GPXParser(gpx_file)
|
||||
except OSError:
|
||||
print(f"ERROR, cannot open {gpx_file=}, skipping")
|
||||
continue
|
||||
|
||||
coordinates = await gpx.get_coordinates_all()
|
||||
elevation = await elevation_api.get_elevation_for_points(coordinates)
|
||||
|
||||
gpx_with_elevation = gpx.add_terrain_elevation(elevation)
|
||||
|
||||
output_name = f"terrain_{flight.gpx_track_filename[-30:]}"
|
||||
gpx.write(gpx_with_elevation, output=f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
||||
await models.Flight.update(
|
||||
db_session=session, obj=flight, data={
|
||||
"has_terrain_elevation": True,
|
||||
"gpx_track_filename": output_name
|
||||
}
|
||||
)
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
await update_track_points_elevation(db, track_points)
|
||||
|
||||
|
||||
async def run_all():
|
||||
await asyncio.gather(add_elevation_to_photos(), add_elevation_to_tracks())
|
||||
await asyncio.gather(
|
||||
add_elevation_to_photos(),
|
||||
add_elevation_to_tracks()
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user