KSvend Claude Happy commited on
Commit
bc642b5
Β·
1 Parent(s): 1e692d7

feat: worker generates maps for all indicators, serves spatial JSON

Browse files

- Switch worker to new render_indicator_map/render_status_map signatures
- Generate map PNG for every indicator (not just ones with SpatialData)
- Serialize spatial data to JSON after each indicator for frontend use
- Add GET /api/jobs/{job_id}/spatial/{indicator_id} endpoint in main.py

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

Files changed (2) hide show
  1. app/main.py +12 -0
  2. app/worker.py +42 -10
app/main.py CHANGED
@@ -103,6 +103,18 @@ def create_app(db_path: str = "aperture.db", run_worker: bool = False) -> FastAP
103
  media_type="image/png",
104
  )
105
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  # ── Static files + SPA root ───────────────────────────────────────
107
  if _FRONTEND_DIR.exists():
108
  app.mount("/static", StaticFiles(directory=str(_FRONTEND_DIR)), name="static")
 
103
  media_type="image/png",
104
  )
105
 
106
+ @app.get("/api/jobs/{job_id}/spatial/{indicator_id}")
107
+ async def get_indicator_spatial(job_id: str, indicator_id: str, email: str = Depends(get_current_user)):
108
+ await _verify_job_access(job_id, email)
109
+ spatial_path = _HERE.parent / "results" / job_id / f"{indicator_id}_spatial.json"
110
+ if not spatial_path.exists():
111
+ raise HTTPException(status_code=404, detail="Spatial data not available for this indicator")
112
+ import json as _json
113
+ with open(spatial_path) as f:
114
+ data = _json.load(f)
115
+ from fastapi.responses import JSONResponse
116
+ return JSONResponse(content=data)
117
+
118
  # ── Static files + SPA root ───────────────────────────────────────
119
  if _FRONTEND_DIR.exists():
120
  app.mount("/static", StaticFiles(directory=str(_FRONTEND_DIR)), name="static")
app/worker.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
  import asyncio
 
3
  import logging
4
  import os
5
  import traceback
@@ -9,12 +10,38 @@ from app.models import JobStatus
9
  from app.outputs.report import generate_pdf_report
10
  from app.outputs.package import create_data_package
11
  from app.outputs.charts import render_timeseries_chart
12
- from app.outputs.maps import render_indicator_map
13
  from app.core.email import send_completion_email
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  async def process_job(job_id: str, db: Database, registry: IndicatorRegistry) -> None:
19
  job = await db.get_job(job_id)
20
  if job is None:
@@ -57,22 +84,27 @@ async def process_job(job_id: str, db: Database, registry: IndicatorRegistry) ->
57
  )
58
  output_files.append(chart_path)
59
 
60
- # Generate map PNG if spatial data is available
61
  spatial = spatial_cache.get(result.indicator_id)
 
62
  if spatial is not None:
63
- map_path = os.path.join(results_dir, f"{result.indicator_id}_map.png")
64
  render_indicator_map(
65
- data=spatial.data,
66
- lons=spatial.lons,
67
- lats=spatial.lats,
68
  aoi=job.request.aoi,
69
- indicator_name=result.indicator_id.replace("_", " ").title(),
70
  status=result.status,
71
  output_path=map_path,
72
- colormap=spatial.colormap,
73
- label=spatial.label,
74
  )
75
- output_files.append(map_path)
 
 
 
 
 
 
 
 
 
 
76
 
77
  # Generate PDF report
78
  report_path = os.path.join(results_dir, "report.pdf")
 
1
  from __future__ import annotations
2
  import asyncio
3
+ import json
4
  import logging
5
  import os
6
  import traceback
 
10
  from app.outputs.report import generate_pdf_report
11
  from app.outputs.package import create_data_package
12
  from app.outputs.charts import render_timeseries_chart
13
+ from app.outputs.maps import render_indicator_map, render_status_map
14
  from app.core.email import send_completion_email
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
 
19
+ def _save_spatial_json(spatial, status_value: str, path: str) -> None:
20
+ """Serialize spatial data to JSON for the frontend."""
21
+ if spatial is None:
22
+ obj = {"map_type": "status", "status": status_value}
23
+ elif spatial.map_type == "grid":
24
+ obj = {
25
+ "map_type": "grid",
26
+ "status": status_value,
27
+ "data": spatial.data.tolist(),
28
+ "lats": spatial.lats.tolist(),
29
+ "lons": spatial.lons.tolist(),
30
+ "label": spatial.label,
31
+ "colormap": spatial.colormap,
32
+ }
33
+ else:
34
+ obj = {
35
+ "map_type": spatial.map_type,
36
+ "status": status_value,
37
+ "geojson": spatial.geojson,
38
+ "label": spatial.label,
39
+ "colormap": spatial.colormap,
40
+ }
41
+ with open(path, "w") as f:
42
+ json.dump(obj, f)
43
+
44
+
45
  async def process_job(job_id: str, db: Database, registry: IndicatorRegistry) -> None:
46
  job = await db.get_job(job_id)
47
  if job is None:
 
84
  )
85
  output_files.append(chart_path)
86
 
87
+ # Generate map PNG for every indicator
88
  spatial = spatial_cache.get(result.indicator_id)
89
+ map_path = os.path.join(results_dir, f"{result.indicator_id}_map.png")
90
  if spatial is not None:
 
91
  render_indicator_map(
92
+ spatial=spatial,
 
 
93
  aoi=job.request.aoi,
 
94
  status=result.status,
95
  output_path=map_path,
 
 
96
  )
97
+ else:
98
+ render_status_map(
99
+ aoi=job.request.aoi,
100
+ status=result.status,
101
+ output_path=map_path,
102
+ )
103
+ output_files.append(map_path)
104
+
105
+ # Save spatial data as JSON for frontend
106
+ spatial_json_path = os.path.join(results_dir, f"{result.indicator_id}_spatial.json")
107
+ _save_spatial_json(spatial, result.status.value, spatial_json_path)
108
 
109
  # Generate PDF report
110
  report_path = os.path.join(results_dir, "report.pdf")