flight-tracer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flight_tracer-0.1.0/LICENSE +121 -0
- flight_tracer-0.1.0/MANIFEST.in +2 -0
- flight_tracer-0.1.0/PKG-INFO +187 -0
- flight_tracer-0.1.0/README.md +156 -0
- flight_tracer-0.1.0/flight_tracer/__init__.py +1 -0
- flight_tracer-0.1.0/flight_tracer/core.py +303 -0
- flight_tracer-0.1.0/flight_tracer.egg-info/PKG-INFO +187 -0
- flight_tracer-0.1.0/flight_tracer.egg-info/SOURCES.txt +14 -0
- flight_tracer-0.1.0/flight_tracer.egg-info/dependency_links.txt +1 -0
- flight_tracer-0.1.0/flight_tracer.egg-info/requires.txt +7 -0
- flight_tracer-0.1.0/flight_tracer.egg-info/top_level.txt +2 -0
- flight_tracer-0.1.0/setup.cfg +32 -0
- flight_tracer-0.1.0/setup.py +29 -0
- flight_tracer-0.1.0/tests/__init__.py +0 -0
- flight_tracer-0.1.0/tests/test_core.py +25 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Creative Commons Legal Code
|
|
2
|
+
|
|
3
|
+
CC0 1.0 Universal
|
|
4
|
+
|
|
5
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
6
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
7
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
8
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
9
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
10
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
11
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
12
|
+
HEREUNDER.
|
|
13
|
+
|
|
14
|
+
Statement of Purpose
|
|
15
|
+
|
|
16
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
17
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
18
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
19
|
+
authorship and/or a database (each, a "Work").
|
|
20
|
+
|
|
21
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
|
22
|
+
the purpose of contributing to a commons of creative, cultural and
|
|
23
|
+
scientific works ("Commons") that the public can reliably and without fear
|
|
24
|
+
of later claims of infringement build upon, modify, incorporate in other
|
|
25
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
26
|
+
and for any purposes, including without limitation commercial purposes.
|
|
27
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
|
28
|
+
culture and the further production of creative, cultural and scientific
|
|
29
|
+
works, or to gain reputation or greater distribution for their Work in
|
|
30
|
+
part through the use and efforts of others.
|
|
31
|
+
|
|
32
|
+
For these and/or other purposes and motivations, and without any
|
|
33
|
+
expectation of additional consideration or compensation, the person
|
|
34
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
35
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
36
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
37
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
38
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
39
|
+
|
|
40
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
41
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
|
42
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
|
43
|
+
limited to, the following:
|
|
44
|
+
|
|
45
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
|
46
|
+
communicate, and translate a Work;
|
|
47
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
48
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
|
49
|
+
likeness depicted in a Work;
|
|
50
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
|
51
|
+
subject to the limitations in paragraph 4(a), below;
|
|
52
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
53
|
+
in a Work;
|
|
54
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
55
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
|
56
|
+
protection of databases, and under any national implementation
|
|
57
|
+
thereof, including any amended or successor version of such
|
|
58
|
+
directive); and
|
|
59
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
|
60
|
+
world based on applicable law or treaty, and any national
|
|
61
|
+
implementations thereof.
|
|
62
|
+
|
|
63
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
64
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
65
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
66
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
67
|
+
of action, whether now known or unknown (including existing as well as
|
|
68
|
+
future claims and causes of action), in the Work (i) in all territories
|
|
69
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
70
|
+
treaty (including future time extensions), (iii) in any current or future
|
|
71
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
72
|
+
including without limitation commercial, advertising or promotional
|
|
73
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
74
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
|
75
|
+
successors, fully intending that such Waiver shall not be subject to
|
|
76
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
|
77
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
78
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
|
79
|
+
|
|
80
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
81
|
+
be judged legally invalid or ineffective under applicable law, then the
|
|
82
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
|
83
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
|
84
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
85
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
86
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
87
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
88
|
+
maximum duration provided by applicable law or treaty (including future
|
|
89
|
+
time extensions), (iii) in any current or future medium and for any number
|
|
90
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
|
91
|
+
limitation commercial, advertising or promotional purposes (the
|
|
92
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
|
93
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
|
94
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
|
95
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
96
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
|
97
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
|
98
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
|
99
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
|
100
|
+
express Statement of Purpose.
|
|
101
|
+
|
|
102
|
+
4. Limitations and Disclaimers.
|
|
103
|
+
|
|
104
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
105
|
+
surrendered, licensed or otherwise affected by this document.
|
|
106
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
|
107
|
+
warranties of any kind concerning the Work, express, implied,
|
|
108
|
+
statutory or otherwise, including without limitation warranties of
|
|
109
|
+
title, merchantability, fitness for a particular purpose, non
|
|
110
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
|
111
|
+
the present or absence of errors, whether or not discoverable, all to
|
|
112
|
+
the greatest extent permissible under applicable law.
|
|
113
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
114
|
+
that may apply to the Work or any use thereof, including without
|
|
115
|
+
limitation any person's Copyright and Related Rights in the Work.
|
|
116
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
117
|
+
consents, permissions or other rights required for any use of the
|
|
118
|
+
Work.
|
|
119
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
120
|
+
party to this document and has no duty or obligation with respect to
|
|
121
|
+
this CC0 or use of the Work.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: flight_tracer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A package to fetch, process, store and plot aircraft trace data from ADS-B Exchange.
|
|
5
|
+
Home-page: https://github.com/stiles/flight_tracer
|
|
6
|
+
Author: Matt Stiles
|
|
7
|
+
Author-email: mattstiles@gmail.com
|
|
8
|
+
License: CC0-1.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: geopandas
|
|
17
|
+
Requires-Dist: boto3
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Requires-Dist: contextily
|
|
20
|
+
Requires-Dist: shapely
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# Tracking ADS-B Exchange flights
|
|
33
|
+
|
|
34
|
+
FlightTracer is a Python package to fetch and process aircraft trace data from ADS-B Exchange. It supports a single ICAO code or a list of codes — and it has an option to upload processed data as CSV and GeoJSON to Amazon S3. FlightTracer also detects separate flight legs based on significant time gaps and creates a combined flight leg identifier (call_sign, date, leg) for easier differentiation in GIS tools. In addition, the package also provides plotting capabilities with a basemap and an option to save the plot as a PNG.
|
|
35
|
+
|
|
36
|
+
This project is in the early stages of development. Contributions and feedback welcome.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
1. Clone the repository or download the source code
|
|
41
|
+
2. Install the required dependencies using pip
|
|
42
|
+
|
|
43
|
+
~~~bash
|
|
44
|
+
pip install requests pandas geopandas boto3 contextily matplotlib
|
|
45
|
+
~~~
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
FlightTracer can be used with either a list of aircraft IDs or a metadata URL that contains aircraft information. You can also configure AWS credentials if you wish to upload the output to S3. In addition, you can pass an AWS profile name if you have multiple sets of credentials in your environment.
|
|
50
|
+
|
|
51
|
+
### Example
|
|
52
|
+
|
|
53
|
+
Below is an example of how to use FlightTracer:
|
|
54
|
+
|
|
55
|
+
~~~python
|
|
56
|
+
#!/usr/bin/env python
|
|
57
|
+
"""
|
|
58
|
+
fetch_example.py
|
|
59
|
+
|
|
60
|
+
This example fetches real flight trace data, processes it (computing the continuous ping_time
|
|
61
|
+
and inferring separate flight legs based on time gaps), saves the results locally
|
|
62
|
+
with a filename that includes the ICAO code(s) and today's date, and optionally
|
|
63
|
+
uploads the CSV and GeoJSON files to S3 using a specified AWS profile.
|
|
64
|
+
It also shows how to generate a plot of the flight activity and save the plot as a PNG.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
import os
|
|
68
|
+
from datetime import date, datetime
|
|
69
|
+
from flight_tracer import FlightTracer
|
|
70
|
+
import geopandas as gpd
|
|
71
|
+
|
|
72
|
+
# Option 1: Use explicit AWS credentials (via environment variables or directly)
|
|
73
|
+
aws_credentials = {
|
|
74
|
+
"aws_access_key_id": os.getenv("MY_AWS_ACCESS_KEY_ID"),
|
|
75
|
+
"aws_secret_access_key": os.getenv("MY_AWS_SECRET_ACCESS_KEY")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Option 2: Alternatively, use an AWS profile from your environment
|
|
79
|
+
aws_profile = os.getenv("MY_AWS_PROFILE") # e.g., "my_profile_name"
|
|
80
|
+
|
|
81
|
+
# Initialize flight_tracer with either explicit credentials or an AWS profile.
|
|
82
|
+
# For example, to use explicit credentials:
|
|
83
|
+
# tracer = flight_tracer(aircraft_ids=["a97753"], aws_creds=aws_credentials)
|
|
84
|
+
# Or, to use an AWS profile:
|
|
85
|
+
tracer = FlightTracer(aircraft_ids=["a97753"], aws_profile=aws_profile)
|
|
86
|
+
|
|
87
|
+
# Define the date range for trace data
|
|
88
|
+
start = date(2025, 1, 28)
|
|
89
|
+
end = date(2025, 2, 1)
|
|
90
|
+
|
|
91
|
+
# Fetch raw flight trace data from ADS-B Exchange
|
|
92
|
+
print("Fetching raw flight trace data...")
|
|
93
|
+
raw_df = tracer.get_traces(start, end)
|
|
94
|
+
if raw_df.empty:
|
|
95
|
+
print("No valid data to process.")
|
|
96
|
+
else:
|
|
97
|
+
print("Raw data sample:")
|
|
98
|
+
print(raw_df.head())
|
|
99
|
+
|
|
100
|
+
# Process trace data into a GeoDataFrame (computing continuous ping_time in UTC
|
|
101
|
+
# and detecting flight leg changes based on time gaps). A new 'flight_leg' column is
|
|
102
|
+
# created that combines call_sign, flight_date, and leg_id.
|
|
103
|
+
print("\nProcessing flight data into a GeoDataFrame...")
|
|
104
|
+
gdf = tracer.process_flight_data(raw_df)
|
|
105
|
+
|
|
106
|
+
# Optionally, retain ground points:
|
|
107
|
+
# gdf = tracer.process_flight_data(raw_df, filter_ground=False)
|
|
108
|
+
|
|
109
|
+
print("Processed GeoDataFrame sample:")
|
|
110
|
+
print(gdf.head())
|
|
111
|
+
|
|
112
|
+
# Optionally, inspect the unique flight legs detected in the data.
|
|
113
|
+
print("\nUnique flight legs detected:")
|
|
114
|
+
print(gdf["flight_leg"].unique())
|
|
115
|
+
|
|
116
|
+
# Build dynamic filenames that include the ICAO code(s) and today's date
|
|
117
|
+
icao_str = "_".join(tracer.aircraft_ids)
|
|
118
|
+
date_str = datetime.today().strftime("%Y%m%d")
|
|
119
|
+
csv_filename = f"data/flight_traces_{icao_str}_{date_str}.csv"
|
|
120
|
+
geojson_filename = f"data/flight_traces_{icao_str}_{date_str}.geojson"
|
|
121
|
+
|
|
122
|
+
# Save the processed data locally as CSV and GeoJSON
|
|
123
|
+
gdf.to_csv(csv_filename, index=False)
|
|
124
|
+
gdf.to_file(geojson_filename, driver="GeoJSON")
|
|
125
|
+
print(f"\nSaved processed data locally as '{csv_filename}' and '{geojson_filename}'.")
|
|
126
|
+
|
|
127
|
+
# Export linestring geometry for each flight leg
|
|
128
|
+
linestring_geojson_filename = f"data/flight_traces_lines_{icao_str}_{date_str}.geojson"
|
|
129
|
+
tracer.export_linestring_geojson(gdf, linestring_geojson_filename)
|
|
130
|
+
|
|
131
|
+
# Optionally upload the processed files to S3
|
|
132
|
+
bucket_name = "your-bucket-name" # replace with your bucket name
|
|
133
|
+
csv_object_name = f"flight_tracer/flight_traces_{icao_str}_{date_str}.csv"
|
|
134
|
+
geojson_object_name = f"flight_tracer/flight_traces_{icao_str}_{date_str}.geojson"
|
|
135
|
+
print("\nUploading files to S3 (if AWS credentials or profile are configured)...")
|
|
136
|
+
tracer.upload_to_s3(gdf, bucket_name, csv_object_name, geojson_object_name)
|
|
137
|
+
print("Upload process completed.")
|
|
138
|
+
|
|
139
|
+
# Plot the points with a basemap and optionally save the plot as a PNG
|
|
140
|
+
fig_filename = f"visuals/flight_map_{icao_str}_{date_str}.png"
|
|
141
|
+
tracer.plot_flights(gdf, geometry_type='points', figsize=(12,10), fig_filename=fig_filename)
|
|
142
|
+
~~~
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
The package supports the following configurations:
|
|
147
|
+
|
|
148
|
+
- **Aircraft IDs or metadata URL**: Provide either a list of ICAO codes or a metadata URL to extract aircraft information.
|
|
149
|
+
- **AWS credentials or profile**: Pass AWS credentials as a dictionary, specify an AWS profile, or set them as environment variables.
|
|
150
|
+
- **Date range**: Define the start and end dates to fetch trace data.
|
|
151
|
+
|
|
152
|
+
## Notes
|
|
153
|
+
|
|
154
|
+
- Ensure that your AWS credentials or profile are configured correctly if you wish to use the S3 upload feature.
|
|
155
|
+
- The package fetches data from ADS-B Exchange so the availability of data depends on the public API.
|
|
156
|
+
- Flight leg detection is based on a configurable time gap threshold (default is 15 minutes). Adjust as needed for your data.
|
|
157
|
+
- Plotting functionality includes an option to expand the plotted extent (via `pad_factor`) for broader context and to save the plot as a PNG.
|
|
158
|
+
|
|
159
|
+
## Roadmap for Enhancements
|
|
160
|
+
|
|
161
|
+
#### Enrich with external metadata:
|
|
162
|
+
- Build tools to “hydrate” your flight data with additional aircraft metadata from external sources such as FAA, ICAO, or other databases (e.g., aircraft type, operator, age, etc.).
|
|
163
|
+
|
|
164
|
+
#### Package distribution:
|
|
165
|
+
- Finalize the code structure (including tests, documentation, and a setup script) and publish the package to PyPI for installation via pip.
|
|
166
|
+
|
|
167
|
+
#### Enhanced documentation and examples:
|
|
168
|
+
- Update the README with comprehensive, end-to-end examples for various scenarios (e.g., pulling all flights for a month of a particular aircraft, processing a list of aircraft from a metadata URL, etc.), including usage examples for linestring export, leg splitting, and metadata enrichment.
|
|
169
|
+
|
|
170
|
+
#### Configurable thresholds and options:
|
|
171
|
+
- Allow users to adjust parameters like the time-gap threshold for determining new flight legs, output formats, and which metadata fields to hydrate.
|
|
172
|
+
|
|
173
|
+
#### Performance and error handling improvements:
|
|
174
|
+
- Add caching or parallel processing options to improve performance when fetching data over a long date range or for multiple aircraft.
|
|
175
|
+
- Improve error handling to gracefully skip problematic dates or flights while logging issues.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
This project is licensed under the **Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication**.
|
|
180
|
+
|
|
181
|
+
This means that to the extent possible under law, the creator has waived all copyright and related or neighboring rights to this work.
|
|
182
|
+
|
|
183
|
+
You can copy, modify, distribute, and perform the work, even for commercial purposes, all without asking permission.
|
|
184
|
+
|
|
185
|
+
For more details, refer to the full [CC0 1.0 Universal License](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
|
|
186
|
+
|
|
187
|
+

|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Tracking ADS-B Exchange flights
|
|
2
|
+
|
|
3
|
+
FlightTracer is a Python package to fetch and process aircraft trace data from ADS-B Exchange. It supports a single ICAO code or a list of codes — and it has an option to upload processed data as CSV and GeoJSON to Amazon S3. FlightTracer also detects separate flight legs based on significant time gaps and creates a combined flight leg identifier (call_sign, date, leg) for easier differentiation in GIS tools. In addition, the package also provides plotting capabilities with a basemap and an option to save the plot as a PNG.
|
|
4
|
+
|
|
5
|
+
This project is in the early stages of development. Contributions and feedback welcome.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
1. Clone the repository or download the source code
|
|
10
|
+
2. Install the required dependencies using pip
|
|
11
|
+
|
|
12
|
+
~~~bash
|
|
13
|
+
pip install requests pandas geopandas boto3 contextily matplotlib
|
|
14
|
+
~~~
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
FlightTracer can be used with either a list of aircraft IDs or a metadata URL that contains aircraft information. You can also configure AWS credentials if you wish to upload the output to S3. In addition, you can pass an AWS profile name if you have multiple sets of credentials in your environment.
|
|
19
|
+
|
|
20
|
+
### Example
|
|
21
|
+
|
|
22
|
+
Below is an example of how to use FlightTracer:
|
|
23
|
+
|
|
24
|
+
~~~python
|
|
25
|
+
#!/usr/bin/env python
|
|
26
|
+
"""
|
|
27
|
+
fetch_example.py
|
|
28
|
+
|
|
29
|
+
This example fetches real flight trace data, processes it (computing the continuous ping_time
|
|
30
|
+
and inferring separate flight legs based on time gaps), saves the results locally
|
|
31
|
+
with a filename that includes the ICAO code(s) and today's date, and optionally
|
|
32
|
+
uploads the CSV and GeoJSON files to S3 using a specified AWS profile.
|
|
33
|
+
It also shows how to generate a plot of the flight activity and save the plot as a PNG.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
from datetime import date, datetime
|
|
38
|
+
from flight_tracer import FlightTracer
|
|
39
|
+
import geopandas as gpd
|
|
40
|
+
|
|
41
|
+
# Option 1: Use explicit AWS credentials (via environment variables or directly)
|
|
42
|
+
aws_credentials = {
|
|
43
|
+
"aws_access_key_id": os.getenv("MY_AWS_ACCESS_KEY_ID"),
|
|
44
|
+
"aws_secret_access_key": os.getenv("MY_AWS_SECRET_ACCESS_KEY")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Option 2: Alternatively, use an AWS profile from your environment
|
|
48
|
+
aws_profile = os.getenv("MY_AWS_PROFILE") # e.g., "my_profile_name"
|
|
49
|
+
|
|
50
|
+
# Initialize flight_tracer with either explicit credentials or an AWS profile.
|
|
51
|
+
# For example, to use explicit credentials:
|
|
52
|
+
# tracer = flight_tracer(aircraft_ids=["a97753"], aws_creds=aws_credentials)
|
|
53
|
+
# Or, to use an AWS profile:
|
|
54
|
+
tracer = FlightTracer(aircraft_ids=["a97753"], aws_profile=aws_profile)
|
|
55
|
+
|
|
56
|
+
# Define the date range for trace data
|
|
57
|
+
start = date(2025, 1, 28)
|
|
58
|
+
end = date(2025, 2, 1)
|
|
59
|
+
|
|
60
|
+
# Fetch raw flight trace data from ADS-B Exchange
|
|
61
|
+
print("Fetching raw flight trace data...")
|
|
62
|
+
raw_df = tracer.get_traces(start, end)
|
|
63
|
+
if raw_df.empty:
|
|
64
|
+
print("No valid data to process.")
|
|
65
|
+
else:
|
|
66
|
+
print("Raw data sample:")
|
|
67
|
+
print(raw_df.head())
|
|
68
|
+
|
|
69
|
+
# Process trace data into a GeoDataFrame (computing continuous ping_time in UTC
|
|
70
|
+
# and detecting flight leg changes based on time gaps). A new 'flight_leg' column is
|
|
71
|
+
# created that combines call_sign, flight_date, and leg_id.
|
|
72
|
+
print("\nProcessing flight data into a GeoDataFrame...")
|
|
73
|
+
gdf = tracer.process_flight_data(raw_df)
|
|
74
|
+
|
|
75
|
+
# Optionally, retain ground points:
|
|
76
|
+
# gdf = tracer.process_flight_data(raw_df, filter_ground=False)
|
|
77
|
+
|
|
78
|
+
print("Processed GeoDataFrame sample:")
|
|
79
|
+
print(gdf.head())
|
|
80
|
+
|
|
81
|
+
# Optionally, inspect the unique flight legs detected in the data.
|
|
82
|
+
print("\nUnique flight legs detected:")
|
|
83
|
+
print(gdf["flight_leg"].unique())
|
|
84
|
+
|
|
85
|
+
# Build dynamic filenames that include the ICAO code(s) and today's date
|
|
86
|
+
icao_str = "_".join(tracer.aircraft_ids)
|
|
87
|
+
date_str = datetime.today().strftime("%Y%m%d")
|
|
88
|
+
csv_filename = f"data/flight_traces_{icao_str}_{date_str}.csv"
|
|
89
|
+
geojson_filename = f"data/flight_traces_{icao_str}_{date_str}.geojson"
|
|
90
|
+
|
|
91
|
+
# Save the processed data locally as CSV and GeoJSON
|
|
92
|
+
gdf.to_csv(csv_filename, index=False)
|
|
93
|
+
gdf.to_file(geojson_filename, driver="GeoJSON")
|
|
94
|
+
print(f"\nSaved processed data locally as '{csv_filename}' and '{geojson_filename}'.")
|
|
95
|
+
|
|
96
|
+
# Export linestring geometry for each flight leg
|
|
97
|
+
linestring_geojson_filename = f"data/flight_traces_lines_{icao_str}_{date_str}.geojson"
|
|
98
|
+
tracer.export_linestring_geojson(gdf, linestring_geojson_filename)
|
|
99
|
+
|
|
100
|
+
# Optionally upload the processed files to S3
|
|
101
|
+
bucket_name = "your-bucket-name" # replace with your bucket name
|
|
102
|
+
csv_object_name = f"flight_tracer/flight_traces_{icao_str}_{date_str}.csv"
|
|
103
|
+
geojson_object_name = f"flight_tracer/flight_traces_{icao_str}_{date_str}.geojson"
|
|
104
|
+
print("\nUploading files to S3 (if AWS credentials or profile are configured)...")
|
|
105
|
+
tracer.upload_to_s3(gdf, bucket_name, csv_object_name, geojson_object_name)
|
|
106
|
+
print("Upload process completed.")
|
|
107
|
+
|
|
108
|
+
# Plot the points with a basemap and optionally save the plot as a PNG
|
|
109
|
+
fig_filename = f"visuals/flight_map_{icao_str}_{date_str}.png"
|
|
110
|
+
tracer.plot_flights(gdf, geometry_type='points', figsize=(12,10), fig_filename=fig_filename)
|
|
111
|
+
~~~
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
The package supports the following configurations:
|
|
116
|
+
|
|
117
|
+
- **Aircraft IDs or metadata URL**: Provide either a list of ICAO codes or a metadata URL to extract aircraft information.
|
|
118
|
+
- **AWS credentials or profile**: Pass AWS credentials as a dictionary, specify an AWS profile, or set them as environment variables.
|
|
119
|
+
- **Date range**: Define the start and end dates to fetch trace data.
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- Ensure that your AWS credentials or profile are configured correctly if you wish to use the S3 upload feature.
|
|
124
|
+
- The package fetches data from ADS-B Exchange so the availability of data depends on the public API.
|
|
125
|
+
- Flight leg detection is based on a configurable time gap threshold (default is 15 minutes). Adjust as needed for your data.
|
|
126
|
+
- Plotting functionality includes an option to expand the plotted extent (via `pad_factor`) for broader context and to save the plot as a PNG.
|
|
127
|
+
|
|
128
|
+
## Roadmap for Enhancements
|
|
129
|
+
|
|
130
|
+
#### Enrich with external metadata:
|
|
131
|
+
- Build tools to “hydrate” your flight data with additional aircraft metadata from external sources such as FAA, ICAO, or other databases (e.g., aircraft type, operator, age, etc.).
|
|
132
|
+
|
|
133
|
+
#### Package distribution:
|
|
134
|
+
- Finalize the code structure (including tests, documentation, and a setup script) and publish the package to PyPI for installation via pip.
|
|
135
|
+
|
|
136
|
+
#### Enhanced documentation and examples:
|
|
137
|
+
- Update the README with comprehensive, end-to-end examples for various scenarios (e.g., pulling all flights for a month of a particular aircraft, processing a list of aircraft from a metadata URL, etc.), including usage examples for linestring export, leg splitting, and metadata enrichment.
|
|
138
|
+
|
|
139
|
+
#### Configurable thresholds and options:
|
|
140
|
+
- Allow users to adjust parameters like the time-gap threshold for determining new flight legs, output formats, and which metadata fields to hydrate.
|
|
141
|
+
|
|
142
|
+
#### Performance and error handling improvements:
|
|
143
|
+
- Add caching or parallel processing options to improve performance when fetching data over a long date range or for multiple aircraft.
|
|
144
|
+
- Improve error handling to gracefully skip problematic dates or flights while logging issues.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
This project is licensed under the **Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication**.
|
|
149
|
+
|
|
150
|
+
This means that to the extent possible under law, the creator has waived all copyright and related or neighboring rights to this work.
|
|
151
|
+
|
|
152
|
+
You can copy, modify, distribute, and perform the work, even for commercial purposes, all without asking permission.
|
|
153
|
+
|
|
154
|
+
For more details, refer to the full [CC0 1.0 Universal License](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
|
|
155
|
+
|
|
156
|
+

|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import FlightTracer
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# flight_tracer/core.py
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import boto3
|
|
6
|
+
import os
|
|
7
|
+
from datetime import date, timedelta
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from shapely.geometry import LineString
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import contextily as ctx
|
|
12
|
+
|
|
13
|
+
class FlightTracer:
|
|
14
|
+
# Default columns from the ADSB trace data (we’ll drop the extra ones)
|
|
15
|
+
DEFAULT_COLUMNS = [
|
|
16
|
+
"time", "lat", "lon", "altitude", "ground_speed", "heading",
|
|
17
|
+
"unknown1", "baro_rate", "details", "code", "alt_geom",
|
|
18
|
+
"unknown2", "unknown3", "unknown4",
|
|
19
|
+
]
|
|
20
|
+
DROP_COLUMNS = ["unknown1", "code", "baro_rate", "unknown2", "unknown3", "unknown4"]
|
|
21
|
+
|
|
22
|
+
def __init__(self, aircraft_ids=None, meta_url=None, aws_creds=None, aws_profile=None):
|
|
23
|
+
"""
|
|
24
|
+
Initialize with a list of aircraft_ids or a metadata URL.
|
|
25
|
+
Optionally pass aws_creds as a dict with keys:
|
|
26
|
+
'aws_access_key_id' and 'aws_secret_access_key'.
|
|
27
|
+
Alternatively, pass aws_profile to use a specific AWS CLI profile.
|
|
28
|
+
"""
|
|
29
|
+
if meta_url:
|
|
30
|
+
meta_df = pd.read_json(meta_url)
|
|
31
|
+
# Clean up and extract ICAO codes from metadata
|
|
32
|
+
self.aircraft_ids = meta_df["icao"].str.strip().str.lower().tolist()
|
|
33
|
+
self.meta_df = meta_df
|
|
34
|
+
elif aircraft_ids:
|
|
35
|
+
self.aircraft_ids = [ac.strip().lower() for ac in aircraft_ids]
|
|
36
|
+
self.meta_df = None
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("Either aircraft_ids or meta_url must be provided")
|
|
39
|
+
|
|
40
|
+
# Set up S3 client using aws_profile if provided, else explicit credentials if provided
|
|
41
|
+
if aws_profile:
|
|
42
|
+
session = boto3.Session(profile_name=aws_profile)
|
|
43
|
+
self.s3_client = session.client('s3')
|
|
44
|
+
elif aws_creds:
|
|
45
|
+
self.s3_client = boto3.client(
|
|
46
|
+
's3',
|
|
47
|
+
aws_access_key_id=aws_creds.get('aws_access_key_id'),
|
|
48
|
+
aws_secret_access_key=aws_creds.get('aws_secret_access_key')
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
self.s3_client = None
|
|
52
|
+
|
|
53
|
+
def generate_urls(self, start_date, end_date):
|
|
54
|
+
"""Generate ADSBExchange URLs for each aircraft over a date range."""
|
|
55
|
+
base_url = "https://globe.adsbexchange.com/globe_history/"
|
|
56
|
+
delta = timedelta(days=1)
|
|
57
|
+
urls = []
|
|
58
|
+
for icao in self.aircraft_ids:
|
|
59
|
+
# Use last 2 chars to build the URL path
|
|
60
|
+
icao_suffix = icao[-2:]
|
|
61
|
+
current_date = start_date
|
|
62
|
+
while current_date <= end_date:
|
|
63
|
+
year = current_date.strftime("%Y")
|
|
64
|
+
month = current_date.strftime("%m")
|
|
65
|
+
day = current_date.strftime("%d")
|
|
66
|
+
url = f"{base_url}{year}/{month}/{day}/traces/{icao_suffix}/trace_full_{icao}.json"
|
|
67
|
+
urls.append((url, icao))
|
|
68
|
+
current_date += delta
|
|
69
|
+
return urls
|
|
70
|
+
|
|
71
|
+
def fetch_trace_data(self, url, icao):
|
|
72
|
+
"""Fetch and return a trace DataFrame from a given URL."""
|
|
73
|
+
headers = {"Referer": f"https://globe.adsbexchange.com/?icao={icao}"}
|
|
74
|
+
response = requests.get(url, headers=headers)
|
|
75
|
+
if response.status_code == 200:
|
|
76
|
+
data = response.json()
|
|
77
|
+
if data and "trace" in data:
|
|
78
|
+
trace_df = pd.DataFrame(data["trace"], columns=self.DEFAULT_COLUMNS)
|
|
79
|
+
trace_df = trace_df.drop(columns=self.DROP_COLUMNS, errors='ignore')
|
|
80
|
+
# Add additional metadata
|
|
81
|
+
trace_df["nnumber"] = data["r"]
|
|
82
|
+
trace_df["model"] = data["t"]
|
|
83
|
+
trace_df["desc"] = data["desc"]
|
|
84
|
+
# Convert the initial timestamp to a datetime
|
|
85
|
+
trace_df["timestamp"] = pd.to_datetime(data["timestamp"], unit="s")
|
|
86
|
+
# Compute the actual ping time by adding the offset
|
|
87
|
+
trace_df["ping_time"] = trace_df["timestamp"] + pd.to_timedelta(trace_df["time"], unit="s")
|
|
88
|
+
trace_df["icao"] = icao # Add the aircraft id to the DataFrame
|
|
89
|
+
return trace_df
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_traces(self, start_date, end_date):
|
|
94
|
+
"""
|
|
95
|
+
Loop through generated URLs to fetch trace data.
|
|
96
|
+
Returns a concatenated DataFrame of all valid traces.
|
|
97
|
+
"""
|
|
98
|
+
urls = self.generate_urls(start_date, end_date)
|
|
99
|
+
traces = []
|
|
100
|
+
for url, icao in urls:
|
|
101
|
+
trace_df = self.fetch_trace_data(url, icao)
|
|
102
|
+
if trace_df is not None and not trace_df.empty:
|
|
103
|
+
traces.append(trace_df)
|
|
104
|
+
# Print a friendly message using the first timestamp
|
|
105
|
+
ts = pd.to_datetime(trace_df["timestamp"].iloc[0]).strftime('%b %-d, %Y')
|
|
106
|
+
print(f"Yay! {icao} flew on {ts}.")
|
|
107
|
+
else:
|
|
108
|
+
print(f"No data for {icao} on {url}.")
|
|
109
|
+
if traces:
|
|
110
|
+
return pd.concat(traces).reset_index(drop=True).sort_values("timestamp")
|
|
111
|
+
else:
|
|
112
|
+
print("No valid trace data collected.")
|
|
113
|
+
return pd.DataFrame()
|
|
114
|
+
|
|
115
|
+
def process_flight_data(self, df, mapping_info=None, filter_ground=True):
|
|
116
|
+
"""
|
|
117
|
+
Process the raw trace data:
|
|
118
|
+
- Compute point_time (UTC) and convert to US/Pacific.
|
|
119
|
+
- Detect flight leg changes by looking at time gaps.
|
|
120
|
+
- Normalize JSON details into columns.
|
|
121
|
+
- Optionally map a flight column to additional info (like owner).
|
|
122
|
+
- Create a combined flight_leg column.
|
|
123
|
+
- Optionally filter out ground-level data.
|
|
124
|
+
- Return a GeoDataFrame.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
df (DataFrame): Raw flight trace data.
|
|
128
|
+
mapping_info (tuple or None): If provided, maps flight details to additional metadata.
|
|
129
|
+
filter_ground (bool): If True (default), removes rows where altitude is "ground".
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
GeoDataFrame: Processed flight data with spatial points.
|
|
133
|
+
"""
|
|
134
|
+
# First sort and compute the continuous ping time
|
|
135
|
+
df = df.sort_values(['timestamp', 'time'])
|
|
136
|
+
df["point_time"] = df["timestamp"] + pd.to_timedelta(df["time"], unit="s")
|
|
137
|
+
df["timestamp_pst"] = df["point_time"].dt.tz_localize("UTC").dt.tz_convert("US/Pacific")
|
|
138
|
+
df["point_time_pst_clean"] = df["timestamp_pst"].dt.strftime("%H:%M:%S")
|
|
139
|
+
df["flight_date_pst"] = df["timestamp_pst"].dt.strftime("%Y-%m-%d")
|
|
140
|
+
|
|
141
|
+
# --- Begin leg detection ---
|
|
142
|
+
df = df.sort_values(['icao', 'point_time'])
|
|
143
|
+
df['time_diff'] = df.groupby(['icao', 'flight_date_pst'])['point_time'].diff()
|
|
144
|
+
threshold = pd.Timedelta(minutes=15)
|
|
145
|
+
df['new_leg'] = (df['time_diff'] > threshold).fillna(0).astype(int)
|
|
146
|
+
df['leg_id'] = df.groupby(['icao', 'flight_date_pst'])['new_leg'].cumsum() + 1
|
|
147
|
+
# --- End leg detection ---
|
|
148
|
+
|
|
149
|
+
# Extract the call sign from the 'details' column and fill forward missing values
|
|
150
|
+
df["call_sign"] = pd.json_normalize(df['details'])['flight'].str.strip()
|
|
151
|
+
df["call_sign"] = df["call_sign"].ffill()
|
|
152
|
+
|
|
153
|
+
# Create a combined flight_leg field: call_sign + "_" + flight_date_pst + "_leg" + leg_id
|
|
154
|
+
df["flight_leg"] = df["call_sign"] + "_" + df["flight_date_pst"] + "_leg" + df["leg_id"].astype(str)
|
|
155
|
+
|
|
156
|
+
# Flatten the 'details' JSON column (drop alt_geom if present)
|
|
157
|
+
details_df = pd.json_normalize(df["details"]).drop(columns=["alt_geom"], errors='ignore')
|
|
158
|
+
df = df.join(details_df)
|
|
159
|
+
|
|
160
|
+
# Optionally apply a mapping (e.g. flight -> owner) if mapping_info is provided
|
|
161
|
+
if mapping_info and "flight" in df.columns:
|
|
162
|
+
meta_df, key_col, value_col, target_col = mapping_info
|
|
163
|
+
mapping = meta_df.set_index(key_col)[value_col].to_dict()
|
|
164
|
+
df[target_col] = df["flight"].map(mapping)
|
|
165
|
+
|
|
166
|
+
# Select a common set of columns, explicitly including point_time for later sorting.
|
|
167
|
+
cols = ["flight", "point_time", "flight_date_pst", "point_time_pst_clean", "altitude",
|
|
168
|
+
"ground_speed", "heading", "lat", "lon", "icao", "call_sign", "leg_id", "flight_leg"]
|
|
169
|
+
if mapping_info:
|
|
170
|
+
cols.append(target_col)
|
|
171
|
+
|
|
172
|
+
# Apply altitude filter based on user preference
|
|
173
|
+
if filter_ground:
|
|
174
|
+
df = df.query('altitude != "ground"').copy()
|
|
175
|
+
|
|
176
|
+
# Create and return a GeoDataFrame with points from lon/lat
|
|
177
|
+
return gpd.GeoDataFrame(df[cols], geometry=gpd.points_from_xy(df.lon, df.lat))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def export_linestring_geojson(self, gdf, output_file):
|
|
181
|
+
"""
|
|
182
|
+
Given a GeoDataFrame of flight trace points (which includes a 'flight_leg'
|
|
183
|
+
and a continuous 'point_time' column), group the data by flight_leg,
|
|
184
|
+
create a LineString for each leg, and export the result as a GeoJSON file.
|
|
185
|
+
|
|
186
|
+
Parameters:
|
|
187
|
+
gdf (GeoDataFrame): The GeoDataFrame of flight points.
|
|
188
|
+
output_file (str): Path to the output GeoJSON file.
|
|
189
|
+
"""
|
|
190
|
+
legs = []
|
|
191
|
+
# Group by the unique flight_leg identifier
|
|
192
|
+
for flight_leg, group in gdf.groupby("flight_leg"):
|
|
193
|
+
# Sort the group by point_time so the points are in order
|
|
194
|
+
group = group.sort_values("point_time")
|
|
195
|
+
points = list(group.geometry)
|
|
196
|
+
# If more than one point, create a LineString; otherwise, use the single point.
|
|
197
|
+
if len(points) > 1:
|
|
198
|
+
geometry = LineString(points)
|
|
199
|
+
else:
|
|
200
|
+
geometry = points[0]
|
|
201
|
+
# Capture representative attributes from the first row.
|
|
202
|
+
legs.append({
|
|
203
|
+
"flight_leg": flight_leg,
|
|
204
|
+
"icao": group["icao"].iloc[0],
|
|
205
|
+
"call_sign": group["call_sign"].iloc[0],
|
|
206
|
+
"flight_date_pst": group["flight_date_pst"].iloc[0],
|
|
207
|
+
"leg_id": group["leg_id"].iloc[0],
|
|
208
|
+
"geometry": geometry
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
gdf_lines = gpd.GeoDataFrame(legs, crs=gdf.crs)
|
|
212
|
+
gdf_lines.to_file(output_file, driver="GeoJSON")
|
|
213
|
+
print(f"Linestring GeoJSON exported to {output_file}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def plot_flights(self, gdf, geometry_type='points', figsize=(10,10), pad_factor=0.2, zoom=None, fig_filename=None):
|
|
218
|
+
"""
|
|
219
|
+
Plot flight activity from a GeoDataFrame with a basemap.
|
|
220
|
+
|
|
221
|
+
Parameters:
|
|
222
|
+
gdf (GeoDataFrame): The GeoDataFrame containing flight trace points or lines.
|
|
223
|
+
geometry_type (str): 'points' or 'lines' – determines what geometry to plot.
|
|
224
|
+
figsize (tuple): Figure size for the plot.
|
|
225
|
+
pad_factor (float): Fraction by which to expand the bounds for additional context.
|
|
226
|
+
zoom (int or None): Optional zoom level override for the basemap.
|
|
227
|
+
fig_filename (str or None): If provided, the plot will be saved to this PNG file.
|
|
228
|
+
|
|
229
|
+
This method reprojects the GeoDataFrame to EPSG:3857, computes a padded extent, plots
|
|
230
|
+
the data with a basemap, and optionally saves the figure.
|
|
231
|
+
"""
|
|
232
|
+
# Ensure the GeoDataFrame has a CRS; assume EPSG:4326 if not set.
|
|
233
|
+
if gdf.crs is None:
|
|
234
|
+
gdf = gdf.set_crs(epsg=4326)
|
|
235
|
+
# Reproject to Web Mercator for basemap compatibility.
|
|
236
|
+
gdf_plot = gdf.to_crs(epsg=3857)
|
|
237
|
+
|
|
238
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
239
|
+
|
|
240
|
+
# Plot based on the desired geometry type.
|
|
241
|
+
if geometry_type == 'points':
|
|
242
|
+
gdf_plot.plot(ax=ax, marker='o', color='#f18851', markersize=5, label='Flight points')
|
|
243
|
+
elif geometry_type == 'lines':
|
|
244
|
+
gdf_plot.plot(ax=ax, linewidth=2, color='#f18851', label='Flight path')
|
|
245
|
+
else:
|
|
246
|
+
raise ValueError("geometry_type must be either 'points' or 'lines'")
|
|
247
|
+
|
|
248
|
+
# Get the total bounds: [xmin, ymin, xmax, ymax]
|
|
249
|
+
xmin, ymin, xmax, ymax = gdf_plot.total_bounds
|
|
250
|
+
# Calculate padding based on the pad_factor.
|
|
251
|
+
x_pad = (xmax - xmin) * pad_factor
|
|
252
|
+
y_pad = (ymax - ymin) * pad_factor
|
|
253
|
+
extent = [xmin - x_pad, ymin - y_pad, xmax + x_pad, ymax + y_pad]
|
|
254
|
+
|
|
255
|
+
# Set the axis limits to the padded extent.
|
|
256
|
+
ax.set_xlim(extent[0], extent[2])
|
|
257
|
+
ax.set_ylim(extent[1], extent[3])
|
|
258
|
+
|
|
259
|
+
# Add a basemap using CartoDB Positron.
|
|
260
|
+
if zoom is not None:
|
|
261
|
+
ctx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, zoom=zoom, reset_extent=False)
|
|
262
|
+
else:
|
|
263
|
+
ctx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, reset_extent=False)
|
|
264
|
+
|
|
265
|
+
# Clean up the plot aesthetics.
|
|
266
|
+
ax.set_axis_off()
|
|
267
|
+
ax.legend()
|
|
268
|
+
plt.tight_layout()
|
|
269
|
+
plt.title("Flight sketch")
|
|
270
|
+
|
|
271
|
+
# If a figure filename is provided, save the figure as a PNG.
|
|
272
|
+
if fig_filename:
|
|
273
|
+
# Create the directory if it doesn't exist.
|
|
274
|
+
os.makedirs(os.path.dirname(fig_filename), exist_ok=True)
|
|
275
|
+
plt.savefig(fig_filename, dpi=300, bbox_inches='tight')
|
|
276
|
+
print(f"Figure saved as {fig_filename}")
|
|
277
|
+
|
|
278
|
+
plt.show()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def upload_to_s3(self, gdf, bucket_name, csv_object_name, geojson_object_name):
|
|
282
|
+
"""Upload the GeoDataFrame as both CSV and GeoJSON to S3 (if configured)."""
|
|
283
|
+
if not self.s3_client:
|
|
284
|
+
print("S3 client not configured; skipping upload.")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
# Save CSV in memory and upload
|
|
288
|
+
csv_buffer = BytesIO()
|
|
289
|
+
gdf.to_csv(csv_buffer, index=False)
|
|
290
|
+
csv_buffer.seek(0)
|
|
291
|
+
self.s3_client.put_object(Bucket=bucket_name, Key=csv_object_name, Body=csv_buffer.getvalue())
|
|
292
|
+
print(f"CSV uploaded to s3://{bucket_name}/{csv_object_name}")
|
|
293
|
+
|
|
294
|
+
# Remove non-serializable columns (e.g., 'point_time') before exporting to GeoJSON.
|
|
295
|
+
# Alternatively, you could convert it to a string using .astype(str)
|
|
296
|
+
gdf_json = gdf.drop(columns=["point_time"], errors="ignore")
|
|
297
|
+
geojson_str = gdf_json.to_json()
|
|
298
|
+
self.s3_client.put_object(
|
|
299
|
+
Bucket=bucket_name,
|
|
300
|
+
Key=geojson_object_name,
|
|
301
|
+
Body=geojson_str.encode('utf-8')
|
|
302
|
+
)
|
|
303
|
+
print(f"GeoJSON uploaded to s3://{bucket_name}/{geojson_object_name}")
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: flight_tracer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A package to fetch, process, store and plot aircraft trace data from ADS-B Exchange.
|
|
5
|
+
Home-page: https://github.com/stiles/flight_tracer
|
|
6
|
+
Author: Matt Stiles
|
|
7
|
+
Author-email: mattstiles@gmail.com
|
|
8
|
+
License: CC0-1.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: geopandas
|
|
17
|
+
Requires-Dist: boto3
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Requires-Dist: contextily
|
|
20
|
+
Requires-Dist: shapely
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# Tracking ADS-B Exchange flights
|
|
33
|
+
|
|
34
|
+
FlightTracer is a Python package to fetch and process aircraft trace data from ADS-B Exchange. It supports a single ICAO code or a list of codes — and it has an option to upload processed data as CSV and GeoJSON to Amazon S3. FlightTracer also detects separate flight legs based on significant time gaps and creates a combined flight leg identifier (call_sign, date, leg) for easier differentiation in GIS tools. In addition, the package also provides plotting capabilities with a basemap and an option to save the plot as a PNG.
|
|
35
|
+
|
|
36
|
+
This project is in the early stages of development. Contributions and feedback welcome.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
1. Clone the repository or download the source code
|
|
41
|
+
2. Install the required dependencies using pip
|
|
42
|
+
|
|
43
|
+
~~~bash
|
|
44
|
+
pip install requests pandas geopandas boto3 contextily matplotlib
|
|
45
|
+
~~~
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
FlightTracer can be used with either a list of aircraft IDs or a metadata URL that contains aircraft information. You can also configure AWS credentials if you wish to upload the output to S3. In addition, you can pass an AWS profile name if you have multiple sets of credentials in your environment.
|
|
50
|
+
|
|
51
|
+
### Example
|
|
52
|
+
|
|
53
|
+
Below is an example of how to use FlightTracer:
|
|
54
|
+
|
|
55
|
+
~~~python
|
|
56
|
+
#!/usr/bin/env python
|
|
57
|
+
"""
|
|
58
|
+
fetch_example.py
|
|
59
|
+
|
|
60
|
+
This example fetches real flight trace data, processes it (computing the continuous ping_time
|
|
61
|
+
and inferring separate flight legs based on time gaps), saves the results locally
|
|
62
|
+
with a filename that includes the ICAO code(s) and today's date, and optionally
|
|
63
|
+
uploads the CSV and GeoJSON files to S3 using a specified AWS profile.
|
|
64
|
+
It also shows how to generate a plot of the flight activity and save the plot as a PNG.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
import os
|
|
68
|
+
from datetime import date, datetime
|
|
69
|
+
from flight_tracer import FlightTracer
|
|
70
|
+
import geopandas as gpd
|
|
71
|
+
|
|
72
|
+
# Option 1: Use explicit AWS credentials (via environment variables or directly)
|
|
73
|
+
aws_credentials = {
|
|
74
|
+
"aws_access_key_id": os.getenv("MY_AWS_ACCESS_KEY_ID"),
|
|
75
|
+
"aws_secret_access_key": os.getenv("MY_AWS_SECRET_ACCESS_KEY")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Option 2: Alternatively, use an AWS profile from your environment
|
|
79
|
+
aws_profile = os.getenv("MY_AWS_PROFILE") # e.g., "my_profile_name"
|
|
80
|
+
|
|
81
|
+
# Initialize flight_tracer with either explicit credentials or an AWS profile.
|
|
82
|
+
# For example, to use explicit credentials:
|
|
83
|
+
# tracer = flight_tracer(aircraft_ids=["a97753"], aws_creds=aws_credentials)
|
|
84
|
+
# Or, to use an AWS profile:
|
|
85
|
+
tracer = FlightTracer(aircraft_ids=["a97753"], aws_profile=aws_profile)
|
|
86
|
+
|
|
87
|
+
# Define the date range for trace data
|
|
88
|
+
start = date(2025, 1, 28)
|
|
89
|
+
end = date(2025, 2, 1)
|
|
90
|
+
|
|
91
|
+
# Fetch raw flight trace data from ADS-B Exchange
|
|
92
|
+
print("Fetching raw flight trace data...")
|
|
93
|
+
raw_df = tracer.get_traces(start, end)
|
|
94
|
+
if raw_df.empty:
|
|
95
|
+
print("No valid data to process.")
|
|
96
|
+
else:
|
|
97
|
+
print("Raw data sample:")
|
|
98
|
+
print(raw_df.head())
|
|
99
|
+
|
|
100
|
+
# Process trace data into a GeoDataFrame (computing continuous ping_time in UTC
|
|
101
|
+
# and detecting flight leg changes based on time gaps). A new 'flight_leg' column is
|
|
102
|
+
# created that combines call_sign, flight_date, and leg_id.
|
|
103
|
+
print("\nProcessing flight data into a GeoDataFrame...")
|
|
104
|
+
gdf = tracer.process_flight_data(raw_df)
|
|
105
|
+
|
|
106
|
+
# Optionally, retain ground points:
|
|
107
|
+
# gdf = tracer.process_flight_data(raw_df, filter_ground=False)
|
|
108
|
+
|
|
109
|
+
print("Processed GeoDataFrame sample:")
|
|
110
|
+
print(gdf.head())
|
|
111
|
+
|
|
112
|
+
# Optionally, inspect the unique flight legs detected in the data.
|
|
113
|
+
print("\nUnique flight legs detected:")
|
|
114
|
+
print(gdf["flight_leg"].unique())
|
|
115
|
+
|
|
116
|
+
# Build dynamic filenames that include the ICAO code(s) and today's date
|
|
117
|
+
icao_str = "_".join(tracer.aircraft_ids)
|
|
118
|
+
date_str = datetime.today().strftime("%Y%m%d")
|
|
119
|
+
csv_filename = f"data/flight_traces_{icao_str}_{date_str}.csv"
|
|
120
|
+
geojson_filename = f"data/flight_traces_{icao_str}_{date_str}.geojson"
|
|
121
|
+
|
|
122
|
+
# Save the processed data locally as CSV and GeoJSON
|
|
123
|
+
gdf.to_csv(csv_filename, index=False)
|
|
124
|
+
gdf.to_file(geojson_filename, driver="GeoJSON")
|
|
125
|
+
print(f"\nSaved processed data locally as '{csv_filename}' and '{geojson_filename}'.")
|
|
126
|
+
|
|
127
|
+
# Export linestring geometry for each flight leg
|
|
128
|
+
linestring_geojson_filename = f"data/flight_traces_lines_{icao_str}_{date_str}.geojson"
|
|
129
|
+
tracer.export_linestring_geojson(gdf, linestring_geojson_filename)
|
|
130
|
+
|
|
131
|
+
# Optionally upload the processed files to S3
|
|
132
|
+
bucket_name = "your-bucket-name" # replace with your bucket name
|
|
133
|
+
csv_object_name = f"flight_tracer/flight_traces_{icao_str}_{date_str}.csv"
|
|
134
|
+
geojson_object_name = f"flight_tracer/flight_traces_{icao_str}_{date_str}.geojson"
|
|
135
|
+
print("\nUploading files to S3 (if AWS credentials or profile are configured)...")
|
|
136
|
+
tracer.upload_to_s3(gdf, bucket_name, csv_object_name, geojson_object_name)
|
|
137
|
+
print("Upload process completed.")
|
|
138
|
+
|
|
139
|
+
# Plot the points with a basemap and optionally save the plot as a PNG
|
|
140
|
+
fig_filename = f"visuals/flight_map_{icao_str}_{date_str}.png"
|
|
141
|
+
tracer.plot_flights(gdf, geometry_type='points', figsize=(12,10), fig_filename=fig_filename)
|
|
142
|
+
~~~
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
The package supports the following configurations:
|
|
147
|
+
|
|
148
|
+
- **Aircraft IDs or metadata URL**: Provide either a list of ICAO codes or a metadata URL to extract aircraft information.
|
|
149
|
+
- **AWS credentials or profile**: Pass AWS credentials as a dictionary, specify an AWS profile, or set them as environment variables.
|
|
150
|
+
- **Date range**: Define the start and end dates to fetch trace data.
|
|
151
|
+
|
|
152
|
+
## Notes
|
|
153
|
+
|
|
154
|
+
- Ensure that your AWS credentials or profile are configured correctly if you wish to use the S3 upload feature.
|
|
155
|
+
- The package fetches data from ADS-B Exchange so the availability of data depends on the public API.
|
|
156
|
+
- Flight leg detection is based on a configurable time gap threshold (default is 15 minutes). Adjust as needed for your data.
|
|
157
|
+
- Plotting functionality includes an option to expand the plotted extent (via `pad_factor`) for broader context and to save the plot as a PNG.
|
|
158
|
+
|
|
159
|
+
## Roadmap for Enhancements
|
|
160
|
+
|
|
161
|
+
#### Enrich with external metadata:
|
|
162
|
+
- Build tools to “hydrate” your flight data with additional aircraft metadata from external sources such as FAA, ICAO, or other databases (e.g., aircraft type, operator, age, etc.).
|
|
163
|
+
|
|
164
|
+
#### Package distribution:
|
|
165
|
+
- Finalize the code structure (including tests, documentation, and a setup script) and publish the package to PyPI for installation via pip.
|
|
166
|
+
|
|
167
|
+
#### Enhanced documentation and examples:
|
|
168
|
+
- Update the README with comprehensive, end-to-end examples for various scenarios (e.g., pulling all flights for a month of a particular aircraft, processing a list of aircraft from a metadata URL, etc.), including usage examples for linestring export, leg splitting, and metadata enrichment.
|
|
169
|
+
|
|
170
|
+
#### Configurable thresholds and options:
|
|
171
|
+
- Allow users to adjust parameters like the time-gap threshold for determining new flight legs, output formats, and which metadata fields to hydrate.
|
|
172
|
+
|
|
173
|
+
#### Performance and error handling improvements:
|
|
174
|
+
- Add caching or parallel processing options to improve performance when fetching data over a long date range or for multiple aircraft.
|
|
175
|
+
- Improve error handling to gracefully skip problematic dates or flights while logging issues.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
This project is licensed under the **Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication**.
|
|
180
|
+
|
|
181
|
+
This means that to the extent possible under law, the creator has waived all copyright and related or neighboring rights to this work.
|
|
182
|
+
|
|
183
|
+
You can copy, modify, distribute, and perform the work, even for commercial purposes, all without asking permission.
|
|
184
|
+
|
|
185
|
+
For more details, refer to the full [CC0 1.0 Universal License](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
|
|
186
|
+
|
|
187
|
+

|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
flight_tracer/__init__.py
|
|
7
|
+
flight_tracer/core.py
|
|
8
|
+
flight_tracer.egg-info/PKG-INFO
|
|
9
|
+
flight_tracer.egg-info/SOURCES.txt
|
|
10
|
+
flight_tracer.egg-info/dependency_links.txt
|
|
11
|
+
flight_tracer.egg-info/requires.txt
|
|
12
|
+
flight_tracer.egg-info/top_level.txt
|
|
13
|
+
tests/__init__.py
|
|
14
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = flight_tracer
|
|
3
|
+
version = 0.1.0
|
|
4
|
+
description = A package to fetch, process, store and plot aircraft trace data from ADS-B Exchange.
|
|
5
|
+
author = Matt Stiles
|
|
6
|
+
author_email = mattstiles@gmail.com
|
|
7
|
+
license = CC0-1.0
|
|
8
|
+
license_files = LICENSE # This is OK
|
|
9
|
+
long_description = file: README.md
|
|
10
|
+
long_description_content_type = text/markdown
|
|
11
|
+
url = https://github.com/stiles/flight_tracer
|
|
12
|
+
classifiers =
|
|
13
|
+
Programming Language :: Python :: 3
|
|
14
|
+
License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
15
|
+
Operating System :: OS Independent
|
|
16
|
+
|
|
17
|
+
[options]
|
|
18
|
+
packages = find:
|
|
19
|
+
install_requires =
|
|
20
|
+
requests
|
|
21
|
+
pandas
|
|
22
|
+
geopandas
|
|
23
|
+
boto3
|
|
24
|
+
matplotlib
|
|
25
|
+
contextily
|
|
26
|
+
shapely
|
|
27
|
+
python_requires = >=3.7
|
|
28
|
+
|
|
29
|
+
[egg_info]
|
|
30
|
+
tag_build =
|
|
31
|
+
tag_date = 0
|
|
32
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="flight_tracer",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
author="Matt Stiles",
|
|
7
|
+
author_email="mattstiles@gmail.com",
|
|
8
|
+
description="A package to fetch, process, store and plot aircraft trace data from ADS-B Exchange.",
|
|
9
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
10
|
+
long_description_content_type="text/markdown",
|
|
11
|
+
url="https://github.com/stiles/flight_tracer",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
"requests",
|
|
15
|
+
"pandas",
|
|
16
|
+
"geopandas",
|
|
17
|
+
"boto3",
|
|
18
|
+
"matplotlib",
|
|
19
|
+
"contextily",
|
|
20
|
+
"shapely"
|
|
21
|
+
],
|
|
22
|
+
classifiers=[
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
],
|
|
27
|
+
license="CC0-1.0",
|
|
28
|
+
python_requires='>=3.7',
|
|
29
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import date
|
|
3
|
+
from flight_tracer import FlightTracer
|
|
4
|
+
|
|
5
|
+
class TestFlightTracer(unittest.TestCase):
|
|
6
|
+
def test_generate_urls_single_day(self):
|
|
7
|
+
# Create a FlightTracer instance with a dummy ICAO list
|
|
8
|
+
tracer = FlightTracer(aircraft_ids=["0d086e"])
|
|
9
|
+
# Using a single day: Jan 1, 2025 to Jan 1, 2025
|
|
10
|
+
urls = tracer.generate_urls(date(2025, 1, 1), date(2025, 1, 1))
|
|
11
|
+
# Expect 1 URL for a single day
|
|
12
|
+
self.assertEqual(len(urls), 1)
|
|
13
|
+
# Verify that the URL contains the correct date parts and ICAO code
|
|
14
|
+
expected_url = "https://globe.adsbexchange.com/globe_history/2025/01/01/traces/6e/trace_full_0d086e.json"
|
|
15
|
+
self.assertEqual(urls[0][0], expected_url)
|
|
16
|
+
|
|
17
|
+
def test_generate_urls_multiple_days(self):
|
|
18
|
+
tracer = FlightTracer(aircraft_ids=["0d086e"])
|
|
19
|
+
# Using two days: Jan 1, 2025 to Jan 2, 2025
|
|
20
|
+
urls = tracer.generate_urls(date(2025, 1, 1), date(2025, 1, 2))
|
|
21
|
+
# Expect 2 URLs (one for each day)
|
|
22
|
+
self.assertEqual(len(urls), 2)
|
|
23
|
+
|
|
24
|
+
if __name__ == '__main__':
|
|
25
|
+
unittest.main()
|