windborne 1.0.8__py3-none-any.whl → 1.1.0__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/cli.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import argparse
2
+ import json
2
3
 
3
4
  from . import (
4
- super_observations,
5
- observations,
5
+ get_super_observations,
6
+ get_observations,
6
7
 
7
8
  get_observations_page,
8
9
  get_super_observations_page,
@@ -17,7 +18,7 @@ from . import (
17
18
  get_point_forecasts,
18
19
  get_initialization_times,
19
20
  get_temperature_2m,
20
- get_dewpoint_2m,
21
+ # get_dewpoint_2m,
21
22
  get_wind_u_10m, get_wind_v_10m,
22
23
  get_500hpa_wind_u, get_500hpa_wind_v,
23
24
  get_500hpa_temperature, get_850hpa_temperature,
@@ -44,9 +45,9 @@ def main():
44
45
  super_obs_parser = subparsers.add_parser('super-observations', help='Poll super observations within a time range')
45
46
  super_obs_parser.add_argument('start_time', help='Starting time (YYYY-MM-DD_HH:MM, "YYYY-MM-DD HH:MM:SS" or YYYY-MM-DDTHH:MM:SS.fffZ)')
46
47
  super_obs_parser.add_argument('end_time', help='End time (YYYY-MM-DD_HH:MM, "YYYY-MM-DD HH:MM:SS" or YYYY-MM-DDTHH:MM:SS.fffZ)', nargs='?', default=None)
47
- super_obs_parser.add_argument('-i', '--interval', type=int, default=60, help='Polling interval in seconds')
48
48
  super_obs_parser.add_argument('-b', '--bucket-hours', type=float, default=6.0, help='Hours per bucket')
49
49
  super_obs_parser.add_argument('-d', '--output-dir', help='Directory path where the separate files should be saved. If not provided, files will be saved in current directory.')
50
+ super_obs_parser.add_argument('-m', '--mission-id', help='Filter by mission ID')
50
51
  super_obs_parser.add_argument('output', help='Save output to a single file (filename.csv, filename.json or filename.little_r) or to or to multiple files (csv, json, netcdf or little_r)')
51
52
 
52
53
  # Observations Command
@@ -58,9 +59,7 @@ def main():
58
59
  obs_parser.add_argument('-xl', '--max-latitude', type=float, help='Maximum latitude filter')
59
60
  obs_parser.add_argument('-mg', '--min-longitude', type=float, help='Minimum longitude filter')
60
61
  obs_parser.add_argument('-xg', '--max-longitude', type=float, help='Maximum longitude filter')
61
- obs_parser.add_argument('-id', '--include-ids', action='store_true', help='Include observation IDs')
62
62
  obs_parser.add_argument('-u', '--include-updated-at', action='store_true', help='Include update timestamps')
63
- obs_parser.add_argument('-i', '--interval', type=int, default=60, help='Polling interval in seconds')
64
63
  obs_parser.add_argument('-b', '--bucket-hours', type=float, default=6.0, help='Hours per bucket')
65
64
  obs_parser.add_argument('-d', '--output-dir', help='Directory path where the separate files should be saved. If not provided, files will be saved in current directory.')
66
65
  obs_parser.add_argument('output', help='Save output to a single file (filename.csv, filename.json or filename.little_r) or to multiple files (csv, json, netcdf or little_r)')
@@ -87,7 +86,6 @@ def main():
87
86
  super_obs_page_parser.add_argument('-mt', '--min-time', help='Minimum time filter (YYYY-MM-DD_HH:MM, "YYYY-MM-DD HH:MM:SS" or YYYY-MM-DDTHH:MM:SS.fffZ)')
88
87
  super_obs_page_parser.add_argument('-xt', '--max-time', help='Maximum time filter (YYYY-MM-DD_HH:MM, "YYYY-MM-DD HH:MM:SS" or YYYY-MM-DDTHH:MM:SS.fffZ)')
89
88
  super_obs_page_parser.add_argument('-m', '--mission-id', help='Filter by mission ID')
90
- super_obs_page_parser.add_argument('-id', '--include-ids', action='store_true', help='Include observation IDs')
91
89
  super_obs_page_parser.add_argument('-mn', '--include-mission-name', action='store_true', help='Include mission names')
92
90
  super_obs_page_parser.add_argument('-u', '--include-updated-at', action='store_true', help='Include update timestamps')
93
91
  super_obs_page_parser.add_argument('output', nargs='?', help='Output file')
@@ -95,9 +93,9 @@ def main():
95
93
  # Poll Super Observations Command
96
94
  poll_super_obs_parser = subparsers.add_parser('poll-super-observations', help='Continuously polls for super observations and saves to files in specified format.')
97
95
  poll_super_obs_parser.add_argument('start_time', help='Starting time (YYYY-MM-DD_HH:MM, "YYYY-MM-DD HH:MM:SS" or YYYY-MM-DDTHH:MM:SS.fffZ)')
98
- poll_super_obs_parser.add_argument('-i', '--interval', type=int, default=60, help='Polling interval in seconds')
99
96
  poll_super_obs_parser.add_argument('-b', '--bucket-hours', type=float, default=6.0, help='Hours per bucket')
100
97
  poll_super_obs_parser.add_argument('-d', '--output-dir', help='Directory path where the separate files should be saved. If not provided, files will be saved in current directory.')
98
+ poll_super_obs_parser.add_argument('-m', '--mission-id', help='Filter observations by mission ID')
101
99
  poll_super_obs_parser.add_argument('output', help='Save output to multiple files (csv, json, netcdf or little_r)')
102
100
 
103
101
  # Poll Observations Command
@@ -108,9 +106,7 @@ def main():
108
106
  poll_obs_parser.add_argument('-xl', '--max-latitude', type=float, help='Maximum latitude filter')
109
107
  poll_obs_parser.add_argument('-mg', '--min-longitude', type=float, help='Minimum longitude filter')
110
108
  poll_obs_parser.add_argument('-xg', '--max-longitude', type=float, help='Maximum longitude filter')
111
- poll_obs_parser.add_argument('-id', '--include-ids', action='store_true', help='Include observation IDs')
112
109
  poll_obs_parser.add_argument('-u', '--include-updated-at', action='store_true', help='Include update timestamps')
113
- poll_obs_parser.add_argument('-i', '--interval', type=int, default=60, help='Polling interval in seconds')
114
110
  poll_obs_parser.add_argument('-b', '--bucket-hours', type=float, default=6.0, help='Hours per bucket')
115
111
  poll_obs_parser.add_argument('-d', '--output-dir', help='Directory path where the separate files should be saved. If not provided, files will be saved in current directory.')
116
112
  poll_obs_parser.add_argument('output', help='Save output to multiple files (csv, json, netcdf or little_r)')
@@ -235,20 +231,20 @@ def main():
235
231
 
236
232
  # In case user wants to save all poll observation data in a single file | filename.format
237
233
  if '.' in args.output:
238
- save_to_file = args.output
234
+ output_file = args.output
239
235
  output_format = None
240
236
  output_dir = None
241
237
  # In case user wants separate file for each data from missions (buckets)
242
238
  else:
243
- save_to_file = None
239
+ output_file = None
244
240
  output_format = args.output
245
241
  output_dir = args.output_dir
246
242
 
247
- super_observations(
243
+ get_super_observations(
248
244
  start_time=args.start_time,
249
245
  end_time=args.end_time,
250
- interval=args.interval,
251
- save_to_file=save_to_file,
246
+ output_file=output_file,
247
+ mission_id=args.mission_id,
252
248
  bucket_hours=args.bucket_hours,
253
249
  output_dir=output_dir,
254
250
  output_format=output_format
@@ -260,7 +256,7 @@ def main():
260
256
 
261
257
  poll_super_observations(
262
258
  start_time=args.start_time,
263
- interval=args.interval,
259
+ mission_id=args.mission_id,
264
260
  bucket_hours=args.bucket_hours,
265
261
  output_dir=output_dir,
266
262
  output_format=output_format
@@ -272,14 +268,12 @@ def main():
272
268
 
273
269
  poll_observations(
274
270
  start_time=args.start_time,
275
- include_ids=args.include_ids,
276
271
  include_updated_at=args.include_updated_at,
277
272
  mission_id=args.mission_id,
278
273
  min_latitude=args.min_latitude,
279
274
  max_latitude=args.max_latitude,
280
275
  min_longitude=args.min_longitude,
281
276
  max_longitude=args.max_longitude,
282
- interval=args.interval,
283
277
  bucket_hours=args.bucket_hours,
284
278
  output_dir=output_dir,
285
279
  output_format=output_format
@@ -292,27 +286,25 @@ def main():
292
286
 
293
287
  # In case user wants to save all poll observation data in a single file | filename.format
294
288
  if '.' in args.output:
295
- save_to_file = args.output
289
+ output_file = args.output
296
290
  output_format = None
297
291
  output_dir = None
298
292
  # In case user wants separate file for each data from missions (buckets)
299
293
  else:
300
- save_to_file = None
294
+ output_file = None
301
295
  output_format = args.output
302
296
  output_dir = args.output_dir
303
297
 
304
- observations(
298
+ get_observations(
305
299
  start_time=args.start_time,
306
300
  end_time=args.end_time,
307
- include_ids=args.include_ids,
308
301
  include_updated_at=args.include_updated_at,
309
302
  mission_id=args.mission_id,
310
303
  min_latitude=args.min_latitude,
311
304
  max_latitude=args.max_latitude,
312
305
  min_longitude=args.min_longitude,
313
306
  max_longitude=args.max_longitude,
314
- interval=args.interval,
315
- save_to_file=save_to_file,
307
+ output_file=output_file,
316
308
  bucket_hours=args.bucket_hours,
317
309
  output_dir=output_dir,
318
310
  output_format=output_format
@@ -320,7 +312,7 @@ def main():
320
312
 
321
313
  elif args.command == 'observations-page':
322
314
  if not args.output:
323
- pprint(get_observations_page(
315
+ print(json.dumps(get_observations_page(
324
316
  since=args.since,
325
317
  min_time=args.min_time,
326
318
  max_time=args.max_time,
@@ -332,7 +324,7 @@ def main():
332
324
  max_latitude=args.max_latitude,
333
325
  min_longitude=args.min_longitude,
334
326
  max_longitude=args.max_longitude
335
- ))
327
+ ), indent=4))
336
328
  else:
337
329
  get_observations_page(
338
330
  since=args.since,
@@ -346,12 +338,12 @@ def main():
346
338
  max_latitude=args.max_latitude,
347
339
  min_longitude=args.min_longitude,
348
340
  max_longitude=args.max_longitude,
349
- save_to_file=args.output
341
+ output_file=args.output
350
342
  )
351
343
 
352
344
  elif args.command == 'super-observations-page':
353
345
  if not args.output:
354
- pprint(get_super_observations_page(
346
+ print(json.dumps(get_super_observations_page(
355
347
  since=args.since,
356
348
  min_time=args.min_time,
357
349
  max_time=args.max_time,
@@ -359,7 +351,7 @@ def main():
359
351
  include_mission_name=args.include_mission_name,
360
352
  include_updated_at=args.include_updated_at,
361
353
  mission_id=args.mission_id
362
- ))
354
+ ), indent=4))
363
355
  else:
364
356
  get_super_observations_page(
365
357
  since=args.since,
@@ -369,22 +361,22 @@ def main():
369
361
  include_mission_name=args.include_mission_name,
370
362
  include_updated_at=args.include_updated_at,
371
363
  mission_id=args.mission_id,
372
- save_to_file=args.output
364
+ output_file=args.output
373
365
  )
374
366
 
375
367
  elif args.command == 'flying-missions':
376
- get_flying_missions(cli=True, save_to_file=args.output)
368
+ get_flying_missions(from_cli=True, output_file=args.output)
377
369
 
378
370
  elif args.command == 'launch-site':
379
371
  get_mission_launch_site(
380
372
  mission_id=args.mission_id,
381
- save_to_file=args.output
373
+ output_file=args.output
382
374
  )
383
375
 
384
376
  elif args.command == 'predict-path':
385
377
  get_predicted_path(
386
378
  mission_id=args.mission_id,
387
- save_to_file=args.output
379
+ output_file=args.output
388
380
  )
389
381
  ####################################################################################################################
390
382
  # FORECASTS API FUNCTIONS CALLED
@@ -403,7 +395,7 @@ def main():
403
395
  min_forecast_hour=min_forecast_hour,
404
396
  max_forecast_hour=max_forecast_hour,
405
397
  initialization_time=initialization_time,
406
- save_to_file=args.output_file
398
+ output_file=args.output_file
407
399
  )
408
400
 
409
401
  elif args.command == 'init_times':
@@ -419,21 +411,21 @@ def main():
419
411
  print("To get the gridded output of global 2m temperature forecast you need to provide the time for which to get the forecast and an output file.")
420
412
  print("\nUsage: windborne grid_temp_2m time output_file")
421
413
  elif len(args.args) == 2:
422
- get_temperature_2m(time=args.args[0], save_to_file=args.args[1])
414
+ get_temperature_2m(time=args.args[0], output_file=args.args[1])
423
415
  else:
424
416
  print("Too many arguments")
425
417
  print("\nUsage: windborne grid_temp_2m time output_file")
426
418
 
427
- elif args.command == 'grid_dewpoint_2m':
428
- # Parse grid_dewpoint_2m arguments
429
- if len(args.args) in [0,1]:
430
- print(f"To get the gridded output of global 2m dew point forecast you need to provide the time for which to get the forecast and an output file.")
431
- print("\nUsage: windborne grid_dewpoint_2m time output_file")
432
- elif len(args.args) == 2:
433
- get_dewpoint_2m(time=args.args[0], save_to_file=args.args[1])
434
- else:
435
- print("Too many arguments")
436
- print("\nUsage: windborne grid_dewpoint_2m time output_file")
419
+ # elif args.command == 'grid_dewpoint_2m':
420
+ # # Parse grid_dewpoint_2m arguments
421
+ # if len(args.args) in [0,1]:
422
+ # print(f"To get the gridded output of global 2m dew point forecast you need to provide the time for which to get the forecast and an output file.")
423
+ # print("\nUsage: windborne grid_dewpoint_2m time output_file")
424
+ # elif len(args.args) == 2:
425
+ # get_dewpoint_2m(time=args.args[0], output_file=args.args[1])
426
+ # else:
427
+ # print("Too many arguments")
428
+ # print("\nUsage: windborne grid_dewpoint_2m time output_file")
437
429
 
438
430
  elif args.command == 'grid_wind_u_10m':
439
431
  # Parse grid_wind_u_10m arguments
@@ -441,7 +433,7 @@ def main():
441
433
  print(f"To get the gridded output of global 10m u-component of wind forecasts you need to provide the time for which to get the forecast and an output file.")
442
434
  print("\nUsage: windborne grid_wind_u_10m time output_file")
443
435
  elif len(args.args) == 2:
444
- get_wind_u_10m(time=args.args[0], save_to_file=args.args[1])
436
+ get_wind_u_10m(time=args.args[0], output_file=args.args[1])
445
437
  else:
446
438
  print("Too many arguments")
447
439
  print("\nUsage: windborne grid_wind_u_10m time output_file")
@@ -452,7 +444,7 @@ def main():
452
444
  print(f"To get the gridded output of global 10m v-component of wind forecasts you need to provide the time for which to get the forecast and an output file.")
453
445
  print("\nUsage: windborne grid_wind_v_10m time output_file")
454
446
  elif len(args.args) == 2:
455
- get_wind_v_10m(time=args.args[0], save_to_file=args.args[1])
447
+ get_wind_v_10m(time=args.args[0], output_file=args.args[1])
456
448
  else:
457
449
  print("Too many arguments")
458
450
  print("\nUsage: windborne grid_wind_v_10m time output_file")
@@ -463,7 +455,7 @@ def main():
463
455
  print(f"To get the gridded output of global 500hPa u-component of wind forecasts you need to provide the time for which to get the forecast and an output file.")
464
456
  print("\nUsage: windborne grid_500hpa_wind_u time output_file")
465
457
  elif len(args.args) == 2:
466
- get_500hpa_wind_u(time=args.args[0], save_to_file=args.args[1])
458
+ get_500hpa_wind_u(time=args.args[0], output_file=args.args[1])
467
459
  else:
468
460
  print("Too many arguments")
469
461
  print("\nUsage: windborne grid_500hpa_wind_u time output_file")
@@ -474,7 +466,7 @@ def main():
474
466
  print(f"To get the gridded output of global 500hPa v-component of wind forecasts you need to provide the time for which to get the forecast and an output file.")
475
467
  print("\nUsage: windborne grid_500hpa_wind_v time output_file")
476
468
  elif len(args.args) == 2:
477
- get_500hpa_wind_v(time=args.args[0], save_to_file=args.args[1])
469
+ get_500hpa_wind_v(time=args.args[0], output_file=args.args[1])
478
470
  else:
479
471
  print("Too many arguments")
480
472
  print("\nUsage: windborne grid_500hpa_wind_v time output_file")
@@ -486,7 +478,7 @@ def main():
486
478
  print("\nUsage: windborne grid_500hpa_temperature time output_file")
487
479
  return
488
480
  elif len(args.args) == 2:
489
- get_500hpa_temperature(time=args.args[0], save_to_file=args.args[1])
481
+ get_500hpa_temperature(time=args.args[0], output_file=args.args[1])
490
482
  else:
491
483
  print("Too many arguments")
492
484
  print("\nUsage: windborne grid_500hpa_temperature time output_file")
@@ -498,7 +490,7 @@ def main():
498
490
  print("\nUsage: windborne grid_850hpa_temperature time output_file")
499
491
  return
500
492
  elif len(args.args) == 2:
501
- get_850hpa_temperature(time=args.args[0], save_to_file=args.args[1])
493
+ get_850hpa_temperature(time=args.args[0], output_file=args.args[1])
502
494
  else:
503
495
  print("Too many arguments")
504
496
  print("\nUsage: windborne grid_850hpa_temperature time output_file")
@@ -509,7 +501,7 @@ def main():
509
501
  print(f"To get the gridded output of global mean sea level pressure forecasts you need to provide the time for which to get the forecast and an output file.")
510
502
  print("\nUsage: windborne grid_pressure_msl time output_file")
511
503
  elif len(args.args) == 2:
512
- get_pressure_msl(time=args.args[0], save_to_file=args.args[1])
504
+ get_pressure_msl(time=args.args[0], output_file=args.args[1])
513
505
  else:
514
506
  print("Too many arguments")
515
507
  print("\nUsage: windborne grid_pressure_msl time output_file")
@@ -521,7 +513,7 @@ def main():
521
513
  print("\nUsage: windborne grid_500hpa_geopotential time output_file")
522
514
  return
523
515
  elif len(args.args) == 2:
524
- get_500hpa_geopotential(time=args.args[0], save_to_file=args.args[1])
516
+ get_500hpa_geopotential(time=args.args[0], output_file=args.args[1])
525
517
  else:
526
518
  print("Too many arguments")
527
519
  print("\nUsage: windborne grid_500hpa_geopotential time output_file")
@@ -533,7 +525,7 @@ def main():
533
525
  print("\nUsage: windborne grid_850hpa_geopotential time output_file")
534
526
  return
535
527
  elif len(args.args) == 2:
536
- get_850hpa_geopotential(time=args.args[0], save_to_file=args.args[1])
528
+ get_850hpa_geopotential(time=args.args[0], output_file=args.args[1])
537
529
  else:
538
530
  print("Too many arguments")
539
531
  print("\nUsage: windborne grid_850hpa_geopotential time output_file")
@@ -550,7 +542,7 @@ def main():
550
542
  print("\nUsage: windborne hist_temp_2m initialization_time forecast_hour output_file")
551
543
  return
552
544
  elif len(args.args) == 3:
553
- get_historical_temperature_2m(initialization_time=args.args[0], forecast_hour=args.args[1], save_to_file=args.args[2])
545
+ get_historical_temperature_2m(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
554
546
  else:
555
547
  print("Too many arguments")
556
548
  print("\nUsage: windborne hist_temp_2m initialization_time forecast_hour output_file")
@@ -565,7 +557,7 @@ def main():
565
557
  print("\nUsage: windborne hist_500hpa_geopotential initialization_time forecast_hour output_file")
566
558
  return
567
559
  elif len(args.args) == 3:
568
- get_historical_500hpa_geopotential(initialization_time=args.args[0], forecast_hour=args.args[1], save_to_file=args.args[2])
560
+ get_historical_500hpa_geopotential(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
569
561
  else:
570
562
  print("Too many arguments")
571
563
  print("\nUsage: windborne hist_500hpa_geopotential initialization_time forecast_hour output_file")
@@ -578,7 +570,7 @@ def main():
578
570
  " - An ouput file to save the data")
579
571
  print("\nUsage: windborne hist_500hpa_wind_u initialization_time forecast_hour output_file")
580
572
  elif len(args.args) == 3:
581
- get_historical_500hpa_wind_u(initialization_time=args.args[0], forecast_hour=args.args[1], save_to_file=args.args[2])
573
+ get_historical_500hpa_wind_u(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
582
574
  else:
583
575
  print("Too many arguments")
584
576
  print("\nUsage: windborne hist_500hpa_wind_u initialization_time forecast_hour output_file")
@@ -591,7 +583,7 @@ def main():
591
583
  " - An ouput file to save the data")
592
584
  print("\nUsage: windborne hist_500hpa_wind_u initialization_time forecast_hour output_file")
593
585
  elif len(args.args) == 3:
594
- get_historical_500hpa_wind_v(initialization_time=args.args[0], forecast_hour=args.args[1], save_to_file=args.args[2])
586
+ get_historical_500hpa_wind_v(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
595
587
  else:
596
588
  print("Too many arguments")
597
589
  print("\nUsage: windborne hist_500hpa_wind_v initialization_time forecast_hour output_file")
@@ -614,7 +606,7 @@ def main():
614
606
  elif len(args.args) == 1:
615
607
  if '.' in args.args[0]:
616
608
  # Save tcs with the latest available initialization time in filename
617
- get_tropical_cyclones(basin=args.basin, save_to_file=args.args[0])
609
+ get_tropical_cyclones(basin=args.basin, output_file=args.args[0])
618
610
  else:
619
611
  # Display tcs for selected initialization time
620
612
  if get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin):
@@ -625,7 +617,7 @@ def main():
625
617
  print(f"No active tropical cyclones for {basin_name} and {args.args[0]} initialization time.")
626
618
  elif len(args.args) == 2:
627
619
  print(f"Saving tropical cyclones for initialization time {args.args[0]} and {basin_name}\n")
628
- get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin, save_to_file=args.args[1])
620
+ get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin, output_file=args.args[1])
629
621
  else:
630
622
  print("Error: Too many arguments")
631
623
  print("Usage: windborne cyclones [initialization_time] output_file")
@@ -0,0 +1,210 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def save_track_as_little_r(filename, cyclone_data):
5
+ """
6
+ Convert and save cyclone data in little_R format.
7
+ """
8
+ with open(filename, 'w', encoding='utf-8') as f:
9
+ for cyclone_id, tracks in cyclone_data.items():
10
+ for track in tracks:
11
+ # Parse the time
12
+ dt = datetime.fromisoformat(track['time'].replace('Z', '+00:00'))
13
+
14
+ # Header line 1
15
+ header1 = f"{float(track['latitude']):20.5f}{float(track['longitude']):20.5f}{'HMS':40}"
16
+ header1 += f"{0:10d}{0:10d}{0:10d}" # Station ID numbers
17
+ header1 += f"{dt.year:10d}{dt.month:10d}{dt.day:10d}{dt.hour:10d}{0:10d}"
18
+ header1 += f"{0:10d}{0:10.3f}{cyclone_id:40}"
19
+ f.write(header1 + '\n')
20
+
21
+ # Header line 2
22
+ header2 = f"{0:20.5f}{1:10d}{0:10.3f}"
23
+ f.write(header2 + '\n')
24
+
25
+ # Data line format: p, z, t, d, s, d (pressure, height, temp, dewpoint, speed, direction)
26
+ # We'll only include position data
27
+ data_line = f"{-888888.0:13.5f}{float(track['latitude']):13.5f}{-888888.0:13.5f}"
28
+ data_line += f"{-888888.0:13.5f}{-888888.0:13.5f}{float(track['longitude']):13.5f}"
29
+ data_line += f"{0:7d}" # End of data line marker
30
+ f.write(data_line + '\n')
31
+
32
+ # End of record line
33
+ f.write(f"{-777777.0:13.5f}\n")
34
+
35
+ print("Saved to", filename)
36
+
37
+
38
+ def save_track_as_kml(filename, cyclone_data):
39
+ """
40
+ Convert and save cyclone data as KML, handling meridian crossing.
41
+ """
42
+ kml = '<?xml version="1.0" encoding="UTF-8"?>\n'
43
+ kml += '<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document>\n'
44
+
45
+ for cyclone_id, tracks in cyclone_data.items():
46
+ kml += f' <Placemark>\n <name>{cyclone_id}</name>\n <MultiGeometry>\n'
47
+
48
+ current_segment = []
49
+
50
+ for i in range(len(tracks)):
51
+ lon = float(tracks[i]['longitude'])
52
+ lat = float(tracks[i]['latitude'])
53
+
54
+ if not current_segment:
55
+ current_segment.append(tracks[i])
56
+ continue
57
+
58
+ prev_lon = float(current_segment[-1]['longitude'])
59
+
60
+ # Check if we've crossed the meridian
61
+ if abs(lon - prev_lon) > 180:
62
+ # Write the current segment
63
+ kml += ' <LineString>\n <coordinates>\n'
64
+ coordinates = [f' {track["longitude"]},{track["latitude"]},{0}'
65
+ for track in current_segment]
66
+ kml += '\n'.join(coordinates)
67
+ kml += '\n </coordinates>\n </LineString>\n'
68
+
69
+ # Start new segment
70
+ current_segment = [tracks[i]]
71
+ else:
72
+ current_segment.append(tracks[i])
73
+
74
+ # Write the last segment if it's not empty
75
+ if current_segment:
76
+ kml += ' <LineString>\n <coordinates>\n'
77
+ coordinates = [f' {track["longitude"]},{track["latitude"]},{0}'
78
+ for track in current_segment]
79
+ kml += '\n'.join(coordinates)
80
+ kml += '\n </coordinates>\n </LineString>\n'
81
+
82
+ kml += ' </MultiGeometry>\n </Placemark>\n'
83
+
84
+ kml += '</Document>\n</kml>'
85
+
86
+ with open(filename, 'w', encoding='utf-8') as f:
87
+ f.write(kml)
88
+ print(f"Saved to {filename}")
89
+
90
+
91
+ def save_track_as_gpx(filename, cyclone_data):
92
+ """Convert and save cyclone data as GPX, handling meridian crossing."""
93
+ gpx = '<?xml version="1.0" encoding="UTF-8"?>\n'
94
+ gpx += '<gpx version="1.1" creator="Windborne" xmlns="http://www.topografix.com/GPX/1/1">\n'
95
+
96
+ for cyclone_id, tracks in cyclone_data.items():
97
+ gpx += f' <trk>\n <name>{cyclone_id}</name>\n'
98
+
99
+ current_segment = []
100
+ segment_count = 1
101
+
102
+ for i in range(len(tracks)):
103
+ lon = float(tracks[i]['longitude'])
104
+ lat = float(tracks[i]['latitude'])
105
+
106
+ if not current_segment:
107
+ current_segment.append(tracks[i])
108
+ continue
109
+
110
+ prev_lon = float(current_segment[-1]['longitude'])
111
+
112
+ # Check if we've crossed the meridian
113
+ if abs(lon - prev_lon) > 180:
114
+ # Write the current segment
115
+ gpx += ' <trkseg>\n'
116
+ for point in current_segment:
117
+ gpx += f' <trkpt lat="{point["latitude"]}" lon="{point["longitude"]}">\n'
118
+ gpx += f' <time>{point["time"]}</time>\n'
119
+ gpx += ' </trkpt>\n'
120
+ gpx += ' </trkseg>\n'
121
+
122
+ # Start new segment
123
+ current_segment = [tracks[i]]
124
+ segment_count += 1
125
+ else:
126
+ current_segment.append(tracks[i])
127
+
128
+ # Write the last segment if it's not empty
129
+ if current_segment:
130
+ gpx += ' <trkseg>\n'
131
+ for point in current_segment:
132
+ gpx += f' <trkpt lat="{point["latitude"]}" lon="{point["longitude"]}">\n'
133
+ gpx += f' <time>{point["time"]}</time>\n'
134
+ gpx += ' </trkpt>\n'
135
+ gpx += ' </trkseg>\n'
136
+
137
+ gpx += ' </trk>\n'
138
+
139
+ gpx += '</gpx>'
140
+
141
+ with open(filename, 'w', encoding='utf-8') as f:
142
+ f.write(gpx)
143
+ print(f"Saved to {filename}")
144
+
145
+
146
+ def save_track_as_geojson(filename, cyclone_data):
147
+ """Convert and save cyclone data as GeoJSON, handling meridian crossing."""
148
+ features = []
149
+ for cyclone_id, tracks in cyclone_data.items():
150
+ # Initialize lists to store line segments
151
+ line_segments = []
152
+ current_segment = []
153
+
154
+ for i in range(len(tracks)):
155
+ lon = float(tracks[i]['longitude'])
156
+ lat = float(tracks[i]['latitude'])
157
+
158
+ if not current_segment:
159
+ current_segment.append([lon, lat])
160
+ continue
161
+
162
+ prev_lon = current_segment[-1][0]
163
+
164
+ # Check if we've crossed the meridian (large longitude jump)
165
+ if abs(lon - prev_lon) > 180:
166
+ # If previous longitude was positive and current is negative
167
+ if prev_lon > 0 and lon < 0:
168
+ # Add point at 180° with same latitude
169
+ current_segment.append([180, lat])
170
+ line_segments.append(current_segment)
171
+ # Start new segment at -180°
172
+ current_segment = [[-180, lat], [lon, lat]]
173
+ # If previous longitude was negative and current is positive
174
+ elif prev_lon < 0 and lon > 0:
175
+ # Add point at -180° with same latitude
176
+ current_segment.append([-180, lat])
177
+ line_segments.append(current_segment)
178
+ # Start new segment at 180°
179
+ current_segment = [[180, lat], [lon, lat]]
180
+ else:
181
+ current_segment.append([lon, lat])
182
+
183
+ # Add the last segment if it's not empty
184
+ if current_segment:
185
+ line_segments.append(current_segment)
186
+
187
+ # Create a MultiLineString feature with all segments
188
+ feature = {
189
+ "type": "Feature",
190
+ "properties": {
191
+ "cyclone_id": cyclone_id,
192
+ "start_time": tracks[0]['time'],
193
+ "end_time": tracks[-1]['time']
194
+ },
195
+ "geometry": {
196
+ "type": "MultiLineString",
197
+ "coordinates": line_segments
198
+ }
199
+ }
200
+ features.append(feature)
201
+
202
+ geojson = {
203
+ "type": "FeatureCollection",
204
+ "features": features
205
+ }
206
+
207
+ with open(filename, 'w', encoding='utf-8') as f:
208
+ json.dump(geojson, f, indent=4)
209
+ print("Saved to", filename)
210
+