aeiche01 commited on
Commit
cd32fc2
·
verified ·
1 Parent(s): ab93a16

Update pages/_common.py

Browse files
Files changed (1) hide show
  1. pages/_common.py +168 -206
pages/_common.py CHANGED
@@ -1,108 +1,52 @@
1
- # ============================================================
2
- # _common.py — Hugging Face SAFE Earth Engine integration
3
- # ============================================================
4
-
5
  import os
6
  import json
7
  import ee
8
  import geemap
9
  import solara
10
- import google.oauth2.credentials
11
- from typing import Optional, Tuple
12
-
13
- # ============================================================
14
- # 1. Global flags & placeholders (SAFE at import time)
15
- # ============================================================
16
-
17
- PROJECT_ID = "scenic-sunspot-485920-j6"
18
-
19
- _EE_INITIALIZED = False
20
- _EE_INIT_ERROR: Optional[str] = None
21
-
22
- M_AREA = None
23
- IUCN_RANGE = None
24
- OCCURRENCES = None
25
-
26
 
27
  # ============================================================
28
- # 2. Earth Engine initialization (STRICT, LAZY)
29
  # ============================================================
30
 
31
- def ee_initialize(token_name="EARTHENGINE_TOKEN"):
32
- """
33
- Initialize Earth Engine using a service account JSON stored
34
- in a Hugging Face secret. Never runs at import time.
35
- """
36
- global _EE_INITIALIZED, _EE_INIT_ERROR
37
-
38
- if _EE_INITIALIZED:
39
- return
40
-
41
- ee_token = os.environ.get(token_name)
42
- if not ee_token:
43
- _EE_INIT_ERROR = f"Missing Hugging Face secret: {token_name}"
44
- return
45
-
46
- try:
47
- stored = json.loads(ee_token)
48
- except Exception as e:
49
- _EE_INIT_ERROR = "EARTHENGINE_TOKEN is not valid JSON"
50
- return
51
 
52
- if stored.get("type") != "service_account":
53
- _EE_INIT_ERROR = "EARTHENGINE_TOKEN must be a service account JSON"
54
- return
55
-
56
- try:
57
- credentials = ee.ServiceAccountCredentials(
58
- stored["client_email"],
59
- key_data=ee_token,
60
- )
61
- ee.Initialize(
62
- credentials=credentials,
63
- project=stored["project_id"],
64
- )
65
- _EE_INITIALIZED = True
66
- except Exception as e:
67
- _EE_INIT_ERROR = str(e)
68
-
69
-
70
- # ============================================================
71
- # 3. Lazy asset loader
72
- # ============================================================
73
-
74
- def ensure_assets_loaded():
75
- """
76
- Loads Earth Engine assets on first use.
77
- Safe to call multiple times.
78
- """
79
- global M_AREA, IUCN_RANGE, OCCURRENCES
80
-
81
- if M_AREA is not None:
82
- return
83
-
84
- ee_initialize()
85
- if not _EE_INITIALIZED:
86
  return
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  try:
89
- M_AREA = ee.FeatureCollection(
90
- "projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_M"
91
- )
92
- IUCN_RANGE = ee.FeatureCollection(
93
- "projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_iucnrange"
94
- )
95
- OCCURRENCES = ee.FeatureCollection(
96
- "projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_occurrence"
97
- )
98
- except Exception as e:
99
- _EE_INIT_ERROR = f"Asset load failed: {e}"
100
- M_AREA = IUCN_RANGE = OCCURRENCES = None
101
-
102
 
103
- # ============================================================
104
- # 4. Metadata (SAFE)
105
- # ============================================================
 
106
 
107
  SPECIES = "Acrobatornis fonsecai"
108
 
@@ -113,185 +57,203 @@ THRESHOLD_IMAGES = {
113
  "sensit": "projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_sensit",
114
  }
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  # ============================================================
118
- # 5. View computation (lazy)
119
  # ============================================================
120
-
121
- def compute_species_view(
122
- fallback_center=(-15, -50),
123
- fallback_zoom=4,
124
- ) -> Tuple[Tuple[float, float], int]:
125
- ensure_assets_loaded()
126
-
127
- if M_AREA is None:
128
- return fallback_center, fallback_zoom
129
-
130
  try:
131
- coords = M_AREA.geometry().bounds().coordinates().getInfo()[0]
132
  lons = [p[0] for p in coords]
133
  lats = [p[1] for p in coords]
134
-
135
  center = (sum(lats) / len(lats), sum(lons) / len(lons))
136
  span = max(max(lons) - min(lons), max(lats) - min(lats))
137
-
138
- if span > 60:
139
- zoom = 3
140
- elif span > 30:
141
- zoom = 4
142
- elif span > 15:
143
- zoom = 5
144
- elif span > 8:
145
- zoom = 6
146
- elif span > 4:
147
- zoom = 7
148
- else:
149
- zoom = 8
150
-
151
  return center, zoom
152
  except Exception:
153
  return fallback_center, fallback_zoom
154
 
 
155
 
156
  # ============================================================
157
- # 6. Global Solara state
158
  # ============================================================
159
 
 
160
  researcher_name = solara.reactive("Guest Researcher")
161
- selected_threshold = solara.reactive(None)
 
162
 
163
- center, zoom_level = compute_species_view()
164
- center = solara.reactive(center)
165
- zoom = solara.reactive(zoom_level)
 
166
 
 
167
  show_raster = solara.reactive(True)
168
  show_absence = solara.reactive(False)
169
  show_m_area = solara.reactive(True)
170
  show_iucn = solara.reactive(True)
171
  show_occurrences = solara.reactive(True)
172
 
 
173
  edits = solara.reactive([])
174
 
175
-
176
  # ============================================================
177
- # 7. Editing logic
178
  # ============================================================
179
 
180
  def apply_edits_logic(img, points, range_polys, edits_list):
 
 
 
 
 
 
 
181
  if not edits_list:
182
- return img, points, range_polys
183
 
184
  for edit in edits_list:
185
- geom = edit.get("geometry")
186
- if not geom:
187
- continue
188
-
189
- layer = edit.get("layer")
190
- op = edit.get("type")
191
-
192
- if layer == "Predicted Presence" and img is not None:
193
- mask = ee.Image.constant(1).clip(geom)
194
- img = img.where(mask, 1 if op == "add" else 0)
195
-
196
- elif layer == "Occurrences" and points is not None:
 
 
 
 
 
 
197
  if op == "remove":
198
- points = points.filter(ee.Filter.bounds(geom).Not())
 
199
  elif op == "add":
200
- points = points.merge(
201
- ee.FeatureCollection([ee.Feature(geom)])
202
- )
 
 
203
 
204
- elif layer == "IUCN Range" and range_polys is not None:
205
  if op == "add":
206
- range_polys = range_polys.merge(
207
- ee.FeatureCollection([ee.Feature(geom)])
208
- )
209
-
210
- return img, points, range_polys
 
 
211
 
212
 
213
- # ============================================================
214
- # 8. Map factory
215
- # ============================================================
216
-
217
- def create_threshold_map(threshold, height="350px", draw_control=False):
218
- ensure_assets_loaded()
219
-
220
  m = geemap.Map(
221
- center=center.value,
222
- zoom=zoom.value,
223
  add_google_map=False,
224
  draw_control=draw_control,
225
  toolbar_control=False,
226
  layer_control=True,
227
  zoom_control=False,
228
- inspector_control=False,
229
  search_control=False,
230
  scale_control=False,
231
  attribution_control=False,
232
- height=height,
233
  )
234
 
235
- if not _EE_INITIALIZED:
236
- m.addLayer(
237
- None,
238
- {},
239
- f"Earth Engine error: {_EE_INIT_ERROR}",
240
- )
241
- return m
242
-
243
  base_img = None
244
- if threshold in THRESHOLD_IMAGES:
245
  try:
246
  base_img = ee.Image(THRESHOLD_IMAGES[threshold])
247
  except Exception:
248
  pass
249
-
250
- img, pts, rng = apply_edits_logic(
251
- base_img,
252
- OCCURRENCES,
253
- IUCN_RANGE,
254
- edits.value,
255
- )
256
-
257
- if img is not None:
258
- m.addLayer(
259
- img.updateMask(img.eq(1)),
260
- {"palette": ["000000"]},
261
- "Predicted Presence (1)",
262
- )
263
-
264
- if M_AREA is not None and show_m_area.value:
265
- m.addLayer(
266
- M_AREA.style(color="#2b8cbe", fillColor="#2b8cbe44", width=2),
267
- {},
268
- "Accessible Area",
269
- )
270
-
271
- if rng is not None and show_iucn.value:
272
- m.addLayer(
273
- rng.style(color="red", fillColor="00000000", width=2),
274
- {},
275
- "IUCN Range",
276
- )
277
-
278
- if pts is not None and show_occurrences.value:
279
- m.addLayer(pts, {"color": "#00BFFF"}, "Occurrences")
280
 
281
  return m
282
 
283
-
284
- # ============================================================
285
- # 9. Solara component
286
- # ============================================================
287
-
288
  @solara.component
289
  def LinkedMap(threshold, height="350px"):
290
  m = solara.use_memo(
291
- lambda: create_threshold_map(threshold, height),
292
- [threshold, edits.value],
293
  )
294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  solara.display(
296
  m,
297
  zoom=zoom.value,
 
 
 
 
 
1
  import os
2
  import json
3
  import ee
4
  import geemap
5
  import solara
6
+ from google.oauth2 import service_account
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  # ============================================================
9
+ # 1. Initialization & Assets
10
  # ============================================================
11
 
12
+ def get_env_var(key: str):
13
+ return os.environ.get(key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ def ee_initialize(token_name="EARTHENGINE_TOKEN", project=None, **kwargs):
16
+ # Skip if already initialized
17
+ if ee.data._get_state().credentials is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  return
19
 
20
+ ee_token = get_env_var(token_name)
21
+ if ee_token is not None:
22
+ try:
23
+ # Parse the Service Account JSON key from the environment variable
24
+ sa_info = json.loads(ee_token)
25
+
26
+ # Generate credentials from the service account dictionary
27
+ credentials = service_account.Credentials.from_service_account_info(
28
+ sa_info,
29
+ scopes=['https://www.googleapis.com/auth/earthengine']
30
+ )
31
+
32
+ # Initialize Earth Engine with the new credentials
33
+ ee.Initialize(credentials=credentials, project=project, **kwargs)
34
+ print("Earth Engine initialized successfully using Service Account.")
35
+ return
36
+ except Exception as e:
37
+ print(f"Failed to initialize with Service Account: {e}")
38
+ pass
39
+
40
+ # Fallback to default initialization
41
  try:
42
+ ee.Initialize(project=project, **kwargs)
43
+ except Exception:
44
+ print("Earth Engine failed to initialize. Please check EARTHENGINE_TOKEN.")
 
 
 
 
 
 
 
 
 
 
45
 
46
+ try:
47
+ ee_initialize(project="scenic-sunspot-485920-j6")
48
+ except Exception as e:
49
+ print(f"EE Init Error: {e}")
50
 
51
  SPECIES = "Acrobatornis fonsecai"
52
 
 
57
  "sensit": "projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_sensit",
58
  }
59
 
60
+ THRESHOLD_TITLES = {
61
+ "mtp": "Minimum Training Presence",
62
+ "tss": "True Skill Statistic",
63
+ "p10": "10th Percentile",
64
+ "sensit": "Sensitivity-Specificity Equality"
65
+ }
66
+
67
+ # Assets
68
+ try:
69
+ M_AREA = ee.FeatureCollection("projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_M")
70
+ IUCN_RANGE = ee.FeatureCollection("projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_iucnrange")
71
+ OCCURRENCES = ee.FeatureCollection("projects/scenic-sunspot-485920-j6/assets/LAmericaResidentReview/Acrobatornis_fonsecai_occurrence")
72
+ except Exception:
73
+ M_AREA = ee.FeatureCollection([])
74
+ IUCN_RANGE = ee.FeatureCollection([])
75
+ OCCURRENCES = ee.FeatureCollection([])
76
 
77
  # ============================================================
78
+ # 2. View Computation
79
  # ============================================================
80
+ def compute_species_view(fc, fallback_center=(-15, -50), fallback_zoom=4):
 
 
 
 
 
 
 
 
 
81
  try:
82
+ coords = fc.geometry().bounds().coordinates().getInfo()[0]
83
  lons = [p[0] for p in coords]
84
  lats = [p[1] for p in coords]
 
85
  center = (sum(lats) / len(lats), sum(lons) / len(lons))
86
  span = max(max(lons) - min(lons), max(lats) - min(lats))
87
+
88
+ if span > 60: zoom = 3
89
+ elif span > 30: zoom = 4
90
+ elif span > 15: zoom = 5
91
+ elif span > 8: zoom = 6
92
+ elif span > 4: zoom = 7
93
+ else: zoom = 8
 
 
 
 
 
 
 
94
  return center, zoom
95
  except Exception:
96
  return fallback_center, fallback_zoom
97
 
98
+ SPECIES_CENTER, SPECIES_ZOOM = compute_species_view(M_AREA)
99
 
100
  # ============================================================
101
+ # 3. Global Shared State
102
  # ============================================================
103
 
104
+ # --- Session / User Metadata ---
105
  researcher_name = solara.reactive("Guest Researcher")
106
+ species_list = ["Acrobatornis fonsecai"]
107
+ selected_species = solara.reactive("Acrobatornis fonsecai")
108
 
109
+ # --- Map State ---
110
+ selected_threshold = solara.reactive(None)
111
+ zoom = solara.reactive(SPECIES_ZOOM)
112
+ center = solara.reactive(SPECIES_CENTER)
113
 
114
+ # --- Layer Visibility ---
115
  show_raster = solara.reactive(True)
116
  show_absence = solara.reactive(False)
117
  show_m_area = solara.reactive(True)
118
  show_iucn = solara.reactive(True)
119
  show_occurrences = solara.reactive(True)
120
 
121
+ # --- Editing State ---
122
  edits = solara.reactive([])
123
 
 
124
  # ============================================================
125
+ # 4. Map Factory & Logic
126
  # ============================================================
127
 
128
  def apply_edits_logic(img, points, range_polys, edits_list):
129
+ """
130
+ Applies a list of geometric edits to the image, points, and range polygons.
131
+ """
132
+ modified_img = img
133
+ modified_points = points
134
+ modified_range = range_polys
135
+
136
  if not edits_list:
137
+ return modified_img, modified_points, modified_range
138
 
139
  for edit in edits_list:
140
+ geom = edit.get('geometry')
141
+ if not geom: continue
142
+
143
+ layer = edit.get('layer')
144
+ op = edit.get('type')
145
+
146
+ if layer == "Predicted Presence":
147
+ # Raster editing
148
+ if modified_img:
149
+ mask = ee.Image.constant(1).clip(geom)
150
+ if op == "add":
151
+ # Mark as 1 (Presence)
152
+ modified_img = modified_img.where(mask, 1)
153
+ elif op == "remove":
154
+ # Mark as 0 (Absence)
155
+ modified_img = modified_img.where(mask, 0)
156
+
157
+ elif layer == "Occurrences":
158
  if op == "remove":
159
+ if modified_points:
160
+ modified_points = modified_points.filter(ee.Filter.bounds(geom).Not())
161
  elif op == "add":
162
+ # Add new point(s)
163
+ if modified_points:
164
+ modified_points = modified_points.merge(ee.FeatureCollection([ee.Feature(geom)]))
165
+ else:
166
+ modified_points = ee.FeatureCollection([ee.Feature(geom)])
167
 
168
+ elif layer == "IUCN Range":
169
  if op == "add":
170
+ # Add new range polygon
171
+ if modified_range:
172
+ modified_range = modified_range.merge(ee.FeatureCollection([ee.Feature(geom)]))
173
+ else:
174
+ modified_range = ee.FeatureCollection([ee.Feature(geom)])
175
+
176
+ return modified_img, modified_points, modified_range
177
 
178
 
179
+ def create_threshold_map(threshold: str, height="350px", draw_control=False, apply_edits_list=None) -> geemap.Map:
180
+ # Explicitly disable optional controls to prevent OOM
 
 
 
 
 
181
  m = geemap.Map(
182
+ center=SPECIES_CENTER,
183
+ zoom=SPECIES_ZOOM,
184
  add_google_map=False,
185
  draw_control=draw_control,
186
  toolbar_control=False,
187
  layer_control=True,
188
  zoom_control=False,
189
+ inspector_control=False,
190
  search_control=False,
191
  scale_control=False,
192
  attribution_control=False,
193
+ height=height,
194
  )
195
 
196
+ # Base assets
 
 
 
 
 
 
 
197
  base_img = None
198
+ if threshold and threshold in THRESHOLD_IMAGES:
199
  try:
200
  base_img = ee.Image(THRESHOLD_IMAGES[threshold])
201
  except Exception:
202
  pass
203
+
204
+ base_points = OCCURRENCES
205
+ base_range = IUCN_RANGE
206
+
207
+ # Apply Edits
208
+ final_img, final_points, final_range = apply_edits_logic(base_img, base_points, base_range, apply_edits_list)
209
+
210
+ # --- Layer Adding Order (Bottom to Top) ---
211
+
212
+ # 1. Raster (Bottom)
213
+ if final_img:
214
+ # Presence (1) -> Black
215
+ presence_layer = final_img.updateMask(final_img.eq(1))
216
+ m.addLayer(presence_layer, {"palette": ["000000"]}, "Predicted Presence (1)", True, 1.0)
217
+
218
+ # Absence (0) -> White
219
+ absence_layer = final_img.updateMask(final_img.eq(0))
220
+ m.addLayer(absence_layer, {"palette": ["ffffff"]}, "Predicted Absence (0)", False, 1.0)
221
+
222
+ # 2. Vectors
223
+ m.addLayer(M_AREA.style(color="#2b8cbe", fillColor="#2b8cbe44", width=2), {}, "Accessible Area")
224
+
225
+ # IUCN (Hollow) - Modified range
226
+ if final_range:
227
+ m.addLayer(final_range.style(color="red", fillColor="00000000", width=2), {}, "IUCN Range")
228
+
229
+ # 3. Occurrences (Top)
230
+ if final_points:
231
+ # Use bright Deep Sky Blue (#00BFFF) for visibility against black raster
232
+ m.addLayer(final_points, {"color": "#00BFFF"}, "Occurrences")
 
233
 
234
  return m
235
 
 
 
 
 
 
236
  @solara.component
237
  def LinkedMap(threshold, height="350px"):
238
  m = solara.use_memo(
239
+ lambda: create_threshold_map(threshold, height, draw_control=False, apply_edits_list=edits.value),
240
+ [threshold, edits.value]
241
  )
242
 
243
+ def update_layers():
244
+ if not m: return
245
+ def set_visible(name, visible):
246
+ for layer in m.layers:
247
+ if name in layer.name: layer.visible = visible
248
+
249
+ set_visible("Predicted Presence (1)", show_raster.value)
250
+ set_visible("Predicted Absence (0)", show_raster.value and show_absence.value)
251
+ set_visible("Accessible Area", show_m_area.value)
252
+ set_visible("IUCN Range", show_iucn.value)
253
+ set_visible("Occurrences", show_occurrences.value)
254
+
255
+ solara.use_effect(update_layers, [show_raster.value, show_absence.value, show_m_area.value, show_iucn.value, show_occurrences.value])
256
+
257
  solara.display(
258
  m,
259
  zoom=zoom.value,