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.
- {oafuncs-0.0.98.31/oafuncs.egg-info → oafuncs-0.0.98.32}/PKG-INFO +1 -1
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/cprogressbar.py +3 -1
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/email.py +0 -19
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_cmap.py +7 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_date.py +58 -54
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/hycom_3hourly.py +2 -2
- oafuncs-0.0.98.32/oafuncs/oa_draw.py +398 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_file.py +22 -10
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32/oafuncs.egg-info}/PKG-INFO +1 -1
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/setup.py +5 -3
- oafuncs-0.0.98.31/oafuncs/oa_draw.py +0 -401
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/LICENSE.txt +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/MANIFEST.in +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/README.md +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/__init__.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_data/hycom.png +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_data/oafuncs.png +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/data_interp.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/netcdf_merge.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/netcdf_modify.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/netcdf_write.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/parallel.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/parallel_test.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/plot_dataset.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/_script/replace_file_content.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_data.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/User_Agent-list.txt +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/__init__.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/hycom_3hourly_proxy.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/idm.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/literature.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/read_proxy.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/test_ua.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_down/user_agent.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_help.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/__init__.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/roms/__init__.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/roms/test.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/wrf/__init__.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_model/wrf/little_r.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_nc.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_python.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/__init__.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/meteorological.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/ocean.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_sign/scientific.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs/oa_tool.py +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/SOURCES.txt +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/dependency_links.txt +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/requires.txt +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/oafuncs.egg-info/top_level.txt +0 -0
- {oafuncs-0.0.98.31 → oafuncs-0.0.98.32}/setup.cfg +0 -0
@@ -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 "
|
73
|
-
Missing parts are
|
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
|
-
'
|
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
|
-
|
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
|
-
|
118
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
158
|
+
import time
|
158
159
|
time.sleep(2)
|
159
160
|
"""
|
160
161
|
|
161
|
-
def __init__(self,
|
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,
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
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
|
-
|
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
|
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} -
|
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} -
|
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
|