# app/routers/green_spaces.py from fastapi import APIRouter, Query, HTTPException from typing import Optional, List from enum import Enum from app.models.green_space import ScoringRequest from app.models.response import GreenSpaceResponse, NearbyAmenitiesResponse from app.services.green_space_service import GreenSpaceService from app.services.berlin_data_service import BerlinDataService router = APIRouter(prefix="/green-spaces", tags=["green-spaces"]) # Services green_space_service = GreenSpaceService() berlin_data = BerlinDataService() class PersonalityType(str, Enum): LITTLE_ADVENTURERS = "little_adventurers" DATE_NIGHT = "date_night" SQUAD_GOALS = "squad_goals" ZEN_MASTERS = "zen_masters" ACTIVE_LIFESTYLE = "active_lifestyle" WILDLIFE_LOVER = "wildlife_lover" ART_NERD = "art_nerd" HISTORY_GEEK = "history_geek" @router.get("/search", response_model=GreenSpaceResponse) async def search_green_spaces( # Location filters lat: Optional[float] = Query(None, description="Latitude for location-based search"), lng: Optional[float] = Query(None, description="Longitude for location-based search"), radius: int = Query(2000, ge=100, le=10000, description="Search radius in meters"), # Area filters neighborhood: Optional[str] = Query(None, description="Berlin neighborhood"), # Personality scoring personality: PersonalityType = Query(..., description="Personality type for scoring"), min_score: int = Query(60, ge=0, le=100, description="Minimum personality score"), # Feature filters min_size: Optional[int] = Query(None, ge=100, description="Minimum area in square meters"), has_water: Optional[bool] = Query(None, description="Must have water features"), has_playground: Optional[bool] = Query(None, description="Must have playground nearby"), max_noise_level: Optional[int] = Query(None, ge=1, le=5, description="Maximum noise level"), # Response options limit: int = Query(20, ge=1, le=100, description="Maximum results"), include_amenities: bool = Query(False, description="Include detailed amenity data"), ): """ Search for green spaces in Berlin and score them for a personality type. This endpoint dynamically analyzes Berlin's green spaces and scores them based on the selected personality preferences. """ try: # Build search request search_request = ScoringRequest( personality=personality.value, location=(lat, lng) if lat and lng else None, radius=radius, neighborhood=neighborhood, min_score=min_score, filters={ "min_size": min_size, "has_water": has_water, "has_playground": has_playground, "max_noise_level": max_noise_level, }, limit=limit, include_amenities=include_amenities ) # Get and score green spaces results = await green_space_service.search_and_score(search_request) return GreenSpaceResponse( green_spaces=results, total_found=len(results), search_params=search_request.dict(), personality=personality.value ) except Exception as e: raise HTTPException(status_code=500, detail=f"Search failed: {str(e)}") @router.get("/score-location") async def score_specific_location( lat: float = Query(..., description="Latitude of location to score"), lng: float = Query(..., description="Longitude of location to score"), personality: PersonalityType = Query(..., description="Personality type"), radius: int = Query(300, ge=50, le=1000, description="Analysis radius in meters"), ): """ Score a specific location for a personality type. Analyzes the immediate area around the given coordinates and provides a personality-based score with detailed explanations. """ try: # Check if location is in a green space green_space = await berlin_data.get_green_space_at_location(lat, lng) if not green_space: return { "score": 0, "explanation": "Location is not in a recognized green space", "location": {"lat": lat, "lng": lng}, "personality": personality.value } # Score the location from app.services.scoring_engine import ScoringEngine scoring_engine = ScoringEngine() score_result = await scoring_engine.score_location( lat, lng, personality.value, radius ) return score_result except Exception as e: raise HTTPException(status_code=500, detail=f"Scoring failed: {str(e)}") @router.get("/discover") async def discover_green_spaces( lat: float = Query(..., description="Your current latitude"), lng: float = Query(..., description="Your current longitude"), max_distance: int = Query(5000, ge=500, le=20000, description="Maximum distance in meters"), personality: PersonalityType = Query(..., description="Your personality type"), ): """ Discover highly-rated green spaces near your location. Returns the best green spaces within walking/cycling distance, scored for your personality type. """ try: discoveries = await green_space_service.discover_nearby( lat, lng, max_distance, personality.value ) return { "discoveries": discoveries, "your_location": {"lat": lat, "lng": lng}, "search_radius": max_distance, "personality": personality.value } except Exception as e: raise HTTPException(status_code=500, detail=f"Discovery failed: {str(e)}") @router.get("/{space_id}/analysis") async def analyze_green_space( space_id: str, personality: Optional[PersonalityType] = Query(None, description="Personality for focused analysis"), ): """ Get detailed analysis of a specific green space. Provides comprehensive scoring across all personalities or focused analysis for a specific personality type. """ try: analysis = await green_space_service.get_detailed_analysis( space_id, personality.value if personality else None ) if not analysis: raise HTTPException(status_code=404, detail="Green space not found") return analysis except Exception as e: raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}") @router.get("/conditions") async def get_current_conditions( lat: float = Query(..., description="Latitude"), lng: float = Query(..., description="Longitude"), ): """Get current conditions at a green space (weather, crowds, etc.).""" try: conditions = await berlin_data.get_current_conditions(lat, lng) return conditions except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get conditions: {str(e)}") @router.get("/all") async def get_all_green_spaces( personality: Optional[PersonalityType] = Query(None, description="Personality type for scoring"), min_score: int = Query(0, ge=0, le=100, description="Minimum personality score (only applies if personality is provided)"), limit: int = Query(50, ge=1, le=200, description="Maximum results"), ): """ Get all available green spaces in Berlin. Optionally score them for a specific personality type. Perfect for frontend dropdowns or full dataset access. """ try: # Get all green spaces all_spaces = await berlin_data.search_green_spaces() # If personality is specified, score and filter if personality: scored_spaces = [] for space in all_spaces: personality_score = await green_space_service.scoring_engine.score_green_space( space, personality.value ) if personality_score.score >= min_score: space.current_personality_score = personality_score scored_spaces.append(space) # Sort by score (highest first) scored_spaces.sort( key=lambda x: x.current_personality_score.score if x.current_personality_score else 0, reverse=True ) all_spaces = scored_spaces # Apply limit limited_spaces = all_spaces[:limit] return { "green_spaces": limited_spaces, "total_available": len(all_spaces), "returned_count": len(limited_spaces), "personality": personality.value if personality else None, "min_score_applied": min_score if personality else None } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get green spaces: {str(e)}") @router.get("/recommendations/{personality}") async def get_personality_recommendations( personality: PersonalityType, limit: int = Query(20, ge=1, le=50, description="Number of recommendations"), neighborhood: Optional[str] = Query(None, description="Preferred neighborhood"), min_score: int = Query(70, ge=50, le=100, description="Minimum personality score"), ): """ Get personalized green space recommendations. Returns the best green spaces for a specific personality type, with explanations of why each space is recommended. """ try: # Get all green spaces all_spaces = await berlin_data.search_green_spaces(neighborhood=neighborhood) # Score and rank for personality recommendations = [] for space in all_spaces: personality_score = await green_space_service.scoring_engine.score_green_space( space, personality.value ) if personality_score.score >= min_score: space.current_personality_score = personality_score # Get additional insights best_features = [] if space.environmental.tree_coverage_percent > 70: best_features.append("Excellent tree coverage") if space.environmental.water_features: best_features.append("Water features") if space.recreation.playground_quality > 60: best_features.append("Good playground facilities") if space.recreation.sports_facilities: best_features.append("Sports facilities") if space.environmental.noise_level.value <= 2: best_features.append("Peaceful atmosphere") recommendation = { "green_space": space, "score": personality_score.score, "explanation": personality_score.explanation, "best_features": best_features[:3], # Top 3 features "visit_recommendation": _get_visit_recommendation(space, personality.value) } recommendations.append(recommendation) # Sort by score recommendations.sort(key=lambda x: x["score"], reverse=True) return { "recommendations": recommendations[:limit], "personality": personality.value, "total_matches": len(recommendations), "search_filters": { "neighborhood": neighborhood, "min_score": min_score } } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get recommendations: {str(e)}") def _get_visit_recommendation(space, personality: str) -> str: """Get a personalized visit recommendation""" if personality == "little_adventurers": if space.recreation.playground_quality > 60: return "Perfect for family adventures with great playground facilities" return "Great for exploring with kids" elif personality == "date_night": if space.environmental.noise_level.value <= 2: return "Romantic and peaceful setting for couples" return "Nice atmosphere for a romantic stroll" elif personality == "zen_masters": if space.environmental.tree_coverage_percent > 70: return "Ideal for peaceful meditation under the trees" return "Perfect for quiet contemplation" elif personality == "active_lifestyle": if space.recreation.sports_facilities: return "Great for workouts and active recreation" return "Perfect for running and outdoor activities" elif personality == "wildlife_lover": if space.environmental.wildlife_diversity_score > 70: return "Excellent biodiversity for nature observation" return "Good spot for wildlife watching" else: return "Highly recommended for your personality type"