openforis-whisp 2.0.0a4__py3-none-any.whl → 2.0.0a6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openforis_whisp/__init__.py +75 -75
- openforis_whisp/data_conversion.py +493 -371
- openforis_whisp/datasets.py +1384 -1381
- openforis_whisp/logger.py +75 -75
- openforis_whisp/parameters/__init__.py +15 -15
- openforis_whisp/parameters/config_runtime.py +44 -44
- openforis_whisp/parameters/lookup_context_and_metadata.csv +13 -13
- openforis_whisp/parameters/lookup_gee_datasets.csv +1 -1
- openforis_whisp/pd_schemas.py +77 -77
- openforis_whisp/reformat.py +495 -495
- openforis_whisp/risk.py +771 -777
- openforis_whisp/stats.py +1134 -953
- openforis_whisp/utils.py +154 -154
- {openforis_whisp-2.0.0a4.dist-info → openforis_whisp-2.0.0a6.dist-info}/LICENSE +21 -21
- {openforis_whisp-2.0.0a4.dist-info → openforis_whisp-2.0.0a6.dist-info}/METADATA +37 -46
- openforis_whisp-2.0.0a6.dist-info/RECORD +17 -0
- {openforis_whisp-2.0.0a4.dist-info → openforis_whisp-2.0.0a6.dist-info}/WHEEL +1 -1
- openforis_whisp-2.0.0a4.dist-info/RECORD +0 -17
|
@@ -1,371 +1,493 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
import geojson
|
|
3
|
-
from shapely.geometry import shape
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
# Existing imports
|
|
7
|
-
from typing import List, Any
|
|
8
|
-
from geojson import Feature, FeatureCollection, Polygon, Point
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import geopandas as gpd
|
|
12
|
-
import ee
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
gdf.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import geojson
|
|
3
|
+
from shapely.geometry import shape
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Existing imports
|
|
7
|
+
from typing import List, Any
|
|
8
|
+
from geojson import Feature, FeatureCollection, Polygon, Point
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import geopandas as gpd
|
|
12
|
+
import ee
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def convert_geojson_to_ee(
|
|
16
|
+
geojson_filepath: Any, enforce_wgs84: bool = True, strip_z_coords: bool = True
|
|
17
|
+
) -> ee.FeatureCollection:
|
|
18
|
+
"""
|
|
19
|
+
Reads a GeoJSON file from the given path and converts it to an Earth Engine FeatureCollection.
|
|
20
|
+
Optionally checks and converts the CRS to WGS 84 (EPSG:4326) if needed.
|
|
21
|
+
Automatically handles 3D coordinates by stripping Z values when necessary.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
geojson_filepath (Any): The filepath to the GeoJSON file.
|
|
25
|
+
enforce_wgs84 (bool): Whether to enforce WGS 84 projection (EPSG:4326). Defaults to True.
|
|
26
|
+
strip_z_coords (bool): Whether to automatically strip Z coordinates from 3D geometries. Defaults to True.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ee.FeatureCollection: Earth Engine FeatureCollection created from the GeoJSON.
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(geojson_filepath, (str, Path)):
|
|
32
|
+
file_path = os.path.abspath(geojson_filepath)
|
|
33
|
+
|
|
34
|
+
# Apply print_once deduplication for file reading message
|
|
35
|
+
if not hasattr(convert_geojson_to_ee, "_printed_file_messages"):
|
|
36
|
+
convert_geojson_to_ee._printed_file_messages = set()
|
|
37
|
+
|
|
38
|
+
if file_path not in convert_geojson_to_ee._printed_file_messages:
|
|
39
|
+
print(f"Reading GeoJSON file from: {file_path}")
|
|
40
|
+
convert_geojson_to_ee._printed_file_messages.add(file_path)
|
|
41
|
+
|
|
42
|
+
# Use GeoPandas to read the file and handle CRS
|
|
43
|
+
gdf = gpd.read_file(file_path)
|
|
44
|
+
|
|
45
|
+
# Check and convert CRS if needed
|
|
46
|
+
if enforce_wgs84:
|
|
47
|
+
if gdf.crs is None:
|
|
48
|
+
print("Warning: Input GeoJSON has no CRS defined, assuming WGS 84")
|
|
49
|
+
elif gdf.crs != "EPSG:4326":
|
|
50
|
+
print(f"Converting CRS from {gdf.crs} to WGS 84 (EPSG:4326)")
|
|
51
|
+
gdf = gdf.to_crs("EPSG:4326")
|
|
52
|
+
|
|
53
|
+
# Convert to GeoJSON
|
|
54
|
+
geojson_data = json.loads(gdf.to_json())
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError("Input must be a file path (str or Path)")
|
|
57
|
+
|
|
58
|
+
validation_errors = validate_geojson(geojson_data)
|
|
59
|
+
if validation_errors:
|
|
60
|
+
raise ValueError(f"GeoJSON validation errors: {validation_errors}")
|
|
61
|
+
|
|
62
|
+
# Try to create the feature collection, handle 3D coordinate issues automatically
|
|
63
|
+
try:
|
|
64
|
+
feature_collection = ee.FeatureCollection(
|
|
65
|
+
create_feature_collection(geojson_data)
|
|
66
|
+
)
|
|
67
|
+
return feature_collection
|
|
68
|
+
except ee.EEException as e:
|
|
69
|
+
if "Invalid GeoJSON geometry" in str(e) and strip_z_coords:
|
|
70
|
+
# Apply print_once deduplication for Z-coordinate stripping messages
|
|
71
|
+
if not hasattr(convert_geojson_to_ee, "_printed_z_messages"):
|
|
72
|
+
convert_geojson_to_ee._printed_z_messages = set()
|
|
73
|
+
|
|
74
|
+
z_message_key = f"z_coords_{file_path}"
|
|
75
|
+
if z_message_key not in convert_geojson_to_ee._printed_z_messages:
|
|
76
|
+
print(
|
|
77
|
+
"Warning: Invalid GeoJSON geometry detected, likely due to 3D coordinates."
|
|
78
|
+
)
|
|
79
|
+
print("Attempting to fix by stripping Z coordinates...")
|
|
80
|
+
convert_geojson_to_ee._printed_z_messages.add(z_message_key)
|
|
81
|
+
|
|
82
|
+
# Apply Z-coordinate stripping
|
|
83
|
+
geojson_data_fixed = _strip_z_coordinates_from_geojson(geojson_data)
|
|
84
|
+
|
|
85
|
+
# Try again with the fixed data
|
|
86
|
+
try:
|
|
87
|
+
feature_collection = ee.FeatureCollection(
|
|
88
|
+
create_feature_collection(geojson_data_fixed)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
success_message_key = f"z_coords_success_{file_path}"
|
|
92
|
+
if success_message_key not in convert_geojson_to_ee._printed_z_messages:
|
|
93
|
+
print("✓ Successfully converted after stripping Z coordinates")
|
|
94
|
+
convert_geojson_to_ee._printed_z_messages.add(success_message_key)
|
|
95
|
+
|
|
96
|
+
return feature_collection
|
|
97
|
+
except Exception as retry_error:
|
|
98
|
+
raise ee.EEException(
|
|
99
|
+
f"Failed to convert GeoJSON even after stripping Z coordinates: {retry_error}"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
raise e
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _strip_z_coordinates_from_geojson(geojson_data: dict) -> dict:
|
|
106
|
+
"""
|
|
107
|
+
Helper function to strip Z coordinates from GeoJSON data.
|
|
108
|
+
Converts 3D coordinates to 2D by removing Z values.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
geojson_data (dict): GeoJSON data dictionary
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
dict: GeoJSON data with Z coordinates stripped
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def strip_z(geometry):
|
|
118
|
+
"""Remove Z coordinates from geometry to make it 2D"""
|
|
119
|
+
if geometry["type"] == "MultiPolygon":
|
|
120
|
+
geometry["coordinates"] = [
|
|
121
|
+
[[[lon, lat] for lon, lat, *_ in ring] for ring in polygon]
|
|
122
|
+
for polygon in geometry["coordinates"]
|
|
123
|
+
]
|
|
124
|
+
elif geometry["type"] == "Polygon":
|
|
125
|
+
geometry["coordinates"] = [
|
|
126
|
+
[[lon, lat] for lon, lat, *_ in ring]
|
|
127
|
+
for ring in geometry["coordinates"]
|
|
128
|
+
]
|
|
129
|
+
elif geometry["type"] == "Point":
|
|
130
|
+
if len(geometry["coordinates"]) > 2:
|
|
131
|
+
geometry["coordinates"] = geometry["coordinates"][:2]
|
|
132
|
+
elif geometry["type"] == "MultiPoint":
|
|
133
|
+
geometry["coordinates"] = [coord[:2] for coord in geometry["coordinates"]]
|
|
134
|
+
elif geometry["type"] == "LineString":
|
|
135
|
+
geometry["coordinates"] = [
|
|
136
|
+
[lon, lat] for lon, lat, *_ in geometry["coordinates"]
|
|
137
|
+
]
|
|
138
|
+
elif geometry["type"] == "MultiLineString":
|
|
139
|
+
geometry["coordinates"] = [
|
|
140
|
+
[[lon, lat] for lon, lat, *_ in line]
|
|
141
|
+
for line in geometry["coordinates"]
|
|
142
|
+
]
|
|
143
|
+
return geometry
|
|
144
|
+
|
|
145
|
+
# Create a deep copy to avoid modifying the original
|
|
146
|
+
import copy
|
|
147
|
+
|
|
148
|
+
geojson_copy = copy.deepcopy(geojson_data)
|
|
149
|
+
|
|
150
|
+
# Process all features
|
|
151
|
+
if "features" in geojson_copy:
|
|
152
|
+
for feature in geojson_copy["features"]:
|
|
153
|
+
if "geometry" in feature and feature["geometry"]:
|
|
154
|
+
feature["geometry"] = strip_z(feature["geometry"])
|
|
155
|
+
|
|
156
|
+
return geojson_copy
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def convert_ee_to_geojson(ee_object, filename=None, indent=2, **kwargs):
|
|
160
|
+
"""Converts Earth Engine object to geojson.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
ee_object (object): An Earth Engine object.
|
|
164
|
+
filename (str, optional): The file path to save the geojson. Defaults to None.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
object: GeoJSON object.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if (
|
|
172
|
+
isinstance(ee_object, ee.Geometry)
|
|
173
|
+
or isinstance(ee_object, ee.Feature)
|
|
174
|
+
or isinstance(ee_object, ee.FeatureCollection)
|
|
175
|
+
):
|
|
176
|
+
json_object = ee_object.getInfo()
|
|
177
|
+
if filename is not None:
|
|
178
|
+
filename = os.path.abspath(filename)
|
|
179
|
+
if not os.path.exists(os.path.dirname(filename)):
|
|
180
|
+
os.makedirs(os.path.dirname(filename))
|
|
181
|
+
with open(filename, "w") as f:
|
|
182
|
+
f.write(json.dumps(json_object, indent=indent, **kwargs) + "\n")
|
|
183
|
+
else:
|
|
184
|
+
return json_object
|
|
185
|
+
else:
|
|
186
|
+
print("Could not convert the Earth Engine object to geojson")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
raise Exception(e)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def convert_geojson_to_shapefile(geojson_path, shapefile_output_path):
|
|
192
|
+
"""
|
|
193
|
+
Convert a GeoJSON file to a Shapefile and save it to a file.
|
|
194
|
+
|
|
195
|
+
Parameters:
|
|
196
|
+
- geojson_path: Path to the GeoJSON file.
|
|
197
|
+
- shapefile_output_path: Path where the Shapefile will be saved.
|
|
198
|
+
"""
|
|
199
|
+
with open(geojson_path, "r") as geojson_file:
|
|
200
|
+
geojson = json.load(geojson_file)
|
|
201
|
+
|
|
202
|
+
gdf = gpd.GeoDataFrame.from_features(geojson["features"])
|
|
203
|
+
|
|
204
|
+
gdf.to_file(shapefile_output_path, driver="ESRI Shapefile")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def convert_shapefile_to_geojson(shapefile_path, geojson_output_path):
|
|
208
|
+
"""
|
|
209
|
+
Convert a Shapefile to GeoJSON and save it to a file.
|
|
210
|
+
|
|
211
|
+
Parameters:
|
|
212
|
+
- shapefile_path: Path to the Shapefile.
|
|
213
|
+
- geojson_output_path: Path where the GeoJSON file will be saved.
|
|
214
|
+
"""
|
|
215
|
+
gdf = gpd.read_file(shapefile_path)
|
|
216
|
+
|
|
217
|
+
geojson_str = gdf.to_json()
|
|
218
|
+
|
|
219
|
+
with open(geojson_output_path, "w") as geojson_file:
|
|
220
|
+
geojson_file.write(geojson_str)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def convert_shapefile_to_ee(shapefile_path):
|
|
224
|
+
"""
|
|
225
|
+
Convert a zipped shapefile to an Earth Engine FeatureCollection.
|
|
226
|
+
NB Making this as existing Geemap function shp_to_ee wouldnt work.
|
|
227
|
+
Args:
|
|
228
|
+
- shapefile_path (str): Path to the shapefile (.zip) to be converted.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
- ee.FeatureCollection: Earth Engine FeatureCollection created from the shapefile.
|
|
232
|
+
"""
|
|
233
|
+
gdf = gpd.read_file(shapefile_path)
|
|
234
|
+
|
|
235
|
+
geo_json = gdf.to_json()
|
|
236
|
+
|
|
237
|
+
roi = ee.FeatureCollection(json.loads(geo_json))
|
|
238
|
+
|
|
239
|
+
return roi
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def convert_ee_to_df(
|
|
243
|
+
ee_object,
|
|
244
|
+
columns=None,
|
|
245
|
+
remove_geom=False,
|
|
246
|
+
sort_columns=False,
|
|
247
|
+
**kwargs,
|
|
248
|
+
):
|
|
249
|
+
"""Converts an ee.FeatureCollection to pandas dataframe.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
ee_object (ee.FeatureCollection): ee.FeatureCollection.
|
|
253
|
+
columns (list): List of column names. Defaults to None.
|
|
254
|
+
remove_geom (bool): Whether to remove the geometry column. Defaults to True.
|
|
255
|
+
sort_columns (bool): Whether to sort the column names. Defaults to False.
|
|
256
|
+
kwargs: Additional arguments passed to ee.data.computeFeature.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
TypeError: ee_object must be an ee.FeatureCollection
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
pd.DataFrame: pandas DataFrame
|
|
263
|
+
"""
|
|
264
|
+
if isinstance(ee_object, ee.Feature):
|
|
265
|
+
ee_object = ee.FeatureCollection([ee_object])
|
|
266
|
+
|
|
267
|
+
if not isinstance(ee_object, ee.FeatureCollection):
|
|
268
|
+
raise TypeError("ee_object must be an ee.FeatureCollection")
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
if remove_geom:
|
|
272
|
+
data = ee_object.map(
|
|
273
|
+
lambda f: ee.Feature(None, f.toDictionary(f.propertyNames().sort()))
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
data = ee_object
|
|
277
|
+
|
|
278
|
+
kwargs["expression"] = data
|
|
279
|
+
kwargs["fileFormat"] = "PANDAS_DATAFRAME"
|
|
280
|
+
|
|
281
|
+
df = ee.data.computeFeatures(kwargs)
|
|
282
|
+
|
|
283
|
+
if isinstance(columns, list):
|
|
284
|
+
df = df[columns]
|
|
285
|
+
|
|
286
|
+
if remove_geom and ("geometry" in df.columns):
|
|
287
|
+
df = df.drop(columns=["geometry"], axis=1)
|
|
288
|
+
|
|
289
|
+
if sort_columns:
|
|
290
|
+
df = df.reindex(sorted(df.columns), axis=1)
|
|
291
|
+
|
|
292
|
+
return df
|
|
293
|
+
except Exception as e:
|
|
294
|
+
raise Exception(e)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def convert_ee_to_shapefile(feature_collection, shapefile_path):
|
|
298
|
+
"""
|
|
299
|
+
Export an Earth Engine FeatureCollection to a shapefile.
|
|
300
|
+
|
|
301
|
+
Parameters:
|
|
302
|
+
- feature_collection: Earth Engine FeatureCollection to be exported.
|
|
303
|
+
- shapefile_path: Path to save the shapefile.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
- Path to the saved shapefile.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
geojson = convert_ee_to_geojson(feature_collection)
|
|
310
|
+
|
|
311
|
+
features = geojson["features"]
|
|
312
|
+
geoms = [shape(feature["geometry"]) for feature in features]
|
|
313
|
+
properties = [feature["properties"] for feature in features]
|
|
314
|
+
gdf = gpd.GeoDataFrame(properties, geometry=geoms)
|
|
315
|
+
|
|
316
|
+
gdf.to_file(shapefile_path, driver="ESRI Shapefile")
|
|
317
|
+
|
|
318
|
+
print(f"Shapefile saved to {shapefile_path}")
|
|
319
|
+
|
|
320
|
+
return shapefile_path
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def validate_geojson(input_data: Any) -> List[str]:
|
|
324
|
+
"""
|
|
325
|
+
Validates GeoJSON data and filters out certain non-critical errors.
|
|
326
|
+
|
|
327
|
+
:param input_data: GeoJSON data as a string, dict, or a file path
|
|
328
|
+
:return: List of validation errors
|
|
329
|
+
"""
|
|
330
|
+
errors = []
|
|
331
|
+
|
|
332
|
+
if isinstance(input_data, (str, Path)):
|
|
333
|
+
try:
|
|
334
|
+
with open(input_data, "r") as f:
|
|
335
|
+
geojson_data = f.read()
|
|
336
|
+
geojson_obj = json.loads(geojson_data)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
errors.append(f"Error reading file: {e}")
|
|
339
|
+
return errors
|
|
340
|
+
elif isinstance(input_data, dict):
|
|
341
|
+
geojson_obj = input_data
|
|
342
|
+
else:
|
|
343
|
+
geojson_data = input_data
|
|
344
|
+
try:
|
|
345
|
+
geojson_obj = json.loads(geojson_data)
|
|
346
|
+
except ValueError as e:
|
|
347
|
+
errors.append(f"Invalid GeoJSON: {e}")
|
|
348
|
+
return errors
|
|
349
|
+
|
|
350
|
+
if "type" not in geojson_obj:
|
|
351
|
+
errors.append("Missing 'type' field in GeoJSON.")
|
|
352
|
+
|
|
353
|
+
return errors
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def extract_features(geojson_obj: Any, features: List[Feature]) -> None:
|
|
357
|
+
"""
|
|
358
|
+
Recursively extracts features from a GeoJSON object and adds them to the feature list.
|
|
359
|
+
|
|
360
|
+
:param geojson_obj: GeoJSON object (could be geometry, feature, or feature collection)
|
|
361
|
+
:param features: List of extracted features
|
|
362
|
+
"""
|
|
363
|
+
if isinstance(geojson_obj, dict):
|
|
364
|
+
obj_type = geojson_obj.get("type")
|
|
365
|
+
|
|
366
|
+
if obj_type == "Feature":
|
|
367
|
+
# Extract the actual Feature with properties
|
|
368
|
+
geometry = geojson_obj.get("geometry", {})
|
|
369
|
+
properties = geojson_obj.get("properties", {})
|
|
370
|
+
|
|
371
|
+
if geometry and geometry.get("type"):
|
|
372
|
+
features.append(Feature(geometry=geometry, properties=properties))
|
|
373
|
+
|
|
374
|
+
elif obj_type == "FeatureCollection":
|
|
375
|
+
# Process each feature in the collection
|
|
376
|
+
for feature in geojson_obj.get("features", []):
|
|
377
|
+
extract_features(feature, features)
|
|
378
|
+
|
|
379
|
+
elif obj_type in [
|
|
380
|
+
"Polygon",
|
|
381
|
+
"Point",
|
|
382
|
+
"MultiPolygon",
|
|
383
|
+
"LineString",
|
|
384
|
+
"MultiPoint",
|
|
385
|
+
"MultiLineString",
|
|
386
|
+
]:
|
|
387
|
+
# This is a raw geometry - create feature with empty properties
|
|
388
|
+
features.append(Feature(geometry=geojson_obj, properties={}))
|
|
389
|
+
|
|
390
|
+
elif obj_type == "GeometryCollection":
|
|
391
|
+
# Handle geometry collections
|
|
392
|
+
for geom in geojson_obj.get("geometries", []):
|
|
393
|
+
extract_features(geom, features)
|
|
394
|
+
|
|
395
|
+
elif isinstance(geojson_obj, list):
|
|
396
|
+
# Handle lists of features/geometries
|
|
397
|
+
for item in geojson_obj:
|
|
398
|
+
extract_features(item, features)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def create_feature_collection(geojson_obj: Any) -> FeatureCollection:
|
|
402
|
+
"""
|
|
403
|
+
Creates a FeatureCollection from a GeoJSON object.
|
|
404
|
+
|
|
405
|
+
:param geojson_obj: GeoJSON object
|
|
406
|
+
:return: GeoJSON FeatureCollection
|
|
407
|
+
"""
|
|
408
|
+
features = []
|
|
409
|
+
extract_features(geojson_obj, features)
|
|
410
|
+
return FeatureCollection(features)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def convert_csv_to_geojson(
|
|
414
|
+
csv_filepath: str, geojson_filepath: str, geo_column: str = "geo"
|
|
415
|
+
):
|
|
416
|
+
"""
|
|
417
|
+
Convert a CSV file with a geo column into a GeoJSON file.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
csv_filepath : str
|
|
422
|
+
The filepath to the input CSV file.
|
|
423
|
+
geojson_filepath : str
|
|
424
|
+
The filepath to save the output GeoJSON file.
|
|
425
|
+
geo_column : str, optional
|
|
426
|
+
The name of the column containing GeoJSON geometries, by default "geo".
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
None
|
|
431
|
+
"""
|
|
432
|
+
try:
|
|
433
|
+
df = pd.read_csv(csv_filepath)
|
|
434
|
+
|
|
435
|
+
df_to_geojson(df, geojson_filepath, geo_column)
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
print(f"An error occurred while converting CSV to GeoJSON: {e}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def convert_df_to_geojson(
|
|
442
|
+
df: pd.DataFrame, geojson_filepath: str, geo_column: str = "geo"
|
|
443
|
+
):
|
|
444
|
+
"""
|
|
445
|
+
Convert a DataFrame with a geo column into a GeoJSON file.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
df : pd.DataFrame
|
|
450
|
+
The DataFrame containing the data.
|
|
451
|
+
geojson_filepath : str
|
|
452
|
+
The filepath to save the output GeoJSON file.
|
|
453
|
+
geo_column : str, optional
|
|
454
|
+
The name of the column containing GeoJSON geometries, by default "geo".
|
|
455
|
+
|
|
456
|
+
Returns
|
|
457
|
+
-------
|
|
458
|
+
None
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
if geo_column not in df.columns:
|
|
462
|
+
raise ValueError(f"Geo column '{geo_column}' not found in the DataFrame.")
|
|
463
|
+
|
|
464
|
+
features = []
|
|
465
|
+
|
|
466
|
+
for index, row in df.iterrows():
|
|
467
|
+
try:
|
|
468
|
+
geojson_str = row[geo_column]
|
|
469
|
+
|
|
470
|
+
geojson_str = geojson_str.replace("'", '"')
|
|
471
|
+
|
|
472
|
+
geometry = geojson.loads(geojson_str)
|
|
473
|
+
|
|
474
|
+
properties = row.drop(geo_column).to_dict()
|
|
475
|
+
properties = {
|
|
476
|
+
k: (v if pd.notna(v) else None) for k, v in properties.items()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
feature = geojson.Feature(geometry=geometry, properties=properties)
|
|
480
|
+
|
|
481
|
+
features.append(feature)
|
|
482
|
+
except Exception as e:
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
feature_collection = geojson.FeatureCollection(features)
|
|
486
|
+
|
|
487
|
+
with open(geojson_filepath, "w") as f:
|
|
488
|
+
geojson.dump(feature_collection, f, indent=2)
|
|
489
|
+
|
|
490
|
+
print(f"GeoJSON saved to {geojson_filepath}")
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
print(f"An error occurred while converting DataFrame to GeoJSON: {e}")
|