diff --git a/app/services/scoring_engine.py b/app/services/scoring_engine.py index aa570bb..6bb8794 100644 --- a/app/services/scoring_engine.py +++ b/app/services/scoring_engine.py @@ -99,9 +99,12 @@ class ScoringEngine: if not weights: raise ValueError(f"Unknown personality type: {personality}") - # Calculate component scores + # 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 + green_space, personality, user_location, tree_data ) # Calculate weighted final score @@ -134,11 +137,26 @@ class ScoringEngine: 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 + user_location: Optional[Tuple[float, float]] = None, + tree_data: Optional[Any] = None ) -> Dict[str, int]: """Calculate individual component scores.""" scores = {} @@ -151,7 +169,7 @@ class ScoringEngine: # Personality-specific components if personality == "little_adventurers": scores["playground_quality"] = green_space.recreation.playground_quality - scores["shade_quality"] = await self._score_shade_quality_with_trees(green_space) + 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) @@ -171,11 +189,11 @@ class ScoringEngine: elif personality == "zen_masters": scores["quietness"] = self._score_quietness(green_space) - scores["nature_immersion"] = await self._score_nature_immersion_with_trees(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"] = await self._score_meditation_spots_with_trees(green_space) - scores["air_quality"] = await self._score_air_quality_with_trees(green_space) + 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 @@ -184,11 +202,11 @@ class ScoringEngine: scores["terrain_variety"] = self._score_terrain_variety(green_space) elif personality == "wildlife_lover": - scores["wildlife_diversity"] = await self._score_wildlife_diversity_with_trees(green_space) - scores["natural_habitat"] = await self._score_natural_habitat_with_trees(green_space) + 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"] = await self._score_tree_coverage_with_real_data(green_space) - scores["observation_spots"] = await self._score_observation_spots_with_trees(green_space) + 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) @@ -391,7 +409,7 @@ class ScoringEngine: score += 25 return min(100, score) - async def _score_air_quality(self, green_space: GreenSpace) -> int: + 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: @@ -570,7 +588,7 @@ class ScoringEngine: personality: str, radius: int ) -> Dict[str, Any]: - """Score a specific location.""" + """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) @@ -582,7 +600,7 @@ class ScoringEngine: "personality": personality } - # Score the green space + # Score the green space (this now uses cached tree data internally) personality_score = await self.score_green_space(green_space, personality, (lat, lng)) return { @@ -628,26 +646,23 @@ class ScoringEngine: # === ENHANCED TREE-BASED SCORING METHODS === - async def _score_tree_coverage_with_real_data(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=300 - ) - # Combine base environmental score with real tree data base_score = green_space.environmental.tree_coverage_percent - tree_shade_coverage = tree_response.shade_analysis.estimated_shade_coverage + 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_response.metrics.trees_per_hectare > 50: + if tree_data.metrics.trees_per_hectare > 50: enhanced_score = min(100, enhanced_score + 15) - elif tree_response.metrics.trees_per_hectare > 20: + elif tree_data.metrics.trees_per_hectare > 20: enhanced_score = min(100, enhanced_score + 10) return int(enhanced_score) @@ -656,18 +671,15 @@ class ScoringEngine: print(f"Error enhancing tree coverage score: {e}") return green_space.environmental.tree_coverage_percent - async def _score_wildlife_diversity_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=400 - ) - base_score = green_space.environmental.wildlife_diversity_score - tree_diversity = tree_response.metrics.species_diversity_score - mature_trees_bonus = min(20, tree_response.metrics.mature_trees_count) + 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) @@ -678,23 +690,20 @@ class ScoringEngine: print(f"Error enhancing wildlife diversity score: {e}") return green_space.environmental.wildlife_diversity_score - async def _score_shade_quality_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=200 - ) - base_shade = green_space.environmental.shade_quality - tree_shade_quality = tree_response.shade_analysis.shade_quality_score + 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_response.shade_analysis.nearby_large_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: @@ -706,15 +715,12 @@ class ScoringEngine: print(f"Error enhancing shade quality score: {e}") return green_space.environmental.shade_quality - async def _score_nature_immersion_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=350 - ) - # Base score from existing method base_score = green_space.environmental.tree_coverage_percent base_score += green_space.environmental.natural_surface_percent // 2 @@ -722,9 +728,9 @@ class ScoringEngine: base_score += 15 # Enhancement from tree data - tree_density_score = min(30, tree_response.metrics.trees_per_hectare) - canopy_density_bonus = int(tree_response.shade_analysis.canopy_density * 20) if tree_response.shade_analysis.canopy_density else 0 - species_diversity_bonus = min(15, tree_response.metrics.species_diversity_score // 5) + 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 @@ -734,23 +740,20 @@ class ScoringEngine: print(f"Error enhancing nature immersion score: {e}") return self._score_nature_immersion(green_space) - async def _score_natural_habitat_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=300 - ) - 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_response.metrics.mature_trees_count // 2) - species_diversity_score = min(20, tree_response.metrics.species_diversity_score // 3) + 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 @@ -760,15 +763,12 @@ class ScoringEngine: print(f"Error enhancing natural habitat score: {e}") return self._score_natural_habitat(green_space) - async def _score_observation_spots_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=250 - ) - base_score = green_space.environmental.tree_coverage_percent // 2 if green_space.environmental.water_features: base_score += 30 @@ -776,11 +776,11 @@ class ScoringEngine: base_score += 20 # Large trees provide better observation opportunities - large_trees_count = len(tree_response.shade_analysis.nearby_large_trees) + 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_response.metrics.species_diversity_score // 4) + diversity_bonus = min(15, tree_data.metrics.species_diversity_score // 4) enhanced_score = base_score + observation_bonus + diversity_bonus @@ -790,15 +790,12 @@ class ScoringEngine: print(f"Error enhancing observation spots score: {e}") return self._score_observation_spots(green_space) - async def _score_meditation_spots_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=200 - ) - base_score = green_space.environmental.tree_coverage_percent // 2 if green_space.environmental.water_features: base_score += 25 @@ -806,8 +803,8 @@ class ScoringEngine: base_score += 25 # Trees enhance meditation through natural sounds and shade - shade_quality_bonus = min(20, tree_response.shade_analysis.shade_quality_score // 4) - canopy_bonus = int(tree_response.shade_analysis.canopy_density * 15) if tree_response.shade_analysis.canopy_density else 0 + 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 @@ -817,22 +814,19 @@ class ScoringEngine: print(f"Error enhancing meditation spots score: {e}") return self._score_meditation_spots(green_space) - async def _score_air_quality_with_trees(self, green_space: GreenSpace) -> int: + 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: - tree_response = await self.street_tree_service.get_trees_near_location( - green_space.coordinates.lat, - green_space.coordinates.lng, - radius_m=400 - ) - 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_response.metrics.trees_per_hectare // 2) - mature_trees_bonus = min(15, tree_response.metrics.mature_trees_count // 3) + 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 @@ -840,4 +834,4 @@ class ScoringEngine: except Exception as e: print(f"Error enhancing air quality score: {e}") - return await self._score_air_quality(green_space) + return self._score_air_quality(green_space)