Stahovani pocasi, drobne upravy
This commit is contained in:
@@ -10,3 +10,5 @@ uploads/*
|
|||||||
!uploads/.gitkeep
|
!uploads/.gitkeep
|
||||||
src/emails/templates/html/*
|
src/emails/templates/html/*
|
||||||
.env
|
.env
|
||||||
|
/static/airports/
|
||||||
|
/static/airports/.gitkeep
|
||||||
|
|||||||
+30
@@ -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 ###
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Vendored
-1
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
))
|
||||||
|
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user