9 Commits
26 changed files with 875 additions and 118 deletions
@@ -0,0 +1,38 @@
"""add flight_plan table
Revision ID: 7a3738a805c5
Revises: aba4c3876b28
Create Date: 2024-03-21 16:42:08.428875
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '7a3738a805c5'
down_revision = 'aba4c3876b28'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('flight_plan',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('markers', mysql.JSON(), nullable=False),
sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False),
sa.Column('created_by_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted', sa.Boolean(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('flight_plan')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""make markers in flight planner separate table
Revision ID: de70fc471699
Revises: 7a3738a805c5
Create Date: 2024-03-24 21:55:12.307588
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'de70fc471699'
down_revision = '7a3738a805c5'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('flight_plan_marker',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('flight_plan_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('gps_latitude', sa.Float(), nullable=True),
sa.Column('gps_longitude', sa.Float(), nullable=True),
sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.drop_column('flight_plan', 'markers')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_plan', sa.Column('markers', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False))
op.drop_table('flight_plan_marker')
# ### end Alembic commands ###
@@ -0,0 +1,42 @@
"""add type and position to flight_plan_marker
Revision ID: 70152c47cb49
Revises: de70fc471699
Create Date: 2024-03-24 22:00:35.234920
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '70152c47cb49'
down_revision = 'de70fc471699'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_plan_marker', sa.Column('type', sa.String(length=64), nullable=False))
op.add_column('flight_plan_marker', sa.Column('position', sa.Integer(), nullable=False))
op.alter_column('flight_plan_marker', 'gps_latitude',
existing_type=mysql.FLOAT(),
nullable=False)
op.alter_column('flight_plan_marker', 'gps_longitude',
existing_type=mysql.FLOAT(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('flight_plan_marker', 'gps_longitude',
existing_type=mysql.FLOAT(),
nullable=True)
op.alter_column('flight_plan_marker', 'gps_latitude',
existing_type=mysql.FLOAT(),
nullable=True)
op.drop_column('flight_plan_marker', 'position')
op.drop_column('flight_plan_marker', 'type')
# ### end Alembic commands ###
@@ -0,0 +1,116 @@
"""reworked airspace and airport db structure
Revision ID: cbdb018c5490
Revises: 70152c47cb49
Create Date: 2024-04-21 22:32:50.058166
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'cbdb018c5490'
down_revision = '70152c47cb49'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('airspace',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('country', sa.String(length=8), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('type', sa.String(length=8), nullable=False),
sa.Column('lower_limit', sa.String(length=32), nullable=False),
sa.Column('upper_limit', sa.String(length=32), nullable=False),
sa.Column('center', mysql.JSON(), nullable=False),
sa.Column('radius', sa.Float(), nullable=True),
sa.Column('bounds', mysql.JSON(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_index(op.f('ix_airspace_country'), 'airspace', ['country'], unique=False)
op.create_index(op.f('ix_airspace_type'), 'airspace', ['type'], unique=False)
op.create_table('frequency',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('airport_id', sa.Integer(), nullable=True),
sa.Column('airspace_id', sa.Integer(), nullable=True),
sa.Column('frequency', sa.String(length=7), nullable=False),
sa.Column('name', sa.String(length=64), nullable=False),
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
sa.ForeignKeyConstraint(['airspace_id'], ['airspace.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('runway',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('airport_id', sa.Integer(), nullable=False),
sa.Column('heading', sa.Integer(), nullable=False),
sa.Column('designator', sa.String(length=4), nullable=False),
sa.Column('surface', sa.Enum('asphalt', 'concrete', 'grass', 'other'), server_default='other', nullable=False),
sa.Column('length', sa.Integer(), nullable=True),
sa.Column('width', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('metar')
op.drop_table('license')
op.drop_index('ix_airspace_item_country', table_name='airspace_item')
op.drop_index('ix_airspace_item_type', table_name='airspace_item')
op.drop_index('name', table_name='airspace_item')
op.drop_table('airspace_item')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('airspace_item',
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
sa.Column('country', mysql.VARCHAR(length=8), nullable=False),
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
sa.Column('type', mysql.VARCHAR(length=8), nullable=False),
sa.Column('lower_limit', mysql.VARCHAR(length=32), nullable=False),
sa.Column('upper_limit', mysql.VARCHAR(length=32), nullable=False),
sa.Column('center', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False),
sa.Column('radius', mysql.FLOAT(), nullable=True),
sa.Column('bounds', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False),
sa.PrimaryKeyConstraint('id'),
mysql_collate='utf8mb4_general_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
op.create_index('name', 'airspace_item', ['name'], unique=True)
op.create_index('ix_airspace_item_type', 'airspace_item', ['type'], unique=False)
op.create_index('ix_airspace_item_country', 'airspace_item', ['country'], unique=False)
op.create_table('license',
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
sa.Column('number', mysql.VARCHAR(length=30), nullable=False),
sa.Column('valid_until', mysql.DATETIME(), nullable=False),
sa.Column('created_by_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
sa.Column('created_at', mysql.DATETIME(), server_default=sa.text('current_timestamp()'), nullable=False),
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], name='license_ibfk_1'),
sa.PrimaryKeyConstraint('id'),
mysql_collate='utf8mb4_general_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
op.create_table('metar',
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
sa.Column('airport_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
sa.Column('metar', mysql.TEXT(), nullable=False),
sa.Column('issued_at', mysql.DATETIME(), nullable=False),
sa.Column('deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], name='metar_ibfk_1'),
sa.PrimaryKeyConstraint('id'),
mysql_collate='utf8mb4_general_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
op.drop_table('runway')
op.drop_table('frequency')
op.drop_index(op.f('ix_airspace_type'), table_name='airspace')
op.drop_index(op.f('ix_airspace_country'), table_name='airspace')
op.drop_table('airspace')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""remove old airspace columns, add altitude unit
Revision ID: e07a5c0f1dcc
Revises: cbdb018c5490
Create Date: 2024-04-22 23:42:52.140389
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'e07a5c0f1dcc'
down_revision = 'cbdb018c5490'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('airspace', sa.Column('altitude_unit', sa.String(length=8), nullable=False))
op.drop_column('airspace', 'center')
op.drop_column('airspace', 'radius')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('airspace', sa.Column('radius', mysql.FLOAT(), nullable=True))
op.add_column('airspace', sa.Column('center', mysql.LONGTEXT(charset='utf8mb4', collation='utf8mb4_bin'), nullable=False))
op.drop_column('airspace', 'altitude_unit')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""add alt unit for lower/upper limits in airspaces
Revision ID: 2012eca075c7
Revises: e07a5c0f1dcc
Create Date: 2024-04-22 23:49:17.725111
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2012eca075c7'
down_revision = 'e07a5c0f1dcc'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('airspace', sa.Column('lower_alt_unit', sa.String(length=8), nullable=False))
op.add_column('airspace', sa.Column('upper_alt_unit', sa.String(length=8), nullable=False))
op.drop_column('airspace', 'altitude_unit')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('airspace', sa.Column('altitude_unit', mysql.VARCHAR(length=8), nullable=False))
op.drop_column('airspace', 'upper_alt_unit')
op.drop_column('airspace', 'lower_alt_unit')
# ### end Alembic commands ###
@@ -0,0 +1,45 @@
"""add table for reporting points
Revision ID: 03ba4dfea911
Revises: 2012eca075c7
Create Date: 2024-04-23 21:39:18.254033
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '03ba4dfea911'
down_revision = '2012eca075c7'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('reporting_point',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('openaip_id', sa.String(length=32), nullable=False),
sa.Column('name', sa.String(length=64), nullable=False),
sa.Column('gps_latitude', sa.Float(), nullable=True),
sa.Column('gps_longitude', sa.Float(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('airport_has_reporting_point',
sa.Column('airport_id', sa.Integer(), nullable=False),
sa.Column('reporting_point_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['airport_id'], ['airport.id'], ),
sa.ForeignKeyConstraint(['reporting_point_id'], ['reporting_point.id'], ),
sa.PrimaryKeyConstraint('airport_id', 'reporting_point_id')
)
op.add_column('airport', sa.Column('openaip_id', sa.String(length=32), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('airport', 'openaip_id')
op.drop_table('airport_has_reporting_point')
op.drop_table('reporting_point')
# ### end Alembic commands ###
@@ -0,0 +1,30 @@
"""remove obsolete airport columns
Revision ID: f7ab4bdac987
Revises: 03ba4dfea911
Create Date: 2024-04-24 06:24:48.219726
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'f7ab4bdac987'
down_revision = '03ba4dfea911'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('airport', 'deleted')
op.drop_column('airport', 'is_public')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('airport', sa.Column('is_public', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
op.add_column('airport', sa.Column('deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
# ### end Alembic commands ###
@@ -0,0 +1,56 @@
"""add flight plan meta information
Revision ID: 790ba00fc021
Revises: f7ab4bdac987
Create Date: 2024-04-29 16:40:09.829317
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '790ba00fc021'
down_revision = 'f7ab4bdac987'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('flight_plan_has_copilot',
sa.Column('flight_plan_id', sa.Integer(), nullable=False),
sa.Column('copilot_id', sa.Integer(), nullable=False),
sa.Column('token', sa.String(length=64), nullable=True),
sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ),
sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ),
sa.PrimaryKeyConstraint('flight_plan_id', 'copilot_id'),
sa.UniqueConstraint('token')
)
op.add_column('flight_plan', sa.Column('is_default_name', sa.Boolean(), server_default='0', nullable=False))
op.add_column('flight_plan', sa.Column('is_editable_by_copilot', sa.Boolean(), server_default='0', nullable=False))
op.add_column('flight_plan', sa.Column('planned_speed', sa.Integer(), nullable=False))
op.add_column('flight_plan', sa.Column('planned_takeoff_datetime', sa.DateTime(), nullable=True))
op.add_column('flight_plan', sa.Column('aircraft_id', sa.Integer(), nullable=True))
op.alter_column('flight_plan', 'name',
existing_type=mysql.VARCHAR(length=128),
type_=sa.String(length=256),
existing_nullable=False)
op.create_foreign_key(None, 'flight_plan', 'aircraft', ['aircraft_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
op.alter_column('flight_plan', 'name',
existing_type=sa.String(length=256),
type_=mysql.VARCHAR(length=128),
existing_nullable=False)
op.drop_column('flight_plan', 'aircraft_id')
op.drop_column('flight_plan', 'planned_takeoff_datetime')
op.drop_column('flight_plan', 'planned_speed')
op.drop_column('flight_plan', 'is_editable_by_copilot')
op.drop_column('flight_plan', 'is_default_name')
op.drop_table('flight_plan_has_copilot')
# ### end Alembic commands ###
+1 -1
View File
@@ -7,7 +7,7 @@ services:
network_mode: "host" network_mode: "host"
restart: "always" restart: "always"
env_file: env_file:
- .env - ../.env
environment: environment:
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983" SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
volumes: volumes:
+4 -9
View File
@@ -3,10 +3,8 @@ services:
image: mariadb:10.11 image: mariadb:10.11
ports: ports:
- 3306:3306 - 3306:3306
environment: env_file:
- MYSQL_USER=root - .env
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=ull_tracker
volumes: volumes:
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- db-data:/var/lib/mysql:rw - db-data:/var/lib/mysql:rw
@@ -37,11 +35,8 @@ services:
scrapy: scrapy:
build: build:
context: ../crawlers context: ../crawlers
environment: env_file:
- MYSQL_USER=root - .env
- MYSQL_PASSWORD=root
- MYSQL_DATABASE=ull_tracker
- MYSQL_HOST=db
volumes: volumes:
- db-data:/var/lib/mysql:rw - db-data:/var/lib/mysql:rw
- ../crawlers:/app - ../crawlers:/app
+1 -1
View File
@@ -8,7 +8,7 @@ from paths import FLIGHT_GPX_TRACK_PATH
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str): async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}") gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
coordinates = await gpx_parser.get_coordinates() coordinates = await gpx_parser.get_coordinates_all()
try: try:
elevation = await elevation_api.get_elevation_for_points(coordinates) elevation = await elevation_api.get_elevation_for_points(coordinates)
+87 -34
View File
@@ -59,6 +59,14 @@ flight_has_copilot = Table(
Column("token", String(64), unique=True) Column("token", String(64), unique=True)
) )
flight_plan_has_copilot = Table(
"flight_plan_has_copilot",
BaseModel.metadata,
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
Column("token", String(64), unique=True)
)
copilot_has_photo = Table( copilot_has_photo = Table(
"copilot_has_photo", "copilot_has_photo",
BaseModel.metadata, BaseModel.metadata,
@@ -67,40 +75,111 @@ copilot_has_photo = Table(
) )
class AirspaceItem(BaseModel): airport_has_reporting_point = Table(
__tablename__ = "airspace_item" "airport_has_reporting_point",
BaseModel.metadata,
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
)
class Airspace(BaseModel):
__tablename__ = "airspace"
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True) country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True) type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False) lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False) upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
center: Mapped[List[dict]] = mapped_column(JSON()) upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
radius: Mapped[float] = mapped_column(Float, nullable=True)
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON()) bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
class FlightPlan(BaseModel):
__tablename__ = "flight_plan"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_plan_has_copilot)
aircraft: Mapped['Aircraft'] = relationship()
markers: Mapped['FlightPlanMarker'] = relationship()
created_by: Mapped['User'] = relationship()
class FlightPlanMarker(BaseModel):
__tablename__ = "flight_plan_marker"
id: Mapped[int] = mapped_column(primary_key=True)
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
name: Mapped[str] = mapped_column(String(128), nullable=False)
type: Mapped[str] = mapped_column(String(64), nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
class Airport(BaseModel): class Airport(BaseModel):
__tablename__ = "airport" __tablename__ = "airport"
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False, unique=True)
icao_code: Mapped[str] = mapped_column(String(8), nullable=False) icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
elevation: Mapped[int] = mapped_column(Integer, nullable=True) elevation: Mapped[int] = mapped_column(Integer, nullable=True)
airport_type: Mapped[str] = mapped_column(Enum("airport", "ull", "heliport"), nullable=False, server_default='airport') # noqa airport_type: Mapped[str] = mapped_column(Enum("airport", "ull", "heliport"), nullable=False, server_default='airport') # noqa
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1') use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
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
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
metars: Mapped['Metar'] = relationship()
created_by: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship()
class Runway(BaseModel):
__tablename__ = "runway"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
heading: Mapped[int] = mapped_column(Integer, nullable=False)
designator: Mapped[str] = mapped_column(String(4), nullable=False)
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
length: Mapped[int] = mapped_column(Integer, nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=True)
class Frequency(BaseModel):
__tablename__ = "frequency"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
class ReportingPoint(BaseModel):
__tablename__ = "reporting_point"
id: Mapped[int] = mapped_column(primary_key=True)
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
name: Mapped[str] = mapped_column(String(64), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
class PointOfInterestType(BaseModel): class PointOfInterestType(BaseModel):
__tablename__ = "point_of_interest_type" __tablename__ = "point_of_interest_type"
@@ -340,37 +419,12 @@ class Copilot(BaseModel):
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot) flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
flight_plans: Mapped[Set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
created_by: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship()
photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo) photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo)
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
class Metar(BaseModel):
__tablename__ = "metar"
id: Mapped[int] = mapped_column(primary_key=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
metar: Mapped[str] = mapped_column(Text, nullable=False)
issued_at: Mapped[datetime] = mapped_column(DateTime)
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
airport: Mapped['Airport'] = relationship()
class License(BaseModel):
__tablename__ = "license"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
number: Mapped[str] = mapped_column(String(30), nullable=False)
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=False)
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
user: Mapped['User'] = relationship()
created_by: Mapped['User'] = relationship()
class User(BaseModel): class User(BaseModel):
__tablename__ = "user" __tablename__ = "user"
excluded_columns_in_dict = ('password_hashed',) excluded_columns_in_dict = ('password_hashed',)
@@ -385,7 +439,6 @@ class User(BaseModel):
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False) password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
licences: Mapped[Set['License']] = relationship()
flights: Mapped[Set['Flight']] = relationship() flights: Mapped[Set['Flight']] = relationship()
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization) organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
+34
View File
@@ -0,0 +1,34 @@
import math
EARTH_RADIUS = 6378.1 # Radius of the Earth
def get_bearing(center_point: tuple[float, float], end_point: tuple[float, float]):
lat3 = math.radians(end_point[0])
long3 = math.radians(end_point[1])
lat1 = math.radians(center_point[0])
long1 = math.radians(center_point[1])
dLon = long3 - long1
X = math.cos(lat3) * math.sin(dLon)
Y = math.cos(lat1) * math.sin(lat3) - math.sin(lat1) * math.cos(lat3) * math.cos(dLon)
end_brng = math.atan2(X, Y)
return end_brng
def get_distance(a: tuple[float, float], b: tuple[float, float]):
lat1 = math.radians(a[0])
lon1 = math.radians(a[1])
lat2 = math.radians(b[0])
lon2 = math.radians(b[1])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return EARTH_RADIUS * c
+11 -4
View File
@@ -80,10 +80,12 @@ class GPXParser:
@cached() @cached()
async def get_coordinates(self) -> List[Dict[str, float]]: async def get_coordinates(self) -> List[Dict[str, float]]:
return await self.average_coordinates(await self.get_coordinates_all())
@cached()
async def get_coordinates_all(self) -> List[Dict[str, float]]:
nodes = self.run_xpath("//gpx:trkpt") nodes = self.run_xpath("//gpx:trkpt")
return await self.average_coordinates( return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
[{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
)
@cached() @cached()
async def get_speed(self) -> List[float]: async def get_speed(self) -> List[float]:
@@ -141,7 +143,12 @@ class GPXParser:
continue continue
extensions = target_node.find("./extensions", self.gpx.getroot().nsmap) extensions = target_node.find("./extensions", self.gpx.getroot().nsmap)
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>")) 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 return self.gpx
+19 -52
View File
@@ -8,12 +8,13 @@ import aiohttp
from sqlalchemy import delete from sqlalchemy import delete
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from external.gps_utils import get_distance, get_bearing, EARTH_RADIUS
# https://pyopenair.readthedocs.io/en/latest/openair.html # https://pyopenair.readthedocs.io/en/latest/openair.html
# http://www.winpilot.com/usersguide/userairspace.asp # http://www.winpilot.com/usersguide/userairspace.asp
class AirspaceItemType(enum.Enum): class AirspaceType(enum.Enum):
AIRSPACE_A = "A" AIRSPACE_A = "A"
AIRSPACE_B = "B" AIRSPACE_B = "B"
AIRSPACE_C = "C" AIRSPACE_C = "C"
@@ -46,8 +47,8 @@ class Coordinates:
@dataclasses.dataclass @dataclasses.dataclass
class AirspaceItem: class Airspace:
type: AirspaceItemType type: AirspaceType
info: str = "" info: str = ""
upper_limit: str = None upper_limit: str = None
lower_limit: str = None lower_limit: str = None
@@ -57,40 +58,6 @@ class AirspaceItem:
class ArcExpander: class ArcExpander:
R = 6378.1 # Radius of the Earth
@staticmethod
def get_bearing(center_point, end_point):
lat3 = math.radians(end_point[0])
long3 = math.radians(end_point[1])
lat1 = math.radians(center_point[0])
long1 = math.radians(center_point[1])
dLon = long3 - long1
X = math.cos(lat3) * math.sin(dLon)
Y = math.cos(lat1) * math.sin(lat3) - math.sin(lat1) * math.cos(lat3) * math.cos(dLon)
end_brng = math.atan2(X, Y)
return end_brng
@classmethod
def get_distance(cls, a, b):
lat1 = math.radians(a[0])
lon1 = math.radians(a[1])
lat2 = math.radians(b[0])
lon2 = math.radians(b[1])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return cls.R * c
@classmethod @classmethod
def get_arc_coordinates( def get_arc_coordinates(
cls, cls,
@@ -98,9 +65,9 @@ class ArcExpander:
origin_point: tuple[float, float], origin_point: tuple[float, float],
end_point: tuple[float, float] end_point: tuple[float, float]
) -> list[Coordinates]: ) -> list[Coordinates]:
initial_bearing = cls.get_bearing(center_point, origin_point) initial_bearing = get_bearing(center_point, origin_point)
target_bearing = cls.get_bearing(center_point, end_point) target_bearing = get_bearing(center_point, end_point)
distance = cls.get_distance(center_point, origin_point) distance = get_distance(center_point, origin_point)
print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}") print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}")
@@ -122,12 +89,12 @@ class ArcExpander:
for bearing in list_bearings: for bearing in list_bearings:
lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1]) lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1])
lat2 = math.asin( lat2 = math.asin(
math.sin(lat1) * math.cos(distance / cls.R) + math.cos(lat1) * math.sin(distance / cls.R) * math.cos(bearing) math.sin(lat1) * math.cos(distance / EARTH_RADIUS) + math.cos(lat1) * math.sin(distance / EARTH_RADIUS) * math.cos(bearing)
) )
lon2 = lon1 + math.atan2( lon2 = lon1 + math.atan2(
math.sin(bearing) * math.sin(distance / cls.R) * math.cos(lat1), math.sin(bearing) * math.sin(distance / EARTH_RADIUS) * math.cos(lat1),
math.cos(distance / cls.R) - math.sin(lat1) * math.sin(lat2) math.cos(distance / EARTH_RADIUS) - math.sin(lat1) * math.sin(lat2)
) )
lat2, lon2 = math.degrees(lat2), math.degrees(lon2) lat2, lon2 = math.degrees(lat2), math.degrees(lon2)
@@ -140,7 +107,7 @@ class OpenAirParser:
def __init__(self, file: Path | str): def __init__(self, file: Path | str):
self.file = None self.file = None
self.context_type: Optional[AirspaceItemType] = None self.context_type: Optional[AirspaceType] = None
self.context = {} self.context = {}
self.file = file self.file = file
@@ -149,7 +116,7 @@ class OpenAirParser:
if not self.context_type: if not self.context_type:
return return
data = AirspaceItem(type=self.context_type, **self.context) data = Airspace(type=self.context_type, **self.context)
self.context_type = None self.context_type = None
self.context = {} self.context = {}
@@ -227,19 +194,19 @@ class OpenAirParser:
pass pass
case 'AC': case 'AC':
try: try:
self.context_type = AirspaceItemType(value) self.context_type = AirspaceType(value)
except ValueError as e: except ValueError as e:
print(f"Neznam {e}, ignoruji") print(f"Neznam {e}, ignoruji")
return return
case 'AN': case 'AN':
if self.context_type == AirspaceItemType.AIRSPACE_G: if self.context_type == AirspaceType.AIRSPACE_G:
match value[0:2]: match value[0:2]:
case 'PG': case 'PG':
self.context_type = AirspaceItemType.PARAGLIDING_ZONE self.context_type = AirspaceType.PARAGLIDING_ZONE
case 'LK': case 'LK':
self.context_type = AirspaceItemType.ATZ self.context_type = AirspaceType.ATZ
case _: case _:
self.context_type = AirspaceItemType.TRAGA self.context_type = AirspaceType.TRAGA
self.context['info'] = value self.context['info'] = value
case 'AL': case 'AL':
@@ -282,7 +249,7 @@ async def download_airspace(source, destination):
async def save_airspace(destination): async def save_airspace(destination):
parser = OpenAirParser(destination) parser = OpenAirParser(destination)
async with get_session() as db: async with get_session() as db:
await db.execute(delete(models.AirspaceItem)) await db.execute(delete(models.Airspace))
for item in parser.parse(): for item in parser.parse():
# if 'CTR KARLOVY VARY' not in item.info: # if 'CTR KARLOVY VARY' not in item.info:
@@ -294,7 +261,7 @@ async def save_airspace(destination):
# print("---" * 10) # print("---" * 10)
async with get_session() as db: async with get_session() as db:
await models.AirspaceItem.create(db, { await models.Airspace.create(db, {
"country": "cz", "country": "cz",
"name": item.info, "name": item.info,
"type": item.type.value, "type": item.type.value,
+34 -1
View File
@@ -20,9 +20,33 @@ flight_copilots_dataloader = DataLoader(
models.flight_has_copilot.c.flight_id == models.Flight.id, models.flight_has_copilot.c.flight_id == models.Flight.id,
models.flight_has_copilot.c.copilot_id == models.Copilot.id, models.flight_has_copilot.c.copilot_id == models.Copilot.id,
], ],
extra_join=[models.Copilot.flights]).load, extra_join=[models.Copilot.flights]
).load,
cache=False) cache=False)
flight_plan_copilots_dataloader = DataLoader(
load_fn=FlightCopilotDataloader(
models.Copilot,
relationship_column=models.FlightPlan.id,
extra_select=[models.flight_plan_has_copilot.c.token],
# filters=[
# models.flight_plan_has_copilot.c.flight_plan_id == models.FlightPlan.id,
# models.flight_plan_has_copilot.c.copilot_id == models.Copilot.id,
# ],
extra_join=[models.Copilot.flight_plans]
).load,
cache=False
)
reporting_points_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.ReportingPoint,
relationship_column=models.airport_has_reporting_point.c.airport_id,
extra_join=[models.airport_has_reporting_point]
).load, cache=False
)
flights_by_copilot_dataloader = DataLoader( flights_by_copilot_dataloader = DataLoader(
load_fn=MultiModelsDataloader( load_fn=MultiModelsDataloader(
models.Flight, models.Flight,
@@ -150,3 +174,12 @@ photos_aircraft_dataloader = DataLoader(
).load, ).load,
cache=False cache=False
) )
flight_plan_markers_dataloader = DataLoader(
load_fn=MultiModelsDataloader(
models.FlightPlanMarker,
relationship_column=models.FlightPlanMarker.flight_plan_id,
order_by=[models.FlightPlanMarker.position, models.FlightPlanMarker.id]
).load,
cache=False
)
+3 -3
View File
@@ -16,11 +16,11 @@ class AirspaceQueries:
filters = [] filters = []
if country: if country:
filters.append(models.AirspaceItem.country == country) filters.append(models.Airspace.country == country)
if types: if types:
filters.append(models.AirspaceItem.type.in_(types)) filters.append(models.Airspace.type.in_(types))
return await BaseQueryResolver(Airspace, models.AirspaceItem).get_list( return await BaseQueryResolver(Airspace, models.Airspace).get_list(
info.context.user_id, info.context.user_id,
country=country, country=country,
filters=filters filters=filters
@@ -0,0 +1,43 @@
from typing import List
import strawberry
from strawberry.types import Info
from database import models
from decorators.error_logging import error_logging
from decorators.endpoints import authenticated_user_only, allow_public
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
from graphql_schema.entities.types.types import FlightPlan
@strawberry.type
class FlightPlanQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
async def flight_plans(root, info: Info) -> List[FlightPlan]:
return await BaseQueryResolver(FlightPlan, models.FlightPlan).get_list(info.context.user_id)
@strawberry.field()
@error_logging
@allow_public
async def flight_plan(root, info: Info, id: int) -> FlightPlan:
return await BaseQueryResolver(FlightPlan, models.FlightPlan).get_one(
user_id=info.context.user_id,
object_id=id
)
@strawberry.type
class FlightPlanMutation:
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def create_flight_plan(root, info, input: CreateFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().create(info.context, data=input)
@strawberry.mutation
@error_logging
@authenticated_user_only()
async def edit_flight_plan(root, info, id: int, input: EditFlightPlanInput) -> FlightPlan:
return await FlightPlanMutationResolver().update(id, input, info.context.user_id)
@@ -87,7 +87,7 @@ class BaseQueryResolver(BaseResolver):
class BaseMutationResolver(BaseResolver): class BaseMutationResolver(BaseResolver):
async def _get_one(self, db: AsyncSession, id: int, created_by_id: int) -> models.BaseModel: async def _get_one(self, db: AsyncSession, id: int, created_by_id: int):
query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id) query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id)
return (await db.scalars(query)).one() return (await db.scalars(query)).one()
@@ -0,0 +1,103 @@
import asyncio
from sqlalchemy import delete, select
from sqlalchemy.dialects.mysql import insert
from database import models
from database.models import flight_plan_has_copilot
from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
from graphql_schema.entities.types.types import FlightPlan
from utils.str_utils import random_str
class FlightPlanMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
@staticmethod
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
position = 0
for marker in markers:
await models.FlightPlanMarker.create(db, data={
"position": position,
"flight_plan_id": flight_plan.id,
"type": marker.type,
"name": marker.name,
"gps_latitude": marker.gps_latitude,
"gps_longitude": marker.gps_longitude
})
position += 1
@staticmethod
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
await db.execute(
delete(models.FlightPlanMarker)
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
)
async def create(self, context, data: CreateFlightPlanInput) -> FlightPlan:
input_data = data.to_dict()
input_data['created_by_id'] = context.user_id
async with get_session() as db:
flight_plan = await self._do_create(db, data=input_data)
await self.save_markers(db, flight_plan, data.markers)
return flight_plan
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id))
copilots = await asyncio.gather(*[
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots
])
for copilot_id in copilots:
await db.execute(insert(flight_plan_has_copilot).values(
flight_plan_id=flight_plan_id,
copilot_id=copilot_id,
token=random_str(64)
))
async def update(self, id: int, data: EditFlightPlanInput, user_id: int) -> FlightPlan:
input_data = data.to_dict()
async with get_session() as db:
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
if data.aircraft is not None:
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
if data.markers is not None:
await self.reset_plan_markers(db, flight_plan_model)
await self.save_markers(db, flight_plan_model, data.markers)
if flight_plan_model.is_default_name:
if data.markers:
markers = data.markers
else:
markers = (await db.scalars(select(models.FlightPlanMarker).filter(models.FlightPlanMarker.flight_plan_id == id))).all()
used_markers = evenly_spaced_elements(markers, 5)
input_data['name'] = " - ".join(m.name for m in used_markers)
if data.copilots is not None:
await self.save_copilots(
db,
flight_plan_id=id,
copilots=data.copilots,
user_id=user_id
)
flight_plan = await self._do_update(db, obj=flight_plan_model, data=input_data)
return flight_plan
def evenly_spaced_elements(lst: list, count: int) -> list:
if count > len(lst):
return lst
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
return [lst[int(round(i * interval))] for i in range(count)]
@@ -5,6 +5,7 @@ import strawberry
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from database import models from database import models
from graphql_schema.entities.types.base import BaseGraphqlInputType from graphql_schema.entities.types.base import BaseGraphqlInputType
from graphql_schema.entities.types.types import Aircraft
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
@@ -14,11 +15,33 @@ class ComboboxInput:
name: str name: str
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
class FlightPlanMarkerInput(BaseGraphqlInputType):
position: Optional[int] = 0
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"]) @strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class CreateCopilotInput(BaseGraphqlInputType): class CreateCopilotInput(BaseGraphqlInputType):
pass pass
@strawberry_sqlalchemy_input(model=models.FlightPlan, exclude_fields=["id", "markers"])
class CreateFlightPlanInput(BaseGraphqlInputType):
markers: list[FlightPlanMarkerInput]
def to_dict(self) -> dict:
data = super().to_dict()
data['markers'] = [marker.to_dict() for marker in self.markers]
return data
@strawberry_sqlalchemy_input(model=models.FlightPlan, exclude_fields=["id", "markers"], all_optional=True)
class EditFlightPlanInput(BaseGraphqlInputType):
markers: Optional[list[FlightPlanMarkerInput]] = None
copilots: Optional[list[ComboboxInput]] = None
aircraft: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"], all_optional=True) @strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"], all_optional=True)
class EditCopilotInput(BaseGraphqlInputType): class EditCopilotInput(BaseGraphqlInputType):
pass pass
+70 -8
View File
@@ -1,9 +1,12 @@
from __future__ import annotations from __future__ import annotations
import math
from datetime import datetime from datetime import datetime
from typing import Optional, List from typing import Optional, List
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from external.gps_utils import get_bearing, get_distance
from external.gpx_parser import GPXParser from external.gpx_parser import GPXParser
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
from graphql_schema.dataloaders.multi_models import ( from graphql_schema.dataloaders.multi_models import (
@@ -11,7 +14,8 @@ from graphql_schema.dataloaders.multi_models import (
photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader, photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader,
aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader, aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader,
flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader, flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader,
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader, flight_plan_markers_dataloader,
reporting_points_dataloader, flight_plan_copilots_dataloader
) )
from graphql_schema.dataloaders.single_model import ( from graphql_schema.dataloaders.single_model import (
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
@@ -43,9 +47,19 @@ class GPXTrack:
avg_altitude: float avg_altitude: float
@strawberry_sqlalchemy_type(models.ReportingPoint)
class ReportingPoint:
pass
@strawberry_sqlalchemy_type(models.Airport) @strawberry_sqlalchemy_type(models.Airport)
class Airport: class Airport:
pass reporting_points: List[ReportingPoint] = strawberry.field(
resolver=lambda root: reporting_points_dataloader.load(root.id)
)
# TODO: loader na ikonu
# TODO: loader na RWY a frekvence
@strawberry.type @strawberry.type
@@ -55,15 +69,12 @@ class Airspace:
country: str country: str
type: str type: str
lower_limit: str lower_limit: str
lower_alt_unit: str
upper_alt_unit: str
upper_limit: str upper_limit: str
center: Optional[tuple[float, float]]
radius: Optional[float]
bounds: list[tuple[float, float]] bounds: list[tuple[float, float]]
map_center: Optional[Point] = strawberry.field(
resolver=lambda root: Point(lat=root.center[0], lng=root.center[1]) if root.center else None
)
map_bounds: list[Point] = strawberry.field( map_bounds: list[Point] = strawberry.field(
resolver=lambda root: [Point(lat=point[0], lng=point[1]) for point in root.bounds] resolver=lambda root: [Point(lat=point[1], lng=point[0]) for point in root.bounds]
) )
@@ -180,6 +191,57 @@ class Flight:
) )
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
class FlightPlanMarker:
pass
@strawberry.type
class FlightPlanTrack:
bearing: Optional[int]
distance: Optional[float]
from_: FlightPlanMarker = strawberry.field(name="from")
to: Optional[FlightPlanMarker]
@strawberry_sqlalchemy_type(model=models.FlightPlan)
class FlightPlan:
async def resolve_track(self):
markers = await self.markers()
if len(markers) == 1:
return [FlightPlanTrack(bearing=None, distance=None, from_=markers[0], to=None)]
navigation = []
for i in range(len(markers) - 1):
from_ = markers[i]
to = markers[i + 1]
point_from = (from_.gps_latitude, from_.gps_longitude)
point_to = (to.gps_latitude, to.gps_longitude)
navigation.append(FlightPlanTrack(
bearing=round(math.degrees(get_bearing(point_from, point_to)) % 360),
distance=round(get_distance(point_from, point_to), 1),
from_=from_,
to=to)
)
return navigation
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_copilots(root):
return await flight_plan_copilots_dataloader.load(root.id)
markers: list[FlightPlanMarker] = strawberry.field(
resolver=lambda root: flight_plan_markers_dataloader.load(root.id)
)
track: Optional[list[FlightPlanTrack]] = strawberry.field(
resolver=resolve_track
)
copilots: List[Copilot] = strawberry.field(resolver=load_copilots)
aircraft: Optional[Aircraft] = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id))
@strawberry_sqlalchemy_type(models.Copilot) @strawberry_sqlalchemy_type(models.Copilot)
class Copilot: class Copilot:
async def resolve_flights(root, info): async def resolve_flights(root, info):
+2
View File
@@ -3,6 +3,7 @@ from graphql_schema.entities.aircraft import AircraftMutation
from graphql_schema.entities.copilot import CopilotMutation from graphql_schema.entities.copilot import CopilotMutation
from graphql_schema.entities.event import EventMutation from graphql_schema.entities.event import EventMutation
from graphql_schema.entities.flight import FlightMutation from graphql_schema.entities.flight import FlightMutation
from graphql_schema.entities.flight_plan import FlightPlanMutation
from graphql_schema.entities.organization import OrganizationUserMutation, OrganizationMutation from graphql_schema.entities.organization import OrganizationUserMutation, OrganizationMutation
from graphql_schema.entities.photo import PhotoMutation from graphql_schema.entities.photo import PhotoMutation
from graphql_schema.entities.poi import PointOfInterestMutation from graphql_schema.entities.poi import PointOfInterestMutation
@@ -11,6 +12,7 @@ from graphql_schema.entities.user import EditUserMutation
Mutation = merge_types("Mutation", ( Mutation = merge_types("Mutation", (
AircraftMutation, AircraftMutation,
FlightMutation, FlightMutation,
FlightPlanMutation,
PhotoMutation, PhotoMutation,
PointOfInterestMutation, PointOfInterestMutation,
CopilotMutation, CopilotMutation,
+2
View File
@@ -5,6 +5,7 @@ from .entities.airspace import AirspaceQueries
from .entities.copilot import CopilotQueries from .entities.copilot import CopilotQueries
from .entities.event import EventQueries from .entities.event import EventQueries
from .entities.flight import FlightQueries from .entities.flight import FlightQueries
from .entities.flight_plan import FlightPlanQueries
from .entities.organization import OrganizationQueries from .entities.organization import OrganizationQueries
from .entities.photo import PhotoQueries from .entities.photo import PhotoQueries
from .entities.poi import PointOfInterestQueries from .entities.poi import PointOfInterestQueries
@@ -19,6 +20,7 @@ Query = merge_types('Query', (
AirportQueries, AirportQueries,
AirspaceQueries, AirspaceQueries,
FlightQueries, FlightQueries,
FlightPlanQueries,
CopilotQueries, CopilotQueries,
UserQueries, UserQueries,
PhotoQueries, PhotoQueries,
+8 -4
View File
@@ -1,7 +1,6 @@
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
@@ -46,13 +45,18 @@ async def add_elevation_to_tracks():
for flight in flights: for flight in flights:
gpx_file = f"{FLIGHT_GPX_TRACK_PATH}/{flight.gpx_track_filename}" gpx_file = f"{FLIGHT_GPX_TRACK_PATH}/{flight.gpx_track_filename}"
gpx = GPXParser(gpx_file) try:
gpx = GPXParser(gpx_file)
except OSError:
print(f"ERROR, cannot open {gpx_file=}, skipping")
continue
coordinates = await gpx.get_coordinates() coordinates = await gpx.get_coordinates_all()
elevation = await elevation_api.get_elevation_for_points(coordinates) elevation = await elevation_api.get_elevation_for_points(coordinates)
gpx_with_elevation = gpx.add_terrain_elevation(elevation) gpx_with_elevation = gpx.add_terrain_elevation(elevation)
output_name = f"terrain_{flight.gpx_track_filename[30:]}" output_name = f"terrain_{flight.gpx_track_filename[-30:]}"
gpx.write(gpx_with_elevation, output=f"{FLIGHT_GPX_TRACK_PATH}/{output_name}") gpx.write(gpx_with_elevation, output=f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
await models.Flight.update( await models.Flight.update(
db_session=session, obj=flight, data={ db_session=session, obj=flight, data={