"""Service layer for generating GeoJSON from PostGIS geometry.""" import json from functools import lru_cache from typing import Any import pandas as pd from sqlalchemy import text from portfolio_app.toronto.models import get_engine def _execute_query(sql: str, params: dict[str, Any] | None = None) -> pd.DataFrame: """Execute SQL query and return DataFrame.""" engine = get_engine() with engine.connect() as conn: return pd.read_sql(text(sql), conn, params=params) @lru_cache(maxsize=8) def get_neighbourhoods_geojson(year: int = 2021) -> dict[str, Any]: """Get GeoJSON FeatureCollection for all neighbourhoods. Queries mart_neighbourhood_overview for geometries and basic properties. Args: year: Year to query for joining properties. Returns: GeoJSON FeatureCollection dictionary. """ # Query geometries with ST_AsGeoJSON sql = """ SELECT neighbourhood_id, neighbourhood_name, ST_AsGeoJSON(geometry)::json as geom, population, livability_score FROM public_marts.mart_neighbourhood_overview WHERE year = :year AND geometry IS NOT NULL """ try: df = _execute_query(sql, {"year": year}) except Exception: # Table might not exist or have data yet return _empty_geojson() if df.empty: return _empty_geojson() # Build GeoJSON features features = [] for _, row in df.iterrows(): geom = row["geom"] if geom is None: continue # Handle geometry that might be a string or dict if isinstance(geom, str): geom = json.loads(geom) feature = { "type": "Feature", "id": row["neighbourhood_id"], "properties": { "neighbourhood_id": int(row["neighbourhood_id"]), "neighbourhood_name": row["neighbourhood_name"], "population": int(row["population"]) if pd.notna(row["population"]) else None, "livability_score": float(row["livability_score"]) if pd.notna(row["livability_score"]) else None, }, "geometry": geom, } features.append(feature) return { "type": "FeatureCollection", "features": features, } @lru_cache(maxsize=4) def get_cmhc_zones_geojson() -> dict[str, Any]: """Get GeoJSON FeatureCollection for CMHC zones. Queries dim_cmhc_zone for zone geometries. Returns: GeoJSON FeatureCollection dictionary. """ sql = """ SELECT zone_code, zone_name, ST_AsGeoJSON(geometry)::json as geom FROM dim_cmhc_zone WHERE geometry IS NOT NULL """ try: df = _execute_query(sql, {}) except Exception: return _empty_geojson() if df.empty: return _empty_geojson() features = [] for _, row in df.iterrows(): geom = row["geom"] if geom is None: continue if isinstance(geom, str): geom = json.loads(geom) feature = { "type": "Feature", "id": row["zone_code"], "properties": { "zone_code": row["zone_code"], "zone_name": row["zone_name"], }, "geometry": geom, } features.append(feature) return { "type": "FeatureCollection", "features": features, } def get_neighbourhood_geometry(neighbourhood_id: int) -> dict[str, Any] | None: """Get GeoJSON geometry for a single neighbourhood. Args: neighbourhood_id: The neighbourhood ID. Returns: GeoJSON geometry dict, or None if not found. """ sql = """ SELECT ST_AsGeoJSON(geometry)::json as geom FROM dim_neighbourhood WHERE neighbourhood_id = :neighbourhood_id AND geometry IS NOT NULL """ try: df = _execute_query(sql, {"neighbourhood_id": neighbourhood_id}) except Exception: return None if df.empty: return None geom = df.iloc[0]["geom"] if isinstance(geom, str): result: dict[str, Any] = json.loads(geom) return result return dict(geom) if geom is not None else None def _empty_geojson() -> dict[str, Any]: """Return an empty GeoJSON FeatureCollection.""" return { "type": "FeatureCollection", "features": [], }