ccfx 1.0.9__tar.gz → 1.1.0__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.
- {ccfx-1.0.9/ccfx.egg-info → ccfx-1.1.0}/PKG-INFO +4 -1
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx/ccfx.py +204 -3
- {ccfx-1.0.9 → ccfx-1.1.0/ccfx.egg-info}/PKG-INFO +4 -1
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx.egg-info/requires.txt +3 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/pyproject.toml +4 -1
- {ccfx-1.0.9 → ccfx-1.1.0}/LICENSE +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/MANIFEST.in +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/README.md +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx/__init__.py +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx/excel.py +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx/mssqlConnection.py +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx/sqliteConnection.py +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx/word.py +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx.egg-info/SOURCES.txt +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx.egg-info/dependency_links.txt +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/ccfx.egg-info/top_level.txt +0 -0
- {ccfx-1.0.9 → ccfx-1.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ccfx
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: This package simplifies regular common actions for quick prototyping in a user friendly way
|
|
5
5
|
Author-email: Celray James CHAWANDA <celray@chawanda.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,9 @@ Requires-Dist: mutagen
|
|
|
26
26
|
Requires-Dist: requests
|
|
27
27
|
Requires-Dist: tqdm
|
|
28
28
|
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: scipy
|
|
30
|
+
Requires-Dist: rasterio
|
|
31
|
+
Requires-Dist: matplotlib
|
|
29
32
|
Dynamic: license-file
|
|
30
33
|
|
|
31
34
|
# ccfx
|
|
@@ -22,7 +22,6 @@ import platform
|
|
|
22
22
|
import zipfile
|
|
23
23
|
import pickle
|
|
24
24
|
import time
|
|
25
|
-
from shapely.geometry import box, Point
|
|
26
25
|
import geopandas, pandas
|
|
27
26
|
from collections import defaultdict
|
|
28
27
|
import py7zr
|
|
@@ -38,6 +37,12 @@ import yt_dlp
|
|
|
38
37
|
from typing import Optional, Any
|
|
39
38
|
from datetime import datetime, timedelta
|
|
40
39
|
from PIL import Image
|
|
40
|
+
import scipy as scipy
|
|
41
|
+
from shapely.geometry import LineString, Polygon, MultiPolygon, box, Point
|
|
42
|
+
from shapely.ops import polygonize, unary_union
|
|
43
|
+
import rasterio
|
|
44
|
+
from rasterio import features
|
|
45
|
+
from rasterio.transform import from_bounds
|
|
41
46
|
|
|
42
47
|
# functions
|
|
43
48
|
def listFiles(path: str, ext: Optional[str] = None) -> list:
|
|
@@ -568,6 +573,14 @@ def correctFisheye(inputFile: str, outputFile: str = '',
|
|
|
568
573
|
subprocess.run(cmd, check=True)
|
|
569
574
|
return outputFile
|
|
570
575
|
|
|
576
|
+
def correctLens(inputFile: str, outputFile: str = '',
|
|
577
|
+
k1: float = -0.1, k2: float = 0.05,
|
|
578
|
+
cx: float = 0.5, cy: float = 0.5,
|
|
579
|
+
crf: int = 20) -> str:
|
|
580
|
+
"""
|
|
581
|
+
Alias for correctFisheye
|
|
582
|
+
"""
|
|
583
|
+
return correctFisheye(inputFile, outputFile, k1, k2, cx, cy, crf)
|
|
571
584
|
|
|
572
585
|
def formatStringBlock(input_str: str, max_chars: int = 70) -> str:
|
|
573
586
|
'''
|
|
@@ -680,6 +693,76 @@ def downloadFile(url: str, save_path: str, exists_action: str = 'resume', num_co
|
|
|
680
693
|
|
|
681
694
|
|
|
682
695
|
|
|
696
|
+
|
|
697
|
+
def createPolygonFromOuterPoints(pointsGdf: geopandas.GeoDataFrame, alpha: float = 1.6, keepHoles: bool = False) -> geopandas.GeoDataFrame:
|
|
698
|
+
"""
|
|
699
|
+
Concave hull (alpha-shape) from points.
|
|
700
|
+
alpha: larger -> tighter/more detailed; too large can fragment.
|
|
701
|
+
keepHoles: keep interior holes if True, otherwise drop them.
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
pointsGdf = pointsGdf[pointsGdf.geometry.type.eq("Point") & pointsGdf.geometry.notna()]
|
|
705
|
+
if pointsGdf.empty or pointsGdf.geometry.nunique() < 3:
|
|
706
|
+
raise ValueError("need at least three distinct points")
|
|
707
|
+
|
|
708
|
+
# coordinates (N, 2)
|
|
709
|
+
coords = numpy.array([(g.x, g.y) for g in pointsGdf.geometry])
|
|
710
|
+
|
|
711
|
+
tri = scipy.spatial.Delaunay(coords)
|
|
712
|
+
simplices = tri.simplices # (M, 3) indices into coords
|
|
713
|
+
triPts = coords[simplices] # (M, 3, 2)
|
|
714
|
+
|
|
715
|
+
# side lengths
|
|
716
|
+
a = numpy.linalg.norm(triPts[:, 1] - triPts[:, 2], axis=1)
|
|
717
|
+
b = numpy.linalg.norm(triPts[:, 0] - triPts[:, 2], axis=1)
|
|
718
|
+
c = numpy.linalg.norm(triPts[:, 0] - triPts[:, 1], axis=1)
|
|
719
|
+
|
|
720
|
+
s = (a + b + c) / 2.0
|
|
721
|
+
# heron area, guard against tiny/negative due to fp error
|
|
722
|
+
areaSq = numpy.maximum(s * (s - a) * (s - b) * (s - c), 0.0)
|
|
723
|
+
area = numpy.sqrt(areaSq)
|
|
724
|
+
valid = area > 0.0
|
|
725
|
+
if not numpy.any(valid):
|
|
726
|
+
hull = pointsGdf.unary_union.convex_hull
|
|
727
|
+
return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
|
|
728
|
+
|
|
729
|
+
circumradius = (a * b * c) / (4.0 * area)
|
|
730
|
+
keep = valid & (circumradius < (1.0 / alpha))
|
|
731
|
+
keptSimplices = simplices[keep]
|
|
732
|
+
if keptSimplices.size == 0:
|
|
733
|
+
hull = pointsGdf.unary_union.convex_hull
|
|
734
|
+
return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
|
|
735
|
+
|
|
736
|
+
# count triangle edges; boundary edges appear exactly once
|
|
737
|
+
edgeCounts: dict[tuple[int, int], int] = {}
|
|
738
|
+
for i0, i1, i2 in keptSimplices:
|
|
739
|
+
for e in ((i0, i1), (i1, i2), (i2, i0)):
|
|
740
|
+
key = (e[0], e[1]) if e[0] < e[1] else (e[1], e[0])
|
|
741
|
+
edgeCounts[key] = edgeCounts.get(key, 0) + 1
|
|
742
|
+
|
|
743
|
+
boundaryLines = [
|
|
744
|
+
LineString([coords[i], coords[j]])
|
|
745
|
+
for (i, j), count in edgeCounts.items()
|
|
746
|
+
if count == 1
|
|
747
|
+
]
|
|
748
|
+
if not boundaryLines:
|
|
749
|
+
hull = pointsGdf.unary_union.convex_hull
|
|
750
|
+
return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
|
|
751
|
+
|
|
752
|
+
polygons = list(polygonize(boundaryLines))
|
|
753
|
+
if not polygons:
|
|
754
|
+
hull = pointsGdf.unary_union.convex_hull
|
|
755
|
+
return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[hull], crs=pointsGdf.crs)
|
|
756
|
+
|
|
757
|
+
merged = unary_union(polygons)
|
|
758
|
+
if isinstance(merged, MultiPolygon):
|
|
759
|
+
merged = max(merged.geoms, key=lambda g: g.area)
|
|
760
|
+
if not keepHoles and isinstance(merged, Polygon):
|
|
761
|
+
merged = Polygon(merged.exterior)
|
|
762
|
+
|
|
763
|
+
return geopandas.GeoDataFrame({"name": ["outerBoundary"]}, geometry=[merged], crs=pointsGdf.crs)
|
|
764
|
+
|
|
765
|
+
|
|
683
766
|
def mergeRasterTiles(tileList:list, outFile:str) -> str:
|
|
684
767
|
'''
|
|
685
768
|
Merge raster tiles into one raster file
|
|
@@ -1030,6 +1113,116 @@ def extractCompressedFile(inputFile: str, outputDir: str, v: bool = False) -> No
|
|
|
1030
1113
|
"""
|
|
1031
1114
|
uncompress(inputFile, outputDir, v)
|
|
1032
1115
|
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def rasterizeGDF(gdf: geopandas.GeoDataFrame, valueField: str, outRasterFN: str, resolution: float, isCOG: bool = True, allTouched: bool = False, profileOverrides: dict | None = None) -> str:
|
|
1119
|
+
"""
|
|
1120
|
+
Rasterize a GeoDataFrame to GeoTIFF/COG.
|
|
1121
|
+
|
|
1122
|
+
Parameters
|
|
1123
|
+
----------
|
|
1124
|
+
gdf : geopandas.GeoDataFrame
|
|
1125
|
+
valueField : str
|
|
1126
|
+
Column to burn as pixel values.
|
|
1127
|
+
outRasterFN : str
|
|
1128
|
+
resolution : float
|
|
1129
|
+
Pixel size in CRS units (assumes a projected CRS).
|
|
1130
|
+
isCOG : bool, default True
|
|
1131
|
+
If True, writes Cloud-Optimized GeoTIFF. Otherwise plain GeoTIFF.
|
|
1132
|
+
allTouched : bool, default False
|
|
1133
|
+
Pass-through to rasterio.features.rasterize.
|
|
1134
|
+
profileOverrides : dict | None
|
|
1135
|
+
Extra GDAL profile options (e.g., {"compress": "LZW"}). Overrides defaults.
|
|
1136
|
+
|
|
1137
|
+
Returns
|
|
1138
|
+
-------
|
|
1139
|
+
str
|
|
1140
|
+
The path written to (outRasterFN).
|
|
1141
|
+
"""
|
|
1142
|
+
# basic checks
|
|
1143
|
+
if gdf is None or len(gdf) == 0 or gdf.geometry.isna().all():
|
|
1144
|
+
raise ValueError("gdf is empty or has no valid geometries.")
|
|
1145
|
+
if gdf.crs is None:
|
|
1146
|
+
raise ValueError("gdf must have a defined CRS.")
|
|
1147
|
+
if resolution <= 0:
|
|
1148
|
+
raise ValueError("resolution must be > 0.")
|
|
1149
|
+
if valueField not in gdf.columns:
|
|
1150
|
+
raise ValueError(f"valueField '{valueField}' not found in gdf.")
|
|
1151
|
+
|
|
1152
|
+
# compute raster shape + transform
|
|
1153
|
+
bounds = gdf.total_bounds # (minx, miny, maxx, maxy)
|
|
1154
|
+
width = int(numpy.ceil((bounds[2] - bounds[0]) / float(resolution)))
|
|
1155
|
+
height = int(numpy.ceil((bounds[3] - bounds[1]) / float(resolution)))
|
|
1156
|
+
if width < 1 or height < 1:
|
|
1157
|
+
raise ValueError("computed raster dimensions are invalid (check resolution and bounds).")
|
|
1158
|
+
|
|
1159
|
+
transform = from_bounds(bounds[0], bounds[1], bounds[2], bounds[3], width, height)
|
|
1160
|
+
|
|
1161
|
+
# infer dtype + nodata
|
|
1162
|
+
pandasDtype = gdf[valueField].dtype
|
|
1163
|
+
if numpy.issubdtype(pandasDtype, numpy.floating):
|
|
1164
|
+
dtype = numpy.float32
|
|
1165
|
+
nodata = numpy.nan
|
|
1166
|
+
fillValue = numpy.nan
|
|
1167
|
+
elif numpy.issubdtype(pandasDtype, numpy.bool_):
|
|
1168
|
+
dtype = numpy.uint8
|
|
1169
|
+
nodata = 255 # sentinel for bool raster
|
|
1170
|
+
fillValue = nodata
|
|
1171
|
+
else:
|
|
1172
|
+
dtype = numpy.int32
|
|
1173
|
+
nodata = -9999
|
|
1174
|
+
fillValue = nodata
|
|
1175
|
+
|
|
1176
|
+
# prefill target array
|
|
1177
|
+
raster = numpy.full((height, width), fillValue, dtype=dtype)
|
|
1178
|
+
|
|
1179
|
+
# build shapes generator (ensure python scalars)
|
|
1180
|
+
shapes = ((geom, (None if numpy.isnan(val) else val) if isinstance(val, float) else int(val) if numpy.issubdtype(type(val), numpy.integer) else float(val))
|
|
1181
|
+
for geom, val in zip(gdf.geometry, gdf[valueField]))
|
|
1182
|
+
|
|
1183
|
+
# burn
|
|
1184
|
+
features.rasterize(
|
|
1185
|
+
shapes=shapes,
|
|
1186
|
+
out_shape=raster.shape,
|
|
1187
|
+
transform=transform,
|
|
1188
|
+
fill=fillValue,
|
|
1189
|
+
out=raster,
|
|
1190
|
+
all_touched=allTouched,
|
|
1191
|
+
dtype=dtype
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
# default profile settings
|
|
1195
|
+
profile = {
|
|
1196
|
+
"driver": "COG" if isCOG else "GTiff",
|
|
1197
|
+
"height": raster.shape[0],
|
|
1198
|
+
"width": raster.shape[1],
|
|
1199
|
+
"count": 1,
|
|
1200
|
+
"dtype": raster.dtype,
|
|
1201
|
+
"crs": gdf.crs,
|
|
1202
|
+
"transform": transform,
|
|
1203
|
+
"nodata": nodata,
|
|
1204
|
+
}
|
|
1205
|
+
# sane compression defaults
|
|
1206
|
+
if isCOG:
|
|
1207
|
+
profile.setdefault("compress", "LZW")
|
|
1208
|
+
profile.setdefault("blocksize", 512) # GDAL COG option via rasterio
|
|
1209
|
+
# overviews are handled by the COG driver
|
|
1210
|
+
else:
|
|
1211
|
+
profile.setdefault("compress", "LZW"),
|
|
1212
|
+
profile.setdefault("tiled", True)
|
|
1213
|
+
profile.setdefault("blockxsize", 512)
|
|
1214
|
+
profile.setdefault("blockysize", 512)
|
|
1215
|
+
|
|
1216
|
+
if profileOverrides:
|
|
1217
|
+
profile.update(profileOverrides)
|
|
1218
|
+
|
|
1219
|
+
# write
|
|
1220
|
+
with rasterio.open(outRasterFN, "w", **profile) as dst:
|
|
1221
|
+
dst.write(raster, 1)
|
|
1222
|
+
|
|
1223
|
+
return outRasterFN
|
|
1224
|
+
|
|
1225
|
+
|
|
1033
1226
|
def moveDirectory(srcDir:str, destDir:str, v:bool = False) -> bool:
|
|
1034
1227
|
'''
|
|
1035
1228
|
this function moves all files from srcDir to destDir
|
|
@@ -2076,7 +2269,7 @@ def getTimeseriesStats(data:pandas.DataFrame, observed:Optional[str] = None, sim
|
|
|
2076
2269
|
|
|
2077
2270
|
return stats
|
|
2078
2271
|
|
|
2079
|
-
def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optional[int] = None, gis_id: Optional[int] = None, name: Optional[str] = None) -> Optional[pandas.DataFrame]:
|
|
2272
|
+
def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optional[int] = None, gis_id: Optional[int] = None, name: Optional[str] = None, coerceNumeric: bool = True) -> Optional[pandas.DataFrame]:
|
|
2080
2273
|
'''
|
|
2081
2274
|
Read SWAT+ output files and return a pandas DataFrame with proper date handling
|
|
2082
2275
|
and optional filtering capabilities.
|
|
@@ -2138,8 +2331,15 @@ def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optio
|
|
|
2138
2331
|
# Convert all columns to numeric except 'name' (which is string)
|
|
2139
2332
|
for col in df.columns:
|
|
2140
2333
|
if col != 'name':
|
|
2141
|
-
|
|
2334
|
+
if coerceNumeric:
|
|
2335
|
+
df[col] = pandas.to_numeric(df[col], errors='coerce')
|
|
2142
2336
|
|
|
2337
|
+
if not coerceNumeric:
|
|
2338
|
+
# If not coercing to numeric, ensure date columns are numeric
|
|
2339
|
+
for mandatoryCol in ['yr', 'mon', 'day', 'gis_id']:
|
|
2340
|
+
if mandatoryCol in df.columns:
|
|
2341
|
+
df[mandatoryCol] = pandas.to_numeric(df[mandatoryCol], errors='coerce')
|
|
2342
|
+
|
|
2143
2343
|
# Create date column from yr, mon, day
|
|
2144
2344
|
try:
|
|
2145
2345
|
df['date'] = pandas.to_datetime(pandas.DataFrame({'year': df.yr, 'month': df.mon, 'day': df.day}))
|
|
@@ -2179,4 +2379,5 @@ def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optio
|
|
|
2179
2379
|
|
|
2180
2380
|
return df
|
|
2181
2381
|
|
|
2382
|
+
|
|
2182
2383
|
ignoreWarnings()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ccfx
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: This package simplifies regular common actions for quick prototyping in a user friendly way
|
|
5
5
|
Author-email: Celray James CHAWANDA <celray@chawanda.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,9 @@ Requires-Dist: mutagen
|
|
|
26
26
|
Requires-Dist: requests
|
|
27
27
|
Requires-Dist: tqdm
|
|
28
28
|
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: scipy
|
|
30
|
+
Requires-Dist: rasterio
|
|
31
|
+
Requires-Dist: matplotlib
|
|
29
32
|
Dynamic: license-file
|
|
30
33
|
|
|
31
34
|
# ccfx
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ccfx"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "This package simplifies regular common actions for quick prototyping in a user friendly way"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -31,6 +31,9 @@ dependencies = [
|
|
|
31
31
|
"requests",
|
|
32
32
|
"tqdm",
|
|
33
33
|
"pillow",
|
|
34
|
+
"scipy",
|
|
35
|
+
"rasterio",
|
|
36
|
+
"matplotlib",
|
|
34
37
|
]
|
|
35
38
|
|
|
36
39
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|