Skip to content

Commit 96d3e30

Browse files
committed
add data
1 parent 17f2bff commit 96d3e30

4 files changed

Lines changed: 507 additions & 2 deletions

File tree

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ docs/_build/
2222
output*.csv
2323
*.log
2424

25-
# Development data
26-
data/
2725

2826
# OS files
2927
.DS_Store

geo_sampling/data/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Data provider modules for GADM and OSM data."""
2+
3+
from .gadm import GADMProvider
4+
from .osm import OSMProvider
5+
6+
__all__ = ["GADMProvider", "OSMProvider"]

geo_sampling/data/gadm.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""GADM (Global Administrative Areas) data provider."""
2+
3+
import os
4+
import zipfile
5+
from typing import List, Tuple, Optional
6+
from urllib.parse import urljoin
7+
8+
import requests
9+
import shapefile
10+
from shapely.geometry import Polygon
11+
from shapely.ops import unary_union
12+
13+
from .._types import BoundingBox
14+
15+
16+
class GADMProvider:
17+
"""Provider for GADM administrative boundary data."""
18+
19+
GADM_BASE_URL = "https://geodata.ucdavis.edu/gadm/gadm4.1/shp/"
20+
GADM_URL_FORMAT = "gadm41_{0}_shp.zip"
21+
22+
def __init__(self, data_dir: str = "data"):
23+
"""Initialize GADM provider.
24+
25+
Args:
26+
data_dir: Directory to store downloaded data
27+
"""
28+
self.data_dir = data_dir
29+
os.makedirs(data_dir, exist_ok=True)
30+
31+
def get_country_list(self) -> List[str]:
32+
"""Get list of available countries from GADM.
33+
34+
Returns:
35+
List of country names
36+
"""
37+
try:
38+
response = requests.get("https://gadm.org/download_country.html", timeout=30)
39+
response.raise_for_status()
40+
41+
# Extract country codes from HTML
42+
# This is a simplified version - in practice you'd parse the HTML properly
43+
countries = []
44+
for line in response.text.split('\n'):
45+
if 'option value=' in line and 'selected' not in line:
46+
# Extract country name from HTML option
47+
start = line.find('>') + 1
48+
end = line.find('<', start)
49+
if start > 0 and end > start:
50+
country = line[start:end].strip()
51+
if country and country != "Select country":
52+
countries.append(country)
53+
54+
return sorted(countries)
55+
56+
except Exception as e:
57+
print(f"Warning: Could not fetch country list: {e}")
58+
return []
59+
60+
def download_country_data(self, country_code: str) -> str:
61+
"""Download GADM shapefile data for a country.
62+
63+
Args:
64+
country_code: Three-letter country code (e.g., 'IND')
65+
66+
Returns:
67+
Path to the downloaded and extracted directory
68+
"""
69+
filename = self.GADM_URL_FORMAT.format(country_code)
70+
url = urljoin(self.GADM_BASE_URL, filename)
71+
72+
local_zip_path = os.path.join(self.data_dir, filename)
73+
extract_dir = os.path.join(self.data_dir, country_code)
74+
75+
# Download if not already present
76+
if not os.path.exists(local_zip_path):
77+
print(f"Downloading {url}...")
78+
self._download_file(url, local_zip_path)
79+
80+
# Extract if not already done
81+
if not os.path.exists(extract_dir):
82+
print(f"Extracting {filename}...")
83+
with zipfile.ZipFile(local_zip_path, 'r') as zip_ref:
84+
zip_ref.extractall(extract_dir)
85+
86+
return extract_dir
87+
88+
def load_boundaries(self, country_code: str, admin_level: int,
89+
region_name: Optional[str] = None) -> Tuple[List[str], Polygon, BoundingBox]:
90+
"""Load administrative boundaries for a country/region.
91+
92+
Args:
93+
country_code: Three-letter country code
94+
admin_level: Administrative level (1-4)
95+
region_name: Specific region name to filter by
96+
97+
Returns:
98+
Tuple of (region_names, combined_polygon, bounding_box)
99+
"""
100+
extract_dir = self.download_country_data(country_code)
101+
shapefile_path = os.path.join(extract_dir, f"gadm41_{country_code}_{admin_level}")
102+
103+
if not os.path.exists(f"{shapefile_path}.shp"):
104+
raise FileNotFoundError(f"Shapefile not found: {shapefile_path}.shp")
105+
106+
# Load shapefile
107+
sf = shapefile.Reader(shapefile_path)
108+
109+
# Get field names to find the appropriate name field
110+
field_names = [field[0] for field in sf.fields[1:]] # Skip DeletionFlag
111+
name_field = f"NAME_{admin_level}"
112+
113+
if name_field not in field_names:
114+
# Fallback to other possible name fields
115+
for field in ["NAME_EN", "NAME", "NAME_1", "NAME_2"]:
116+
if field in field_names:
117+
name_field = field
118+
break
119+
120+
region_names = []
121+
polygons = []
122+
123+
# Process each shape record
124+
for record in sf.iterShapeRecords():
125+
shape = record.shape
126+
rec = record.record
127+
128+
# Get region name
129+
try:
130+
name_index = field_names.index(name_field)
131+
current_name = rec[name_index]
132+
except (ValueError, IndexError):
133+
current_name = "Unknown"
134+
135+
# Filter by region name if specified
136+
if region_name and region_name.lower() not in current_name.lower():
137+
continue
138+
139+
region_names.append(current_name)
140+
141+
# Convert shape to Shapely polygon
142+
if shape.shapeType == 5: # Polygon
143+
polygons.append(Polygon(shape.points))
144+
elif shape.shapeType == 15: # PolygonZ
145+
polygons.append(Polygon([(x, y) for x, y, z in shape.points]))
146+
147+
if not polygons:
148+
if region_name:
149+
raise ValueError(f"Region '{region_name}' not found in {country_code} level {admin_level}")
150+
else:
151+
raise ValueError(f"No boundaries found for {country_code} level {admin_level}")
152+
153+
# Combine all polygons
154+
combined_polygon = unary_union(polygons)
155+
156+
# Calculate bounding box
157+
bounds = combined_polygon.bounds
158+
bbox = BoundingBox(
159+
min_long=bounds[0],
160+
min_lat=bounds[1],
161+
max_long=bounds[2],
162+
max_lat=bounds[3]
163+
)
164+
165+
return region_names, combined_polygon, bbox
166+
167+
def _download_file(self, url: str, local_path: str) -> None:
168+
"""Download a file from URL to local path."""
169+
response = requests.get(url, timeout=30, stream=True)
170+
response.raise_for_status()
171+
172+
with open(local_path, 'wb') as f:
173+
for chunk in response.iter_content(chunk_size=8192):
174+
f.write(chunk)

0 commit comments

Comments
 (0)