openforis-whisp 2.0.0a5__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.
@@ -1,493 +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 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}")
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}")