ncftools 0.4.0__tar.gz → 0.5.2__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.
- {ncftools-0.4.0 → ncftools-0.5.2}/PKG-INFO +6 -1
- {ncftools-0.4.0 → ncftools-0.5.2}/README.md +3 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools/__init__.py +3 -1
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools/describe.py +12 -0
- ncftools-0.5.2/ncftools/nc2shp.py +274 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools.egg-info/PKG-INFO +6 -1
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools.egg-info/SOURCES.txt +1 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools.egg-info/entry_points.txt +1 -0
- ncftools-0.5.2/ncftools.egg-info/requires.txt +4 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/pyproject.toml +4 -1
- ncftools-0.4.0/ncftools.egg-info/requires.txt +0 -2
- {ncftools-0.4.0 → ncftools-0.5.2}/MANIFEST.in +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools/cli.py +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools/meshinfo.py +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools/tests/__init__.py +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools.egg-info/dependency_links.txt +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/ncftools.egg-info/top_level.txt +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/requirements.txt +0 -0
- {ncftools-0.4.0 → ncftools-0.5.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ncftools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: A collection of tools for working with NetCDF files
|
|
5
5
|
Author-email: aaronchh <aaronhsu219@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,8 @@ Requires-Python: >=3.8
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: numpy>=1.20.0
|
|
13
13
|
Requires-Dist: netCDF4>=1.6.0
|
|
14
|
+
Requires-Dist: geopandas>=0.12.0
|
|
15
|
+
Requires-Dist: shapely>=1.8.0
|
|
14
16
|
|
|
15
17
|
# NCFTOOLS
|
|
16
18
|
|
|
@@ -31,6 +33,9 @@ pip install -e .
|
|
|
31
33
|
### Command-line
|
|
32
34
|
|
|
33
35
|
```bash
|
|
36
|
+
# Check installed version
|
|
37
|
+
meshinfo --version
|
|
38
|
+
|
|
34
39
|
# List all available commands
|
|
35
40
|
ncftools-info
|
|
36
41
|
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
NCFTOOLS - A collection of tools for working with NetCDF files.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = '0.
|
|
5
|
+
__version__ = '0.5.2'
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
'meshinfo',
|
|
9
|
+
'nc2shp',
|
|
9
10
|
'describe',
|
|
10
11
|
]
|
|
11
12
|
|
|
12
13
|
from . import meshinfo
|
|
14
|
+
from . import nc2shp
|
|
13
15
|
from . import describe
|
|
14
16
|
from . import cli
|
|
@@ -18,6 +18,18 @@ TOOL_DESCRIPTIONS = {
|
|
|
18
18
|
meshinfo -i FlowFM_net.nc # Display mesh info for a given file
|
|
19
19
|
meshinfo -i grid.nc # Any FlowFM mesh NetCDF file
|
|
20
20
|
""",
|
|
21
|
+
'nc2shp': """
|
|
22
|
+
Convert a NetCDF mesh file to ESRI Shapefiles.
|
|
23
|
+
|
|
24
|
+
Reads a UGRID-compliant NetCDF mesh file and writes two shapefiles:
|
|
25
|
+
{stem}_faces.shp one polygon per mesh face
|
|
26
|
+
{stem}_dissolved.shp single dissolved polygon of the entire mesh
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
nc2shp -i FlowFM_net.nc
|
|
30
|
+
nc2shp -i mesh.nc -o output --crs EPSG:4326
|
|
31
|
+
nc2shp -i mesh.nc -q
|
|
32
|
+
""",
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
nc2shp - Convert NetCDF mesh faces to ESRI Shapefiles
|
|
4
|
+
|
|
5
|
+
Reads a UGRID-compliant NetCDF mesh file and writes two shapefiles:
|
|
6
|
+
{stem}_faces.shp - one polygon per mesh face
|
|
7
|
+
{stem}_dissolved.shp - single dissolved polygon of the entire mesh
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import netCDF4 as nc
|
|
16
|
+
import numpy as np
|
|
17
|
+
import geopandas as gpd
|
|
18
|
+
from shapely.geometry import Polygon
|
|
19
|
+
from shapely.ops import unary_union
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_mesh_netcdf(file_path, quiet=False):
|
|
23
|
+
"""
|
|
24
|
+
Read mesh face coordinates from a UGRID NetCDF file.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
tuple: (face_x_coords, face_y_coords, node_x, node_y, face_nodes, var_names)
|
|
28
|
+
"""
|
|
29
|
+
_print(f"Reading NetCDF file: {file_path}", quiet)
|
|
30
|
+
|
|
31
|
+
dataset = nc.Dataset(file_path, 'r')
|
|
32
|
+
|
|
33
|
+
var_names = {}
|
|
34
|
+
for var_name in dataset.variables:
|
|
35
|
+
vl = var_name.lower()
|
|
36
|
+
if 'node_x' in vl:
|
|
37
|
+
var_names['node_x'] = var_name
|
|
38
|
+
elif 'node_y' in vl:
|
|
39
|
+
var_names['node_y'] = var_name
|
|
40
|
+
elif 'face_nodes' in vl:
|
|
41
|
+
var_names['face_nodes'] = var_name
|
|
42
|
+
elif 'face_x_bnd' in vl:
|
|
43
|
+
var_names['face_x_bnd'] = var_name
|
|
44
|
+
elif 'face_y_bnd' in vl:
|
|
45
|
+
var_names['face_y_bnd'] = var_name
|
|
46
|
+
|
|
47
|
+
required = ['node_x', 'node_y', 'face_nodes', 'face_x_bnd', 'face_y_bnd']
|
|
48
|
+
missing = [v for v in required if v not in var_names]
|
|
49
|
+
if missing:
|
|
50
|
+
dataset.close()
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Missing required variables: {missing}. "
|
|
53
|
+
f"Available: {list(dataset.variables.keys())}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
node_x = dataset.variables[var_names['node_x']][:]
|
|
57
|
+
node_y = dataset.variables[var_names['node_y']][:]
|
|
58
|
+
face_nodes = dataset.variables[var_names['face_nodes']][:]
|
|
59
|
+
face_x = dataset.variables[var_names['face_x_bnd']][:]
|
|
60
|
+
face_y = dataset.variables[var_names['face_y_bnd']][:]
|
|
61
|
+
dataset.close()
|
|
62
|
+
|
|
63
|
+
if not quiet:
|
|
64
|
+
print(f" Nodes: {len(node_x):,} Faces: {len(face_x):,} "
|
|
65
|
+
f"Max nodes/face: {face_x.shape[1]}")
|
|
66
|
+
|
|
67
|
+
return face_x, face_y, node_x, node_y, face_nodes, var_names
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_polygons(face_x, face_y, quiet=False):
|
|
71
|
+
"""
|
|
72
|
+
Build Shapely Polygon objects from mesh face coordinate arrays.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
list[Polygon]
|
|
76
|
+
"""
|
|
77
|
+
_print("Creating polygons from mesh faces...", quiet)
|
|
78
|
+
|
|
79
|
+
polygons = []
|
|
80
|
+
n_invalid = 0
|
|
81
|
+
tri = quad = 0
|
|
82
|
+
|
|
83
|
+
for i in range(len(face_x)):
|
|
84
|
+
coords = [
|
|
85
|
+
(float(face_x[i, j]), float(face_y[i, j]))
|
|
86
|
+
for j in range(face_x.shape[1])
|
|
87
|
+
if (not np.ma.is_masked(face_x[i, j])
|
|
88
|
+
and not np.ma.is_masked(face_y[i, j])
|
|
89
|
+
and abs(face_x[i, j]) < 1e30
|
|
90
|
+
and abs(face_y[i, j]) < 1e30)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
if len(coords) < 3:
|
|
94
|
+
n_invalid += 1
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if coords[0] != coords[-1]:
|
|
98
|
+
coords.append(coords[0])
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
poly = Polygon(coords)
|
|
102
|
+
if not poly.is_valid or poly.is_empty:
|
|
103
|
+
poly = poly.buffer(0)
|
|
104
|
+
if poly.is_valid and not poly.is_empty:
|
|
105
|
+
polygons.append(poly)
|
|
106
|
+
n = len(coords) - 1 # exclude closing duplicate
|
|
107
|
+
if n == 3:
|
|
108
|
+
tri += 1
|
|
109
|
+
elif n == 4:
|
|
110
|
+
quad += 1
|
|
111
|
+
else:
|
|
112
|
+
n_invalid += 1
|
|
113
|
+
except Exception as e:
|
|
114
|
+
n_invalid += 1
|
|
115
|
+
if not quiet:
|
|
116
|
+
print(f" Warning: face {i} skipped: {e}")
|
|
117
|
+
|
|
118
|
+
if not quiet:
|
|
119
|
+
print(f" Valid polygons: {len(polygons)} "
|
|
120
|
+
f"(triangles: {tri}, quads: {quad}, skipped: {n_invalid})")
|
|
121
|
+
|
|
122
|
+
return polygons
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_geodataframe(polygons, crs="EPSG:3826"):
|
|
126
|
+
"""Build a GeoDataFrame from a list of Shapely polygons."""
|
|
127
|
+
data = {
|
|
128
|
+
'face_id': range(len(polygons)),
|
|
129
|
+
'area': [p.area for p in polygons],
|
|
130
|
+
'type': [
|
|
131
|
+
'triangle' if len(list(p.exterior.coords)) == 4
|
|
132
|
+
else 'quadrilateral' if len(list(p.exterior.coords)) == 5
|
|
133
|
+
else 'other'
|
|
134
|
+
for p in polygons
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
return gpd.GeoDataFrame(data, geometry=polygons, crs=crs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def dissolve_geodataframe(gdf, quiet=False):
|
|
141
|
+
"""Dissolve all polygons into a single geometry."""
|
|
142
|
+
_print("Dissolving polygons...", quiet)
|
|
143
|
+
geom = unary_union(gdf.geometry.tolist())
|
|
144
|
+
dissolved = gpd.GeoDataFrame(
|
|
145
|
+
{'id': [1], 'total_area': [geom.area], 'count': [len(gdf)]},
|
|
146
|
+
geometry=[geom],
|
|
147
|
+
crs=gdf.crs,
|
|
148
|
+
)
|
|
149
|
+
_print(f" Total area: {geom.area:.2f} sq units", quiet)
|
|
150
|
+
return dissolved
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def mesh_to_shp(input_file, output_dir="SHP_NC", crs="EPSG:3826", quiet=False):
|
|
154
|
+
"""
|
|
155
|
+
Convert a NetCDF mesh file to face and dissolved shapefiles.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
input_file (str): Path to the input NetCDF file.
|
|
159
|
+
output_dir (str): Directory for output shapefiles.
|
|
160
|
+
crs (str): CRS for output shapefiles.
|
|
161
|
+
quiet (bool): Suppress non-error output.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
tuple[str, str]: Paths to (faces_shp, dissolved_shp).
|
|
165
|
+
"""
|
|
166
|
+
_print("=== NetCDF Mesh to Shapefile Converter ===", quiet)
|
|
167
|
+
|
|
168
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
169
|
+
stem = os.path.splitext(os.path.basename(input_file))[0]
|
|
170
|
+
out_faces = os.path.join(output_dir, f"{stem}_faces.shp")
|
|
171
|
+
out_dissolved = os.path.join(output_dir, f"{stem}_dissolved.shp")
|
|
172
|
+
|
|
173
|
+
face_x, face_y, node_x, node_y, face_nodes, var_names = read_mesh_netcdf(
|
|
174
|
+
input_file, quiet
|
|
175
|
+
)
|
|
176
|
+
polygons = create_polygons(face_x, face_y, quiet)
|
|
177
|
+
|
|
178
|
+
if not polygons:
|
|
179
|
+
raise RuntimeError("No valid polygons were created from the mesh.")
|
|
180
|
+
|
|
181
|
+
gdf = create_geodataframe(polygons, crs)
|
|
182
|
+
_print(f"Saving faces shapefile: {out_faces}", quiet)
|
|
183
|
+
gdf.to_file(out_faces)
|
|
184
|
+
|
|
185
|
+
dissolved = dissolve_geodataframe(gdf, quiet)
|
|
186
|
+
_print(f"Saving dissolved shapefile: {out_dissolved}", quiet)
|
|
187
|
+
dissolved.to_file(out_dissolved)
|
|
188
|
+
|
|
189
|
+
if not quiet:
|
|
190
|
+
bounds = gdf.total_bounds
|
|
191
|
+
counts = gdf['type'].value_counts()
|
|
192
|
+
print("\n=== SUMMARY ===")
|
|
193
|
+
print(f" Input: {input_file}")
|
|
194
|
+
print(f" Faces: {out_faces}")
|
|
195
|
+
print(f" Dissolved: {out_dissolved}")
|
|
196
|
+
print(f" Faces processed: {len(face_x):,} Valid: {len(polygons):,}")
|
|
197
|
+
print(f" CRS: {gdf.crs}")
|
|
198
|
+
for t, n in counts.items():
|
|
199
|
+
print(f" {t}: {n} ({n/len(gdf)*100:.1f}%)")
|
|
200
|
+
print(f" Area — min: {gdf.geometry.area.min():.2f} "
|
|
201
|
+
f"max: {gdf.geometry.area.max():.2f} "
|
|
202
|
+
f"mean: {gdf.geometry.area.mean():.2f}")
|
|
203
|
+
print(f" X: {bounds[0]:.2f} to {bounds[2]:.2f}")
|
|
204
|
+
print(f" Y: {bounds[1]:.2f} to {bounds[3]:.2f}")
|
|
205
|
+
|
|
206
|
+
return out_faces, out_dissolved
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _print(msg, quiet=False):
|
|
210
|
+
if not quiet:
|
|
211
|
+
print(msg)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
parser = argparse.ArgumentParser(
|
|
216
|
+
prog='nc2shp',
|
|
217
|
+
description='Convert a NetCDF mesh file to ESRI Shapefiles',
|
|
218
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
219
|
+
epilog="""
|
|
220
|
+
Examples:
|
|
221
|
+
nc2shp -i FlowFM_net.nc
|
|
222
|
+
nc2shp -i mesh.nc -o output --crs EPSG:4326
|
|
223
|
+
nc2shp -i mesh.nc -q
|
|
224
|
+
""",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
'-v', '--version',
|
|
229
|
+
action='version',
|
|
230
|
+
version=f'%(prog)s {importlib.metadata.version("ncftools")}',
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
'-i', '--input',
|
|
234
|
+
required=True,
|
|
235
|
+
metavar='FILE',
|
|
236
|
+
help='Path to the NetCDF mesh file',
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
'-o', '--output-dir',
|
|
240
|
+
default='SHP_NC',
|
|
241
|
+
metavar='DIR',
|
|
242
|
+
help='Output directory for shapefiles (default: SHP_NC)',
|
|
243
|
+
)
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
'--crs',
|
|
246
|
+
default='EPSG:3826',
|
|
247
|
+
help='Coordinate reference system (default: EPSG:3826)',
|
|
248
|
+
)
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
'-q', '--quiet',
|
|
251
|
+
action='store_true',
|
|
252
|
+
help='Suppress non-error output',
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
args = parser.parse_args()
|
|
256
|
+
|
|
257
|
+
if not os.path.isfile(args.input):
|
|
258
|
+
print(f"Error: File '{args.input}' not found.", file=sys.stderr)
|
|
259
|
+
sys.exit(1)
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
faces, dissolved = mesh_to_shp(
|
|
263
|
+
args.input, args.output_dir, args.crs, args.quiet
|
|
264
|
+
)
|
|
265
|
+
if args.quiet:
|
|
266
|
+
print(faces)
|
|
267
|
+
print(dissolved)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
270
|
+
sys.exit(1)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ncftools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: A collection of tools for working with NetCDF files
|
|
5
5
|
Author-email: aaronchh <aaronhsu219@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,8 @@ Requires-Python: >=3.8
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: numpy>=1.20.0
|
|
13
13
|
Requires-Dist: netCDF4>=1.6.0
|
|
14
|
+
Requires-Dist: geopandas>=0.12.0
|
|
15
|
+
Requires-Dist: shapely>=1.8.0
|
|
14
16
|
|
|
15
17
|
# NCFTOOLS
|
|
16
18
|
|
|
@@ -31,6 +33,9 @@ pip install -e .
|
|
|
31
33
|
### Command-line
|
|
32
34
|
|
|
33
35
|
```bash
|
|
36
|
+
# Check installed version
|
|
37
|
+
meshinfo --version
|
|
38
|
+
|
|
34
39
|
# List all available commands
|
|
35
40
|
ncftools-info
|
|
36
41
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ncftools"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "aaronchh", email = "aaronhsu219@gmail.com" },
|
|
10
10
|
]
|
|
@@ -15,6 +15,8 @@ requires-python = ">=3.8"
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"numpy>=1.20.0",
|
|
17
17
|
"netCDF4>=1.6.0",
|
|
18
|
+
"geopandas>=0.12.0",
|
|
19
|
+
"shapely>=1.8.0",
|
|
18
20
|
]
|
|
19
21
|
classifiers = [
|
|
20
22
|
"Programming Language :: Python :: 3",
|
|
@@ -31,3 +33,4 @@ Homepage = "https://github.com/AaronOET/ncftools"
|
|
|
31
33
|
ncftools-info = "ncftools.describe:main"
|
|
32
34
|
ncftools = "ncftools.cli:main"
|
|
33
35
|
meshinfo = "ncftools.meshinfo:main"
|
|
36
|
+
nc2shp = "ncftools.nc2shp:main"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|