windborne 1.1.2__py3-none-any.whl → 1.1.4__py3-none-any.whl
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.
- windborne/__init__.py +6 -0
- windborne/cli.py +44 -2
- windborne/data_api.py +125 -17
- windborne/forecasts_api.py +10 -43
- windborne/{cyclone_formatting.py → track_formatting.py} +75 -23
- {windborne-1.1.2.dist-info → windborne-1.1.4.dist-info}/METADATA +1 -1
- windborne-1.1.4.dist-info/RECORD +13 -0
- windborne-1.1.2.dist-info/RECORD +0 -13
- {windborne-1.1.2.dist-info → windborne-1.1.4.dist-info}/WHEEL +0 -0
- {windborne-1.1.2.dist-info → windborne-1.1.4.dist-info}/entry_points.txt +0 -0
- {windborne-1.1.2.dist-info → windborne-1.1.4.dist-info}/top_level.txt +0 -0
windborne/__init__.py
CHANGED
@@ -14,6 +14,8 @@ from .data_api import (
|
|
14
14
|
get_flying_missions,
|
15
15
|
get_mission_launch_site,
|
16
16
|
get_predicted_path,
|
17
|
+
get_current_location,
|
18
|
+
get_flight_path
|
17
19
|
)
|
18
20
|
|
19
21
|
# Import Forecasts API functions
|
@@ -21,6 +23,7 @@ from .forecasts_api import (
|
|
21
23
|
get_point_forecasts,
|
22
24
|
get_initialization_times,
|
23
25
|
|
26
|
+
get_full_gridded_forecast,
|
24
27
|
get_temperature_2m,
|
25
28
|
# get_dewpoint_2m,
|
26
29
|
get_wind_u_10m, get_wind_v_10m,
|
@@ -50,10 +53,13 @@ __all__ = [
|
|
50
53
|
"get_flying_missions",
|
51
54
|
"get_mission_launch_site",
|
52
55
|
"get_predicted_path",
|
56
|
+
"get_current_location",
|
57
|
+
"get_flight_path",
|
53
58
|
|
54
59
|
"get_point_forecasts",
|
55
60
|
"get_initialization_times",
|
56
61
|
|
62
|
+
"get_full_gridded_forecast",
|
57
63
|
"get_temperature_2m",
|
58
64
|
# "get_dewpoint_2m",
|
59
65
|
"get_wind_u_10m",
|
windborne/cli.py
CHANGED
@@ -14,9 +14,12 @@ from . import (
|
|
14
14
|
get_flying_missions,
|
15
15
|
get_mission_launch_site,
|
16
16
|
get_predicted_path,
|
17
|
+
get_current_location,
|
18
|
+
get_flight_path,
|
17
19
|
|
18
20
|
get_point_forecasts,
|
19
21
|
get_initialization_times,
|
22
|
+
get_full_gridded_forecast,
|
20
23
|
get_temperature_2m,
|
21
24
|
# get_dewpoint_2m,
|
22
25
|
get_wind_u_10m, get_wind_v_10m,
|
@@ -120,11 +123,21 @@ def main():
|
|
120
123
|
launch_site_parser.add_argument('mission_id', help='Mission ID')
|
121
124
|
launch_site_parser.add_argument('output', nargs='?', help='Output file')
|
122
125
|
|
126
|
+
# Get Current Location Command
|
127
|
+
current_location_parser = subparsers.add_parser('current-location', help='Get current location')
|
128
|
+
current_location_parser.add_argument('mission_id', help='Mission ID')
|
129
|
+
current_location_parser.add_argument('output', nargs='?', help='Output file')
|
130
|
+
|
123
131
|
# Get Predicted Path Command
|
124
132
|
prediction_parser = subparsers.add_parser('predict-path', help='Get predicted flight path')
|
125
133
|
prediction_parser.add_argument('mission_id', help='Mission ID')
|
126
134
|
prediction_parser.add_argument('output', nargs='?', help='Output file')
|
127
135
|
|
136
|
+
# Get Flight Path Command
|
137
|
+
flight_path_parser = subparsers.add_parser('flight-path', help='Get traveled flight path')
|
138
|
+
flight_path_parser.add_argument('mission_id', help='Mission ID')
|
139
|
+
flight_path_parser.add_argument('output', nargs='?', help='Output file')
|
140
|
+
|
128
141
|
####################################################################################################################
|
129
142
|
# FORECASTS API FUNCTIONS
|
130
143
|
####################################################################################################################
|
@@ -143,6 +156,9 @@ def main():
|
|
143
156
|
|
144
157
|
# GRIDDED FORECASTS
|
145
158
|
####################################################################################################################
|
159
|
+
full_gridded_parser = subparsers.add_parser('grid_full', help='Get full gridded forecast')
|
160
|
+
full_gridded_parser.add_argument('args', nargs='*', help='time output_file')
|
161
|
+
|
146
162
|
# Gridded 2m temperature Command
|
147
163
|
gridded_temperature_2m_parser = subparsers.add_parser('grid_temp_2m', help='Get gridded output of global 2m temperature forecasts')
|
148
164
|
gridded_temperature_2m_parser.add_argument('args', nargs='*', help='time output_file')
|
@@ -373,12 +389,27 @@ def main():
|
|
373
389
|
output_file=args.output,
|
374
390
|
print_result=(not args.output)
|
375
391
|
)
|
376
|
-
|
392
|
+
elif args.command == 'current-location':
|
393
|
+
get_current_location(
|
394
|
+
mission_id=args.mission_id,
|
395
|
+
output_file=args.output,
|
396
|
+
print_result=(not args.output)
|
397
|
+
)
|
377
398
|
elif args.command == 'predict-path':
|
378
399
|
get_predicted_path(
|
379
400
|
mission_id=args.mission_id,
|
380
|
-
output_file=args.output
|
401
|
+
output_file=args.output,
|
402
|
+
print_result=(not args.output)
|
403
|
+
)
|
404
|
+
|
405
|
+
elif args.command == 'flight-path':
|
406
|
+
get_flight_path(
|
407
|
+
mission_id=args.mission_id,
|
408
|
+
output_file=args.output,
|
409
|
+
print_result=(not args.output)
|
381
410
|
)
|
411
|
+
|
412
|
+
|
382
413
|
####################################################################################################################
|
383
414
|
# FORECASTS API FUNCTIONS CALLED
|
384
415
|
####################################################################################################################
|
@@ -403,6 +434,17 @@ def main():
|
|
403
434
|
elif args.command == 'init_times':
|
404
435
|
get_initialization_times(print_response=True)
|
405
436
|
|
437
|
+
if args.command == 'grid_full':
|
438
|
+
# Parse get_full_gridded_forecast arguments
|
439
|
+
if len(args.args) in [0,1]:
|
440
|
+
print("To get the full gridded forecast you need to provide the time for which to get the forecast and an output file.")
|
441
|
+
print("\nUsage: windborne get_full_gridded_forecast time output_file")
|
442
|
+
elif len(args.args) == 2:
|
443
|
+
get_full_gridded_forecast(time=args.args[0], output_file=args.args[1])
|
444
|
+
else:
|
445
|
+
print("Too many arguments")
|
446
|
+
print("\nUsage: windborne get_full_gridded_forecast time output_file")
|
447
|
+
|
406
448
|
elif args.command == 'grid_temp_2m':
|
407
449
|
# Parse grid_temp_2m arguments
|
408
450
|
if len(args.args) in [0,1]:
|
windborne/data_api.py
CHANGED
@@ -7,6 +7,7 @@ import json
|
|
7
7
|
from .api_request import make_api_request
|
8
8
|
from .observation_formatting import format_little_r, convert_to_netcdf
|
9
9
|
from .utils import to_unix_timestamp, save_arbitrary_response, print_table
|
10
|
+
from .track_formatting import save_track
|
10
11
|
|
11
12
|
DATA_API_BASE_URL = "https://sensor-data.windbornesystems.com/api/v1"
|
12
13
|
|
@@ -551,6 +552,9 @@ def get_flying_missions(output_file=None, print_results=False):
|
|
551
552
|
url = f"{DATA_API_BASE_URL}/missions.json"
|
552
553
|
flying_missions_response = make_api_request(url)
|
553
554
|
flying_missions = flying_missions_response.get("missions", [])
|
555
|
+
for mission in flying_missions:
|
556
|
+
if mission.get('number'):
|
557
|
+
mission['name'] = f"W-{mission['number']}"
|
554
558
|
|
555
559
|
# Display currently flying missions only if we are in cli and we don't save info in file
|
556
560
|
if print_results:
|
@@ -617,7 +621,49 @@ def get_mission_launch_site(mission_id=None, output_file=None, print_result=Fals
|
|
617
621
|
|
618
622
|
return response.get('launch_site')
|
619
623
|
|
620
|
-
|
624
|
+
|
625
|
+
def get_flying_mission(mission_id, verify_flying=True):
|
626
|
+
"""
|
627
|
+
Fetches a flying mission by ID.
|
628
|
+
If the mission is not flying, displays a list of currently flying missions.
|
629
|
+
|
630
|
+
Args:
|
631
|
+
mission_id (str): The ID of the mission to fetch.
|
632
|
+
verify_flying (bool): Whether to always check if the mission is flying.
|
633
|
+
|
634
|
+
Returns:
|
635
|
+
dict: The API response containing the mission data, or None if the mission is not flying.
|
636
|
+
"""
|
637
|
+
if not verify_flying and not mission_id.startswith('W-'):
|
638
|
+
return {
|
639
|
+
'id': mission_id,
|
640
|
+
}
|
641
|
+
|
642
|
+
# Check if provided mission ID belong to a flying mission
|
643
|
+
flying_missions = get_flying_missions()
|
644
|
+
|
645
|
+
mission = None
|
646
|
+
for candidate in flying_missions:
|
647
|
+
if candidate.get('id') == mission_id or candidate.get('name') == mission_id:
|
648
|
+
mission = candidate
|
649
|
+
break
|
650
|
+
|
651
|
+
if mission is None:
|
652
|
+
print(f"Provided mission ID '{mission_id}' does not belong to a mission that is currently flying.")
|
653
|
+
|
654
|
+
# Display currently flying missions
|
655
|
+
if flying_missions:
|
656
|
+
print("\nCurrently flying missions:\n")
|
657
|
+
|
658
|
+
print_table(flying_missions, keys=['id', 'name'], headers=['Mission ID', 'Mission Name'])
|
659
|
+
else:
|
660
|
+
print("No missions are currently flying.")
|
661
|
+
return None
|
662
|
+
|
663
|
+
return mission
|
664
|
+
|
665
|
+
|
666
|
+
def get_predicted_path(mission_id=None, output_file=None, print_result=False):
|
621
667
|
"""
|
622
668
|
Fetches the predicted flight path for a given mission.
|
623
669
|
Displays currently flying missions if the provided mission ID is invalid.
|
@@ -625,7 +671,7 @@ def get_predicted_path(mission_id=None, output_file=None):
|
|
625
671
|
Args:
|
626
672
|
mission_id (str): The ID of the mission to fetch the prediction for.
|
627
673
|
output_file (str): Optional path to save the response data.
|
628
|
-
|
674
|
+
print_result (bool): Whether to print the results in the CLI.
|
629
675
|
|
630
676
|
Returns:
|
631
677
|
list: The API response containing the predicted flight path data.
|
@@ -634,26 +680,88 @@ def get_predicted_path(mission_id=None, output_file=None):
|
|
634
680
|
print("To get the predicted flight path for a given mission you must provide a mission ID.")
|
635
681
|
return
|
636
682
|
|
637
|
-
|
638
|
-
flying_missions_response = get_flying_missions()
|
639
|
-
flying_missions = flying_missions_response.get("missions", [])
|
683
|
+
mission = get_flying_mission(mission_id)
|
640
684
|
|
641
|
-
|
642
|
-
|
685
|
+
url = f"{DATA_API_BASE_URL}/missions/{mission.get('id')}/prediction.json"
|
686
|
+
response = make_api_request(url)
|
643
687
|
|
644
|
-
|
645
|
-
|
646
|
-
print("\nCurrently flying missions:\n")
|
688
|
+
if response is None:
|
689
|
+
return
|
647
690
|
|
648
|
-
|
649
|
-
|
650
|
-
|
691
|
+
if output_file:
|
692
|
+
name = mission.get('name', mission_id)
|
693
|
+
save_track(output_file, {name: response['prediction']}, time_key='time')
|
694
|
+
|
695
|
+
if print_result:
|
696
|
+
print("Predicted flight path\n")
|
697
|
+
print_table(response['prediction'], keys=['time', 'latitude', 'longitude', 'altitude'], headers=['Time', 'Latitude', 'Longitude', 'Altitude'])
|
698
|
+
|
699
|
+
return response.get('prediction')
|
700
|
+
|
701
|
+
|
702
|
+
def get_current_location(mission_id=None, output_file=None, print_result=False, verify_flying=True):
|
703
|
+
"""
|
704
|
+
Fetches the current location for a given mission.
|
705
|
+
|
706
|
+
Args:
|
707
|
+
mission_id (str): The ID of the mission to fetch the current location for.
|
708
|
+
output_file (str): Optional path to save the response data.
|
709
|
+
print_result (bool): Whether to print the results in the CLI.
|
710
|
+
verify_flying (bool): Whether to verify the mission is flying before trying to fetch the current location
|
711
|
+
|
712
|
+
Returns:
|
713
|
+
dict: Current location with latitude, longitude, and altitude, or None if not found
|
714
|
+
"""
|
715
|
+
if not mission_id:
|
716
|
+
print("To get the current location for a given mission you must provide a mission ID.")
|
651
717
|
return
|
652
718
|
|
653
|
-
|
719
|
+
mission = get_flying_mission(mission_id, verify_flying=verify_flying)
|
720
|
+
|
721
|
+
url = f"{DATA_API_BASE_URL}/missions/{mission.get('id')}/current_location.json"
|
654
722
|
response = make_api_request(url)
|
655
723
|
|
724
|
+
if response is None:
|
725
|
+
return
|
726
|
+
|
656
727
|
if output_file:
|
657
|
-
save_arbitrary_response(output_file, response, csv_data_key=
|
658
|
-
|
659
|
-
|
728
|
+
save_arbitrary_response(output_file, response, csv_data_key=None)
|
729
|
+
|
730
|
+
if print_result:
|
731
|
+
print("Current location\n")
|
732
|
+
print_table([response], keys=['latitude', 'longitude', 'altitude'],
|
733
|
+
headers=['Latitude', 'Longitude', 'Altitude'])
|
734
|
+
|
735
|
+
return response
|
736
|
+
|
737
|
+
|
738
|
+
def get_flight_path(mission_id=None, output_file=None, print_result=False):
|
739
|
+
"""
|
740
|
+
Fetches the flight path for a given mission.
|
741
|
+
|
742
|
+
Args:
|
743
|
+
mission_id (str): The ID of the mission to fetch the flight path for.
|
744
|
+
output_file (str): Optional path to save the response data.
|
745
|
+
print_result (bool): Whether to print the results in the CLI.
|
746
|
+
|
747
|
+
Returns:
|
748
|
+
list: The API response containing the flight path.
|
749
|
+
"""
|
750
|
+
if not mission_id:
|
751
|
+
print("A mission id is required to get a flight path")
|
752
|
+
return
|
753
|
+
|
754
|
+
url = f"{DATA_API_BASE_URL}/missions/{mission_id}/flight_data.json"
|
755
|
+
response = make_api_request(url)
|
756
|
+
|
757
|
+
if response is None:
|
758
|
+
return
|
759
|
+
|
760
|
+
if output_file:
|
761
|
+
save_track(output_file, {mission_id: response['flight_data']}, time_key='transmit_time')
|
762
|
+
|
763
|
+
if print_result:
|
764
|
+
print("Flight path\n")
|
765
|
+
print_table(response['flight_data'], keys=['transmit_time', 'latitude', 'longitude', 'altitude'], headers=['Time', 'Latitude', 'Longitude', 'Altitude'])
|
766
|
+
|
767
|
+
return response.get('flight_data')
|
windborne/forecasts_api.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import requests
|
2
|
-
import json
|
3
2
|
|
4
3
|
from .utils import (
|
5
4
|
parse_time,
|
@@ -7,22 +6,14 @@ from .utils import (
|
|
7
6
|
print_table
|
8
7
|
)
|
9
8
|
|
10
|
-
from .api_request import
|
11
|
-
|
12
|
-
)
|
13
|
-
|
14
|
-
from .cyclone_formatting import (
|
15
|
-
save_track_as_geojson,
|
16
|
-
save_track_as_gpx,
|
17
|
-
save_track_as_kml,
|
18
|
-
save_track_as_little_r
|
19
|
-
)
|
9
|
+
from .api_request import make_api_request
|
10
|
+
from .track_formatting import save_track
|
20
11
|
|
21
12
|
FORECASTS_API_BASE_URL = "https://forecasts.windbornesystems.com/api/v1"
|
22
13
|
FORECASTS_GRIDDED_URL = f"{FORECASTS_API_BASE_URL}/gridded"
|
23
14
|
FORECASTS_HISTORICAL_URL = f"{FORECASTS_API_BASE_URL}/gridded/historical"
|
24
15
|
FORECASTS_TCS_URL = f"{FORECASTS_API_BASE_URL}/tropical_cyclones"
|
25
|
-
TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', 'little_r')
|
16
|
+
TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', '.little_r')
|
26
17
|
|
27
18
|
|
28
19
|
# Point forecasts
|
@@ -150,6 +141,9 @@ def get_gridded_forecast(time, variable, output_file=None):
|
|
150
141
|
|
151
142
|
return response
|
152
143
|
|
144
|
+
def get_full_gridded_forecast(time, output_file=None):
|
145
|
+
return get_gridded_forecast(time, "FULL", output_file)
|
146
|
+
|
153
147
|
def get_temperature_2m(time, output_file=None):
|
154
148
|
return get_gridded_forecast(time, "temperature_2m", output_file)
|
155
149
|
|
@@ -263,11 +257,7 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
263
257
|
response = make_api_request(FORECASTS_TCS_URL, params=params)
|
264
258
|
|
265
259
|
if output_file:
|
266
|
-
if
|
267
|
-
print("You have to provide a filetype for your output file.")
|
268
|
-
print_tc_supported_formats()
|
269
|
-
exit (4)
|
270
|
-
elif not output_file.lower().endswith(TCS_SUPPORTED_FORMATS):
|
260
|
+
if not output_file.lower().endswith(TCS_SUPPORTED_FORMATS):
|
271
261
|
print("Unsupported file format.")
|
272
262
|
print_tc_supported_formats()
|
273
263
|
exit(44)
|
@@ -276,37 +266,14 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
276
266
|
# make_api_request covers 403, 404, 502, HTTP, Connections Errors
|
277
267
|
# If we pass all of these and we get an empty dictionary ==> there are no active TCs
|
278
268
|
print("There are no active tropical cyclones for your request\n")
|
279
|
-
print("We didn't save any file on your machine.")
|
280
269
|
# It's pointless to save an empty file
|
281
270
|
# save_response_to_file() will throw error on saving {}
|
282
271
|
elif response is None:
|
283
272
|
print("-------------------------------------------------------")
|
284
273
|
print("You are too quick!\nThe tropical cyclone data for initialization time are not uploaded yet.")
|
285
|
-
print('You may check again in a few
|
286
|
-
|
287
|
-
|
288
|
-
flattened_data = []
|
289
|
-
for cyclone_id, tracks in response.items():
|
290
|
-
for track in tracks:
|
291
|
-
track_data = {
|
292
|
-
'cyclone_id': cyclone_id,
|
293
|
-
'latitude': track['latitude'],
|
294
|
-
'longitude': track['longitude'],
|
295
|
-
'time': track['time']
|
296
|
-
}
|
297
|
-
flattened_data.append(track_data)
|
298
|
-
save_arbitrary_response(output_file, {'prediction': flattened_data}, csv_data_key='prediction')
|
299
|
-
elif output_file.lower().endswith('.json'):
|
300
|
-
# Direct save for JSON
|
301
|
-
save_arbitrary_response(output_file, response)
|
302
|
-
elif output_file.lower().endswith('.geojson'):
|
303
|
-
save_track_as_geojson(output_file, response)
|
304
|
-
elif output_file.lower().endswith('.gpx'):
|
305
|
-
save_track_as_gpx(output_file, response)
|
306
|
-
elif output_file.lower().endswith('.kml'):
|
307
|
-
save_track_as_kml(output_file, response)
|
308
|
-
elif output_file.lower().endswith('.little_r'):
|
309
|
-
save_track_as_little_r(output_file, response)
|
274
|
+
print('You may check again in a few hour.')
|
275
|
+
else:
|
276
|
+
save_track(output_file, response, require_ids=True)
|
310
277
|
|
311
278
|
if print_response:
|
312
279
|
if len(response) == 0:
|
@@ -1,15 +1,69 @@
|
|
1
|
-
from datetime import datetime
|
1
|
+
from datetime import datetime
|
2
|
+
import json
|
2
3
|
|
4
|
+
TRACK_SUPPORTED_FORMATS = ['.csv', '.json', '.geojson', '.gpx', '.kml', 'little_r']
|
3
5
|
|
4
|
-
def
|
6
|
+
def save_track(output_file, track_data, time_key='time', require_ids=False):
|
5
7
|
"""
|
6
|
-
|
8
|
+
Save track data to a file in the specified format.
|
9
|
+
Expects track_data to be a dictionary with cyclone/mission IDs as keys and lists of track points as values.
|
7
10
|
"""
|
11
|
+
include_id = require_ids or len(track_data) > 1
|
12
|
+
|
13
|
+
if output_file.lower().endswith('.json'):
|
14
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
15
|
+
json.dump(track_data, f, indent=4)
|
16
|
+
elif output_file.lower().endswith('.csv'):
|
17
|
+
save_track_as_csv(output_file, track_data, time_key=time_key, include_id=include_id)
|
18
|
+
elif output_file.lower().endswith('.geojson'):
|
19
|
+
save_track_as_geojson(output_file, track_data, time_key=time_key)
|
20
|
+
elif output_file.lower().endswith('.gpx'):
|
21
|
+
save_track_as_gpx(output_file, track_data, time_key=time_key)
|
22
|
+
elif output_file.lower().endswith('.kml'):
|
23
|
+
save_track_as_kml(output_file, track_data)
|
24
|
+
elif output_file.lower().endswith('.little_r'):
|
25
|
+
save_track_as_little_r(output_file, track_data, time_key=time_key)
|
26
|
+
else:
|
27
|
+
print(f"Unsupported file format. Supported formats are: {', '.join(TRACK_SUPPORTED_FORMATS)}")
|
28
|
+
return
|
29
|
+
|
30
|
+
|
31
|
+
def save_track_as_csv(filename, track_data, time_key='time', include_id=False):
|
32
|
+
"""
|
33
|
+
Convert and save track data as CSV.
|
34
|
+
"""
|
35
|
+
flattened_data = []
|
36
|
+
for name, tracks in track_data.items():
|
37
|
+
for track in tracks:
|
38
|
+
track_data = {
|
39
|
+
'id': name,
|
40
|
+
'latitude': track['latitude'],
|
41
|
+
'longitude': track['longitude'],
|
42
|
+
'time': track[time_key]
|
43
|
+
}
|
44
|
+
flattened_data.append(track_data)
|
45
|
+
|
8
46
|
with open(filename, 'w', encoding='utf-8') as f:
|
9
|
-
|
47
|
+
if include_id:
|
48
|
+
f.write('id,latitude,longitude,time\n')
|
49
|
+
else:
|
50
|
+
f.write('latitude,longitude,time\n')
|
51
|
+
|
52
|
+
for row in flattened_data:
|
53
|
+
if include_id:
|
54
|
+
f.write(f"{row['id']},{row['latitude']},{row['longitude']},{row['time']}\n")
|
55
|
+
else:
|
56
|
+
f.write(f"{row['latitude']},{row['longitude']},{row['time']}\n")
|
57
|
+
|
58
|
+
def save_track_as_little_r(filename, track_data, time_key='time'):
|
59
|
+
"""
|
60
|
+
Convert and save track data in little_R format.
|
61
|
+
"""
|
62
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
63
|
+
for cyclone_id, tracks in track_data.items():
|
10
64
|
for track in tracks:
|
11
65
|
# Parse the time
|
12
|
-
dt = datetime.fromisoformat(track[
|
66
|
+
dt = datetime.fromisoformat(track[time_key].replace('Z', '+00:00'))
|
13
67
|
|
14
68
|
# Header line 1
|
15
69
|
header1 = f"{float(track['latitude']):20.5f}{float(track['longitude']):20.5f}{'HMS':40}"
|
@@ -35,21 +89,20 @@ def save_track_as_little_r(filename, cyclone_data):
|
|
35
89
|
print("Saved to", filename)
|
36
90
|
|
37
91
|
|
38
|
-
def save_track_as_kml(filename,
|
92
|
+
def save_track_as_kml(filename, track_data):
|
39
93
|
"""
|
40
|
-
Convert and save
|
94
|
+
Convert and save track data as KML, handling meridian crossing.
|
41
95
|
"""
|
42
96
|
kml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
43
97
|
kml += '<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document>\n'
|
44
98
|
|
45
|
-
for
|
46
|
-
kml += f' <Placemark>\n <name>{
|
99
|
+
for id, tracks in track_data.items():
|
100
|
+
kml += f' <Placemark>\n <name>{id}</name>\n <MultiGeometry>\n'
|
47
101
|
|
48
102
|
current_segment = []
|
49
103
|
|
50
104
|
for i in range(len(tracks)):
|
51
105
|
lon = float(tracks[i]['longitude'])
|
52
|
-
lat = float(tracks[i]['latitude'])
|
53
106
|
|
54
107
|
if not current_segment:
|
55
108
|
current_segment.append(tracks[i])
|
@@ -61,7 +114,7 @@ def save_track_as_kml(filename, cyclone_data):
|
|
61
114
|
if abs(lon - prev_lon) > 180:
|
62
115
|
# Write the current segment
|
63
116
|
kml += ' <LineString>\n <coordinates>\n'
|
64
|
-
coordinates = [f' {track["longitude"]},{track["latitude"]},{0}'
|
117
|
+
coordinates = [f' {track["longitude"]},{track["latitude"]},{track.get("altitude", 0)}'
|
65
118
|
for track in current_segment]
|
66
119
|
kml += '\n'.join(coordinates)
|
67
120
|
kml += '\n </coordinates>\n </LineString>\n'
|
@@ -74,7 +127,7 @@ def save_track_as_kml(filename, cyclone_data):
|
|
74
127
|
# Write the last segment if it's not empty
|
75
128
|
if current_segment:
|
76
129
|
kml += ' <LineString>\n <coordinates>\n'
|
77
|
-
coordinates = [f' {track["longitude"]},{track["latitude"]},{0}'
|
130
|
+
coordinates = [f' {track["longitude"]},{track["latitude"]},{track.get("altitude", 0)}'
|
78
131
|
for track in current_segment]
|
79
132
|
kml += '\n'.join(coordinates)
|
80
133
|
kml += '\n </coordinates>\n </LineString>\n'
|
@@ -88,12 +141,12 @@ def save_track_as_kml(filename, cyclone_data):
|
|
88
141
|
print(f"Saved to {filename}")
|
89
142
|
|
90
143
|
|
91
|
-
def save_track_as_gpx(filename,
|
92
|
-
"""Convert and save
|
144
|
+
def save_track_as_gpx(filename, track_data, time_key='time'):
|
145
|
+
"""Convert and save track data as GPX, handling meridian crossing."""
|
93
146
|
gpx = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
94
147
|
gpx += '<gpx version="1.1" creator="Windborne" xmlns="http://www.topografix.com/GPX/1/1">\n'
|
95
148
|
|
96
|
-
for cyclone_id, tracks in
|
149
|
+
for cyclone_id, tracks in track_data.items():
|
97
150
|
gpx += f' <trk>\n <name>{cyclone_id}</name>\n'
|
98
151
|
|
99
152
|
current_segment = []
|
@@ -101,7 +154,6 @@ def save_track_as_gpx(filename, cyclone_data):
|
|
101
154
|
|
102
155
|
for i in range(len(tracks)):
|
103
156
|
lon = float(tracks[i]['longitude'])
|
104
|
-
lat = float(tracks[i]['latitude'])
|
105
157
|
|
106
158
|
if not current_segment:
|
107
159
|
current_segment.append(tracks[i])
|
@@ -130,7 +182,7 @@ def save_track_as_gpx(filename, cyclone_data):
|
|
130
182
|
gpx += ' <trkseg>\n'
|
131
183
|
for point in current_segment:
|
132
184
|
gpx += f' <trkpt lat="{point["latitude"]}" lon="{point["longitude"]}">\n'
|
133
|
-
gpx += f' <time>{point[
|
185
|
+
gpx += f' <time>{point[time_key]}</time>\n'
|
134
186
|
gpx += ' </trkpt>\n'
|
135
187
|
gpx += ' </trkseg>\n'
|
136
188
|
|
@@ -143,10 +195,10 @@ def save_track_as_gpx(filename, cyclone_data):
|
|
143
195
|
print(f"Saved to {filename}")
|
144
196
|
|
145
197
|
|
146
|
-
def save_track_as_geojson(filename,
|
147
|
-
"""Convert and save
|
198
|
+
def save_track_as_geojson(filename, track_data, time_key='time'):
|
199
|
+
"""Convert and save track data as GeoJSON, handling meridian crossing."""
|
148
200
|
features = []
|
149
|
-
for
|
201
|
+
for id, tracks in track_data.items():
|
150
202
|
# Initialize lists to store line segments
|
151
203
|
line_segments = []
|
152
204
|
current_segment = []
|
@@ -188,9 +240,9 @@ def save_track_as_geojson(filename, cyclone_data):
|
|
188
240
|
feature = {
|
189
241
|
"type": "Feature",
|
190
242
|
"properties": {
|
191
|
-
"
|
192
|
-
"start_time": tracks[0][
|
193
|
-
"end_time": tracks[-1][
|
243
|
+
"id": id,
|
244
|
+
"start_time": tracks[0][time_key],
|
245
|
+
"end_time": tracks[-1][time_key]
|
194
246
|
},
|
195
247
|
"geometry": {
|
196
248
|
"type": "MultiLineString",
|
@@ -0,0 +1,13 @@
|
|
1
|
+
windborne/__init__.py,sha256=jPqxHTqOEcAnPU-7VCJionGToE2tqY7Q3SuSdQrdl9Q,1944
|
2
|
+
windborne/api_request.py,sha256=zh1TaaZAaRfAXp2NYMja75fKeduWLfao02JRRFVpQCA,11108
|
3
|
+
windborne/cli.py,sha256=fJ4cLcBpc7YasfMu3pNrqCX9idGCWA-2j2necNpDfck,36696
|
4
|
+
windborne/data_api.py,sha256=f2TdMpSxQLiWKcUJxbG_9SqAp4DU-sZ3rBvqTcBHbuk,33343
|
5
|
+
windborne/forecasts_api.py,sha256=Jea1hGd2CQHbyRXnJXLamF1GdgBsoqX7oGyg8S6YoRo,14102
|
6
|
+
windborne/observation_formatting.py,sha256=c739aaun6aaYhXl5VI-SRGR-TDS355_0Bfu1t6McoiM,14993
|
7
|
+
windborne/track_formatting.py,sha256=LaLfTyjpWoOtHmReJPLViY0MKm_iPL_5I2OB_lNvGGA,10054
|
8
|
+
windborne/utils.py,sha256=H8gvZ4Lrr0UmLl25iMZs6NsZliCY_73Ved_rBIqxJg4,7240
|
9
|
+
windborne-1.1.4.dist-info/METADATA,sha256=PqFm1t3RoKqj0K1UtUtJYMfIwl1gauKIAinS1Elyir4,1235
|
10
|
+
windborne-1.1.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
11
|
+
windborne-1.1.4.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
|
12
|
+
windborne-1.1.4.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
|
13
|
+
windborne-1.1.4.dist-info/RECORD,,
|
windborne-1.1.2.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
windborne/__init__.py,sha256=0bPtPzBG3djZMVfyUNhapiEqCSyN8SSDsm_eCZ4kwhc,1783
|
2
|
-
windborne/api_request.py,sha256=zh1TaaZAaRfAXp2NYMja75fKeduWLfao02JRRFVpQCA,11108
|
3
|
-
windborne/cli.py,sha256=XvI9a9mO921a_LpI1ojN9Rn_x3sYq3XTkmUUT37wg4g,34835
|
4
|
-
windborne/cyclone_formatting.py,sha256=0S8S_PflRGm6ftUlyR2aGGyX6IRn0hbUTEWptu7f8Q8,7886
|
5
|
-
windborne/data_api.py,sha256=qVA0UGcnkBGYe8TFgqgFlFjZYCsS4ly1IL97tSpgKgI,29751
|
6
|
-
windborne/forecasts_api.py,sha256=QzdayDr3MaswsFibOdBoo6SCdRY86eE2E7kWqn-Jc1Y,15479
|
7
|
-
windborne/observation_formatting.py,sha256=c739aaun6aaYhXl5VI-SRGR-TDS355_0Bfu1t6McoiM,14993
|
8
|
-
windborne/utils.py,sha256=H8gvZ4Lrr0UmLl25iMZs6NsZliCY_73Ved_rBIqxJg4,7240
|
9
|
-
windborne-1.1.2.dist-info/METADATA,sha256=NR1lkRL5m-j3UoWVj_2ti14bSrIXZLtCbwS4DfUvko0,1235
|
10
|
-
windborne-1.1.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
11
|
-
windborne-1.1.2.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
|
12
|
-
windborne-1.1.2.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
|
13
|
-
windborne-1.1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|