| """ |
| Mapbox Static Images API β satellite + topographic map fetchers. |
| |
| The risk analyst (v0.10+) reasons about three visual perspectives in |
| a single Gemma 4 inference call: |
| |
| - Street view (Google) β eye-level: lot grade, basement entries, downspouts |
| - Satellite (Mapbox) β bird's-eye: impervious surface %, drainage proximity |
| - Topographic (Mapbox) β contour lines: micro-depressions, terrain slope |
| |
| Each capture is fetched server-side, base64-encoded, and inlined as a |
| data: URL into the risk analyst's prompt. No upstream re-fetch. |
| |
| Free tier: Mapbox gives 50K static-image loads/month per account, so |
| 2 maps Γ 25K assessments/month is well within budget. |
| |
| If MAPBOX_ACCESS_TOKEN is not set the helpers return None and the |
| risk analyst gracefully falls back to whatever images it does have |
| (Street View only, or none). |
| """ |
| import base64 |
| from typing import Optional |
|
|
| import httpx |
|
|
| from app.config import MAPBOX_ACCESS_TOKEN |
|
|
| |
| |
| |
| _BASE = "https://api.mapbox.com/styles/v1/mapbox" |
|
|
| |
| |
| _SAT_STYLE = "satellite-v9" |
| _SAT_ZOOM = 17 |
|
|
| |
| |
| _TOPO_STYLE = "outdoors-v12" |
| _TOPO_ZOOM = 15 |
|
|
| |
| |
| _SIZE = "600x600@2x" |
|
|
|
|
| def _is_configured() -> bool: |
| return bool(MAPBOX_ACCESS_TOKEN) |
|
|
|
|
| async def _fetch_static(style: str, zoom: int, lat: float, lon: float) -> Optional[dict]: |
| if not _is_configured(): |
| return None |
| url = ( |
| f"{_BASE}/{style}/static/{lon},{lat},{zoom},0/{_SIZE}" |
| f"?access_token={MAPBOX_ACCESS_TOKEN}" |
| ) |
| async with httpx.AsyncClient(timeout=20) as client: |
| resp = await client.get(url) |
| if resp.status_code != 200: |
| return None |
| ct = resp.headers.get("content-type", "image/png").split(";")[0].strip() |
| b64 = base64.b64encode(resp.content).decode("ascii") |
| return { |
| "data_url": f"data:{ct};base64,{b64}", |
| "bytes": len(resp.content), |
| "style": style, |
| "zoom": zoom, |
| "lat": lat, |
| "lon": lon, |
| } |
|
|
|
|
| async def get_satellite_image(lat: float, lon: float) -> Optional[dict]: |
| return await _fetch_static(_SAT_STYLE, _SAT_ZOOM, lat, lon) |
|
|
|
|
| async def get_topo_image(lat: float, lon: float) -> Optional[dict]: |
| return await _fetch_static(_TOPO_STYLE, _TOPO_ZOOM, lat, lon) |
|
|