geoai-py 0.4.3__py2.py3-none-any.whl → 0.5.0__py2.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.
geoai/download.py CHANGED
@@ -207,98 +207,130 @@ def json_serializable(obj: Any) -> Any:
207
207
  return obj
208
208
 
209
209
 
210
+ def get_overture_latest_release(patch=True) -> str:
211
+ """
212
+ Retrieves the value of the 'latest' key from the Overture Maps release JSON file.
213
+
214
+ Args:
215
+ patch (bool): If True, returns the full version string (e.g., "2025-02-19.0").
216
+
217
+ Returns:
218
+ str: The value of the 'latest' key from the releases.json file.
219
+
220
+ Raises:
221
+ requests.RequestException: If there's an issue with the HTTP request.
222
+ KeyError: If the 'latest' key is not found in the JSON data.
223
+ json.JSONDecodeError: If the response cannot be parsed as JSON.
224
+ """
225
+ url = "https://labs.overturemaps.org/data/releases.json"
226
+
227
+ try:
228
+ response = requests.get(url)
229
+ response.raise_for_status() # Raise an exception for HTTP errors
230
+
231
+ data = response.json()
232
+ if patch:
233
+ latest_release = data.get("latest")
234
+ else:
235
+ latest_release = data.get("latest").split(".")[
236
+ 0
237
+ ] # Extract the version number
238
+
239
+ if latest_release is None:
240
+ raise KeyError("The 'latest' key was not found in the releases.json file")
241
+
242
+ return latest_release
243
+
244
+ except requests.RequestException as e:
245
+ print(f"Error making the request: {e}")
246
+ raise
247
+ except json.JSONDecodeError as e:
248
+ print(f"Error parsing JSON response: {e}")
249
+ raise
250
+ except KeyError as e:
251
+ print(f"Key error: {e}")
252
+ raise
253
+
254
+
255
+ def get_all_overture_types():
256
+ """Get a list of all available Overture Maps data types.
257
+
258
+ Returns:
259
+ list: List of available Overture Maps data types.
260
+ """
261
+ from overturemaps import core
262
+
263
+ return core.get_all_overture_types()
264
+
265
+
210
266
  def download_overture_buildings(
211
267
  bbox: Tuple[float, float, float, float],
212
- output_file: str,
213
- output_format: str = "geojson",
214
- data_type: str = "building",
215
- verbose: bool = True,
268
+ output: str,
269
+ overture_type: str = "building",
270
+ **kwargs: Any,
216
271
  ) -> str:
217
272
  """Download building data from Overture Maps for a given bounding box using the overturemaps CLI tool.
218
273
 
219
274
  Args:
220
275
  bbox: Bounding box in the format (min_lon, min_lat, max_lon, max_lat) in WGS84 coordinates.
221
- output_file: Path to save the output file.
222
- output_format: Format to save the output, one of "geojson", "geojsonseq", or "geoparquet".
223
- data_type: The Overture Maps data type to download (building, place, etc.).
224
- verbose: Whether to print verbose output.
276
+ output: Path to save the output file.
277
+ overture_type: The Overture Maps data type to download (building, place, etc.).
225
278
 
226
279
  Returns:
227
280
  Path to the output file.
228
281
  """
229
- # Create output directory if needed
230
- output_dir = os.path.dirname(output_file)
231
- if output_dir and not os.path.exists(output_dir):
232
- os.makedirs(output_dir, exist_ok=True)
233
-
234
- # Format the bounding box string for the command
235
- west, south, east, north = bbox
236
- bbox_str = f"{west},{south},{east},{north}"
237
-
238
- # Build the command
239
- cmd = [
240
- "overturemaps",
241
- "download",
242
- "--bbox",
243
- bbox_str,
244
- "-f",
245
- output_format,
246
- "--type",
247
- data_type,
248
- "--output",
249
- output_file,
250
- ]
251
-
252
- if verbose:
253
- logger.info(f"Running command: {' '.join(cmd)}")
254
- logger.info("Downloading %s data for area: %s", data_type, bbox_str)
282
+
283
+ return get_overture_data(
284
+ overture_type=overture_type, bbox=bbox, output=output, **kwargs
285
+ )
286
+
287
+
288
+ def get_overture_data(
289
+ overture_type: str,
290
+ bbox: Tuple[float, float, float, float] = None,
291
+ columns: List[str] = None,
292
+ output: str = None,
293
+ **kwargs: Any,
294
+ ) -> "gpd.GeoDataFrame":
295
+ """Fetches overture data and returns it as a GeoDataFrame.
296
+
297
+ Args:
298
+ overture_type (str): The type of overture data to fetch.It can be one of the following:
299
+ address|building|building_part|division|division_area|division_boundary|place|
300
+ segment|connector|infrastructure|land|land_cover|land_use|water
301
+ bbox (Tuple[float, float, float, float], optional): The bounding box to
302
+ filter the data. Defaults to None.
303
+ columns (List[str], optional): The columns to include in the output.
304
+ Defaults to None.
305
+ output (str, optional): The file path to save the output GeoDataFrame.
306
+ Defaults to None.
307
+
308
+ Returns:
309
+ gpd.GeoDataFrame: The fetched overture data as a GeoDataFrame.
310
+
311
+ Raises:
312
+ ImportError: If the overture package is not installed.
313
+ """
255
314
 
256
315
  try:
257
- # Run the command
258
- result = subprocess.run(
259
- cmd,
260
- check=True,
261
- stdout=subprocess.PIPE if not verbose else None,
262
- stderr=subprocess.PIPE,
263
- text=True,
264
- )
316
+ from overturemaps import core
317
+ except ImportError:
318
+ raise ImportError("The overturemaps package is required to use this function")
265
319
 
266
- # Check if the file was created
267
- if os.path.exists(output_file):
268
- file_size = os.path.getsize(output_file) / (1024 * 1024) # Size in MB
269
- logger.info(
270
- f"Successfully downloaded data to {output_file} ({file_size:.2f} MB)"
271
- )
320
+ gdf = core.geodataframe(overture_type, bbox=bbox)
321
+ if columns is not None:
322
+ gdf = gdf[columns]
272
323
 
273
- # Optionally show some stats about the downloaded data
274
- if output_format == "geojson" and os.path.getsize(output_file) > 0:
275
- try:
276
- gdf = gpd.read_file(output_file)
277
- logger.info(f"Downloaded {len(gdf)} features")
278
-
279
- if len(gdf) > 0 and verbose:
280
- # Show a sample of the attribute names
281
- attrs = list(gdf.columns)
282
- attrs.remove("geometry")
283
- logger.info(f"Available attributes: {', '.join(attrs[:10])}...")
284
- except Exception as e:
285
- logger.warning(f"Could not read the GeoJSON file: {str(e)}")
324
+ gdf.crs = "EPSG:4326"
286
325
 
287
- return output_file
288
- else:
289
- logger.error(f"Command completed but file {output_file} was not created")
290
- if result.stderr:
291
- logger.error(f"Command error output: {result.stderr}")
292
- return None
293
-
294
- except subprocess.CalledProcessError as e:
295
- logger.error(f"Error running overturemaps command: {str(e)}")
296
- if e.stderr:
297
- logger.error(f"Command error output: {e.stderr}")
298
- raise RuntimeError(f"Failed to download Overture Maps data: {str(e)}")
299
- except Exception as e:
300
- logger.error(f"Unexpected error: {str(e)}")
301
- raise
326
+ out_dir = os.path.dirname(os.path.abspath(output))
327
+ if not os.path.exists(out_dir):
328
+ os.makedirs(out_dir, exist_ok=True)
329
+
330
+ if output is not None:
331
+ gdf.to_file(output, **kwargs)
332
+
333
+ return gdf
302
334
 
303
335
 
304
336
  def convert_vector_format(
@@ -361,18 +393,23 @@ def convert_vector_format(
361
393
  raise
362
394
 
363
395
 
364
- def extract_building_stats(geojson_file: str) -> Dict[str, Any]:
396
+ def extract_building_stats(data: str) -> Dict[str, Any]:
365
397
  """Extract statistics from the building data.
366
398
 
367
399
  Args:
368
- geojson_file: Path to the GeoJSON file.
400
+ data: Path to the GeoJSON file or GeoDataFrame containing building data.
369
401
 
370
402
  Returns:
371
403
  Dictionary with statistics.
372
404
  """
373
405
  try:
374
406
  # Read the GeoJSON file
375
- gdf = gpd.read_file(geojson_file)
407
+
408
+ if isinstance(data, gpd.GeoDataFrame):
409
+ gdf = data
410
+ else:
411
+
412
+ gdf = gpd.read_file(data)
376
413
 
377
414
  # Calculate statistics
378
415
  bbox = gdf.total_bounds.tolist()
@@ -903,7 +940,7 @@ def pc_stac_download(
903
940
  from concurrent.futures import ThreadPoolExecutor, as_completed
904
941
 
905
942
  # Handle single item case
906
- if isinstance(items, pystac.Item):
943
+ if isinstance(items, pystac.Item) or isinstance(items, str):
907
944
  items = [items]
908
945
  elif not isinstance(items, list):
909
946
  raise TypeError("items must be a STAC Item or list of STAC Items")
@@ -973,6 +1010,8 @@ def pc_stac_download(
973
1010
 
974
1011
  for item in items:
975
1012
  item_assets = {}
1013
+ if isinstance(item, str):
1014
+ item = pystac.Item.from_file(item)
976
1015
  item_id = item.id
977
1016
  print(f"Processing STAC item: {item_id}")
978
1017
 
geoai/geoai.py CHANGED
@@ -13,6 +13,7 @@ from .download import (
13
13
  read_pc_item_asset,
14
14
  view_pc_item,
15
15
  )
16
+ from .classify import train_classifier, classify_image, classify_images
16
17
  from .extract import *
17
18
  from .hf import *
18
19
  from .segment import *
geoai/utils.py CHANGED
@@ -6053,3 +6053,165 @@ def try_common_architectures(state_dict):
6053
6053
 
6054
6054
  except Exception as e:
6055
6055
  print(f"- {name}: Failed to load - {str(e)}")
6056
+
6057
+
6058
+ def mosaic_geotiffs(input_dir, output_file, mask_file=None):
6059
+ """Create a mosaic from all GeoTIFF files as a Cloud Optimized GeoTIFF (COG).
6060
+
6061
+ This function identifies all GeoTIFF files in the specified directory,
6062
+ creates a seamless mosaic with proper handling of nodata values, and saves
6063
+ as a Cloud Optimized GeoTIFF format. If a mask file is provided, the output
6064
+ will be clipped to the extent of the mask.
6065
+
6066
+ Args:
6067
+ input_dir (str): Path to the directory containing GeoTIFF files.
6068
+ output_file (str): Path to the output Cloud Optimized GeoTIFF file.
6069
+ mask_file (str, optional): Path to a mask file to clip the output.
6070
+ If provided, the output will be clipped to the extent of this mask.
6071
+ Defaults to None.
6072
+
6073
+ Returns:
6074
+ bool: True if the mosaic was created successfully, False otherwise.
6075
+
6076
+ Examples:
6077
+ >>> mosaic_geotiffs('naip', 'merged_naip.tif')
6078
+ True
6079
+ >>> mosaic_geotiffs('naip', 'merged_naip.tif', 'boundary.tif')
6080
+ True
6081
+ """
6082
+ import glob
6083
+ from osgeo import gdal
6084
+
6085
+ gdal.UseExceptions()
6086
+ # Get all tif files in the directory
6087
+ tif_files = glob.glob(os.path.join(input_dir, "*.tif"))
6088
+
6089
+ if not tif_files:
6090
+ print("No GeoTIFF files found in the specified directory.")
6091
+ return False
6092
+
6093
+ # Analyze the first input file to determine compression and nodata settings
6094
+ ds = gdal.Open(tif_files[0])
6095
+ if ds is None:
6096
+ print(f"Unable to open {tif_files[0]}")
6097
+ return False
6098
+
6099
+ # Get driver metadata from the first file
6100
+ driver = ds.GetDriver()
6101
+ creation_options = []
6102
+
6103
+ # Check compression type
6104
+ metadata = ds.GetMetadata("IMAGE_STRUCTURE")
6105
+ if "COMPRESSION" in metadata:
6106
+ compression = metadata["COMPRESSION"]
6107
+ creation_options.append(f"COMPRESS={compression}")
6108
+ else:
6109
+ # Default compression if none detected
6110
+ creation_options.append("COMPRESS=LZW")
6111
+
6112
+ # Add COG-specific creation options
6113
+ creation_options.extend(["TILED=YES", "BLOCKXSIZE=512", "BLOCKYSIZE=512"])
6114
+
6115
+ # Check for nodata value in the first band of the first file
6116
+ band = ds.GetRasterBand(1)
6117
+ has_nodata = band.GetNoDataValue() is not None
6118
+ nodata_value = band.GetNoDataValue() if has_nodata else None
6119
+
6120
+ # Close the dataset
6121
+ ds = None
6122
+
6123
+ # Create a temporary VRT (Virtual Dataset)
6124
+ vrt_path = os.path.join(input_dir, "temp_mosaic.vrt")
6125
+
6126
+ # Build VRT from input files with proper nodata handling
6127
+ vrt_options = gdal.BuildVRTOptions(
6128
+ resampleAlg="nearest",
6129
+ srcNodata=nodata_value if has_nodata else None,
6130
+ VRTNodata=nodata_value if has_nodata else None,
6131
+ )
6132
+ vrt_dataset = gdal.BuildVRT(vrt_path, tif_files, options=vrt_options)
6133
+
6134
+ # Close the VRT dataset to flush it to disk
6135
+ vrt_dataset = None
6136
+
6137
+ # Create temp mosaic
6138
+ temp_mosaic = output_file + ".temp.tif"
6139
+
6140
+ # Convert VRT to GeoTIFF with the same compression as input
6141
+ translate_options = gdal.TranslateOptions(
6142
+ format="GTiff",
6143
+ creationOptions=creation_options,
6144
+ noData=nodata_value if has_nodata else None,
6145
+ )
6146
+ gdal.Translate(temp_mosaic, vrt_path, options=translate_options)
6147
+
6148
+ # Apply mask if provided
6149
+ if mask_file and os.path.exists(mask_file):
6150
+ print(f"Clipping mosaic to mask: {mask_file}")
6151
+
6152
+ # Create a temporary clipped file
6153
+ clipped_mosaic = output_file + ".clipped.tif"
6154
+
6155
+ # Open mask file
6156
+ mask_ds = gdal.Open(mask_file)
6157
+ if mask_ds is None:
6158
+ print(f"Unable to open mask file: {mask_file}")
6159
+ # Continue without clipping
6160
+ else:
6161
+ # Get mask extent
6162
+ mask_geotransform = mask_ds.GetGeoTransform()
6163
+ mask_projection = mask_ds.GetProjection()
6164
+ mask_ulx = mask_geotransform[0]
6165
+ mask_uly = mask_geotransform[3]
6166
+ mask_lrx = mask_ulx + (mask_geotransform[1] * mask_ds.RasterXSize)
6167
+ mask_lry = mask_uly + (mask_geotransform[5] * mask_ds.RasterYSize)
6168
+
6169
+ # Close mask dataset
6170
+ mask_ds = None
6171
+
6172
+ # Use warp options to clip
6173
+ warp_options = gdal.WarpOptions(
6174
+ format="GTiff",
6175
+ outputBounds=[mask_ulx, mask_lry, mask_lrx, mask_uly],
6176
+ dstSRS=mask_projection,
6177
+ creationOptions=creation_options,
6178
+ srcNodata=nodata_value if has_nodata else None,
6179
+ dstNodata=nodata_value if has_nodata else None,
6180
+ )
6181
+
6182
+ # Apply clipping
6183
+ gdal.Warp(clipped_mosaic, temp_mosaic, options=warp_options)
6184
+
6185
+ # Remove the unclipped temp mosaic and use the clipped one
6186
+ os.remove(temp_mosaic)
6187
+ temp_mosaic = clipped_mosaic
6188
+
6189
+ # Create internal overviews for the temp mosaic
6190
+ ds = gdal.Open(temp_mosaic, gdal.GA_Update)
6191
+ overview_list = [2, 4, 8, 16, 32]
6192
+ ds.BuildOverviews("NEAREST", overview_list)
6193
+ ds = None # Close the dataset to ensure overviews are written
6194
+
6195
+ # Convert the temp mosaic to a proper COG
6196
+ cog_options = gdal.TranslateOptions(
6197
+ format="GTiff",
6198
+ creationOptions=[
6199
+ "TILED=YES",
6200
+ "COPY_SRC_OVERVIEWS=YES",
6201
+ "COMPRESS=DEFLATE",
6202
+ "PREDICTOR=2",
6203
+ "BLOCKXSIZE=512",
6204
+ "BLOCKYSIZE=512",
6205
+ ],
6206
+ noData=nodata_value if has_nodata else None,
6207
+ )
6208
+ gdal.Translate(output_file, temp_mosaic, options=cog_options)
6209
+
6210
+ # Clean up temporary files
6211
+ if os.path.exists(vrt_path):
6212
+ os.remove(vrt_path)
6213
+ if os.path.exists(temp_mosaic):
6214
+ os.remove(temp_mosaic)
6215
+
6216
+ print(f"Cloud Optimized GeoTIFF mosaic created successfully: {output_file}")
6217
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geoai-py
3
- Version: 0.4.3
3
+ Version: 0.5.0
4
4
  Summary: A Python package for using Artificial Intelligence (AI) with geospatial data
5
5
  Author-email: Qiusheng Wu <giswqs@gmail.com>
6
6
  License: MIT License
@@ -0,0 +1,16 @@
1
+ geoai/__init__.py,sha256=jGJl23LMoJDPdWTrlWAPxXK8cjwWllGQcFQ5IJOJ0s0,3765
2
+ geoai/classify.py,sha256=_e-193QzAx3pIxUflPIsIs1qZevQx5ADu7i3bOL1G70,35055
3
+ geoai/download.py,sha256=lJ1GsJOZsKc2i6_dQyPV-XXIXmlADOpmSBo-wha4DEU,40892
4
+ geoai/extract.py,sha256=GocJufMmrwEWxNBL1J91EXXHL8AKcO8m_lmtUF5AKPw,119102
5
+ geoai/geoai.py,sha256=5zXt7WQ0FbiksXKQ9fBNnfa9dhJ3QVd8LXicMxyynTg,698
6
+ geoai/hf.py,sha256=mLKGxEAS5eHkxZLwuLpYc1o7e3-7QIXdBv-QUY-RkFk,17072
7
+ geoai/segment.py,sha256=g3YW17ftr--CKq6VB32TJEPY8owGQ7uQ0sg_tUT2ooE,13681
8
+ geoai/segmentation.py,sha256=AtPzCvguHAEeuyXafa4bzMFATvltEYcah1B8ZMfkM_s,11373
9
+ geoai/train.py,sha256=-l2j1leTxDnFDLaBslu1q6CobXjm3LEdiQwUWOU8P6M,40088
10
+ geoai/utils.py,sha256=xxhIkNFFtv-s_tzrlAyD4qIOaxo2JtKJEzG6vFgA5y8,238737
11
+ geoai_py-0.5.0.dist-info/licenses/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
12
+ geoai_py-0.5.0.dist-info/METADATA,sha256=tzHz2Q1iTpCO25T8W3N2RtyaHdwdb9umS9YIgCciFdo,6049
13
+ geoai_py-0.5.0.dist-info/WHEEL,sha256=MAQBAzGbXNI3bUmkDsiV_duv8i-gcdnLzw7cfUFwqhU,109
14
+ geoai_py-0.5.0.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
15
+ geoai_py-0.5.0.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
16
+ geoai_py-0.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- geoai/__init__.py,sha256=b12E2HztHEPaaKMVKpk6GPjC7ElUMxOW9pYKl4VZkmE,3592
2
- geoai/download.py,sha256=BvCEpcBwZlEOWieixsdyvQDiE0CXRjU7oayLmy5_Dgs,40110
3
- geoai/extract.py,sha256=GocJufMmrwEWxNBL1J91EXXHL8AKcO8m_lmtUF5AKPw,119102
4
- geoai/geoai.py,sha256=BqKdWzNruDdGqwqoyTaJzUq4lKGj-RDBZlSO3t3-GxQ,626
5
- geoai/hf.py,sha256=mLKGxEAS5eHkxZLwuLpYc1o7e3-7QIXdBv-QUY-RkFk,17072
6
- geoai/segment.py,sha256=g3YW17ftr--CKq6VB32TJEPY8owGQ7uQ0sg_tUT2ooE,13681
7
- geoai/segmentation.py,sha256=AtPzCvguHAEeuyXafa4bzMFATvltEYcah1B8ZMfkM_s,11373
8
- geoai/train.py,sha256=-l2j1leTxDnFDLaBslu1q6CobXjm3LEdiQwUWOU8P6M,40088
9
- geoai/utils.py,sha256=Wg9jbMBKUZSGUmU8Vkp6v19QcDNg5KmcyZxuHqJvgnc,233016
10
- geoai_py-0.4.3.dist-info/licenses/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
11
- geoai_py-0.4.3.dist-info/METADATA,sha256=geDmJ-1zHImsOdcj4gypgq8JqSy8MznnxAnICwh0EbA,6049
12
- geoai_py-0.4.3.dist-info/WHEEL,sha256=aoLN90hLOL0c0qxXMxWYUM3HA3WmFGZQqEJHX1V_OJE,109
13
- geoai_py-0.4.3.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
14
- geoai_py-0.4.3.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
15
- geoai_py-0.4.3.dist-info/RECORD,,