"""
Functions for retrieving common summary and study area FeatureCollections.
geeViz.getSummaryAreasLib provides helpers that return filtered
``ee.FeatureCollection`` objects for political boundaries, USFS
administrative units, census geographies, buildings, roads, protected
areas, and more. Every public function accepts an ``area`` parameter
(an ``ee.FeatureCollection``, ``ee.Feature``, or ``ee.Geometry``) that
is used to spatially filter the result.
"""
"""
Copyright 2026 Ian Housman
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import ee
# ---------------------------------------------------------------------------
# Asset IDs
# ---------------------------------------------------------------------------
# Custom RCR assets (public)
_USFS_FORESTS = "projects/rcr-geeviz/assets/public/summaryAreas/S_USA-AdministrativeForest_3-26-25"
_USFS_DISTRICTS = "projects/rcr-geeviz/assets/public/summaryAreas/S_USA-RangerDistrict_3-26-25"
_USFS_REGIONS = "projects/rcr-geeviz/assets/public/summaryAreas/FS_Region_Boundaries"
_TIGER_URBAN_AREAS = "projects/rcr-geeviz/assets/public/summaryAreas/TIGER_Urban_Areas_2024"
_TIGER_COUNTIES = "projects/rcr-geeviz/assets/public/summaryAreas/tl_2024_us_county_wNames"
# Official GEE datasets
_TIGER_STATES = "TIGER/2018/States"
_TIGER_ROADS = "TIGER/2016/Roads"
_TIGER_BLOCKS_2020 = "TIGER/2020/TABBLOCK20"
# TIGER Roads (community catalog, 2012-2025)
_TIGER_ROADS_COMMUNITY = "projects/sat-io/open-datasets/TIGER/{year}/Roads"
# GRIP4 — Global Roads Inventory Project (community catalog, 7 regions)
_GRIP4_REGIONS = {
"Africa": "projects/sat-io/open-datasets/GRIP4/Africa",
"Central-South-America": "projects/sat-io/open-datasets/GRIP4/Central-South-America",
"Europe": "projects/sat-io/open-datasets/GRIP4/Europe",
"Middle-East-Central-Asia": "projects/sat-io/open-datasets/GRIP4/Middle-East-Central-Asia",
"North-America": "projects/sat-io/open-datasets/GRIP4/North-America",
"Oceania": "projects/sat-io/open-datasets/GRIP4/Oceania",
"South-East-Asia": "projects/sat-io/open-datasets/GRIP4/South-East-Asia",
}
_TIGER_BLOCK_GROUPS_2020 = "TIGER/2020/BG"
_TIGER_TRACTS_2020 = "TIGER/2020/TRACT"
# Admin boundary sources — keyed by (source, level)
# geoBoundaries v6 (official GEE catalog, levels 0-2)
_GEOB_V6 = {
0: "WM/geoLab/geoBoundaries/600/ADM0",
1: "WM/geoLab/geoBoundaries/600/ADM1",
2: "WM/geoLab/geoBoundaries/600/ADM2",
}
# FAO GAUL 2015 (official GEE catalog, levels 0-2)
_GAUL_2015 = {
0: "FAO/GAUL/2015/level0",
1: "FAO/GAUL/2015/level1",
2: "FAO/GAUL/2015/level2",
}
# FAO GAUL 2024 (community catalog, levels 0-2)
_GAUL_2024 = {
0: "projects/sat-io/open-datasets/FAO/GAUL/GAUL_2024_L0",
1: "projects/sat-io/open-datasets/FAO/GAUL/GAUL_2024_L1",
2: "projects/sat-io/open-datasets/FAO/GAUL/GAUL_2024_L2",
}
# FieldMaps humanitarian edge-matched (community catalog, levels 1-4)
_FIELDMAPS = {
1: "projects/sat-io/open-datasets/field-maps/edge-matched-humanitarian/adm1_polygons",
2: "projects/sat-io/open-datasets/field-maps/edge-matched-humanitarian/adm2_polygons",
3: "projects/sat-io/open-datasets/field-maps/edge-matched-humanitarian/adm3_polygons",
4: "projects/sat-io/open-datasets/field-maps/edge-matched-humanitarian/adm4_polygons",
}
_ADMIN_SOURCES = {
"geob": _GEOB_V6,
"gaul": _GAUL_2015,
"gaul2024": _GAUL_2024,
"fieldmaps": _FIELDMAPS,
}
# Name properties for each source (the column containing the admin unit name)
_ADMIN_NAME_PROPS = {
"geob": "shapeName",
"gaul": lambda level: f"ADM{level}_NAME",
"gaul2024": lambda level: f"gaul{level}_name",
"fieldmaps": lambda level: f"adm{level}_name",
}
# Buildings
_VIDA_COMBINED_ROOT = "projects/sat-io/open-datasets/VIDA_COMBINED"
_MS_BUILDINGS_ROOT = "projects/sat-io/open-datasets/MSBuildings"
_GOOGLE_OPEN_BUILDINGS = "GOOGLE/Research/open-buildings/v3/polygons"
# Protected areas
_WDPA = "WCMC/WDPA/current/polygons"
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _to_geometry(area):
"""Convert an ee.FeatureCollection, ee.Feature, or ee.Geometry to ee.Geometry."""
if isinstance(area, ee.FeatureCollection):
return area.geometry()
elif isinstance(area, ee.Feature):
return area.geometry()
elif isinstance(area, ee.Geometry):
return area
raise TypeError(f"Expected ee.FeatureCollection, ee.Feature, or ee.Geometry, got {type(area)}")
def _filter_bounds(asset_id, area):
"""Load a FeatureCollection and filter by area bounds."""
return ee.FeatureCollection(asset_id).filterBounds(_to_geometry(area))
def _get_intersecting_country_names(area, source="geob"):
"""Return a server-side ee.List of country names that intersect ``area``."""
geom = _to_geometry(area)
asset = _ADMIN_SOURCES.get(source, _GEOB_V6).get(0, _GEOB_V6[0])
name_prop = getAdminNameProperty(level=0, source=source) if source in _ADMIN_NAME_PROPS else "shapeName"
return ee.FeatureCollection(asset).filterBounds(geom).aggregate_array(name_prop)
def _get_intersecting_country_iso3(area):
"""Return ee.List of ISO-3 codes that intersect ``area`` (via geoBoundaries v6)."""
geom = _to_geometry(area)
countries = ee.FeatureCollection(_GEOB_V6[0]).filterBounds(geom)
return countries.aggregate_array("shapeGroup")
# ---------------------------------------------------------------------------
# Geometry helpers
# ---------------------------------------------------------------------------
[docs]
def simple_buffer(geom, size=15000):
"""Create a square buffer around a point using simple coordinate arithmetic.
A lightweight alternative to ``ee.Geometry.buffer()`` that avoids
the server-side geodesic circle computation. Transforms the
point to EPSG:3857 (Web Mercator), applies a latitude-corrected
offset so that ``size`` represents true ground meters, and
returns the polygon in EPSG:3857.
Accepts a point in any projection — it will be transformed to
EPSG:3857 internally.
Args:
geom (ee.Geometry): A point geometry in any projection.
size (int or float, optional): Half-width of the square in
meters on the ground. The resulting square spans
``2 * size`` on each side. Defaults to ``15000``
(producing a 30 km x 30 km square).
Returns:
ee.Geometry.Polygon: A square polygon centered on the input
point, defined in EPSG:3857.
Example:
>>> pt = ee.Geometry.Point([-111.5, 40.5])
>>> square = simple_buffer(pt, size=5000) # 10 km x 10 km
"""
projection = ee.Projection("EPSG:3857")
geom_3857 = geom.transform(projection)
coordinates = geom_3857.coordinates()
x = ee.Number(coordinates.get(0))
y = ee.Number(coordinates.get(1))
# Compensate for Mercator scale distortion: 1/cos(lat)
lat_rad = ee.Number(geom.transform("EPSG:4326").coordinates().get(1)) \
.multiply(3.141592653589793).divide(180)
scale_factor = ee.Number(1).divide(lat_rad.cos())
adjusted = ee.Number(size).multiply(scale_factor)
poly_pts = ee.List([
ee.List([x.subtract(adjusted), y.subtract(adjusted)]),
ee.List([x.subtract(adjusted), y.add(adjusted)]),
ee.List([x.add(adjusted), y.add(adjusted)]),
ee.List([x.add(adjusted), y.subtract(adjusted)]),
ee.List([x.subtract(adjusted), y.subtract(adjusted)]),
])
return ee.Geometry.Polygon(poly_pts, projection)
# ---------------------------------------------------------------------------
# Political / administrative boundaries
# ---------------------------------------------------------------------------
[docs]
def getAdminBoundaries(area, level=0, source="geob"):
"""Return administrative boundaries at a given level that intersect ``area``.
Levels follow the standard admin hierarchy:
- **0** — Countries
- **1** — States / provinces
- **2** — Districts / counties / municipalities
- **3** — Sub-districts / wards (FieldMaps only)
- **4** — Neighborhoods / localities (FieldMaps only)
Available sources and their level coverage:
- ``"geob"`` — geoBoundaries v6.0 (official GEE catalog, levels 0–2).
Name property: ``shapeName``.
- ``"gaul"`` — FAO GAUL 2015 (official GEE catalog, levels 0–2).
Name property: ``ADM{level}_NAME`` (e.g. ``ADM0_NAME``).
- ``"gaul2024"`` — FAO GAUL 2024 (community catalog, levels 0–2).
Name property: ``gaul{level}_name``.
- ``"fieldmaps"`` — FieldMaps humanitarian edge-matched boundaries
(community catalog, levels 1–4). Name property:
``adm{level}_name``. Includes parent admin names and ISO codes.
For levels 3–4, if the requested source doesn't support them the
function automatically falls back to FieldMaps.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry to filter by.
level (int): Administrative level (0–4). Default ``0``.
source (str): Boundary source. Default ``"geob"``.
Returns:
ee.FeatureCollection of admin boundary polygons.
Example:
>>> countries = getAdminBoundaries(my_area, level=0)
>>> states = getAdminBoundaries(my_area, level=1)
>>> districts = getAdminBoundaries(my_area, level=2, source="gaul")
>>> wards = getAdminBoundaries(my_area, level=3) # auto-uses FieldMaps
"""
source_assets = _ADMIN_SOURCES.get(source)
if source_assets is None:
raise ValueError(
f"Unknown source: {source!r}. "
f"Use one of: {', '.join(repr(s) for s in _ADMIN_SOURCES)}."
)
asset = source_assets.get(level)
# Auto-fallback to FieldMaps for levels not in the requested source
if asset is None and source != "fieldmaps":
asset = _FIELDMAPS.get(level)
if asset is None:
available = sorted(source_assets.keys())
raise ValueError(
f"Admin level {level} is not available for source {source!r}. "
f"Available levels: {available}. "
f"Levels 3–4 are available via source='fieldmaps'."
)
return _filter_bounds(asset, area)
[docs]
def getAdminNameProperty(level=0, source="geob"):
"""Return the feature property name that contains the admin unit name.
Useful for setting ``feature_label`` in ``summarize_and_chart`` or
``selectLayerNameProperty`` in ``Map.addSelectLayer``.
Args:
level (int): Administrative level (0–4).
source (str): Boundary source (same options as :func:`getAdminBoundaries`).
Returns:
str: The property name (e.g. ``"shapeName"``, ``"ADM1_NAME"``).
Example:
>>> prop = getAdminNameProperty(level=1, source="gaul") # "ADM1_NAME"
"""
name_prop = _ADMIN_NAME_PROPS.get(source)
if name_prop is None:
raise ValueError(f"Unknown source: {source!r}.")
if callable(name_prop):
return name_prop(level)
return name_prop
# ---------------------------------------------------------------------------
# US-specific political/census boundaries
# ---------------------------------------------------------------------------
[docs]
def getUSStates(area):
"""Return US state boundaries (TIGER 2018) that intersect ``area``.
Properties include ``NAME``, ``STUSPS`` (abbreviation), ``STATEFP``
(FIPS code), ``REGION``, ``DIVISION``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
Returns:
ee.FeatureCollection.
"""
return _filter_bounds(_TIGER_STATES, area)
[docs]
def getUSCounties(area, state_fips=None, state_abbr=None):
"""Return US county boundaries that intersect ``area``.
Optionally filter to a single state by FIPS code or postal abbreviation.
Properties include ``NAME``, ``FULL_NAME``, ``STATEFP``, ``STUSPS``,
``COUNTYFP``, ``GEOID``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
state_fips (str, optional): Two-digit state FIPS (e.g. ``"49"`` for Utah).
state_abbr (str, optional): Two-letter postal abbreviation (e.g. ``"UT"``).
Returns:
ee.FeatureCollection.
"""
fc = _filter_bounds(_TIGER_COUNTIES, area)
if state_fips is not None:
fc = fc.filter(ee.Filter.eq("STATEFP", state_fips))
if state_abbr is not None:
fc = fc.filter(ee.Filter.eq("STUSPS", state_abbr.upper()))
return fc
[docs]
def getUSUrbanAreas(area):
"""Return TIGER 2024 urban area boundaries that intersect ``area``.
Properties include ``NAME20``, ``NAMELSAD20``, ``ALAND20``, ``AWATER20``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
Returns:
ee.FeatureCollection.
"""
return _filter_bounds(_TIGER_URBAN_AREAS, area)
[docs]
def getUSCensusBlocks(area):
"""Return TIGER 2020 census blocks that intersect ``area``.
.. warning::
Census blocks are extremely numerous. Use a small study area
or the query may be slow / exceed memory limits.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
Returns:
ee.FeatureCollection.
"""
return _filter_bounds(_TIGER_BLOCKS_2020, area)
[docs]
def getUSBlockGroups(area):
"""Return TIGER 2020 census block groups that intersect ``area``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
Returns:
ee.FeatureCollection.
"""
return _filter_bounds(_TIGER_BLOCK_GROUPS_2020, area)
[docs]
def getUSCensusTracts(area):
"""Return TIGER 2020 census tracts that intersect ``area``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
Returns:
ee.FeatureCollection.
"""
return _filter_bounds(_TIGER_TRACTS_2020, area)
# ---------------------------------------------------------------------------
# USFS Administrative boundaries
# ---------------------------------------------------------------------------
[docs]
def getUSFSForests(area, region=None):
"""Return USFS National Forest boundaries that intersect ``area``.
Properties include ``FORESTNAME``, ``FORESTNUMB``, ``REGION``,
``FORESTORGC``, ``GIS_ACRES``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
region (str, optional): Two-digit USFS region number to filter by
(e.g. ``"01"`` for Northern Region).
Returns:
ee.FeatureCollection.
"""
fc = _filter_bounds(_USFS_FORESTS, area)
if region is not None:
fc = fc.filter(ee.Filter.eq("REGION", str(region).zfill(2)))
return fc
[docs]
def getUSFSDistricts(area, forest_name=None, region=None):
"""Return USFS Ranger District boundaries that intersect ``area``.
Properties include ``DISTRICTNA``, ``FORESTNAME``, ``FORESTNUMB``,
``REGION``, ``GIS_ACRES``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
forest_name (str, optional): Filter to districts within a specific
National Forest (exact match on ``FORESTNAME``).
region (str, optional): Two-digit USFS region number.
Returns:
ee.FeatureCollection.
"""
fc = _filter_bounds(_USFS_DISTRICTS, area)
if forest_name is not None:
fc = fc.filter(ee.Filter.eq("FORESTNAME", forest_name))
if region is not None:
fc = fc.filter(ee.Filter.eq("REGION", str(region).zfill(2)))
return fc
[docs]
def getUSFSRegions(area):
"""Return USFS region boundaries that intersect ``area``.
Properties include ``REGION``, ``REGIONNAME``, ``REGIONHEAD``
(headquarters city), ``FS_ADMINAC`` (admin acres).
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
Returns:
ee.FeatureCollection with one feature per USFS region.
"""
return _filter_bounds(_USFS_REGIONS, area)
# ---------------------------------------------------------------------------
# Roads
# ---------------------------------------------------------------------------
[docs]
def getRoads(area, source="tiger", year=2024):
"""Return road features that intersect ``area``.
Supports two road data sources covering different geographies and
classification schemes.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
source (str): Road data source:
- ``"tiger"`` (default) — US Census TIGER roads. Detailed
classification via MTFCC codes. Available 2012-2025 via
the community catalog, plus 2016 in the official GEE
catalog. US only. Properties: ``FULLNAME``, ``MTFCC``,
``RTTYP``, ``LINEARID``.
- ``"grip"`` — GRIP4 (Global Roads Inventory Project).
Global coverage across 7 regional shards. Road type
classification via ``GP_RTP`` (1=Highway, 2=Primary,
3=Secondary, 4=Tertiary, 5=Local). Based on
OpenStreetMap and other sources. CC-BY 4.0.
year (int): Year for TIGER roads (2012-2025). Ignored for
other sources. Years other than 2016 use the community
catalog (``projects/sat-io/open-datasets/TIGER/{year}/Roads``).
Defaults to ``2024``.
Returns:
ee.FeatureCollection of road line features.
Common TIGER MTFCC codes:
- ``S1100`` — Primary road (interstate)
- ``S1200`` — Secondary road (US/state highway)
- ``S1400`` — Local road
- ``S1500`` — Vehicular trail (4WD)
- ``S1630`` — Ramp
- ``S1640`` — Service drive
- ``S1730`` — Alley
- ``S1780`` — Parking lot road
- ``S1820`` — Bike path / trail
GRIP4 GP_RTP road types:
- ``1`` — Highway
- ``2`` — Primary road
- ``3`` — Secondary road
- ``4`` — Tertiary road
- ``5`` — Local / residential
Examples:
>>> # US interstates from TIGER 2024
>>> interstates = getRoads(my_area).filter(ee.Filter.eq('MTFCC', 'S1100'))
>>> # Global highways from GRIP4
>>> highways = getRoads(my_area, source='grip').filter(ee.Filter.eq('GP_RTP', 1))
>>> # TIGER roads from a specific year
>>> roads_2020 = getRoads(my_area, year=2020)
"""
source = str(source).lower().strip()
if source == "tiger":
year = int(year)
if year == 2016:
asset_id = _TIGER_ROADS
elif 2012 <= year <= 2025:
asset_id = _TIGER_ROADS_COMMUNITY.format(year=year)
else:
raise ValueError(f"TIGER roads year must be 2012-2025, got {year}")
return _filter_bounds(asset_id, area)
if source == "grip":
return _get_multi_region_roads(area, _GRIP4_REGIONS)
raise ValueError(f"Unknown roads source: {source!r}. Use 'tiger' or 'grip'.")
def _get_multi_region_roads(area, region_dict):
"""Load and merge regional road FeatureCollections that intersect ``area``.
Tries all regions and merges those that have features intersecting
the given area. Since road collections are large, filtering by
bounds is essential.
"""
geom = _to_geometry(area)
collections = [
ee.FeatureCollection(asset_id).filterBounds(geom)
for asset_id in region_dict.values()
]
return ee.FeatureCollection(collections).flatten()
# ---------------------------------------------------------------------------
# Buildings
# ---------------------------------------------------------------------------
[docs]
def getBuildings(area, source="vida"):
"""Return building footprints that intersect ``area``.
This function determines which countries intersect the given area,
then loads and merges per-country building footprint collections.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
source (str): Building footprint source.
- ``"vida"`` — VIDA Combined Building Footprints (179 countries,
ISO-3 keyed). Properties: ``area_in_meters``, ``confidence``,
``bf_source``.
- ``"ms"`` — Microsoft Building Footprints (202 countries,
country-name keyed). Properties vary by country.
- ``"google"`` — Google Open Buildings v3 (Africa, South/Southeast
Asia). Properties: ``area_in_meters``, ``confidence``,
``full_plus_code``.
Returns:
ee.FeatureCollection of building footprint polygons.
Note:
Building collections are very large. Use a small study area
or the query may be slow / exceed memory limits.
Example:
>>> buildings = getBuildings(ee.Geometry.Point([-111, 40.7]).buffer(1000))
"""
geom = _to_geometry(area)
if source == "google":
return ee.FeatureCollection(_GOOGLE_OPEN_BUILDINGS).filterBounds(geom)
if source == "vida":
return _get_multi_country_fc(
geom,
root=_VIDA_COMBINED_ROOT,
key_type="iso3",
)
if source == "ms":
return _get_multi_country_fc(
geom,
root=_MS_BUILDINGS_ROOT,
key_type="country_name",
)
raise ValueError(f"Unknown building source: {source!r}. Use 'vida', 'ms', or 'google'.")
# Country-name mapping for MS Buildings (ISO-3 → folder name)
_MS_ISO3_TO_NAME = {
"USA": "US", "GBR": "United_Kingdom", "DEU": "Germany", "FRA": "France",
"ITA": "Italy", "ESP": "Spain", "CAN": "Canada", "AUS": "Australia",
"BRA": "Brazil", "MEX": "Mexico", "ARG": "Argentina", "COL": "Colombia",
"PER": "Peru", "CHL": "Chile", "JPN": "Japan", "CHN": "China",
"IND": "India", "RUS": "Russia", "ZAF": "South_Africa", "NGA": "Nigeria",
"KEN": "Kenya", "EGY": "Egypt", "MAR": "Morocco", "DZA": "Algeria",
"TUN": "Tunisia", "LBY": "Libya", "SDN": "Sudan", "ETH": "Ethiopia",
"TZA": "Tanzania", "UGA": "Uganda", "GHA": "Ghana", "CMR": "Cameroon",
"CIV": "Ivory_Coast", "SEN": "Senegal", "MLI": "Mali", "BFA": "Burkina_Faso",
"NER": "Niger", "TCD": "Chad", "COD": "Congo_DRC", "COG": "Republic_of_the_Congo",
"AGO": "Angola", "MOZ": "Mozambique", "MDG": "Madagascar", "MWI": "Malawi",
"ZMB": "Zambia", "ZWE": "Zimbabwe", "BWA": "Botswana", "NAM": "Namibia",
"SWZ": "Swaziland", "LSO": "Lesotho", "MUS": "Mauritius", "SYC": "Seychelles",
"RWA": "Rwanda", "BDI": "Burundi", "SSD": "South_Sudan", "SOM": "Somalia",
"DJI": "Djibouti", "ERI": "Eritrea", "CAF": "Central_African_Republic",
"GNQ": "Equatorial_Guinea", "GAB": "Gabon", "STP": "Sao_Tome_and_Principe",
"CPV": "Cape_Verde", "GMB": "The_Gambia", "GNB": "Guinea-Bissau",
"GIN": "Guinea", "SLE": "Sierra_Leone", "LBR": "Liberia", "TGO": "Togo",
"BEN": "Benin", "MRT": "Mauritania", "PAK": "Pakistan", "BGD": "Bangladesh",
"LKA": "Sri_Lanka", "NPL": "Nepal", "BTN": "Bhutan", "MMR": "Myanmar",
"THA": "Thailand", "VNM": "Vietnam", "LAO": "Laos", "KHM": "Cambodia",
"MYS": "Malaysia", "IDN": "Indonesia", "PHL": "Philippines", "MNG": "Mongolia",
"KAZ": "Kazakhstan", "UZB": "Uzbekistan", "TKM": "Turkmenistan",
"TJK": "Tajikistan", "KGZ": "Kyrgyzstan", "AFG": "Afghanistan",
"IRN": "Iran", "IRQ": "Iraq", "SYR": "Syria", "JOR": "Jordan",
"LBN": "Lebanon", "ISR": "Israel", "SAU": "Kingdom_of_Saudi_Arabia",
"YEM": "Republic_of_Yemen", "OMN": "Sultanate_of_Oman", "ARE": "United_Arab_Emirates",
"QAT": "State_of_Qatar", "BHR": "Bahrain", "KWT": "Kuwait",
"TUR": "Turkey", "GEO": "Georgia", "ARM": "Armenia", "AZE": "Azerbaijan",
"UKR": "Ukraine", "BLR": "Belarus", "MDA": "Moldova", "ROU": "Romania",
"BGR": "Bulgaria", "SRB": "Serbia", "MNE": "Montenegro", "BIH": "Bosnia_and_Herzegovina",
"HRV": "Croatia", "SVN": "Slovenia", "MKD": "FYRO_Makedonija",
"ALB": "Albania", "GRC": "Greece", "CYP": "Cyprus", "MLT": "Malta",
"POL": "Poland", "CZE": "Czech_Republic", "SVK": "Slovakia", "HUN": "Hungary",
"AUT": "Austria", "CHE": "Switzerland", "NLD": "Netherlands", "BEL": "Belgium",
"LUX": "Luxembourg", "DNK": "Denmark", "SWE": "Sweden", "NOR": "Norway",
"FIN": "Finland", "ISL": "Iceland", "IRL": "Ireland", "PRT": "Portugal",
"EST": "Estonia", "LVA": "Latvia", "LTU": "Lithuania", "AND": "Andorra",
"MCO": "Monaco", "SMR": "San_Marino", "VAT": "Vatican_City",
"ECU": "Ecuador", "VEN": "Venezuela", "BOL": "Bolivia", "PRY": "Paraguay",
"URY": "Uruguay", "GUY": "Guyana", "SUR": "Suriname", "GTM": "Guatemala",
"HND": "Honduras", "SLV": "El_Salvador", "NIC": "Nicaragua", "CRI": "Costa_Rica",
"PAN": "Panama", "CUB": "Cuba", "HTI": "Haiti", "DOM": "Dominican_Republic",
"JAM": "Jamaica", "TTO": "Trinidad_and_Tobago", "BRB": "Barbados",
"BHS": "The_Bahamas", "BLZ": "Belize", "GRD": "Grenada", "LCA": "Saint_Lucia",
"DMA": "Dominica", "KNA": "St_Kitts_and_Nevis", "VCT": "St_Vincent_and_the_Grenadines",
"ATG": "Antigua_and_Barbuda", "MDV": "Maldives", "BRN": "Brunei",
"PNG": "Papua_New_Guinea", "KSV": "Kosovo",
}
def _get_multi_country_fc(geom, root, key_type="iso3"):
"""Load and merge per-country FeatureCollections that intersect ``geom``.
Uses a client-side approach: first determines which countries intersect
the area (via ``getInfo()``), then builds constant asset IDs and merges
the results server-side. This is necessary because ``ee.FeatureCollection``
requires a constant string for the asset ID.
Args:
geom: ee.Geometry to filter by.
root: Root asset folder (e.g. VIDA_COMBINED or MSBuildings).
key_type: ``"iso3"`` for VIDA (subfolder is ISO-3 code) or
``"country_name"`` for MS Buildings (subfolder is country name).
Returns:
ee.FeatureCollection — merged and spatially filtered.
"""
# Client-side: determine which countries intersect
iso3_codes = _get_intersecting_country_iso3(geom).getInfo()
if not iso3_codes:
return ee.FeatureCollection([])
collections = []
for iso3 in iso3_codes:
if key_type == "iso3":
asset_ids = [f"{root}/{iso3}"]
else:
country_name = _MS_ISO3_TO_NAME.get(iso3)
if country_name is None:
continue
# Some MS Buildings entries (e.g. US) are IndexedFolders
# with per-state sub-collections. Try loading them and
# fall back to listing sub-assets.
asset_ids = [f"{root}/{country_name}"]
for asset_id in asset_ids:
try:
# Verify asset is a TABLE, not an IndexedFolder
info = ee.data.getAsset(asset_id)
asset_type = info.get("type", "")
if asset_type in ("TABLE", "FEATURE_COLLECTION"):
fc = ee.FeatureCollection(asset_id).filterBounds(geom)
collections.append(fc)
elif asset_type == "FOLDER":
# IndexedFolder — load sub-assets
sub_result = ee.data.listAssets({"parent": asset_id})
for sub in sub_result.get("assets", []):
if sub.get("type") in ("TABLE", "FEATURE_COLLECTION"):
sub_fc = ee.FeatureCollection(sub["name"]).filterBounds(geom)
collections.append(sub_fc)
except Exception:
pass
if not collections:
return ee.FeatureCollection([])
merged = collections[0]
for fc in collections[1:]:
merged = merged.merge(fc)
return merged
# ---------------------------------------------------------------------------
# Protected areas
# ---------------------------------------------------------------------------
[docs]
def getProtectedAreas(area, iucn_cat=None, desig_type=None):
"""Return WDPA protected area polygons that intersect ``area``.
Properties include ``NAME``, ``DESIG_ENG``, ``IUCN_CAT``, ``STATUS``,
``STATUS_YR``, ``GOV_TYPE``, ``DESIG_TYPE``, ``REP_AREA``, ``GIS_AREA``,
``ISO3``.
Args:
area: ee.FeatureCollection, ee.Feature, or ee.Geometry.
iucn_cat (str, optional): Filter by IUCN category (e.g.
``"II"`` for National Parks, ``"Ia"`` for Strict Nature Reserve).
desig_type (str, optional): Filter by designation type
(``"National"``, ``"Regional"``, ``"International"``,
``"Not Applicable"``).
Returns:
ee.FeatureCollection.
"""
fc = _filter_bounds(_WDPA, area)
if iucn_cat is not None:
fc = fc.filter(ee.Filter.eq("IUCN_CAT", iucn_cat))
if desig_type is not None:
fc = fc.filter(ee.Filter.eq("DESIG_TYPE", desig_type))
return fc
# ---------------------------------------------------------------------------
# Convenience: all available summary area types
# ---------------------------------------------------------------------------
AVAILABLE_SUMMARY_AREAS = {
"admin_boundaries": {
"function": "getAdminBoundaries",
"description": "Admin boundaries: level 0 (countries), 1 (states), 2 (counties), 3 (sub-districts), 4 (localities). Sources: geob, gaul, gaul2024, fieldmaps.",
},
"us_states": {
"function": "getUSStates",
"description": "US state boundaries (TIGER 2018)",
},
"us_counties": {
"function": "getUSCounties",
"description": "US county boundaries with names (TIGER 2024)",
},
"us_urban_areas": {
"function": "getUSUrbanAreas",
"description": "US urban area boundaries (TIGER 2024)",
},
"us_census_blocks": {
"function": "getUSCensusBlocks",
"description": "US census blocks (TIGER 2020)",
},
"us_block_groups": {
"function": "getUSBlockGroups",
"description": "US census block groups (TIGER 2020)",
},
"us_census_tracts": {
"function": "getUSCensusTracts",
"description": "US census tracts (TIGER 2020)",
},
"usfs_forests": {
"function": "getUSFSForests",
"description": "USFS National Forest boundaries",
},
"usfs_districts": {
"function": "getUSFSDistricts",
"description": "USFS Ranger District boundaries",
},
"usfs_regions": {
"function": "getUSFSRegions",
"description": "USFS region boundaries (dissolved from forests)",
},
"roads": {
"function": "getRoads",
"description": "TIGER 2016 road features",
},
"buildings": {
"function": "getBuildings",
"description": "Building footprints (VIDA, MS, or Google)",
},
"protected_areas": {
"function": "getProtectedAreas",
"description": "WDPA protected area polygons",
},
}