Skip to content

Commit 8c235dd

Browse files
committed
feat: Add custom polygon boundary support with automatic clipping (#18)
Implement comprehensive polygon clipping functionality allowing users to: - Use custom polygon boundaries (GeoJSON, Shapefile, GeoDataFrame) - Download data for polygon bbox and automatically clip to shape - Use actual state/county boundaries instead of just bounding boxes - Store polygon geometry in location configurations **New Features:** 1. **Polygon Utilities Module** (`gridfia/utils/polygon_utils.py`) - `load_polygon()`: Load polygons from various formats - `clip_geotiff_to_polygon()`: Clip single GeoTIFF to polygon - `clip_geotiffs_batch()`: Batch clip multiple GeoTIFFs - `get_polygon_bounds()`: Extract bounding box from polygon 2. **LocationConfig Enhancements** (`gridfia/utils/location_config.py`) - Added `from_polygon()` class method for polygon-based configs - Added `store_boundary` parameter to `from_state()` and `from_county()` - Store polygon geometry as GeoJSON in config files - New properties: `polygon_geojson`, `polygon_gdf`, `has_polygon` 3. **GridFIA API Updates** (`gridfia/api.py`) - Added `polygon` parameter to `download_species()` - Added `use_boundary_clip` parameter for state/county downloads - Added `clip_to_polygon` parameter to `create_zarr()` - Auto-detect and use polygon from saved config **Testing:** - Comprehensive test suite in `tests/unit/test_polygon_utils.py` - Tests for loading, clipping, and config management **Documentation:** - Added `examples/polygon_clipping_example.py` with usage examples Closes #18
1 parent 9794274 commit 8c235dd

5 files changed

Lines changed: 987 additions & 53 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""
2+
Example: Using Custom Polygon Boundaries for Data Download and Clipping
3+
4+
This example demonstrates how to:
5+
1. Use a custom polygon boundary for data downloads
6+
2. Automatically clip downloaded data to the polygon shape
7+
3. Use county boundaries with actual shape clipping (not just bbox)
8+
"""
9+
10+
from pathlib import Path
11+
from gridfia import BigMapAPI
12+
import geopandas as gpd
13+
14+
# Initialize API
15+
api = BigMapAPI()
16+
17+
# =============================================================================
18+
# Example 1: Using a Custom Polygon File
19+
# =============================================================================
20+
print("\n" + "="*70)
21+
print("Example 1: Download and clip using custom polygon")
22+
print("="*70)
23+
24+
# You can use GeoJSON, Shapefile, or any format supported by GeoPandas
25+
polygon_file = "study_area.geojson" # Your polygon file
26+
27+
# Download species data - downloads bbox and clips to actual polygon
28+
files = api.download_species(
29+
polygon=polygon_file,
30+
species_codes=["0202", "0122"], # Douglas-fir, Ponderosa Pine
31+
output_dir="downloads/polygon_study"
32+
)
33+
34+
# Create Zarr with automatic clipping
35+
zarr_path = api.create_zarr(
36+
input_dir="downloads/polygon_study",
37+
output_path="data/polygon_study.zarr",
38+
clip_to_polygon=True # Auto-detects polygon from saved config
39+
)
40+
41+
print(f"Created clipped Zarr store: {zarr_path}")
42+
43+
# =============================================================================
44+
# Example 2: County Boundaries with Actual Shape Clipping
45+
# =============================================================================
46+
print("\n" + "="*70)
47+
print("Example 2: Download county data with boundary clipping")
48+
print("="*70)
49+
50+
# Download for Lane County, Oregon with actual boundary clipping
51+
files = api.download_species(
52+
state="Oregon",
53+
county="Lane",
54+
species_codes=["0202", "0122"],
55+
use_boundary_clip=True, # Store and use actual county boundary
56+
output_dir="downloads/lane_county"
57+
)
58+
59+
# Create Zarr - will automatically clip to county boundary
60+
zarr_path = api.create_zarr(
61+
input_dir="downloads/lane_county",
62+
output_path="data/lane_county_clipped.zarr",
63+
clip_to_polygon=True
64+
)
65+
66+
print(f"Created county-clipped Zarr store: {zarr_path}")
67+
68+
# Calculate metrics on the clipped data
69+
results = api.calculate_metrics(
70+
zarr_path,
71+
calculations=["species_richness", "shannon_diversity", "total_biomass"]
72+
)
73+
74+
for result in results:
75+
print(f"\nCalculated {result.name}: {result.output_path}")
76+
77+
# =============================================================================
78+
# Example 3: Using a GeoDataFrame Directly
79+
# =============================================================================
80+
print("\n" + "="*70)
81+
print("Example 3: Using GeoDataFrame for custom area")
82+
print("="*70)
83+
84+
# Load and subset a larger dataset to get a specific polygon
85+
# For example, select parcels from a geopackage
86+
parcel_file = "merged_tim_FACTnobids_neversold_only.gpkg"
87+
88+
if Path(parcel_file).exists():
89+
# Load a few parcels as our study area
90+
gdf = gpd.read_file(parcel_file)
91+
92+
# Select first 10 parcels as study area (just as an example)
93+
study_area = gdf.head(10)
94+
95+
# Download and clip using this GeoDataFrame
96+
files = api.download_species(
97+
polygon=study_area,
98+
species_codes=["0202"],
99+
output_dir="downloads/parcels"
100+
)
101+
102+
# Create clipped Zarr
103+
zarr_path = api.create_zarr(
104+
input_dir="downloads/parcels",
105+
output_path="data/parcels.zarr",
106+
clip_to_polygon=study_area # Pass GeoDataFrame directly
107+
)
108+
109+
print(f"Created parcel-clipped Zarr store: {zarr_path}")
110+
111+
# =============================================================================
112+
# Example 4: Creating a Location Configuration with Polygon
113+
# =============================================================================
114+
print("\n" + "="*70)
115+
print("Example 4: Creating and reusing location configurations")
116+
print("="*70)
117+
118+
# Create a location config from polygon for reuse
119+
config = api.get_location_config(
120+
polygon="study_area.geojson",
121+
output_path="configs/my_study_area.yaml"
122+
)
123+
124+
print(f"Configuration saved to: configs/my_study_area.yaml")
125+
print(f"Location: {config.location_name}")
126+
print(f"Has polygon boundary: {config.has_polygon}")
127+
print(f"Bounding box: {config.wgs84_bbox}")
128+
129+
# Later, reuse this config
130+
files = api.download_species(
131+
location_config="configs/my_study_area.yaml",
132+
species_codes=["0122"]
133+
)
134+
135+
# =============================================================================
136+
# Example 5: Manual Polygon Clipping
137+
# =============================================================================
138+
print("\n" + "="*70)
139+
print("Example 5: Manual polygon clipping of existing GeoTIFFs")
140+
print("="*70)
141+
142+
from gridfia.utils.polygon_utils import clip_geotiffs_batch
143+
144+
# If you already have downloaded GeoTIFFs and want to clip them
145+
clipped_files = clip_geotiffs_batch(
146+
input_dir="downloads/existing_species",
147+
polygon="study_area.geojson",
148+
output_dir="downloads/clipped_species"
149+
)
150+
151+
print(f"Clipped {len(clipped_files)} files")
152+
153+
# =============================================================================
154+
# Workflow Summary
155+
# =============================================================================
156+
print("\n" + "="*70)
157+
print("WORKFLOW SUMMARY")
158+
print("="*70)
159+
print("""
160+
The typical workflow is:
161+
162+
1. Provide a polygon boundary (GeoJSON, Shapefile, or GeoDataFrame)
163+
2. Download species data - system downloads bbox and saves polygon config
164+
3. Create Zarr with clip_to_polygon=True - automatically clips to polygon
165+
4. Analyze the clipped data using standard BigMap methods
166+
167+
Benefits:
168+
- Reduces storage by excluding areas outside your region of interest
169+
- More accurate statistics for irregular study areas
170+
- Cleaner visualizations showing only relevant areas
171+
- Works with any polygon format supported by GeoPandas
172+
""")

0 commit comments

Comments
 (0)