berlin-picnic-api/app/services/green_space_service.py

234 lines
9.7 KiB
Python

# app/services/green_space_service.py
from typing import List, Optional, Tuple, Dict, Any
from app.models.green_space import GreenSpace, ScoringRequest
from app.models.response import DetailedAnalysis, DiscoveryResult
from app.services.scoring_engine import ScoringEngine
from app.services.berlin_data_service import BerlinDataService
class GreenSpaceService:
"""Service for managing green space operations and scoring."""
def __init__(self):
self.scoring_engine = ScoringEngine()
self.berlin_data = BerlinDataService()
async def search_and_score(self, request: ScoringRequest) -> List[GreenSpace]:
"""Search for green spaces and score them based on personality preferences."""
try:
# Get green spaces based on search criteria
green_spaces = await self.berlin_data.search_green_spaces(
location=request.location,
radius=request.radius,
neighborhood=request.neighborhood,
filters=request.filters
)
# Score each green space for the personality
scored_spaces = []
for space in green_spaces:
personality_score = await self.scoring_engine.score_green_space(
space, request.personality, request.location
)
# Only include if meets minimum score
if personality_score.score >= request.min_score:
space.current_personality_score = personality_score
# Include amenities if requested
if request.include_amenities:
space.nearby_amenities = await self.berlin_data.get_amenities_near_point(
space.coordinates.lat,
space.coordinates.lng,
500 # 500m radius for amenities
)
scored_spaces.append(space)
# Sort by score (highest first) and limit results
scored_spaces.sort(
key=lambda x: x.current_personality_score.score if x.current_personality_score else 0,
reverse=True
)
return scored_spaces[:request.limit]
except Exception as e:
raise Exception(f"Failed to search and score green spaces: {str(e)}")
async def discover_nearby(
self,
lat: float,
lng: float,
max_distance: int,
personality: str
) -> List[DiscoveryResult]:
"""Discover highly-rated green spaces near a location."""
try:
# Get nearby green spaces
nearby_spaces = await self.berlin_data.get_green_spaces_within_radius(
lat, lng, max_distance
)
discoveries = []
for space in nearby_spaces:
# Score the space
personality_score = await self.scoring_engine.score_green_space(
space, personality, (lat, lng)
)
# Only include high-scoring spaces
if personality_score.score >= 70:
space.current_personality_score = personality_score
# Calculate travel info
distance = await self.berlin_data.calculate_distance(
lat, lng, space.coordinates.lat, space.coordinates.lng
)
discovery = DiscoveryResult(
green_space=space,
distance_meters=distance,
travel_time_walking=max(5, distance // 80), # ~5 km/h walking speed
travel_time_cycling=max(2, distance // 250), # ~15 km/h cycling speed
why_recommended=personality_score.explanation,
best_route_description=await self._get_route_description(
lat, lng, space.coordinates.lat, space.coordinates.lng
)
)
discoveries.append(discovery)
# Sort by score and distance
discoveries.sort(
key=lambda x: (x.green_space.current_personality_score.score, -x.distance_meters),
reverse=True
)
return discoveries[:10] # Top 10 discoveries
except Exception as e:
raise Exception(f"Failed to discover green spaces: {str(e)}")
async def get_detailed_analysis(
self,
space_id: str,
focus_personality: Optional[str] = None
) -> Optional[DetailedAnalysis]:
"""Get detailed analysis of a specific green space."""
try:
# Get the green space
green_space = await self.berlin_data.get_green_space_by_id(space_id)
if not green_space:
return None
# Score for all personalities or just the focused one
personality_breakdown = {}
personalities = [focus_personality] if focus_personality else [
"little_adventurers", "date_night", "squad_goals", "zen_masters",
"active_lifestyle", "wildlife_lover", "art_nerd", "history_geek"
]
for personality in personalities:
if personality: # Skip None values
score = await self.scoring_engine.score_green_space(
green_space, personality
)
personality_breakdown[personality] = score
# Find best locations within the space
best_locations = await self.scoring_engine.find_best_locations_within(
green_space, focus_personality or "zen_masters"
)
# Generate seasonal recommendations
seasonal_recommendations = self._generate_seasonal_recommendations(
green_space, focus_personality
)
# Find similar spaces
similar_spaces = await self.berlin_data.find_similar_green_spaces(
green_space, limit=5
)
return DetailedAnalysis(
green_space=green_space,
personality_breakdown=personality_breakdown,
best_locations_within=best_locations,
seasonal_recommendations=seasonal_recommendations,
optimal_visit_times=self._get_optimal_visit_times(green_space),
similar_spaces=[space.id for space in similar_spaces]
)
except Exception as e:
raise Exception(f"Failed to analyze green space: {str(e)}")
def _generate_seasonal_recommendations(
self,
green_space: GreenSpace,
personality: Optional[str]
) -> Dict[str, str]:
"""Generate seasonal visit recommendations."""
recommendations = {}
# Base recommendations on green space features
if green_space.environmental.water_features:
recommendations["summer"] = "Perfect for cooling off by the water features"
recommendations["spring"] = "Beautiful reflections in the water as nature awakens"
if green_space.environmental.tree_coverage_percent > 70:
recommendations["autumn"] = "Stunning fall foliage display"
recommendations["spring"] = "Fresh green canopy and blooming trees"
if green_space.recreation.sports_facilities:
recommendations["summer"] = "Great weather for outdoor sports and activities"
recommendations["winter"] = "Crisp air perfect for winter sports if available"
# Fill in any missing seasons with generic recommendations
seasons = ["spring", "summer", "autumn", "winter"]
for season in seasons:
if season not in recommendations:
recommendations[season] = f"Enjoy the peaceful {season} atmosphere"
return recommendations
def _get_optimal_visit_times(self, green_space: GreenSpace) -> List[str]:
"""Get optimal visit times based on green space characteristics."""
times = []
if green_space.environmental.noise_level.value <= 2:
times.append("Early morning (7-9 AM) for peaceful solitude")
if green_space.recreation.playground_quality > 50:
times.append("Mid-morning (9-11 AM) for family activities")
if green_space.accessibility.lighting_quality >= 4:
times.append("Evening (6-8 PM) for romantic walks")
if green_space.recreation.sports_facilities:
times.append("Late afternoon (4-6 PM) for sports activities")
# Always include at least one recommendation
if not times:
times.append("Midday (11 AM - 2 PM) for general enjoyment")
return times
async def _get_route_description(
self,
from_lat: float,
from_lng: float,
to_lat: float,
to_lng: float
) -> str:
"""Get a simple route description."""
# This is a simplified version - in a real app you'd use a routing service
distance = await self.berlin_data.calculate_distance(from_lat, from_lng, to_lat, to_lng)
if distance < 500:
return "Short walk through the neighborhood"
elif distance < 1500:
return "Pleasant walk or quick bike ride"
elif distance < 3000:
return "Good cycling distance or longer walk"
else:
return "Best reached by bike or public transport"