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/__init__.py +6 -15
- windborne/api_request.py +227 -0
- windborne/cli.py +53 -60
- windborne/cyclone_formatting.py +210 -0
- windborne/data_api.py +429 -1051
- windborne/forecasts_api.py +186 -305
- windborne/observation_formatting.py +456 -0
- windborne/utils.py +15 -887
- {windborne-1.0.9.dist-info → windborne-1.1.1.dist-info}/METADATA +1 -2
- windborne-1.1.1.dist-info/RECORD +13 -0
- windborne/config.py +0 -42
- windborne-1.0.9.dist-info/RECORD +0 -11
- {windborne-1.0.9.dist-info → windborne-1.1.1.dist-info}/WHEEL +0 -0
- {windborne-1.0.9.dist-info → windborne-1.1.1.dist-info}/entry_points.txt +0 -0
- {windborne-1.0.9.dist-info → windborne-1.1.1.dist-info}/top_level.txt +0 -0
windborne/cli.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
import argparse
|
2
|
+
import json
|
2
3
|
|
3
4
|
from . import (
|
4
|
-
|
5
|
-
|
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
|
-
|
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
|
-
|
239
|
+
output_file = None
|
244
240
|
output_format = args.output
|
245
241
|
output_dir = args.output_dir
|
246
242
|
|
247
|
-
|
243
|
+
get_super_observations(
|
248
244
|
start_time=args.start_time,
|
249
245
|
end_time=args.end_time,
|
250
|
-
|
251
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
294
|
+
output_file = None
|
301
295
|
output_format = args.output
|
302
296
|
output_dir = args.output_dir
|
303
297
|
|
304
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
341
|
+
output_file=args.output
|
350
342
|
)
|
351
343
|
|
352
344
|
elif args.command == 'super-observations-page':
|
353
345
|
if not args.output:
|
354
|
-
|
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
|
-
|
364
|
+
output_file=args.output
|
373
365
|
)
|
374
366
|
|
375
367
|
elif args.command == 'flying-missions':
|
376
|
-
get_flying_missions(
|
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
|
-
|
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
|
-
|
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
|
-
|
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],
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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,
|
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,
|
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
|
+
|