327 lines
13 KiB
Python
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"
|