Stahovani pocasi, drobne upravy

This commit is contained in:
Michal Kváček
2024-05-27 22:30:44 +02:00
parent 94613a5b1b
commit ba5e73108f
13 changed files with 293 additions and 26 deletions
+2
View File
@@ -10,3 +10,5 @@ uploads/*
!uploads/.gitkeep !uploads/.gitkeep
src/emails/templates/html/* src/emails/templates/html/*
.env .env
/static/airports/
/static/airports/.gitkeep
@@ -0,0 +1,30 @@
"""add weather info to flight plan markers
Revision ID: 8a67af3909aa
Revises: 790ba00fc021
Create Date: 2024-05-16 06:19:13.112911
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8a67af3909aa'
down_revision = '790ba00fc021'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_plan_marker', sa.Column('weather_info_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'flight_plan_marker', 'weather_info', ['weather_info_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
op.drop_column('flight_plan_marker', 'weather_info_id')
# ### end Alembic commands ###
+27 -7
View File
@@ -6,19 +6,14 @@ from external.weather import weather_api
from logger import log from logger import log
async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']): async def get_weather(date_time: datetime, gps: tuple[float, float]):
async with get_session() as db:
airport = await models.Airport.get_one(db, id=airport_id)
gps = (airport.gps_latitude, airport.gps_longitude)
try: try:
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps) weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
log.warning(weather)
except Exception as e: except Exception as e:
log.error(f"Error in downloading weather: {e}") log.error(f"Error in downloading weather: {e}")
return None return None
data = { return {
"datetime": weather['datetime'], "datetime": weather['datetime'],
"qnh": weather['pressure_msl'], "qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'], "temperature_surface": weather['temperature_2m'],
@@ -30,6 +25,14 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
"wind_direction_surface": weather['winddirection_10m'], "wind_direction_surface": weather['winddirection_10m'],
} }
async def download_weather_for_flight(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
async with get_session() as db:
airport = await models.Airport.get_one(db, id=airport_id)
gps = (airport.gps_latitude, airport.gps_longitude)
data = await get_weather(date_time, gps)
async with get_session() as db: async with get_session() as db:
flight = await models.Flight.get_one(db, id=flight_id) flight = await models.Flight.get_one(db, id=flight_id)
existing_weather_id = getattr(flight, f'{type_}_weather_info_id') existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
@@ -39,3 +42,20 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
else: else:
weather_model = await models.WeatherInfo.create(db, data=data) weather_model = await models.WeatherInfo.create(db, data=data)
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id}) await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
async def download_weather_for_plan(date_time: datetime, flight_plan_id: int, gps: tuple[float, float]):
data = await get_weather(date_time, gps)
async with get_session() as db:
plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
if plan.weather_info_id:
await models.WeatherInfo.update(db, id=plan.weather_info.id, data=data)
else:
weather_model = await models.WeatherInfo.create(db, data=data)
await models.FlightPlan.update(
db,
obj=plan,
data={"weather_info_id": weather_model.id}
)
+1 -1
View File
@@ -1,6 +1,6 @@
import os import os
APP_DEBUG = True APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
GRAPHIQL = True GRAPHIQL = True
REFRESH_TOKEN_VALIDITY_DAYS = 30 REFRESH_TOKEN_VALIDITY_DAYS = 30
+2 -2
View File
@@ -128,7 +128,7 @@ class FlightPlanMarker(BaseModel):
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0) position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False) gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False) gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
class Airport(BaseModel): class Airport(BaseModel):
__tablename__ = "airport" __tablename__ = "airport"
@@ -145,7 +145,7 @@ class Airport(BaseModel):
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa
runways: Mapped[list['Runway']] = relationship() runways: Mapped[list['Runway']] = relationship('Runway')
frequencies: Mapped[list['Frequency']] = relationship() frequencies: Mapped[list['Frequency']] = relationship()
created_by: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship()
+2
View File
@@ -33,6 +33,8 @@ class LoginEndpoint(AuthEndpoint):
access_token = self.access_security.create_access_token(subject=subject) access_token = self.access_security.create_access_token(subject=subject)
refresh_token = self.refresh_security.create_refresh_token(subject=subject) refresh_token = self.refresh_security.create_refresh_token(subject=subject)
# TODO: SameSite na refresh_token_cookie!
# https://medium.com/swlh/how-the-new-chrome-80-cookie-rule-samesite-none-secure-affects-web-development-c06380220ced
# self.access_security.set_access_cookie(resp, access_token) # self.access_security.set_access_cookie(resp, access_token)
self.refresh_security.set_refresh_cookie( self.refresh_security.set_refresh_cookie(
resp, refresh_token, resp, refresh_token,
-1
View File
@@ -52,7 +52,6 @@ class Weather:
data = await self.download_weather_for_day(date_time.date(), gps) data = await self.download_weather_for_day(date_time.date(), gps)
# TODO: kontrola timezone! # TODO: kontrola timezone!
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
idx = date_time.hour idx = date_time.hour
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS} result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M") result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
@@ -4,7 +4,7 @@ from sqlalchemy import delete, insert, select, func, text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_flight from background_jobs.elevation import add_terrain_elevation_to_flight
from background_jobs.weather import download_weather from background_jobs.weather import download_weather_for_flight
from database import models from database import models
from database.models import flight_has_copilot from database.models import flight_has_copilot
from database.transaction import get_session from database.transaction import get_session
@@ -155,12 +155,12 @@ class FlightMutationResolver(BaseMutationResolver):
flight = await self._do_create(db, data) flight = await self._do_create(db, data)
context.background_tasks.add_task( context.background_tasks.add_task(
download_weather, download_weather_for_flight,
flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime, flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime,
type_="takeoff" type_="takeoff"
) )
context.background_tasks.add_task( context.background_tasks.add_task(
download_weather, download_weather_for_flight,
flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime, flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime,
type_="landing" type_="landing"
) )
@@ -193,7 +193,7 @@ class FlightMutationResolver(BaseMutationResolver):
data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime'] data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime']
context.background_tasks.add_task( context.background_tasks.add_task(
download_weather, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'], download_weather_for_flight, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'],
type_="takeoff" type_="takeoff"
) )
@@ -207,7 +207,7 @@ class FlightMutationResolver(BaseMutationResolver):
data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime'] data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime']
context.background_tasks.add_task( context.background_tasks.add_task(
download_weather, flight_id=id, airport_id=landing_airport_id, date_time=data['landing_datetime'], download_weather_for_flight, flight_id=id, airport_id=landing_airport_id, date_time=data['landing_datetime'],
type_="landing" type_="landing"
) )
+4 -9
View File
@@ -24,7 +24,7 @@ from graphql_schema.dataloaders.single_model import (
) )
from graphql_schema.permissions import IsAuthenticated from graphql_schema.permissions import IsAuthenticated
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH, airport_icon_url
@strawberry.type @strawberry.type
@@ -67,14 +67,9 @@ class Airport:
reporting_points: List[ReportingPoint] = strawberry.field( reporting_points: List[ReportingPoint] = strawberry.field(
resolver=lambda root: reporting_points_dataloader.load(root.id) resolver=lambda root: reporting_points_dataloader.load(root.id)
) )
runways: List[Runway] = strawberry.field( runways: List[Runway] = strawberry.field(resolver=lambda root: runways_dataloader.load(root.id))
resolver=lambda root: runways_dataloader.load(root.id) frequencies: List[Frequency] = strawberry.field(resolver=lambda root: frequencies_dataloader.load(root.id))
) icon_url: str = strawberry.field(resolver=lambda root: airport_icon_url(root.icao_code))
frequencies: List[Frequency] = strawberry.field(
resolver=lambda root: frequencies_dataloader.load(root.id)
)
# TODO: loader na ikonu
@strawberry.type @strawberry.type
+2 -1
View File
@@ -9,7 +9,7 @@ from starlette.background import BackgroundTasks
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.responses import Response, JSONResponse from starlette.responses import Response, JSONResponse
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS, APP_DEBUG
from endpoints.contact import ContactEndpoint, ContactInput from endpoints.contact import ContactEndpoint, ContactInput
from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword
from endpoints.graphql import setup_graphql_endpoint from endpoints.graphql import setup_graphql_endpoint
@@ -35,6 +35,7 @@ class App:
access_security = JwtAccessBearerCookie( access_security = JwtAccessBearerCookie(
secret_key=APP_SECRET_KEY, secret_key=APP_SECRET_KEY,
auto_error=False, auto_error=False,
access_expires_delta=timedelta(days=1) if APP_DEBUG else timedelta(minutes=20)
) )
refresh_security = JwtRefreshBearerCookie( refresh_security = JwtRefreshBearerCookie(
secret_key=APP_SECRET_KEY, secret_key=APP_SECRET_KEY,
+4
View File
@@ -18,6 +18,10 @@ def get_public_url(filename: Optional[str]) -> str:
return f"{API_URL}/uploads/{filename}" if filename else None return f"{API_URL}/uploads/{filename}" if filename else None
def airport_icon_url(icao_code: str) -> str:
return f"{API_URL}/static/airports/{icao_code}.png"
def get_photo_url(root) -> str: def get_photo_url(root) -> str:
filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}" filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
return get_public_url(f"photos/{root.flight_id}/{filename}?cache={root.cache_key}") return get_public_url(f"photos/{root.flight_id}/{filename}?cache={root.cache_key}")
+22
View File
@@ -0,0 +1,22 @@
from sqlalchemy import select
from database import models
from database.transaction import get_session
class FlighPlanWeather:
@staticmethod
async def get_plans() -> list:
async with get_session() as db:
markers_without_weather = (await db.execute(
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
))
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
if not flight_plan_ids:
return []
return (await db.execute(
select(models.FlightPlan, models.FlightPlan.markers)
.filter(models.FlightPlan.id.in_(flight_plan_ids))
))
+192
View File
@@ -0,0 +1,192 @@
import asyncio
import dataclasses
import sys
from collections import defaultdict
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from sqlalchemy import select
sys.path.insert(0, "/app/src")
from database import models
from database.transaction import get_session
@dataclasses.dataclass(frozen=True)
class Runway:
heading: int
surface: str
designator: str
@dataclasses.dataclass()
class Airport:
name: str
runways: list[Runway]
async def get_airports():
async with get_session() as db:
airports_data = (await db.scalars(
select(models.Airport)
# .filter(models.Airport.icao_code.in_(["LKJH", "LKLT", "LKBE"]))
)).all()
runways_data = (await db.execute(
select(models.Runway.designator, models.Runway.heading, models.Runway.surface, models.Runway.airport_id)
.distinct()
.order_by(models.Runway.designator)
))
runways = defaultdict(set)
for rwy in runways_data:
runways[rwy.airport_id].add(rwy)
airports = []
for airport in airports_data:
airport_rwy = runways[airport.id]
if not airport_rwy:
continue
airports.append(
Airport(
name=airport.icao_code,
runways=[Runway(heading=rwy.heading, surface=rwy.surface, designator=rwy.designator) for rwy in airport_rwy]
)
)
return airports
def draw_airport(draw, icon_size):
diameter = icon_size // 2
draw.ellipse(
[(diameter // 2, diameter // 2), (icon_size - diameter // 2, icon_size - diameter // 2)],
fill="#cccccc",
outline="black"
)
return draw
# def make_rectangle(l, w, theta, offset=(0, 0)):
# c, s = math.cos(theta), math.sin(theta)
# rect_coords = [(l / 2.0, w / 2.0), (l / 2.0, -w / 2.0), (-l / 2.0, -w / 2.0), (-l / 2.0, w / 2.0)]
# return [(c * x - s * y + offset[0], s * x + c * y + offset[1]) for (x, y) in rect_coords]
#
# def draw_runway(airport_img, runway: Runway, size):
# if runway.heading > 180:
# return
#
# rwy = Image.new("RGBA", (size, size), (0, 0, 0, 0))
#
# draw = ImageDraw.Draw(rwy)
#
# width = 60
# center = size // 2 - width // 2
#
# colors = {
# "asphalt": "#333333",
# "concrete": "#dddddd",
# "grass": "#00ff00"
# }
#
# draw.rectangle((center, 60, center + width, size-60), fill=colors[runway.surface])
#
# font = ImageFont.truetype('/app/static/Calibri.ttf', 60)
# draw.text((center, size - width), runway.designator, font=font, fill="black")
# rwy = rwy.rotate(-1 * runway.heading, expand=False)
#
# airport_img.paste(rwy, mask=rwy)
# print(f"Kreslim RWY {runway}")
# length = size * 0.9
# width = 40
# vertices = make_rectangle(length, width, theta=runway.heading, offset=(250, 250))
# draw.polygon(vertices, fill="#dddddd", outline="black")
# return draw
def add_edge_border(image, border_size=5, border_color=(255, 0, 0, 255)):
mask = image.split()[3]
edge_mask = mask.filter(ImageFilter.FIND_EDGES)
border_image = Image.new("RGBA", image.size, (0, 0, 0, 0))
border_image.paste(border_color, mask=edge_mask)
for _ in range(border_size - 1):
edge_mask = edge_mask.filter(ImageFilter.MaxFilter(3))
border_image.paste(border_color, mask=edge_mask)
bordered_image = Image.alpha_composite(border_image, image)
return bordered_image
def draw_runways(airport_img, runways: list[Runway], size: int):
rwy_width = 60
rwy_margin = 10
center = size // 2
all_runways_width = len(runways) * rwy_width + (len(runways) - 1) * rwy_margin
start = center - all_runways_width // 2
colors = {
"asphalt": "#555555",
"concrete": "#dddddd",
"grass": "#00aa00"
}
font = ImageFont.truetype('/app/static/Calibri.ttf', 50)
rwy = Image.new("RGBA", (size, size), (0, 0, 0, 0))
heading_sum = 0
draw = ImageDraw.Draw(rwy)
for runway in runways:
top_left = (start, 80)
bottom_right = (start + rwy_width, size - 80)
draw.rectangle((*top_left, *bottom_right), fill=colors[runway.surface])
# draw.text((start, size - 60), runway.designator, font=font, fill="black")
start += rwy_width + rwy_margin
heading_sum += runway.heading
rwy = rwy.rotate(-1 * heading_sum // len(runways), expand=False)
airport_img.paste(rwy, mask=rwy)
async def generate_icon(airport: Airport):
icon_size = 500
# creating new Image object
img = Image.new("RGBA", (icon_size, icon_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
if len(airport.name) == 4:
draw = draw_airport(draw, icon_size)
parallel_runways = defaultdict(list)
for runway in airport.runways:
key = runway.designator[:2]
parallel_runways[key].append(runway)
for designator, runways in parallel_runways.items():
draw_runways(img, runways, icon_size)
img = add_edge_border(img, border_size=10, border_color=(0, 0, 255))
img = img.resize((40, 40), resample=Image.LANCZOS)
img.save(f'/app/static/airports/{airport.name}.png', 'PNG')
# img.save(f'/app/static/airports/{airport.name}.webp', 'WEBP')
print(airport.name, [(rwy.designator, rwy.surface) for rwy in airport.runways])
async def generate_icons():
airports = await get_airports()
for airport in airports:
await generate_icon(airport)
# return
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(generate_icons())