Spaces:
Sleeping
Sleeping
""" | |
Intelligent Map Management System | |
Dynamic zoom, markers, and weather layer visualization | |
""" | |
import folium | |
import json | |
from typing import List, Dict, Tuple, Optional | |
import logging | |
import math | |
import re | |
logger = logging.getLogger(__name__) | |
class WeatherMapManager: | |
"""Advanced map management with intelligent features""" | |
def __init__(self): | |
self.default_center = (39.8283, -98.5795) # Geographic center of US | |
self.default_zoom = 4 | |
# Enhanced Weather condition to icon mapping with more specific conditions | |
self.weather_icons = { | |
# Sunny/Clear conditions | |
'sunny': {'icon': 'sun', 'color': 'orange'}, | |
'clear': {'icon': 'sun', 'color': 'orange'}, | |
'fair': {'icon': 'sun', 'color': 'orange'}, | |
'hot': {'icon': 'sun', 'color': 'red'}, | |
'warm': {'icon': 'sun', 'color': 'orange'}, | |
'bright': {'icon': 'sun', 'color': 'yellow'}, | |
'clear skies': {'icon': 'sun', 'color': 'orange'}, | |
# Cloudy conditions | |
'cloudy': {'icon': 'cloud', 'color': 'gray'}, | |
'partly cloudy': {'icon': 'cloud-sun', 'color': 'lightblue'}, | |
'mostly cloudy': {'icon': 'cloud', 'color': 'gray'}, | |
'overcast': {'icon': 'cloud', 'color': 'darkgray'}, | |
'partly sunny': {'icon': 'cloud-sun', 'color': 'lightblue'}, | |
'mostly sunny': {'icon': 'cloud-sun', 'color': 'orange'}, | |
'scattered clouds': {'icon': 'cloud-sun', 'color': 'lightblue'}, | |
'broken clouds': {'icon': 'cloud', 'color': 'gray'}, | |
'few clouds': {'icon': 'cloud-sun', 'color': 'lightblue'}, | |
# Rainy conditions | |
'rainy': {'icon': 'cloud-rain', 'color': 'blue'}, | |
'rain': {'icon': 'cloud-rain', 'color': 'blue'}, | |
'light rain': {'icon': 'cloud-drizzle', 'color': 'lightblue'}, | |
'heavy rain': {'icon': 'cloud-showers-heavy', 'color': 'darkblue'}, | |
'drizzle': {'icon': 'cloud-drizzle', 'color': 'lightblue'}, | |
'showers': {'icon': 'cloud-showers-heavy', 'color': 'blue'}, | |
'scattered showers': {'icon': 'cloud-rain', 'color': 'blue'}, | |
'isolated showers': {'icon': 'cloud-rain', 'color': 'lightblue'}, | |
'occasional showers': {'icon': 'cloud-rain', 'color': 'blue'}, | |
'intermittent rain': {'icon': 'cloud-drizzle', 'color': 'lightblue'}, | |
# Stormy conditions | |
'thunderstorm': {'icon': 'bolt', 'color': 'purple'}, | |
'thunderstorms': {'icon': 'bolt', 'color': 'purple'}, | |
'severe thunderstorms': {'icon': 'bolt', 'color': 'darkred'}, | |
'storm': {'icon': 'bolt', 'color': 'purple'}, | |
'storms': {'icon': 'bolt', 'color': 'purple'}, | |
'lightning': {'icon': 'bolt', 'color': 'purple'}, | |
'severe weather': {'icon': 'bolt', 'color': 'darkred'}, | |
'tornado': {'icon': 'tornado', 'color': 'darkred'}, | |
'hurricane': {'icon': 'hurricane', 'color': 'darkred'}, | |
# Snow conditions | |
'snow': {'icon': 'snowflake', 'color': 'white'}, | |
'snowy': {'icon': 'snowflake', 'color': 'white'}, | |
'light snow': {'icon': 'snowflake', 'color': 'lightgray'}, | |
'heavy snow': {'icon': 'snowflake', 'color': 'gray'}, | |
'snow showers': {'icon': 'snowflake', 'color': 'lightgray'}, | |
'blizzard': {'icon': 'snowflake', 'color': 'darkgray'}, | |
'flurries': {'icon': 'snowflake', 'color': 'lightgray'}, | |
'snow storm': {'icon': 'snowflake', 'color': 'darkgray'}, | |
'winter storm': {'icon': 'snowflake', 'color': 'darkgray'}, | |
# Windy conditions | |
'windy': {'icon': 'wind', 'color': 'green'}, | |
'breezy': {'icon': 'wind', 'color': 'lightgreen'}, | |
'gusty': {'icon': 'wind', 'color': 'green'}, | |
'strong winds': {'icon': 'wind', 'color': 'darkgreen'}, | |
'gale': {'icon': 'wind', 'color': 'darkgreen'}, | |
# Foggy/Misty conditions | |
'fog': {'icon': 'smog', 'color': 'gray'}, | |
'foggy': {'icon': 'smog', 'color': 'gray'}, | |
'mist': {'icon': 'smog', 'color': 'lightgray'}, | |
'misty': {'icon': 'smog', 'color': 'lightgray'}, | |
'haze': {'icon': 'smog', 'color': 'gray'}, | |
'hazy': {'icon': 'smog', 'color': 'gray'}, | |
'dense fog': {'icon': 'smog', 'color': 'darkgray'}, | |
'patchy fog': {'icon': 'smog', 'color': 'lightgray'}, | |
# Mixed conditions | |
'rain and snow': {'icon': 'cloud-rain', 'color': 'blue'}, | |
'wintry mix': {'icon': 'snowflake', 'color': 'lightblue'}, | |
'sleet': {'icon': 'snowflake', 'color': 'lightblue'}, | |
'freezing rain': {'icon': 'icicles', 'color': 'lightblue'}, | |
'ice': {'icon': 'icicles', 'color': 'lightblue'}, | |
'icy': {'icon': 'icicles', 'color': 'lightblue'}, | |
'freezing drizzle': {'icon': 'icicles', 'color': 'lightblue'}, | |
'mixed precipitation': {'icon': 'cloud-rain', 'color': 'blue'}, | |
# Temperature extremes | |
'cold': {'icon': 'thermometer-quarter', 'color': 'blue'}, | |
'freezing': {'icon': 'thermometer-empty', 'color': 'lightblue'}, | |
'cool': {'icon': 'thermometer-half', 'color': 'lightblue'}, | |
'extreme heat': {'icon': 'thermometer-full', 'color': 'red'}, | |
'heat wave': {'icon': 'thermometer-full', 'color': 'darkred'}, | |
# Special conditions | |
'dust storm': {'icon': 'wind', 'color': 'brown'}, | |
'sandstorm': {'icon': 'wind', 'color': 'brown'}, | |
'smoke': {'icon': 'smog', 'color': 'gray'}, | |
'volcanic ash': {'icon': 'smog', 'color': 'darkgray'}, | |
'air quality alert': {'icon': 'exclamation-triangle', 'color': 'red'}, | |
# Default fallback | |
'default': {'icon': 'cloud', 'color': 'blue'} | |
} | |
def _get_weather_icon(self, weather_condition: str) -> Dict[str, str]: | |
"""Determine appropriate icon and color based on weather condition with enhanced matching""" | |
if not weather_condition: | |
return self.weather_icons['default'] | |
# Convert to lowercase for matching | |
condition_lower = weather_condition.lower().strip() | |
# Direct match first (exact match) | |
if condition_lower in self.weather_icons: | |
return self.weather_icons[condition_lower] | |
# Enhanced partial matching with priority order | |
# Priority 1: Severe weather conditions (highest priority) | |
severe_conditions = [ | |
('tornado', 'tornado'), ('hurricane', 'hurricane'), | |
('severe thunderstorms', 'severe thunderstorms'), | |
('severe weather', 'severe weather'), ('blizzard', 'blizzard'), | |
('winter storm', 'winter storm'), ('heat wave', 'heat wave') | |
] | |
for keyword, icon_key in severe_conditions: | |
if keyword in condition_lower: | |
return self.weather_icons[icon_key] | |
# Priority 2: Precipitation types | |
precip_conditions = [ | |
('freezing rain', 'freezing rain'), ('freezing drizzle', 'freezing drizzle'), | |
('heavy snow', 'heavy snow'), ('light snow', 'light snow'), | |
('snow showers', 'snow showers'), ('heavy rain', 'heavy rain'), | |
('light rain', 'light rain'), ('scattered showers', 'scattered showers'), | |
('thunderstorms', 'thunderstorms'), ('thunderstorm', 'thunderstorm'), | |
('wintry mix', 'wintry mix'), ('sleet', 'sleet') | |
] | |
for keyword, icon_key in precip_conditions: | |
if keyword in condition_lower: | |
return self.weather_icons[icon_key] | |
# Priority 3: Cloud conditions | |
cloud_conditions = [ | |
('mostly cloudy', 'mostly cloudy'), ('partly cloudy', 'partly cloudy'), | |
('scattered clouds', 'scattered clouds'), ('broken clouds', 'broken clouds'), | |
('few clouds', 'few clouds'), ('mostly sunny', 'mostly sunny'), | |
('partly sunny', 'partly sunny'), ('overcast', 'overcast') | |
] | |
for keyword, icon_key in cloud_conditions: | |
if keyword in condition_lower: | |
return self.weather_icons[icon_key] | |
# Priority 4: Clear conditions | |
clear_conditions = [ | |
('clear skies', 'clear skies'), ('sunny', 'sunny'), ('clear', 'clear'), | |
('fair', 'fair'), ('bright', 'bright') | |
] | |
for keyword, icon_key in clear_conditions: | |
if keyword in condition_lower: | |
return self.weather_icons[icon_key] | |
# Priority 5: General keyword matching with improved logic | |
if any(term in condition_lower for term in ['tornado', 'hurricane']): | |
return self.weather_icons.get('tornado', self.weather_icons['severe weather']) | |
elif any(term in condition_lower for term in ['thunder', 'lightning', 'storm']): | |
if 'severe' in condition_lower: | |
return self.weather_icons['severe thunderstorms'] | |
return self.weather_icons['thunderstorms'] | |
elif any(term in condition_lower for term in ['snow', 'blizzard', 'flurries', 'winter']): | |
if 'heavy' in condition_lower or 'blizzard' in condition_lower: | |
return self.weather_icons['heavy snow'] | |
elif 'light' in condition_lower or 'flurries' in condition_lower: | |
return self.weather_icons['light snow'] | |
return self.weather_icons['snow'] | |
elif any(term in condition_lower for term in ['rain', 'shower', 'drizzle', 'precipitation']): | |
if 'heavy' in condition_lower: | |
return self.weather_icons['heavy rain'] | |
elif 'light' in condition_lower or 'drizzle' in condition_lower: | |
return self.weather_icons['light rain'] | |
elif 'freezing' in condition_lower: | |
return self.weather_icons['freezing rain'] | |
return self.weather_icons['rain'] | |
elif any(term in condition_lower for term in ['cloud', 'overcast']): | |
if 'partly' in condition_lower: | |
return self.weather_icons['partly cloudy'] | |
elif 'mostly' in condition_lower: | |
return self.weather_icons['mostly cloudy'] | |
return self.weather_icons['cloudy'] | |
elif any(term in condition_lower for term in ['fog', 'mist', 'haze']): | |
if 'dense' in condition_lower: | |
return self.weather_icons.get('dense fog', self.weather_icons['fog']) | |
elif 'patchy' in condition_lower: | |
return self.weather_icons.get('patchy fog', self.weather_icons['mist']) | |
return self.weather_icons['fog'] | |
elif any(term in condition_lower for term in ['wind', 'breezy', 'gusty', 'gale']): | |
if 'strong' in condition_lower or 'gale' in condition_lower: | |
return self.weather_icons.get('strong winds', self.weather_icons['windy']) | |
elif 'breezy' in condition_lower: | |
return self.weather_icons['breezy'] | |
return self.weather_icons['windy'] | |
elif any(term in condition_lower for term in ['hot', 'warm', 'heat']): | |
if 'extreme' in condition_lower or 'wave' in condition_lower: | |
return self.weather_icons.get('heat wave', self.weather_icons['hot']) | |
return self.weather_icons['hot'] | |
elif any(term in condition_lower for term in ['cold', 'cool', 'freezing']): | |
if 'freezing' in condition_lower: | |
return self.weather_icons['freezing'] | |
elif 'cold' in condition_lower: | |
return self.weather_icons['cold'] | |
return self.weather_icons['cool'] | |
elif any(term in condition_lower for term in ['dust', 'sand']): | |
return self.weather_icons.get('dust storm', self.weather_icons['windy']) | |
elif any(term in condition_lower for term in ['smoke', 'ash']): | |
return self.weather_icons.get('smoke', self.weather_icons['fog']) | |
# Final fallback | |
return self.weather_icons['default'] | |
def _get_temperature_icon_enhancement(self, temperature: Optional[int]) -> Dict[str, str]: | |
"""Get additional icon styling based on temperature with enhanced granularity""" | |
if temperature is None: | |
return {} | |
# Enhanced temperature-based color adjustments with more granular ranges | |
if temperature >= 100: | |
return {'color': 'darkred'} # Extreme heat | |
elif temperature >= 95: | |
return {'color': 'red'} # Very hot | |
elif temperature >= 85: | |
return {'color': 'orange'} # Hot | |
elif temperature >= 75: | |
return {'color': 'yellow'} # Warm | |
elif temperature >= 65: | |
return {'color': 'green'} # Pleasant | |
elif temperature >= 55: | |
return {'color': 'lightgreen'} # Cool | |
elif temperature >= 45: | |
return {'color': 'lightblue'} # Cold | |
elif temperature >= 35: | |
return {'color': 'blue'} # Very cold | |
elif temperature >= 25: | |
return {'color': 'purple'} # Freezing | |
elif temperature >= 15: | |
return {'color': 'darkblue'} # Very freezing | |
else: | |
return {'color': 'black'} # Extreme cold | |
def _get_weather_icon_with_context(self, weather_condition: str, temperature: Optional[int] = None, | |
wind_speed: Optional[str] = None) -> Dict[str, str]: | |
"""Get weather icon with additional context like temperature and wind""" | |
# Get base icon from weather condition | |
base_icon = self._get_weather_icon(weather_condition) | |
# Apply temperature-based color enhancement | |
if temperature is not None: | |
temp_enhancement = self._get_temperature_icon_enhancement(temperature) | |
if temp_enhancement.get('color'): | |
base_icon = {**base_icon, 'color': temp_enhancement['color']} | |
# Consider wind speed for icon selection | |
if wind_speed and isinstance(wind_speed, str): | |
try: | |
# Extract wind speed number | |
import re | |
wind_match = re.search(r'(\d+)', wind_speed) | |
if wind_match: | |
wind_val = int(wind_match.group(1)) | |
# High wind conditions override some icons | |
if wind_val >= 35: # Very strong winds | |
if base_icon['icon'] not in ['bolt', 'tornado', 'hurricane']: | |
# Override with wind icon for extreme wind | |
base_icon = {'icon': 'wind', 'color': 'darkred'} | |
elif wind_val >= 25: # Strong winds | |
if base_icon['icon'] in ['cloud', 'cloud-sun']: | |
# Modify cloudy conditions to show wind | |
base_icon['icon'] = 'wind' | |
base_icon['color'] = 'green' | |
except: | |
pass | |
return base_icon | |
def _get_weather_emoji(self, weather_condition: str) -> str: | |
"""Get appropriate emoji for weather condition""" | |
if not weather_condition: | |
return "🌤️" | |
condition_lower = weather_condition.lower() | |
# Direct emoji mapping | |
emoji_map = { | |
'sunny': '☀️', 'clear': '☀️', 'fair': '🌤️', 'hot': '🌡️', | |
'cloudy': '☁️', 'partly cloudy': '⛅', 'mostly cloudy': '☁️', 'overcast': '☁️', | |
'partly sunny': '⛅', 'mostly sunny': '🌤️', | |
'rainy': '🌧️', 'rain': '🌧️', 'light rain': '🌦️', 'heavy rain': '🌧️', | |
'drizzle': '🌦️', 'showers': '🌦️', 'scattered showers': '🌦️', | |
'thunderstorm': '⛈️', 'thunderstorms': '⛈️', 'storm': '⛈️', 'storms': '⛈️', | |
'snow': '❄️', 'snowy': '❄️', 'light snow': '🌨️', 'heavy snow': '❄️', | |
'snow showers': '🌨️', 'blizzard': '🌨️', 'flurries': '🌨️', | |
'windy': '💨', 'breezy': '🍃', 'gusty': '💨', | |
'fog': '🌫️', 'foggy': '🌫️', 'mist': '🌫️', 'misty': '🌫️', 'haze': '🌫️', | |
'freezing rain': '🧊', 'sleet': '🧊', 'ice': '🧊', 'icy': '🧊' | |
} | |
# Direct match | |
if condition_lower in emoji_map: | |
return emoji_map[condition_lower] | |
# Partial matching | |
for key, emoji in emoji_map.items(): | |
if key in condition_lower: | |
return emoji | |
return "🌤️" # Default emoji | |
def calculate_optimal_bounds(self, coordinates: List[Tuple[float, float]]) -> Dict: | |
"""Calculate optimal map bounds for given coordinates""" | |
if not coordinates: | |
return {'center': self.default_center, 'zoom': self.default_zoom} | |
if len(coordinates) == 1: | |
return {'center': coordinates[0], 'zoom': 10} | |
# Calculate center point | |
lats = [coord[0] for coord in coordinates] | |
lons = [coord[1] for coord in coordinates] | |
center_lat = sum(lats) / len(lats) | |
center_lon = sum(lons) / len(lons) | |
# Calculate zoom based on coordinate spread | |
lat_range = max(lats) - min(lats) | |
lon_range = max(lons) - min(lons) | |
max_range = max(lat_range, lon_range) | |
# Determine appropriate zoom level | |
if max_range < 0.5: | |
zoom = 11 | |
elif max_range < 1: | |
zoom = 9 | |
elif max_range < 3: | |
zoom = 8 | |
elif max_range < 5: | |
zoom = 7 | |
elif max_range < 10: | |
zoom = 6 | |
elif max_range < 20: | |
zoom = 5 | |
else: | |
zoom = 4 | |
return {'center': (center_lat, center_lon), 'zoom': zoom} | |
def create_weather_map(self, cities_data: List[Dict], comparison_mode: bool = False, | |
show_weather_layers: bool = True) -> str: | |
"""Create comprehensive weather map with all features""" | |
try: | |
# Calculate optimal view | |
if cities_data: | |
coordinates = [(city['lat'], city['lon']) for city in cities_data] | |
bounds = self.calculate_optimal_bounds(coordinates) | |
else: | |
bounds = {'center': self.default_center, 'zoom': self.default_zoom} | |
# Create base map with street map as default | |
m = folium.Map( | |
location=bounds['center'], | |
zoom_start=bounds['zoom'], | |
tiles='OpenStreetMap', | |
attr='OpenStreetMap' | |
) | |
# Add alternative tile layers | |
folium.TileLayer( | |
'CartoDB dark_matter', | |
name='Dark Theme', | |
overlay=False, | |
control=True, | |
attr='CartoDB & OpenStreetMap contributors' | |
).add_to(m) | |
folium.TileLayer( | |
'CartoDB positron', | |
name='Light Theme', | |
overlay=False, | |
control=True, | |
attr='CartoDB & OpenStreetMap contributors' | |
).add_to(m) | |
# Add weather layers if requested | |
if show_weather_layers: | |
self._add_weather_layers(m) | |
# Add city markers | |
if cities_data: | |
self._add_city_markers(m, cities_data) | |
# Add comparison features | |
if comparison_mode and len(cities_data) > 1: | |
self._add_comparison_features(m, cities_data) | |
# Add map controls | |
folium.LayerControl().add_to(m) | |
# Add fullscreen button | |
from folium.plugins import Fullscreen | |
Fullscreen().add_to(m) | |
return m._repr_html_() | |
except Exception as e: | |
logger.error(f"Error creating weather map: {e}") | |
return self._create_error_map(str(e)) | |
def create_enhanced_weather_map(self, cities_data: List[Dict], mcp_data: Dict = None) -> str: | |
"""Create enhanced weather map with MCP data integration""" | |
try: | |
# Calculate optimal view | |
if cities_data: | |
coordinates = [(city['lat'], city['lon']) for city in cities_data] | |
bounds = self.calculate_optimal_bounds(coordinates) | |
else: | |
bounds = {'center': self.default_center, 'zoom': self.default_zoom} | |
# Create base map with enhanced styling | |
m = folium.Map( | |
location=bounds['center'], | |
zoom_start=bounds['zoom'], | |
tiles='OpenStreetMap', | |
attr='OpenStreetMap' | |
) | |
# Add enhanced tile layers | |
folium.TileLayer( | |
'CartoDB dark_matter', | |
name='Dark Theme', | |
overlay=False, | |
control=True, | |
attr='CartoDB & OpenStreetMap contributors' | |
).add_to(m) | |
folium.TileLayer( | |
'OpenTopoMap', | |
name='Topographic', | |
overlay=False, | |
control=True, | |
attr='OpenTopoMap contributors' | |
).add_to(m) | |
folium.TileLayer( | |
'CartoDB positron', | |
name='Light Theme', | |
overlay=False, | |
control=True, | |
attr='CartoDB & OpenStreetMap contributors' | |
).add_to(m) | |
# Add weather layers | |
self._add_weather_layers(m) | |
# Add enhanced markers with MCP data | |
for city in cities_data: | |
city_name = city.get('name', 'Unknown City') | |
# Enhanced popup with MCP integration | |
popup_content = self._create_enhanced_popup(city, mcp_data) | |
# Get weather condition for enhanced icon selection | |
forecast = city.get('forecast', []) | |
if forecast: | |
weather_condition = forecast[0].get('shortForecast', '') | |
temperature = forecast[0].get('temperature') | |
wind_speed = forecast[0].get('windSpeed') | |
# Use enhanced icon system with context | |
icon_info = self._get_weather_icon_with_context( | |
weather_condition, temperature, wind_speed | |
) | |
else: | |
icon_info = self.weather_icons['default'] | |
# Enhanced marker with more data and better styling | |
folium.Marker( | |
location=[city['lat'], city['lon']], | |
popup=folium.Popup(popup_content, max_width=300), | |
tooltip=f"🌤️ {city_name} - Enhanced weather data available", | |
icon=folium.Icon( | |
icon=icon_info['icon'], | |
prefix='fa', | |
color=icon_info['color'], | |
icon_color='white' | |
) | |
).add_to(m) | |
# Add enhanced weather circle indicator | |
if forecast: | |
current_weather = forecast[0] | |
current_temp = current_weather.get('temperature', 70) | |
precip_prob = current_weather.get('precipitationProbability', 0) or 0 | |
weather_condition = current_weather.get('shortForecast', '').lower() | |
# Enhanced color coding based on temperature and conditions | |
if 'snow' in weather_condition: | |
circle_color = '#87ceeb' # Sky blue for snow | |
elif 'rain' in weather_condition or precip_prob > 50: | |
circle_color = '#4682b4' # Steel blue for rain | |
elif current_temp >= 85: | |
circle_color = '#ff6b35' # Hot orange | |
elif current_temp >= 75: | |
circle_color = '#f7931e' # Warm orange | |
elif current_temp >= 60: | |
circle_color = '#ffdc00' # Yellow | |
elif current_temp >= 40: | |
circle_color = '#00bfff' # Deep sky blue | |
else: | |
circle_color = '#6495ed' # Cornflower blue | |
# Adjust circle size and opacity based on conditions | |
base_radius = 25000 | |
radius_modifier = (precip_prob / 100) * 15000 # Up to 15km extra for high precipitation | |
final_radius = base_radius + radius_modifier | |
fill_opacity = 0.15 + (precip_prob / 100) * 0.25 # 0.15 to 0.4 opacity | |
folium.Circle( | |
location=[city['lat'], city['lon']], | |
radius=final_radius, | |
popup=f"🌡️ {current_temp}°F | 🌧️ {precip_prob}% chance | Enhanced with MCP data", | |
tooltip=f"Enhanced weather zone: {current_temp}°F, {precip_prob}% precipitation", | |
color=circle_color, | |
fill=True, | |
fillOpacity=fill_opacity, | |
weight=2 | |
).add_to(m) | |
# Add layer control and fullscreen | |
folium.LayerControl().add_to(m) | |
try: | |
from folium.plugins import Fullscreen | |
Fullscreen().add_to(m) | |
except ImportError: | |
pass # Fullscreen plugin not available | |
return m._repr_html_() | |
except Exception as e: | |
logger.error(f"Error creating enhanced weather map: {e}") | |
return self._create_error_map(str(e)) | |
def _add_weather_layers(self, map_obj: folium.Map): | |
"""Add weather overlay layers""" | |
try: | |
# Precipitation radar layer | |
precipitation_layer = folium.raster_layers.WmsTileLayer( | |
url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi', | |
layers='nexrad-n0r-900913', | |
name='Precipitation Radar', | |
overlay=True, | |
control=True, | |
transparent=True, | |
format='image/png', | |
opacity=0.6, | |
attr='Iowa Environmental Mesonet' | |
) | |
precipitation_layer.add_to(map_obj) | |
# Temperature layer (simplified) | |
# Note: In production, you'd use actual weather service WMS layers | |
except Exception as e: | |
logger.warning(f"Could not add weather layers: {e}") | |
def _add_city_markers(self, map_obj: folium.Map, cities_data: List[Dict]): | |
"""Add markers for cities with weather information using enhanced icon system""" | |
for i, city_data in enumerate(cities_data): | |
# Get weather condition and temperature from forecast | |
forecast = city_data.get('forecast', []) | |
current_weather = forecast[0] if forecast else {} | |
weather_condition = current_weather.get('shortForecast', '') | |
temperature = current_weather.get('temperature') | |
wind_speed = current_weather.get('windSpeed') | |
# Use enhanced weather icon selection with context | |
weather_icon_info = self._get_weather_icon_with_context( | |
weather_condition, temperature, wind_speed | |
) | |
icon_name = weather_icon_info['icon'] | |
icon_color = weather_icon_info['color'] | |
# Create enhanced weather condition tooltip with more details | |
weather_tooltip = f"📍 {city_data['name'].title()}" | |
if weather_condition: | |
weather_tooltip += f" - {weather_condition}" | |
if temperature: | |
weather_tooltip += f" ({temperature}°F)" | |
if wind_speed: | |
weather_tooltip += f" | Wind: {wind_speed}" | |
weather_tooltip += " - Click for details" | |
# Create detailed popup | |
popup_html = self._create_popup_html(city_data) | |
# Add main marker with enhanced weather-appropriate icon | |
folium.Marker( | |
location=[city_data['lat'], city_data['lon']], | |
popup=folium.Popup(popup_html, max_width=420), | |
tooltip=weather_tooltip, | |
icon=folium.Icon( | |
color=icon_color, | |
icon=icon_name, | |
prefix='fa', | |
icon_color='white' # Ensure icon is visible | |
) | |
).add_to(map_obj) | |
# Add wind flow arrow if wind data is available | |
if wind_speed and current_weather.get('windDirection'): | |
self._add_wind_arrow(map_obj, city_data['lat'], city_data['lon'], | |
wind_speed, current_weather['windDirection']) | |
# Add enhanced weather circle indicator with precipitation info | |
if forecast: | |
current_temp = current_weather.get('temperature', 0) | |
precip_prob = current_weather.get('precipitationProbability', 0) or 0 | |
# Enhanced color coding based on temperature and precipitation | |
if current_temp >= 95: | |
circle_color = 'darkred' | |
elif current_temp >= 85: | |
circle_color = 'red' | |
elif current_temp >= 75: | |
circle_color = 'orange' | |
elif current_temp >= 65: | |
circle_color = 'yellow' | |
elif current_temp >= 55: | |
circle_color = 'green' | |
elif current_temp >= 45: | |
circle_color = 'lightblue' | |
elif current_temp >= 35: | |
circle_color = 'blue' | |
else: | |
circle_color = 'purple' | |
# Adjust circle opacity based on precipitation probability | |
fill_opacity = 0.2 + (precip_prob / 100) * 0.3 # 0.2 to 0.5 opacity | |
folium.Circle( | |
location=[city_data['lat'], city_data['lon']], | |
radius=25000 + (precip_prob * 200), # Size varies with precipitation | |
popup=f"🌡️ {current_temp}°F | 🌧️ {precip_prob}% chance", | |
tooltip=f"Temperature zone with {precip_prob}% rain chance", | |
color=circle_color, | |
fill=True, | |
fillOpacity=fill_opacity, | |
weight=2 | |
).add_to(map_obj) | |
def _add_comparison_features(self, map_obj: folium.Map, cities_data: List[Dict]): | |
"""Add comparison lines and features between cities""" | |
if len(cities_data) < 2: | |
return | |
# Add connection lines between all cities | |
coordinates = [[city['lat'], city['lon']] for city in cities_data] | |
# Create comparison route | |
folium.PolyLine( | |
coordinates, | |
color='yellow', | |
weight=4, | |
opacity=0.8, | |
popup="Weather Comparison Route", | |
tooltip="Cities being compared" | |
).add_to(map_obj) | |
# Add midpoint marker for comparison summary | |
if len(cities_data) == 2: | |
city1, city2 = cities_data[0], cities_data[1] | |
mid_lat = (city1['lat'] + city2['lat']) / 2 | |
mid_lon = (city1['lon'] + city2['lon']) / 2 | |
comparison_summary = self._create_comparison_summary(city1, city2) | |
folium.Marker( | |
location=[mid_lat, mid_lon], | |
popup=folium.Popup(comparison_summary, max_width=400), | |
tooltip="Comparison Summary", | |
icon=folium.Icon( | |
color='lightblue', | |
icon='balance-scale', | |
prefix='fa' | |
) | |
).add_to(map_obj) | |
def _create_popup_html(self, city_data: Dict) -> str: | |
"""Create detailed HTML popup for city marker""" | |
forecast = city_data.get('forecast', []) | |
if not forecast: | |
return f""" | |
<div style="width: 300px; font-family: Arial, sans-serif;"> | |
<h3 style="margin: 0; color: #2c3e50;">📍 {city_data['name'].title()}</h3> | |
<p>No weather data available</p> | |
</div> | |
""" | |
current = forecast[0] | |
next_period = forecast[1] if len(forecast) > 1 else {} | |
html = f""" | |
<div style="width: 320px; font-family: Arial, sans-serif; line-height: 1.4;"> | |
<h3 style="margin: 0 0 10px 0; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px;"> | |
📍 {city_data['name'].title()} | |
</h3> | |
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px;"> | |
<h4 style="margin: 0 0 8px 0; color: #e74c3c;">🌡️ Current Conditions</h4> | |
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px; font-size: 13px;"> | |
<div><strong>Temperature:</strong></div> | |
<div>{current.get('temperature', 'N/A')}°{current.get('temperatureUnit', 'F')}</div> | |
<div><strong>Conditions:</strong></div> | |
<div>{current.get('shortForecast', 'N/A')}</div> | |
<div><strong>Wind:</strong></div> | |
<div>{current.get('windSpeed', 'N/A')} {current.get('windDirection', '')}</div> | |
<div><strong>Rain Chance:</strong></div> | |
<div>{current.get('precipitationProbability', 0)}%</div> | |
</div> | |
</div> | |
<div style="background: #e8f4f8; padding: 10px; border-radius: 5px; margin-bottom: 10px;"> | |
<h4 style="margin: 0 0 8px 0; color: #2980b9;">📅 Next Period</h4> | |
<div style="font-size: 13px;"> | |
<strong>{next_period.get('name', 'N/A')}:</strong><br> | |
{next_period.get('temperature', 'N/A')}°{next_period.get('temperatureUnit', 'F')} - | |
{next_period.get('shortForecast', 'N/A')} | |
</div> | |
</div> | |
<div style="background: #fff3cd; padding: 8px; border-radius: 5px; font-size: 12px;"> | |
<strong>📝 Details:</strong><br> | |
{current.get('detailedForecast', 'No detailed forecast available')[:150]}... | |
</div> | |
<div style="text-align: center; margin-top: 10px; font-size: 11px; color: #6c757d;"> | |
📊 Coordinates: {city_data['lat']:.3f}°, {city_data['lon']:.3f}° | |
</div> | |
</div> | |
""" | |
return html | |
def _create_comparison_summary(self, city1: Dict, city2: Dict) -> str: | |
"""Create comparison summary popup""" | |
name1, name2 = city1['name'].title(), city2['name'].title() | |
forecast1 = city1.get('forecast', [{}])[0] | |
forecast2 = city2.get('forecast', [{}])[0] | |
temp1 = forecast1.get('temperature', 0) | |
temp2 = forecast2.get('temperature', 0) | |
temp_diff = abs(temp1 - temp2) | |
warmer_city = name1 if temp1 > temp2 else name2 | |
rain1 = forecast1.get('precipitationProbability', 0) | |
rain2 = forecast2.get('precipitationProbability', 0) | |
rain_diff = abs(rain1 - rain2) | |
rainier_city = name1 if rain1 > rain2 else name2 | |
html = f""" | |
<div style="width: 350px; font-family: Arial, sans-serif;"> | |
<h3 style="margin: 0 0 15px 0; color: #2c3e50; text-align: center; border-bottom: 2px solid #f39c12; padding-bottom: 8px;"> | |
⚖️ Weather Comparison | |
</h3> | |
<div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 12px; border-radius: 8px; margin-bottom: 15px;"> | |
<h4 style="margin: 0 0 10px 0; text-align: center;">🌡️ Temperature Comparison</h4> | |
<div style="display: grid; grid-template-columns: 1fr auto 1fr; gap: 10px; align-items: center;"> | |
<div style="text-align: center;"> | |
<div style="font-weight: bold;">{name1}</div> | |
<div style="font-size: 18px;">{temp1}°F</div> | |
</div> | |
<div style="text-align: center; font-size: 20px;">VS</div> | |
<div style="text-align: center;"> | |
<div style="font-weight: bold;">{name2}</div> | |
<div style="font-size: 18px;">{temp2}°F</div> | |
</div> | |
</div> | |
<div style="text-align: center; margin-top: 10px; font-size: 14px;"> | |
<strong>{warmer_city}</strong> is {temp_diff}°F warmer | |
</div> | |
</div> | |
<div style="background: linear-gradient(135deg, #4ecdc4, #44a08d); color: white; padding: 12px; border-radius: 8px; margin-bottom: 15px;"> | |
<h4 style="margin: 0 0 10px 0; text-align: center;">🌧️ Precipitation Comparison</h4> | |
<div style="display: grid; grid-template-columns: 1fr auto 1fr; gap: 10px; align-items: center;"> | |
<div style="text-align: center;"> | |
<div style="font-weight: bold;">{name1}</div> | |
<div style="font-size: 18px;">{rain1}%</div> | |
</div> | |
<div style="text-align: center; font-size: 20px;">VS</div> | |
<div style="text-align: center;"> | |
<div style="font-weight: bold;">{name2}</div> | |
<div style="font-size: 18px;">{rain2}%</div> | |
</div> | |
</div> | |
<div style="text-align: center; margin-top: 10px; font-size: 14px;"> | |
<strong>{rainier_city}</strong> has {rain_diff}% higher chance | |
</div> | |
</div> | |
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px; font-size: 13px; text-align: center;"> | |
<strong>📏 Distance:</strong> {self._calculate_distance(city1['lat'], city1['lon'], city2['lat'], city2['lon']):.0f} miles | |
</div> | |
</div> | |
""" | |
return html | |
def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float: | |
"""Calculate distance between two points using Haversine formula""" | |
R = 3959 # Earth's radius in miles | |
lat1_rad = math.radians(lat1) | |
lat2_rad = math.radians(lat2) | |
delta_lat = math.radians(lat2 - lat1) | |
delta_lon = math.radians(lon2 - lon1) | |
a = (math.sin(delta_lat / 2) ** 2 + | |
math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon / 2) ** 2) | |
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) | |
return R * c | |
def _create_error_map(self, error_message: str) -> str: | |
"""Create error map when main map creation fails""" | |
try: | |
m = folium.Map( | |
location=self.default_center, | |
zoom_start=self.default_zoom, | |
tiles='CartoDB dark_matter' | |
) | |
folium.Marker( | |
location=self.default_center, | |
popup=f"Error: {error_message}", | |
tooltip="Map Error", | |
icon=folium.Icon(color='red', icon='exclamation-triangle', prefix='fa') | |
).add_to(m) | |
return m._repr_html_() | |
except: | |
return f""" | |
<div style="width: 100%; height: 400px; background: #2c3e50; color: white; | |
display: flex; align-items: center; justify-content: center; | |
font-family: Arial, sans-serif; border-radius: 10px;"> | |
<div style="text-align: center;"> | |
<h3>🗺️ Map Error</h3> | |
<p>Unable to load map: {error_message}</p> | |
</div> | |
</div> | |
""" | |
def _add_wind_arrow(self, map_obj: folium.Map, lat: float, lon: float, | |
wind_speed: str, wind_direction: str) -> None: | |
"""Add wind flow arrow to the map""" | |
try: | |
# Parse wind speed and direction | |
if not wind_speed or not wind_direction or wind_speed == 'N/A': | |
return | |
# Extract numeric wind speed | |
speed_match = re.search(r'(\d+)', str(wind_speed)) | |
if not speed_match: | |
return | |
speed = int(speed_match.group(1)) | |
# Convert wind direction to degrees | |
direction_map = { | |
'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5, | |
'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5, | |
'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5, | |
'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5 | |
} | |
direction_deg = direction_map.get(wind_direction.upper(), 0) | |
# Calculate wind arrow properties | |
arrow_length = min(max(speed * 0.001, 0.01), 0.05) # Scale arrow length | |
arrow_color = self._get_wind_arrow_color(speed) | |
# Calculate arrow endpoint | |
import math | |
# Convert to radians and adjust for map orientation (wind direction is "from") | |
rad = math.radians(direction_deg + 180) # +180 because wind direction is "from" | |
end_lat = lat + arrow_length * math.cos(rad) | |
end_lon = lon + arrow_length * math.sin(rad) | |
# Create wind arrow as a polyline with arrowhead | |
folium.PolyLine( | |
locations=[(lat, lon), (end_lat, end_lon)], | |
color=arrow_color, | |
weight=4, | |
opacity=0.8, | |
popup=f"Wind: {wind_speed} from {wind_direction}", | |
tooltip=f"💨 {wind_speed} {wind_direction}" | |
).add_to(map_obj) | |
# Add arrowhead marker | |
folium.Marker( | |
location=[end_lat, end_lon], | |
icon=folium.Icon( | |
icon='arrow-up', | |
prefix='fa', | |
color=arrow_color, | |
icon_size=(10, 10) | |
), | |
popup=f"Wind: {wind_speed} from {wind_direction}" | |
).add_to(map_obj) | |
except Exception as e: | |
logger.warning(f"Could not add wind arrow: {e}") | |
def _get_wind_arrow_color(self, speed: int) -> str: | |
"""Get color for wind arrow based on speed""" | |
if speed >= 30: | |
return 'red' # Strong winds | |
elif speed >= 20: | |
return 'orange' # Moderate winds | |
elif speed >= 10: | |
return 'yellow' # Light winds | |
else: | |
return 'green' # Calm winds | |
def _create_enhanced_popup(self, city: Dict, mcp_data: Dict = None) -> str: | |
"""Create compact horizontal-layout popup for better map integration""" | |
try: | |
city_name = city.get('name', 'Unknown City').title() | |
forecast = city.get('forecast', []) | |
if not forecast: | |
return f""" | |
<div style="width: 320px; font-family: Arial, sans-serif;"> | |
<h3 style="color: #2c3e50; margin-bottom: 10px; text-align: center;">📍 {city_name}</h3> | |
<p style="text-align: center; color: #777;">No weather data available</p> | |
</div> | |
""" | |
current = forecast[0] | |
next_period = forecast[1] if len(forecast) > 1 else {} | |
# Extract essential weather data | |
temperature = current.get('temperature', 'N/A') | |
temp_unit = current.get('temperatureUnit', 'F') | |
feels_like = current.get('apparentTemperature', {}) | |
feels_like_val = feels_like.get('value', 'N/A') if isinstance(feels_like, dict) else 'N/A' | |
conditions = current.get('shortForecast', 'N/A') | |
# Compact weather details | |
wind_speed = current.get('windSpeed', 'N/A') | |
wind_direction = current.get('windDirection', '') | |
precip_prob = current.get('precipitationProbability', 0) or 0 | |
humidity = current.get('relativeHumidity', {}) | |
humidity_val = humidity.get('value', 'N/A') if isinstance(humidity, dict) else 'N/A' | |
# Get weather emoji | |
weather_emoji = self._get_weather_emoji(conditions) | |
# Create compact horizontal popup content | |
popup_content = f""" | |
<div style="width: 320px; font-family: Arial, sans-serif; line-height: 1.2;"> | |
<!-- Header with gradient background --> | |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px; border-radius: 8px 8px 0 0; text-align: center; margin-bottom: 8px;"> | |
<h3 style="margin: 0; font-size: 16px; font-weight: bold;"> | |
{weather_emoji} {city_name} | |
</h3> | |
<p style="margin: 2px 0 0 0; font-size: 11px; opacity: 0.9;"> | |
📍 {city.get('lat', 'N/A'):.3f}°, {city.get('lon', 'N/A'):.3f}° | |
</p> | |
</div> | |
<!-- Main temperature and conditions in horizontal layout --> | |
<div style="display: flex; gap: 8px; margin-bottom: 8px;"> | |
<div style="flex: 1; background: #f8f9fa; padding: 8px; border-radius: 6px; border-left: 4px solid #e74c3c;"> | |
<div style="font-size: 20px; font-weight: bold; color: #e74c3c; margin-bottom: 2px;"> | |
{temperature}°{temp_unit} | |
</div> | |
<div style="font-size: 10px; color: #666;"> | |
Feels {feels_like_val}°{temp_unit} | |
</div> | |
</div> | |
<div style="flex: 2; background: #e8f4f8; padding: 8px; border-radius: 6px; border-left: 4px solid #2980b9;"> | |
<div style="font-size: 12px; font-weight: bold; color: #2980b9; margin-bottom: 2px;"> | |
{conditions} | |
</div> | |
<div style="font-size: 10px; color: #666;"> | |
💧 {humidity_val}% • 🌧️ {precip_prob}% | |
</div> | |
</div> | |
</div> | |
<!-- Wind and additional data in compact grid --> | |
<div style="display: flex; gap: 6px; margin-bottom: 8px;"> | |
<div style="flex: 1; background: #fff3cd; padding: 6px; border-radius: 4px; text-align: center;"> | |
<div style="font-size: 11px; font-weight: bold; color: #856404;">💨 Wind</div> | |
<div style="font-size: 10px; color: #666;">{wind_speed} {wind_direction}</div> | |
</div> | |
""" | |
# Add next period in compact format | |
if next_period: | |
next_temp = next_period.get('temperature', 'N/A') | |
next_conditions = next_period.get('shortForecast', 'N/A')[:15] + ('...' if len(next_period.get('shortForecast', '')) > 15 else '') | |
next_name = next_period.get('name', 'Next')[:8] # Truncate long period names | |
popup_content += f""" | |
<div style="flex: 2; background: #e6f3ff; padding: 6px; border-radius: 4px; text-align: center;"> | |
<div style="font-size: 11px; font-weight: bold; color: #0056b3;">📅 {next_name}</div> | |
<div style="font-size: 10px; color: #666;">{next_temp}°F • {next_conditions}</div> | |
</div> | |
</div> | |
""" | |
else: | |
popup_content += "</div>" | |
# Add compact 3-day outlook if available | |
if len(forecast) > 2: | |
popup_content += f""" | |
<div style="background: #f0f8ff; padding: 6px; border-radius: 6px; margin-bottom: 8px;"> | |
<div style="font-size: 11px; font-weight: bold; color: #4169e1; margin-bottom: 4px; text-align: center;"> | |
📊 3-Day Outlook | |
</div> | |
<div style="display: flex; gap: 4px;"> | |
""" | |
for i, period in enumerate(forecast[2:5]): # Next 3 periods only | |
period_name = period.get('name', f'D{i+3}')[:3] # Very short names | |
period_temp = period.get('temperature', 'N/A') | |
period_emoji = self._get_weather_emoji(period.get('shortForecast', '')) | |
popup_content += f""" | |
<div style="flex: 1; text-align: center; background: rgba(255,255,255,0.7); padding: 4px; border-radius: 3px;"> | |
<div style="font-size: 9px; font-weight: bold;">{period_name}</div> | |
<div style="font-size: 12px;">{period_emoji}</div> | |
<div style="font-size: 9px;">{period_temp}°F</div> | |
</div> | |
""" | |
popup_content += "</div></div>" | |
# Add compact MCP data if available | |
if mcp_data: | |
mcp_section = self._create_compact_mcp_section(city_name, mcp_data) | |
if mcp_section: | |
popup_content += mcp_section | |
# Compact footer | |
popup_content += f""" | |
<div style="background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px; border-radius: 0 0 8px 8px; text-align: center;"> | |
<div style="font-size: 9px; opacity: 0.9;"> | |
🤖 Enhanced Weather Intelligence | |
</div> | |
</div> | |
</div> | |
""" | |
return popup_content | |
except Exception as e: | |
logger.error(f"Error creating enhanced popup: {e}") | |
return f""" | |
<div style="width: 300px;"> | |
<h3 style="color: #d32f2f;">❌ Error Loading Weather Data</h3> | |
<p>Unable to load weather data for {city.get('name', 'Unknown City')}</p> | |
<p style="font-size: 12px; color: #666;">Error: {str(e)}</p> | |
</div> | |
""" | |
def _create_compact_mcp_section(self, city_name: str, mcp_data: Dict) -> str: | |
"""Create compact MCP section for horizontal layout popup""" | |
if not mcp_data: | |
return "" | |
# Find matching city data (case insensitive) | |
city_mcp = None | |
for key, value in mcp_data.items(): | |
if key.lower() == city_name.lower(): | |
city_mcp = value | |
break | |
if not city_mcp: | |
return "" | |
mcp_html = """ | |
<div style="background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); color: white; padding: 6px; border-radius: 6px; margin-bottom: 6px;"> | |
<div style="font-size: 11px; font-weight: bold; margin-bottom: 4px; text-align: center;"> | |
🔬 Enhanced Intelligence | |
</div> | |
<div style="display: flex; flex-wrap: wrap; gap: 4px;"> | |
""" | |
# Compact data items in horizontal pills | |
compact_items = [] | |
# Air Quality Data | |
if 'air_quality' in city_mcp or any('air' in str(k).lower() for k in city_mcp.keys()): | |
air_quality = str(city_mcp.get('air_quality', 'Good'))[:10] | |
compact_items.append(f'<span style="background: rgba(255,255,255,0.3); padding: 2px 6px; border-radius: 10px; font-size: 9px; white-space: nowrap;">🌬️ {air_quality}</span>') | |
# Historical Data | |
if 'historical_avg' in city_mcp or any('historical' in str(k).lower() for k in city_mcp.keys()): | |
historical = str(city_mcp.get('historical_avg', 'N/A'))[:8] | |
compact_items.append(f'<span style="background: rgba(255,255,255,0.3); padding: 2px 6px; border-radius: 10px; font-size: 9px; white-space: nowrap;">📊 {historical}</span>') | |
# UV Index | |
if 'uv_index' in city_mcp or any('uv' in str(k).lower() for k in city_mcp.keys()): | |
uv = str(city_mcp.get('uv_index', 'Mod'))[:8] | |
compact_items.append(f'<span style="background: rgba(255,255,255,0.3); padding: 2px 6px; border-radius: 10px; font-size: 9px; white-space: nowrap;">☀️ {uv}</span>') | |
# Marine/Travel (prioritize most relevant) | |
if 'marine_conditions' in city_mcp: | |
marine = str(city_mcp.get('marine_conditions', ''))[:8] | |
compact_items.append(f'<span style="background: rgba(255,255,255,0.3); padding: 2px 6px; border-radius: 10px; font-size: 9px; white-space: nowrap;">🌊 {marine}</span>') | |
elif 'travel_advice' in city_mcp: | |
travel = str(city_mcp.get('travel_advice', ''))[:8] | |
compact_items.append(f'<span style="background: rgba(255,255,255,0.3); padding: 2px 6px; border-radius: 10px; font-size: 9px; white-space: nowrap;">✈️ {travel}</span>') | |
# Severe Weather Alerts (high priority) | |
if any('severe' in str(k).lower() or 'alert' in str(k).lower() for k in city_mcp.keys()): | |
compact_items.append(f'<span style="background: rgba(255,100,100,0.6); padding: 2px 6px; border-radius: 10px; font-size: 9px; white-space: nowrap;">⚠️ Alert</span>') | |
# Add up to 4 most important items | |
for item in compact_items[:4]: | |
mcp_html += item | |
mcp_html += """ | |
</div> | |
</div> | |
""" | |
return mcp_html | |
def create_map_manager() -> WeatherMapManager: | |
"""Factory function to create map manager""" | |
return WeatherMapManager() | |