838 lines
36 KiB
Python
838 lines
36 KiB
Python
# app/services/scoring_engine.py
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
import math
|
|
from geopy.distance import geodesic
|
|
|
|
from app.models.green_space import (
|
|
GreenSpace, PersonalityScore, LocationScore,
|
|
Coordinates, Amenity, AmenityType
|
|
)
|
|
from app.services.berlin_data_service import BerlinDataService
|
|
from app.services.street_tree_service import StreetTreeService
|
|
|
|
class ScoringEngine:
|
|
"""Dynamic scoring engine for green spaces based on personality preferences."""
|
|
|
|
def __init__(self):
|
|
self.berlin_data = BerlinDataService()
|
|
self.street_tree_service = StreetTreeService()
|
|
self.personality_weights = self._initialize_personality_weights()
|
|
|
|
def _initialize_personality_weights(self) -> Dict[str, Dict[str, float]]:
|
|
"""Define scoring weights for each personality type."""
|
|
return {
|
|
"little_adventurers": {
|
|
"playground_quality": 25,
|
|
"safety_score": 20,
|
|
"toilet_proximity": 15,
|
|
"shade_quality": 10,
|
|
"noise_level": 10, # Lower noise = higher score
|
|
"family_amenities": 15,
|
|
"accessibility": 5
|
|
},
|
|
"date_night": {
|
|
"romantic_atmosphere": 25,
|
|
"noise_level": 20, # Quiet = romantic
|
|
"scenic_beauty": 20,
|
|
"restaurant_proximity": 15,
|
|
"privacy_score": 10,
|
|
"lighting_quality": 10
|
|
},
|
|
"squad_goals": {
|
|
"space_size": 25,
|
|
"group_amenities": 20,
|
|
"accessibility": 15,
|
|
"food_options": 15,
|
|
"parking_availability": 10,
|
|
"activity_options": 15
|
|
},
|
|
"zen_masters": {
|
|
"quietness": 30,
|
|
"nature_immersion": 25,
|
|
"crowd_density": 15, # Lower crowds = higher score
|
|
"water_features": 10,
|
|
"meditation_spots": 10,
|
|
"air_quality": 10
|
|
},
|
|
"active_lifestyle": {
|
|
"fitness_facilities": 25,
|
|
"running_cycling_paths": 25,
|
|
"sports_areas": 20,
|
|
"accessibility": 15,
|
|
"terrain_variety": 10,
|
|
"safety_score": 5
|
|
},
|
|
"wildlife_lover": {
|
|
"wildlife_diversity": 30,
|
|
"natural_habitat": 25,
|
|
"water_features": 15,
|
|
"tree_coverage": 15,
|
|
"noise_level": 10, # Quiet for animals
|
|
"observation_spots": 5
|
|
},
|
|
"art_nerd": {
|
|
"cultural_proximity": 30,
|
|
"artistic_features": 20,
|
|
"museum_accessibility": 20,
|
|
"creative_atmosphere": 15,
|
|
"cafe_culture": 10,
|
|
"inspiring_views": 5
|
|
},
|
|
"history_geek": {
|
|
"historical_significance": 35,
|
|
"monument_proximity": 25,
|
|
"educational_value": 20,
|
|
"architectural_features": 10,
|
|
"cultural_context": 10
|
|
}
|
|
}
|
|
|
|
async def score_green_space(
|
|
self,
|
|
green_space: GreenSpace,
|
|
personality: str,
|
|
user_location: Optional[Tuple[float, float]] = None
|
|
) -> PersonalityScore:
|
|
"""Score a green space for a specific personality type."""
|
|
|
|
weights = self.personality_weights.get(personality, {})
|
|
if not weights:
|
|
raise ValueError(f"Unknown personality type: {personality}")
|
|
|
|
# Pre-fetch tree data once for all calculations
|
|
tree_data = await self._fetch_tree_data_once(green_space)
|
|
|
|
# Calculate component scores with cached tree data
|
|
component_scores = await self._calculate_component_scores(
|
|
green_space, personality, user_location, tree_data
|
|
)
|
|
|
|
# Calculate weighted final score
|
|
final_score = 0
|
|
explanation_parts = []
|
|
key_factors = []
|
|
|
|
for component, weight in weights.items():
|
|
if component in component_scores:
|
|
component_score = component_scores[component]
|
|
weighted_score = (component_score * weight) / 100
|
|
final_score += weighted_score
|
|
|
|
if component_score > 70: # Highlight strong factors
|
|
key_factors.append(component)
|
|
explanation_parts.append(
|
|
f"{component.replace('_', ' ')}: {component_score}/100"
|
|
)
|
|
|
|
# Generate recommendations
|
|
recommendations = self._generate_recommendations(
|
|
green_space, personality, component_scores
|
|
)
|
|
|
|
return PersonalityScore(
|
|
personality=personality,
|
|
score=min(100, max(0, int(final_score))),
|
|
explanation=self._create_explanation(personality, explanation_parts),
|
|
key_factors=key_factors,
|
|
recommendations=recommendations
|
|
)
|
|
|
|
async def _fetch_tree_data_once(self, green_space: GreenSpace) -> Optional[Any]:
|
|
"""Fetch tree data once and reuse for all calculations."""
|
|
try:
|
|
# Use the largest radius needed across all methods (400m)
|
|
tree_response = await self.street_tree_service.get_trees_near_location(
|
|
green_space.coordinates.lat,
|
|
green_space.coordinates.lng,
|
|
radius_m=400
|
|
)
|
|
return tree_response
|
|
except Exception as e:
|
|
print(f"Error fetching tree data: {e}")
|
|
return None
|
|
|
|
async def _calculate_component_scores(
|
|
self,
|
|
green_space: GreenSpace,
|
|
personality: str,
|
|
user_location: Optional[Tuple[float, float]] = None,
|
|
tree_data: Optional[Any] = None
|
|
) -> Dict[str, int]:
|
|
"""Calculate individual component scores."""
|
|
scores = {}
|
|
|
|
# Universal components
|
|
scores["accessibility"] = self._score_accessibility(green_space, user_location)
|
|
scores["safety_score"] = self._score_safety(green_space)
|
|
scores["noise_level"] = self._score_noise_level(green_space)
|
|
|
|
# Personality-specific components
|
|
if personality == "little_adventurers":
|
|
scores["playground_quality"] = green_space.recreation.playground_quality
|
|
scores["shade_quality"] = self._score_shade_quality_with_trees(green_space, tree_data)
|
|
scores["toilet_proximity"] = await self._score_toilet_proximity(green_space)
|
|
scores["family_amenities"] = await self._score_family_amenities(green_space)
|
|
|
|
elif personality == "date_night":
|
|
scores["romantic_atmosphere"] = await self._score_romantic_atmosphere(green_space)
|
|
scores["scenic_beauty"] = self._score_scenic_beauty(green_space)
|
|
scores["restaurant_proximity"] = await self._score_restaurant_proximity(green_space)
|
|
scores["privacy_score"] = self._score_privacy(green_space)
|
|
scores["lighting_quality"] = green_space.accessibility.lighting_quality * 20
|
|
|
|
elif personality == "squad_goals":
|
|
scores["space_size"] = self._score_space_size(green_space)
|
|
scores["group_amenities"] = await self._score_group_amenities(green_space)
|
|
scores["food_options"] = await self._score_food_options(green_space)
|
|
scores["parking_availability"] = green_space.accessibility.parking_availability * 20
|
|
scores["activity_options"] = self._score_activity_options(green_space)
|
|
|
|
elif personality == "zen_masters":
|
|
scores["quietness"] = self._score_quietness(green_space)
|
|
scores["nature_immersion"] = self._score_nature_immersion_with_trees(green_space, tree_data)
|
|
scores["crowd_density"] = await self._score_crowd_density(green_space)
|
|
scores["water_features"] = 100 if green_space.environmental.water_features else 0
|
|
scores["meditation_spots"] = self._score_meditation_spots_with_trees(green_space, tree_data)
|
|
scores["air_quality"] = self._score_air_quality_with_trees(green_space, tree_data)
|
|
|
|
elif personality == "active_lifestyle":
|
|
scores["fitness_facilities"] = 100 if green_space.recreation.sports_facilities else 0
|
|
scores["running_cycling_paths"] = self._score_running_cycling_paths(green_space)
|
|
scores["sports_areas"] = self._score_sports_areas(green_space)
|
|
scores["terrain_variety"] = self._score_terrain_variety(green_space)
|
|
|
|
elif personality == "wildlife_lover":
|
|
scores["wildlife_diversity"] = self._score_wildlife_diversity_with_trees(green_space, tree_data)
|
|
scores["natural_habitat"] = self._score_natural_habitat_with_trees(green_space, tree_data)
|
|
scores["water_features"] = 100 if green_space.environmental.water_features else 0
|
|
scores["tree_coverage"] = self._score_tree_coverage_with_real_data(green_space, tree_data)
|
|
scores["observation_spots"] = self._score_observation_spots_with_trees(green_space, tree_data)
|
|
|
|
elif personality == "art_nerd":
|
|
scores["cultural_proximity"] = await self._score_cultural_proximity(green_space)
|
|
scores["artistic_features"] = await self._score_artistic_features(green_space)
|
|
scores["museum_accessibility"] = await self._score_museum_accessibility(green_space)
|
|
scores["creative_atmosphere"] = self._score_creative_atmosphere(green_space)
|
|
scores["cafe_culture"] = await self._score_cafe_culture(green_space)
|
|
scores["inspiring_views"] = self._score_inspiring_views(green_space)
|
|
|
|
elif personality == "history_geek":
|
|
scores["historical_significance"] = await self._score_historical_significance(green_space)
|
|
scores["monument_proximity"] = await self._score_monument_proximity(green_space)
|
|
scores["educational_value"] = self._score_educational_value(green_space)
|
|
scores["architectural_features"] = self._score_architectural_features(green_space)
|
|
scores["cultural_context"] = await self._score_cultural_context(green_space)
|
|
|
|
return scores
|
|
|
|
# === COMPONENT SCORING METHODS ===
|
|
|
|
def _score_accessibility(self, green_space: GreenSpace, user_location: Optional[Tuple[float, float]]) -> int:
|
|
"""Score overall accessibility."""
|
|
score = green_space.accessibility.public_transport_score * 20
|
|
|
|
if green_space.accessibility.wheelchair_accessible:
|
|
score += 20
|
|
|
|
if user_location:
|
|
distance = geodesic(
|
|
user_location,
|
|
(green_space.coordinates.lat, green_space.coordinates.lng)
|
|
).meters
|
|
# Score based on distance (closer = better)
|
|
if distance <= 500:
|
|
score += 20
|
|
elif distance <= 1000:
|
|
score += 15
|
|
elif distance <= 2000:
|
|
score += 10
|
|
else:
|
|
score += 5
|
|
|
|
return min(100, score)
|
|
|
|
def _score_safety(self, green_space: GreenSpace) -> int:
|
|
"""Score safety based on lighting and accessibility."""
|
|
score = green_space.accessibility.lighting_quality * 15
|
|
if green_space.accessibility.wheelchair_accessible:
|
|
score += 20
|
|
score += green_space.accessibility.public_transport_score * 10
|
|
return min(100, score)
|
|
|
|
def _score_noise_level(self, green_space: GreenSpace) -> int:
|
|
"""Score based on noise level (lower noise = higher score)."""
|
|
return (6 - green_space.environmental.noise_level.value) * 20
|
|
|
|
async def _score_toilet_proximity(self, green_space: GreenSpace) -> int:
|
|
"""Score based on toilet proximity."""
|
|
toilets = [a for a in green_space.nearby_amenities if a.type == AmenityType.TOILET]
|
|
if not toilets:
|
|
return 20 # Assume some basic facilities
|
|
closest_distance = min(toilet.distance_meters for toilet in toilets)
|
|
if closest_distance <= 100:
|
|
return 100
|
|
elif closest_distance <= 300:
|
|
return 80
|
|
elif closest_distance <= 500:
|
|
return 60
|
|
else:
|
|
return 40
|
|
|
|
async def _score_family_amenities(self, green_space: GreenSpace) -> int:
|
|
"""Score family-friendly amenities."""
|
|
score = green_space.recreation.playground_quality
|
|
if green_space.environmental.shade_quality > 70:
|
|
score += 20
|
|
if green_space.accessibility.wheelchair_accessible:
|
|
score += 10
|
|
return min(100, score)
|
|
|
|
async def _score_romantic_atmosphere(self, green_space: GreenSpace) -> int:
|
|
"""Score romantic atmosphere."""
|
|
score = 0
|
|
if green_space.environmental.water_features:
|
|
score += 30
|
|
if green_space.environmental.noise_level.value <= 2:
|
|
score += 25
|
|
score += green_space.environmental.tree_coverage_percent // 2
|
|
score += green_space.accessibility.lighting_quality * 5
|
|
return min(100, score)
|
|
|
|
def _score_scenic_beauty(self, green_space: GreenSpace) -> int:
|
|
"""Score scenic beauty."""
|
|
score = green_space.environmental.tree_coverage_percent
|
|
if green_space.environmental.water_features:
|
|
score += 20
|
|
score += green_space.environmental.shade_quality // 2
|
|
return min(100, score)
|
|
|
|
async def _score_restaurant_proximity(self, green_space: GreenSpace) -> int:
|
|
"""Score restaurant proximity."""
|
|
restaurants = [a for a in green_space.nearby_amenities
|
|
if a.type in [AmenityType.RESTAURANT, AmenityType.CAFE]]
|
|
if not restaurants:
|
|
return 30
|
|
closest_distance = min(r.distance_meters for r in restaurants)
|
|
if closest_distance <= 200:
|
|
return 100
|
|
elif closest_distance <= 500:
|
|
return 80
|
|
else:
|
|
return 60
|
|
|
|
def _score_privacy(self, green_space: GreenSpace) -> int:
|
|
"""Score privacy based on size and tree coverage."""
|
|
score = 0
|
|
if green_space.area_sqm and green_space.area_sqm > 50000:
|
|
score += 40
|
|
elif green_space.area_sqm and green_space.area_sqm > 10000:
|
|
score += 20
|
|
score += green_space.environmental.tree_coverage_percent // 2
|
|
return min(100, score)
|
|
|
|
def _score_space_size(self, green_space: GreenSpace) -> int:
|
|
"""Score based on space size for groups."""
|
|
if not green_space.area_sqm:
|
|
return 50
|
|
if green_space.area_sqm > 100000:
|
|
return 100
|
|
elif green_space.area_sqm > 50000:
|
|
return 80
|
|
elif green_space.area_sqm > 20000:
|
|
return 60
|
|
else:
|
|
return 40
|
|
|
|
async def _score_group_amenities(self, green_space: GreenSpace) -> int:
|
|
"""Score amenities suitable for groups."""
|
|
score = 0
|
|
if green_space.recreation.bbq_allowed:
|
|
score += 30
|
|
if green_space.recreation.sports_facilities:
|
|
score += 25
|
|
toilets = [a for a in green_space.nearby_amenities if a.type == AmenityType.TOILET]
|
|
if toilets:
|
|
score += 20
|
|
score += green_space.accessibility.parking_availability * 5
|
|
return min(100, score)
|
|
|
|
async def _score_food_options(self, green_space: GreenSpace) -> int:
|
|
"""Score food options nearby."""
|
|
food_amenities = [a for a in green_space.nearby_amenities
|
|
if a.type in [AmenityType.RESTAURANT, AmenityType.CAFE,
|
|
AmenityType.ICE_CREAM, AmenityType.SPATI]]
|
|
return min(100, len(food_amenities) * 25)
|
|
|
|
def _score_activity_options(self, green_space: GreenSpace) -> int:
|
|
"""Score activity options."""
|
|
score = 0
|
|
if green_space.recreation.sports_facilities:
|
|
score += 30
|
|
if green_space.recreation.running_paths:
|
|
score += 20
|
|
if green_space.recreation.cycling_paths:
|
|
score += 20
|
|
if green_space.recreation.playground_quality > 0:
|
|
score += 15
|
|
if green_space.recreation.bbq_allowed:
|
|
score += 15
|
|
return min(100, score)
|
|
|
|
def _score_quietness(self, green_space: GreenSpace) -> int:
|
|
"""Score quietness for zen seekers."""
|
|
return (6 - green_space.environmental.noise_level.value) * 25
|
|
|
|
def _score_nature_immersion(self, green_space: GreenSpace) -> int:
|
|
"""Score nature immersion."""
|
|
score = green_space.environmental.tree_coverage_percent
|
|
score += green_space.environmental.natural_surface_percent // 2
|
|
if green_space.environmental.water_features:
|
|
score += 15
|
|
return min(100, score)
|
|
|
|
async def _score_crowd_density(self, green_space: GreenSpace) -> int:
|
|
"""Score based on crowd density (lower crowds = higher score)."""
|
|
# Mock implementation - in reality would use real-time data
|
|
base_score = 70
|
|
if green_space.area_sqm and green_space.area_sqm > 100000:
|
|
base_score += 20
|
|
if green_space.accessibility.public_transport_score <= 3:
|
|
base_score += 10
|
|
return min(100, base_score)
|
|
|
|
def _score_meditation_spots(self, green_space: GreenSpace) -> int:
|
|
"""Score meditation spot quality."""
|
|
score = green_space.environmental.tree_coverage_percent // 2
|
|
if green_space.environmental.water_features:
|
|
score += 25
|
|
if green_space.environmental.noise_level.value <= 2:
|
|
score += 25
|
|
return min(100, score)
|
|
|
|
def _score_air_quality(self, green_space: GreenSpace) -> int:
|
|
"""Score air quality."""
|
|
score = green_space.environmental.tree_coverage_percent
|
|
if green_space.environmental.natural_surface_percent > 80:
|
|
score += 20
|
|
return min(100, score)
|
|
|
|
def _score_running_cycling_paths(self, green_space: GreenSpace) -> int:
|
|
"""Score running and cycling infrastructure."""
|
|
score = 0
|
|
if green_space.recreation.running_paths:
|
|
score += 50
|
|
if green_space.recreation.cycling_paths:
|
|
score += 50
|
|
return score
|
|
|
|
def _score_sports_areas(self, green_space: GreenSpace) -> int:
|
|
"""Score sports areas."""
|
|
return 100 if green_space.recreation.sports_facilities else 30
|
|
|
|
def _score_terrain_variety(self, green_space: GreenSpace) -> int:
|
|
"""Score terrain variety for active users."""
|
|
score = 50 # Base score
|
|
if green_space.area_sqm and green_space.area_sqm > 50000:
|
|
score += 30
|
|
if green_space.environmental.natural_surface_percent > 70:
|
|
score += 20
|
|
return min(100, score)
|
|
|
|
def _score_natural_habitat(self, green_space: GreenSpace) -> int:
|
|
"""Score natural habitat quality."""
|
|
score = green_space.environmental.tree_coverage_percent
|
|
score += green_space.environmental.natural_surface_percent // 2
|
|
if green_space.environmental.water_features:
|
|
score += 15
|
|
return min(100, score)
|
|
|
|
def _score_observation_spots(self, green_space: GreenSpace) -> int:
|
|
"""Score wildlife observation opportunities."""
|
|
score = green_space.environmental.tree_coverage_percent // 2
|
|
if green_space.environmental.water_features:
|
|
score += 30
|
|
if green_space.environmental.noise_level.value <= 2:
|
|
score += 20
|
|
return min(100, score)
|
|
|
|
async def _score_cultural_proximity(self, green_space: GreenSpace) -> int:
|
|
"""Score proximity to cultural venues."""
|
|
# Mock implementation - would check for nearby museums, galleries, etc.
|
|
if green_space.neighborhood.lower() in ["mitte", "kreuzberg", "prenzlauer berg"]:
|
|
return 80
|
|
return 50
|
|
|
|
async def _score_artistic_features(self, green_space: GreenSpace) -> int:
|
|
"""Score artistic features in the space."""
|
|
# Mock implementation - would check for sculptures, installations, etc.
|
|
return 60 # Base score
|
|
|
|
async def _score_museum_accessibility(self, green_space: GreenSpace) -> int:
|
|
"""Score museum accessibility."""
|
|
# Mock implementation
|
|
if green_space.neighborhood.lower() == "mitte":
|
|
return 90
|
|
elif green_space.neighborhood.lower() in ["kreuzberg", "friedrichshain"]:
|
|
return 70
|
|
return 40
|
|
|
|
def _score_creative_atmosphere(self, green_space: GreenSpace) -> int:
|
|
"""Score creative atmosphere."""
|
|
score = 50 # Base score
|
|
if green_space.environmental.tree_coverage_percent > 60:
|
|
score += 20
|
|
if green_space.environmental.water_features:
|
|
score += 15
|
|
if green_space.accessibility.lighting_quality >= 4:
|
|
score += 15
|
|
return min(100, score)
|
|
|
|
async def _score_cafe_culture(self, green_space: GreenSpace) -> int:
|
|
"""Score cafe culture nearby."""
|
|
cafes = [a for a in green_space.nearby_amenities if a.type == AmenityType.CAFE]
|
|
return min(100, len(cafes) * 30 + 40)
|
|
|
|
def _score_inspiring_views(self, green_space: GreenSpace) -> int:
|
|
"""Score inspiring views."""
|
|
score = green_space.environmental.tree_coverage_percent // 2
|
|
if green_space.environmental.water_features:
|
|
score += 25
|
|
if green_space.area_sqm and green_space.area_sqm > 50000:
|
|
score += 25
|
|
return min(100, score)
|
|
|
|
async def _score_historical_significance(self, green_space: GreenSpace) -> int:
|
|
"""Score historical significance."""
|
|
# Mock implementation - would check historical databases
|
|
historical_neighborhoods = ["mitte", "kreuzberg", "charlottenburg"]
|
|
if green_space.neighborhood.lower() in historical_neighborhoods:
|
|
return 80
|
|
return 40
|
|
|
|
async def _score_monument_proximity(self, green_space: GreenSpace) -> int:
|
|
"""Score proximity to monuments."""
|
|
# Mock implementation
|
|
if green_space.neighborhood.lower() == "mitte":
|
|
return 90
|
|
return 50
|
|
|
|
def _score_educational_value(self, green_space: GreenSpace) -> int:
|
|
"""Score educational value."""
|
|
score = 50 # Base score
|
|
if green_space.environmental.wildlife_diversity_score > 70:
|
|
score += 25
|
|
if green_space.environmental.water_features:
|
|
score += 15
|
|
if green_space.area_sqm and green_space.area_sqm > 100000:
|
|
score += 10
|
|
return min(100, score)
|
|
|
|
def _score_architectural_features(self, green_space: GreenSpace) -> int:
|
|
"""Score architectural features."""
|
|
# Mock implementation
|
|
return 60
|
|
|
|
async def _score_cultural_context(self, green_space: GreenSpace) -> int:
|
|
"""Score cultural context."""
|
|
# Mock implementation
|
|
cultural_neighborhoods = ["mitte", "kreuzberg", "prenzlauer berg", "friedrichshain"]
|
|
if green_space.neighborhood.lower() in cultural_neighborhoods:
|
|
return 75
|
|
return 45
|
|
|
|
def _create_explanation(self, personality: str, explanation_parts: List[str]) -> str:
|
|
"""Create a human-readable explanation."""
|
|
if not explanation_parts:
|
|
return f"This space has moderate appeal for {personality.replace('_', ' ')} personality type."
|
|
|
|
return f"Great for {personality.replace('_', ' ')}: " + ", ".join(explanation_parts[:3])
|
|
|
|
def _generate_recommendations(
|
|
self,
|
|
green_space: GreenSpace,
|
|
personality: str,
|
|
component_scores: Dict[str, int]
|
|
) -> List[str]:
|
|
"""Generate personalized recommendations."""
|
|
recommendations = []
|
|
|
|
if personality == "little_adventurers":
|
|
if green_space.recreation.playground_quality > 70:
|
|
recommendations.append("Perfect playground for kids to explore")
|
|
if green_space.environmental.shade_quality > 70:
|
|
recommendations.append("Plenty of shade for family comfort")
|
|
|
|
elif personality == "date_night":
|
|
if green_space.environmental.water_features:
|
|
recommendations.append("Romantic walks by the water")
|
|
if component_scores.get("lighting_quality", 0) > 70:
|
|
recommendations.append("Beautiful evening atmosphere")
|
|
|
|
elif personality == "zen_masters":
|
|
if component_scores.get("quietness", 0) > 80:
|
|
recommendations.append("Perfect for meditation and reflection")
|
|
if green_space.environmental.water_features:
|
|
recommendations.append("Peaceful water sounds enhance tranquility")
|
|
|
|
# Add generic recommendations if none specific
|
|
if not recommendations:
|
|
recommendations.append("Enjoy the natural beauty of this space")
|
|
recommendations.append("Great for relaxation and outdoor activities")
|
|
|
|
return recommendations
|
|
|
|
async def score_location(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
personality: str,
|
|
radius: int
|
|
) -> Dict[str, Any]:
|
|
"""Score a specific location with optimized performance."""
|
|
# Check if location is in a green space
|
|
green_space = await self.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
|
|
}
|
|
|
|
# Score the green space (this now uses cached tree data internally)
|
|
personality_score = await self.score_green_space(green_space, personality, (lat, lng))
|
|
|
|
return {
|
|
"score": personality_score.score,
|
|
"explanation": personality_score.explanation,
|
|
"location": {"lat": lat, "lng": lng},
|
|
"personality": personality,
|
|
"green_space": green_space.name,
|
|
"recommendations": personality_score.recommendations
|
|
}
|
|
|
|
async def find_best_locations_within(
|
|
self,
|
|
green_space: GreenSpace,
|
|
personality: str
|
|
) -> List[LocationScore]:
|
|
"""Find best locations within a green space."""
|
|
# Mock implementation - in reality would analyze specific spots
|
|
locations = []
|
|
|
|
# Generate a few sample locations within the space
|
|
base_lat, base_lng = green_space.coordinates.lat, green_space.coordinates.lng
|
|
|
|
for i, (offset_lat, offset_lng, description) in enumerate([
|
|
(0.001, 0.001, "Northeast corner with mature trees"),
|
|
(-0.001, 0.001, "Southeast area near water feature"),
|
|
(0.0, -0.001, "Western meadow area")
|
|
]):
|
|
location = LocationScore(
|
|
coordinates=Coordinates(
|
|
lat=base_lat + offset_lat,
|
|
lng=base_lng + offset_lng
|
|
),
|
|
score=85 - i * 5, # Decreasing scores
|
|
explanation=f"Excellent spot: {description}",
|
|
nearby_features=["trees", "seating", "paths"],
|
|
best_for=[personality],
|
|
recommendations=[f"Perfect for {personality.replace('_', ' ')} activities"]
|
|
)
|
|
locations.append(location)
|
|
|
|
return locations
|
|
|
|
# === ENHANCED TREE-BASED SCORING METHODS ===
|
|
|
|
def _score_tree_coverage_with_real_data(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced tree coverage scoring using real street tree data."""
|
|
if not tree_data:
|
|
return green_space.environmental.tree_coverage_percent
|
|
|
|
try:
|
|
# Combine base environmental score with real tree data
|
|
base_score = green_space.environmental.tree_coverage_percent
|
|
tree_shade_coverage = tree_data.shade_analysis.estimated_shade_coverage
|
|
|
|
# Use the higher of the two scores, with bonus for high tree density
|
|
enhanced_score = max(base_score, tree_shade_coverage)
|
|
|
|
# Bonus for high tree density
|
|
if tree_data.metrics.trees_per_hectare > 50:
|
|
enhanced_score = min(100, enhanced_score + 15)
|
|
elif tree_data.metrics.trees_per_hectare > 20:
|
|
enhanced_score = min(100, enhanced_score + 10)
|
|
|
|
return int(enhanced_score)
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing tree coverage score: {e}")
|
|
return green_space.environmental.tree_coverage_percent
|
|
|
|
def _score_wildlife_diversity_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced wildlife diversity scoring using real tree species data."""
|
|
if not tree_data:
|
|
return green_space.environmental.wildlife_diversity_score
|
|
|
|
try:
|
|
base_score = green_space.environmental.wildlife_diversity_score
|
|
tree_diversity = tree_data.metrics.species_diversity_score
|
|
mature_trees_bonus = min(20, tree_data.metrics.mature_trees_count)
|
|
|
|
# Combine scores with weighting
|
|
enhanced_score = int((base_score * 0.6) + (tree_diversity * 0.4) + mature_trees_bonus)
|
|
|
|
return min(100, enhanced_score)
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing wildlife diversity score: {e}")
|
|
return green_space.environmental.wildlife_diversity_score
|
|
|
|
def _score_shade_quality_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced shade quality scoring using real tree data."""
|
|
if not tree_data:
|
|
return green_space.environmental.shade_quality
|
|
|
|
try:
|
|
base_shade = green_space.environmental.shade_quality
|
|
tree_shade_quality = tree_data.shade_analysis.shade_quality_score
|
|
|
|
# Use the better of the two scores
|
|
enhanced_score = max(base_shade, tree_shade_quality)
|
|
|
|
# Bonus for large nearby trees
|
|
large_trees_count = len(tree_data.shade_analysis.nearby_large_trees)
|
|
if large_trees_count > 5:
|
|
enhanced_score = min(100, enhanced_score + 15)
|
|
elif large_trees_count > 2:
|
|
enhanced_score = min(100, enhanced_score + 10)
|
|
|
|
return int(enhanced_score)
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing shade quality score: {e}")
|
|
return green_space.environmental.shade_quality
|
|
|
|
def _score_nature_immersion_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced nature immersion scoring using real tree data."""
|
|
if not tree_data:
|
|
return self._score_nature_immersion(green_space)
|
|
|
|
try:
|
|
# Base score from existing method
|
|
base_score = green_space.environmental.tree_coverage_percent
|
|
base_score += green_space.environmental.natural_surface_percent // 2
|
|
if green_space.environmental.water_features:
|
|
base_score += 15
|
|
|
|
# Enhancement from tree data
|
|
tree_density_score = min(30, tree_data.metrics.trees_per_hectare)
|
|
canopy_density_bonus = int(tree_data.shade_analysis.canopy_density * 20) if tree_data.shade_analysis.canopy_density else 0
|
|
species_diversity_bonus = min(15, tree_data.metrics.species_diversity_score // 5)
|
|
|
|
enhanced_score = base_score + tree_density_score + canopy_density_bonus + species_diversity_bonus
|
|
|
|
return min(100, int(enhanced_score))
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing nature immersion score: {e}")
|
|
return self._score_nature_immersion(green_space)
|
|
|
|
def _score_natural_habitat_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced natural habitat scoring using real tree data."""
|
|
if not tree_data:
|
|
return self._score_natural_habitat(green_space)
|
|
|
|
try:
|
|
base_score = green_space.environmental.tree_coverage_percent
|
|
base_score += green_space.environmental.natural_surface_percent // 2
|
|
if green_space.environmental.water_features:
|
|
base_score += 15
|
|
|
|
# Tree habitat quality factors
|
|
mature_trees_score = min(25, tree_data.metrics.mature_trees_count // 2)
|
|
species_diversity_score = min(20, tree_data.metrics.species_diversity_score // 3)
|
|
|
|
enhanced_score = base_score + mature_trees_score + species_diversity_score
|
|
|
|
return min(100, int(enhanced_score))
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing natural habitat score: {e}")
|
|
return self._score_natural_habitat(green_space)
|
|
|
|
def _score_observation_spots_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced wildlife observation scoring using real tree data."""
|
|
if not tree_data:
|
|
return self._score_observation_spots(green_space)
|
|
|
|
try:
|
|
base_score = green_space.environmental.tree_coverage_percent // 2
|
|
if green_space.environmental.water_features:
|
|
base_score += 30
|
|
if green_space.environmental.noise_level.value <= 2:
|
|
base_score += 20
|
|
|
|
# Large trees provide better observation opportunities
|
|
large_trees_count = len(tree_data.shade_analysis.nearby_large_trees)
|
|
observation_bonus = min(25, large_trees_count * 3)
|
|
|
|
# Species diversity attracts more wildlife to observe
|
|
diversity_bonus = min(15, tree_data.metrics.species_diversity_score // 4)
|
|
|
|
enhanced_score = base_score + observation_bonus + diversity_bonus
|
|
|
|
return min(100, int(enhanced_score))
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing observation spots score: {e}")
|
|
return self._score_observation_spots(green_space)
|
|
|
|
def _score_meditation_spots_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced meditation spots scoring using real tree data."""
|
|
if not tree_data:
|
|
return self._score_meditation_spots(green_space)
|
|
|
|
try:
|
|
base_score = green_space.environmental.tree_coverage_percent // 2
|
|
if green_space.environmental.water_features:
|
|
base_score += 25
|
|
if green_space.environmental.noise_level.value <= 2:
|
|
base_score += 25
|
|
|
|
# Trees enhance meditation through natural sounds and shade
|
|
shade_quality_bonus = min(20, tree_data.shade_analysis.shade_quality_score // 4)
|
|
canopy_bonus = int(tree_data.shade_analysis.canopy_density * 15) if tree_data.shade_analysis.canopy_density else 0
|
|
|
|
enhanced_score = base_score + shade_quality_bonus + canopy_bonus
|
|
|
|
return min(100, int(enhanced_score))
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing meditation spots score: {e}")
|
|
return self._score_meditation_spots(green_space)
|
|
|
|
def _score_air_quality_with_trees(self, green_space: GreenSpace, tree_data: Optional[Any] = None) -> int:
|
|
"""Enhanced air quality scoring using real tree data."""
|
|
if not tree_data:
|
|
return self._score_air_quality(green_space)
|
|
|
|
try:
|
|
base_score = green_space.environmental.tree_coverage_percent
|
|
if green_space.environmental.natural_surface_percent > 80:
|
|
base_score += 20
|
|
|
|
# More trees = better air quality
|
|
tree_density_bonus = min(25, tree_data.metrics.trees_per_hectare // 2)
|
|
mature_trees_bonus = min(15, tree_data.metrics.mature_trees_count // 3)
|
|
|
|
enhanced_score = base_score + tree_density_bonus + mature_trees_bonus
|
|
|
|
return min(100, int(enhanced_score))
|
|
|
|
except Exception as e:
|
|
print(f"Error enhancing air quality score: {e}")
|
|
return self._score_air_quality(green_space)
|