234 lines
9.7 KiB
Python
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"
|