Areas of interest
Every public function takes its area of interest in any of these forms:
ef.composite((-111.9, 40.7, -111.8, 40.8), ...) # bbox tuple (WGS84)
ef.composite("Yosemite National Park", ...) # place name (geocoded)
ef.composite("watershed.geojson", ...) # GeoJSON file
ef.composite("/data/fields.shp", ...) # shapefile (or .gpkg, .kml)
ef.composite(geojson_dict, ...) # GeoJSON dict
ef.composite(shapely_polygon, ...) # __geo_interface__
- bbox —
(min_lon, min_lat, max_lon, max_lat) in WGS84 degrees.
- place name — resolved with OpenStreetMap Nominatim (free, no key).
- GeoJSON / shapely — Polygon, MultiPolygon, Point, LineString, and
collections. Degenerate geometries (a Point, or an axis-aligned line)
resolve to a small valid bbox around the feature.
Vector files (shapefile, GeoPackage, KML)
Point any function at a local vector file. GeoJSON works out of the box;
shapefiles, GeoPackages, and KML need the vector extra
(pip install "earthfetch[vector]"):
ef.load_dem("/data/basin.shp", ...) # shapefile
ef.composite("/data/parcels.gpkg", ...) # GeoPackage
All features in the file are unioned into one AOI, and — importantly — the
file's CRS is read and reprojected to WGS84 automatically, so a shapefile
stored in a UTM or State-Plane projection lands in the right place without any
manual step.
Clipping
crs="utm" (the default for composite, terrain, load_naip) picks the
right UTM zone for metric pixels. Explicit polygons clip results to their
boundary; geocoded place names return the full rectangle (pass clip=True
to cut to the boundary).
earthfetch.aoi.resolve_aoi
Normalize any supported AOI input to an AOI (see module docs).
Source code in src/earthfetch/aoi.py
| def resolve_aoi(aoi) -> AOI:
"""Normalize any supported AOI input to an ``AOI`` (see module docs)."""
if isinstance(aoi, AOI):
return aoi
if hasattr(aoi, "__geo_interface__"):
return _from_geojson(dict(aoi.__geo_interface__))
if isinstance(aoi, dict):
return _from_geojson(aoi)
if isinstance(aoi, str) or isinstance(aoi, os.PathLike):
text = str(aoi)
low = text.lower()
if low.endswith((".geojson", ".json")):
return _from_geojson(json.loads(Path(text).read_text()))
from .vector import VECTOR_EXTENSIONS
if low.endswith(VECTOR_EXTENSIONS):
from .vector import read_vector
return read_vector(text)
if Path(text).is_file():
# a local file with an unfamiliar name — try GeoJSON, else vector
try:
return _from_geojson(json.loads(Path(text).read_text()))
except (ValueError, UnicodeDecodeError):
from .vector import read_vector
return read_vector(text)
return geocode(text)
if isinstance(aoi, Sequence) and len(aoi) == 4:
return AOI(bbox=validate_bbox(aoi))
raise EarthfetchError(
f"cannot interpret AOI of type {type(aoi).__name__}: pass a bbox "
"tuple, GeoJSON, a .geojson/.shp/.gpkg path, a shapely geometry, "
"or a place name"
)
|
earthfetch.aoi.geocode
geocode(place: str) -> AOI
Resolve a place name to a bbox via OpenStreetMap Nominatim (free).
Source code in src/earthfetch/aoi.py
| def geocode(place: str) -> AOI:
"""Resolve a place name to a bbox via OpenStreetMap Nominatim (free)."""
resp = get_session().get(
NOMINATIM_URL,
params={"q": place, "format": "json", "limit": 1, "polygon_geojson": 1},
timeout=30,
)
resp.raise_for_status()
results = resp.json()
if not results:
raise EarthfetchError(f"place not found: {place!r}")
hit = results[0]
south, north, west, east = (float(v) for v in hit["boundingbox"])
geometry = hit.get("geojson")
if geometry and geometry.get("type") not in ("Polygon", "MultiPolygon"):
geometry = None # points/lines can't mask an area
logger.info("geocoded %r -> %s (%s)", place, hit.get("display_name"),
(west, south, east, north))
return AOI(bbox=validate_bbox((west, south, east, north)),
geometry=geometry, name=hit.get("display_name"),
clip_default=False)
|
earthfetch.aoi.utm_crs
utm_crs(bbox: Sequence[float]) -> str
EPSG code of the UTM zone at a bbox center, e.g. 'EPSG:32612'.
Source code in src/earthfetch/aoi.py
| def utm_crs(bbox: Sequence[float]) -> str:
"""EPSG code of the UTM zone at a bbox center, e.g. 'EPSG:32612'."""
min_lon, min_lat, max_lon, max_lat = validate_bbox(bbox)
lon = (min_lon + max_lon) / 2
lat = (min_lat + max_lat) / 2
zone = min(60, max(1, math.floor((lon + 180) / 6) + 1))
return f"EPSG:{(32600 if lat >= 0 else 32700) + zone}"
|