├── .gitignore ├── Dockerfile ├── Dockerfile.alpine ├── LICENSE ├── README.md ├── backend_fastapi.py ├── backend_vrs_db.py ├── clip_geojson_precision.py ├── covid_data.json ├── docker-compose.yml ├── environment.yml ├── fir_uir_info.py ├── fir_uir_statistics.json ├── flightroutes └── recurring_callsigns.json ├── flights_statistics.json ├── index.html ├── jsbuild ├── README.md └── main.js ├── nginx_gzip.conf ├── prepare_covid_data.py ├── prepare_fir_uir_ec_only.py ├── prepare_fir_uir_shapefile.py ├── prepare_recurring_callsigns.py ├── prepare_static_airports.py ├── prepare_vrs_database.py ├── remove_third_dimension.py ├── static ├── aiga_air_transportation.svg ├── aiga_air_transportation_orange.svg ├── aircraft_interactive.js ├── aircraft_static.js ├── airports_interactive.js ├── airports_static.js ├── airports_static.json ├── airspaces_static.js ├── favicon.ico ├── flightmap.js ├── flightmap_europe_fir_uir.json ├── flightmap_europe_fir_uir_ec_only.json ├── flightmap_europe_static.html ├── flightmap_europe_with_north_america.json ├── flightmap_fir_uir_north_america.json ├── flightmap_test.html ├── flightsearch.html ├── flightsearch.js ├── index.html ├── leaflet.rotatedMarker.js ├── my_turf.min.js ├── polyfill_string_includes.js ├── statistics.html ├── statistics.js ├── statistics_static.html ├── string_to_colour.js └── upper_lower_airspace_limits.js ├── statistics.html └── update_statistics ├── Dockerfile ├── prepare_statistics.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Mac OS 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8-slim 2 | RUN pip install aiofiles redis 3 | COPY static /app/static 4 | COPY backend_vrs_db.py /app/ 5 | COPY backend_fastapi.py /app/main.py 6 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8-alpine3.10 2 | RUN pip install --upgrade pip \ 3 | && pip install aiofiles redis \ 4 | && rm -rf ~/.cache/pip/* 5 | COPY static /app/static 6 | COPY backend_vrs_db.py /app/ 7 | COPY backend_fastapi.py /app/main.py 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jannis Lübbe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlightMapEuropeSimple 2 | Aircraft positions received by OpenSky Network contributors as well as airports and airspaces are shown on a map. 3 | 4 | ## Airspace shapefiles 5 | The GeoJSON shapefile flightmap_europe_fir_uir_ec_only.json contains simplified 6 | shapes of the FIRs and UIRs of all Eurocontrol member states. 7 | It was created from the shapefile found at 8 | https://github.com/euctrl-pru/eurocontrol-atlas/blob/master/zip/FirUir_NM.zip . 9 | The shapefile was converted from SHP to GeoJSON by ```ogr2ogr -f geoJSON 10 | fir_uir_nm.json FirUir_NM/FirUir_NM.shp```. After that 11 | prepare_fir_uir_ec_only.py was called. 12 | 13 | The GeoJSON shapefile flightmap_europe_fir_uir.json covers the whole world. 14 | However, the focus of this map is Europe. Distant airspaces are shown with 15 | less detail and are approximately merged according to the ICAO regions. It 16 | may not reflect the most recent situation as some publicly available data 17 | sources may be outdated. 18 | This file is generated using prepare_fir_uir_shapefile.py . 19 | It requires flightmap_europe_fir_uir_ec_only.json to be generated first. 20 | Further data sources are an GeoJSON API for FAA airspace boundary 21 | (https://ais-faa.opendata.arcgis.com/datasets/67885972e4e940b2aa6d74024901c561_0) 22 | and another shapefile found at 23 | https://github.com/euctrl-pru/eurocontrol-atlas/blob/master/zip/FirUir_EAD.zip . 24 | The preparation script may be modified to show other regions in more detail. 25 | 26 | ## Airport shapefiles 27 | The shapefile containing the airports for the static version of the map is 28 | created using prepare_static_airports.py . 29 | Data from ourairports.com is filtered and converted to GeoJSON. 30 | 31 | ## Static version of the map 32 | The version of the map hosted at 33 | https://jaluebbe.github.io/FlightMapEuropeSimple/ 34 | consists of HTML, CSS and JavaScript only. 35 | Airspaces and airports consist of static data. 36 | Aircraft positions are downloaded directly from OpenSky Network every 900s. 37 | Interactive controls providing flight route information for airports or 38 | aircraft are not available on this map. 39 | 40 | ## Dynamic version of the map 41 | Airports as well as flight routes are taken from the VRS database which is published daily. 42 | Clicking on an aircraft shows the current flight route while clicking on an airport shows all known destinations at once. 43 | 44 | ### Software requirements 45 | 46 | #### conda/apt 47 | 48 | gunicorn or hypercorn (alternative to gunicorn+uvicorn which may run on windows) 49 | 50 | #### pip 51 | 52 | fastapi uvicorn aiofiles 53 | 54 | ### Data Sources 55 | 56 | #### Flight route database of the Virtual Radar Server project 57 | 58 | A crowd-sourced flight route database is available at http://www.virtualradarserver.co.uk/FlightRoutes.aspx . 59 | 60 | Call prepare_vrs_database.py to download the VRS database and to create a table FlightRoute. 61 | 62 | ### Startup of the web interface 63 | 64 | The web interface and API is hosted using FastAPI. It could also be run as a Docker container. 65 | 66 | #### FastAPI 67 | ``` 68 | gunicorn -w8 -b 0.0.0.0:5000 backend_fastapi:app -k uvicorn.workers.UvicornWorker 69 | ``` 70 | or 71 | ``` 72 | hypercorn -w8 -b 0.0.0.0:5000 backend_fastapi:app 73 | ``` 74 | The number of workers is defined via the -w argument. Instead of using multiple workers the --reload argument would restart the worker as soon as any source code is changed. 75 | #### Build and run as a Docker container 76 | ``` 77 | docker build -t flightmap_europe_simple ./ 78 | docker run -d -p 8000:80 --mount src=`pwd`/flightroutes,target=/app/flightroutes,type=bind flightmap_europe_simple 79 | ``` 80 | or for the alpine based image which consumes less disk space (recommended): 81 | ``` 82 | docker build -t flightmap_europe_simple:alpine -f Dockerfile.alpine ./ 83 | docker run -d -p 8000:80 --mount src=`pwd`/flightroutes,target=/app/flightroutes,type=bind flightmap_europe_simple:alpine 84 | ``` 85 | #### Downloading images from hub.docker.com 86 | Instead of building the image, you may try to download it from hub.docker.com. 87 | Simply use jaluebbe/flightmap_europe_simple or jaluebbe/flightmap_europe_simple:alpine as image to run. 88 | 89 | ### Accessing the API and web interface 90 | 91 | You'll find an interative map at http://127.0.0.1:8000 . 92 | You may click on airports to show all known destinations. 93 | A click on an aircraft shows its current route if it is known. 94 | Alternatively, you may enter a callsign to show the respective flight route. 95 | Searching for an operator ICAO shows all known connections of this operator. 96 | 97 | An interactive flightsearch is available at http://127.0.0.1:8000/flightsearch.html . 98 | You may click on two loction on the map to set origin an destination of you trip. 99 | Now select the maximum acceptable distance between origin, destination and the respective airports. 100 | Finally, select the maximum acceptable number of stops and filter for an airline alliance if required. 101 | -------------------------------------------------------------------------------- /backend_fastapi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import FastAPI, Query, HTTPException, Response 3 | from starlette.staticfiles import StaticFiles, FileResponse 4 | from fastapi.middleware.gzip import GZipMiddleware 5 | from pydantic import BaseModel, confloat, conint, constr 6 | import redis 7 | import uvicorn 8 | import backend_vrs_db 9 | import logging 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | 13 | redis_connection = redis.Redis(os.getenv("REDIS_HOST"), decode_responses=True) 14 | 15 | app = FastAPI( 16 | title="FlightMapEuropeSimple", 17 | openapi_url="/api/openapi.json", 18 | redoc_url="/api/redoc", 19 | docs_url="/api/docs", 20 | ) 21 | app.add_middleware(GZipMiddleware, minimum_size=500) 22 | 23 | 24 | class Location(BaseModel): 25 | lat: confloat(ge=-90, le=90) 26 | lng: confloat(ge=-180, le=180) 27 | 28 | 29 | class FlightSearch(BaseModel): 30 | origin: Location 31 | destination: Location 32 | originRadius: conint(gt=0, le=600e3) 33 | destinationRadius: conint(gt=0, le=600e3) 34 | numberOfStops: conint(ge=0, le=2) 35 | filterAirlineAlliance: constr(regex="^(Star Alliance|Oneworld|SkyTeam|)$") 36 | 37 | 38 | @app.get("/", include_in_schema=False) 39 | async def root(): 40 | return FileResponse("static/index.html") 41 | 42 | 43 | @app.get("/api/geojson/airports") 44 | def get_geojson_airports(): 45 | return backend_vrs_db.get_geojson_airports() 46 | 47 | 48 | @app.get("/api/geojson/callsign/") 49 | def get_geojson_callsign( 50 | callsign: str = Query( 51 | ..., 52 | min_length=4, 53 | max_length=8, 54 | regex=( 55 | "^([A-Z]{3})[0-9](([0-9]{0,3})|([0-9]{0,2})([A-Z])|([0-9]?)" 56 | "([A-Z]{2}))$" 57 | ), 58 | ) 59 | ): 60 | return backend_vrs_db.get_geojson_callsign(callsign) 61 | 62 | 63 | @app.get("/api/geojson/airline/") 64 | def get_geojson_airline( 65 | icao: str = Query(..., min_length=3, max_length=3, regex="^[A-Z]{3}$") 66 | ): 67 | return backend_vrs_db.get_geojson_airline(icao) 68 | 69 | 70 | @app.get("/api/geojson/airport/") 71 | def get_geojson_airport( 72 | icao: str = Query(..., min_length=4, max_length=4, regex="^[0-9A-Z]{4}$") 73 | ): 74 | return backend_vrs_db.get_geojson_airport(icao) 75 | 76 | 77 | @app.post("/api/geojson/flightsearch") 78 | def post_geojson_flightsearch(data: FlightSearch): 79 | return backend_vrs_db.get_geojson_flightsearch(data) 80 | 81 | 82 | @app.get("/api/covid_data.json") 83 | def get_covid_data(): 84 | json_data = redis_connection.get("covid_data") 85 | if json_data is not None: 86 | return Response(content=json_data, media_type="application/json") 87 | else: 88 | raise HTTPException(status_code=404, detail="Item not found") 89 | 90 | 91 | @app.get("/api/flights_statistics.json") 92 | def get_flights_statistics(): 93 | json_data = redis_connection.get("flights_statistics") 94 | if json_data is not None: 95 | return Response(content=json_data, media_type="application/json") 96 | else: 97 | raise HTTPException(status_code=404, detail="Item not found") 98 | 99 | 100 | @app.get("/api/fir_uir_statistics.json") 101 | def get_fir_uir_statistics(): 102 | json_data = redis_connection.get("fir_uir_statistics") 103 | if json_data is not None: 104 | return Response(content=json_data, media_type="application/json") 105 | else: 106 | raise HTTPException(status_code=404, detail="Item not found") 107 | 108 | 109 | app.mount("/", StaticFiles(directory="static"), name="static") 110 | 111 | if __name__ == "__main__": 112 | uvicorn.run(app, host="0.0.0.0", port=8000) 113 | -------------------------------------------------------------------------------- /backend_vrs_db.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import re 4 | import sqlite3 5 | import math 6 | from collections import namedtuple 7 | import logging 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def timeit(method): 12 | def timed(*args, **kw): 13 | ts = time.time() 14 | result = method(*args, **kw) 15 | te = time.time() 16 | if 'log_time' in kw: 17 | name = kw.get('log_name', method.__name__.upper()) 18 | kw['log_time'][name] = int((te - ts) * 1000) 19 | else: 20 | logger.warning('%r %2.2f ms' % 21 | (method.__name__, (te - ts) * 1000)) 22 | return result 23 | return timed 24 | 25 | 26 | directory = "flightroutes/" 27 | 28 | # The following lists of airline alliances including affiliate members are 29 | # based on several public non-authoritative sources and may be incorrect, 30 | # incomplete or outdated. 31 | skyteam_icaos = [ 32 | 'CYL', 'CSH', 'CXA', 'HVN', 'ROT', 'SVA', 'MEA', 'KAL', 'KLM', 'KQA', 'GIA', 33 | 'DAL', 'EDV', 'CPZ', 'GJS', 'RPA', 'SKW', 'CSA', 'CES', 'CAL', 'AZA', 'AFR', 34 | 'HOP', 'AEA', 'AMX', 'SLI', 'ARG', 'AUT', 'AFL', 'KLC'] 35 | star_alliance_icaos = [ 36 | 'SWR', 'NIS', 'GLG', 'THY', 'CLH', 'LRC', 'LLR', 'EVS', 'ASH', 'ROU', 'TAI', 37 | 'SKV', 'GUG', 'OAL', 'GGN', 'EVA', 'BEL', 'UCA', 'CMP', 'CTN', 'JZA', 'AMU', 38 | 'ANA', 'AWI', 'SAS', 'THA', 'AIC', 'UAL', 'LNK', 'AAR', 'AKX', 'SAA', 'AJX', 39 | 'RLK', 'ANZ', 'EXY', 'CCA', 'AEE', 'AUA', 'AVA', 'TPU', 'EST', 'SKW', 'TAP', 40 | 'CSZ', 'ASQ', 'MSR', 'LOF', 'ACA', 'ETH', 'RPA', 'DLH', 'SIA', 'LOT', 'GJS', 41 | 'ISV', 'NZM'] 42 | oneworld_icaos = [ 43 | 'ENY', 'QTR', 'ASH', 'TAM', 'QFA', 'ARE', 'CPZ', 'JIA', 'CFE', 'JAL', 'FCM', 44 | 'BAW', 'MAS', 'JLJ', 'AAL', 'QJE', 'ANE', 'GLP', 'JTA', 'FJI', 'IBE', 'FIN', 45 | 'DSM', 'SUS', 'IBS', 'SHT', 'SBI', 'ALK', 'LNE', 'RJA', 'QLK', 'SKW', 'LTM', 46 | 'PDT', 'RPA', 'CAW', 'NWK', 'LAN', 'LPE', 'CPA', 'HDA'] 47 | 48 | icao24_pattern = re.compile("^[0-9a-f]{6}$") 49 | 50 | 51 | def get_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: 52 | deg_rad = 2 * math.pi / 360 53 | distance = ( 54 | 6.370e6 * math.acos(math.sin(lat1 * deg_rad) * math.sin(lat2 * deg_rad) 55 | + math.cos(lat1 * deg_rad) * math.cos(lat2 * deg_rad) 56 | * math.cos((lon2 - lon1) * deg_rad))) 57 | return distance 58 | 59 | 60 | def cos_deg(angle: float) -> float: 61 | return math.cos(angle * math.pi / 180) 62 | 63 | 64 | def namedtuple_factory(cursor, row): 65 | """ 66 | Usage: 67 | con.row_factory = namedtuple_factory 68 | """ 69 | fields = [col[0] for col in cursor.description] 70 | Row = namedtuple("Row", fields) 71 | return Row(*row) 72 | 73 | 74 | def regexp(expr, item): 75 | reg = re.compile(expr) 76 | return reg.search(item) is not None 77 | 78 | 79 | def get_geojson_airports(): 80 | try: 81 | connection = sqlite3.connect("file:" + directory + 82 | "StandingData.sqb?mode=ro", uri=True) 83 | connection.row_factory = namedtuple_factory 84 | connection.create_function("REGEXP", 2, regexp) 85 | cursor = connection.cursor() 86 | cursor.execute( 87 | "SELECT Name, Icao, Iata, ROUND(Latitude, 6) AS Latitude, " 88 | "ROUND(Longitude, 6) AS Longitude, Destinations FROM Airport " 89 | "WHERE ICAO REGEXP '^[A-Z]{4}$' AND (LENGTH(IATA)=3 OR " 90 | "Destinations + Origins > 0)") 91 | result = cursor.fetchall() 92 | connection.close() 93 | except sqlite3.DatabaseError: 94 | logger.exception('get_geojson_airports()') 95 | return None 96 | _features = [] 97 | for row in result: 98 | _point = {"type": "Point", "coordinates": [row.Longitude, row.Latitude]} 99 | _feature = {"geometry": _point, "type": "Feature", "properties": { 100 | 'name': row.Name, 'icao': row.Icao, 'iata': row.Iata, 101 | 'known_destinations': row.Destinations}} 102 | _features.append(_feature) 103 | _collection = {"type": "FeatureCollection", "properties": { 104 | "utc": time.time()}, "features": _features} 105 | airports_data = _collection 106 | return airports_data 107 | 108 | 109 | def get_airport_position(airport_icao): 110 | try: 111 | connection = sqlite3.connect("file:" + directory + 112 | "StandingData.sqb?mode=ro", uri=True) 113 | connection.row_factory = namedtuple_factory 114 | cursor = connection.cursor() 115 | cursor.execute( 116 | "SELECT Name, Icao, Iata, ROUND(Latitude, 6) AS Latitude, " 117 | "ROUND(Longitude, 6) AS Longitude FROM Airport " 118 | "WHERE Icao = ?", (airport_icao,)) 119 | result = cursor.fetchone() 120 | connection.close() 121 | except sqlite3.DatabaseError: 122 | logger.exception('get_airport_position({})'.format(airport_icao)) 123 | return None 124 | return result 125 | 126 | 127 | def get_airport_positions(): 128 | try: 129 | connection = sqlite3.connect("file:" + directory + 130 | "StandingData.sqb?mode=ro", uri=True) 131 | connection.row_factory = namedtuple_factory 132 | connection.create_function("REGEXP", 2, regexp) 133 | cursor = connection.cursor() 134 | cursor.execute(""" 135 | SELECT Icao, ROUND(Latitude, 6) AS Latitude, 136 | ROUND(Longitude, 6) AS Longitude FROM Airport 137 | WHERE Icao REGEXP '^[A-Z]{4}$' 138 | OR Destinations > 0 139 | OR Origins > 0 140 | """) 141 | result = cursor.fetchall() 142 | connection.close() 143 | except sqlite3.DatabaseError: 144 | logger.exception('get_airport_positions()') 145 | return None 146 | airport_positions = {} 147 | for row in result: 148 | airport_positions[row.Icao] = [row.Longitude, row.Latitude] 149 | return airport_positions 150 | 151 | 152 | def get_distinct_routes_by_airport(airport_icao): 153 | try: 154 | connection = sqlite3.connect("file:" + directory + 155 | "StandingData.sqb?mode=ro", uri=True) 156 | connection.row_factory = namedtuple_factory 157 | cursor = connection.cursor() 158 | cursor.execute( 159 | "SELECT DISTINCT Origin || '-' || Destination AS DirectRoute " 160 | "FROM FlightLegs WHERE Origin = ?", (airport_icao,)) 161 | result = cursor.fetchall() 162 | connection.close() 163 | except sqlite3.DatabaseError: 164 | logger.exception( 165 | 'get_distinct_routes_by_airport({})'.format(airport_icao)) 166 | return None 167 | if result is not None: 168 | return [row.DirectRoute for row in result] 169 | 170 | 171 | def get_distinct_routes_by_airline(operator_icao): 172 | try: 173 | connection = sqlite3.connect("file:" + directory + 174 | "StandingData.sqb?mode=ro", uri=True) 175 | connection.row_factory = namedtuple_factory 176 | cursor = connection.cursor() 177 | cursor.execute( 178 | "SELECT DISTINCT Origin || '-' || Destination AS DirectRoute, " 179 | "o.Name AS OperatorName, o.Iata AS OperatorIata " 180 | "FROM FlightLegs, Operator o " 181 | "WHERE OperatorIcao = ? AND o.Icao=OperatorIcao", (operator_icao,)) 182 | result = cursor.fetchall() 183 | connection.close() 184 | except sqlite3.DatabaseError: 185 | logger.exception( 186 | 'get_distinct_routes_by_airline({})'.format(operator_icao)) 187 | return None 188 | if result is not None and len(result) > 0: 189 | return { 190 | 'routes': [row.DirectRoute for row in result], 191 | 'operator_icao': operator_icao, 192 | 'operator_iata': result[0].OperatorIata, 193 | 'operator_name': result[0].OperatorName} 194 | 195 | 196 | def get_route_by_callsign(callsign): 197 | try: 198 | connection = sqlite3.connect("file:" + directory + 199 | "StandingData.sqb?mode=ro", uri=True) 200 | connection.row_factory = namedtuple_factory 201 | cursor = connection.cursor() 202 | cursor.execute( 203 | "SELECT Callsign, OperatorIcao, OperatorIata, OperatorName, Route " 204 | "FROM FlightRoute WHERE Callsign = ?", (callsign,)) 205 | result = cursor.fetchone() 206 | connection.close() 207 | except sqlite3.DatabaseError: 208 | logger.exception('get_route_by_callsign({})'.format(callsign)) 209 | return None 210 | return result 211 | 212 | 213 | def get_geojson_callsign(callsign): 214 | _flight = get_route_by_callsign(callsign) 215 | if _flight is None: 216 | return '{}' 217 | _route_items = _flight.Route.split('-') 218 | _airport_infos = [] 219 | _line_coordinates = [] 220 | for _icao in _route_items: 221 | _info = get_airport_position(_icao) 222 | if _info is None: 223 | return '{}' 224 | _line_coordinates.append([_info.Longitude, _info.Latitude]) 225 | _airport_infos.append({'name': _info.Name, 'icao': _icao, 226 | 'iata': _info.Iata}) 227 | _line_string = { 228 | "type": "LineString", 229 | "coordinates": _line_coordinates 230 | } 231 | _feature = { 232 | "properties": { 233 | 'callsign': _flight.Callsign, 234 | 'route': _flight.Route, 235 | 'operator_name': _flight.OperatorName, 236 | 'operator_iata': _flight.OperatorIata, 237 | 'airports': _airport_infos}, 238 | "geometry": _line_string, 239 | "type": "Feature" 240 | } 241 | _collection = {"type": "FeatureCollection", "features": [_feature]} 242 | return _collection 243 | 244 | 245 | def get_geojson_airport(icao): 246 | _feature_collection = { 247 | "type": "FeatureCollection", "features": [{"type": "Feature", 248 | "properties": {}, "geometry": {"type": "MultiLineString", 249 | "coordinates": []}}]} 250 | _routes_info = get_distinct_routes_by_airport(icao) 251 | _info = get_airport_position(icao) 252 | if _info is None: 253 | return json.dumps(_feature_collection) 254 | local_coords = [_info.Longitude, _info.Latitude] 255 | if len(_routes_info) == 0: 256 | return json.dumps(_feature_collection) 257 | _features = [] 258 | _coordinates = [] 259 | for _route in _routes_info: 260 | _line_coordinates = [] 261 | _route_items = _route.split('-') 262 | for _icao in _route_items: 263 | if _icao == icao: 264 | _line_coordinates.append(local_coords) 265 | else: 266 | _info = get_airport_position(_icao) 267 | if _info is None: 268 | _line_coordinates = [] 269 | break 270 | _line_coordinates.append([_info.Longitude, _info.Latitude]) 271 | if len(_line_coordinates) > 0: 272 | _coordinates.append(_line_coordinates) 273 | _feature_collection['features'] = [{"type": "Feature", 274 | "properties": {}, 275 | "geometry": {"type": "MultiLineString", 276 | "coordinates": _coordinates}}] 277 | return _feature_collection 278 | 279 | 280 | def get_geojson_airline(icao): 281 | _feature_collection = { 282 | "type": "FeatureCollection", "features": [{"type": "Feature", 283 | "properties": {}, "geometry": {"type": "MultiLineString", 284 | "coordinates": []}}]} 285 | _airline_info = get_distinct_routes_by_airline(icao) 286 | if _airline_info is None: 287 | return json.dumps(_feature_collection) 288 | _routes_info = _airline_info['routes'] 289 | if len(_routes_info) == 0: 290 | return json.dumps(_feature_collection) 291 | _airport_positions = get_airport_positions() 292 | _features = [] 293 | _coordinates = [] 294 | for _route in _routes_info: 295 | _line_coordinates = [] 296 | _route_items = _route.split('-') 297 | for _icao in _route_items: 298 | _position = _airport_positions.get(_icao) 299 | if _position is None: 300 | _line_coordinates = [] 301 | break 302 | _line_coordinates.append(_position) 303 | if len(_line_coordinates) > 0: 304 | _coordinates.append(_line_coordinates) 305 | _feature_collection['features'] = [{"type": "Feature", 306 | "properties": {"operator_icao": _airline_info['operator_icao'], 307 | "operator_iata": _airline_info['operator_iata'], 308 | "operator_name": _airline_info['operator_name']}, 309 | "geometry": {"type": "MultiLineString", 310 | "coordinates": _coordinates}}] 311 | return _feature_collection 312 | 313 | 314 | @timeit 315 | def flightsearch(request_data): 316 | stops = request_data.numberOfStops 317 | lat_origin = request_data.origin.lat 318 | lon_origin = request_data.origin.lng 319 | radius_origin = float(request_data.originRadius) 320 | lat_destination = request_data.destination.lat 321 | lon_destination = request_data.destination.lng 322 | radius_destination = float(request_data.destinationRadius) 323 | filter_airline_alliance = request_data.filterAirlineAlliance 324 | if filter_airline_alliance == 'Star Alliance': 325 | operator_icaos = ','.join(map(repr, star_alliance_icaos)) 326 | elif filter_airline_alliance == 'Oneworld': 327 | operator_icaos = ','.join(map(repr, oneworld_icaos)) 328 | elif filter_airline_alliance == 'SkyTeam': 329 | operator_icaos = ','.join(map(repr, skyteam_icaos)) 330 | if filter_airline_alliance == '': 331 | filter_string_1 = "" 332 | filter_string_2 = "" 333 | filter_string_3 = "" 334 | filter_string_stopovers = "" 335 | else: 336 | filter_string_1 = f"AND FL1.OperatorIcao IN ({operator_icaos})" 337 | filter_string_2 = f"AND FL2.OperatorIcao IN ({operator_icaos})" 338 | filter_string_3 = f"AND FL3.OperatorIcao IN ({operator_icaos})" 339 | filter_string_stopovers = f"AND OperatorIcao IN ({operator_icaos})" 340 | distance = get_distance(lat_origin, lon_origin, lat_destination, 341 | lon_destination) 342 | max_distance = 16*math.sqrt(1e3)*math.sqrt(distance) + 1.05*distance 343 | logger.info(f'distance: {distance/1e3}km') 344 | connection = sqlite3.connect("file:" + directory + 345 | "StandingData.sqb?mode=ro", uri=True) 346 | connection.row_factory = namedtuple_factory 347 | connection.create_function('cosd', 1, cos_deg) 348 | connection.create_function('distance', 4, get_distance) 349 | cursor = connection.cursor() 350 | sql_query_origin = f""" 351 | SELECT Icao FROM Airport 352 | WHERE Destinations > 0 353 | AND {lat_origin} - {radius_origin}/110500 < Latitude 354 | AND Latitude < {lat_origin} + {radius_origin}/110500 355 | AND COSD({lat_origin}) * 110500 * ({lon_origin} - Longitude) 356 | < {radius_origin} 357 | AND COSD({lat_origin}) * 110500 * (- {lon_origin} + Longitude) 358 | < {radius_origin} 359 | AND DISTANCE(Latitude, Longitude, {lat_origin}, {lon_origin}) 360 | < {radius_origin}; 361 | """ 362 | cursor.execute(sql_query_origin) 363 | origin_icaos = [x.Icao for x in cursor.fetchall()] 364 | origins = ','.join(map(repr, origin_icaos)) 365 | logger.debug(f'origins: {origins}') 366 | sql_query_destination = f""" 367 | SELECT Icao FROM Airport 368 | WHERE Origins > 0 369 | AND {lat_destination} - {radius_destination}/110500 < Latitude 370 | AND Latitude < {lat_destination} + {radius_destination}/110500 371 | AND COSD({lat_destination}) * 110500 372 | * ({lon_destination} - Longitude) < {radius_destination} 373 | AND COSD({lat_destination}) * 110500 374 | * (- {lon_destination} + Longitude) < {radius_destination} 375 | AND DISTANCE(Latitude, Longitude, {lat_destination}, {lon_destination}) 376 | < {radius_destination} 377 | """ 378 | cursor.execute(sql_query_destination) 379 | destination_icaos = [x.Icao for x in cursor.fetchall()] 380 | destinations = ','.join(map(repr, destination_icaos)) 381 | logger.debug(f'destinations: {destinations}') 382 | if not origins or not destinations: 383 | cursor.close() 384 | connection.close() 385 | return [] 386 | sql_query = f""" 387 | SELECT DISTINCT 388 | FL1.Origin || '-' || FL1.Destination AS Route, 389 | FL1.Length AS TotalLength, 0 AS Stops 390 | FROM FlightLegs FL1 391 | WHERE FL1.Origin IN ({origins}) 392 | AND FL1.Destination IN ({destinations}) 393 | {filter_string_1} 394 | AND TotalLength < {max_distance}; 395 | """ 396 | logger.debug(f'flight search query: {sql_query}') 397 | cursor.execute(sql_query) 398 | sql_results = cursor.fetchall() 399 | direct_routes = [f"{x.Route}" for x in sql_results] 400 | logger.info(f'found {len(direct_routes)} direct routes.') 401 | logger.debug('direct_routes: {direct_routes}') 402 | if stops == 0 or len(direct_routes) > 10: 403 | cursor.close() 404 | connection.close() 405 | return direct_routes 406 | sql_query = f""" 407 | SELECT DISTINCT 408 | FL1.Origin || '-' || FL1.Destination || '-' || FL2.Destination AS 409 | Route, (FL1.Length+FL2.Length) AS TotalLength, 1 AS Stops 410 | FROM FlightLegs FL1, FlightLegs FL2 411 | WHERE FL1.Origin IN ({origins}) 412 | AND NOT FL1.Destination IN ({destinations}) 413 | AND FL2.Destination IN ({destinations}) 414 | AND NOT FL2.Origin IN ({origins}) 415 | AND FL1.Destination=FL2.Origin 416 | {filter_string_1} 417 | {filter_string_2} 418 | AND TotalLength < {max_distance} 419 | """ 420 | logger.debug(f'flight search query: {sql_query}') 421 | cursor.execute(sql_query) 422 | sql_results = cursor.fetchall() 423 | single_stopover_routes = [f"{x.Route}" for x in sql_results] 424 | logger.info(f'found {len(single_stopover_routes)} single stopover routes.') 425 | logger.debug('single_stopover_routes: {single_stopover_routes}') 426 | if stops == 1 or len(single_stopover_routes + direct_routes) > 10: 427 | cursor.close() 428 | connection.close() 429 | return direct_routes + single_stopover_routes 430 | sql_query_stopover_origins = f""" 431 | SELECT DISTINCT Destination FROM FlightLegs 432 | WHERE Origin IN ({origins}) 433 | AND Length < {max_distance} 434 | AND NOT Destination IN ({destinations}) 435 | {filter_string_stopovers} 436 | """ 437 | cursor.execute(sql_query_stopover_origins) 438 | stopover_origin_icaos = set([x.Destination for x in cursor.fetchall()]) 439 | sql_query_stopover_destinations = f""" 440 | SELECT DISTINCT Origin FROM FlightLegs 441 | WHERE Destination IN ({destinations}) 442 | AND Length < {max_distance} 443 | AND NOT Origin IN ({origins}) 444 | {filter_string_stopovers} 445 | """ 446 | cursor.execute(sql_query_stopover_destinations) 447 | stopover_destination_icaos = [x.Origin for x in cursor.fetchall()] 448 | stopover_origins = ','.join(map(repr, stopover_origin_icaos)) 449 | stopover_destinations = ','.join(map(repr, stopover_destination_icaos)) 450 | logger.debug(f'stopover destinations: {stopover_destinations}') 451 | logger.debug(f'stopover origins: {stopover_origins}') 452 | sql_query = f""" 453 | SELECT DISTINCT 454 | FL1.Origin || '-' || FL1.Destination || '-' || FL2.Destination || 455 | '-' || FL3.Destination AS Route, 456 | FL1.Length+FL2.Length+FL3.Length AS TotalLength, 2 AS Stops 457 | FROM FlightLegs FL1, FlightLegs FL2, FlightLegs FL3 458 | WHERE FL1.Origin IN ({origins}) 459 | AND NOT FL1.Destination IN ({destinations}) 460 | AND FL1.Destination IN ({stopover_origins}) 461 | AND FL3.Destination IN ({destinations}) 462 | AND NOT FL3.Origin IN ({origins}) 463 | AND FL3.Origin IN ({stopover_destinations}) 464 | AND FL2.Origin IN ({stopover_origins}) 465 | AND FL2.Destination IN ({stopover_destinations}) 466 | AND FL2.Destination != FL1.Origin 467 | AND FL3.Destination != FL2.Origin 468 | AND FL1.Destination=FL2.Origin AND FL2.Destination=FL3.Origin 469 | {filter_string_1} 470 | {filter_string_2} 471 | {filter_string_3} 472 | AND TotalLength < {max_distance} 473 | """ 474 | logger.debug(f'flight search query: {sql_query}') 475 | cursor.execute(sql_query) 476 | sql_results = cursor.fetchall() 477 | double_stopover_routes = [f"{x.Route}" for x in sql_results] 478 | logger.info(f'found {len(double_stopover_routes)} double stopover routes.') 479 | logger.debug('double_stopover_routes: {double_stopover_routes}') 480 | cursor.close() 481 | connection.close() 482 | return direct_routes + single_stopover_routes + double_stopover_routes 483 | 484 | 485 | @timeit 486 | def get_geojson_flightsearch(request_data): 487 | _airport_positions = get_airport_positions() 488 | _feature_collection = { 489 | "type": "FeatureCollection", "features": [{"type": "Feature", 490 | "properties": {}, "geometry": {"type": "MultiLineString", 491 | "coordinates": []}}]} 492 | _routes_info = flightsearch(request_data) 493 | _features = [] 494 | _coordinates = [] 495 | for _route in _routes_info: 496 | _line_coordinates = [] 497 | _route_items = _route.split('-') 498 | for _icao in _route_items: 499 | _airport_coordinates = _airport_positions.get(_icao) 500 | if _airport_coordinates is None: 501 | break 502 | _line_coordinates.append(_airport_coordinates) 503 | if len(_line_coordinates) > 0: 504 | _coordinates.append(_line_coordinates) 505 | _feature_collection['features'] = [{"type": "Feature", 506 | "properties": {}, "geometry": {"type": "MultiLineString", 507 | "coordinates": _coordinates}}] 508 | return _feature_collection 509 | 510 | 511 | def get_country_by_icao24(icao24, include_military=False): 512 | if icao24_pattern.match(icao24) is None: 513 | return None 514 | if include_military: 515 | sql_query = f""" 516 | SELECT Country FROM CodeBlockView WHERE BitMask > 0 517 | AND 0x{icao24} & SignificantBitMask = BitMask; 518 | """ 519 | else: 520 | sql_query = f""" 521 | SELECT Country FROM CodeBlockView WHERE BitMask > 0 522 | AND IsMilitary = 0 AND 0x{icao24} & SignificantBitMask = BitMask; 523 | """ 524 | try: 525 | connection = sqlite3.connect("file:" + directory + 526 | "StandingData.sqb?mode=ro", uri=True) 527 | connection.row_factory = namedtuple_factory 528 | cursor = connection.cursor() 529 | cursor.execute(sql_query) 530 | result = cursor.fetchone() 531 | connection.close() 532 | except sqlite3.DatabaseError: 533 | logger.exception('get_country_by_icao24({})'.format(icao24)) 534 | return None 535 | if result is not None: 536 | return result.Country 537 | -------------------------------------------------------------------------------- /clip_geojson_precision.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # clip coordinates in a geoJSON to six decimal digits. 4 | def clip(infile, outfile=None): 5 | 6 | if outfile is None: 7 | outfile = infile 8 | with open (infile, 'r' ) as f: 9 | content = f.read() 10 | 11 | content_new = re.sub('([0-9]+\.[0-9]{6})([0-9]+)', r'\1', content, 12 | flags = re.M) 13 | 14 | with open(outfile, 'w') as f: 15 | f.write(content_new) 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | 5 | traefik: 6 | image: "traefik:v2.4" 7 | container_name: "traefik" 8 | command: 9 | - --entrypoints.web.address=:80 10 | - --entrypoints.websecure.address=:443 11 | - --providers.docker 12 | - --certificatesresolvers.leresolver.acme.tlschallenge=true 13 | - --certificatesresolvers.leresolver.acme.email=${MY_EMAIL} 14 | - --certificatesresolvers.leresolver.acme.storage=/letsencrypt/acme.json 15 | labels: 16 | # global redirect to https 17 | - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" 18 | - "traefik.http.routers.http-catchall.entrypoints=web" 19 | - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" 20 | # middleware redirect 21 | - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" 22 | ports: 23 | - "80:80" 24 | - "443:443" 25 | volumes: 26 | - "./letsencrypt:/letsencrypt" 27 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 28 | 29 | redis: 30 | image: "redis:alpine" 31 | # uncomment if you need redis access for debugging. 32 | # ports: 33 | # - "6379:6379" 34 | 35 | flightmap_api: 36 | image: jaluebbe/flightmap_europe_simple:alpine 37 | volumes: 38 | - $PWD/flightroutes:/app/flightroutes 39 | - $PWD/datenschutz.html:/app/static/datenschutz.html:ro 40 | labels: 41 | - "traefik.http.routers.flightmap-api.rule=(Host(`${FLIGHTMAP_HOST}.${MY_DOMAIN}`) && PathPrefix(`/api`))" 42 | - "traefik.http.routers.flightmap-api.entrypoints=websecure" 43 | - "traefik.http.routers.flightmap-api.tls=true" 44 | - "traefik.http.routers.flightmap-api.tls.certresolver=leresolver" 45 | environment: 46 | - REDIS_HOST=redis 47 | depends_on: 48 | - redis 49 | - prepare_flightmap_data 50 | 51 | prepare_flightmap_data: 52 | image: jaluebbe/flightmap_europe_simple:update_statistics 53 | environment: 54 | - REDIS_HOST=redis 55 | - STATISTICS_URL=${STATISTICS_URL} 56 | depends_on: 57 | - redis 58 | 59 | nginx: 60 | image: nginx:alpine 61 | volumes: 62 | - $PWD/static:/usr/share/nginx/html 63 | - $PWD/datenschutz.html:/usr/share/nginx/html/datenschutz.html:ro 64 | - $PWD/nginx_gzip.conf:/etc/nginx/conf.d/gzip.conf:ro 65 | labels: 66 | - "traefik.http.routers.flightmap.rule=Host(`${FLIGHTMAP_HOST}.${MY_DOMAIN}`)" 67 | - "traefik.http.routers.flightmap.entrypoints=websecure" 68 | - "traefik.http.routers.flightmap.tls=true" 69 | - "traefik.http.routers.flightmap.tls.certresolver=leresolver" 70 | depends_on: 71 | - flightmap_api 72 | 73 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: flightmap 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3 6 | - gdal 7 | - shapely 8 | - gunicorn 9 | - uvloop 10 | - requests 11 | - redis-py 12 | - pandas 13 | - nodejs 14 | - pip 15 | - pip: 16 | - geojson 17 | - fastapi 18 | - uvicorn 19 | - aiofiles 20 | - httptools 21 | - pyopensky 22 | - sqlalchemy 23 | 24 | -------------------------------------------------------------------------------- /fir_uir_info.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import shapely 4 | import time 5 | from shapely import speedups 6 | if speedups.available: 7 | speedups.enable() 8 | from shapely.geometry import shape, Point 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | # load GeoJSON file containing sectors 13 | with open('static/flightmap_europe_fir_uir.json') as f: 14 | firuirs = json.load(f) 15 | 16 | def get_fir_uir_by_position(latitude, longitude, flight_level=0): 17 | # construct point based on lon/lat of aircraft position 18 | point = Point(longitude, latitude) 19 | # check each polygon to see if it contains the point 20 | for feature in firuirs['features']: 21 | if feature['properties']['MAX_FLIGHT'] < flight_level: 22 | # altitude too high for this airspace 23 | continue 24 | elif feature['properties']['MIN_FLIGHT'] == 0: 25 | # altitude cannot be too low for this airspace 26 | pass 27 | elif feature['properties']['MIN_FLIGHT'] > flight_level: 28 | # altitude too low for this airspace 29 | continue 30 | minx, miny, maxx, maxy = feature['bbox'] 31 | if not (maxy >= latitude >= miny and 32 | maxx >= longitude >= minx): 33 | continue 34 | polygon = shape(feature['geometry']) 35 | if feature['geometry']['type'] == 'MultiPolygon': 36 | # If the FIR consists of multiple polygons we need to iterate over 37 | # each single polygon. 38 | multipolygon = polygon 39 | for polygon in multipolygon: 40 | if polygon.contains(point): 41 | return feature['properties'] 42 | elif polygon.contains(point): 43 | return feature['properties'] 44 | logging.debug('No FIR found at ({}, {}).'.format(latitude, longitude)) 45 | 46 | if __name__ == '__main__': 47 | 48 | latitude = 52. 49 | longitude = 7.3 50 | flight_level = 244.9+0.1 51 | t_start = time.time() 52 | data = get_fir_uir_by_position(latitude, longitude, flight_level) 53 | t_stop = time.time() 54 | print (t_stop - t_start) 55 | print (data) 56 | -------------------------------------------------------------------------------- /flights_statistics.json: -------------------------------------------------------------------------------- 1 | {"Dates":["2019-12-16","2019-12-17","2019-12-18","2019-12-19","2019-12-20","2019-12-21","2019-12-22","2019-12-23","2019-12-24","2019-12-25","2019-12-26","2019-12-27","2019-12-28","2019-12-29","2019-12-30","2019-12-31","2020-01-01","2020-01-02","2020-01-03","2020-01-04","2020-01-05","2020-01-06","2020-01-07","2020-01-08","2020-01-09","2020-01-10","2020-01-11","2020-01-12","2020-01-13","2020-01-14","2020-01-15","2020-01-16","2020-01-17","2020-01-18","2020-01-19","2020-01-20","2020-01-21","2020-01-22","2020-01-23","2020-01-24","2020-01-25","2020-01-26","2020-01-27","2020-01-28","2020-01-29","2020-01-30","2020-01-31","2020-02-01","2020-02-02","2020-02-03","2020-02-04","2020-02-05","2020-02-06","2020-02-07","2020-02-08","2020-02-09","2020-02-10","2020-02-11","2020-02-12","2020-02-13","2020-02-14","2020-02-15","2020-02-16","2020-02-17","2020-02-18","2020-02-19","2020-02-20","2020-02-21","2020-02-22","2020-02-23","2020-02-24","2020-02-25","2020-02-26","2020-02-27","2020-02-28","2020-02-29","2020-03-01","2020-03-02","2020-03-03","2020-03-04","2020-03-05","2020-03-06","2020-03-07","2020-03-08","2020-03-09","2020-03-10","2020-03-11","2020-03-12","2020-03-13","2020-03-14","2020-03-15","2020-03-16","2020-03-17","2020-03-18","2020-03-19","2020-03-20","2020-03-21","2020-03-22","2020-03-23","2020-03-24","2020-03-25","2020-03-26","2020-03-27","2020-03-28","2020-03-29","2020-03-30","2020-03-31","2020-04-01","2020-04-02","2020-04-03","2020-04-04","2020-04-05","2020-04-06","2020-04-07","2020-04-08","2020-04-09","2020-04-10","2020-04-11","2020-04-12","2020-04-13","2020-04-14","2020-04-15","2020-04-16","2020-04-17","2020-04-18","2020-04-19","2020-04-20","2020-04-21","2020-04-22","2020-04-23","2020-04-24","2020-04-25","2020-04-26","2020-04-27","2020-04-28","2020-04-29","2020-04-30","2020-05-01","2020-05-02","2020-05-03","2020-05-04","2020-05-05","2020-05-06","2020-05-07","2020-05-08","2020-05-09","2020-05-10","2020-05-11","2020-05-12","2020-05-13","2020-05-14","2020-05-15","2020-05-16","2020-05-17","2020-05-18","2020-05-19","2020-05-20","2020-05-21","2020-05-22","2020-05-23","2020-05-24","2020-05-25","2020-05-26","2020-05-27","2020-05-28","2020-05-29","2020-05-30","2020-05-31","2020-06-01","2020-06-02","2020-06-03","2020-06-04","2020-06-05","2020-06-06","2020-06-07","2020-06-08","2020-06-09","2020-06-10","2020-06-11","2020-06-12","2020-06-13","2020-06-14","2020-06-15","2020-06-16","2020-06-17","2020-06-18","2020-06-19","2020-06-20","2020-06-21","2020-06-22","2020-06-23","2020-06-24","2020-06-25","2020-06-26","2020-06-27","2020-06-28","2020-06-29","2020-06-30","2020-07-01","2020-07-02","2020-07-03","2020-07-04","2020-07-05","2020-07-06","2020-07-07","2020-07-08","2020-07-09","2020-07-10","2020-07-11","2020-07-12","2020-07-13","2020-07-14","2020-07-15","2020-07-16","2020-07-17","2020-07-18","2020-07-19","2020-07-20","2020-07-21","2020-07-22","2020-07-23","2020-07-24","2020-07-25","2020-07-26","2020-07-27","2020-07-28","2020-07-29","2020-07-30","2020-07-31","2020-08-01","2020-08-02","2020-08-03","2020-08-04","2020-08-05","2020-08-06","2020-08-07","2020-08-08","2020-08-09","2020-08-10","2020-08-11","2020-08-12","2020-08-13","2020-08-14","2020-08-15","2020-08-16","2020-08-17","2020-08-18","2020-08-19","2020-08-20","2020-08-21","2020-08-22","2020-08-23","2020-08-24","2020-08-25","2020-08-26","2020-08-27","2020-08-28","2020-08-29","2020-08-30","2020-08-31","2020-09-01","2020-09-02","2020-09-03","2020-09-04","2020-09-05","2020-09-06","2020-09-07","2020-09-08","2020-09-09","2020-09-10","2020-09-11","2020-09-12","2020-09-13","2020-09-14","2020-09-15","2020-09-16","2020-09-17","2020-09-18","2020-09-19","2020-09-20","2020-09-21","2020-09-22","2020-09-23","2020-09-24","2020-09-25","2020-09-26","2020-09-27","2020-09-28","2020-09-29","2020-09-30","2020-10-01","2020-10-02","2020-10-03","2020-10-04","2020-10-05","2020-10-06","2020-10-07","2020-10-08","2020-10-09","2020-10-10","2020-10-11","2020-10-12","2020-10-13","2020-10-14","2020-10-15","2020-10-16","2020-10-17","2020-10-18","2020-10-19","2020-10-20","2020-10-21","2020-10-22","2020-10-23","2020-10-24","2020-10-25","2020-10-26","2020-10-27","2020-10-28","2020-10-29","2020-10-30","2020-10-31","2020-11-01","2020-11-02","2020-11-03","2020-11-04","2020-11-05","2020-11-06","2020-11-07","2020-11-08","2020-11-09","2020-11-10","2020-11-11","2020-11-12","2020-11-13","2020-11-14","2020-11-15","2020-11-16","2020-11-17","2020-11-18","2020-11-19","2020-11-20","2020-11-21","2020-11-22","2020-11-23","2020-11-24","2020-11-25","2020-11-26","2020-11-27","2020-11-28","2020-11-29","2020-11-30","2020-12-01","2020-12-02","2020-12-03","2020-12-04","2020-12-05","2020-12-06","2020-12-07","2020-12-08","2020-12-09","2020-12-10","2020-12-11","2020-12-12","2020-12-13","2020-12-14","2020-12-15","2020-12-16","2020-12-17","2020-12-18","2020-12-19","2020-12-20","2020-12-21","2020-12-22","2020-12-23","2020-12-24","2020-12-25","2020-12-26","2020-12-27","2020-12-28","2020-12-29","2020-12-30","2020-12-31","2021-01-01","2021-01-02","2021-01-03","2021-01-04","2021-01-05","2021-01-06","2021-01-07","2021-01-08","2021-01-09","2021-01-10","2021-01-11","2021-01-12","2021-01-13","2021-01-14","2021-01-15","2021-01-16","2021-01-17","2021-01-18","2021-01-19","2021-01-20","2021-01-21","2021-01-22","2021-01-23","2021-01-24","2021-01-25","2021-01-26","2021-01-27","2021-01-28","2021-01-29","2021-01-30","2021-01-31","2021-02-01","2021-02-02","2021-02-03","2021-02-04","2021-02-05","2021-02-06","2021-02-07","2021-02-08","2021-02-09","2021-02-10","2021-02-11","2021-02-12","2021-02-13","2021-02-14","2021-02-15","2021-02-16","2021-02-17","2021-02-18","2021-02-19","2021-02-20","2021-02-21","2021-02-22","2021-02-23","2021-02-24","2021-02-25","2021-02-26","2021-02-27","2021-02-28","2021-03-01","2021-03-02","2021-03-03","2021-03-04","2021-03-05","2021-03-06","2021-03-07","2021-03-08","2021-03-09","2021-03-10","2021-03-11","2021-03-12","2021-03-13","2021-03-14","2021-03-15","2021-03-16","2021-03-17","2021-03-18","2021-03-19","2021-03-20","2021-03-21","2021-03-22","2021-03-23","2021-03-24","2021-03-25","2021-03-26","2021-03-27","2021-03-28","2021-03-29","2021-03-30","2021-03-31","2021-04-01","2021-04-02","2021-04-03","2021-04-04","2021-04-05","2021-04-06","2021-04-07","2021-04-08","2021-04-09","2021-04-10","2021-04-11","2021-04-12","2021-04-13","2021-04-14","2021-04-15","2021-04-16","2021-04-17","2021-04-18","2021-04-19","2021-04-20","2021-04-21","2021-04-22","2021-04-23","2021-04-24","2021-04-25","2021-04-26","2021-04-27","2021-04-28","2021-04-29","2021-04-30","2021-05-01","2021-05-02","2021-05-03","2021-05-04","2021-05-05","2021-05-06","2021-05-07","2021-05-08","2021-05-09","2021-05-10","2021-05-11","2021-05-12","2021-05-13","2021-05-14","2021-05-15","2021-05-16","2021-05-17","2021-05-18","2021-05-19","2021-05-20","2021-05-21","2021-05-22","2021-05-23","2021-05-24","2021-05-25","2021-05-26","2021-05-27","2021-05-28","2021-05-29","2021-05-30","2021-05-31","2021-06-01","2021-06-02","2021-06-03","2021-06-04","2021-06-05","2021-06-06","2021-06-07","2021-06-08","2021-06-09","2021-06-10","2021-06-11","2021-06-12","2021-06-13","2021-06-14","2021-06-15","2021-06-16","2021-06-17","2021-06-18","2021-06-19","2021-06-20","2021-06-21","2021-06-22","2021-06-23","2021-06-24","2021-06-25","2021-06-26","2021-06-27","2021-06-28","2021-06-29","2021-06-30","2021-07-01","2021-07-02","2021-07-03","2021-07-04","2021-07-05","2021-07-06","2021-07-07","2021-07-08","2021-07-09","2021-07-10","2021-07-11","2021-07-12","2021-07-13","2021-07-14","2021-07-15","2021-07-16","2021-07-17","2021-07-18","2021-07-19","2021-07-20","2021-07-21","2021-07-22","2021-07-23","2021-07-24","2021-07-25","2021-07-26","2021-07-27","2021-07-28","2021-07-29","2021-07-30","2021-07-31","2021-08-01","2021-08-02","2021-08-03","2021-08-04","2021-08-05","2021-08-06","2021-08-07","2021-08-08","2021-08-09","2021-08-10","2021-08-11","2021-08-12","2021-08-13","2021-08-14","2021-08-15","2021-08-16","2021-08-17","2021-08-18","2021-08-19","2021-08-20","2021-08-21","2021-08-22","2021-08-23","2021-08-24","2021-08-25","2021-08-26","2021-08-27","2021-08-28","2021-08-29","2021-08-30","2021-08-31","2021-09-01","2021-09-02","2021-09-03","2021-09-04","2021-09-05","2021-09-06","2021-09-07","2021-09-08","2021-09-09","2021-09-10","2021-09-11","2021-09-12","2021-09-13","2021-09-14","2021-09-15","2021-09-16","2021-09-17","2021-09-18","2021-09-19","2021-09-20","2021-09-21","2021-09-22","2021-09-23","2021-09-24","2021-09-25","2021-09-26","2021-09-27","2021-09-28","2021-09-29","2021-09-30","2021-10-01","2021-10-02","2021-10-03","2021-10-04","2021-10-05","2021-10-06","2021-10-07","2021-10-08","2021-10-09","2021-10-10","2021-10-11","2021-10-12","2021-10-13","2021-10-14","2021-10-15","2021-10-16","2021-10-17","2021-10-18","2021-10-19","2021-10-20","2021-10-21","2021-10-22","2021-10-23","2021-10-24","2021-10-25","2021-10-26","2021-10-27","2021-10-28","2021-10-29","2021-10-30","2021-10-31","2021-11-01","2021-11-02","2021-11-03","2021-11-04","2021-11-05","2021-11-06","2021-11-07","2021-11-08","2021-11-09","2021-11-10","2021-11-11","2021-11-12","2021-11-13","2021-11-14","2021-11-15","2021-11-16","2021-11-17","2021-11-18","2021-11-19","2021-11-20","2021-11-21","2021-11-22","2021-11-23","2021-11-24","2021-11-25","2021-11-26","2021-11-27","2021-11-28","2021-11-29","2021-11-30","2021-12-01","2021-12-02","2021-12-03","2021-12-04","2021-12-05","2021-12-06","2021-12-07","2021-12-08","2021-12-09","2021-12-10","2021-12-11","2021-12-12","2021-12-13","2021-12-14","2021-12-15","2021-12-16","2021-12-17","2021-12-18","2021-12-19","2021-12-20","2021-12-21","2021-12-22","2021-12-23","2021-12-24","2021-12-25","2021-12-26","2021-12-27","2021-12-28","2021-12-29","2021-12-30","2021-12-31","2022-01-01","2022-01-02","2022-01-03","2022-01-04","2022-01-05","2022-01-06","2022-01-07","2022-01-08","2022-01-09","2022-01-10","2022-01-11","2022-01-12","2022-01-13","2022-01-14","2022-01-15","2022-01-16","2022-01-17","2022-01-18","2022-01-19","2022-01-20","2022-01-21","2022-01-22","2022-01-23","2022-01-24","2022-01-25","2022-01-26","2022-01-27","2022-01-28","2022-01-29","2022-01-30","2022-01-31","2022-02-01","2022-02-02","2022-02-03","2022-02-04","2022-02-05","2022-02-06","2022-02-07","2022-02-08","2022-02-09","2022-02-10","2022-02-11","2022-02-12","2022-02-13","2022-02-14","2022-02-15","2022-02-16","2022-02-17","2022-02-18","2022-02-19","2022-02-20","2022-02-21","2022-02-22","2022-02-23","2022-02-24","2022-02-25","2022-02-26","2022-02-27","2022-02-28","2022-03-01","2022-03-02","2022-03-03","2022-03-04","2022-03-05","2022-03-06","2022-03-07","2022-03-08","2022-03-09","2022-03-10","2022-03-11","2022-03-12","2022-03-13","2022-03-14","2022-03-15","2022-03-16","2022-03-17","2022-03-18","2022-03-19","2022-03-20","2022-03-21","2022-03-22","2022-03-23","2022-03-24","2022-03-25","2022-03-26","2022-03-27","2022-03-28","2022-03-29","2022-03-30","2022-03-31","2022-04-01","2022-04-02","2022-04-03","2022-04-04","2022-04-05","2022-04-06","2022-04-07","2022-04-08","2022-04-09","2022-04-10","2022-04-11","2022-04-12","2022-04-13","2022-04-14","2022-04-15","2022-04-16","2022-04-17","2022-04-18","2022-04-19","2022-04-20","2022-04-21","2022-04-22","2022-04-23","2022-04-24","2022-04-25","2022-04-26","2022-04-27","2022-04-28","2022-04-29","2022-04-30","2022-05-01","2022-05-02","2022-05-03","2022-05-04","2022-05-05","2022-05-06","2022-05-07","2022-05-08","2022-05-09","2022-05-10","2022-05-11","2022-05-12","2022-05-13","2022-05-14","2022-05-15","2022-05-16","2022-05-17","2022-05-18","2022-05-19","2022-05-20","2022-05-21","2022-05-22","2022-05-23","2022-05-24","2022-05-25","2022-05-26","2022-05-27","2022-05-28","2022-05-29","2022-05-30","2022-05-31","2022-06-01","2022-06-02","2022-06-03","2022-06-04","2022-06-05","2022-06-06","2022-06-07","2022-06-08","2022-06-09","2022-06-10","2022-06-11","2022-06-12","2022-06-13","2022-06-14","2022-06-15","2022-06-16","2022-06-17","2022-06-18","2022-06-19","2022-06-20","2022-06-21","2022-06-22","2022-06-23","2022-06-24","2022-06-25","2022-06-26","2022-06-27","2022-06-28","2022-06-29","2022-06-30","2022-07-01","2022-07-02","2022-07-03","2022-07-04","2022-07-05","2022-07-06","2022-07-07","2022-07-08","2022-07-09","2022-07-10","2022-07-11","2022-07-12","2022-07-13","2022-07-14","2022-07-15","2022-07-16","2022-07-17","2022-07-18","2022-07-19","2022-07-20","2022-07-21","2022-07-22","2022-07-23","2022-07-24","2022-07-25","2022-07-26","2022-07-27","2022-07-28","2022-07-29","2022-07-30","2022-07-31","2022-08-01","2022-08-02","2022-08-03","2022-08-04","2022-08-05","2022-08-06","2022-08-07","2022-08-08","2022-08-09","2022-08-10","2022-08-11","2022-08-12","2022-08-13","2022-08-14","2022-08-15","2022-08-16","2022-08-17","2022-08-18","2022-08-19","2022-08-20","2022-08-21","2022-08-22","2022-08-23","2022-08-24","2022-08-25","2022-08-26","2022-08-27","2022-08-28","2022-08-29","2022-08-30","2022-08-31","2022-09-01","2022-09-02","2022-09-03","2022-09-04","2022-09-05","2022-09-06","2022-09-07","2022-09-08","2022-09-09","2022-09-10","2022-09-11","2022-09-12","2022-09-13","2022-09-14","2022-09-15","2022-09-16","2022-09-17","2022-09-18","2022-09-19","2022-09-20","2022-09-21","2022-09-22","2022-09-23","2022-09-24","2022-09-25","2022-09-26","2022-09-27","2022-09-28","2022-09-29","2022-09-30","2022-10-01","2022-10-02","2022-10-03","2022-10-04","2022-10-05","2022-10-06","2022-10-07","2022-10-08","2022-10-09","2022-10-10","2022-10-11","2022-10-12","2022-10-13","2022-10-14","2022-10-15","2022-10-16","2022-10-17","2022-10-18","2022-10-19","2022-10-20","2022-10-21","2022-10-22","2022-10-23","2022-10-24","2022-10-25","2022-10-26","2022-10-27","2022-10-28","2022-10-29","2022-10-30","2022-10-31","2022-11-01","2022-11-02","2022-11-03","2022-11-04","2022-11-05","2022-11-06","2022-11-07","2022-11-08","2022-11-09","2022-11-10","2022-11-11","2022-11-12","2022-11-13","2022-11-14","2022-11-15","2022-11-16","2022-11-17","2022-11-18","2022-11-19","2022-11-20","2022-11-21","2022-11-22","2022-11-23","2022-11-24","2022-11-25","2022-11-26","2022-11-27","2022-11-28","2022-11-29","2022-11-30","2022-12-01","2022-12-02","2022-12-03","2022-12-04","2022-12-05","2022-12-06","2022-12-07","2022-12-08","2022-12-09","2022-12-10","2022-12-11","2022-12-12","2022-12-13","2022-12-14","2022-12-15","2022-12-16","2022-12-17","2022-12-18","2022-12-19","2022-12-20","2022-12-21","2022-12-22","2022-12-23","2022-12-24","2022-12-25","2022-12-26","2022-12-27","2022-12-28","2022-12-29","2022-12-30","2022-12-31","2023-01-01","2023-01-02","2023-01-03","2023-01-04","2023-01-05","2023-01-06","2023-01-07","2023-01-08","2023-01-09","2023-01-10","2023-01-11","2023-01-12","2023-01-13","2023-01-14","2023-01-15","2023-01-16","2023-01-17","2023-01-18","2023-01-19","2023-01-20","2023-01-21","2023-01-22","2023-01-23","2023-01-24","2023-01-25","2023-01-26","2023-01-27","2023-01-28","2023-01-29","2023-01-30","2023-01-31","2023-02-01","2023-02-02","2023-02-03","2023-02-04","2023-02-05","2023-02-06","2023-02-07","2023-02-08","2023-02-09","2023-02-10","2023-02-11","2023-02-12","2023-02-13","2023-02-14","2023-02-15","2023-02-16","2023-02-17","2023-02-18"],"FlightsDetected":[76177,74847,76722,79788,82228,77506,78075,78048,71493,60907,74013,78807,75117,76083,75474,70714,67308,76939,78912,75195,77083,76734,74648,73753,75845,78046,70411,72550,75362,72732,73748,75981,78114,64178,71967,75480,74022,73929,76226,77777,70093,72429,75282,72788,72310,74699,76278,68136,69447,71580,69095,69137,71446,73068,66836,66206,69308,68229,69145,71223,74337,67180,68154,70014,68825,70518,72142,74616,67802,69195,71532,69916,70878,72247,74296,67437,70050,71785,69812,70187,71643,73657,66608,69267,69738,67106,67095,68220,69271,61781,62862,61597,56875,53760,51311,49570,41956,38738,34984,32730,29010,27786,27373,24050,22793,22373,23390,22467,20832,20941,17655,17446,18437,19017,18884,18496,17765,15342,14648,15232,17230,17193,16314,16756,14258,13908,14800,16111,16650,16998,16950,14365,14108,15210,16783,16527,17010,17667,14909,14888,16188,18212,18057,18800,18631,14580,14638,15995,17231,17815,18620,18442,16192,16905,17815,18937,20176,20401,21167,17747,17237,18354,18665,21036,21457,21902,18178,18793,21086,22126,23226,24337,25079,21337,21791,23086,23855,25014,25851,26633,22518,23598,25690,26281,26338,28068,28731,24811,25698,27481,28178,29228,29728,30313,26882,27436,28363,29112,33545,35355,36842,31084,33482,35037,35757,37083,38298,40454,36828,37268,37862,37463,38831,40195,42161,38539,38137,39210,38431,39420,40483,42711,39249,39447,39033,38184,38840,38163,42267,41236,41324,41985,40473,41126,43254,45239,41994,42131,42215,40792,41077,42996,45100,38825,42340,42336,40840,40991,43383,44626,41333,42228,42075,41153,40749,43303,45302,41492,42536,42209,40585,40231,43476,45527,40107,39564,40430,39027,40370,42734,44078,39073,40799,40467,39218,40615,42573,44618,40002,41488,41226,40078,40694,43207,44880,39615,41443,41210,39987,41278,42956,44757,38220,41174,40905,38965,40006,43106,45378,40482,41617,41771,39574,40547,43128,45924,40678,41963,41779,39066,39738,42919,45218,39577,40650,40792,38769,39720,42433,45148,38971,40492,41559,39331,40602,42856,44944,39770,41217,41060,38979,39388,42126,44019,38620,40034,41210,39194,39767,42212,44687,40225,40793,40823,40897,42217,37853,39489,40229,42185,42443,41716,41583,42652,44735,38970,40470,41045,39791,40797,43297,45628,40050,41077,41774,40999,41810,45834,50849,48315,48121,46397,46960,49227,44620,35365,42392,46505,45649,45676,47019,42238,35263,43122,48211,46501,43099,40089,43131,44637,40720,42248,40948,39432,39640,42247,43883,38349,39097,39716,38174,38784,40837,42627,37095,38011,38651,37213,38178,39753,41472,37008,37576,37023,35733,37761,40186,41889,37774,37504,38390,37416,38759,39203,41051,36740,35506,35663,34615,37219,38475,41824,39431,40844,41102,39596,40155,42802,44884,40571,41239,42294,40559,41347,44327,46693,41935,42268,43397,42186,42386,45356,47963,43241,43233,43090,42927,43974,46080,48862,44949,45663,45676,44430,45245,48753,50777,46494,47530,47263,46281,47652,50459,50491,46918,46795,48479,47258,47074,48890,51687,46617,47436,47106,45692,45660,48529,49781,44726,46844,46634,45550,45537,49111,50483,44862,46118,46411,45339,46096,49946,50945,46064,46538,46933,45604,46588,48230,50535,45232,46220,47010,45932,47252,48348,49584,45320,47680,47567,47351,47622,50989,52141,46650,48121,48608,48146,48416,51309,53653,48139,48022,48131,48195,50595,52075,54759,50692,51774,51656,50414,51008,53735,56204,51493,52073,52952,52480,53480,55815,57944,52993,53974,53484,53305,53953,56129,60030,53929,55056,55355,54045,55414,59324,61204,58232,56770,57051,56450,58524,60616,63226,60034,59717,59198,58622,59435,62253,64463,60734,60397,61070,59586,59975,62452,63878,60633,60541,59317,60040,60611,62676,64344,61307,60949,60287,59347,59612,61615,63499,60197,60320,59856,59110,59340,61677,63261,60271,60230,60445,58898,58948,61665,63418,59287,59165,60090,58501,59048,61752,64217,60346,58471,60308,59272,59905,61666,62437,58512,57007,57095,55188,57770,60823,62616,57875,59236,58703,56752,57668,60042,63235,58873,59130,60166,56906,57594,60761,62324,57897,59464,59548,57805,57372,61084,62452,57867,58130,58903,56582,57272,61088,62743,57048,57762,60410,58466,59241,62584,65421,60117,60593,62343,59654,60504,62649,64899,60397,61171,61325,58954,59625,62285,64632,59110,58185,59529,57524,59016,61545,63790,56969,58710,60017,57772,57925,60409,62373,55716,58183,59661,58085,58429,62100,64797,58236,60604,61275,60908,61647,58793,58153,57733,62369,62062,60317,60437,62551,65085,58087,59654,60973,58801,60082,62596,65236,57775,59792,61918,59473,61039,65166,68919,63282,64849,65436,64562,66293,68269,61905,48917,60226,61994,61044,62293,63605,58784,51126,61729,63229,61782,60253,60611,62532,58962,60967,59247,55807,55800,58188,61059,54893,54087,54262,53929,54264,56974,60591,54947,56722,55676,53642,54828,58044,61217,53213,55797,54604,52933,52390,53141,56283,54613,57581,57912,56264,56771,59400,63485,58019,58472,58496,57453,58364,60858,63206,60246,60230,59523,57480,58457,59949,63423,60018,60236,59962,59384,59488,62074,65066,59550,60706,60291,58649,58736,61792,64412,57373,60133,60363,57981,58938,61471,63940,58242,60424,60409,57754,58860,61570,63815,58659,62995,63640,62246,62431,65202,67348,61000,62201,63078,62429,63589,64770,67338,62142,63657,64484,63671,64897,66783,66924,60839,61848,63765,64363,64826,66397,67919,61974,63728,65244,64566,65333,68187,70277,64334,65405,66506,66498,67143,69485,71226,65129,66088,67776,67513,67689,69941,70891,64891,66087,66759,65829,67195,69662,70432,64181,65592,66900,67169,68941,69856,70545,65554,66730,68078,67396,70227,71061,72703,67634,68799,71333,71333,70960,73343,75287,69563,70332,71803,71659,72597,72438,73626,68837,70400,72239,72957,72911,74296,75069,69691,70908,72003,72405,73271,74678,75596,70594,71368,71323,71137,73497,75014,76349,71489,72038,72824,72832,73178,74697,76468,71164,71566,73214,73114,74224,75311,76761,71538,72415,73191,72943,72810,75640,76884,72463,73358,73916,73673,73970,74016,75240,71057,72036,73183,73869,74188,75103,76901,72238,73186,74092,74333,74134,75195,76481,71624,72688,72704,71954,73462,75092,76575,71338,72487,73566,73580,74471,76043,75465,70025,69463,71512,70465,72511,74262,75530,68764,70166,72168,71635,71878,74514,73172,68869,69438,70003,71116,71707,73700,75137,68122,69153,71668,71264,70593,71549,73496,67265,68658,70487,69508,69502,72464,74315,68410,69333,71158,70758,71543,72980,75013,68318,69818,70824,70048,70769,73268,73030,67899,68828,70457,69686,71169,73099,74154,67397,67178,68072,66446,67091,69441,71381,63731,66536,67938,66373,66282,68211,70476,63427,66028,67768,66601,67164,69859,72153,64316,66555,68315,67747,68326,64513,64661,63814,67208,68576,66407,66554,68947,70859,63692,65979,67527,66650,67195,69550,70991,64427,64878,67121,66730,67137,70158,72942,68122,68581,69705,69857,70526,67684,50672,43863,38108,46197,58411,66647,68333,69924,61310,58084,63492,46539,66473,67039,68241,63871,65851,66506,64662,66420,68325,71660,65812,65724,68165,66825,66994,68933,72122,64948,65866,67882,65796,65908,68928,71521,65840,66576,68140,64808,65690,68938,72304,65921,67234,69980,68275,68785,71108,74121,67884,68640,70180,68286,69355,72273,72932,69559],"percentage":[100.97,99.21,101.69,105.75,108.99,102.73,103.48,103.45,94.76,80.73,98.1,104.45,99.56,100.84,100.04,93.73,89.21,101.98,104.59,99.67,102.17,101.71,98.94,97.76,100.53,103.45,93.33,96.16,99.89,96.4,97.75,100.71,103.54,85.06,95.39,100.04,98.11,97.99,101.03,103.09,92.9,96.0,99.78,96.48,95.84,99.01,101.1,90.31,92.05,94.88,91.58,91.64,94.7,96.85,88.59,87.75,91.86,90.43,91.65,94.4,98.53,89.04,90.33,92.8,91.22,93.47,95.62,98.9,89.87,91.71,94.81,92.67,93.94,95.76,98.48,89.38,92.85,95.15,92.53,93.03,94.96,97.63,88.29,91.81,92.43,88.95,88.93,90.42,91.81,81.89,83.32,81.64,75.38,71.26,68.01,65.7,55.61,51.35,46.37,43.38,38.45,36.83,36.28,31.88,30.21,29.65,31.0,29.78,27.61,27.76,23.4,23.12,24.44,25.21,25.03,24.52,23.55,20.33,19.42,20.19,22.84,22.79,21.62,22.21,18.9,18.43,19.62,21.35,22.07,22.53,22.47,19.04,18.7,20.16,22.24,21.91,22.55,23.42,19.76,19.73,21.46,24.14,23.93,24.92,24.69,19.33,19.4,21.2,22.84,23.61,24.68,24.44,21.46,22.41,23.61,25.1,26.74,27.04,28.06,23.52,22.85,24.33,24.74,27.88,28.44,29.03,24.09,24.91,27.95,29.33,30.78,32.26,33.24,28.28,28.88,30.6,31.62,33.15,34.26,35.3,29.85,31.28,34.05,34.83,34.91,37.2,38.08,32.89,34.06,36.42,37.35,38.74,39.4,40.18,35.63,36.36,37.59,38.59,44.46,46.86,48.83,41.2,44.38,46.44,47.39,49.15,50.76,53.62,48.81,49.4,50.18,49.66,51.47,53.28,55.88,51.08,50.55,51.97,50.94,52.25,53.66,56.61,52.02,52.28,51.74,50.61,51.48,50.58,56.02,54.66,54.77,55.65,53.64,54.51,57.33,59.96,55.66,55.84,55.95,54.07,54.45,56.99,59.78,51.46,56.12,56.11,54.13,54.33,57.5,59.15,54.78,55.97,55.77,54.55,54.01,57.4,60.05,55.0,56.38,55.95,53.79,53.32,57.63,60.34,53.16,52.44,53.59,51.73,53.51,56.64,58.42,51.79,54.08,53.64,51.98,53.83,56.43,59.14,53.02,54.99,54.64,53.12,53.94,57.27,59.49,52.51,54.93,54.62,53.0,54.71,56.94,59.32,50.66,54.57,54.22,51.65,53.03,57.13,60.15,53.66,55.16,55.37,52.45,53.74,57.16,60.87,53.92,55.62,55.38,51.78,52.67,56.89,59.93,52.46,53.88,54.07,51.39,52.65,56.24,59.84,51.65,53.67,55.08,52.13,53.82,56.8,59.57,52.71,54.63,54.42,51.66,52.21,55.84,58.34,51.19,53.06,54.62,51.95,52.71,55.95,59.23,53.32,54.07,54.11,54.21,55.96,50.17,52.34,53.32,55.91,56.26,55.29,55.12,56.53,59.29,51.65,53.64,54.4,52.74,54.07,57.39,60.48,53.08,54.45,55.37,54.34,55.42,60.75,67.4,64.04,63.78,61.5,62.24,65.25,59.14,46.87,56.19,61.64,60.51,60.54,62.32,55.98,46.74,57.16,63.9,61.63,57.13,53.14,57.17,59.16,53.97,56.0,54.27,52.26,52.54,56.0,58.16,50.83,51.82,52.64,50.6,51.41,54.13,56.5,49.17,50.38,51.23,49.32,50.6,52.69,54.97,49.05,49.8,49.07,47.36,50.05,53.26,55.52,50.07,49.71,50.88,49.59,51.37,51.96,54.41,48.7,47.06,47.27,45.88,49.33,51.0,55.44,52.26,54.14,54.48,52.48,53.22,56.73,59.49,53.77,54.66,56.06,53.76,54.8,58.75,61.89,55.58,56.02,57.52,55.92,56.18,60.12,63.57,57.31,57.3,57.11,56.9,58.29,61.08,64.76,59.58,60.52,60.54,58.89,59.97,64.62,67.3,61.63,63.0,62.64,61.34,63.16,66.88,66.92,62.19,62.02,64.26,62.64,62.39,64.8,68.51,61.79,62.87,62.44,60.56,60.52,64.32,65.98,59.28,62.09,61.81,60.37,60.36,65.09,66.91,59.46,61.13,61.52,60.09,61.1,66.2,67.52,61.06,61.68,62.21,60.45,61.75,63.93,66.98,59.95,61.26,62.31,60.88,62.63,64.08,65.72,60.07,63.2,63.05,62.76,63.12,67.58,69.11,61.83,63.78,64.43,63.81,64.17,68.01,71.11,63.81,63.65,63.8,63.88,67.06,69.02,72.58,67.19,68.62,68.47,66.82,67.61,71.22,74.5,68.25,69.02,70.19,69.56,70.88,73.98,76.8,70.24,71.54,70.89,70.65,71.51,74.4,79.57,71.48,72.97,73.37,71.63,73.45,78.63,81.12,77.18,75.25,75.62,74.82,77.57,80.34,83.8,79.57,79.15,78.46,77.7,78.78,82.51,85.44,80.5,80.05,80.94,78.98,79.49,82.78,84.67,80.37,80.24,78.62,79.58,80.34,83.07,85.28,81.26,80.78,79.91,78.66,79.01,81.67,84.16,79.79,79.95,79.34,78.35,78.65,81.75,83.85,79.89,79.83,80.12,78.07,78.13,81.73,84.06,78.58,78.42,79.65,77.54,78.26,81.85,85.12,79.99,77.5,79.94,78.56,79.4,81.73,82.76,77.55,75.56,75.68,73.15,76.57,80.62,82.99,76.71,78.51,77.81,75.22,76.44,79.58,83.81,78.03,78.37,79.75,75.43,76.34,80.54,82.61,76.74,78.82,78.93,76.62,76.04,80.96,82.78,76.7,77.05,78.07,75.0,75.91,80.97,83.16,75.61,76.56,80.07,77.49,78.52,82.95,86.71,79.68,80.31,82.63,79.07,80.19,83.04,86.02,80.05,81.08,81.28,78.14,79.03,82.56,85.67,78.35,77.12,78.9,76.24,78.22,81.57,84.55,75.51,77.82,79.55,76.57,76.78,80.07,82.67,73.85,77.12,79.08,76.99,77.44,82.31,85.88,77.19,80.33,81.22,80.73,81.71,77.93,77.08,76.52,82.67,82.26,79.95,80.11,82.91,86.27,76.99,79.07,80.82,77.94,79.64,82.97,86.47,76.58,79.25,82.07,78.83,80.9,86.37,91.35,83.88,85.95,86.73,85.57,87.87,90.49,82.05,64.84,79.83,82.17,80.91,82.57,84.3,77.92,67.76,81.82,83.81,81.89,79.86,80.34,82.88,78.15,80.81,78.53,73.97,73.96,77.13,80.93,72.76,71.69,71.92,71.48,71.92,75.52,80.31,72.83,75.18,73.8,71.1,72.67,76.93,81.14,70.53,73.96,72.37,70.16,69.44,70.44,74.6,72.39,76.32,76.76,74.57,75.25,78.73,84.15,76.9,77.5,77.53,76.15,77.36,80.66,83.78,79.85,79.83,78.89,76.19,77.48,79.46,84.06,79.55,79.84,79.48,78.71,78.85,82.28,86.24,78.93,80.46,79.91,77.74,77.85,81.9,85.37,76.04,79.7,80.01,76.85,78.12,81.48,84.75,77.2,80.09,80.07,76.55,78.02,81.61,84.58,77.75,83.5,84.35,82.5,82.75,86.42,89.27,80.85,82.44,83.61,82.75,84.28,85.85,89.25,82.37,84.37,85.47,84.39,86.02,88.52,88.7,80.64,81.98,84.52,85.31,85.92,88.01,90.02,82.14,84.47,86.48,85.58,86.6,90.38,93.15,85.27,86.69,88.15,88.14,88.99,92.1,94.41,86.32,87.6,89.83,89.48,89.72,92.7,93.96,86.01,87.59,88.49,87.25,89.06,92.33,93.35,85.07,86.94,88.67,89.03,91.38,92.59,93.5,86.89,88.45,90.23,89.33,93.08,94.19,96.36,89.65,91.19,94.55,94.55,94.05,97.21,99.79,92.2,93.22,95.17,94.98,96.22,96.01,97.59,91.24,93.31,95.75,96.7,96.64,98.48,99.5,92.37,93.98,95.44,95.97,97.12,98.98,100.2,93.57,94.59,94.53,94.29,97.42,99.43,101.2,94.75,95.48,96.52,96.53,96.99,99.01,101.35,94.32,94.86,97.04,96.91,98.38,99.82,101.74,94.82,95.98,97.01,96.68,96.51,100.26,101.91,96.05,97.23,97.97,97.65,98.04,98.1,99.73,94.18,95.48,97.0,97.91,98.33,99.54,101.93,95.75,97.0,98.2,98.52,98.26,99.67,101.37,94.93,96.34,96.37,95.37,97.37,99.53,101.5,94.55,96.08,97.51,97.53,98.71,100.79,100.02,92.81,92.07,94.79,93.4,96.11,98.43,100.11,91.14,93.0,95.65,94.95,95.27,98.76,96.99,91.28,92.04,92.79,94.26,95.04,97.69,99.59,90.29,91.66,94.99,94.46,93.57,94.83,97.41,89.16,91.0,93.43,92.13,92.12,96.05,98.5,90.67,91.9,94.32,93.79,94.83,96.73,99.43,90.55,92.54,93.87,92.84,93.8,97.11,96.8,90.0,91.23,93.39,92.37,94.33,96.89,98.29,89.33,89.04,90.23,88.07,88.93,92.04,94.61,84.47,88.19,90.05,87.97,87.85,90.41,93.41,84.07,87.52,89.82,88.28,89.02,92.59,95.63,85.25,88.22,90.55,89.79,90.56,85.51,85.7,84.58,89.08,90.89,88.02,88.21,91.39,93.92,84.42,87.45,89.5,88.34,89.06,92.18,94.09,85.39,85.99,88.97,88.45,88.99,92.99,96.68,90.29,90.9,92.39,92.59,93.48,89.71,67.16,58.14,50.51,61.23,77.42,88.34,90.57,92.68,81.26,76.99,84.16,61.68,88.11,88.86,90.45,84.66,87.28,88.15,85.71,88.04,90.56,94.98,87.23,87.11,90.35,88.57,88.8,91.37,95.59,86.09,87.3,89.97,87.21,87.36,91.36,94.8,87.27,88.24,90.32,85.9,87.07,91.37,95.84,87.37,89.12,92.75,90.49,91.17,94.25,98.24,89.98,90.98,93.02,90.51,91.93,95.79,96.67,92.2]} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flight statistics 6 | 7 | 8 | 9 | 10 |

Redirect to new URL

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jsbuild/README.md: -------------------------------------------------------------------------------- 1 | # This describes how to create a minified stripped-down version of turf.js . 2 | 3 | At first install browserify and uglify. 4 | 5 | ``` 6 | npm install -g browserify uglify 7 | ``` 8 | 9 | Install all required turf.js modules. 10 | ``` 11 | npm install @turf/meta @turf/great-circle @turf/distance @turf/helpers @turf/boolean-point-in-polygon 12 | ``` 13 | 14 | Create my_turf.min.js . 15 | ``` 16 | browserify main.js -s turf|uglifyjs -cm > ../static/my_turf.min.js 17 | ``` 18 | -------------------------------------------------------------------------------- /jsbuild/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | meta: require('@turf/meta'), 3 | greatCircle: require('@turf/great-circle'), 4 | distance: require('@turf/distance').default, 5 | booleanPointInPolygon: require('@turf/boolean-point-in-polygon').default, 6 | helpers: require('@turf/helpers') 7 | }; 8 | -------------------------------------------------------------------------------- /nginx_gzip.conf: -------------------------------------------------------------------------------- 1 | gzip on; 2 | gzip_types application/json application/javascript image/x-icon; 3 | -------------------------------------------------------------------------------- /prepare_covid_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pandas as pd 4 | import redis 5 | 6 | url = 'https://covid.ourworldindata.org/data/owid-covid-data.csv' 7 | data = pd.read_csv(url) 8 | 9 | keys = ['date', 'total_cases', 'new_cases', 'total_deaths', 'new_deaths', 10 | 'total_vaccinations', 'people_vaccinated', 'people_fully_vaccinated'] 11 | 12 | world_data = data[data.iso_code == 'OWID_WRL'][keys] 13 | covid_data = {'world': world_data.to_dict(orient='list')} 14 | json_data = json.dumps(world_data.to_dict(orient='list')).replace( 15 | 'NaN', 'null').replace('.0,', ',').replace('.0]', ']') 16 | 17 | with open('covid_data.json', 'w') as f: 18 | f.write(json_data+'\n') 19 | -------------------------------------------------------------------------------- /prepare_fir_uir_ec_only.py: -------------------------------------------------------------------------------- 1 | import geojson 2 | from shapely.geometry import shape 3 | import numpy as np 4 | 5 | import remove_third_dimension 6 | import clip_geojson_precision 7 | 8 | bounds = [] 9 | 10 | eurocontrol_fir_icaos = [ 11 | 'ENOR', 'EHAA', 'LTAA', 'LGGG', 'LECB', 'LFBB', 'LZBB', 'EDWW', 12 | 'LFRR', 'LIBB', 'EBBU', 'LHCC', 'LRBB', 'LUUU', 'EFIN', 'LTBB', 13 | 'UKDV', 'EKDK', 'UKLV', 'EDGG', 'LPPC', 'LJLA', 'EGTT', 'LECM', 14 | 'LMMM', 'LFMM', 'LIMM', 'EDMM', 'GCCC', 'LCCC', 'LFFF', 'LKAA', 15 | 'LFEE', 'EVRR', 'LIRR', 'LQSB', 'EGPX', 'EISN', 'ESAA', 'LBSR', 16 | 'LSAS', 'EETT', 'UGGG', 'LAAA', 'EYVL', 'EPWW', 'LOVV', 'UDDD', 17 | 'LDZO', 'LWSS', 'LYBA', 'UKFV', 'UKOV', 'UKBV', 'ENOB', 'EGGX', 18 | 'LPPO', 'EDUU', 'EDVV', 'EBUR', 'UKBU'] 19 | 20 | file_name = 'fir_uir_nm.json' 21 | with open(file_name) as f: 22 | data = geojson.load(f) 23 | 24 | for firuir in data['features']: 25 | properties = firuir['properties'] 26 | if firuir['properties']['AV_NAME'] == 'CANARIS UIR': 27 | firuir['properties']['AV_NAME'] = 'CANARIAS UIR' 28 | if firuir['properties']['AV_AIRSPAC'] == 'EFINFIR': 29 | firuir['properties']['AV_NAME'] = 'HELSINKI FIR' 30 | firuir['properties']['UL_VISIBLE'] = 'both' 31 | firuir['properties']['MAX_FLIGHT'] = 999 32 | elif firuir['properties']['AV_AIRSPAC'] == 'ENORFIR': 33 | firuir['properties']['AV_NAME'] = 'POLARIS FIR' 34 | elif firuir['properties']['AV_AIRSPAC'] == 'EDMMFIR': 35 | firuir['properties']['AV_NAME'] = 'MUENCHEN FIR' 36 | elif firuir['properties']['AV_AIRSPAC'] == 'LDZOFIR': 37 | firuir['properties']['AV_NAME'] = 'ZAGREB FIR/UIR' 38 | elif firuir['properties']['AV_AIRSPAC'] == 'LGGGUIR': 39 | firuir['properties']['AV_NAME'] = 'HELLAS UIR' 40 | elif firuir['properties']['AV_AIRSPAC'] == 'LPPOFIR': 41 | firuir['properties']['AV_NAME'] = 'SANTA MARIA OCEANIC FIR' 42 | elif firuir['properties']['AV_AIRSPAC'] == 'LRBBFIR': 43 | firuir['properties']['AV_NAME'] = 'BUCURESTI FIR' 44 | elif firuir['properties']['AV_AIRSPAC'] == 'UKBVUIR': 45 | firuir['properties']['AV_NAME'] = 'KYIV UIR' 46 | firuir['properties']['AV_AIRSPAC'] = 'UKBUUIR' 47 | elif firuir['properties']['AV_AIRSPAC'] == 'UKBVFIR': 48 | firuir['properties']['AV_NAME'] = 'KYIV FIR' 49 | elif firuir['properties']['AV_AIRSPAC'] == 'UKDVFIR': 50 | firuir['properties']['AV_NAME'] = 'DNIPRO FIR' 51 | elif firuir['properties']['AV_AIRSPAC'] == 'UKLVFIR': 52 | firuir['properties']['AV_NAME'] = 'LVIV FIR' 53 | elif firuir['properties']['AV_AIRSPAC'] == 'UKOVFIR': 54 | firuir['properties']['AV_NAME'] = 'ODESA FIR' 55 | elif firuir['properties']['AV_AIRSPAC'] == 'EKDKFIR': 56 | firuir['properties']['AV_NAME'] = 'KOEBENHAVN FIR' 57 | elif firuir['properties']['OBJECTID'] == 24929: 58 | firuir['properties']['MIN_FLIGHT'] = 0 59 | firuir['properties']['MAX_FLIGHT'] = 660 # TBILISI FIR 60 | firuir['properties']['AV_NAME'] = 'TBILISI FIR' 61 | 62 | polygon = shape(firuir['geometry']) 63 | firuir['bbox'] = list(polygon.bounds) 64 | if properties['MIN_FLIGHT'] == 0 and properties['MAX_FLIGHT'] > 450: 65 | properties['UL_VISIBLE'] = 'both' 66 | elif properties['MIN_FLIGHT'] == 0: 67 | properties['UL_VISIBLE'] = 'lower' 68 | elif properties['MAX_FLIGHT'] > 450: 69 | properties['UL_VISIBLE'] = 'upper' 70 | 71 | if firuir['properties']['AV_AIRSPAC'][:4] in eurocontrol_fir_icaos: 72 | bounds.append(polygon.bounds) 73 | 74 | min_bounds = np.min(bounds, axis=0) 75 | max_bounds = np.max(bounds, axis=0) 76 | ec_bounds = (min_bounds[0], min_bounds[1], max_bounds[2], max_bounds[3]) 77 | print(f'Eurocontrol area bounds: lon_ll={ec_bounds[0]}, ' 78 | f'lat_ll={ec_bounds[1]}, lon_ur={ec_bounds[2]}, lat_ur={ec_bounds[3]}') 79 | 80 | ignored_objectids = [24764, 24995, 24989, 24944, 25042, 24928, 24951, 24961,] 81 | data['features'][:] = [d for d in data['features'] if 82 | d['properties']['OBJECTID'] not in ignored_objectids] 83 | 84 | for feature in data['features']: 85 | del feature['properties']['SHAPE_AREA'] 86 | del feature['properties']['OBJECTID'] 87 | del feature['properties']['SHAPE_LEN'] 88 | del feature['properties']['AV_ICAO_ST'] 89 | del feature['properties']['AC_ID'] 90 | 91 | data['features'][:] = [d for d in data['features'] if 92 | d['properties']['AV_AIRSPAC'][:4] in eurocontrol_fir_icaos] 93 | 94 | for feature in data['features']: 95 | feature['geometry'] = remove_third_dimension.remove_third_dimension( 96 | shape(feature['geometry'])) 97 | 98 | file_name = 'static/flightmap_europe_fir_uir_ec_only.json' 99 | with open(file_name, 'w') as f: 100 | geojson.dump(data, f) 101 | 102 | clip_geojson_precision.clip(file_name) 103 | -------------------------------------------------------------------------------- /prepare_fir_uir_shapefile.py: -------------------------------------------------------------------------------- 1 | import geojson 2 | import shapely 3 | import requests 4 | import copy 5 | from shapely.geometry import shape 6 | from shapely.geometry import Polygon 7 | from shapely.ops import cascaded_union 8 | 9 | import remove_third_dimension 10 | import clip_geojson_precision 11 | 12 | file_name = 'static/flightmap_europe_fir_uir_ec_only.json' 13 | with open(file_name) as f: 14 | ec_data = geojson.load(f) 15 | 16 | file_name = 'fir_uir_nm.json' 17 | with open(file_name) as f: 18 | nm_data = geojson.load(f) 19 | 20 | file_name = 'fir_uir_ead.json' 21 | with open(file_name) as f: 22 | ead_data = geojson.load(f) 23 | 24 | uri = ('https://opendata.arcgis.com/' 25 | 'datasets/67885972e4e940b2aa6d74024901c561_0.geojson') 26 | faa_airspace_data = requests.get(uri).json() 27 | 28 | ignored_objectids = [24764, 24995, 24989, 24944, 25042, 24928, 24951, 24961,] 29 | 30 | total_bounds = Polygon([(-180, -90), (180, -90), (180, 90),(-180, 90)]) 31 | 32 | icao_south_hole = Polygon([[-120, 5], [-104.5, 10], [-92, 1.416666031], 33 | [-92, -3.4], [-90, -3.4], [-90, -15], [-120, -15], [-120, 5]]) 34 | 35 | uhhh_mask = Polygon( 36 | [[130.57, 42.52], [130.62, 42.42], [130.67, 42.33], [130.70, 42.29], 37 | [130.88, 42.15], [131.52, 41.67], [135.93, 40.5], [136, 40.55], 38 | [140, 45.75], [142, 45.75], [145.67, 44.5], [145.32, 44.05], [145.37, 43.5], 39 | [145.58, 43.4], [145.81, 43.43], [145.83, 43.38], [145.83, 43.33], 40 | [146.83, 43], [150, 45], [150, 54.87], [130.57, 42.52]]) 41 | 42 | afghanistan_pakistan_mask = Polygon( 43 | [[59.79, 37.32], [61.33, 24.66], [61.33, 23.5], [68.38, 23.5], 44 | [71.51, 23.85], [80.2, 36.15], [70.6, 38.93], [59.79, 37.32]]) 45 | 46 | car_shape = Polygon([[-120.84, 40], [-120, 5], [-104.5, 10], [-92, 1.416666031], 47 | [-92.0, -0.5], [-89.8, -0.5], [-58.8, -0.5], [-58.8, 40], [-120.84, 40]]) 48 | 49 | nam_polygons = [] 50 | afi_polygons = [] 51 | russia_polygons = [] 52 | asia_polygons = [] 53 | mid_polygons =[] 54 | sam_polygons = [] 55 | kzwy_polygons = [] 56 | uaaa_polygons = [] 57 | utaa_polygons = [] 58 | 59 | for _feature in faa_airspace_data['features']: 60 | if _feature['properties']['NAME'] == 'ANCHORAGE ARCTIC FIR': 61 | (minx, miny, maxx, maxy) = shape(_feature['geometry']).bounds 62 | north_of_alaska = Polygon([[maxx+1, miny], [minx, miny], [minx, maxy], 63 | [maxx+1, maxy]]) 64 | nam_polygons.append(north_of_alaska) 65 | if (_feature['properties']['COUNTRY'] == 'United States' and 66 | _feature['properties']['TYPE_CODE'] == 'ARTCC' and 67 | _feature['properties']['LOCAL_TYPE'] == 'ARTCC_L' or 68 | _feature['properties']['TYPE_CODE'] == 'FIR' and 69 | _feature['properties']['IDENT'] in ['PAZA', 'CZVR', 'CZWG', 'CZYZ', 70 | 'CZQM', 'CZUL']): 71 | nam_polygons.append(shape(_feature['geometry'])) 72 | if (_feature['properties']['TYPE_CODE'] == 'FIR' and 73 | _feature['properties']['IDENT'] in ['UHMM',]): 74 | russia_polygons.append(shape(_feature['geometry'])) 75 | if (_feature['properties']['TYPE_CODE'] == 'FIR' and 76 | _feature['properties']['IDENT'] in ['KZAK', 'NZZO', 'RJJJ', 77 | 'NFFF', 'RPHI', 'WAAZ', 'ANAU', 'AGGG'] 78 | or _feature['properties']['TYPE_CODE'] == 'OCA' and 79 | _feature['properties']['IDENT'] in ['NTTT',]): 80 | asia_polygons.append(shape(_feature['geometry'])) 81 | if (_feature['properties']['TYPE_CODE'] == 'FIR' and 82 | _feature['properties']['IDENT'] in ['KZWY']): 83 | _polygon = shape(_feature['geometry']) 84 | kzwy_polygons.append(_polygon) 85 | nam_shape = cascaded_union(nam_polygons) 86 | kzwy_shape = cascaded_union(kzwy_polygons) 87 | # closing gaps between USA and Canada 88 | nam_shape = nam_shape.union(Polygon( 89 | [[-117.9, 35.3], [-100.9, 31.8], [-90.1, 30], [-79.8, 31.7], [-75, 35.5], 90 | [-72, 41.2], [-71.1, 42.4], [-63.6, 44.7], [-66.0, 50.3], [-97.2, 50], 91 | [-141.5, 69.8], [-142, 60], [-135.3, 57], [-123.9, 48.8], [-122.4, 37.8]] 92 | )) 93 | 94 | ec_nm_neighbour_icaos = [ 95 | 'CZQX', 'DAAA', 'BGGL', 'UMMV', 'OLBB', 'LLLL', 'GMMM', 'UCFF', 'UMKK', 96 | 'UTDD', 'DTTC', 'UTTT', 'BIRD', 'GOOO', 'UBBA', 'GVSC', 'GLRB'] 97 | 98 | for firuir in nm_data['features']: 99 | if firuir['properties']['OBJECTID'] in ignored_objectids: 100 | continue 101 | properties = firuir['properties'] 102 | if firuir['properties']['AV_AIRSPAC'] == 'BGGLFIR': 103 | firuir['properties']['AV_NAME'] = 'NUUK FIR' 104 | elif firuir['properties']['AV_AIRSPAC'] == 'UTAKFIR': 105 | firuir['properties']['MAX_FLIGHT'] = 490 106 | elif firuir['properties']['AV_AIRSPAC'] == 'DAAAFIR': 107 | firuir['properties']['AV_NAME'] = 'ALGER FIR' 108 | elif firuir['properties']['AV_AIRSPAC'] == 'UTDDFIR': 109 | firuir['properties']['AV_NAME'] = 'DUSHANBE FIR' 110 | utdd_shape = shape(firuir['geometry']) 111 | elif firuir['properties']['AV_AIRSPAC'] == 'UCFFFIR': 112 | ucff_shape = shape(firuir['geometry']) 113 | elif firuir['properties']['AV_AIRSPAC'] == 'UTTTFIR': 114 | firuir['properties']['AV_NAME'] = 'UZBEKISTAN MERGED FIRS' 115 | elif firuir['properties']['AV_AIRSPAC'] == 'HAAAFIR': 116 | firuir['properties']['AV_NAME'] = 'ADDIS ABEBA FIR' 117 | elif firuir['properties']['AV_AIRSPAC'] == 'HUECFIR': 118 | firuir['properties']['AV_NAME'] = 'ENTEBBE FIR' 119 | elif firuir['properties']['AV_AIRSPAC'] == 'HHAAFIR': 120 | firuir['properties']['AV_NAME'] = 'ASMARA FIR' 121 | elif firuir['properties']['AV_AIRSPAC'] == 'HCSMFIR': 122 | firuir['properties']['AV_NAME'] = 'MOGADISHU FIR/UIR' 123 | elif firuir['properties']['AV_AIRSPAC'] == 'DNKKFIR': 124 | firuir['properties']['AV_NAME'] = 'KANO FIR' 125 | elif firuir['properties']['AV_AIRSPAC'] == 'HTDCFIR': 126 | firuir['properties']['AV_NAME'] = 'DAR-ES-SALAAM FIR' 127 | elif firuir['properties']['AV_AIRSPAC'] == 'HKNAFIR': 128 | firuir['properties']['AV_NAME'] = 'NAIROBI FIR' 129 | elif firuir['properties']['AV_AIRSPAC'] == 'CZZZFIR': 130 | firuir['properties']['AV_NAME'] = 'GANDER OCEANIC FIR' 131 | firuir['properties']['AV_AIRSPAC'] = 'CZQXFIR' 132 | elif firuir['properties']['AV_AIRSPAC'] == 'URRVFIR': 133 | firuir['properties']['AV_NAME'] = 'ROSTOV-NA-DONU FIR' 134 | elif firuir['properties']['AV_AIRSPAC'] == 'OLBBFIR': 135 | olbbuir = copy.deepcopy(firuir) 136 | firuir['properties']['MAX_FLIGHT'] = 195 137 | firuir['properties']['UL_VISIBLE'] = 'lower' 138 | olbbuir['properties']['MIN_FLIGHT'] = 195 139 | olbbuir['properties']['MAX_FLIGHT'] = 460 140 | olbbuir['properties']['AV_NAME'] = 'BEIRUT UIR' 141 | olbbuir['properties']['AV_AIRSPAC'] = 'OLBBUIR' 142 | olbbuir['properties']['UL_VISIBLE'] = 'upper' 143 | elif firuir['properties']['AV_AIRSPAC'] == 'GVSCFIR': 144 | gvscuir = copy.deepcopy(firuir) 145 | firuir['properties']['MAX_FLIGHT'] = 245 146 | firuir['properties']['UL_VISIBLE'] = 'lower' 147 | gvscuir['properties']['MIN_FLIGHT'] = 245 148 | gvscuir['properties']['MAX_FLIGHT'] = 999 149 | gvscuir['properties']['AV_NAME'] = 'SAL OCEANIC UIR' 150 | gvscuir['properties']['AV_AIRSPAC'] = 'GVSCUIR' 151 | gvscuir['properties']['UL_VISIBLE'] = 'upper' 152 | gvscuir['bbox'] = list(shape(gvscuir['geometry']).bounds) 153 | elif firuir['properties']['AV_AIRSPAC'] == 'GOOOFIR': 154 | if firuir['properties']['MIN_FLIGHT'] ==0: 155 | firuir['properties']['AV_NAME'] = 'DAKAR FIR' 156 | else: 157 | firuir['properties']['AV_NAME'] = 'DAKAR UIR' 158 | firuir['properties']['AV_AIRSPAC'] = 'GOOOUIR' 159 | elif firuir['properties']['OBJECTID'] == 24945: 160 | firuir['geometry']['coordinates'].pop() 161 | firuir['properties']['AV_NAME'] = 'REYKJAVIK FIR' 162 | firuir['properties']['MIN_FLIGHT'] = 0 163 | firuir['properties']['AV_AIRSPAC'] = 'BIRDFIR' 164 | firuir['properties']['UL_VISIBLE'] = 'both' 165 | elif firuir['properties']['OBJECTID'] == 24763: 166 | firuir['properties']['MAX_FLIGHT'] = 660 # MINSK FIR 167 | firuir['properties']['UL_VISIBLE'] = 'both' 168 | 169 | polygon = shape(firuir['geometry']) 170 | firuir['bbox'] = list(polygon.bounds) 171 | if properties['MIN_FLIGHT'] == 0 and properties['MAX_FLIGHT'] > 450: 172 | properties['UL_VISIBLE'] = 'both' 173 | elif properties['MIN_FLIGHT'] == 0: 174 | properties['UL_VISIBLE'] = 'lower' 175 | elif properties['MAX_FLIGHT'] > 450: 176 | properties['UL_VISIBLE'] = 'upper' 177 | 178 | del firuir['properties']['SHAPE_AREA'] 179 | del firuir['properties']['OBJECTID'] 180 | del firuir['properties']['SHAPE_LEN'] 181 | del firuir['properties']['AV_ICAO_ST'] 182 | del firuir['properties']['AC_ID'] 183 | if firuir['properties']['AV_AIRSPAC'][:4] in ec_nm_neighbour_icaos: 184 | ec_data['features'].append(firuir) 185 | if firuir['properties']['AV_AIRSPAC'][:4] in ['CCCC',]: 186 | _polygon = shape(firuir['geometry']) 187 | nam_shape = nam_shape.union(_polygon) 188 | elif firuir['properties']['AV_AIRSPAC'][:4] in ['CZQX']: 189 | _polygon = shape(firuir['geometry']) 190 | nam_shape = nam_shape.difference(_polygon) 191 | if firuir['properties']['AV_AIRSPAC'][:4] in ['UAAA', 'UACC', 'UATT', 192 | 'UAII']: 193 | _polygon = shape(firuir['geometry']) 194 | uaaa_polygons.append(_polygon) 195 | if firuir['properties']['AV_AIRSPAC'][:4] in ['UTAK', 'UTAA']: 196 | _polygon = shape(firuir['geometry']) 197 | utaa_polygons.append(_polygon) 198 | if firuir['properties']['AV_AIRSPAC'][:4] in ['ULMM', 'ULLL', 'UUWV', 199 | 'UWWW', 'URRV', 'USCM', 'UUUU']: 200 | _polygon = shape(firuir['geometry']) 201 | russia_polygons.append(_polygon) 202 | if firuir['properties']['AV_AIRSPAC'][:4] in ['FAAA', 'DGAC', 'HHAA', 203 | 'HCSM', 'DRRR', 'DNKK', 'HKNA', 'HTDC', 'HRYR', 'HBBA', 'HUEC', 'HAAA']: 204 | _polygon = shape(firuir['geometry']) 205 | afi_polygons.append(_polygon) 206 | if firuir['properties']['AV_AIRSPAC'][:4] in ['CZQX']: 207 | _polygon = shape(firuir['geometry']) 208 | kzwy_shape = kzwy_shape.difference(_polygon) 209 | if firuir['properties']['AV_AIRSPAC'][:4] in ['YYYY', 'VVVV', 'ZYYY', 210 | 'RJJJ', 'OOOO',]: 211 | _polygon = shape(firuir['geometry']) 212 | asia_polygons.append(_polygon) 213 | if firuir['properties']['AV_AIRSPAC'][:4] in ['OSTT', 'OJAC', 'HECC', 214 | 'HLLL', 'HSSS', 'OYSC', 'OKAC', 'OBBB', 'OMAE', 'OOMM', 'OOOO']: 215 | _polygon = shape(firuir['geometry']) 216 | mid_polygons.append(_polygon) 217 | if firuir['properties']['AV_AIRSPAC'][:4] in ['SBBB',]: 218 | _polygon = shape(firuir['geometry']) 219 | sam_polygons.append(_polygon) 220 | 221 | mid_shape = cascaded_union(mid_polygons) 222 | mid_shape = mid_shape.difference(afghanistan_pakistan_mask) 223 | sam_shape = cascaded_union(sam_polygons) 224 | uaaa_shape = cascaded_union(uaaa_polygons) 225 | utaa_shape = cascaded_union(utaa_polygons) 226 | russia_shape = cascaded_union(russia_polygons) 227 | russia_shape = russia_shape.intersection(total_bounds) 228 | asia_shape = cascaded_union(asia_polygons) 229 | 230 | for firuir in ead_data['features']: 231 | if firuir['properties']['IDENT'] in ['PAZA', 232 | ]: 233 | _polygon = shape(firuir['geometry']) 234 | nam_shape = nam_shape.union(_polygon) 235 | if firuir['properties']['IDENT'] == 'PAZA': 236 | nam_shape = nam_shape.union(shapely.affinity.translate( 237 | shape(firuir['geometry']), xoff=-360)) 238 | if firuir['properties']['IDENT'] in ['TTZP', 'SOOO', 'SMPM', 'SVZM', 'MPZL', 239 | 'SKEC', 'SKED',]: 240 | _polygon = shape(firuir['geometry']) 241 | sam_shape = sam_shape.union(_polygon) 242 | if firuir['properties']['IDENT'] in ['MHTG', 'NTTT',]: 243 | _polygon = shape(firuir['geometry']) 244 | sam_shape = sam_shape.difference(_polygon) 245 | if firuir['properties']['IDENT'] in ['ZKKP', 'RJJJ', 'ZWUQ', 'YBBB', 'YMMM', 246 | 'AGGG', 'AYPM', 'ANAU', 'WAAF', 'NTTT', 'NZZO', 'NFFF',]: 247 | _polygon = shape(firuir['geometry']) 248 | asia_shape = asia_shape.union(_polygon) 249 | if firuir['properties']['IDENT'] in ['MMFR',]: 250 | _polygon = shape(firuir['geometry']) 251 | car_shape = car_shape.union(_polygon) 252 | if firuir['properties']['IDENT'] in ['NTTT',]: 253 | _polygon = shape(firuir['geometry']) 254 | car_shape = car_shape.difference(_polygon) 255 | # fill residual gaps 256 | sam_shape = sam_shape.union(Polygon( 257 | [[-90, -15], [-135, -15], [-135, -89], [-90, -89]])).union( 258 | Polygon([[-80.6, 6.05], [-75.59, 6.05], [-75.59, 9.71], [-80.6, 9.71], 259 | [-80.6, 6.05]])) 260 | asia_shape = asia_shape.union(Polygon( 261 | [[180, -45.5], [180, 18.1], [107.6, 15.5], [101.8, -9.1]])) 262 | asia_shape = asia_shape.union(Polygon( 263 | [[-180, -37.4], [-166.6, -37.4], [-166.6, 4.2], [-180, 4.2]])) 264 | 265 | for _feature in faa_airspace_data['features']: 266 | if _feature['properties']['IDENT'] in ['RJJJ', 'KZMA', 'KZHU', 'KZWY', 267 | 'KZAK',]: 268 | nam_shape = nam_shape.difference(shape(_feature['geometry'])) 269 | if _feature['properties']['IDENT'] in ['NZZO', 'NTTT',]: 270 | _polygon = shape(_feature['geometry']) 271 | sam_shape = sam_shape.difference(_polygon) 272 | if _feature['properties']['IDENT'] == 'ZAN' and _feature['properties'][ 273 | 'LOCAL_TYPE'] == 'ARTCC_L' or _feature['properties']['IDENT'] in [ 274 | 'PAZA', 'KZAK']: 275 | _polygon = shape(_feature['geometry']) 276 | russia_shape = russia_shape.difference(_polygon) 277 | if _feature['properties']['IDENT'] in ['KZAK', 'NTTT',]: 278 | _polygon = shape(_feature['geometry']) 279 | car_shape = car_shape.difference(_polygon) 280 | 281 | for firuir in nm_data['features']: 282 | if firuir['properties']['AV_AIRSPAC'][:4] in ['BGGL', 'CZZZ', 'LPPO']: 283 | _polygon = shape(firuir['geometry']) 284 | nam_shape = nam_shape.difference(_polygon) 285 | if firuir['properties']['AV_AIRSPAC'][:4] in ['OIIX', 'OMAE', 'ORBB']: 286 | # add shapes after application of the afghanistan_pakistan_mask 287 | _polygon = shape(firuir['geometry']) 288 | mid_shape = mid_shape.union(_polygon) 289 | 290 | sam_shape = sam_shape.difference(icao_south_hole) 291 | car_shape = car_shape.difference(nam_shape) 292 | 293 | nam_shape = nam_shape.difference(kzwy_shape) 294 | car_shape = car_shape.difference(kzwy_shape) 295 | sam_shape = sam_shape.union(Polygon([[-78.4, 7.3], [-76.8, 6.9], [-77.3, 9.1]])) 296 | car_shape = car_shape.difference(sam_shape) 297 | 298 | russia_shape = russia_shape.union(uhhh_mask) 299 | for firuir in ead_data['features']: 300 | if firuir['properties']['IDENT'] in ['CZEG', 'RJJJ', 'ZKKP',]: 301 | russia_shape = russia_shape.difference(shape(firuir['geometry'])) 302 | 303 | russia_shape = russia_shape.union(Polygon( 304 | [[-168.9, 65.9], [-180, 60.5], [-180, 59], [-168.7, 64.8]])) 305 | russia_shape = russia_shape.intersection(total_bounds) 306 | russia_shape = russia_shape.difference(nam_shape) 307 | 308 | asia_shape = asia_shape.difference(russia_shape) 309 | russia_shape = russia_shape.union(Polygon( 310 | [[158.8, 50.1], [156.9, 49.4], [157.3, 48.9], [159, 50]])) 311 | russia_shape = russia_shape.difference(asia_shape) 312 | asia_shape = asia_shape.difference(nam_shape) 313 | nam_shape = nam_shape.union(Polygon( 314 | [[160, 48.6], [162.8, 45.4], [163.4, 45.5], [160.5, 48.8]])) 315 | nam_shape = nam_shape.difference(asia_shape) 316 | asia_shape = asia_shape.difference(mid_shape) 317 | asia_shape = asia_shape.difference(uaaa_shape) 318 | asia_shape = asia_shape.difference(utaa_shape) 319 | asia_shape = asia_shape.difference(utdd_shape) 320 | asia_shape = asia_shape.difference(ucff_shape) 321 | nam_shape = nam_shape.intersection(total_bounds) 322 | 323 | afi_shape = cascaded_union(afi_polygons) 324 | afi_region = _feature = { 325 | "properties": { 326 | "AV_NAME": "AFRICA-INDIAN OCEAN (AFI) REGION", 327 | "AV_AIRSPAC": "FFFFFIR", 328 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 329 | "type": "Feature", 330 | "geometry": afi_shape, 331 | "bbox": afi_shape.bounds 332 | } 333 | nam_region = _feature = { 334 | "properties": { 335 | "AV_NAME": "NORTH AMERICAN (NAM) REGION", 336 | "AV_AIRSPAC": "CCCCFIR", 337 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 338 | "type": "Feature", 339 | "geometry": nam_shape, 340 | "bbox": nam_shape.bounds 341 | } 342 | car_region = _feature = { 343 | "properties": { 344 | "AV_NAME": "CARIBBEAN (CAR) REGION", 345 | "AV_AIRSPAC": "MMMMFIR", 346 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 347 | "type": "Feature", 348 | "geometry": car_shape, 349 | "bbox": car_shape.bounds 350 | } 351 | uaaa_region = _feature = { 352 | "properties": { 353 | "AV_NAME": "KAZAHKSTAN MERGED FIRS", 354 | "AV_AIRSPAC": "UAAAFIR", 355 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 356 | "type": "Feature", 357 | "geometry": uaaa_shape, 358 | "bbox": uaaa_shape.bounds 359 | } 360 | utaa_region = _feature = { 361 | "properties": { 362 | "AV_NAME": "TURKMENISTAN MERGED FIRS", 363 | "AV_AIRSPAC": "UTAAFIR", 364 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 365 | "type": "Feature", 366 | "geometry": utaa_shape, 367 | "bbox": utaa_shape.bounds 368 | } 369 | russia_region = _feature = { 370 | "properties": { 371 | "AV_NAME": "RUSSIA MERGED FIRS", 372 | "AV_AIRSPAC": "UUUUFIR", 373 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 374 | "type": "Feature", 375 | "geometry": russia_shape, 376 | "bbox": russia_shape.bounds 377 | } 378 | asia_region = _feature = { 379 | "properties": { 380 | "AV_NAME": "ASIA/PACIFIC (ASIA/PAC) REGION", 381 | "AV_AIRSPAC": "YYYYFIR", 382 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 383 | "type": "Feature", 384 | "geometry": asia_shape, 385 | "bbox": asia_shape.bounds 386 | } 387 | mid_region = _feature = { 388 | "properties": { 389 | "AV_NAME": "MIDDLE EAST (MID) REGION", 390 | "AV_AIRSPAC": "OOOOFIR", 391 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 392 | "type": "Feature", 393 | "geometry": mid_shape, 394 | "bbox": mid_shape.bounds 395 | } 396 | kzwy_region = _feature = { 397 | "properties": { 398 | "AV_NAME": "NEW YORK OCEANIC EAST FIR", 399 | "AV_AIRSPAC": "KZWYFIR", 400 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 401 | "type": "Feature", 402 | "geometry": kzwy_shape, 403 | "bbox": kzwy_shape.bounds 404 | } 405 | sam_region = _feature = { 406 | "properties": { 407 | "AV_NAME": "SOUTH AMERICAN (SAM) REGION", 408 | "AV_AIRSPAC": "SSSSFIR", 409 | "MIN_FLIGHT": 0, "MAX_FLIGHT": 999, "UL_VISIBLE": "both"}, 410 | "type": "Feature", 411 | "geometry": sam_shape, 412 | "bbox": sam_shape.bounds 413 | } 414 | polygon = shape(olbbuir['geometry']) 415 | olbbuir['bbox'] = list(polygon.bounds) 416 | ec_data['features'].extend([ 417 | uaaa_region, utaa_region, gvscuir, olbbuir, afi_region, mid_region, 418 | sam_region, russia_region, kzwy_region, car_region, nam_region, asia_region, 419 | ]) 420 | 421 | for feature in ec_data['features']: 422 | feature['geometry'] = remove_third_dimension.remove_third_dimension( 423 | shape(feature['geometry'])) 424 | 425 | file_name = 'static/flightmap_europe_fir_uir.json' 426 | with open(file_name, 'w') as f: 427 | geojson.dump(ec_data, f) 428 | 429 | clip_geojson_precision.clip(file_name) 430 | -------------------------------------------------------------------------------- /prepare_recurring_callsigns.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import pandas as pd 4 | from sqlalchemy import select, func, text 5 | from pyopensky.schema import StateVectorsData4 6 | from pyopensky.trino import Trino 7 | 8 | raw_callsign_pattern = re.compile( 9 | r"^(?P[A-Z]{3})0*(?P[1-9][A-Z0-9]*)$" 10 | ) 11 | callsign_pattern = re.compile( 12 | r"^(?:[A-Z]{3})[1-9](?:(?:[0-9]{0,3})|(?:[0-9]{0,2})" 13 | "(?:[A-Z])|(?:[0-9]?)(?:[A-Z]{2}))$" 14 | ) 15 | 16 | 17 | def recombine_callsign_components(callsign): 18 | raw_match = raw_callsign_pattern.match(callsign) 19 | if not raw_match: 20 | return 21 | combined_callsign = raw_match.group("operator") + raw_match.group("suffix") 22 | if not callsign_pattern.match(combined_callsign): 23 | return 24 | return combined_callsign 25 | 26 | 27 | def fetch_data(trino_connection, start_hour, stop_hour): 28 | query = ( 29 | select( 30 | StateVectorsData4.callsign, 31 | func.min(StateVectorsData4.time).label("first_seen"), 32 | func.max(StateVectorsData4.time).label("last_seen"), 33 | ) 34 | .where( 35 | StateVectorsData4.hour >= start_hour, 36 | StateVectorsData4.hour < stop_hour, 37 | StateVectorsData4.callsign.isnot(None), 38 | StateVectorsData4.onground == False, 39 | StateVectorsData4.time.isnot(None), 40 | StateVectorsData4.icao24.isnot(None), 41 | StateVectorsData4.lat.isnot(None), 42 | StateVectorsData4.lon.isnot(None), 43 | StateVectorsData4.velocity.isnot(None), 44 | StateVectorsData4.heading.isnot(None), 45 | StateVectorsData4.vertrate.isnot(None), 46 | StateVectorsData4.baroaltitude.isnot(None), 47 | StateVectorsData4.lastposupdate.isnot(None), 48 | StateVectorsData4.baroaltitude <= 18288, 49 | text( 50 | "REGEXP_LIKE(callsign, " 51 | "'^[A-Z][A-Z][A-Z][0-9][0-9]?[0-9A-Z]?[0-9A-Z]?')" 52 | ), 53 | ) 54 | .group_by(StateVectorsData4.callsign) 55 | ) 56 | return trino_connection.query(query) 57 | 58 | 59 | stop_ts = pd.Timestamp.utcnow().floor("D") - pd.Timedelta(hours=1) 60 | start_ts = stop_ts - pd.Timedelta(days=22) 61 | stop_hour = stop_ts.floor("1h") 62 | start_hour = start_ts.floor("1h") 63 | 64 | _before = pd.Timestamp.utcnow() 65 | trino = Trino() 66 | callsign_occurences = fetch_data(trino, start_hour, stop_hour) 67 | _after = pd.Timestamp.utcnow() 68 | duration = (_after - _before).total_seconds() 69 | 70 | callsign_occurences.callsign = callsign_occurences.callsign.str.rstrip() 71 | callsign_occurences["callsign"] = callsign_occurences["callsign"].apply( 72 | recombine_callsign_components 73 | ) 74 | callsign_occurences.dropna(subset=["callsign"], inplace=True) 75 | 76 | recurring_callsigns = callsign_occurences[ 77 | callsign_occurences.last_seen - callsign_occurences.first_seen 78 | > pd.Timedelta(days=1) 79 | ] 80 | _json_data = json.dumps( 81 | { 82 | "start_date": start_ts.timestamp(), 83 | "end_date": stop_ts.timestamp(), 84 | "recurring_callsigns": recurring_callsigns.callsign.to_list(), 85 | } 86 | ) 87 | with open("flightroutes/recurring_callsigns.json", "w") as f: 88 | f.write(_json_data + "\n") 89 | 90 | print( 91 | f"Found {len(recurring_callsigns)} recurring callsigns out of " 92 | f"{len(callsign_occurences)} different callsigns in the time range from " 93 | f"{start_hour.strftime('%Y-%m-%d %H:%M')} to " 94 | f"{stop_hour.strftime('%Y-%m-%d %H:%M')} within {duration:.1f}s." 95 | ) 96 | -------------------------------------------------------------------------------- /prepare_static_airports.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import csv 4 | import requests 5 | import json 6 | import re 7 | from collections import Counter 8 | import clip_geojson_precision 9 | 10 | CSV_URL = 'https://ourairports.com/data/airports.csv' 11 | 12 | accepted_icaos = [] 13 | ignored_types = [] 14 | # possible types: 'balloonport', 'heliport', 'seaplane_base', 'small_airport', 15 | # 'medium_airport', 'large_airport', '' 16 | 17 | icao_pattern = re.compile('^[A-Z]{4}$') 18 | 19 | with requests.Session() as s: 20 | download = s.get(CSV_URL) 21 | download.encoding = 'utf-8' 22 | reader = csv.DictReader(download.text.splitlines(), delimiter=',') 23 | airports = [ 24 | row for row in reader if icao_pattern.match(row['gps_code']) is not None] 25 | 26 | icao_count = Counter() 27 | for row in airports: 28 | icao_count.update([row['gps_code']]) 29 | icao_count.subtract(list(icao_count)) 30 | duplicate_icaos = set(+icao_count) 31 | 32 | _features = [] 33 | for row in airports: 34 | if row['type'] in ignored_types: 35 | continue 36 | if not len(row['iata_code']) in (0, 3): 37 | if row['iata_code'] == '0': 38 | row['iata_code'] = '' 39 | else: 40 | continue 41 | if row['type'] == 'closed': 42 | continue 43 | if len(row['iata_code']) != 3 and not row['gps_code'] in accepted_icaos: 44 | continue 45 | if row['scheduled_service'] == 'no': 46 | continue 47 | if row['gps_code'] in duplicate_icaos and row['ident'] != row['gps_code']: 48 | print( 49 | f"ignoring duplicate entry {row['ident']} for " 50 | f"{row['gps_code']} / {row['iata_code']}.") 51 | continue 52 | _point = {"type": "Point", "coordinates": [float(row['longitude_deg']), 53 | float(row['latitude_deg'])]} 54 | _feature = {"geometry": _point, "type": "Feature", "properties": { 55 | 'name': row['name'], 56 | 'icao': row['gps_code'], 57 | 'iata': row['iata_code'], 58 | 'type': row['type'] 59 | }} 60 | _features.append(_feature) 61 | _collection = {"type": "FeatureCollection", "properties": {}, 62 | "features": _features} 63 | 64 | file_name = 'static/airports_static.json' 65 | print(f"writing {len(_features)} airports to {file_name}.") 66 | with open(file_name, 'w') as f: 67 | json.dump(_collection, f) 68 | 69 | clip_geojson_precision.clip(file_name) 70 | -------------------------------------------------------------------------------- /prepare_vrs_database.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import os 4 | import sqlite3 5 | import gzip 6 | import numpy as np 7 | import math 8 | import json 9 | 10 | def get_distance(lat1, lon1, lat2, lon2): 11 | if None in (lat1, lon1, lat2, lon2): 12 | return 13 | degRad = 2 * math.pi / 360 14 | distance = ( 15 | 6.370e6 * math.acos(math.sin(lat1 * degRad) * math.sin(lat2 * degRad) 16 | + math.cos(lat1 * degRad) * math.cos(lat2 * degRad) * math.cos((lon2 - lon1) 17 | * degRad))) 18 | return int(round(distance)) 19 | 20 | vrs_url = "http://www.virtualradarserver.co.uk/Files/StandingData.sqb.gz" 21 | directory = "flightroutes/" 22 | file_info = {} 23 | if os.path.isfile(directory + "StandingData.sqb"): 24 | file_info['vrs'] = os.stat(directory + "StandingData.sqb").st_ctime 25 | 26 | def check_data(): 27 | # if not os.path.isfile(directory + "StandingData.sqb") or ( 28 | # time.time() - file_info['vrs'] > 86400): 29 | download_vrs_database() 30 | create_flightroute_table() 31 | 32 | def download_vrs_database(): 33 | if not os.path.exists(directory): 34 | os.makedirs(directory) 35 | r = requests.get(vrs_url) 36 | if r.status_code == requests.codes.ok: 37 | with open(directory + "StandingData.sqb.gz", 'wb') as f: 38 | f.write(r.content) 39 | with gzip.GzipFile(directory + "StandingData.sqb.gz", 'rb') as infile: 40 | data = infile.read() 41 | with open(directory + "StandingData.sqb", 'wb') as outfile: 42 | outfile.write(data) 43 | file_info['vrs'] = time.time() 44 | 45 | def create_flightroute_table(): 46 | """ 47 | SQL statements were formatted using 48 | https://www.freeformatter.com/sql-formatter.html 49 | """ 50 | connection = sqlite3.connect(os.path.join(directory, "StandingData.sqb")) 51 | connection.create_function('distance', 4, get_distance) 52 | cursor = connection.cursor() 53 | cursor.execute("DROP TABLE IF EXISTS FlightRoute") 54 | cursor.execute(""" 55 | CREATE TABLE FlightRoute AS 56 | SELECT 57 | Callsign, 58 | OperatorIcao, 59 | OperatorIata, 60 | OperatorName, 61 | FromAirportIcao || '-' || IFNULL(GROUP_CONCAT( 62 | RouteStopView.AirportIcao, '-') || '-', '') || ToAirportIcao 63 | AS Route 64 | FROM 65 | RouteView 66 | LEFT JOIN 67 | RouteStopView 68 | ON RouteView.RouteId = RouteStopView.RouteId 69 | WHERE 70 | LENGTH(FromAirportIcao) > 0 71 | AND LENGTH(ToAirportIcao) > 0 72 | GROUP BY 73 | Callsign 74 | """) 75 | with open(os.path.join(directory, "recurring_callsigns.json")) as f: 76 | recurring_callsigns = json.load(f)['recurring_callsigns'] 77 | 78 | cursor.execute("DROP TABLE IF EXISTS RecentFlightRoutes") 79 | cursor.execute(""" 80 | CREATE TABLE RecentFlightRoutes AS 81 | SELECT 82 | Callsign, 83 | OperatorIcao, 84 | Route 85 | FROM 86 | FlightRoute 87 | WHERE 88 | Callsign IN {} 89 | """.format(tuple(recurring_callsigns))) 90 | 91 | cursor.execute("DROP TABLE IF EXISTS FlightLegs") 92 | cursor.execute(""" 93 | CREATE TABLE FlightLegs AS 94 | SELECT 95 | Origin, 96 | Destination, 97 | OperatorIcao, 98 | DISTANCE(o.Latitude, o. Longitude, d.Latitude, d.Longitude) 99 | AS Length 100 | FROM 101 | ( 102 | SELECT DISTINCT 103 | SUBSTR(Route, 1, 4) AS Origin, 104 | SUBSTR(Route, 6, 4) AS Destination, 105 | OperatorIcao 106 | FROM 107 | RecentFlightRoutes 108 | UNION 109 | SELECT DISTINCT 110 | SUBSTR(Route, 6, 4) AS Origin, 111 | SUBSTR(Route, 11, 4) AS Destination, 112 | OperatorIcao 113 | FROM 114 | RecentFlightRoutes 115 | WHERE 116 | LENGTH(Destination) > 0 117 | UNION 118 | SELECT DISTINCT 119 | SUBSTR(Route, 11, 4) AS Origin, 120 | SUBSTR(Route, 16, 4) AS Destination, 121 | OperatorIcao 122 | FROM 123 | RecentFlightRoutes 124 | WHERE 125 | LENGTH(Destination) > 0 126 | UNION 127 | SELECT DISTINCT 128 | SUBSTR(Route, 16, 4) AS Origin, 129 | SUBSTR(Route, 21, 4) AS Destination, 130 | OperatorIcao 131 | FROM 132 | RecentFlightRoutes 133 | WHERE 134 | LENGTH(Destination) > 0 135 | UNION 136 | SELECT DISTINCT 137 | SUBSTR(Route, 21, 4) AS Origin, 138 | SUBSTR(Route, 26, 4) AS Destination, 139 | OperatorIcao 140 | FROM 141 | RecentFlightRoutes 142 | WHERE 143 | LENGTH(Destination) > 0 144 | UNION 145 | SELECT DISTINCT 146 | SUBSTR(Route, 26, 4) AS Origin, 147 | SUBSTR(Route, 31, 4) AS Destination, 148 | OperatorIcao 149 | FROM 150 | RecentFlightRoutes 151 | WHERE 152 | LENGTH(Destination) > 0 153 | UNION 154 | SELECT DISTINCT 155 | SUBSTR(Route, 31, 4) AS Origin, 156 | SUBSTR(Route, 36, 4) AS Destination, 157 | OperatorIcao 158 | FROM 159 | RecentFlightRoutes 160 | WHERE 161 | LENGTH(Destination) > 0 162 | UNION 163 | SELECT DISTINCT 164 | SUBSTR(Route, 36, 4) AS Origin, 165 | SUBSTR(Route, 41, 4) AS Destination, 166 | OperatorIcao 167 | FROM 168 | RecentFlightRoutes 169 | WHERE 170 | LENGTH(Destination) > 0 171 | UNION 172 | SELECT DISTINCT 173 | SUBSTR(Route, 41, 4) AS Origin, 174 | SUBSTR(Route, 46, 4) AS Destination, 175 | OperatorIcao 176 | FROM 177 | RecentFlightRoutes 178 | WHERE 179 | LENGTH(Destination) > 0 180 | UNION 181 | SELECT DISTINCT 182 | SUBSTR(Route, 46, 4) AS Origin, 183 | SUBSTR(Route, 51, 4) AS Destination, 184 | OperatorIcao 185 | FROM 186 | RecentFlightRoutes 187 | WHERE 188 | LENGTH(Destination) > 0 189 | UNION 190 | SELECT DISTINCT 191 | SUBSTR(Route, 51, 4) AS Origin, 192 | SUBSTR(Route, 56, 4) AS Destination, 193 | OperatorIcao 194 | FROM 195 | RecentFlightRoutes 196 | WHERE 197 | LENGTH(Destination) > 0 198 | ) 199 | , 200 | Airport AS o, 201 | Airport AS d 202 | WHERE 203 | Origin = o.Icao 204 | AND Destination = d.Icao 205 | AND Origin != Destination 206 | """) 207 | cursor.execute("ALTER TABLE Airport ADD COLUMN Destinations INTEGER " 208 | "DEFAULT 0") 209 | cursor.execute("ALTER TABLE Airport ADD COLUMN Origins INTEGER DEFAULT 0") 210 | cursor.execute("UPDATE Airport SET Destinations = (SELECT COUNT(" 211 | "Destination) FROM FlightLegs WHERE Origin=Airport.Icao)") 212 | cursor.execute("UPDATE Airport SET Origins = (SELECT COUNT(Origin) FROM " 213 | "FlightLegs WHERE Destination=Airport.Icao)") 214 | connection.commit() 215 | connection.close() 216 | 217 | if __name__ == '__main__': 218 | 219 | check_data() 220 | -------------------------------------------------------------------------------- /remove_third_dimension.py: -------------------------------------------------------------------------------- 1 | # taken from https://gis.stackexchange.com/a/220374 2 | from shapely.geometry import * 3 | 4 | def remove_third_dimension(geom): 5 | if geom.is_empty: 6 | return geom 7 | 8 | if isinstance(geom, Polygon): 9 | exterior = geom.exterior 10 | new_exterior = remove_third_dimension(exterior) 11 | 12 | interiors = geom.interiors 13 | new_interiors = [] 14 | for int in interiors: 15 | new_interiors.append(remove_third_dimension(int)) 16 | 17 | return Polygon(new_exterior, new_interiors) 18 | 19 | elif isinstance(geom, LinearRing): 20 | return LinearRing([xy[0:2] for xy in list(geom.coords)]) 21 | 22 | elif isinstance(geom, LineString): 23 | return LineString([xy[0:2] for xy in list(geom.coords)]) 24 | 25 | elif isinstance(geom, Point): 26 | return Point([xy[0:2] for xy in list(geom.coords)]) 27 | 28 | elif isinstance(geom, MultiPoint): 29 | points = list(geom.geoms) 30 | new_points = [] 31 | for point in points: 32 | new_points.append(remove_third_dimension(point)) 33 | 34 | return MultiPoint(new_points) 35 | 36 | elif isinstance(geom, MultiLineString): 37 | lines = list(geom.geoms) 38 | new_lines = [] 39 | for line in lines: 40 | new_lines.append(remove_third_dimension(line)) 41 | 42 | return MultiLineString(new_lines) 43 | 44 | elif isinstance(geom, MultiPolygon): 45 | pols = list(geom.geoms) 46 | 47 | new_pols = [] 48 | for pol in pols: 49 | new_pols.append(remove_third_dimension(pol)) 50 | 51 | return MultiPolygon(new_pols) 52 | 53 | elif isinstance(geom, GeometryCollection): 54 | geoms = list(geom.geoms) 55 | 56 | new_geoms = [] 57 | for geom in geoms: 58 | new_geoms.append(remove_third_dimension(geom)) 59 | 60 | return GeometryCollection(new_geoms) 61 | 62 | else: 63 | raise RuntimeError("Currently this type of geometry is not supported: {}".format(type(geom))) 64 | -------------------------------------------------------------------------------- /static/aiga_air_transportation.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 40 | 43 | 48 | 50 | 52 | 54 | 56 | image/svg+xml 59 | 62 | 65 | 67 | 70 | Openclipart 73 | 75 | 77 | aiga air transportation 80 | 2009-04-22T07:11:50 83 | Set of international airport symbols. \n \nSource: http://www.aiga.org/content.cfm/symbol-signs \n \nConverted to SVG by Jean-Victor Balin. 86 | https://openclipart.org/detail/25248/aiga-air-transportation-by-anonymous-25248 89 | 91 | 93 | Anonymous 96 | 98 | 100 | 102 | 104 | aiga 107 | aiga no bg 110 | airplane 113 | airport 116 | black and white 119 | externalsource 122 | icon 125 | map symbol 128 | sign 131 | silhouette 134 | symbol 137 | 139 | 141 | 143 | 146 | 149 | 152 | 155 | 157 | 159 | 161 | 163 | -------------------------------------------------------------------------------- /static/aiga_air_transportation_orange.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 40 | 43 | 48 | 50 | 52 | 54 | 56 | image/svg+xml 59 | 62 | 65 | 67 | 70 | Openclipart 73 | 75 | 77 | aiga air transportation 80 | 2009-04-22T07:11:50 83 | Set of international airport symbols. \n \nSource: http://www.aiga.org/content.cfm/symbol-signs \n \nConverted to SVG by Jean-Victor Balin. 86 | https://openclipart.org/detail/25248/aiga-air-transportation-by-anonymous-25248 89 | 91 | 93 | Anonymous 96 | 98 | 100 | 102 | 104 | aiga 107 | aiga no bg 110 | airplane 113 | airport 116 | black and white 119 | externalsource 122 | icon 125 | map symbol 128 | sign 131 | silhouette 134 | symbol 137 | 139 | 141 | 143 | 146 | 149 | 152 | 155 | 157 | 159 | 161 | 163 | -------------------------------------------------------------------------------- /static/aircraft_interactive.js: -------------------------------------------------------------------------------- 1 | var downloadingRoute = false; 2 | var activeFeature; 3 | 4 | function processedActiveFeature(feature) { 5 | if (activeFeature != undefined && feature.properties.callsign == activeFeature.properties.callsign) { 6 | activeAircraftMarker.clearLayers(); 7 | activeFeature = feature; 8 | activeAircraftMarker.addData(feature); 9 | return true; 10 | } 11 | return false; 12 | } 13 | var activePlaneIcon = L.icon({ 14 | iconUrl: 'aiga_air_transportation_orange.svg', 15 | iconSize: [16, 16] 16 | }) 17 | 18 | activeAircraftMarker = L.geoJSON(null, { 19 | onEachFeature: function(feature, layer) { 20 | var tooltipContent = 21 | "" + feature.properties.callsign + "
" + 22 | "FL " + feature.properties.flight_level + "
" + 23 | feature.properties.heading + " deg
" + 24 | feature.properties.ground_speed + " kn
" + 25 | feature.properties.vertical_speed + " ft/min"; 26 | layer.bindTooltip(tooltipContent, { 27 | direction: "top", 28 | offset: [0, -5] 29 | }); 30 | }, 31 | pointToLayer: function(feature, latlng) { 32 | return L.marker(latlng, { 33 | icon: activePlaneIcon, 34 | rotationOrigin: 'center center', 35 | rotationAngle: feature['properties']['heading'] 36 | }) 37 | } 38 | }).addTo(map); 39 | 40 | // method that we will use to update the control based on feature properties passed 41 | info.update = function(callsign, props) { 42 | var text = '' + props.callsign + '
' + 43 | props.operator_name + '
'; 44 | if (typeof props.flight_number !== 'undefined') { 45 | if (props.operator_iata === undefined || props.operator_iata == '') { 46 | text += props.operator_icao + ' ' + props.flight_number + '
'; 47 | } else { 48 | text += props.operator_iata + ' ' + props.flight_number + '
'; 49 | } 50 | } 51 | for (index = 0; index < props.airports.length; index++) { 52 | text += props.airports[index].name + ' (' + props.airports[index].icao; 53 | if (props.airports[index].iata !== undefined && props.airports[index].iata != '') 54 | text += ' / ' + props.airports[index].iata; 55 | text += ')
'; 56 | if (index < props.airports.length - 1) { 57 | text += ' – '; 58 | } 59 | } 60 | this._div.innerHTML = '

Route information

' + text; 61 | }; 62 | 63 | info.unknown = function(callsign) { 64 | this._div.innerHTML = '

Route information

' + callsign + ' is unknown'; 65 | routePlot.clearLayers(); 66 | }; 67 | 68 | info.invalid = function(callsign) { 69 | this._div.innerHTML = '

Route information

' + callsign + ' is not an airline callsign'; 70 | routePlot.clearLayers(); 71 | }; 72 | 73 | info.reset = function() { 74 | this._div.innerHTML = '

Route information

Click on any aircraft or airport'; 75 | routePlot.clearLayers(); 76 | }; 77 | 78 | info.addTo(map); 79 | 80 | var callsignSearch = L.control({ 81 | position: 'topright' 82 | }); 83 | callsignSearch.onAdd = function(map) { 84 | this._div = L.DomUtil.create('div', 'info legend'); 85 | this._div.innerHTML = 86 | '

Callsign search

' + 87 | '' + 90 | ' '; 91 | L.DomEvent.disableClickPropagation(this._div); 92 | return this._div; 93 | }; 94 | callsignSearch.addTo(map) 95 | const callsignInput = document.getElementById('callsignInput'); 96 | const searchButton = document.getElementById('searchButton'); 97 | callsignInput.addEventListener('keyup', function(event) { 98 | isValidCallsign = callsignInput.checkValidity(); 99 | if (isValidCallsign) { 100 | searchButton.disabled = false; 101 | } else { 102 | searchButton.disabled = true; 103 | } 104 | }); 105 | searchButton.addEventListener('click', function(event) { 106 | if (callsignInput.value.length == 3) 107 | airlineRoutesInfo(callsignInput.value.toUpperCase()); 108 | else 109 | routeInfo(callsignInput.value.toUpperCase(), true); 110 | }); 111 | 112 | function clickAircraft(eo) { 113 | // add the previously active feature to the aircraft markers. 114 | if (typeof activeFeature != 'undefined') { 115 | aircraftMarkers.addData(activeFeature); 116 | clusteredAircraftMarkers.clearLayers(); 117 | clusteredAircraftMarkers.addLayer(aircraftMarkers); 118 | } 119 | // remember the clicked feature 120 | activeFeature = eo.target.feature; 121 | // remove the selected feature from aircraft markers 122 | eo.target.remove(); 123 | // and clear active marker 124 | activeAircraftMarker.clearLayers(); 125 | activeAircraftMarker.addData(activeFeature); 126 | routeInfo(activeFeature.properties.callsign); 127 | } 128 | 129 | function clickAirport(eo) { 130 | // add the previously active feature to the aircraft markers. 131 | if (typeof activeFeature != 'undefined') { 132 | aircraftMarkers.addData(activeFeature); 133 | clusteredAircraftMarkers.clearLayers(); 134 | clusteredAircraftMarkers.addLayer(aircraftMarkers); 135 | } 136 | activeFeature = undefined; 137 | // and clear active marker 138 | activeAircraftMarker.clearLayers(); 139 | info.reset(); 140 | routesInfo(eo.target.feature.properties); 141 | } 142 | 143 | function routeInfo(callsign, fitBounds) { 144 | fitBounds = fitBounds || false; 145 | if (downloadingRoute == true) { 146 | return 147 | }; 148 | downloadingRoute = true; 149 | var xhr = new XMLHttpRequest(); 150 | xhr.open('GET', './api/geojson/callsign/?callsign=' + callsign); 151 | xhr.setRequestHeader('Content-Type', 'application/json'); 152 | xhr.onload = function() { 153 | if (xhr.status === 200) { 154 | var route_info = JSON.parse(xhr.responseText); 155 | if (typeof route_info.features === 'undefined') { 156 | info.unknown(callsign); 157 | } else { 158 | info.update(callsign, route_info.features[0].properties); 159 | routePlot.clearLayers(); 160 | turf.meta.segmentEach(route_info, function(currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) { 161 | var start = currentSegment.geometry.coordinates[0]; 162 | var end = currentSegment.geometry.coordinates[1]; 163 | routePlot.addData(turf.greatCircle(start, end)); 164 | }); 165 | if (fitBounds) 166 | map.fitBounds(routePlot.getBounds()); 167 | } 168 | } else if (xhr.status === 422) { 169 | info.invalid(callsign); 170 | } 171 | downloadingRoute = false; 172 | }; 173 | xhr.send(); 174 | } 175 | 176 | info.updateAirlineInfo = function(airlineInfo) { 177 | var text = '' + airlineInfo.operator_name + ' (' + airlineInfo.operator_icao; 178 | if (airlineInfo.operator_iata !== undefined && airlineInfo.operator_iata != '') 179 | text += ' / ' + airlineInfo.operator_iata; 180 | text += ')
'; 181 | text += ''; 182 | this._div.innerHTML = text; 183 | } 184 | 185 | function airlineRoutesInfo(operatorIcao) { 186 | var xhr = new XMLHttpRequest(); 187 | xhr.open('GET', './api/geojson/airline/?icao=' + operatorIcao); 188 | xhr.setRequestHeader('Content-Type', 'application/json'); 189 | xhr.onload = function() { 190 | if (xhr.status === 200) { 191 | var airlineInfo = JSON.parse(xhr.responseText); 192 | routePlot.clearLayers(); 193 | if (typeof airlineInfo.features === 'undefined') { 194 | info.unknown(operatorIcao); 195 | } else { 196 | turf.meta.propEach(airlineInfo, function(currentProperties, featureIndex) { 197 | info.updateAirlineInfo(currentProperties); 198 | }); 199 | turf.meta.segmentEach(airlineInfo, function(currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) { 200 | var start = currentSegment.geometry.coordinates[0]; 201 | var end = currentSegment.geometry.coordinates[1]; 202 | var distance = turf.distance(start, end); 203 | if (distance > 0) 204 | routePlot.addData(turf.greatCircle(start, end, { 205 | npoints: Math.round(distance * 0.009), 206 | offset: 10 207 | })); 208 | }); 209 | } 210 | } 211 | }; 212 | xhr.send(); 213 | } 214 | 215 | map.on('click', function(eo) { 216 | // add the previously active feature to the aircraft markers. 217 | if (typeof activeFeature != 'undefined') { 218 | aircraftMarkers.addData(activeFeature); 219 | clusteredAircraftMarkers.clearLayers(); 220 | clusteredAircraftMarkers.addLayer(aircraftMarkers); 221 | } 222 | activeFeature = undefined; 223 | activeAircraftMarker.clearLayers(); 224 | info.reset(); 225 | }); 226 | -------------------------------------------------------------------------------- /static/aircraft_static.js: -------------------------------------------------------------------------------- 1 | map.attributionControl.addAttribution( 2 | '© The OpenSky Network'); 3 | var aircraftPositions; 4 | var downloadingPositions = false; 5 | 6 | var planeIcon = L.icon({ 7 | iconUrl: 'aiga_air_transportation.svg', 8 | iconSize: [16, 16] 9 | }) 10 | function processedActiveFeature(feature) { 11 | return false; 12 | } 13 | function clickAircraft(eo) { 14 | return; 15 | } 16 | var aircraftMarkers = L.geoJSON(null, { 17 | onEachFeature: function(feature, layer) { 18 | layer.once('click', function(eo) { 19 | clickAircraft(eo); 20 | }); 21 | var tooltipContent = 22 | "" + feature.properties.callsign + "
" + 23 | "FL " + feature.properties.flight_level + "
" + 24 | feature.properties.heading + " deg
" + 25 | feature.properties.ground_speed + " kn
" + 26 | feature.properties.vertical_speed + " ft/min"; 27 | layer.bindTooltip(tooltipContent, { 28 | direction: "top", 29 | offset: [0, -5] 30 | }); 31 | }, 32 | filter: function(feature) { 33 | if (processedActiveFeature(feature)) 34 | return false; 35 | switch (feature.properties.ul_visible) { 36 | case 'both': 37 | return true; 38 | case 'lower': 39 | return (map.hasLayer(lowerAirspace) || !map.hasLayer(upperAirspace)); 40 | case 'upper': 41 | return (map.hasLayer(upperAirspace) || !map.hasLayer(lowerAirspace)); 42 | } 43 | }, 44 | pointToLayer: function(feature, latlng) { 45 | return L.marker(latlng, { 46 | icon: planeIcon, 47 | rotationOrigin: 'center center', 48 | rotationAngle: feature['properties']['heading'] 49 | }); 50 | } 51 | }); 52 | var clusteredAircraftMarkers = L.markerClusterGroup({ 53 | disableClusteringAtZoom: 8, 54 | spiderfyOnMaxZoom: false, 55 | maxClusterRadius: 60 56 | }); 57 | map.addLayer(clusteredAircraftMarkers); 58 | layerControl.addOverlay(clusteredAircraftMarkers, "Aircraft"); 59 | 60 | function reloadAircraftPositions() { 61 | aircraftMarkers.clearLayers(); 62 | aircraftMarkers.addData(aircraftPositions); 63 | clusteredAircraftMarkers.clearLayers(); 64 | clusteredAircraftMarkers.addLayer(aircraftMarkers); 65 | } 66 | map.on('overlayremove', function(eo) { 67 | if ((eo.name.indexOf('Lower Airspace') != -1) && 68 | (eo.name.indexOf('Upper Airspace') != -1)) { 69 | return 70 | } 71 | if (!map.hasLayer(upperAirspace) && !map.hasLayer(lowerAirspace)) 72 | reloadAircraftPositions(); 73 | }); 74 | 75 | function refreshAircraftPositions() { 76 | var xhr = new XMLHttpRequest(); 77 | var url = 'https://opensky-network.org/api/states/all'; 78 | if (L.Browser.mobile) 79 | var bBox = map.getBounds(); 80 | else 81 | var bBox = L.latLngBounds([-90, -180], [90, 180]); 82 | xhr.open('GET', url + '?lamin=' + bBox.getSouth() + '&lomin=' + bBox.getWest() + '&lamax=' + bBox.getNorth() + '&lomax=' + bBox.getEast()); 83 | xhr.setRequestHeader('Content-Type', 'application/json'); 84 | xhr.timeout = 8000; 85 | xhr.onload = function() { 86 | if (xhr.status === 200) { 87 | var t_start = Date.now(); 88 | aircraftPositions = []; 89 | var response = JSON.parse(xhr.responseText); 90 | response.states.forEach(function(aircraft) { 91 | var callsign = aircraft[1].trim(); 92 | if (callsign.length == 0) 93 | return; 94 | else if (aircraft[5] === null || aircraft[6] === null) 95 | return; 96 | else if (!callsign.match('^[A-Z]{3}[0-9]{1,4}[A-Z]{0,2}')) 97 | return; 98 | var aircraftPosition = turf.helpers.point([aircraft[5], aircraft[6]], { 99 | "callsign": callsign, 100 | "heading": Math.round(aircraft[10]), 101 | "ul_visible": "both", 102 | "flight_level": Math.round(aircraft[7] / 0.3048 / 100), 103 | "ground_speed": Math.round(aircraft[9] * (3600 / 1852)), 104 | "vertical_speed": Math.round(aircraft[11] * (60 / 0.3048)) 105 | }); 106 | turf.meta.featureEach(upper_lower_airspace_limits, function (airspace, featureIndex) { 107 | if (aircraftPosition.properties.flight_level >= airspace.properties.MIN_FLIGHT && 108 | aircraftPosition.properties.flight_level <= airspace.properties.MAX_FLIGHT && 109 | turf.booleanPointInPolygon(aircraftPosition, airspace)) { 110 | aircraftPosition.properties.ul_visible = airspace.properties.UL_VISIBLE; 111 | } 112 | }); 113 | aircraftPositions.push(aircraftPosition); 114 | }); 115 | reloadAircraftPositions() 116 | var t_stop = Date.now(); 117 | // console.log('duration: ' + (t_stop-t_start) / 1e3 + 's'); 118 | } 119 | }; 120 | xhr.send(); 121 | } 122 | -------------------------------------------------------------------------------- /static/airports_interactive.js: -------------------------------------------------------------------------------- 1 | function clickAirport(eo) { 2 | routesInfo(eo.target.feature.properties); 3 | } 4 | 5 | var routePlot = L.geoJSON(null, { 6 | pane: 'routePlot', 7 | style: function(feature) { 8 | return { 9 | weight: 1.5, 10 | opacity: 1, 11 | color: 'blue' 12 | }; 13 | } 14 | }); 15 | routePlot.addTo(map); 16 | 17 | var info = L.control({ 18 | position: 'bottomright' 19 | }); 20 | 21 | info.onAdd = function(map) { 22 | this._div = L.DomUtil.create('div', 'info'); 23 | this.reset(); 24 | return this._div; 25 | }; 26 | 27 | info.reset = function() { 28 | this._div.innerHTML = '

Route information

Click on any airport'; 29 | routePlot.clearLayers(); 30 | }; 31 | info.addTo(map); 32 | info.updateAirportInfo = function(airportInfo) { 33 | var text = '' + airportInfo.name + ' (' + airportInfo.icao; 34 | if (airportInfo.iata !== undefined && airportInfo.iata != '') 35 | text += ' / ' + airportInfo.iata; 36 | text += ')
'; 37 | if (typeof airportInfo.known_destinations !== 'undefined') { 38 | text += airportInfo.known_destinations + ' known destinations
'; 39 | } 40 | text += ''; 41 | this._div.innerHTML = text; 42 | } 43 | 44 | function routesInfo(airportInfo) { 45 | var xhr = new XMLHttpRequest(); 46 | xhr.open('GET', './api/geojson/airport/?icao=' + airportInfo.icao); 47 | xhr.setRequestHeader('Content-Type', 'application/json'); 48 | xhr.onload = function() { 49 | if (xhr.status === 200) { 50 | var routes_info = JSON.parse(xhr.responseText); 51 | if (typeof routes_info.features === 'undefined') {} else { 52 | info.updateAirportInfo(airportInfo) 53 | routePlot.clearLayers(); 54 | turf.meta.segmentEach(routes_info, function(currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) { 55 | var start = currentSegment.geometry.coordinates[0]; 56 | var end = currentSegment.geometry.coordinates[1]; 57 | var distance = turf.distance(start, end); 58 | if (distance > 0) 59 | routePlot.addData(turf.greatCircle(start, end, { 60 | npoints: Math.round(distance * 0.009), 61 | offset: 10 62 | })); 63 | }); 64 | } 65 | } 66 | }; 67 | xhr.send(); 68 | } 69 | 70 | map.on('click', function(eo) { 71 | clickMap(eo); 72 | }); 73 | 74 | function clickMap(eo) { 75 | info.reset(); 76 | } 77 | -------------------------------------------------------------------------------- /static/airports_static.js: -------------------------------------------------------------------------------- 1 | map.createPane('routePlot'); 2 | map.getPane('routePlot').style.zIndex = 395; 3 | 4 | map.createPane('airports'); 5 | map.getPane('airports').style.zIndex = 397; 6 | 7 | function clickAirport(eo) { 8 | return; 9 | } 10 | 11 | var airportMarkers = L.geoJSON(null, { 12 | pane: 'airports', 13 | onEachFeature: function(feature, layer) { 14 | layer.on('click', function(eo) { 15 | clickAirport(eo); 16 | }); 17 | var iata = feature.properties.iata; 18 | if (typeof iata === 'undefined' || iata == '') { 19 | iata = '-'; 20 | } 21 | var tooltipContent = 22 | "" + feature.properties.name + "
" + 23 | feature.properties.icao + " / " + 24 | feature.properties.iata; 25 | if (typeof feature.properties.known_destinations !== 'undefined') { 26 | tooltipContent += "
" + feature.properties.known_destinations + " known destinations"; 27 | } 28 | if (typeof feature.properties.known_departures !== 'undefined') { 29 | tooltipContent += "
" + feature.properties.known_departures + " known departures"; 30 | } 31 | layer.bindTooltip(tooltipContent, { 32 | direction: "top", 33 | offset: [0, -5] 34 | }); 35 | 36 | }, 37 | pointToLayer: function(feature, latlng) { 38 | var radius = 1000; 39 | if (typeof feature.properties.known_departures !== 'undefined') { 40 | radius += feature.properties.known_departures * 3.5; 41 | } else if (typeof feature.properties.known_destinations !== 'undefined') { 42 | radius += feature.properties.known_destinations * 5; 43 | } else if (feature.properties.type == 'medium_airport') { 44 | radius += 1500; 45 | } else if (feature.properties.type == 'large_airport') { 46 | radius += 3000; 47 | }; 48 | return L.circle(latlng, { 49 | color: '#d50000', 50 | fillColor: '#d50000', 51 | fillOpacity: 0.2, 52 | radius: radius 53 | }) 54 | } 55 | }) 56 | 57 | layerControl.addOverlay(airportMarkers, 58 | "Airports"); 59 | 60 | function loadAirportData(url) { 61 | if (!url) url = './airports_static.json'; 62 | var xhr = new XMLHttpRequest(); 63 | xhr.open('GET', url); 64 | xhr.setRequestHeader('Content-Type', 'application/json'); 65 | xhr.onload = function() { 66 | if (xhr.status === 200) { 67 | var airports = JSON.parse(xhr.responseText); 68 | airportMarkers.clearLayers(); 69 | airportMarkers.addData(airports); 70 | } 71 | }; 72 | xhr.send(); 73 | } 74 | -------------------------------------------------------------------------------- /static/airspaces_static.js: -------------------------------------------------------------------------------- 1 | map.createPane('firs'); 2 | map.getPane('firs').style.zIndex = 390; 3 | 4 | var airspaceData; 5 | 6 | function clickAirspace(eo) { 7 | return; 8 | } 9 | 10 | function styleAirspace(feature, styleProperties) { 11 | return styleProperties; 12 | } 13 | 14 | function onEachFeatureFir(feature, layer) { 15 | layer.on('click', function(eo) { 16 | clickAirspace(eo); 17 | }); 18 | var tooltipContent = 19 | "" + feature.properties.AV_NAME + 20 | " (" + feature.properties.AV_AIRSPAC + ")
FL" + 21 | feature.properties.MIN_FLIGHT + " to FL" + 22 | feature.properties.MAX_FLIGHT + "
"; 23 | layer.bindTooltip(tooltipContent, { 24 | sticky: true, 25 | direction: "top", 26 | offset: [0, -5] 27 | }); 28 | } 29 | 30 | var upperAirspace = L.geoJSON([], { 31 | onEachFeature: onEachFeatureFir, 32 | pane: 'firs', 33 | filter: function(feature) { 34 | return ('upper'.includes(feature.properties.UL_VISIBLE)) 35 | }, 36 | style: function(feature) { 37 | if (feature.properties.UL_VISIBLE == 'upper') { 38 | return styleAirspace(feature, { 39 | fillColor: "#003399", 40 | fillOpacity: 0.1, 41 | weight: 1.5, 42 | color: "grey" 43 | }); 44 | } 45 | } 46 | }); 47 | 48 | var lowerAirspace = L.geoJSON([], { 49 | onEachFeature: onEachFeatureFir, 50 | pane: 'firs', 51 | filter: function(feature) { 52 | return ('lower'.includes(feature.properties.UL_VISIBLE)) 53 | }, 54 | style: function(feature) { 55 | return styleAirspace(feature, { 56 | fillColor: "#ffcc00", 57 | fillOpacity: 0.15, 58 | weight: 1.5, 59 | color: "grey" 60 | }); 61 | } 62 | }).addTo(map); 63 | 64 | var singleAirspace = L.geoJSON([], { 65 | onEachFeature: onEachFeatureFir, 66 | pane: 'firs', 67 | filter: function(feature) { 68 | return ('both'.includes(feature.properties.UL_VISIBLE)) 69 | }, 70 | style: function(feature) { 71 | return styleAirspace(feature, { 72 | fillColor: "#00ee00", 73 | fillOpacity: 0.1, 74 | weight: 1.5, 75 | color: "grey" 76 | }); 77 | } 78 | 79 | }).addTo(map); 80 | 81 | var lowerAirspaceLabel = "Lower Airspace (FIRs)"; 82 | var upperAirspaceLabel = "Upper Airspace (UIRs)"; 83 | var singleAirspaceLabel = "Airspaces (FIR/UIR)"; 84 | layerControl.addOverlay(lowerAirspace, lowerAirspaceLabel); 85 | layerControl.addOverlay(upperAirspace, upperAirspaceLabel); 86 | layerControl.addOverlay(singleAirspace, singleAirspaceLabel); 87 | layerControl.addTo(map); 88 | 89 | function reloadAircraftPositions() { 90 | return; 91 | } 92 | // For regions where upper airspaces (UIR) as well as lower airspaces (FIR) 93 | // exist, shapes are treated as baselayers to allow a choice by radio buttons. 94 | // To make upperAirspace and lowerAirspace mutually exclusive the setTimeout 95 | // is needed to keep the 'overlayadd' event from firing multiple times. 96 | map.on('overlayadd', function(eo) { 97 | if (eo.name === lowerAirspaceLabel) { 98 | setTimeout(function() { 99 | map.removeLayer(upperAirspace); 100 | reloadAircraftPositions(); 101 | }, 10); 102 | } else if (eo.name === upperAirspaceLabel) { 103 | setTimeout(function() { 104 | map.removeLayer(lowerAirspace); 105 | reloadAircraftPositions(); 106 | }, 10); 107 | } 108 | }); 109 | function refreshAirspaces() { 110 | if (typeof airspaceData === 'undefined') { 111 | return 112 | } 113 | lowerAirspace.clearLayers(); 114 | lowerAirspace.addData(airspaceData); 115 | upperAirspace.clearLayers(); 116 | upperAirspace.addData(airspaceData); 117 | singleAirspace.clearLayers(); 118 | singleAirspace.addData(airspaceData); 119 | } 120 | 121 | function loadFirUirShapes(shapefile) { 122 | if (!shapefile) shapefile = './flightmap_europe_fir_uir.json'; 123 | var xhr = new XMLHttpRequest(); 124 | xhr.open('GET', shapefile); 125 | xhr.setRequestHeader('Content-Type', 'application/json'); 126 | xhr.onload = function() { 127 | if (xhr.status === 200) { 128 | airspaceData = JSON.parse(xhr.responseText); 129 | refreshAirspaces(); 130 | } 131 | 132 | }; 133 | xhr.send(); 134 | } 135 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaluebbe/FlightMapEuropeSimple/bc746980c1b23555d1decbdcff232d18754233ff/static/favicon.ico -------------------------------------------------------------------------------- /static/flightmap.js: -------------------------------------------------------------------------------- 1 | var map = L.map('map', { 2 | zoomSnap: 0.5, 3 | zoomDelta: 0.5, 4 | tap: false 5 | }); 6 | map.attributionControl.addAttribution( 7 | 'Source on GitHub'); 8 | // add link to an imprint and a privacy statement if the file is available. 9 | function addPrivacyStatement() { 10 | var xhr = new XMLHttpRequest(); 11 | xhr.open('HEAD', "./datenschutz.html"); 12 | xhr.onload = function() { 13 | if (xhr.status === 200) 14 | map.attributionControl.addAttribution( 15 | 'Impressum & Datenschutzerklärung' 16 | ); 17 | } 18 | xhr.send(); 19 | } 20 | addPrivacyStatement(); 21 | if (L.Browser.mobile) { 22 | map.setView([52, 4.5], 3.5); 23 | map.removeControl(map.zoomControl); 24 | } else { 25 | map.setView([52, 4.5], 4); 26 | } 27 | L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { 28 | minZoom: 1, 29 | maxZoom: 15, 30 | attribution: '© OpenStreetMap contributors, ' + 31 | 'CC-BY-SA, ' + 32 | '© CARTO', 33 | }).addTo(map); 34 | var layerControl = L.control.layers({}, {}, { 35 | collapsed: L.Browser.mobile, // hide on mobile devices 36 | position: 'topright', 37 | hideSingleBase: true 38 | }) 39 | -------------------------------------------------------------------------------- /static/flightmap_europe_static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flightmap europe static 7 | 8 | 9 | 10 | 11 | 12 | 13 | 58 | 59 | 60 | 61 |
62 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /static/flightmap_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test page for airspace shapefiles 7 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /static/flightsearch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flight search 7 | 8 | 9 | 10 | 11 | 70 | 71 | 72 | 73 |
74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /static/flightsearch.js: -------------------------------------------------------------------------------- 1 | var initialDestination = [51.5, -0.12] 2 | var initialOrigin = [52.5, 7.3] 3 | var numberOfStops = { 4 | value: 0 5 | }; 6 | var legend = L.control({ 7 | position: 'topright' 8 | }); 9 | legend.onAdd = function(map) { 10 | var div = L.DomUtil.create('div', 'info legend'); 11 | div.innerHTML = 12 | '' + 21 | '' + 30 | '' + 33 | '' + 37 | '' 38 | '
Origin
Destination
Stops
Filter
Click for your origin!
'; 39 | L.DomEvent.on(div, 'click', function(ev) { 40 | L.DomEvent.stopPropagation(ev); 41 | }); 42 | 43 | return div; 44 | }; 45 | 46 | legend.addTo(map) 47 | 48 | map.createPane('radius'); 49 | map.getPane('radius').style.zIndex = 396; 50 | map.createPane('searchPlot'); 51 | map.getPane('searchPlot').style.zIndex = 398; 52 | 53 | document.getElementById("originRadiusKm").onchange = function() { 54 | originCircle.setRadius(document.getElementById("originRadiusKm").value * 1e3); 55 | }; 56 | document.getElementById("destinationRadiusKm").onchange = function() { 57 | destinationCircle.setRadius(document.getElementById("destinationRadiusKm").value * 1e3); 58 | }; 59 | //document.getElementById("numberOfStops").onchange = function() { 60 | //TODO: max 300km radius for 2 stops 61 | //}; 62 | map.on('mouseup', () => { 63 | map.dragging.enable(); 64 | map.removeEventListener('mousemove'); 65 | }); 66 | originCircle = L.circle(initialOrigin, document.getElementById("originRadiusKm").value * 1e3, { 67 | color: 'blue', 68 | fillColor: '#00f', 69 | fillOpacity: 0.1, 70 | pane: 'radius' 71 | }); 72 | destinationCircle = L.circle(initialDestination, document.getElementById("destinationRadiusKm").value * 1e3, { 73 | draggable: true, 74 | color: 'green', 75 | fillColor: '#0f0', 76 | fillOpacity: 0.1, 77 | pane: 'radius' 78 | }); 79 | L.DomEvent.on(originCircle, 'click', function(ev) { 80 | if (map.hasLayer(destinationCircle)) { 81 | flightSearch(); 82 | } 83 | L.DomEvent.stopPropagation(ev); 84 | }); 85 | L.DomEvent.on(destinationCircle, 'click', function(ev) { 86 | if (map.hasLayer(originCircle)) { 87 | flightSearch(); 88 | } 89 | L.DomEvent.stopPropagation(ev); 90 | }); 91 | 92 | originCircle.on('mousedown', function(event) { 93 | if (!map.hasLayer(destinationCircle)) { 94 | return; 95 | } 96 | map.dragging.disable(); 97 | let { 98 | lat: circleStartingLat, 99 | lng: circleStartingLng 100 | } = originCircle._latlng; 101 | let { 102 | lat: mouseStartingLat, 103 | lng: mouseStartingLng 104 | } = event.latlng; 105 | 106 | map.on('mousemove', event => { 107 | let { 108 | lat: mouseNewLat, 109 | lng: mouseNewLng 110 | } = event.latlng; 111 | let latDifference = mouseStartingLat - mouseNewLat; 112 | let lngDifference = mouseStartingLng - mouseNewLng; 113 | 114 | let center = [circleStartingLat - latDifference, circleStartingLng - lngDifference]; 115 | originCircle.setLatLng(center); 116 | }); 117 | }); 118 | 119 | destinationCircle.on('mousedown', function(event) { 120 | if (!map.hasLayer(originCircle)) { 121 | return; 122 | } 123 | map.dragging.disable(); 124 | let { 125 | lat: circleStartingLat, 126 | lng: circleStartingLng 127 | } = destinationCircle._latlng; 128 | let { 129 | lat: mouseStartingLat, 130 | lng: mouseStartingLng 131 | } = event.latlng; 132 | 133 | map.on('mousemove', event => { 134 | let { 135 | lat: mouseNewLat, 136 | lng: mouseNewLng 137 | } = event.latlng; 138 | let latDifference = mouseStartingLat - mouseNewLat; 139 | let lngDifference = mouseStartingLng - mouseNewLng; 140 | 141 | let center = [circleStartingLat - latDifference, circleStartingLng - lngDifference]; 142 | destinationCircle.setLatLng(center); 143 | }); 144 | }); 145 | 146 | var searchPlot = L.geoJSON(null, { 147 | pane: 'searchPlot', 148 | style: function (feature) { 149 | return { 150 | weight: 2.5, 151 | opacity: 1, 152 | color: 'darkorange', 153 | }; 154 | } 155 | }); 156 | 157 | function flightSearch(e) { 158 | var data = { 159 | destination: destinationCircle.getLatLng(), 160 | origin: originCircle.getLatLng(), 161 | destinationRadius: destinationCircle.getRadius(), 162 | originRadius: originCircle.getRadius(), 163 | numberOfStops: document.getElementById("numberOfStops").value, 164 | filterAirlineAlliance: document.getElementById("filterAirlineAlliance").value 165 | } 166 | var xhr = new XMLHttpRequest(); 167 | xhr.open('POST', './api/geojson/flightsearch'); 168 | xhr.setRequestHeader('Content-Type', 'application/json'); 169 | xhr.onload = function() { 170 | if (xhr.status === 200) { 171 | var routes_info = JSON.parse(xhr.responseText); 172 | if (typeof routes_info.features === 'undefined') {} else { 173 | searchPlot.clearLayers(); 174 | turf.meta.segmentEach(routes_info, function(currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) { 175 | var start = currentSegment.geometry.coordinates[0]; 176 | var end = currentSegment.geometry.coordinates[1]; 177 | var distance = turf.distance(start, end); 178 | if (distance > 0) 179 | searchPlot.addData(turf.greatCircle(start, end, { 180 | npoints: Math.round(distance * 0.009), 181 | offset: 10 182 | })); 183 | }); 184 | searchPlot.addTo(map); 185 | } 186 | } 187 | }; 188 | xhr.send(JSON.stringify(data)); 189 | } 190 | 191 | function clickMap(eo) { 192 | if (!putCircle(eo)) { 193 | info.reset(); 194 | } 195 | } 196 | 197 | function clickAirport(eo) { 198 | if (!putCircle(eo)) { 199 | routesInfo(eo.target.feature.properties); 200 | } 201 | } 202 | 203 | function putCircle(eo) { 204 | if (!map.hasLayer(originCircle)) { 205 | originCircle.setLatLng(eo.latlng).addTo(map); 206 | document.getElementById("helptext").innerHTML = 207 | 'Select destination! ' 208 | } 209 | else if (!map.hasLayer(destinationCircle)) { 210 | destinationCircle.setLatLng(eo.latlng).addTo(map); 211 | document.getElementById("helptext").innerHTML = 212 | '' + 213 | '' 214 | } 215 | else { 216 | return false; 217 | } 218 | L.DomEvent.stopPropagation(eo); 219 | return true; 220 | } 221 | 222 | function resetMap() { 223 | map.removeLayer(searchPlot); 224 | map.removeLayer(destinationCircle); 225 | map.removeLayer(originCircle); 226 | document.getElementById("helptext").innerHTML = "Click for your origin!" 227 | } 228 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flightmap europe simple 7 | 8 | 9 | 10 | 11 | 12 | 13 | 72 | 73 | 74 | 75 |
76 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /static/leaflet.rotatedMarker.js: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/bbecquet/Leaflet.RotatedMarker/ 2 | /* 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Benjamin Becquet 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | (function() { 27 | // save these original methods before they are overwritten 28 | var proto_initIcon = L.Marker.prototype._initIcon; 29 | var proto_setPos = L.Marker.prototype._setPos; 30 | 31 | var oldIE = (L.DomUtil.TRANSFORM === 'msTransform'); 32 | 33 | L.Marker.addInitHook(function () { 34 | var iconOptions = this.options.icon && this.options.icon.options; 35 | var iconAnchor = iconOptions && this.options.icon.options.iconAnchor; 36 | if (iconAnchor) { 37 | iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px'); 38 | } 39 | this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ; 40 | this.options.rotationAngle = this.options.rotationAngle || 0; 41 | 42 | // Ensure marker keeps rotated during dragging 43 | this.on('drag', function(e) { e.target._applyRotation(); }); 44 | }); 45 | 46 | L.Marker.include({ 47 | _initIcon: function() { 48 | proto_initIcon.call(this); 49 | }, 50 | 51 | _setPos: function (pos) { 52 | proto_setPos.call(this, pos); 53 | this._applyRotation(); 54 | }, 55 | 56 | _applyRotation: function () { 57 | if(this.options.rotationAngle) { 58 | this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin; 59 | 60 | if(oldIE) { 61 | // for IE 9, use the 2D rotation 62 | this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)'; 63 | } else { 64 | // for modern browsers, prefer the 3D accelerated version 65 | this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)'; 66 | } 67 | } 68 | }, 69 | 70 | setRotationAngle: function(angle) { 71 | this.options.rotationAngle = angle; 72 | this.update(); 73 | return this; 74 | }, 75 | 76 | setRotationOrigin: function(origin) { 77 | this.options.rotationOrigin = origin; 78 | this.update(); 79 | return this; 80 | } 81 | }); 82 | })(); 83 | -------------------------------------------------------------------------------- /static/my_turf.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).turf=e()}(function(){return function n(o,i,a){function s(t,e){if(!i[t]){if(!o[t]){var r="function"==typeof require&&require;if(!e&&r)return r(t,!0);if(u)return u(t,!0);throw(r=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",r}r=i[t]={exports:{}},o[t][0].call(r.exports,function(e){return s(o[t][1][e]||e)},r,r.exports,n,o,i,a)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;ee[1]!=l>e[1]&&e[0]<(u-a)*(e[1]-s)/(l-s)+a&&(n=!n)}return n}r.default=function(e,t,r){if(void 0===r&&(r={}),!e)throw new Error("point is required");if(!t)throw new Error("polygon is required");var n,o=c.getCoord(e),e=(n=c.getGeom(t)).type,t=t.bbox,i=n.coordinates;if(t&&!1==(n=o,(t=t)[0]<=n[0]&&t[1]<=n[1]&&t[2]>=n[0]&&t[3]>=n[1]))return!1;"Polygon"===e&&(i=[i]);for(var a=!1,s=0;sc?(M=parseFloat(r[m-1][0]),P=parseFloat(r[m-1][1]),v=parseFloat(r[m][0]),b=parseFloat(r[m][1]),-180u&&r[m-1][0]<180?(y.push([180,r[m][1]]),m++,y.push([r[m][0],r[m][1]])):(Mu?180:-180,P]),(y=[]).push([r[m-1][0]>u?-180:180,P])):y=[],p.push(y),y.push([E,r[m][1]]))):y.push([r[m][0],r[m][1]])}}else{var x=[];p.push(x);for(var F=0;F this.length) { 10 | return false; 11 | } else { 12 | return this.indexOf(search, start) !== -1; 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /static/statistics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flight statistics 7 | 8 | 9 | 10 | 11 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |

Flight statistics based on FIR/UIR

78 |

Usage

79 | Click on any airspace (FIR/UIR) on the map to display the respective development of the 80 | number of airline flights during the COVID-19 pandemic. You may select multiple airspaces 81 | at the same time. Clicking a second time on the same airspace deselects 82 | it. Switching between FIRs and UIRs on the map is done via the legend. 83 | The view can be toggled between absolute numbers and a percentage 84 | related to the average number of flights in the first ten days of the 85 | year 2020. 86 |

Content

87 | Some regions don't discriminate between FIR and UIR or 88 | have been simplified due to a lack of reliable openly available shape 89 | files and to save computation power in data processing. 90 | Aircraft position data was consumed as state vectors from the 91 | OpenSky Network API 92 | as well as from the 93 | 94 | OpenSky Network historical database 95 | at intervals between 10 seconds and 5 minutes. 96 | State vectors with incomplete positional information or on_ground state 97 | were ignored. 98 | This statistics is focused on airline flights and should ignore general 99 | aviation. Only callsigns matching the following regular expression have 100 | been processed: 101 | ^([A-Z]{3})[0-9](([0-9]{0,3})|([0-9]{0,2})([A-Z])|([0-9]?)([A-Z]{2}))$ 102 |
103 |

World wide statistics

104 |
105 | The flight statistics is based on OpenSky Network 106 | data and was processed as described above. 107 |
108 | For simplicity, the COVID-19 related data is shown for the whole 109 | world only. Much more information can be found at the source of the 110 | dataset: 111 |
112 | Max Roser, Hannah Ritchie, Esteban Ortiz-Ospina and Joe Hasell 113 | (2020) - "Coronavirus Pandemic (COVID-19)" 114 |
115 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /static/statistics.js: -------------------------------------------------------------------------------- 1 | var flightStatistics; 2 | var firUirStatistics; 3 | var covidData; 4 | var selection = new Set(["EGTTFIR", "LFFFFIR"]); 5 | var data = []; 6 | var worldData = []; 7 | var names = { 8 | EGTTFIR: "LONDON FIR", 9 | LFFFFIR: "PARIS FIR" 10 | }; 11 | var show_percentage = false; 12 | var show_percentage_fir_uir = false; 13 | var d3colors = d3.scale.category10(); 14 | var firColors = new Map(); 15 | 16 | function plotFirUirStatistics() { 17 | data.length = 0; 18 | firColors.clear(); 19 | var i = 0; 20 | var gridSetting = undefined; 21 | selection.forEach(function(airspace) { 22 | var firUirData = firUirStatistics[airspace]; 23 | data.push({ 24 | x: firUirData.Dates, 25 | y: show_percentage_fir_uir ? firUirData.percentage : firUirData.FlightsDetected, 26 | name: "" + names[airspace], 27 | type: 'scatter' 28 | }); 29 | firColors.set(airspace, d3colors(i)); 30 | i++; 31 | }); 32 | 33 | var percent_icon = { 34 | svg: '%Logo' 35 | }; 36 | var config = { 37 | modeBarButtonsToAdd: [{ 38 | name: show_percentage_fir_uir ? 'show absolute values' : 'show percentages', 39 | icon: show_percentage_fir_uir ? Plotly.Icons.pencil : percent_icon, 40 | click: function(gd) { 41 | show_percentage_fir_uir = !show_percentage_fir_uir; 42 | plotFirUirStatistics() 43 | } 44 | }], 45 | modeBarButtonsToRemove: [ 46 | 'select2d', 'lasso2d', 'toggleSpikelines', 'zoomIn2d', 47 | 'zoomOut2d', 'resetScale2d', 'hoverClosestCartesian', 48 | 'hoverCompareCartesian' 49 | ], 50 | displaylogo: false, 51 | responsive: true, 52 | displayModeBar: true 53 | }; 54 | var layout = { 55 | margin: { 56 | t: 20, 57 | r: 0, 58 | b: 0, 59 | l: 0 60 | }, 61 | grid: gridSetting, 62 | xaxis: { 63 | automargin: true, 64 | range: ['2019-12-16', Date.now()], 65 | title: { 66 | text: 'Date (UTC)', 67 | standoff: 15 68 | } 69 | }, 70 | yaxis: { 71 | automargin: true, 72 | title: { 73 | text: show_percentage_fir_uir ? 'Airline flights (%)' : 'Airline flights', 74 | standoff: 20 75 | }, 76 | rangemode: 'tozero', 77 | }, 78 | yaxis2: { 79 | automargin: true, 80 | title: { 81 | text: 'COVID-19', 82 | standoff: 20 83 | }, 84 | type: 'log' 85 | }, 86 | showlegend: !L.Browser.mobile 87 | }; 88 | Plotly.react('fir_uir_statistics', data, layout, config); 89 | } 90 | 91 | function plotStatistics() { 92 | worldData.length = 0; 93 | var gridSetting = undefined; 94 | if (flightStatistics !== undefined) { 95 | worldData.push({ 96 | x: flightStatistics.Dates, 97 | y: show_percentage ? flightStatistics.percentage : flightStatistics.FlightsDetected, 98 | name: "world flights", 99 | type: 'scatter' 100 | }); 101 | } 102 | if (covidData !== undefined) { 103 | worldData.push({ 104 | x: covidData.date, 105 | y: covidData.new_cases, 106 | name: "new cases", 107 | type: 'scatter', 108 | xaxis: 'x', 109 | yaxis: 'y2' 110 | }); 111 | worldData.push({ 112 | x: covidData.date, 113 | y: covidData.new_deaths, 114 | name: "new deaths", 115 | type: 'scatter', 116 | xaxis: 'x', 117 | yaxis: 'y2' 118 | }); 119 | worldData.push({ 120 | x: covidData.date, 121 | y: covidData.total_cases, 122 | name: "total cases", 123 | type: 'scatter', 124 | visible: 'legendonly', 125 | xaxis: 'x', 126 | yaxis: 'y2' 127 | }); 128 | worldData.push({ 129 | x: covidData.date, 130 | y: covidData.total_deaths, 131 | name: "total deaths", 132 | type: 'scatter', 133 | visible: 'legendonly', 134 | xaxis: 'x', 135 | yaxis: 'y2' 136 | }); 137 | worldData.push({ 138 | x: covidData.date, 139 | y: covidData.total_vaccinations, 140 | name: "total vaccinations", 141 | type: 'scatter', 142 | visible: 'legendonly', 143 | xaxis: 'x', 144 | yaxis: 'y2' 145 | }); 146 | worldData.push({ 147 | x: covidData.date, 148 | y: covidData.people_vaccinated, 149 | name: "people vaccinated", 150 | type: 'scatter', 151 | visible: 'legendonly', 152 | xaxis: 'x', 153 | yaxis: 'y2' 154 | }); 155 | worldData.push({ 156 | x: covidData.date, 157 | y: covidData.people_fully_vaccinated, 158 | name: "people fully vaccinated", 159 | type: 'scatter', 160 | visible: 'legendonly', 161 | xaxis: 'x', 162 | yaxis: 'y2' 163 | }); 164 | } 165 | if (flightStatistics !== undefined && covidData !== undefined) { 166 | gridSetting = { 167 | rows: 2, 168 | columns: 1, 169 | subplots: [ 170 | ['xy'], 171 | ['xy2'] 172 | ] 173 | }; 174 | } 175 | var percent_icon = { 176 | svg: '%Logo' 177 | }; 178 | var config = { 179 | modeBarButtonsToAdd: [{ 180 | name: show_percentage ? 'show absolute values' : 'show percentages', 181 | icon: show_percentage ? Plotly.Icons.pencil : percent_icon, 182 | click: function(gd) { 183 | show_percentage = !show_percentage; 184 | plotStatistics() 185 | } 186 | }], 187 | modeBarButtonsToRemove: [ 188 | 'select2d', 'lasso2d', 'toggleSpikelines', 'zoomIn2d', 189 | 'zoomOut2d', 'resetScale2d', 'hoverClosestCartesian', 190 | 'hoverCompareCartesian' 191 | ], 192 | displaylogo: false, 193 | responsive: true, 194 | displayModeBar: true 195 | }; 196 | var layout = { 197 | margin: { 198 | t: 20, 199 | r: 0, 200 | b: 0, 201 | l: 0 202 | }, 203 | grid: gridSetting, 204 | xaxis: { 205 | automargin: true, 206 | range: ['2019-12-16', Date.now()], 207 | title: { 208 | text: 'Date (UTC)', 209 | standoff: 15 210 | } 211 | }, 212 | yaxis: { 213 | automargin: true, 214 | title: { 215 | text: show_percentage ? 'Airline flights (%)' : 'Airline flights', 216 | standoff: 20 217 | }, 218 | rangemode: 'tozero', 219 | }, 220 | yaxis2: { 221 | automargin: true, 222 | title: { 223 | text: 'COVID-19', 224 | standoff: 20 225 | }, 226 | type: 'log' 227 | }, 228 | showlegend: !L.Browser.mobile 229 | }; 230 | Plotly.react('world_statistics', worldData, layout, config); 231 | 232 | } 233 | 234 | function clickAirspace(eo) { 235 | if (typeof firUirStatistics === 'undefined') { 236 | console.error('statistical data not available yet.'); 237 | return; 238 | } 239 | var feature = eo.target.feature; 240 | names[feature.properties.AV_AIRSPAC] = feature.properties.AV_NAME; 241 | if (selection.has(feature.properties.AV_AIRSPAC)) { 242 | selection.delete(feature.properties.AV_AIRSPAC); 243 | } else if (selection.size < 10) { 244 | selection.add(feature.properties.AV_AIRSPAC); 245 | } else { 246 | console.error('Selection too large. Unselect airspaces or reset selection.'); 247 | } 248 | plotFirUirStatistics(); 249 | refreshAirspaces(); 250 | } 251 | 252 | function styleAirspace(feature, styleProperties) { 253 | if (selection.has(feature.properties.AV_AIRSPAC)) { 254 | styleProperties.fillColor = firColors.get(feature.properties.AV_AIRSPAC); 255 | styleProperties.fillOpacity = 0.4; 256 | } 257 | return styleProperties; 258 | } 259 | 260 | function loadFirUirStatistics(url) { 261 | var xhr = new XMLHttpRequest(); 262 | xhr.open('GET', url); 263 | xhr.setRequestHeader('Content-Type', 'application/json'); 264 | xhr.onload = function() { 265 | if (xhr.status === 200) { 266 | firUirStatistics = JSON.parse(xhr.responseText); 267 | plotFirUirStatistics(); 268 | refreshAirspaces(); 269 | } 270 | }; 271 | xhr.send(); 272 | } 273 | 274 | function loadFlightStatistics(url) { 275 | var xhr = new XMLHttpRequest(); 276 | xhr.open('GET', url); 277 | xhr.setRequestHeader('Content-Type', 'application/json'); 278 | xhr.onload = function() { 279 | if (xhr.status === 200) { 280 | flightStatistics = JSON.parse(xhr.responseText); 281 | plotStatistics(); 282 | } 283 | }; 284 | xhr.send(); 285 | } 286 | 287 | function loadCovidData(url) { 288 | var xhr = new XMLHttpRequest(); 289 | xhr.open('GET', url); 290 | xhr.setRequestHeader('Content-Type', 'application/json'); 291 | xhr.onload = function() { 292 | if (xhr.status === 200) { 293 | covidData = JSON.parse(xhr.responseText); 294 | plotStatistics(); 295 | } 296 | }; 297 | xhr.send(); 298 | } 299 | -------------------------------------------------------------------------------- /static/statistics_static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flight statistics 7 | 8 | 9 | 10 | 11 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |

Flight statistics based on FIR/UIR

49 |

Usage

50 | Click on any airspace (FIR/UIR) on the map to display the respective development of the 51 | number of airline flights during the COVID-19 pandemic. You may select multiple airspaces 52 | at the same time. Clicking a second time on the same airspace deselects 53 | it. Switching between FIRs and UIRs on the map is done via the legend. 54 | The view can be toggled between absolute numbers and a percentage 55 | related to the average number of flights in the first ten days of the 56 | year 2020. 57 |

Content

58 | Some regions don't discriminate between FIR and UIR or 59 | have been simplified due to a lack of reliable openly available shape 60 | files and to save computation power in data processing. 61 | Aircraft position data was consumed as state vectors from the 62 | OpenSky Network API 63 | as well as from the 64 | 65 | OpenSky Network historical database 66 | at intervals between 10 seconds and 5 minutes. 67 | State vectors with incomplete positional information or on_ground state 68 | were ignored. 69 | This statistics is focused on airline flights and should ignore general 70 | aviation. Only callsigns matching the following regular expression have 71 | been processed: 72 | ^([A-Z]{3})[0-9](([0-9]{0,3})|([0-9]{0,2})([A-Z])|([0-9]?)([A-Z]{2}))$ 73 |
74 |

World wide statistics

75 |
76 | The flight statistics is based on OpenSky Network 77 | data and was processed as described above. 78 |
79 | For simplicity, the COVID-19 related data is shown for the whole 80 | world only. Much more information can be found at the source of the 81 | dataset: 82 |
83 | Max Roser, Hannah Ritchie, Esteban Ortiz-Ospina and Joe Hasell 84 | (2020) - "Coronavirus Pandemic (COVID-19)" 85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /static/string_to_colour.js: -------------------------------------------------------------------------------- 1 | // inspired by https://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-javascript 2 | String.prototype.getHashCode = function() { 3 | var hash = 0; 4 | if (this.length == 0) return hash; 5 | for (var i = 0; i < this.length; i++) { 6 | hash = this.charCodeAt(i) + ((hash << 5) - hash); 7 | hash = hash & hash; // Convert to 32bit integer 8 | } 9 | return hash; 10 | }; 11 | Number.prototype.intToHSL = function() { 12 | var shortened = this % 360; 13 | return "hsl(" + shortened + ",100%,30%)"; 14 | }; 15 | -------------------------------------------------------------------------------- /statistics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flight statistics 6 | 7 | 8 | 9 | 10 |

Redirect to new URL

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /update_statistics/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | 10 | CMD [ "python", "./prepare_statistics.py" ] 11 | -------------------------------------------------------------------------------- /update_statistics/prepare_statistics.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import io 4 | import pandas as pd 5 | import redis 6 | import requests 7 | 8 | url = "https://covid.ourworldindata.org/data/owid-covid-data.csv" 9 | keys = [ 10 | "date", 11 | "total_cases", 12 | "new_cases", 13 | "total_deaths", 14 | "new_deaths", 15 | "total_vaccinations", 16 | "people_vaccinated", 17 | "people_fully_vaccinated", 18 | ] 19 | _response = requests.get(url) 20 | if _response.status_code == 200: 21 | data = pd.read_csv(io.StringIO(_response.text)) 22 | world_data = data[data.iso_code == "OWID_WRL"][keys] 23 | covid_data = {"world": world_data.to_dict(orient="list")} 24 | json_data = ( 25 | json.dumps(world_data.to_dict(orient="list")) 26 | .replace("NaN", "null") 27 | .replace(".0,", ",") 28 | .replace(".0]", "]") 29 | ) 30 | redis_connection = redis.Redis( 31 | os.getenv("REDIS_HOST"), decode_responses=True 32 | ) 33 | redis_connection.set("covid_data", json_data) 34 | 35 | statistics_url = os.getenv("STATISTICS_URL") 36 | if statistics_url is None: 37 | statistics_url = "https://jaluebbe.github.io/FlightMapEuropeSimple/static" 38 | 39 | _response = requests.get(statistics_url + "/flights_statistics.json") 40 | if _response.status_code == 200: 41 | redis_connection.set("flights_statistics", _response.text) 42 | 43 | _response = requests.get(statistics_url + "/fir_uir_statistics.json") 44 | if _response.status_code == 200: 45 | redis_connection.set("fir_uir_statistics", _response.text) 46 | -------------------------------------------------------------------------------- /update_statistics/requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | requests 3 | pandas 4 | --------------------------------------------------------------------------------