oafuncs 0.0.98.31__tar.gz → 0.0.98.32__tar.gz

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.
Files changed (52) hide show
  1. {oafuncs-0.0.98.31/oafuncs.egg-info → oafuncs-0.0.98.32}/PKG-INFO +1 -1
  2. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/cprogressbar.py +3 -1
  3. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/email.py +0 -19
  4. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_cmap.py +7 -0
  5. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_date.py +58 -54
  6. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/hycom_3hourly.py +2 -2
  7. oafuncs-0.0.98.32/oafuncs/oa_draw.py +398 -0
  8. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_file.py +22 -10
  9. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32/oafuncs.egg-info}/PKG-INFO +1 -1
  10. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/setup.py +5 -3
  11. oafuncs-0.0.98.31/oafuncs/oa_draw.py +0 -401
  12. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/LICENSE.txt +0 -0
  13. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/MANIFEST.in +0 -0
  14. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/README.md +0 -0
  15. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/__init__.py +0 -0
  16. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_data/hycom.png +0 -0
  17. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_data/oafuncs.png +0 -0
  18. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/data_interp.py +0 -0
  19. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/netcdf_merge.py +0 -0
  20. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/netcdf_modify.py +0 -0
  21. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/netcdf_write.py +0 -0
  22. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/parallel.py +0 -0
  23. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/parallel_test.py +0 -0
  24. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/plot_dataset.py +0 -0
  25. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/replace_file_content.py +0 -0
  26. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_data.py +0 -0
  27. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/User_Agent-list.txt +0 -0
  28. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/__init__.py +0 -0
  29. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/hycom_3hourly_proxy.py +0 -0
  30. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/idm.py +0 -0
  31. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/literature.py +0 -0
  32. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/read_proxy.py +0 -0
  33. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/test_ua.py +0 -0
  34. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/user_agent.py +0 -0
  35. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_help.py +0 -0
  36. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/__init__.py +0 -0
  37. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/roms/__init__.py +0 -0
  38. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/roms/test.py +0 -0
  39. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/wrf/__init__.py +0 -0
  40. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/wrf/little_r.py +0 -0
  41. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_nc.py +0 -0
  42. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_python.py +0 -0
  43. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/__init__.py +0 -0
  44. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/meteorological.py +0 -0
  45. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/ocean.py +0 -0
  46. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/scientific.py +0 -0
  47. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_tool.py +0 -0
  48. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/SOURCES.txt +0 -0
  49. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/dependency_links.txt +0 -0
  50. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/requires.txt +0 -0
  51. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/top_level.txt +0 -0
  52. {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oafuncs
3
- Version: 0.0.98.31
3
+ Version: 0.0.98.32
4
4
  Summary: Oceanic and Atmospheric Functions
5
5
  Home-page: https://github.com/Industry-Pays/OAFuncs
6
6
  Author: Kun Liu
@@ -300,11 +300,13 @@ class ColorProgressBar:
300
300
  # 获取终端宽度
301
301
  try:
302
302
  term_width = self.bar_length or (shutil.get_terminal_size().columns if self._is_terminal else 80)
303
+ # print(f'Terminal width: {term_width}') # 调试输出
303
304
  except (AttributeError, OSError):
304
305
  term_width = 80 # 默认终端宽度
305
306
 
306
307
  # 确保有效宽度不小于最低限制
307
- effective_width = max(15, term_width - 40)
308
+ # effective_width = max(15, term_width - 40)
309
+ effective_width = max(15, int(term_width * 0.6)) # 保留40个字符用于其他信息
308
310
  if effective_width < 10:
309
311
  warnings.warn("Terminal width is too small for proper progress bar rendering.")
310
312
  effective_width = 10 # 设置最低宽度限制
@@ -1,21 +1,3 @@
1
- #!/usr/bin/env python
2
- # coding=utf-8
3
- """
4
- Author: Liu Kun && 16031215@qq.com
5
- Date: 2025-04-04 20:21:59
6
- LastEditors: Liu Kun && 16031215@qq.com
7
- LastEditTime: 2025-04-04 20:21:59
8
- FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\_script\\email.py
9
- Description:
10
- EditPlatform: vscode
11
- ComputerInfo: XPS 15 9510
12
- SystemInfo: Windows 11
13
- Python Version: 3.12
14
- """
15
-
16
-
17
-
18
-
19
1
  import random
20
2
  import smtplib
21
3
  from email.header import Header
@@ -26,7 +8,6 @@ from rich import print
26
8
 
27
9
  __all__ = ["send"]
28
10
 
29
-
30
11
  def _email_info():
31
12
  email_dict = {
32
13
  "liukun0312@vip.qq.com": [4, 13, -10, 2, -10, 4, -7, -8, 8, -1, 3, -2, -11, -6, -9, -7],
@@ -258,10 +258,17 @@ def get(colormap_name: Optional[str] = None, show_available: bool = False) -> Op
258
258
  "cool_1": ["#4e00b3", "#0000FF", "#00c0ff", "#a1d3ff", "#DCDCDC"],
259
259
  "warm_1": ["#DCDCDC", "#FFD39B", "#FF8247", "#FF0000", "#FF5F9E"],
260
260
  # ---------------------------------------------------------------------------
261
+ # suitable for diverging color maps, wind vector
261
262
  "diverging_2": ["#4A235A", "#1F618D", "#1ABC9C", "#A9DFBF", "#F2F3F4", "#FDEBD0", "#F5B041", "#E74C3C", "#78281F"],
262
263
  "cool_2": ["#4A235A", "#1F618D", "#1ABC9C", "#A9DFBF", "#F2F3F4"],
263
264
  "warm_2": ["#F2F3F4", "#FDEBD0", "#F5B041", "#E74C3C", "#78281F"],
264
265
  # ---------------------------------------------------------------------------
266
+ "diverging_3": ["#1a66f2", "#5DADE2", "#48C9B0", "#A9DFBF", "#F2F3F4", "#FFDAB9", "#FF9E80", "#FF6F61", "#FF1744"],
267
+ "cool_3": ["#1a66f2", "#5DADE2", "#48C9B0", "#A9DFBF", "#F2F3F4"],
268
+ "warm_3": ["#F2F3F4", "#FFDAB9", "#FF9E80", "#FF6F61", "#FF1744"],
269
+ # ---------------------------------------------------------------------------
270
+ "diverging_4": ["#5DADE2", "#A2D9F7", "#D6EAF8", "#F2F3F4", "#FADBD8", "#F1948A", "#E74C3C"],
271
+ # ----------------------------------------------------------------------------
265
272
  "colorful_1": ["#6d00db", "#9800cb", "#F2003C", "#ff4500", "#ff7f00", "#FE28A2", "#FFC0CB", "#DDA0DD", "#40E0D0", "#1a66f2", "#00f7fb", "#8fff88", "#E3FF00"],
266
273
  }
267
274
 
@@ -1,20 +1,6 @@
1
- #!/usr/bin/env python
2
- # coding=utf-8
3
- """
4
- Author: Liu Kun && 16031215@qq.com
5
- Date: 2025-03-27 16:56:57
6
- LastEditors: Liu Kun && 16031215@qq.com
7
- LastEditTime: 2025-04-04 12:58:15
8
- FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\oa_date.py
9
- Description:
10
- EditPlatform: vscode
11
- ComputerInfo: XPS 15 9510
12
- SystemInfo: Windows 11
13
- Python Version: 3.12
14
- """
15
-
16
1
  import calendar
17
2
  import datetime
3
+ import functools
18
4
  from typing import List, Optional
19
5
 
20
6
  from rich import print
@@ -64,15 +50,16 @@ def hour_range(start_time: str, end_time: str, hour_interval: int = 6) -> List[s
64
50
  date_s += datetime.timedelta(hours=hour_interval)
65
51
  return date_list
66
52
 
53
+
67
54
  def adjust_time(base_time: str, time_delta: int, delta_unit: str = "hours", output_format: Optional[str] = None) -> str:
68
55
  """
69
56
  Adjust a given base time by adding a specified time delta.
70
57
 
71
58
  Args:
72
- base_time (str): Base time in the format "yyyymmdd" to "yyyymmddHHMMSS".
73
- Missing parts are assumed to be "0".
59
+ base_time (str): Base time in the format "yyyy" to "yyyymmddHHMMSS".
60
+ Missing parts are padded with appropriate defaults.
74
61
  time_delta (int): The amount of time to add.
75
- delta_unit (str): The unit of time to add ("seconds", "minutes", "hours", "days").
62
+ delta_unit (str): The unit of time to add ("seconds", "minutes", "hours", "days", "months", "years").
76
63
  output_format (str, optional): Custom output format for the adjusted time. Defaults to None.
77
64
 
78
65
  Returns:
@@ -84,17 +71,31 @@ def adjust_time(base_time: str, time_delta: int, delta_unit: str = "hours", outp
84
71
  >>> adjust_time("20240101000000", 2, "hours", "%Y-%m-%d %H:%M:%S")
85
72
  '2024-01-01 02:00:00'
86
73
  >>> adjust_time("20240101000000", 30, "minutes")
87
- '2024-01-01 00:30:00'
74
+ '20240101003000'
88
75
  """
89
76
  # Normalize the input time to "yyyymmddHHMMSS" format
90
77
  time_format = "%Y%m%d%H%M%S"
91
- if len(base_time) == 4:
92
- base_time += "0101"
93
- elif len(base_time) == 6:
94
- base_time += "01"
95
- base_time = base_time.ljust(14, "0")
96
78
 
97
- time_obj = datetime.datetime.strptime(base_time, time_format)
79
+ # Pad the time string to full format
80
+ if len(base_time) == 4: # yyyy
81
+ base_time += "0101000000"
82
+ elif len(base_time) == 6: # yyyymm
83
+ base_time += "01000000"
84
+ elif len(base_time) == 8: # yyyymmdd
85
+ base_time += "000000"
86
+ elif len(base_time) == 10: # yyyymmddhh
87
+ base_time += "0000"
88
+ elif len(base_time) == 12: # yyyymmddhhmm
89
+ base_time += "00"
90
+ elif len(base_time) == 14: # yyyymmddhhmmss
91
+ pass # Already complete
92
+ else:
93
+ raise ValueError(f"Invalid base_time format. Expected 4-14 digits, got {len(base_time)}")
94
+
95
+ try:
96
+ time_obj = datetime.datetime.strptime(base_time, time_format)
97
+ except ValueError as e:
98
+ raise ValueError(f"Invalid date format: {base_time}. Error: {e}")
98
99
 
99
100
  # Add the specified amount of time
100
101
  if delta_unit == "seconds":
@@ -114,8 +115,18 @@ def adjust_time(base_time: str, time_delta: int, delta_unit: str = "hours", outp
114
115
  time_obj = time_obj.replace(year=year, month=month, day=day)
115
116
  elif delta_unit == "years":
116
117
  # Handle year addition separately
117
- year = time_obj.year + time_delta
118
- time_obj = time_obj.replace(year=year)
118
+ try:
119
+ year = time_obj.year + time_delta
120
+ # Handle leap year edge case for Feb 29
121
+ if time_obj.month == 2 and time_obj.day == 29:
122
+ if not calendar.isleap(year):
123
+ time_obj = time_obj.replace(year=year, day=28)
124
+ else:
125
+ time_obj = time_obj.replace(year=year)
126
+ else:
127
+ time_obj = time_obj.replace(year=year)
128
+ except ValueError as e:
129
+ raise ValueError(f"Invalid year calculation: {e}")
119
130
  else:
120
131
  raise ValueError("Invalid time unit. Use 'seconds', 'minutes', 'hours', 'days', 'months', or 'years'.")
121
132
 
@@ -123,19 +134,9 @@ def adjust_time(base_time: str, time_delta: int, delta_unit: str = "hours", outp
123
134
  if output_format:
124
135
  return time_obj.strftime(output_format)
125
136
  else:
126
- if delta_unit == "seconds":
127
- default_format = "%Y%m%d%H%M%S"
128
- elif delta_unit == "minutes":
129
- default_format = "%Y%m%d%H%M"
130
- elif delta_unit == "hours":
131
- default_format = "%Y%m%d%H"
132
- elif delta_unit == "days":
133
- default_format = "%Y%m%d"
134
- elif delta_unit == "months":
135
- default_format = "%Y%m"
136
- elif delta_unit == "years":
137
- default_format = "%Y"
138
- return time_obj.strftime(default_format)
137
+ # Use default format based on delta_unit
138
+ format_map = {"seconds": "%Y%m%d%H%M%S", "minutes": "%Y%m%d%H%M%S", "hours": "%Y%m%d%H", "days": "%Y%m%d", "months": "%Y%m", "years": "%Y"}
139
+ return time_obj.strftime(format_map[delta_unit])
139
140
 
140
141
 
141
142
  class timeit:
@@ -154,26 +155,29 @@ class timeit:
154
155
  Example:
155
156
  @timeit(log_to_file=True, display_time=True)
156
157
  def example_function():
157
- # Simulate some work
158
+ import time
158
159
  time.sleep(2)
159
160
  """
160
161
 
161
- def __init__(self, func, log_to_file: bool = False, display_time: bool = True):
162
- self.func = func
162
+ def __init__(self, log_to_file: bool = False, display_time: bool = True):
163
163
  self.log_to_file = log_to_file
164
164
  self.display_time = display_time
165
165
 
166
- def __call__(self, *args, **kwargs):
167
- start_time = datetime.datetime.now()
168
- result = self.func(*args, **kwargs)
169
- end_time = datetime.datetime.now()
170
- elapsed_time = (end_time - start_time).total_seconds()
166
+ def __call__(self, func):
167
+ @functools.wraps(func)
168
+ def wrapper(*args, **kwargs):
169
+ start_time = datetime.datetime.now()
170
+ result = func(*args, **kwargs)
171
+ end_time = datetime.datetime.now()
172
+ elapsed_time = (end_time - start_time).total_seconds()
173
+
174
+ if self.display_time:
175
+ print(f"[bold green]Function '{func.__name__}' executed in {elapsed_time:.2f} seconds.[/bold green]")
171
176
 
172
- if self.display_time:
173
- print(f"[bold green]Function '{self.func.__name__}' executed in {elapsed_time:.2f} seconds.[/bold green]")
177
+ if self.log_to_file:
178
+ with open("execution_time.log", "a", encoding="utf-8") as log_file:
179
+ log_file.write(f"{datetime.datetime.now()} - Function '{func.__name__}' executed in {elapsed_time:.2f} seconds.\n")
174
180
 
175
- if self.log_to_file:
176
- with open("execution_time.log", "a") as log_file:
177
- log_file.write(f"{datetime.datetime.now()} - Function '{self.func.__name__}' executed in {elapsed_time:.2f} seconds.\n")
181
+ return result
178
182
 
179
- return result
183
+ return wrapper
@@ -719,7 +719,7 @@ class _HycomDownloader:
719
719
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
720
720
  # print(f"{timestamp} - INFO - ", end="") # Output log prefix without newline
721
721
  # print("[bold #3dfc40]Success")
722
- print(f"{timestamp} - RESULT - [bold #3dfc40]Success")
722
+ print(f"{timestamp} - INFO - [bold #3dfc40]Success")
723
723
  return
724
724
 
725
725
  except Exception as e:
@@ -736,7 +736,7 @@ class _HycomDownloader:
736
736
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
737
737
  # print(f"{timestamp} - ERROR - ", end="")
738
738
  # print("[bold red]Failed")
739
- print(f"{timestamp} - RESULT - [bold red]Failure")
739
+ print(f"{timestamp} - INFO - [bold red]Failure")
740
740
  return
741
741
 
742
742
  async def run(self):
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+ """
4
+ Author: Liu Kun && 16031215@qq.com
5
+ Date: 2024-09-17 17:26:11
6
+ LastEditors: Liu Kun && 16031215@qq.com
7
+ LastEditTime: 2024-11-21 13:10:47
8
+ FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\oa_draw.py
9
+ Description:
10
+ EditPlatform: vscode
11
+ ComputerInfo: XPS 15 9510
12
+ SystemInfo: Windows 11
13
+ Python Version: 3.11
14
+ """
15
+
16
+ import warnings
17
+
18
+ import cartopy.crs as ccrs
19
+ import cartopy.feature as cfeature
20
+ import cv2
21
+ import matplotlib as mpl
22
+ import matplotlib.pyplot as plt
23
+ import numpy as np
24
+ from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter
25
+ from rich import print
26
+
27
+ __all__ = ["fig_minus", "gif", "movie", "setup_map", "MidpointNormalize"]
28
+
29
+ warnings.filterwarnings("ignore")
30
+
31
+
32
+ def fig_minus(x_axis: plt.Axes = None, y_axis: plt.Axes = None, colorbar: mpl.colorbar.Colorbar = None, decimal_places: int = None, add_spacing: bool = False) -> plt.Axes | mpl.colorbar.Colorbar | None:
33
+ """Replace negative signs with minus signs in axis tick labels.
34
+
35
+ Args:
36
+ x_axis (plt.Axes, optional): Matplotlib x-axis object to modify.
37
+ y_axis (plt.Axes, optional): Matplotlib y-axis object to modify.
38
+ colorbar (mpl.colorbar.Colorbar, optional): Matplotlib colorbar object to modify.
39
+ decimal_places (int, optional): Number of decimal places to display.
40
+ add_spacing (bool, optional): Whether to add spaces before non-negative numbers.
41
+
42
+ Returns:
43
+ plt.Axes | mpl.colorbar.Colorbar | None: The modified axis or colorbar object.
44
+
45
+ Example:
46
+ >>> fig_minus(x_axis=ax, decimal_places=2, add_spacing=True)
47
+ """
48
+ current_ticks = None
49
+ target_object = None
50
+
51
+ # Determine which object to use and get its ticks
52
+ if x_axis is not None:
53
+ current_ticks = x_axis.get_xticks()
54
+ target_object = x_axis
55
+ elif y_axis is not None:
56
+ current_ticks = y_axis.get_yticks()
57
+ target_object = y_axis
58
+ elif colorbar is not None:
59
+ current_ticks = colorbar.get_ticks()
60
+ target_object = colorbar
61
+ else:
62
+ print("[yellow]Warning:[/yellow] No valid axis or colorbar provided.")
63
+ return None
64
+
65
+ # Find index for adding space to non-negative values if needed
66
+ if add_spacing:
67
+ index = 0
68
+ for i, tick in enumerate(current_ticks):
69
+ if tick >= 0:
70
+ index = i
71
+ break
72
+
73
+ # Format according to decimal places if specified
74
+ if decimal_places is not None:
75
+ current_ticks = [f"{val:.{decimal_places}f}" if val != 0 else "0" for val in current_ticks]
76
+
77
+ # Replace negative signs with minus signs
78
+ out_ticks = [f"{val}".replace("-", "\u2212") for val in current_ticks]
79
+
80
+ # Add spaces before non-negative values if specified
81
+ if add_spacing:
82
+ out_ticks[index:] = [" " + m for m in out_ticks[index:]]
83
+
84
+ # Apply formatted ticks to the appropriate object
85
+ if x_axis is not None:
86
+ x_axis.set_xticklabels(out_ticks)
87
+ elif y_axis is not None:
88
+ y_axis.set_yticklabels(out_ticks)
89
+ elif colorbar is not None:
90
+ colorbar.set_ticklabels(out_ticks)
91
+
92
+ print("[green]Axis tick labels updated successfully.[/green]")
93
+ return target_object
94
+
95
+
96
+ def gif(image_paths: list[str], output_gif_name: str, frame_duration: float = 0.2, resize_dimensions: tuple[int, int] = None) -> None:
97
+ """Create a GIF from a list of images.
98
+
99
+ Args:
100
+ image_paths (list[str]): List of image file paths.
101
+ output_gif_name (str): Name of the output GIF file.
102
+ frame_duration (float): Duration of each frame in seconds. Defaults to 0.2.
103
+ resize_dimensions (tuple[int, int], optional): Resize dimensions (width, height). Defaults to None.
104
+
105
+ Returns:
106
+ None
107
+
108
+ Example:
109
+ >>> gif(['image1.png', 'image2.png'], 'output.gif', frame_duration=0.5, resize_dimensions=(800, 600))
110
+ """
111
+ import imageio.v2 as imageio
112
+ from PIL import Image
113
+
114
+ if not image_paths:
115
+ print("[red]Error:[/red] Image paths list is empty.")
116
+ return
117
+
118
+ frames = []
119
+
120
+ # Get target dimensions
121
+ if resize_dimensions is None and image_paths:
122
+ with Image.open(image_paths[0]) as img:
123
+ resize_dimensions = img.size
124
+
125
+ # Read and resize all images
126
+ for image_name in image_paths:
127
+ try:
128
+ with Image.open(image_name) as img:
129
+ if resize_dimensions:
130
+ img = img.resize(resize_dimensions, Image.LANCZOS)
131
+ frames.append(np.array(img))
132
+ except Exception as e:
133
+ print(f"[yellow]Warning:[/yellow] Failed to read image {image_name}: {e}")
134
+ continue
135
+
136
+ if not frames:
137
+ print("[red]Error:[/red] No valid images found.")
138
+ return
139
+
140
+ # Create GIF
141
+ try:
142
+ imageio.mimsave(output_gif_name, frames, format="GIF", duration=frame_duration)
143
+ print(f"[green]GIF created successfully![/green] Size: {resize_dimensions}, Frame duration: {frame_duration}s")
144
+ except Exception as e:
145
+ print(f"[red]Error:[/red] Failed to create GIF: {e}")
146
+
147
+
148
+ def movie(image_files: list[str], output_video_path: str, fps: int) -> None:
149
+ """Create a video from a list of image files.
150
+
151
+ Args:
152
+ image_files (list[str]): List of image file paths in order.
153
+ output_video_path (str): Output video file path (e.g., 'output.mp4').
154
+ fps (int): Video frame rate.
155
+
156
+ Returns:
157
+ None
158
+
159
+ Example:
160
+ >>> movie(['img1.jpg', 'img2.jpg'], 'output.mp4', fps=30)
161
+ """
162
+ if not image_files:
163
+ print("[red]Error:[/red] Image files list is empty.")
164
+ return
165
+
166
+ # Read first image to get frame dimensions
167
+ try:
168
+ frame = cv2.imread(image_files[0])
169
+ if frame is None:
170
+ print(f"[red]Error:[/red] Cannot read first image: {image_files[0]}")
171
+ return
172
+ height, width, layers = frame.shape
173
+ size = (width, height)
174
+ print(f"Video dimensions set to: {size}")
175
+ except Exception as e:
176
+ print(f"[red]Error:[/red] Error reading first image: {e}")
177
+ return
178
+
179
+ # Create VideoWriter object
180
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
181
+ out = cv2.VideoWriter(output_video_path, fourcc, fps, size)
182
+
183
+ if not out.isOpened():
184
+ print(f"[red]Error:[/red] Cannot open video file for writing: {output_video_path}")
185
+ print("Please check if the codec is supported and the path is valid.")
186
+ return
187
+
188
+ print(f"Starting to write images to video: {output_video_path}...")
189
+ successful_frames = 0
190
+
191
+ for i, filename in enumerate(image_files):
192
+ try:
193
+ frame = cv2.imread(filename)
194
+ if frame is None:
195
+ print(f"[yellow]Warning:[/yellow] Skipping unreadable image: {filename}")
196
+ continue
197
+
198
+ # Ensure frame dimensions match initialization
199
+ current_height, current_width, _ = frame.shape
200
+ if (current_width, current_height) != size:
201
+ frame = cv2.resize(frame, size)
202
+
203
+ out.write(frame)
204
+ successful_frames += 1
205
+
206
+ # Print progress
207
+ if (i + 1) % 50 == 0 or (i + 1) == len(image_files):
208
+ print(f"Processed {i + 1}/{len(image_files)} frames")
209
+
210
+ except Exception as e:
211
+ print(f"[yellow]Warning:[/yellow] Error processing image {filename}: {e}")
212
+ continue
213
+
214
+ # Release resources
215
+ out.release()
216
+ print(f"[green]Video created successfully:[/green] {output_video_path} ({successful_frames} frames)")
217
+
218
+
219
+ def setup_map(
220
+ axes: plt.Axes,
221
+ longitude_data: np.ndarray = None,
222
+ latitude_data: np.ndarray = None,
223
+ map_projection: ccrs.Projection = ccrs.PlateCarree(),
224
+ # Map features
225
+ show_land: bool = True,
226
+ show_ocean: bool = True,
227
+ show_coastline: bool = True,
228
+ show_borders: bool = False,
229
+ land_color: str = "lightgrey",
230
+ ocean_color: str = "lightblue",
231
+ coastline_linewidth: float = 0.5,
232
+ # Gridlines and ticks
233
+ show_gridlines: bool = False,
234
+ longitude_ticks: list[float] = None,
235
+ latitude_ticks: list[float] = None,
236
+ tick_decimals: int = 0,
237
+ # Gridline styling
238
+ grid_color: str = "k",
239
+ grid_alpha: float = 0.5,
240
+ grid_style: str = "--",
241
+ grid_width: float = 0.5,
242
+ # Label options
243
+ show_labels: bool = True,
244
+ left_labels: bool = True,
245
+ bottom_labels: bool = True,
246
+ right_labels: bool = False,
247
+ top_labels: bool = False,
248
+ ) -> plt.Axes:
249
+ """Setup a complete cartopy map with customizable features.
250
+
251
+ Args:
252
+ axes (plt.Axes): The axes to setup as a map.
253
+ longitude_data (np.ndarray, optional): Array of longitudes to set map extent.
254
+ latitude_data (np.ndarray, optional): Array of latitudes to set map extent.
255
+ map_projection (ccrs.Projection, optional): Coordinate reference system. Defaults to PlateCarree.
256
+
257
+ show_land (bool, optional): Whether to show land features. Defaults to True.
258
+ show_ocean (bool, optional): Whether to show ocean features. Defaults to True.
259
+ show_coastline (bool, optional): Whether to show coastlines. Defaults to True.
260
+ show_borders (bool, optional): Whether to show country borders. Defaults to False.
261
+ land_color (str, optional): Color of land. Defaults to "lightgrey".
262
+ ocean_color (str, optional): Color of oceans. Defaults to "lightblue".
263
+ coastline_linewidth (float, optional): Line width for coastlines. Defaults to 0.5.
264
+
265
+ show_gridlines (bool, optional): Whether to show gridlines. Defaults to False.
266
+ longitude_ticks (list[float], optional): Longitude tick positions.
267
+ latitude_ticks (list[float], optional): Latitude tick positions.
268
+ tick_decimals (int, optional): Number of decimal places for tick labels. Defaults to 0.
269
+
270
+ grid_color (str, optional): Gridline color. Defaults to "k".
271
+ grid_alpha (float, optional): Gridline transparency. Defaults to 0.5.
272
+ grid_style (str, optional): Gridline style. Defaults to "--".
273
+ grid_width (float, optional): Gridline width. Defaults to 0.5.
274
+
275
+ show_labels (bool, optional): Whether to show coordinate labels. Defaults to True.
276
+ left_labels (bool, optional): Show labels on left side. Defaults to True.
277
+ bottom_labels (bool, optional): Show labels on bottom. Defaults to True.
278
+ right_labels (bool, optional): Show labels on right side. Defaults to False.
279
+ top_labels (bool, optional): Show labels on top. Defaults to False.
280
+
281
+ Returns:
282
+ plt.Axes: The configured map axes.
283
+
284
+ Examples:
285
+ >>> # Basic map setup
286
+ >>> ax = setup_map(ax)
287
+
288
+ >>> # Map with gridlines and custom extent
289
+ >>> ax = setup_map(ax, longitude_data=lon, latitude_data=lat, show_gridlines=True)
290
+
291
+ >>> # Customized map
292
+ >>> ax = setup_map(
293
+ ... ax,
294
+ ... show_gridlines=True,
295
+ ... longitude_ticks=[0, 30, 60],
296
+ ... latitude_ticks=[-30, 0, 30],
297
+ ... land_color='wheat',
298
+ ... ocean_color='lightcyan'
299
+ ... )
300
+ """
301
+ from matplotlib import ticker as mticker
302
+
303
+ # Add map features
304
+ if show_land:
305
+ axes.add_feature(cfeature.LAND, facecolor=land_color)
306
+ if show_ocean:
307
+ axes.add_feature(cfeature.OCEAN, facecolor=ocean_color)
308
+ if show_coastline:
309
+ axes.add_feature(cfeature.COASTLINE, linewidth=coastline_linewidth)
310
+ if show_borders:
311
+ axes.add_feature(cfeature.BORDERS, linewidth=coastline_linewidth, linestyle=":")
312
+
313
+ # Setup coordinate formatting
314
+ lon_formatter = LongitudeFormatter(zero_direction_label=False, number_format=f".{tick_decimals}f")
315
+ lat_formatter = LatitudeFormatter(number_format=f".{tick_decimals}f")
316
+
317
+ # Handle gridlines and ticks
318
+ if show_gridlines:
319
+ # Add gridlines with labels
320
+ gl = axes.gridlines(crs=map_projection, draw_labels=show_labels, linewidth=grid_width, color=grid_color, alpha=grid_alpha, linestyle=grid_style)
321
+
322
+ # Configure label positions
323
+ gl.left_labels = left_labels
324
+ gl.bottom_labels = bottom_labels
325
+ gl.right_labels = right_labels
326
+ gl.top_labels = top_labels
327
+
328
+ # Set formatters
329
+ gl.xformatter = lon_formatter
330
+ gl.yformatter = lat_formatter
331
+
332
+ # Set custom tick positions if provided
333
+ if longitude_ticks is not None:
334
+ gl.xlocator = mticker.FixedLocator(np.array(longitude_ticks))
335
+ if latitude_ticks is not None:
336
+ gl.ylocator = mticker.FixedLocator(np.array(latitude_ticks))
337
+
338
+ elif show_labels:
339
+ # Add tick labels without gridlines
340
+ # Use current tick positions if not provided
341
+ if longitude_ticks is None:
342
+ longitude_ticks = axes.get_xticks()
343
+ if latitude_ticks is None:
344
+ latitude_ticks = axes.get_yticks()
345
+
346
+ # Set tick positions and formatters
347
+ axes.set_xticks(longitude_ticks, crs=map_projection)
348
+ axes.set_yticks(latitude_ticks, crs=map_projection)
349
+ axes.xaxis.set_major_formatter(lon_formatter)
350
+ axes.yaxis.set_major_formatter(lat_formatter)
351
+
352
+ # Set map extent if data is provided
353
+ if longitude_data is not None and latitude_data is not None:
354
+ lon_min, lon_max = np.nanmin(longitude_data), np.nanmax(longitude_data)
355
+ lat_min, lat_max = np.nanmin(latitude_data), np.nanmax(latitude_data)
356
+ axes.set_extent([lon_min, lon_max, lat_min, lat_max], crs=map_projection)
357
+
358
+ return axes
359
+
360
+
361
+ class MidpointNormalize(mpl.colors.Normalize):
362
+ """Custom normalization class to center a specific value.
363
+
364
+ Args:
365
+ vmin (float, optional): Minimum data value. Defaults to None.
366
+ vmax (float, optional): Maximum data value. Defaults to None.
367
+ vcenter (float, optional): Center value for normalization. Defaults to 0.
368
+ clip (bool, optional): Whether to clip data outside the range. Defaults to False.
369
+
370
+ Example:
371
+ >>> norm = MidpointNormalize(vmin=-2, vmax=1, vcenter=0)
372
+ """
373
+
374
+ def __init__(self, vmin: float = None, vmax: float = None, vcenter: float = 0, clip: bool = False) -> None:
375
+ self.vcenter = vcenter
376
+ super().__init__(vmin, vmax, clip)
377
+
378
+ def __call__(self, value: np.ndarray, clip: bool = None) -> np.ma.MaskedArray:
379
+ # Use the clip parameter from initialization if not provided
380
+ if clip is None:
381
+ clip = self.clip
382
+
383
+ x, y = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1.0]
384
+ result = np.interp(value, x, y)
385
+
386
+ # Apply clipping if requested
387
+ if clip:
388
+ result = np.clip(result, 0, 1)
389
+
390
+ return np.ma.masked_array(result)
391
+
392
+ def inverse(self, value: np.ndarray) -> np.ndarray:
393
+ y, x = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1]
394
+ return np.interp(value, x, y)
395
+
396
+
397
+ if __name__ == "__main__":
398
+ pass