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