Parsovani vzdusneho prostoru
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
"""Add airspace_item table
|
||||||
|
|
||||||
|
Revision ID: aba4c3876b28
|
||||||
|
Revises: 7dd139549851
|
||||||
|
Create Date: 2024-03-18 10:05:57.516697
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'aba4c3876b28'
|
||||||
|
down_revision = '7dd139549851'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('airspace_item',
|
||||||
|
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_item_country'), 'airspace_item', ['country'], unique=False)
|
||||||
|
op.create_index(op.f('ix_airspace_item_type'), 'airspace_item', ['type'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_airspace_item_type'), table_name='airspace_item')
|
||||||
|
op.drop_index(op.f('ix_airspace_item_country'), table_name='airspace_item')
|
||||||
|
op.drop_table('airspace_item')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+10
-3
@@ -5,10 +5,10 @@ services:
|
|||||||
- 3306:3306
|
- 3306:3306
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_USER=root
|
- MYSQL_USER=root
|
||||||
|
- MYSQL_ROOT_PASSWORD=root
|
||||||
- MYSQL_DATABASE=ull_tracker
|
- MYSQL_DATABASE=ull_tracker
|
||||||
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
|
|
||||||
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
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
@@ -23,16 +23,23 @@ services:
|
|||||||
TZ: "Europe/Prague"
|
TZ: "Europe/Prague"
|
||||||
API_URL: "http://localhost:8000"
|
API_URL: "http://localhost:8000"
|
||||||
APP_ENV: "development"
|
APP_ENV: "development"
|
||||||
|
MYSQL_USER: "root"
|
||||||
|
MYSQL_PASSWORD: "root"
|
||||||
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
|
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
|
||||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
|
||||||
scrapy:
|
scrapy:
|
||||||
build:
|
build:
|
||||||
context: ../crawlers
|
context: ../crawlers
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_USER=root
|
- MYSQL_USER=root
|
||||||
- MYSQL_PASSWORD=
|
- MYSQL_PASSWORD=root
|
||||||
- MYSQL_DATABASE=ull_tracker
|
- MYSQL_DATABASE=ull_tracker
|
||||||
- MYSQL_HOST=db
|
- MYSQL_HOST=db
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
fastapi==0.109.0
|
fastapi==0.110.0
|
||||||
fastapi-jwt==0.2.0
|
fastapi-jwt==0.2.0
|
||||||
fastapi-mail==1.4.1
|
fastapi-mail==1.4.1
|
||||||
strawberry-graphql[fastapi]==0.217.1
|
strawberry-graphql[fastapi]==0.217.1
|
||||||
@@ -12,5 +12,5 @@ sentry-sdk[fastapi]
|
|||||||
pillow==10.2.0
|
pillow==10.2.0
|
||||||
exif==1.6.0
|
exif==1.6.0
|
||||||
aiocache==0.12.2
|
aiocache==0.12.2
|
||||||
aiohttp==3.9.1
|
aiohttp==3.9.3
|
||||||
lxml==5.1.0
|
lxml==5.1.0
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Set, List
|
from typing import Set, List
|
||||||
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float, Enum
|
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float, Enum
|
||||||
|
from sqlalchemy.dialects.mysql import JSON
|
||||||
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
|
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -66,6 +67,20 @@ copilot_has_photo = Table(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirspaceItem(BaseModel):
|
||||||
|
__tablename__ = "airspace_item"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||||
|
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
lower_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())
|
||||||
|
radius: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
|
|
||||||
|
|
||||||
class Airport(BaseModel):
|
class Airport(BaseModel):
|
||||||
__tablename__ = "airport"
|
__tablename__ = "airport"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
import strawberry
|
||||||
|
from database import models
|
||||||
|
from decorators.error_logging import error_logging
|
||||||
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
|
from graphql_schema.entities.types.types import Airspace
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class AirspaceQueries:
|
||||||
|
@strawberry.field()
|
||||||
|
@error_logging
|
||||||
|
async def airspaces(
|
||||||
|
root, info, country: Optional[str] = None, types: Optional[list[str]] = None
|
||||||
|
) -> List[Airspace]:
|
||||||
|
|
||||||
|
filters = []
|
||||||
|
if country:
|
||||||
|
filters.append(models.AirspaceItem.country == country)
|
||||||
|
if types:
|
||||||
|
filters.append(models.AirspaceItem.type.in_(types))
|
||||||
|
|
||||||
|
return await BaseQueryResolver(Airspace, models.AirspaceItem).get_list(
|
||||||
|
info.context.user_id,
|
||||||
|
country=country,
|
||||||
|
filters=filters
|
||||||
|
)
|
||||||
@@ -24,21 +24,24 @@ class AircraftQueryResolver(BaseQueryResolver):
|
|||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
filters = {}
|
filters = []
|
||||||
|
search = {}
|
||||||
if object_id:
|
if object_id:
|
||||||
filters['object_id'] = object_id
|
filters.append(models.Aircraft.id == object_id)
|
||||||
if not organization_ids:
|
if not organization_ids:
|
||||||
filters['user_id'] = user_id
|
filters.append(models.Aircraft.created_by_id == user_id)
|
||||||
if kwargs.get("call_sign"):
|
if kwargs.get("call_sign"):
|
||||||
filters['call_sign'] = kwargs["call_sign"]
|
filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
|
||||||
|
|
||||||
if kwargs.get("search"):
|
if kwargs.get("search"):
|
||||||
filters['search'] = kwargs.pop("search", None)
|
search['search'] = kwargs.pop("search", None)
|
||||||
|
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
only_my=False,
|
only_my=False,
|
||||||
only_public=kwargs.get("only_public", False),
|
only_public=kwargs.get("only_public", False),
|
||||||
order_by=order_by,
|
order_by=order_by,
|
||||||
**filters
|
filters=filters,
|
||||||
|
**search
|
||||||
)
|
)
|
||||||
if organization_ids:
|
if organization_ids:
|
||||||
query = (
|
query = (
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class BaseQueryResolver(BaseResolver):
|
|||||||
only_my: Optional[bool] = False,
|
only_my: Optional[bool] = False,
|
||||||
include_others_public: Optional[bool] = False,
|
include_others_public: Optional[bool] = False,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: Optional[str] = None,
|
||||||
|
filters: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
query = self.query_builder.get_simple_query(
|
query = self.query_builder.get_simple_query(
|
||||||
@@ -56,8 +57,8 @@ class BaseQueryResolver(BaseResolver):
|
|||||||
|
|
||||||
query = self._handle_search(query, kwargs.pop("search", None))
|
query = self._handle_search(query, kwargs.pop("search", None))
|
||||||
|
|
||||||
if kwargs:
|
if filters:
|
||||||
query = query.filter_by(**kwargs)
|
query = query.filter(**kwargs)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,25 @@ class Airport:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class Airspace:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
country: str
|
||||||
|
type: str
|
||||||
|
lower_limit: str
|
||||||
|
upper_limit: str
|
||||||
|
center: Optional[tuple[float, float]]
|
||||||
|
radius: Optional[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(
|
||||||
|
resolver=lambda root: [Point(lat=point[0], lng=point[1]) for point in root.bounds]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.FlightTrack)
|
@strawberry_sqlalchemy_type(models.FlightTrack)
|
||||||
class FlightTrack:
|
class FlightTrack:
|
||||||
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from strawberry.tools import merge_types
|
from strawberry.tools import merge_types
|
||||||
from .entities.aircraft import AircraftQueries
|
from .entities.aircraft import AircraftQueries
|
||||||
from .entities.airport import AirportQueries
|
from .entities.airport import AirportQueries
|
||||||
|
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
|
||||||
@@ -16,6 +17,7 @@ from .entities.user import UserQueries
|
|||||||
Query = merge_types('Query', (
|
Query = merge_types('Query', (
|
||||||
AircraftQueries,
|
AircraftQueries,
|
||||||
AirportQueries,
|
AirportQueries,
|
||||||
|
AirspaceQueries,
|
||||||
FlightQueries,
|
FlightQueries,
|
||||||
CopilotQueries,
|
CopilotQueries,
|
||||||
UserQueries,
|
UserQueries,
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
import aiohttp
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
|
||||||
|
|
||||||
|
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
||||||
|
# http://www.winpilot.com/usersguide/userairspace.asp
|
||||||
|
|
||||||
|
class AirspaceItemType(enum.Enum):
|
||||||
|
AIRSPACE_A = "A"
|
||||||
|
AIRSPACE_B = "B"
|
||||||
|
AIRSPACE_C = "C"
|
||||||
|
AIRSPACE_D = "D"
|
||||||
|
AIRSPACE_E = "E"
|
||||||
|
AIRSPACE_F = "F"
|
||||||
|
AIRSPACE_G = "G"
|
||||||
|
ATZ = "ATZ"
|
||||||
|
PARAGLIDING_ZONE = "PGZ"
|
||||||
|
TRAGA = "GSEC"
|
||||||
|
CTR = "CTR"
|
||||||
|
TMA = "TMA"
|
||||||
|
TMZ = "TMZ"
|
||||||
|
FIR = "FIR"
|
||||||
|
RMZ = "RMZ"
|
||||||
|
RESTRICTED = "R"
|
||||||
|
DANGER = "Q"
|
||||||
|
PROHIBITED = "P"
|
||||||
|
|
||||||
|
|
||||||
|
class ArcDirection(enum.Enum):
|
||||||
|
CLOCKWISE = "+"
|
||||||
|
COUNTER_CLOCKWISE = "-"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Coordinates:
|
||||||
|
lat: float
|
||||||
|
lng: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AirspaceItem:
|
||||||
|
type: AirspaceItemType
|
||||||
|
info: str = ""
|
||||||
|
upper_limit: str = None
|
||||||
|
lower_limit: str = None
|
||||||
|
center: Optional[Coordinates] = None
|
||||||
|
radius_nm: Optional[float] = None
|
||||||
|
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
def get_arc_coordinates(
|
||||||
|
cls,
|
||||||
|
center_point: tuple[float, float],
|
||||||
|
origin_point: tuple[float, float],
|
||||||
|
end_point: tuple[float, float]
|
||||||
|
) -> list[Coordinates]:
|
||||||
|
initial_bearing = cls.get_bearing(center_point, origin_point)
|
||||||
|
target_bearing = cls.get_bearing(center_point, end_point)
|
||||||
|
distance = cls.get_distance(center_point, origin_point)
|
||||||
|
|
||||||
|
print(f"DISTANCE {distance} meters, {center_point=}, {origin_point=}, {end_point=}")
|
||||||
|
|
||||||
|
def radians_range(start: float, stop: float, step_deg: int = 15):
|
||||||
|
start_deg = int(math.degrees(start)) % 360
|
||||||
|
stop_deg = int(math.degrees(stop)) % 360
|
||||||
|
|
||||||
|
for i in range(start_deg, stop_deg, step_deg):
|
||||||
|
yield math.radians(i)
|
||||||
|
|
||||||
|
list_bearings = list(radians_range(initial_bearing, target_bearing))
|
||||||
|
print("RADIANS", list_bearings)
|
||||||
|
print("\n")
|
||||||
|
print(f"DEGREES {list(map(math.degrees, list_bearings))}")
|
||||||
|
print(f"INIT DEGREES {math.degrees(initial_bearing)=}, {math.degrees(target_bearing)=}")
|
||||||
|
print("==========================================")
|
||||||
|
|
||||||
|
coordinates = []
|
||||||
|
for bearing in list_bearings:
|
||||||
|
lat1, lon1 = math.radians(center_point[0]), math.radians(center_point[1])
|
||||||
|
lat2 = math.asin(
|
||||||
|
math.sin(lat1) * math.cos(distance / cls.R) + math.cos(lat1) * math.sin(distance / cls.R) * math.cos(bearing)
|
||||||
|
)
|
||||||
|
|
||||||
|
lon2 = lon1 + math.atan2(
|
||||||
|
math.sin(bearing) * math.sin(distance / cls.R) * math.cos(lat1),
|
||||||
|
math.cos(distance / cls.R) - math.sin(lat1) * math.sin(lat2)
|
||||||
|
)
|
||||||
|
|
||||||
|
lat2, lon2 = math.degrees(lat2), math.degrees(lon2)
|
||||||
|
coordinates.append(Coordinates(lat2, lon2))
|
||||||
|
|
||||||
|
return coordinates
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAirParser:
|
||||||
|
def __init__(self, file: Path | str):
|
||||||
|
self.file = None
|
||||||
|
|
||||||
|
self.context_type: Optional[AirspaceItemType] = None
|
||||||
|
self.context = {}
|
||||||
|
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
def save_context(self):
|
||||||
|
if not self.context_type:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = AirspaceItem(type=self.context_type, **self.context)
|
||||||
|
|
||||||
|
self.context_type = None
|
||||||
|
self.context = {}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
with open(self.file, "r") as file:
|
||||||
|
for line in file:
|
||||||
|
stripped_line = line.strip()
|
||||||
|
if not stripped_line:
|
||||||
|
if self.context_type:
|
||||||
|
yield self.save_context()
|
||||||
|
else:
|
||||||
|
self.parse_line(stripped_line)
|
||||||
|
|
||||||
|
if self.context_type:
|
||||||
|
yield self.save_context()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dms_to_dd(dms, hemisphere):
|
||||||
|
if '.' in dms:
|
||||||
|
degrees, minutes = dms.split(':')
|
||||||
|
seconds = 0
|
||||||
|
else:
|
||||||
|
degrees, minutes, seconds = dms.split(":")
|
||||||
|
|
||||||
|
decimal = float(degrees) + float(minutes) / 60 + float(seconds) / 3600
|
||||||
|
if hemisphere in ['S', 'W']:
|
||||||
|
decimal *= -1
|
||||||
|
|
||||||
|
return decimal
|
||||||
|
|
||||||
|
def parse_coordinates(self, coordinates: str) -> Coordinates:
|
||||||
|
parts = coordinates.split()
|
||||||
|
return Coordinates(
|
||||||
|
self.dms_to_dd(parts[0], parts[1]),
|
||||||
|
self.dms_to_dd(parts[2], parts[3])
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_arcs(self, arcs: str) -> list[Coordinates]:
|
||||||
|
arc_from, arc_to = arcs.split(",")
|
||||||
|
if self.context.get('arc_direction', ArcDirection.CLOCKWISE) == ArcDirection.CLOCKWISE:
|
||||||
|
starting_point = self.parse_coordinates(arc_from)
|
||||||
|
ending_point = self.parse_coordinates(arc_to)
|
||||||
|
else:
|
||||||
|
# souradnice jsou zadane proti smeru hodinovych rucicek -> prohodim souradnice a pocitam jako by byly po smeru
|
||||||
|
starting_point = self.parse_coordinates(arc_to)
|
||||||
|
ending_point = self.parse_coordinates(arc_from)
|
||||||
|
|
||||||
|
center = self.context['center']
|
||||||
|
|
||||||
|
del self.context['center']
|
||||||
|
if self.context.get('arc_direction'):
|
||||||
|
del self.context['arc_direction']
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/62712947/calculate-arc-coordinates-based-on-original-point-end-point-center-distance-a
|
||||||
|
|
||||||
|
return ArcExpander.get_arc_coordinates(
|
||||||
|
(center.lat, center.lng),
|
||||||
|
(starting_point.lat, starting_point.lng),
|
||||||
|
(ending_point.lat, ending_point.lng)
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_line(self, line: str):
|
||||||
|
tokens = line.split(" ")
|
||||||
|
keyword = tokens[0]
|
||||||
|
value = None
|
||||||
|
if len(tokens) > 1:
|
||||||
|
value = " ".join(tokens[1:])
|
||||||
|
|
||||||
|
match keyword:
|
||||||
|
case '*':
|
||||||
|
# comment
|
||||||
|
pass
|
||||||
|
case 'AC':
|
||||||
|
try:
|
||||||
|
self.context_type = AirspaceItemType(value)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Neznam {e}, ignoruji")
|
||||||
|
return
|
||||||
|
case 'AN':
|
||||||
|
if self.context_type == AirspaceItemType.AIRSPACE_G:
|
||||||
|
match value[0:2]:
|
||||||
|
case 'PG':
|
||||||
|
self.context_type = AirspaceItemType.PARAGLIDING_ZONE
|
||||||
|
case 'LK':
|
||||||
|
self.context_type = AirspaceItemType.ATZ
|
||||||
|
case _:
|
||||||
|
self.context_type = AirspaceItemType.TRAGA
|
||||||
|
|
||||||
|
self.context['info'] = value
|
||||||
|
case 'AL':
|
||||||
|
self.context['lower_limit'] = value
|
||||||
|
case 'AH':
|
||||||
|
self.context['upper_limit'] = value
|
||||||
|
case 'DP':
|
||||||
|
if 'bounds' not in self.context:
|
||||||
|
self.context['bounds'] = []
|
||||||
|
self.context['bounds'].append(self.parse_coordinates(value))
|
||||||
|
case 'V':
|
||||||
|
match value[0]:
|
||||||
|
case 'X':
|
||||||
|
self.context['center'] = self.parse_coordinates(value[2:]) # odstranit X= na zacatku
|
||||||
|
case 'D':
|
||||||
|
self.context['arc_direction'] = ArcDirection(value[2:])
|
||||||
|
|
||||||
|
case 'DB':
|
||||||
|
if 'bounds' not in self.context:
|
||||||
|
self.context['bounds'] = []
|
||||||
|
|
||||||
|
self.context['bounds'] = [*self.context['bounds'], *self.parse_arcs(value)]
|
||||||
|
case 'DC':
|
||||||
|
self.context['radius_nm'] = float(value)
|
||||||
|
case _:
|
||||||
|
print(f"Neznam {keyword}, radek: {line}")
|
||||||
|
|
||||||
|
|
||||||
|
src = "https://www.alus.it/AirspaceConverter/openAIP/cz_asp.txt"
|
||||||
|
dest = "/app/static/airspaces/cz_asp.txt"
|
||||||
|
|
||||||
|
|
||||||
|
async def download_airspace(source, destination):
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(source) as response:
|
||||||
|
with open(destination, "w") as f:
|
||||||
|
f.write(await response.text(encoding="latin-1"))
|
||||||
|
|
||||||
|
|
||||||
|
async def save_airspace(destination):
|
||||||
|
parser = OpenAirParser(destination)
|
||||||
|
async with get_session() as db:
|
||||||
|
await db.execute(delete(models.AirspaceItem))
|
||||||
|
|
||||||
|
for item in parser.parse():
|
||||||
|
# if 'CTR KARLOVY VARY' not in item.info:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# print("---" * 10)
|
||||||
|
# print(len(item.bounds))
|
||||||
|
# print(item.bounds)
|
||||||
|
# print("---" * 10)
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
await models.AirspaceItem.create(db, {
|
||||||
|
"country": "cz",
|
||||||
|
"name": item.info,
|
||||||
|
"type": item.type.value,
|
||||||
|
'lower_limit': item.lower_limit,
|
||||||
|
"upper_limit": item.upper_limit,
|
||||||
|
"center": [item.center.lat, item.center.lng] if item.center else None,
|
||||||
|
"radius": item.radius_nm * 1000 * 1.852 if item.radius_nm else None,
|
||||||
|
"bounds": [(round(i.lat, 6), round(i.lng, 6)) for i in item.bounds]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_airspaces(src, dest):
|
||||||
|
await download_airspace(src, dest)
|
||||||
|
await save_airspace(dest)
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(refresh_airspaces(src, dest))
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user