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 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
- def get_predicted_path(mission_id=None, output_file=None):
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
- If provided, saves the data in CSV format.
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
- # Check if provided mission ID belong to a flying mission
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
- if mission_id not in [mission.get("id") for mission in flying_missions]:
642
- print(f"Provided mission ID '{mission_id}' does not belong to a mission that is currently flying.")
685
+ url = f"{DATA_API_BASE_URL}/missions/{mission.get('id')}/prediction.json"
686
+ response = make_api_request(url)
643
687
 
644
- # Display currently flying missions
645
- if flying_missions:
646
- print("\nCurrently flying missions:\n")
688
+ if response is None:
689
+ return
647
690
 
648
- print_table(flying_missions, keys=['i', 'id', 'name'], headers=['Index', 'Mission ID', 'Mission Name'])
649
- else:
650
- print("No missions are currently flying.")
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
- url = f"{DATA_API_BASE_URL}/missions/{mission_id}/prediction.json"
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='prediction')
658
-
659
- return response.get('prediction')
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')
@@ -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
- make_api_request
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 '.' not in output_file:
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 hours again.')
286
- elif output_file.lower().endswith('.csv'):
287
- # Flatten for CSV
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, timezone
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 save_track_as_little_r(filename, cyclone_data):
6
+ def save_track(output_file, track_data, time_key='time', require_ids=False):
5
7
  """
6
- Convert and save cyclone data in little_R format.
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
- for cyclone_id, tracks in cyclone_data.items():
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['time'].replace('Z', '+00:00'))
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, cyclone_data):
92
+ def save_track_as_kml(filename, track_data):
39
93
  """
40
- Convert and save cyclone data as KML, handling meridian crossing.
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 cyclone_id, tracks in cyclone_data.items():
46
- kml += f' <Placemark>\n <name>{cyclone_id}</name>\n <MultiGeometry>\n'
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, cyclone_data):
92
- """Convert and save cyclone data as GPX, handling meridian crossing."""
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 cyclone_data.items():
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["time"]}</time>\n'
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, cyclone_data):
147
- """Convert and save cyclone data as GeoJSON, handling meridian crossing."""
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 cyclone_id, tracks in cyclone_data.items():
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
- "cyclone_id": cyclone_id,
192
- "start_time": tracks[0]['time'],
193
- "end_time": tracks[-1]['time']
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: windborne
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: A Python library for interacting with WindBorne Data and Forecasts API
5
5
  Author-email: WindBorne Systems <data@windbornesystems.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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,,
@@ -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,,