windborne 1.0.9__py3-none-any.whl → 1.1.1__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,23 @@ 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(output_file=args.output, print_results=(not 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,
374
+ print_result=(not args.output)
382
375
  )
383
376
 
384
377
  elif args.command == 'predict-path':
385
378
  get_predicted_path(
386
379
  mission_id=args.mission_id,
387
- save_to_file=args.output
380
+ output_file=args.output
388
381
  )
389
382
  ####################################################################################################################
390
383
  # FORECASTS API FUNCTIONS CALLED
@@ -403,7 +396,7 @@ def main():
403
396
  min_forecast_hour=min_forecast_hour,
404
397
  max_forecast_hour=max_forecast_hour,
405
398
  initialization_time=initialization_time,
406
- save_to_file=args.output_file
399
+ output_file=args.output_file
407
400
  )
408
401
 
409
402
  elif args.command == 'init_times':
@@ -419,21 +412,21 @@ def main():
419
412
  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
413
  print("\nUsage: windborne grid_temp_2m time output_file")
421
414
  elif len(args.args) == 2:
422
- get_temperature_2m(time=args.args[0], save_to_file=args.args[1])
415
+ get_temperature_2m(time=args.args[0], output_file=args.args[1])
423
416
  else:
424
417
  print("Too many arguments")
425
418
  print("\nUsage: windborne grid_temp_2m time output_file")
426
419
 
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")
420
+ # elif args.command == 'grid_dewpoint_2m':
421
+ # # Parse grid_dewpoint_2m arguments
422
+ # if len(args.args) in [0,1]:
423
+ # 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.")
424
+ # print("\nUsage: windborne grid_dewpoint_2m time output_file")
425
+ # elif len(args.args) == 2:
426
+ # get_dewpoint_2m(time=args.args[0], output_file=args.args[1])
427
+ # else:
428
+ # print("Too many arguments")
429
+ # print("\nUsage: windborne grid_dewpoint_2m time output_file")
437
430
 
438
431
  elif args.command == 'grid_wind_u_10m':
439
432
  # Parse grid_wind_u_10m arguments
@@ -441,7 +434,7 @@ def main():
441
434
  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
435
  print("\nUsage: windborne grid_wind_u_10m time output_file")
443
436
  elif len(args.args) == 2:
444
- get_wind_u_10m(time=args.args[0], save_to_file=args.args[1])
437
+ get_wind_u_10m(time=args.args[0], output_file=args.args[1])
445
438
  else:
446
439
  print("Too many arguments")
447
440
  print("\nUsage: windborne grid_wind_u_10m time output_file")
@@ -452,7 +445,7 @@ def main():
452
445
  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
446
  print("\nUsage: windborne grid_wind_v_10m time output_file")
454
447
  elif len(args.args) == 2:
455
- get_wind_v_10m(time=args.args[0], save_to_file=args.args[1])
448
+ get_wind_v_10m(time=args.args[0], output_file=args.args[1])
456
449
  else:
457
450
  print("Too many arguments")
458
451
  print("\nUsage: windborne grid_wind_v_10m time output_file")
@@ -463,7 +456,7 @@ def main():
463
456
  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
457
  print("\nUsage: windborne grid_500hpa_wind_u time output_file")
465
458
  elif len(args.args) == 2:
466
- get_500hpa_wind_u(time=args.args[0], save_to_file=args.args[1])
459
+ get_500hpa_wind_u(time=args.args[0], output_file=args.args[1])
467
460
  else:
468
461
  print("Too many arguments")
469
462
  print("\nUsage: windborne grid_500hpa_wind_u time output_file")
@@ -474,7 +467,7 @@ def main():
474
467
  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
468
  print("\nUsage: windborne grid_500hpa_wind_v time output_file")
476
469
  elif len(args.args) == 2:
477
- get_500hpa_wind_v(time=args.args[0], save_to_file=args.args[1])
470
+ get_500hpa_wind_v(time=args.args[0], output_file=args.args[1])
478
471
  else:
479
472
  print("Too many arguments")
480
473
  print("\nUsage: windborne grid_500hpa_wind_v time output_file")
@@ -486,7 +479,7 @@ def main():
486
479
  print("\nUsage: windborne grid_500hpa_temperature time output_file")
487
480
  return
488
481
  elif len(args.args) == 2:
489
- get_500hpa_temperature(time=args.args[0], save_to_file=args.args[1])
482
+ get_500hpa_temperature(time=args.args[0], output_file=args.args[1])
490
483
  else:
491
484
  print("Too many arguments")
492
485
  print("\nUsage: windborne grid_500hpa_temperature time output_file")
@@ -498,7 +491,7 @@ def main():
498
491
  print("\nUsage: windborne grid_850hpa_temperature time output_file")
499
492
  return
500
493
  elif len(args.args) == 2:
501
- get_850hpa_temperature(time=args.args[0], save_to_file=args.args[1])
494
+ get_850hpa_temperature(time=args.args[0], output_file=args.args[1])
502
495
  else:
503
496
  print("Too many arguments")
504
497
  print("\nUsage: windborne grid_850hpa_temperature time output_file")
@@ -509,7 +502,7 @@ def main():
509
502
  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
503
  print("\nUsage: windborne grid_pressure_msl time output_file")
511
504
  elif len(args.args) == 2:
512
- get_pressure_msl(time=args.args[0], save_to_file=args.args[1])
505
+ get_pressure_msl(time=args.args[0], output_file=args.args[1])
513
506
  else:
514
507
  print("Too many arguments")
515
508
  print("\nUsage: windborne grid_pressure_msl time output_file")
@@ -521,7 +514,7 @@ def main():
521
514
  print("\nUsage: windborne grid_500hpa_geopotential time output_file")
522
515
  return
523
516
  elif len(args.args) == 2:
524
- get_500hpa_geopotential(time=args.args[0], save_to_file=args.args[1])
517
+ get_500hpa_geopotential(time=args.args[0], output_file=args.args[1])
525
518
  else:
526
519
  print("Too many arguments")
527
520
  print("\nUsage: windborne grid_500hpa_geopotential time output_file")
@@ -533,7 +526,7 @@ def main():
533
526
  print("\nUsage: windborne grid_850hpa_geopotential time output_file")
534
527
  return
535
528
  elif len(args.args) == 2:
536
- get_850hpa_geopotential(time=args.args[0], save_to_file=args.args[1])
529
+ get_850hpa_geopotential(time=args.args[0], output_file=args.args[1])
537
530
  else:
538
531
  print("Too many arguments")
539
532
  print("\nUsage: windborne grid_850hpa_geopotential time output_file")
@@ -550,7 +543,7 @@ def main():
550
543
  print("\nUsage: windborne hist_temp_2m initialization_time forecast_hour output_file")
551
544
  return
552
545
  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])
546
+ get_historical_temperature_2m(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
554
547
  else:
555
548
  print("Too many arguments")
556
549
  print("\nUsage: windborne hist_temp_2m initialization_time forecast_hour output_file")
@@ -565,7 +558,7 @@ def main():
565
558
  print("\nUsage: windborne hist_500hpa_geopotential initialization_time forecast_hour output_file")
566
559
  return
567
560
  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])
561
+ get_historical_500hpa_geopotential(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
569
562
  else:
570
563
  print("Too many arguments")
571
564
  print("\nUsage: windborne hist_500hpa_geopotential initialization_time forecast_hour output_file")
@@ -578,7 +571,7 @@ def main():
578
571
  " - An ouput file to save the data")
579
572
  print("\nUsage: windborne hist_500hpa_wind_u initialization_time forecast_hour output_file")
580
573
  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])
574
+ get_historical_500hpa_wind_u(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
582
575
  else:
583
576
  print("Too many arguments")
584
577
  print("\nUsage: windborne hist_500hpa_wind_u initialization_time forecast_hour output_file")
@@ -591,7 +584,7 @@ def main():
591
584
  " - An ouput file to save the data")
592
585
  print("\nUsage: windborne hist_500hpa_wind_u initialization_time forecast_hour output_file")
593
586
  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])
587
+ get_historical_500hpa_wind_v(initialization_time=args.args[0], forecast_hour=args.args[1], output_file=args.args[2])
595
588
  else:
596
589
  print("Too many arguments")
597
590
  print("\nUsage: windborne hist_500hpa_wind_v initialization_time forecast_hour output_file")
@@ -614,7 +607,7 @@ def main():
614
607
  elif len(args.args) == 1:
615
608
  if '.' in args.args[0]:
616
609
  # Save tcs with the latest available initialization time in filename
617
- get_tropical_cyclones(basin=args.basin, save_to_file=args.args[0])
610
+ get_tropical_cyclones(basin=args.basin, output_file=args.args[0])
618
611
  else:
619
612
  # Display tcs for selected initialization time
620
613
  if get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin):
@@ -625,7 +618,7 @@ def main():
625
618
  print(f"No active tropical cyclones for {basin_name} and {args.args[0]} initialization time.")
626
619
  elif len(args.args) == 2:
627
620
  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])
621
+ get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin, output_file=args.args[1])
629
622
  else:
630
623
  print("Error: Too many arguments")
631
624
  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
+