Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions sam-geo-information/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"requests==2.32.5",
"timezonefinder==8.1.0",
"pytz==2025.2",
"PyYAML==6.0.2"
]

Expand Down
149 changes: 133 additions & 16 deletions sam-geo-information/src/sam_geo_information/city_to_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,132 @@

import logging
from typing import Dict, Any, Optional
from timezonefinder import TimezoneFinder
import pytz
from datetime import datetime
from zoneinfo import ZoneInfo, available_timezones
import yaml
from google.adk.tools import ToolContext

from .services import MapsCoGeocodingService

log = logging.getLogger(__name__)
def _find_timezone_from_coordinates(lat: float, lng: float) -> Optional[str]:
"""
Find timezone from coordinates using a simple mapping approach.

This is a simplified implementation that maps coordinates to timezones
based on longitude ranges. For more accurate results, consider using
an external service or a comprehensive timezone boundary database.

Args:
lat: Latitude
lng: Longitude

Returns:
Timezone string (e.g., 'America/New_York') or None if not found
"""
# Get all available timezones
timezones = available_timezones()

# Simple heuristic: use longitude to estimate timezone
# This is approximate and works best for major cities
# Longitude ranges roughly correspond to UTC offsets (15 degrees per hour)

# Common timezone mappings based on regions and estimated offset
# This is a simplified approach - for production use, consider a more comprehensive solution
timezone_candidates = []

# North America
if 25 <= lat <= 50 and -130 <= lng <= -60:
if lng < -120:
timezone_candidates.extend(['America/Los_Angeles', 'America/Vancouver'])
elif lng < -105:
timezone_candidates.extend(['America/Denver', 'America/Phoenix'])
elif lng < -90:
timezone_candidates.extend(['America/Chicago', 'America/Mexico_City'])
else:
timezone_candidates.extend(['America/New_York', 'America/Toronto'])

# Europe
elif 35 <= lat <= 70 and -10 <= lng <= 40:
if lng < 5:
timezone_candidates.extend(['Europe/London', 'Europe/Dublin'])
elif lng < 15:
timezone_candidates.extend(['Europe/Paris', 'Europe/Berlin', 'Europe/Rome'])
else:
timezone_candidates.extend(['Europe/Athens', 'Europe/Helsinki', 'Europe/Moscow'])

# Asia
elif -10 <= lat <= 50 and 60 <= lng <= 150:
if lng < 80:
timezone_candidates.extend(['Asia/Kolkata', 'Asia/Dubai'])
elif lng < 110:
timezone_candidates.extend(['Asia/Bangkok', 'Asia/Singapore'])
elif lng < 130:
timezone_candidates.extend(['Asia/Shanghai', 'Asia/Hong_Kong'])
else:
timezone_candidates.extend(['Asia/Tokyo', 'Asia/Seoul'])

# Australia
elif -45 <= lat <= -10 and 110 <= lng <= 155:
if lng < 130:
timezone_candidates.extend(['Australia/Perth'])
elif lng < 145:
timezone_candidates.extend(['Australia/Adelaide', 'Australia/Darwin'])
else:
timezone_candidates.extend(['Australia/Sydney', 'Australia/Melbourne'])

# South America
elif -55 <= lat <= 15 and -80 <= lng <= -35:
if lng < -70:
timezone_candidates.extend(['America/Lima', 'America/Bogota'])
elif lng < -55:
timezone_candidates.extend(['America/Santiago', 'America/La_Paz'])
else:
timezone_candidates.extend(['America/Sao_Paulo', 'America/Buenos_Aires'])

# Africa
elif -35 <= lat <= 35 and -20 <= lng <= 50:
if lng < 10:
timezone_candidates.extend(['Africa/Lagos', 'Africa/Accra'])
elif lng < 30:
timezone_candidates.extend(['Africa/Cairo', 'Africa/Johannesburg'])
else:
timezone_candidates.extend(['Africa/Nairobi', 'Africa/Addis_Ababa'])

# Return first valid candidate that exists in available timezones
for tz in timezone_candidates:
if tz in timezones:
return tz

# Fallback: if no specific timezone found, return a reasonable default based on longitude
if -180 <= lng < -120:
return 'America/Los_Angeles'
elif -120 <= lng < -90:
return 'America/Denver'
elif -90 <= lng < -60:
return 'America/Chicago'
elif -60 <= lng < -30:
return 'America/New_York'
elif -30 <= lng < 0:
return 'Atlantic/Azores'
elif 0 <= lng < 30:
return 'Europe/London'
elif 30 <= lng < 60:
return 'Europe/Moscow'
elif 60 <= lng < 90:
return 'Asia/Kolkata'
elif 90 <= lng < 120:
return 'Asia/Shanghai'
elif 120 <= lng < 150:
return 'Asia/Tokyo'
elif 150 <= lng <= 180:
return 'Pacific/Auckland'

return None


async def city_to_timezone(
city: str,
tool_context: Optional[ToolContext] = None,
tool_config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Expand All @@ -32,31 +146,34 @@ async def city_to_timezone(
try:
geocoding_api_key = tool_config.get("geocoding_api_key")
geocoding_service = MapsCoGeocodingService(api_key=geocoding_api_key)
timezone_finder = TimezoneFinder()

locations = await geocoding_service.geocode(city)
if not locations:
raise ValueError(f"No locations found for city: {city}")

results = []
for loc in locations:
timezone_str = timezone_finder.timezone_at(
timezone_str = _find_timezone_from_coordinates(
lat=loc.latitude, lng=loc.longitude
)
if not timezone_str:
continue

timezone = pytz.timezone(timezone_str)
now = pytz.datetime.datetime.now(timezone)

result = {
"location": loc.display_name,
"timezone": timezone_str,
"utc_offset": now.strftime("%z"),
"dst_active": bool(now.dst()),
"current_time": now.strftime("%Y-%m-%d %H:%M:%S %Z"),
}
results.append(result)
try:
timezone = ZoneInfo(timezone_str)
now = datetime.now(timezone)

result = {
"location": loc.display_name,
"timezone": timezone_str,
"utc_offset": now.strftime("%z"),
"dst_active": bool(now.dst()),
"current_time": now.strftime("%Y-%m-%d %H:%M:%S %Z"),
}
results.append(result)
except Exception as tz_error:
log.warning("%s Could not process timezone %s: %s", log_identifier, timezone_str, tz_error)
continue

if not results:
raise ValueError(
Expand Down