├── .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 | ' search ';
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 += 'reset airline routes ';
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 += 'reset airport routes ';
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 | 'Origin ' +
13 | '25km 50km ' +
14 | '75km 100km ' +
15 | '125km 150km ' +
16 | '175km ' +
17 | '200km 250km ' +
18 | '300km 400km ' +
19 | '500km 600km ' +
20 | ' ' +
21 | 'Destination ' +
22 | '25km 50km ' +
23 | '75km 100km ' +
24 | '125km 150km ' +
25 | '175km ' +
26 | '200km 250km ' +
27 | '300km 400km ' +
28 | '500km 600km ' +
29 | ' ' +
30 | 'Stops ' +
31 | '0 1 2 ' +
32 | ' ' +
33 | 'Filter ' +
34 | '- Star Alliance ' +
35 | 'Oneworld SkyTeam ' +
36 | ' ' +
37 | 'Click for your origin!
'
38 | '
';
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! reset '
208 | }
209 | else if (!map.hasLayer(destinationCircle)) {
210 | destinationCircle.setLatLng(eo.latlng).addTo(map);
211 | document.getElementById("helptext").innerHTML =
212 | 'search ' +
213 | 'reset '
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 |
--------------------------------------------------------------------------------