berlin-picnic-api/app/routers/green_spaces.py

327 lines
13 KiB
Python

# 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"