Spaces:
Sleeping
Sleeping
Update pages/_common_m.py
Browse files- pages/_common_m.py +42 -35
pages/_common_m.py
CHANGED
|
@@ -22,27 +22,19 @@ IUCN_ASSET = f"{EE_LAMER_ROOT}/iucn_ranges"
|
|
| 22 |
# =============================================================================
|
| 23 |
|
| 24 |
RESIDENT_PARTS = {
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
-
"M_buffer1deg": ["M_buffer1deg_part1", "M_buffer1deg_part2"],
|
| 28 |
-
"M_occsrangehull": ["M_occsrangehull_part1", "M_occsrangehull_part2"],
|
| 29 |
}
|
| 30 |
|
| 31 |
MIGRANT_PARTS = {
|
| 32 |
-
("breeding","
|
| 33 |
-
("breeding","
|
| 34 |
-
|
| 35 |
-
("
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
("
|
| 39 |
-
("
|
| 40 |
-
("nonbreeding","M_occsrangehull"): ["nonbreeding_M_occsrangehull_part1","nonbreeding_M_occsrangehull_part2"],
|
| 41 |
-
|
| 42 |
-
("year_round","M_adj"): ["year_round_M_adj"],
|
| 43 |
-
("year_round","M_noadj"): ["year_round_M_noadj"],
|
| 44 |
-
("year_round","M_buffer1deg"): ["year_round_M_buffer1deg"],
|
| 45 |
-
("year_round","M_occsrangehull"): ["year_round_M_occsrangehull"],
|
| 46 |
}
|
| 47 |
|
| 48 |
# =============================================================================
|
|
@@ -50,13 +42,11 @@ MIGRANT_PARTS = {
|
|
| 50 |
# =============================================================================
|
| 51 |
|
| 52 |
M_METADATA = {
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"M_buffer1deg": ("M - 1° Buffer", "Ecoregions within 1° buffer"),
|
| 56 |
-
"M_occsrangehull": ("M - Occurrence Hull", "Convex hull"),
|
| 57 |
}
|
| 58 |
|
| 59 |
-
M_ORDER = ["
|
| 60 |
BIRD_TYPES = ["Residents", "Migrants"]
|
| 61 |
SEASONS = ["breeding", "nonbreeding", "year_round"]
|
| 62 |
PASS_DECISIONS = ["pass","reviewed_pass","proxim_pass"]
|
|
@@ -85,6 +75,7 @@ center = solara.reactive([0, -70])
|
|
| 85 |
|
| 86 |
reviewer_name = solara.reactive("")
|
| 87 |
reviewer_notes = solara.reactive("")
|
|
|
|
| 88 |
|
| 89 |
submission_status = solara.reactive("idle")
|
| 90 |
submission_result = solara.reactive(None)
|
|
@@ -131,16 +122,18 @@ def get_species_list(btype=None, season=None):
|
|
| 131 |
|
| 132 |
def get_m_fc(hyp, species, btype, season=None):
|
| 133 |
if btype == "Residents":
|
| 134 |
-
parts = RESIDENT_PARTS
|
| 135 |
root = EE_RESIDENT_ROOT
|
| 136 |
filt = ee.Filter.eq("species", species)
|
| 137 |
else:
|
| 138 |
-
parts = MIGRANT_PARTS
|
| 139 |
root = EE_MIGRANT_ROOT
|
| 140 |
filt = ee.Filter.And(
|
| 141 |
ee.Filter.eq("species", species),
|
| 142 |
ee.Filter.eq("season", season),
|
| 143 |
)
|
|
|
|
|
|
|
| 144 |
fcs = [ee.FeatureCollection(f"{root}/{p}") for p in parts]
|
| 145 |
fc = ee.FeatureCollection(fcs).flatten() if len(fcs) > 1 else fcs[0]
|
| 146 |
return fc.filter(filt)
|
|
@@ -154,6 +147,18 @@ def compute_eco_color_map(species, btype=None, season=None):
|
|
| 154 |
ecos.update([e for e in vals if e])
|
| 155 |
return {eco: ECO_PALETTE[i % len(ECO_PALETTE)] for i, eco in enumerate(sorted(ecos))}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
def get_eco_geojson(hypothesis, species, btype=None, season=None, eco_color_map=None):
|
| 158 |
try:
|
| 159 |
fc = get_m_fc(hypothesis, species, btype, season)
|
|
@@ -185,7 +190,8 @@ def get_m_bounds(hypothesis, species, btype=None, season=None):
|
|
| 185 |
# =============================================================================
|
| 186 |
|
| 187 |
def create_m_map(hypothesis, species, eco_color_map, btype=None, season=None):
|
| 188 |
-
|
|
|
|
| 189 |
geojson = get_eco_geojson(hypothesis, species, btype, season, eco_color_map)
|
| 190 |
if geojson:
|
| 191 |
m.add_geojson(geojson, layer_name="M regions", info_mode="on_click")
|
|
@@ -193,17 +199,20 @@ def create_m_map(hypothesis, species, eco_color_map, btype=None, season=None):
|
|
| 193 |
occ = ee.FeatureCollection(get_pres_backg_asset(btype, season)).filter(ee.Filter.eq("species", species))
|
| 194 |
fail = occ.filter(ee.Filter.inList("final_decision", FAIL_DECISIONS))
|
| 195 |
passed = occ.filter(ee.Filter.inList("final_decision", PASS_DECISIONS))
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
except Exception as e:
|
| 199 |
print("[WARN points]", e)
|
| 200 |
return m
|
| 201 |
|
| 202 |
# =============================================================================
|
| 203 |
-
# SUBMISSION
|
| 204 |
# =============================================================================
|
| 205 |
|
| 206 |
-
def submit_review(hypothesis, species, reviewer, notes, btype, season=None):
|
| 207 |
"""Submits review data to Airtable using specific column names."""
|
| 208 |
api_key = os.environ.get("AIRTABLE_API_KEY")
|
| 209 |
base_id = os.environ.get("AIRTABLE_BASE_ID")
|
|
@@ -218,15 +227,14 @@ def submit_review(hypothesis, species, reviewer, notes, btype, season=None):
|
|
| 218 |
"Content-Type": "application/json"
|
| 219 |
}
|
| 220 |
|
| 221 |
-
#
|
| 222 |
-
# Reviewer, Species, Selected_Hypothesis, Notes
|
| 223 |
-
# (Timestamp is usually handled automatically by Airtable if set to 'Created Time')
|
| 224 |
data = {
|
| 225 |
"fields": {
|
| 226 |
"Reviewer": reviewer,
|
| 227 |
"Species": species,
|
| 228 |
"Selected_Hypothesis": hypothesis,
|
| 229 |
-
"Notes": notes
|
|
|
|
| 230 |
}
|
| 231 |
}
|
| 232 |
|
|
@@ -234,7 +242,6 @@ def submit_review(hypothesis, species, reviewer, notes, btype, season=None):
|
|
| 234 |
response = requests.post(url, headers=headers, json=data)
|
| 235 |
if response.status_code != 200:
|
| 236 |
error_info = response.json().get("error", {})
|
| 237 |
-
# Provides the specific reason Airtable rejected the data
|
| 238 |
msg = error_info.get("message", "Unknown error") if isinstance(error_info, dict) else error_info
|
| 239 |
return {"success": False, "error": f"Airtable Error: {msg}"}
|
| 240 |
|
|
|
|
| 22 |
# =============================================================================
|
| 23 |
|
| 24 |
RESIDENT_PARTS = {
|
| 25 |
+
"M": ["M"],
|
| 26 |
+
"M_ecoregions": ["M_ecoregions"],
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
MIGRANT_PARTS = {
|
| 30 |
+
("breeding","M"): ["breeding_M"],
|
| 31 |
+
("breeding","M_ecoregions"): ["breeding_M_ecoregions"],
|
| 32 |
+
|
| 33 |
+
("nonbreeding","M"): ["nonbreeding_M"],
|
| 34 |
+
("nonbreeding","M_ecoregions"): ["nonbreeding_M_ecoregions"],
|
| 35 |
+
|
| 36 |
+
("year_round","M"): ["year_round_M"],
|
| 37 |
+
("year_round","M_ecoregions"): ["year_round_M_ecoregions"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
# =============================================================================
|
|
|
|
| 42 |
# =============================================================================
|
| 43 |
|
| 44 |
M_METADATA = {
|
| 45 |
+
"M": ("M", "M Hypothesis"),
|
| 46 |
+
"M_ecoregions": ("M Ecoregions", "Restricted by Ecoregions"),
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
+
M_ORDER = ["M", "M_ecoregions"]
|
| 50 |
BIRD_TYPES = ["Residents", "Migrants"]
|
| 51 |
SEASONS = ["breeding", "nonbreeding", "year_round"]
|
| 52 |
PASS_DECISIONS = ["pass","reviewed_pass","proxim_pass"]
|
|
|
|
| 75 |
|
| 76 |
reviewer_name = solara.reactive("")
|
| 77 |
reviewer_notes = solara.reactive("")
|
| 78 |
+
reviewer_ecoregions = solara.reactive("")
|
| 79 |
|
| 80 |
submission_status = solara.reactive("idle")
|
| 81 |
submission_result = solara.reactive(None)
|
|
|
|
| 122 |
|
| 123 |
def get_m_fc(hyp, species, btype, season=None):
|
| 124 |
if btype == "Residents":
|
| 125 |
+
parts = RESIDENT_PARTS.get(hyp, [])
|
| 126 |
root = EE_RESIDENT_ROOT
|
| 127 |
filt = ee.Filter.eq("species", species)
|
| 128 |
else:
|
| 129 |
+
parts = MIGRANT_PARTS.get((season, hyp), [])
|
| 130 |
root = EE_MIGRANT_ROOT
|
| 131 |
filt = ee.Filter.And(
|
| 132 |
ee.Filter.eq("species", species),
|
| 133 |
ee.Filter.eq("season", season),
|
| 134 |
)
|
| 135 |
+
if not parts:
|
| 136 |
+
return ee.FeatureCollection([])
|
| 137 |
fcs = [ee.FeatureCollection(f"{root}/{p}") for p in parts]
|
| 138 |
fc = ee.FeatureCollection(fcs).flatten() if len(fcs) > 1 else fcs[0]
|
| 139 |
return fc.filter(filt)
|
|
|
|
| 147 |
ecos.update([e for e in vals if e])
|
| 148 |
return {eco: ECO_PALETTE[i % len(ECO_PALETTE)] for i, eco in enumerate(sorted(ecos))}
|
| 149 |
|
| 150 |
+
def get_species_ecoregions(species, btype=None, season=None):
|
| 151 |
+
"""Fetches unique ecoregions from the M_ecoregions hypothesis layer."""
|
| 152 |
+
if not species: return ""
|
| 153 |
+
try:
|
| 154 |
+
fc = get_m_fc("M_ecoregions", species, btype, season)
|
| 155 |
+
vals = fc.aggregate_array("ECO_NAME").distinct().getInfo()
|
| 156 |
+
valid_vals = [str(e) for e in vals if e]
|
| 157 |
+
return "; ".join(sorted(valid_vals))
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print("[WARN ecoregions]", e)
|
| 160 |
+
return ""
|
| 161 |
+
|
| 162 |
def get_eco_geojson(hypothesis, species, btype=None, season=None, eco_color_map=None):
|
| 163 |
try:
|
| 164 |
fc = get_m_fc(hypothesis, species, btype, season)
|
|
|
|
| 190 |
# =============================================================================
|
| 191 |
|
| 192 |
def create_m_map(hypothesis, species, eco_color_map, btype=None, season=None):
|
| 193 |
+
# 1. Updated basemap to a simple gray map ("CartoDB.Positron")
|
| 194 |
+
m = geemap.Map(zoom=zoom.value, center=center.value, toolbar_control=False, lite_mode=True, basemap="CartoDB.Positron")
|
| 195 |
geojson = get_eco_geojson(hypothesis, species, btype, season, eco_color_map)
|
| 196 |
if geojson:
|
| 197 |
m.add_geojson(geojson, layer_name="M regions", info_mode="on_click")
|
|
|
|
| 199 |
occ = ee.FeatureCollection(get_pres_backg_asset(btype, season)).filter(ee.Filter.eq("species", species))
|
| 200 |
fail = occ.filter(ee.Filter.inList("final_decision", FAIL_DECISIONS))
|
| 201 |
passed = occ.filter(ee.Filter.inList("final_decision", PASS_DECISIONS))
|
| 202 |
+
|
| 203 |
+
# 2 & 3. Made points smaller and changed fail point to "triangle"
|
| 204 |
+
# (Earth Engine Server-Side styling does not support a "teardrop" shape natively)
|
| 205 |
+
m.addLayer(fail.style(**{"color": "ff4444", "pointSize": 3, "pointShape": "triangle"}), {}, "Fail")
|
| 206 |
+
m.addLayer(passed.style(**{"color": "2ca02c", "pointSize": 3, "pointShape": "circle"}), {}, "Pass")
|
| 207 |
except Exception as e:
|
| 208 |
print("[WARN points]", e)
|
| 209 |
return m
|
| 210 |
|
| 211 |
# =============================================================================
|
| 212 |
+
# SUBMISSION
|
| 213 |
# =============================================================================
|
| 214 |
|
| 215 |
+
def submit_review(hypothesis, species, reviewer, notes, ecoregions, btype, season=None):
|
| 216 |
"""Submits review data to Airtable using specific column names."""
|
| 217 |
api_key = os.environ.get("AIRTABLE_API_KEY")
|
| 218 |
base_id = os.environ.get("AIRTABLE_BASE_ID")
|
|
|
|
| 227 |
"Content-Type": "application/json"
|
| 228 |
}
|
| 229 |
|
| 230 |
+
# 4. Added Ecoregions column submission data
|
|
|
|
|
|
|
| 231 |
data = {
|
| 232 |
"fields": {
|
| 233 |
"Reviewer": reviewer,
|
| 234 |
"Species": species,
|
| 235 |
"Selected_Hypothesis": hypothesis,
|
| 236 |
+
"Notes": notes,
|
| 237 |
+
"Ecoregions": ecoregions
|
| 238 |
}
|
| 239 |
}
|
| 240 |
|
|
|
|
| 242 |
response = requests.post(url, headers=headers, json=data)
|
| 243 |
if response.status_code != 200:
|
| 244 |
error_info = response.json().get("error", {})
|
|
|
|
| 245 |
msg = error_info.get("message", "Unknown error") if isinstance(error_info, dict) else error_info
|
| 246 |
return {"success": False, "error": f"Airtable Error: {msg}"}
|
| 247 |
|