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/__init__.py +6 -15
- windborne/api_request.py +227 -0
- windborne/cli.py +52 -60
- windborne/cyclone_formatting.py +210 -0
- windborne/data_api.py +390 -1028
- windborne/forecasts_api.py +186 -305
- windborne/observation_formatting.py +456 -0
- windborne/utils.py +15 -887
- {windborne-1.0.8.dist-info → windborne-1.1.0.dist-info}/METADATA +1 -2
- windborne-1.1.0.dist-info/RECORD +13 -0
- windborne/config.py +0 -42
- windborne-1.0.8.dist-info/RECORD +0 -11
- {windborne-1.0.8.dist-info → windborne-1.1.0.dist-info}/WHEEL +0 -0
- {windborne-1.0.8.dist-info → windborne-1.1.0.dist-info}/entry_points.txt +0 -0
- {windborne-1.0.8.dist-info → windborne-1.1.0.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,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
|
-
|
364
|
+
output_file=args.output
|
373
365
|
)
|
374
366
|
|
375
367
|
elif args.command == 'flying-missions':
|
376
|
-
get_flying_missions(
|
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
|
-
|
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
|
-
|
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
|
-
|
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],
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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],
|
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,
|
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,
|
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
|
+
|