geoai-py 0.4.1__py2.py3-none-any.whl → 0.4.3__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
@@ -8,9 +8,12 @@ from typing import Any, Dict, List, Optional, Tuple
8
8
  import geopandas as gpd
9
9
  import matplotlib.pyplot as plt
10
10
  import numpy as np
11
+ import pandas as pd
11
12
  import planetary_computer as pc
13
+ import pystac
12
14
  import requests
13
- import rioxarray
15
+ import rioxarray as rxr
16
+ import xarray as xr
14
17
  from pystac_client import Client
15
18
  from shapely.geometry import box
16
19
  from tqdm import tqdm
@@ -119,7 +122,7 @@ def download_naip(
119
122
  #
120
123
  else:
121
124
  # Fallback to direct rioxarray opening (less common case)
122
- data = rioxarray.open_rasterio(rgb_asset.href)
125
+ data = rxr.open_rasterio(rgb_asset.href)
123
126
  data.rio.to_raster(output_path)
124
127
 
125
128
  downloaded_files.append(output_path)
@@ -127,7 +130,7 @@ def download_naip(
127
130
 
128
131
  # Optional: Display a preview (uncomment if needed)
129
132
  if preview:
130
- data = rioxarray.open_rasterio(output_path)
133
+ data = rxr.open_rasterio(output_path)
131
134
  preview_raster(data)
132
135
 
133
136
  except Exception as e:
@@ -394,3 +397,748 @@ def extract_building_stats(geojson_file: str) -> Dict[str, Any]:
394
397
  except Exception as e:
395
398
  logger.error(f"Error extracting statistics: {str(e)}")
396
399
  return {"error": str(e)}
400
+
401
+
402
+ def download_pc_stac_item(
403
+ item_url,
404
+ bands=None,
405
+ output_dir=None,
406
+ show_progress=True,
407
+ merge_bands=False,
408
+ merged_filename=None,
409
+ overwrite=False,
410
+ cell_size=None,
411
+ ):
412
+ """
413
+ Downloads a STAC item from Microsoft Planetary Computer with specified bands.
414
+
415
+ This function fetches a STAC item by URL, signs the assets using Planetary Computer
416
+ credentials, and downloads the specified bands with a progress bar. Can optionally
417
+ merge bands into a single multi-band GeoTIFF.
418
+
419
+ Args:
420
+ item_url (str): The URL of the STAC item to download.
421
+ bands (list, optional): List of specific bands to download (e.g., ['B01', 'B02']).
422
+ If None, all available bands will be downloaded.
423
+ output_dir (str, optional): Directory to save downloaded bands. If None,
424
+ bands are returned as xarray DataArrays.
425
+ show_progress (bool, optional): Whether to display a progress bar. Default is True.
426
+ merge_bands (bool, optional): Whether to merge downloaded bands into a single
427
+ multi-band GeoTIFF file. Default is False.
428
+ merged_filename (str, optional): Filename for the merged bands. If None and
429
+ merge_bands is True, uses "{item_id}_merged.tif".
430
+ overwrite (bool, optional): Whether to overwrite existing files. Default is False.
431
+ cell_size (float, optional): Resolution in meters for the merged output. If None,
432
+ uses the resolution of the first band.
433
+
434
+ Returns:
435
+ dict: Dictionary mapping band names to their corresponding xarray DataArrays
436
+ or file paths if output_dir is provided. If merge_bands is True, also
437
+ includes a 'merged' key with the path to the merged file.
438
+
439
+ Raises:
440
+ ValueError: If the item cannot be retrieved or a requested band is not available.
441
+ """
442
+ from rasterio.enums import Resampling
443
+
444
+ # Get the item ID from the URL
445
+ item_id = item_url.split("/")[-1]
446
+ collection = item_url.split("/collections/")[1].split("/items/")[0]
447
+
448
+ # Connect to the Planetary Computer STAC API
449
+ catalog = Client.open(
450
+ "https://planetarycomputer.microsoft.com/api/stac/v1",
451
+ modifier=pc.sign_inplace,
452
+ )
453
+
454
+ # Search for the specific item
455
+ search = catalog.search(collections=[collection], ids=[item_id])
456
+
457
+ # Get the first item from the search results
458
+ items = list(search.get_items())
459
+ if not items:
460
+ raise ValueError(f"Item with ID {item_id} not found")
461
+
462
+ item = items[0]
463
+
464
+ # Determine which bands to download
465
+ available_assets = list(item.assets.keys())
466
+
467
+ if bands is None:
468
+ # If no bands specified, download all band assets
469
+ bands_to_download = [
470
+ asset for asset in available_assets if asset.startswith("B")
471
+ ]
472
+ else:
473
+ # Verify all requested bands exist
474
+ missing_bands = [band for band in bands if band not in available_assets]
475
+ if missing_bands:
476
+ raise ValueError(
477
+ f"The following bands are not available: {missing_bands}. "
478
+ f"Available assets are: {available_assets}"
479
+ )
480
+ bands_to_download = bands
481
+
482
+ # Create output directory if specified and doesn't exist
483
+ if output_dir and not os.path.exists(output_dir):
484
+ os.makedirs(output_dir)
485
+
486
+ result = {}
487
+ band_data_arrays = []
488
+ resampled_arrays = []
489
+ band_names = [] # Track band names in order
490
+
491
+ # Set up progress bar
492
+ progress_iter = (
493
+ tqdm(bands_to_download, desc="Downloading bands")
494
+ if show_progress
495
+ else bands_to_download
496
+ )
497
+
498
+ # Download each requested band
499
+ for band in progress_iter:
500
+ if band not in item.assets:
501
+ if show_progress and not isinstance(progress_iter, list):
502
+ progress_iter.write(
503
+ f"Warning: Band {band} not found in assets, skipping."
504
+ )
505
+ continue
506
+
507
+ band_url = item.assets[band].href
508
+
509
+ if output_dir:
510
+ file_path = os.path.join(output_dir, f"{item.id}_{band}.tif")
511
+
512
+ # Check if file exists and skip if overwrite is False
513
+ if os.path.exists(file_path) and not overwrite:
514
+ if show_progress and not isinstance(progress_iter, list):
515
+ progress_iter.write(
516
+ f"File {file_path} already exists, skipping (use overwrite=True to force download)."
517
+ )
518
+ # Still need to open the file to get the data for merging
519
+ if merge_bands:
520
+ band_data = rxr.open_rasterio(file_path)
521
+ band_data_arrays.append((band, band_data))
522
+ band_names.append(band)
523
+ result[band] = file_path
524
+ continue
525
+
526
+ if show_progress and not isinstance(progress_iter, list):
527
+ progress_iter.set_description(f"Downloading {band}")
528
+
529
+ band_data = rxr.open_rasterio(band_url)
530
+
531
+ # Store the data array for potential merging later
532
+ if merge_bands:
533
+ band_data_arrays.append((band, band_data))
534
+ band_names.append(band)
535
+
536
+ if output_dir:
537
+ file_path = os.path.join(output_dir, f"{item.id}_{band}.tif")
538
+ band_data.rio.to_raster(file_path)
539
+ result[band] = file_path
540
+ else:
541
+ result[band] = band_data
542
+
543
+ # Merge bands if requested
544
+ if merge_bands and output_dir:
545
+ if merged_filename is None:
546
+ merged_filename = f"{item.id}_merged.tif"
547
+
548
+ merged_path = os.path.join(output_dir, merged_filename)
549
+
550
+ # Check if merged file exists and skip if overwrite is False
551
+ if os.path.exists(merged_path) and not overwrite:
552
+ if show_progress:
553
+ print(
554
+ f"Merged file {merged_path} already exists, skipping (use overwrite=True to force creation)."
555
+ )
556
+ result["merged"] = merged_path
557
+ else:
558
+ if show_progress:
559
+ print("Resampling and merging bands...")
560
+
561
+ # Determine target cell size if not provided
562
+ if cell_size is None and band_data_arrays:
563
+ # Use the resolution of the first band (usually 10m for B02, B03, B04, B08)
564
+ # Get the affine transform (containing resolution info)
565
+ first_band_data = band_data_arrays[0][1]
566
+ # Extract resolution from transform
567
+ cell_size = abs(first_band_data.rio.transform()[0])
568
+ if show_progress:
569
+ print(f"Using detected resolution: {cell_size}m")
570
+ elif cell_size is None:
571
+ # Default to 10m if no bands are available
572
+ cell_size = 10
573
+ if show_progress:
574
+ print(f"Using default resolution: {cell_size}m")
575
+
576
+ # Process bands in memory-efficient way
577
+ for i, (band_name, data_array) in enumerate(band_data_arrays):
578
+ if show_progress:
579
+ print(f"Processing band: {band_name}")
580
+
581
+ # Get current resolution
582
+ current_res = abs(data_array.rio.transform()[0])
583
+
584
+ # Resample if needed
585
+ if (
586
+ abs(current_res - cell_size) > 0.01
587
+ ): # Small tolerance for floating point comparison
588
+ if show_progress:
589
+ print(
590
+ f"Resampling {band_name} from {current_res}m to {cell_size}m"
591
+ )
592
+
593
+ # Use bilinear for downsampling (higher to lower resolution)
594
+ # Use nearest for upsampling (lower to higher resolution)
595
+ resampling_method = (
596
+ Resampling.bilinear
597
+ if current_res < cell_size
598
+ else Resampling.nearest
599
+ )
600
+
601
+ resampled = data_array.rio.reproject(
602
+ data_array.rio.crs,
603
+ resolution=(cell_size, cell_size),
604
+ resampling=resampling_method,
605
+ )
606
+ resampled_arrays.append(resampled)
607
+ else:
608
+ resampled_arrays.append(data_array)
609
+
610
+ if show_progress:
611
+ print("Stacking bands...")
612
+
613
+ # Concatenate all resampled arrays along the band dimension
614
+ try:
615
+ merged_data = xr.concat(resampled_arrays, dim="band")
616
+
617
+ if show_progress:
618
+ print(f"Writing merged data to {merged_path}...")
619
+
620
+ # Add description metadata
621
+ merged_data.attrs["description"] = (
622
+ f"Multi-band image containing {', '.join(band_names)}"
623
+ )
624
+
625
+ # Create a dictionary mapping band indices to band names
626
+ band_descriptions = {}
627
+ for i, name in enumerate(band_names):
628
+ band_descriptions[i + 1] = name
629
+
630
+ # Write the merged data to file with band descriptions
631
+ merged_data.rio.to_raster(
632
+ merged_path,
633
+ tags={"BAND_NAMES": ",".join(band_names)},
634
+ descriptions=band_names,
635
+ )
636
+
637
+ result["merged"] = merged_path
638
+
639
+ if show_progress:
640
+ print(f"Merged bands saved to: {merged_path}")
641
+ print(f"Band order in merged file: {', '.join(band_names)}")
642
+ except Exception as e:
643
+ if show_progress:
644
+ print(f"Error during merging: {str(e)}")
645
+ print(f"Error details: {type(e).__name__}: {str(e)}")
646
+ raise
647
+
648
+ return result
649
+
650
+
651
+ def pc_collection_list(
652
+ endpoint="https://planetarycomputer.microsoft.com/api/stac/v1",
653
+ detailed=False,
654
+ filter_by=None,
655
+ sort_by="id",
656
+ ):
657
+ """
658
+ Retrieves and displays the list of available collections from Planetary Computer.
659
+
660
+ This function connects to the Planetary Computer STAC API and retrieves the
661
+ list of all available collections, with options to filter and sort the results.
662
+
663
+ Args:
664
+ endpoint (str, optional): STAC API endpoint URL.
665
+ Defaults to "https://planetarycomputer.microsoft.com/api/stac/v1".
666
+ detailed (bool, optional): Whether to return detailed information for each
667
+ collection. If False, returns only basic info. Defaults to False.
668
+ filter_by (dict, optional): Dictionary of field:value pairs to filter
669
+ collections. For example, {"license": "CC-BY-4.0"}. Defaults to None.
670
+ sort_by (str, optional): Field to sort the collections by.
671
+ Defaults to "id".
672
+
673
+ Returns:
674
+ pandas.DataFrame: DataFrame containing collection information.
675
+
676
+ Raises:
677
+ ConnectionError: If there's an issue connecting to the API.
678
+ """
679
+ # Initialize the STAC client
680
+ try:
681
+ catalog = Client.open(endpoint)
682
+ except Exception as e:
683
+ raise ConnectionError(f"Failed to connect to STAC API at {endpoint}: {str(e)}")
684
+
685
+ # Get all collections
686
+ try:
687
+ collections = list(catalog.get_collections())
688
+ except Exception as e:
689
+ raise Exception(f"Error retrieving collections: {str(e)}")
690
+
691
+ # Basic info to extract from all collections
692
+ collection_info = []
693
+
694
+ # Extract information based on detail level
695
+ for collection in collections:
696
+ # Basic information always included
697
+ info = {
698
+ "id": collection.id,
699
+ "title": collection.title or "No title",
700
+ "description": (
701
+ collection.description[:100] + "..."
702
+ if collection.description and len(collection.description) > 100
703
+ else collection.description
704
+ ),
705
+ }
706
+
707
+ # Add detailed information if requested
708
+ if detailed:
709
+ # Get temporal extent if available
710
+ temporal_extent = "Unknown"
711
+ if collection.extent and collection.extent.temporal:
712
+ interval = (
713
+ collection.extent.temporal.intervals[0]
714
+ if collection.extent.temporal.intervals
715
+ else None
716
+ )
717
+ if interval:
718
+ start = interval[0] or "Unknown Start"
719
+ end = interval[1] or "Present"
720
+ if isinstance(start, datetime.datetime):
721
+ start = start.strftime("%Y-%m-%d")
722
+ if isinstance(end, datetime.datetime):
723
+ end = end.strftime("%Y-%m-%d")
724
+ temporal_extent = f"{start} to {end}"
725
+
726
+ # Add additional details
727
+ info.update(
728
+ {
729
+ "license": collection.license or "Unknown",
730
+ "keywords": (
731
+ ", ".join(collection.keywords)
732
+ if collection.keywords
733
+ else "None"
734
+ ),
735
+ "temporal_extent": temporal_extent,
736
+ "asset_count": len(collection.assets) if collection.assets else 0,
737
+ "providers": (
738
+ ", ".join([p.name for p in collection.providers])
739
+ if collection.providers
740
+ else "Unknown"
741
+ ),
742
+ }
743
+ )
744
+
745
+ # Add spatial extent if available
746
+ if collection.extent and collection.extent.spatial:
747
+ info["bbox"] = (
748
+ str(collection.extent.spatial.bboxes[0])
749
+ if collection.extent.spatial.bboxes
750
+ else "Unknown"
751
+ )
752
+
753
+ collection_info.append(info)
754
+
755
+ # Convert to DataFrame for easier filtering and sorting
756
+ df = pd.DataFrame(collection_info)
757
+
758
+ # Apply filtering if specified
759
+ if filter_by:
760
+ for field, value in filter_by.items():
761
+ if field in df.columns:
762
+ df = df[df[field].astype(str).str.contains(value, case=False, na=False)]
763
+
764
+ # Apply sorting
765
+ if sort_by in df.columns:
766
+ df = df.sort_values(by=sort_by)
767
+
768
+ print(f"Retrieved {len(df)} collections from Planetary Computer")
769
+
770
+ # # Print a nicely formatted table
771
+ # if not df.empty:
772
+ # print("\nAvailable collections:")
773
+ # print(tabulate(df, headers="keys", tablefmt="grid", showindex=False))
774
+
775
+ return df
776
+
777
+
778
+ def pc_stac_search(
779
+ collection,
780
+ bbox=None,
781
+ time_range=None,
782
+ query=None,
783
+ limit=10,
784
+ max_items=None,
785
+ endpoint="https://planetarycomputer.microsoft.com/api/stac/v1",
786
+ ):
787
+ """
788
+ Search for STAC items in the Planetary Computer catalog.
789
+
790
+ This function queries the Planetary Computer STAC API to find items matching
791
+ the specified criteria, including collection, bounding box, time range, and
792
+ additional query parameters.
793
+
794
+ Args:
795
+ collection (str): The STAC collection ID to search within.
796
+ bbox (list, optional): Bounding box coordinates [west, south, east, north].
797
+ Defaults to None.
798
+ time_range (str or tuple, optional): Time range as a string "start/end" or
799
+ a tuple of (start, end) datetime objects. Defaults to None.
800
+ query (dict, optional): Additional query parameters for filtering.
801
+ Defaults to None.
802
+ limit (int, optional): Number of items to return per page. Defaults to 10.
803
+ max_items (int, optional): Maximum total number of items to return.
804
+ Defaults to None (returns all matching items).
805
+ endpoint (str, optional): STAC API endpoint URL.
806
+ Defaults to "https://planetarycomputer.microsoft.com/api/stac/v1".
807
+
808
+ Returns:
809
+ list: List of STAC Item objects matching the search criteria.
810
+
811
+ Raises:
812
+ ValueError: If invalid parameters are provided.
813
+ ConnectionError: If there's an issue connecting to the API.
814
+ """
815
+ import datetime
816
+
817
+ # Initialize the STAC client
818
+ try:
819
+ catalog = Client.open(endpoint)
820
+ except Exception as e:
821
+ raise ConnectionError(f"Failed to connect to STAC API at {endpoint}: {str(e)}")
822
+
823
+ # Process time_range if provided
824
+ if time_range:
825
+ if isinstance(time_range, tuple) and len(time_range) == 2:
826
+ # Convert datetime objects to ISO format strings
827
+ start, end = time_range
828
+ if isinstance(start, datetime.datetime):
829
+ start = start.isoformat()
830
+ if isinstance(end, datetime.datetime):
831
+ end = end.isoformat()
832
+ time_str = f"{start}/{end}"
833
+ elif isinstance(time_range, str):
834
+ time_str = time_range
835
+ else:
836
+ raise ValueError(
837
+ "time_range must be a 'start/end' string or tuple of (start, end)"
838
+ )
839
+ else:
840
+ time_str = None
841
+
842
+ # Create the search object
843
+ search = catalog.search(
844
+ collections=[collection], bbox=bbox, datetime=time_str, query=query, limit=limit
845
+ )
846
+
847
+ # Collect the items
848
+ items = []
849
+ try:
850
+ # Use max_items if specified, otherwise get all items
851
+ if max_items:
852
+ items_gen = search.get_items()
853
+ for item in items_gen:
854
+ items.append(item)
855
+ if len(items) >= max_items:
856
+ break
857
+ else:
858
+ items = list(search.get_items())
859
+ except Exception as e:
860
+ raise Exception(f"Error retrieving search results: {str(e)}")
861
+
862
+ print(f"Found {len(items)} items matching search criteria")
863
+
864
+ return items
865
+
866
+
867
+ def pc_stac_download(
868
+ items,
869
+ output_dir=".",
870
+ assets=None,
871
+ max_workers=4,
872
+ skip_existing=True,
873
+ ):
874
+ """
875
+ Download assets from STAC items retrieved from the Planetary Computer.
876
+
877
+ This function downloads specified assets from a list of STAC items to the
878
+ specified output directory. It supports parallel downloads and can skip
879
+ already downloaded files.
880
+
881
+ Args:
882
+ items (list or pystac.Item): STAC Item object or list of STAC Item objects.
883
+ output_dir (str, optional): Directory where assets will be saved.
884
+ Defaults to current directory.
885
+ assets (list, optional): List of asset keys to download. If None,
886
+ downloads all available assets. Defaults to None.
887
+ max_workers (int, optional): Maximum number of concurrent download threads.
888
+ Defaults to 4.
889
+ skip_existing (bool, optional): Skip download if the file already exists.
890
+ Defaults to True.
891
+ sign_urls (bool, optional): Whether to sign URLs for authenticated access.
892
+ Defaults to True.
893
+
894
+ Returns:
895
+ dict: Dictionary mapping STAC item IDs to dictionaries of their downloaded
896
+ assets {asset_key: file_path}.
897
+
898
+ Raises:
899
+ TypeError: If items is not a STAC Item or list of STAC Items.
900
+ IOError: If there's an error writing the downloaded assets to disk.
901
+ """
902
+
903
+ from concurrent.futures import ThreadPoolExecutor, as_completed
904
+
905
+ # Handle single item case
906
+ if isinstance(items, pystac.Item):
907
+ items = [items]
908
+ elif not isinstance(items, list):
909
+ raise TypeError("items must be a STAC Item or list of STAC Items")
910
+
911
+ # Create output directory if it doesn't exist
912
+ os.makedirs(output_dir, exist_ok=True)
913
+
914
+ # Function to download a single asset
915
+ def download_asset(item, asset_key, asset):
916
+ item = pc.sign(item)
917
+ item_id = item.id
918
+
919
+ # Get the asset URL and sign it if needed
920
+ asset_url = item.assets[asset_key].href
921
+ # Determine output filename
922
+ if asset.media_type:
923
+ # Use appropriate file extension based on media type
924
+ if "tiff" in asset.media_type or "geotiff" in asset.media_type:
925
+ ext = ".tif"
926
+ elif "jpeg" in asset.media_type:
927
+ ext = ".jpg"
928
+ elif "png" in asset.media_type:
929
+ ext = ".png"
930
+ elif "json" in asset.media_type:
931
+ ext = ".json"
932
+ else:
933
+ # Default extension based on the original URL
934
+ ext = os.path.splitext(asset_url.split("?")[0])[1] or ".data"
935
+ else:
936
+ # Default extension based on the original URL
937
+ ext = os.path.splitext(asset_url.split("?")[0])[1] or ".data"
938
+
939
+ output_path = os.path.join(output_dir, f"{item_id}_{asset_key}{ext}")
940
+
941
+ # Skip if file exists and skip_existing is True
942
+ if skip_existing and os.path.exists(output_path):
943
+ print(f"Skipping existing asset: {asset_key} -> {output_path}")
944
+ return asset_key, output_path
945
+
946
+ try:
947
+ # Download the asset with progress bar
948
+ with requests.get(asset_url, stream=True) as r:
949
+ r.raise_for_status()
950
+ total_size = int(r.headers.get("content-length", 0))
951
+ with open(output_path, "wb") as f:
952
+ with tqdm(
953
+ total=total_size,
954
+ unit="B",
955
+ unit_scale=True,
956
+ unit_divisor=1024,
957
+ desc=f"Downloading {item_id}_{asset_key}",
958
+ ncols=100,
959
+ ) as pbar:
960
+ for chunk in r.iter_content(chunk_size=8192):
961
+ f.write(chunk)
962
+ pbar.update(len(chunk))
963
+
964
+ return asset_key, output_path
965
+ except Exception as e:
966
+ print(f"Error downloading {asset_key} for item {item_id}: {str(e)}")
967
+ if os.path.exists(output_path):
968
+ os.remove(output_path) # Clean up partial download
969
+ return asset_key, None
970
+
971
+ # Process all items and their assets
972
+ results = {}
973
+
974
+ for item in items:
975
+ item_assets = {}
976
+ item_id = item.id
977
+ print(f"Processing STAC item: {item_id}")
978
+
979
+ # Determine which assets to download
980
+ if assets:
981
+ assets_to_download = {k: v for k, v in item.assets.items() if k in assets}
982
+ if not assets_to_download:
983
+ print(
984
+ f"Warning: None of the specified asset keys {assets} found in item {item_id}"
985
+ )
986
+ print(f"Available asset keys: {list(item.assets.keys())}")
987
+ continue
988
+ else:
989
+ assets_to_download = item.assets
990
+
991
+ # Download assets concurrently
992
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
993
+ # Submit all download tasks
994
+ future_to_asset = {
995
+ executor.submit(download_asset, item, asset_key, asset): (
996
+ asset_key,
997
+ asset,
998
+ )
999
+ for asset_key, asset in assets_to_download.items()
1000
+ }
1001
+
1002
+ # Process results as they complete
1003
+ for future in as_completed(future_to_asset):
1004
+ asset_key, asset = future_to_asset[future]
1005
+ try:
1006
+ key, path = future.result()
1007
+ if path:
1008
+ item_assets[key] = path
1009
+ except Exception as e:
1010
+ print(
1011
+ f"Error processing asset {asset_key} for item {item_id}: {str(e)}"
1012
+ )
1013
+
1014
+ results[item_id] = item_assets
1015
+
1016
+ # Count total downloaded assets
1017
+ total_assets = sum(len(assets) for assets in results.values())
1018
+ print(f"\nDownloaded {total_assets} assets for {len(results)} items")
1019
+
1020
+ return results
1021
+
1022
+
1023
+ def pc_item_asset_list(item):
1024
+ """
1025
+ Retrieve the list of asset keys from a STAC item in the Planetary Computer catalog.
1026
+
1027
+ Args:
1028
+ item (str): The URL of the STAC item.
1029
+
1030
+ Returns:
1031
+ list: A list of asset keys available in the signed STAC item.
1032
+ """
1033
+ if isinstance(item, str):
1034
+ item = pystac.Item.from_file(item)
1035
+
1036
+ if not isinstance(item, pystac.Item):
1037
+ raise ValueError("item_url must be a string (URL) or a pystac.Item object")
1038
+
1039
+ return list(item.assets.keys())
1040
+
1041
+
1042
+ def read_pc_item_asset(item, asset, output=None, as_cog=True, **kwargs):
1043
+ """
1044
+ Read a specific asset from a STAC item in the Planetary Computer catalog.
1045
+
1046
+ Args:
1047
+ item (str): The URL of the STAC item.
1048
+ asset (str): The key of the asset to read.
1049
+ output (str, optional): If specified, the path to save the asset as a raster file.
1050
+ as_cog (bool, optional): If True, save the asset as a Cloud Optimized GeoTIFF (COG).
1051
+
1052
+ Returns:
1053
+ xarray.DataArray: The data array for the specified asset.
1054
+ """
1055
+ if isinstance(item, str):
1056
+ item = pystac.Item.from_file(item)
1057
+
1058
+ if not isinstance(item, pystac.Item):
1059
+ raise ValueError("item must be a string (URL) or a pystac.Item object")
1060
+
1061
+ signed_item = pc.sign(item)
1062
+
1063
+ if asset not in signed_item.assets:
1064
+ raise ValueError(
1065
+ f"Asset '{asset}' not found in item '{item.id}'. It has available assets: {list(signed_item.assets.keys())}"
1066
+ )
1067
+
1068
+ asset_url = signed_item.assets[asset].href
1069
+ ds = rxr.open_rasterio(asset_url)
1070
+
1071
+ if as_cog:
1072
+ kwargs["driver"] = "COG" # Ensure the output is a Cloud Optimized GeoTIFF
1073
+
1074
+ if output:
1075
+ print(f"Saving asset '{asset}' to {output}...")
1076
+ ds.rio.to_raster(output, **kwargs)
1077
+ print(f"Asset '{asset}' saved successfully.")
1078
+ return ds
1079
+
1080
+
1081
+ def view_pc_item(
1082
+ url=None,
1083
+ collection=None,
1084
+ item=None,
1085
+ assets=None,
1086
+ bands=None,
1087
+ titiler_endpoint=None,
1088
+ name="STAC Item",
1089
+ attribution="Planetary Computer",
1090
+ opacity=1.0,
1091
+ shown=True,
1092
+ fit_bounds=True,
1093
+ layer_index=None,
1094
+ backend="folium",
1095
+ basemap=None,
1096
+ map_args=None,
1097
+ **kwargs,
1098
+ ):
1099
+
1100
+ if backend == "folium":
1101
+ import leafmap.foliumap as leafmap
1102
+
1103
+ elif backend == "ipyleaflet":
1104
+ import leafmap.leafmap as leafmap
1105
+
1106
+ else:
1107
+ raise ValueError(
1108
+ f"Unsupported backend: {backend}. Supported backends are 'folium' and 'ipyleaflet'."
1109
+ )
1110
+
1111
+ if map_args is None:
1112
+ map_args = {}
1113
+
1114
+ if "draw_control" not in map_args:
1115
+ map_args["draw_control"] = False
1116
+
1117
+ if url is not None:
1118
+
1119
+ item = pystac.Item.from_file(url)
1120
+
1121
+ if isinstance(item, pystac.Item):
1122
+ collection = item.collection_id
1123
+ if assets is None:
1124
+ assets = [list(item.assets.keys())[0]]
1125
+ item = item.id
1126
+
1127
+ m = leafmap.Map(**map_args)
1128
+ if basemap is not None:
1129
+ m.add_basemap(basemap)
1130
+ m.add_stac_layer(
1131
+ collection=collection,
1132
+ item=item,
1133
+ assets=assets,
1134
+ bands=bands,
1135
+ titiler_endpoint=titiler_endpoint,
1136
+ name=name,
1137
+ attribution=attribution,
1138
+ opacity=opacity,
1139
+ shown=shown,
1140
+ fit_bounds=fit_bounds,
1141
+ layer_index=layer_index,
1142
+ **kwargs,
1143
+ )
1144
+ return m