138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
from fastapi import FastAPI, File, UploadFile, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
import pandas as pd
|
|
import io
|
|
from pydantic import BaseModel
|
|
from typing import Dict, List
|
|
|
|
app = FastAPI()
|
|
|
|
# Configure CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # Allows all origins
|
|
allow_credentials=True,
|
|
allow_methods=["*"], # Allows all methods
|
|
allow_headers=["*"], # Allows all headers
|
|
)
|
|
|
|
# --- In-memory Storage & Data Loading ---
|
|
df_storage = None
|
|
plz_column_name = None
|
|
plz_geocoord_df = None
|
|
|
|
@app.on_event("startup")
|
|
def load_plz_data():
|
|
global plz_geocoord_df
|
|
try:
|
|
print("--- Loading PLZ geocoordinates dataset... ---")
|
|
df = pd.read_csv("plz_geocoord.csv", dtype={'plz': str})
|
|
df['plz'] = df['plz'].str.zfill(5)
|
|
plz_geocoord_df = df.set_index('plz')
|
|
print(f"--- Successfully loaded {len(plz_geocoord_df)} PLZ coordinates. ---")
|
|
except FileNotFoundError:
|
|
print("--- FATAL ERROR: plz_geocoord.csv not found. Geocoding will not work. ---")
|
|
# In a real app, you might want to exit or handle this more gracefully
|
|
plz_geocoord_df = pd.DataFrame()
|
|
|
|
# --- Pydantic Models ---
|
|
class FilterRequest(BaseModel):
|
|
filters: Dict[str, List[str]]
|
|
|
|
# --- API Endpoints ---
|
|
@app.get("/")
|
|
def read_root():
|
|
return {"message": "Heatmap Tool Backend"}
|
|
|
|
@app.post("/api/upload")
|
|
async def upload_file(file: UploadFile = File(...)):
|
|
global df_storage, plz_column_name
|
|
print(f"--- Received request to /api/upload for file: {file.filename} ---")
|
|
if not file.filename.endswith('.xlsx'):
|
|
raise HTTPException(status_code=400, detail="Invalid file format. Please upload an .xlsx file.")
|
|
|
|
try:
|
|
contents = await file.read()
|
|
df = pd.read_excel(io.BytesIO(contents), dtype=str) # Read all as string to be safe
|
|
df.fillna('N/A', inplace=True)
|
|
|
|
|
|
# --- PLZ Column Detection ---
|
|
temp_plz_col = None
|
|
for col in df.columns:
|
|
if 'plz' in col.lower():
|
|
temp_plz_col = col
|
|
break
|
|
|
|
if not temp_plz_col:
|
|
raise HTTPException(status_code=400, detail="No column with 'PLZ' found in the file.")
|
|
|
|
plz_column_name = temp_plz_col
|
|
# Normalize PLZ data
|
|
df[plz_column_name] = df[plz_column_name].str.strip().str.zfill(5)
|
|
|
|
|
|
# --- Dynamic Filter Detection ---
|
|
filters = {}
|
|
for col in df.columns:
|
|
if col != plz_column_name:
|
|
unique_values = df[col].unique().tolist()
|
|
filters[col] = sorted(unique_values)
|
|
|
|
df_storage = df
|
|
|
|
print(f"Successfully processed file. Found PLZ column: '{plz_column_name}'. Detected {len(filters)} filterable columns.")
|
|
return {"filename": file.filename, "filters": filters, "plz_column": plz_column_name}
|
|
|
|
except Exception as e:
|
|
print(f"ERROR processing file: {e}")
|
|
raise HTTPException(status_code=500, detail=f"An error occurred while processing the file: {e}")
|
|
|
|
|
|
@app.post("/api/heatmap")
|
|
async def get_heatmap_data(request: FilterRequest):
|
|
global df_storage, plz_column_name, plz_geocoord_df
|
|
print(f"--- Received request to /api/heatmap with filters: {request.filters} ---")
|
|
if df_storage is None:
|
|
print("ERROR: No data in df_storage. File must be uploaded first.")
|
|
raise HTTPException(status_code=404, detail="No data available. Please upload a file first.")
|
|
if plz_geocoord_df.empty:
|
|
raise HTTPException(status_code=500, detail="Geocoding data is not available on the server.")
|
|
|
|
try:
|
|
filtered_df = df_storage.copy()
|
|
|
|
# Apply filters from the request
|
|
for column, values in request.filters.items():
|
|
if values:
|
|
filtered_df = filtered_df[filtered_df[column].isin(values)]
|
|
|
|
if filtered_df.empty:
|
|
return []
|
|
|
|
# Aggregate data by PLZ
|
|
plz_counts = filtered_df.groupby(plz_column_name).size().reset_index(name='count')
|
|
|
|
# --- Geocoding Step ---
|
|
# Merge the aggregated counts with the geocoding dataframe
|
|
merged_df = pd.merge(
|
|
plz_counts,
|
|
plz_geocoord_df,
|
|
left_on=plz_column_name,
|
|
right_index=True,
|
|
how='inner'
|
|
)
|
|
|
|
# Rename columns to match frontend expectations ('lon' and 'lat')
|
|
merged_df.rename(columns={'x': 'lon', 'y': 'lat'}, inplace=True)
|
|
|
|
# Convert to the required JSON format
|
|
heatmap_data = merged_df[['plz', 'lat', 'lon', 'count']].to_dict(orient='records')
|
|
|
|
print(f"Generated heatmap data with {len(heatmap_data)} PLZ points.")
|
|
return heatmap_data
|
|
|
|
except Exception as e:
|
|
print(f"ERROR generating heatmap: {e}")
|
|
raise HTTPException(status_code=500, detail=f"An error occurred while generating heatmap data: {e}")
|