mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-28 13:24:09 +00:00
Refactor geocoding methods in commands to utilize shared utility functions for improved consistency and efficiency. Introduce geocode_zipcode_sync and geocode_city_sync for streamlined location retrieval, enhancing error handling and caching mechanisms. Update AQI, solar forecast, and weather commands to leverage these new methods, reducing code duplication and improving maintainability.
This commit is contained in:
+106
-157
@@ -10,7 +10,7 @@ import requests_cache
|
||||
from retry_requests import retry
|
||||
from datetime import datetime
|
||||
from geopy.geocoders import Nominatim
|
||||
from ..utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder
|
||||
from ..utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder, abbreviate_location, geocode_zipcode_sync, geocode_city_sync
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
@@ -352,25 +352,10 @@ class AqiCommand(BaseCommand):
|
||||
except ValueError:
|
||||
return f"Invalid coordinates format: {location}. Use format: lat,lon (e.g., 47.6,-122.3)"
|
||||
elif location_type == "zipcode":
|
||||
# Handle ZIP code geocoding using structured Nominatim queries
|
||||
# Handle ZIP code geocoding with AQI-specific structured queries
|
||||
try:
|
||||
# Use the original ZIP code directly
|
||||
zip_code = location.strip()
|
||||
|
||||
# Use structured query approach for better ZIP code handling
|
||||
# Try different structured approaches based on Nominatim documentation
|
||||
structured_queries = [
|
||||
# Direct postalcode search
|
||||
{"postalcode": zip_code, "country": "US"},
|
||||
# Postalcode with state
|
||||
{"postalcode": zip_code, "state": self.default_state, "country": "US"},
|
||||
# Postalcode with country code
|
||||
{"postalcode": zip_code, "countrycode": "US"},
|
||||
# Fallback to text-based search
|
||||
f"{zip_code} USA",
|
||||
f"{zip_code} United States"
|
||||
]
|
||||
|
||||
# Check for known problematic ZIP codes that need specific mapping
|
||||
zip_code_mappings = {
|
||||
'98013': 'Vashon, WA, USA',
|
||||
@@ -392,17 +377,20 @@ class AqiCommand(BaseCommand):
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Mapping failed for ZIP {zip_code}: {e}")
|
||||
|
||||
# If no mapping or mapping failed, try structured queries
|
||||
# If no mapping, try structured queries (AQI-specific feature for better ZIP handling)
|
||||
if not location_result:
|
||||
structured_queries = [
|
||||
# Direct postalcode search
|
||||
{"postalcode": zip_code, "country": "US"},
|
||||
# Postalcode with state
|
||||
{"postalcode": zip_code, "state": self.default_state, "country": "US"},
|
||||
# Postalcode with country code
|
||||
{"postalcode": zip_code, "countrycode": "US"},
|
||||
]
|
||||
|
||||
for query in structured_queries:
|
||||
try:
|
||||
if isinstance(query, dict):
|
||||
# Use structured query
|
||||
result = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
|
||||
else:
|
||||
# Use text-based query
|
||||
result = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
|
||||
|
||||
result = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
|
||||
if result and result.address:
|
||||
# Check if it's a US location
|
||||
if 'united states' in result.address.lower() or 'usa' in result.address.lower():
|
||||
@@ -420,22 +408,38 @@ class AqiCommand(BaseCommand):
|
||||
self.logger.debug(f"Structured query failed for {query}: {e}")
|
||||
continue
|
||||
|
||||
if location_result:
|
||||
# If structured queries didn't work, use shared function as fallback
|
||||
if not location_result:
|
||||
lat, lon = geocode_zipcode_sync(self.bot, zip_code, timeout=10)
|
||||
if lat and lon:
|
||||
# Use the shared function result directly
|
||||
pass
|
||||
else:
|
||||
lat, lon = None, None
|
||||
else:
|
||||
lat = location_result.latitude
|
||||
lon = location_result.longitude
|
||||
|
||||
if lat and lon:
|
||||
|
||||
# Get detailed address info via reverse geocoding
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{lat}, {lon}", timeout=10)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
else:
|
||||
# Get detailed address info via reverse geocoding (check cache first)
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = self.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{lat}, {lon}", timeout=10)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
self.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
else:
|
||||
address_info = {}
|
||||
except:
|
||||
address_info = {}
|
||||
except:
|
||||
address_info = {}
|
||||
|
||||
# Validate that the found location makes sense for the ZIP code
|
||||
# Check if the found location is in the expected state or region
|
||||
if address_info:
|
||||
found_state = address_info.get('state', '')
|
||||
found_country = address_info.get('country', '')
|
||||
@@ -486,7 +490,7 @@ class AqiCommand(BaseCommand):
|
||||
state = address_info.get('state', '')
|
||||
|
||||
# For US cities, use the state; for international cities, use the country
|
||||
if country == "United States" or country == "US":
|
||||
if country == "United States" or country == "US" or country == "United States of America":
|
||||
# US city - use the state
|
||||
actual_state = state or self.default_state
|
||||
# Convert full state name to abbreviation if needed
|
||||
@@ -497,9 +501,9 @@ class AqiCommand(BaseCommand):
|
||||
actual_state = (country or
|
||||
address_info.get('province') or
|
||||
self.default_state)
|
||||
# Abbreviate "United States" to "US" to save characters
|
||||
if actual_state == "United States":
|
||||
actual_state = "US"
|
||||
# Normalize "United States" variants to "USA" to save characters
|
||||
if actual_state == "United States" or actual_state == "United States of America":
|
||||
actual_state = "USA"
|
||||
|
||||
# Also check if the default state needs to be converted for comparison
|
||||
default_state_full = self.default_state
|
||||
@@ -518,7 +522,9 @@ class AqiCommand(BaseCommand):
|
||||
location_prefix = ""
|
||||
if location_type == "city" and address_info:
|
||||
# Always try to include city name if there's space
|
||||
city_display = f"{actual_city}, {actual_state}"
|
||||
# Use abbreviate_location to shorten long location strings (e.g., "United States of America" -> "USA")
|
||||
full_location = f"{actual_city}, {actual_state}"
|
||||
city_display = abbreviate_location(full_location, max_length=30)
|
||||
|
||||
# Check if we have space for the city name
|
||||
test_output = f"{city_display}: {aqi_data}"
|
||||
@@ -529,7 +535,9 @@ class AqiCommand(BaseCommand):
|
||||
states_different = (actual_state != self.default_state and
|
||||
actual_state != default_state_full)
|
||||
if states_different:
|
||||
location_prefix = f"{actual_city}, {actual_state}: "
|
||||
# Use abbreviated version for shorter display
|
||||
city_display_short = abbreviate_location(full_location, max_length=20)
|
||||
location_prefix = f"{city_display_short}: "
|
||||
elif location_type == "zipcode":
|
||||
# Add location info for ZIP codes to confirm geocoding accuracy
|
||||
if address_info:
|
||||
@@ -559,7 +567,7 @@ class AqiCommand(BaseCommand):
|
||||
country = address_info.get('country', '')
|
||||
state = address_info.get('state', '')
|
||||
|
||||
if country == "United States" or country == "US":
|
||||
if country == "United States" or country == "US" or country == "United States of America":
|
||||
# US city - use the state
|
||||
actual_state = state or self.default_state
|
||||
# Convert full state name to abbreviation if needed
|
||||
@@ -570,11 +578,13 @@ class AqiCommand(BaseCommand):
|
||||
actual_state = (country or
|
||||
address_info.get('province') or
|
||||
self.default_state)
|
||||
# Abbreviate "United States" to "US" to save characters
|
||||
if actual_state == "United States":
|
||||
actual_state = "US"
|
||||
# Normalize "United States" variants to "USA" to save characters
|
||||
if actual_state == "United States" or actual_state == "United States of America":
|
||||
actual_state = "USA"
|
||||
|
||||
city_display = f"{actual_city}, {actual_state}"
|
||||
# Use abbreviate_location to shorten long location strings (e.g., "United States of America" -> "USA")
|
||||
full_location = f"{actual_city}, {actual_state}"
|
||||
city_display = abbreviate_location(full_location, max_length=30)
|
||||
|
||||
# Check if we have space for the city name
|
||||
test_output = f"{city_display}: {aqi_data}"
|
||||
@@ -585,7 +595,9 @@ class AqiCommand(BaseCommand):
|
||||
states_different = (actual_state != self.default_state and
|
||||
actual_state != default_state_full)
|
||||
if states_different:
|
||||
location_prefix = f"{actual_city}, {actual_state}: "
|
||||
# Use abbreviated version for shorter display
|
||||
city_display_short = abbreviate_location(full_location, max_length=20)
|
||||
location_prefix = f"{city_display_short}: "
|
||||
else:
|
||||
# No address info available
|
||||
location_prefix = f"{zip_code}: "
|
||||
@@ -602,20 +614,6 @@ class AqiCommand(BaseCommand):
|
||||
def city_to_lat_lon(self, city: str) -> tuple:
|
||||
"""Convert city name to latitude and longitude using default state"""
|
||||
try:
|
||||
# Check cache first for default state query
|
||||
cache_query = f"{city}, {self.default_state}, USA"
|
||||
cached_lat, cached_lon = self.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat is not None and cached_lon is not None:
|
||||
self.logger.debug(f"Using cached geocoding for {city}")
|
||||
# Still need to do reverse geocoding for address details
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{cached_lat}, {cached_lon}", timeout=10)
|
||||
if reverse_location:
|
||||
return cached_lat, cached_lon, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return cached_lat, cached_lon, {}
|
||||
|
||||
# Check if the input contains a comma (city, state/country format)
|
||||
if ',' in city:
|
||||
# Parse city, state/country format
|
||||
@@ -624,121 +622,72 @@ class AqiCommand(BaseCommand):
|
||||
city_name = city_parts[0]
|
||||
state_or_country = city_parts[1]
|
||||
|
||||
# Check if it's a country (not a US state)
|
||||
# AQI-specific: Check if it's a country (not a US state)
|
||||
country_indicators = ['canada', 'mexico', 'uk', 'united kingdom', 'france', 'germany', 'italy', 'spain', 'australia', 'japan', 'china', 'india', 'brazil', 'uae', 'russia', 'korea', 'thailand', 'singapore', 'egypt', 'turkey', 'israel', 'south africa', 'kenya', 'nigeria', 'argentina', 'peru', 'chile', 'colombia', 'venezuela', 'cuba', 'jamaica', 'puerto rico', 'iceland', 'norway', 'sweden', 'denmark', 'finland', 'poland', 'czech republic', 'hungary', 'romania', 'bulgaria', 'croatia', 'serbia', 'greece', 'portugal', 'ireland', 'belgium', 'netherlands', 'switzerland', 'austria', 'monaco', 'andorra', 'san marino', 'vatican', 'luxembourg', 'malta', 'cyprus', 'albania', 'macedonia', 'montenegro', 'bosnia', 'slovenia', 'slovakia', 'lithuania', 'latvia', 'estonia', 'belarus', 'ukraine', 'moldova', 'georgia', 'armenia', 'azerbaijan', 'kazakhstan', 'uzbekistan', 'kyrgyzstan', 'tajikistan', 'turkmenistan', 'afghanistan', 'pakistan', 'bangladesh', 'sri lanka', 'nepal', 'bhutan', 'myanmar', 'laos', 'cambodia', 'vietnam', 'malaysia', 'indonesia', 'philippines', 'taiwan', 'north korea', 'mongolia']
|
||||
is_country = state_or_country.lower() in country_indicators
|
||||
|
||||
if is_country:
|
||||
# Handle international cities
|
||||
# Handle international cities explicitly (AQI-specific feature)
|
||||
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city_name}, {state_or_country}", timeout=10)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city_name}, {state_or_country}", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
# Use reverse geocoding to get detailed address info (check cache first)
|
||||
reverse_cache_key = f"reverse_{location.latitude}_{location.longitude}"
|
||||
cached_address = self.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
return location.latitude, location.longitude, cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
self.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
return location.latitude, location.longitude, address_info
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
else:
|
||||
# Handle US city, state format
|
||||
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city_name}, {state_or_country}, USA", timeout=10)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city_name}, {state_or_country}, USA", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
|
||||
# For common city names, try major cities first to avoid small towns
|
||||
major_city_mappings = {
|
||||
'albany': ['Albany, NY, USA', 'Albany, OR, USA', 'Albany, CA, USA'],
|
||||
'portland': ['Portland, OR, USA', 'Portland, ME, USA'],
|
||||
'boston': ['Boston, MA, USA'],
|
||||
'paris': ['Paris, TX, USA', 'Paris, IL, USA', 'Paris, TN, USA'],
|
||||
'springfield': ['Springfield, IL, USA', 'Springfield, MO, USA', 'Springfield, MA, USA'],
|
||||
'franklin': ['Franklin, TN, USA', 'Franklin, MA, USA'],
|
||||
'georgetown': ['Georgetown, TX, USA', 'Georgetown, SC, USA'],
|
||||
'madison': ['Madison, WI, USA', 'Madison, AL, USA'],
|
||||
'auburn': ['Auburn, AL, USA', 'Auburn, WA, USA'],
|
||||
'troy': ['Troy, NY, USA', 'Troy, MI, USA'],
|
||||
'clinton': ['Clinton, IA, USA', 'Clinton, MS, USA']
|
||||
}
|
||||
# Use shared geocode_city_sync function with address info
|
||||
default_country = self.bot.config.get('Weather', 'default_country', fallback='US')
|
||||
lat, lon, address_info = geocode_city_sync(
|
||||
self.bot, city, default_state=self.default_state,
|
||||
default_country=default_country,
|
||||
include_address_info=True, timeout=10
|
||||
)
|
||||
|
||||
# If it's a major city with multiple locations, try the major ones first
|
||||
if city.lower() in major_city_mappings:
|
||||
for major_city_query in major_city_mappings[city.lower()]:
|
||||
location = rate_limited_nominatim_geocode_sync(self.bot, major_city_query, timeout=10)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
if lat and lon:
|
||||
return lat, lon, address_info or {}
|
||||
|
||||
# First try with default state
|
||||
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city}, {self.default_state}, USA", timeout=10)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city}, {self.default_state}, USA", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
else:
|
||||
# Try neighborhood-specific queries for major cities
|
||||
neighborhood_queries = self.get_neighborhood_queries(city)
|
||||
# AQI-specific fallback: Try neighborhood-specific queries for major cities
|
||||
neighborhood_queries = self.get_neighborhood_queries(city)
|
||||
if neighborhood_queries:
|
||||
for query in neighborhood_queries:
|
||||
location = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(query, location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
|
||||
# Try without state as final fallback
|
||||
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city}, USA", timeout=10)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city}, USA", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
else:
|
||||
return (None, None, None)
|
||||
# Use reverse geocoding to get detailed address info (check cache first)
|
||||
reverse_cache_key = f"reverse_{location.latitude}_{location.longitude}"
|
||||
cached_address = self.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
return location.latitude, location.longitude, cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
self.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
return location.latitude, location.longitude, address_info
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
|
||||
return (None, None, None)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error geocoding city {city}: {e}")
|
||||
return (None, None, None)
|
||||
|
||||
@@ -10,12 +10,12 @@ import time
|
||||
import hashlib
|
||||
import pytz
|
||||
from geopy.geocoders import Nominatim
|
||||
from ..utils import rate_limited_nominatim_geocode, rate_limited_nominatim_reverse, get_nominatim_geocoder
|
||||
from ..utils import rate_limited_nominatim_reverse, get_nominatim_geocoder, geocode_zipcode, geocode_city
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Tuple, Dict
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
from ..utils import abbreviate_location, get_major_city_queries
|
||||
from ..utils import abbreviate_location
|
||||
|
||||
|
||||
class SolarforecastCommand(BaseCommand):
|
||||
@@ -345,103 +345,30 @@ class SolarforecastCommand(BaseCommand):
|
||||
return None, None
|
||||
|
||||
async def _zipcode_to_lat_lon(self, zipcode: str) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""Convert zipcode to lat/lon"""
|
||||
import asyncio
|
||||
"""Convert zipcode to lat/lon using shared geocoding function"""
|
||||
try:
|
||||
cache_query = f"{zipcode}, USA"
|
||||
cached_lat, cached_lon = self.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat and cached_lon:
|
||||
return cached_lat, cached_lon
|
||||
|
||||
location = await rate_limited_nominatim_geocode(
|
||||
self.bot, f"{zipcode}, USA", timeout=self.url_timeout
|
||||
)
|
||||
if location:
|
||||
self.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
lat, lon = await geocode_zipcode(self.bot, zipcode, timeout=self.url_timeout)
|
||||
return lat, lon
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error geocoding zipcode {zipcode}: {e}")
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
async def _city_to_lat_lon(self, city: str) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""Convert city name to lat/lon"""
|
||||
import asyncio
|
||||
"""Convert city name to lat/lon using shared geocoding function"""
|
||||
try:
|
||||
# Run geocoding in executor to avoid blocking
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Parse state abbreviation if present (e.g., "new york, ny" -> "new york" and "NY")
|
||||
city_clean = city.strip()
|
||||
state_abbr = None
|
||||
if ',' in city_clean:
|
||||
parts = [p.strip() for p in city_clean.rsplit(',', 1)]
|
||||
if len(parts) == 2 and len(parts[1]) <= 2:
|
||||
# Likely a state abbreviation
|
||||
city_clean = parts[0]
|
||||
state_abbr = parts[1].upper()
|
||||
|
||||
# Handle major cities with multiple locations (prioritize major cities)
|
||||
# Use shared utility function for consistency across commands
|
||||
major_city_queries = get_major_city_queries(city_clean, state_abbr)
|
||||
if major_city_queries:
|
||||
# Try major city options first
|
||||
for major_city_query in major_city_queries:
|
||||
cached_lat, cached_lon = self.db_manager.get_cached_geocoding(major_city_query)
|
||||
if cached_lat and cached_lon:
|
||||
return cached_lat, cached_lon
|
||||
|
||||
location = await rate_limited_nominatim_geocode(
|
||||
self.bot, major_city_query, timeout=self.url_timeout
|
||||
)
|
||||
if location:
|
||||
self.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
|
||||
# If state abbreviation was parsed, use it
|
||||
if state_abbr:
|
||||
state_query = f"{city_clean}, {state_abbr}, USA"
|
||||
cached_lat, cached_lon = self.db_manager.get_cached_geocoding(state_query)
|
||||
if cached_lat and cached_lon:
|
||||
return cached_lat, cached_lon
|
||||
|
||||
location = await rate_limited_nominatim_geocode(
|
||||
self.bot, state_query, timeout=self.url_timeout
|
||||
)
|
||||
if location:
|
||||
self.db_manager.cache_geocoding(state_query, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
|
||||
# Try with default state first
|
||||
cache_query = f"{city_clean}, {self.default_state}, USA"
|
||||
cached_lat, cached_lon = self.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat and cached_lon:
|
||||
return cached_lat, cached_lon
|
||||
|
||||
location = await rate_limited_nominatim_geocode(
|
||||
self.bot, cache_query, timeout=self.url_timeout
|
||||
# Get defaults from config
|
||||
default_country = self.bot.config.get('Weather', 'default_country', fallback='US')
|
||||
lat, lon, _ = await geocode_city(
|
||||
self.bot, city,
|
||||
default_state=self.default_state,
|
||||
default_country=default_country,
|
||||
include_address_info=False, # Don't need address info, just coordinates
|
||||
timeout=self.url_timeout
|
||||
)
|
||||
if location:
|
||||
self.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
|
||||
# Try without state
|
||||
location = await rate_limited_nominatim_geocode(
|
||||
self.bot, f"{city_clean}, USA", timeout=self.url_timeout
|
||||
)
|
||||
if location:
|
||||
self.db_manager.cache_geocoding(f"{city_clean}, USA", location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
|
||||
# Try international
|
||||
location = await rate_limited_nominatim_geocode(
|
||||
self.bot, city_clean, timeout=self.url_timeout
|
||||
)
|
||||
if location:
|
||||
self.db_manager.cache_geocoding(city_clean, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
return lat, lon
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error geocoding city {city}: {e}")
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
async def _get_location_name(self, lat: float, lon: float, original_location: str,
|
||||
location_type: str) -> str:
|
||||
|
||||
+28
-128
@@ -10,7 +10,7 @@ import requests
|
||||
import xml.dom.minidom
|
||||
from datetime import datetime, timedelta
|
||||
from geopy.geocoders import Nominatim
|
||||
from ..utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder
|
||||
from ..utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder, geocode_zipcode_sync, geocode_city_sync
|
||||
import maidenhead as mh
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
@@ -320,14 +320,8 @@ class WxCommand(BaseCommand):
|
||||
def zipcode_to_lat_lon(self, zipcode: str) -> tuple:
|
||||
"""Convert zipcode to latitude and longitude"""
|
||||
try:
|
||||
# Use rate-limited Nominatim to geocode the zipcode
|
||||
location = rate_limited_nominatim_geocode_sync(
|
||||
self.bot, f"{zipcode}, USA", timeout=10
|
||||
)
|
||||
if location:
|
||||
return location.latitude, location.longitude
|
||||
else:
|
||||
return None, None
|
||||
lat, lon = geocode_zipcode_sync(self.bot, zipcode, timeout=10)
|
||||
return lat, lon
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error geocoding zipcode {zipcode}: {e}")
|
||||
return None, None
|
||||
@@ -335,124 +329,18 @@ class WxCommand(BaseCommand):
|
||||
def city_to_lat_lon(self, city: str) -> tuple:
|
||||
"""Convert city name to latitude and longitude using default state"""
|
||||
try:
|
||||
# Check cache first for default state query
|
||||
cache_query = f"{city}, {self.default_state}, USA"
|
||||
cached_lat, cached_lon = self.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat is not None and cached_lon is not None:
|
||||
self.logger.debug(f"Using cached geocoding for {city}")
|
||||
# Still need to do reverse geocoding for address details
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(
|
||||
self.bot, f"{cached_lat}, {cached_lon}", timeout=10
|
||||
)
|
||||
if reverse_location:
|
||||
return cached_lat, cached_lon, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return cached_lat, cached_lon, {}
|
||||
|
||||
# Check if the input contains a comma (city, state format)
|
||||
if ',' in city:
|
||||
# Parse city, state format
|
||||
city_parts = [part.strip() for part in city.split(',')]
|
||||
if len(city_parts) >= 2:
|
||||
city_name = city_parts[0]
|
||||
state = city_parts[1]
|
||||
|
||||
# Try the specific city, state combination first
|
||||
location = rate_limited_nominatim_geocode_sync(
|
||||
self.bot, f"{city_name}, {state}, USA", timeout=10
|
||||
)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city_name}, {state}, USA", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(
|
||||
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
|
||||
)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
|
||||
# For common city names, try major cities first to avoid small towns
|
||||
major_city_mappings = {
|
||||
'albany': ['Albany, NY, USA', 'Albany, OR, USA', 'Albany, CA, USA'],
|
||||
'portland': ['Portland, OR, USA', 'Portland, ME, USA'],
|
||||
'boston': ['Boston, MA, USA'],
|
||||
'paris': ['Paris, TX, USA', 'Paris, IL, USA', 'Paris, TN, USA'],
|
||||
'springfield': ['Springfield, IL, USA', 'Springfield, MO, USA', 'Springfield, MA, USA'],
|
||||
'franklin': ['Franklin, TN, USA', 'Franklin, MA, USA'],
|
||||
'georgetown': ['Georgetown, TX, USA', 'Georgetown, SC, USA'],
|
||||
'madison': ['Madison, WI, USA', 'Madison, AL, USA'],
|
||||
'auburn': ['Auburn, AL, USA', 'Auburn, WA, USA'],
|
||||
'troy': ['Troy, NY, USA', 'Troy, MI, USA'],
|
||||
'clinton': ['Clinton, IA, USA', 'Clinton, MS, USA']
|
||||
}
|
||||
|
||||
# If it's a major city with multiple locations, try the major ones first
|
||||
if city.lower() in major_city_mappings:
|
||||
for major_city_query in major_city_mappings[city.lower()]:
|
||||
location = rate_limited_nominatim_geocode_sync(
|
||||
self.bot, major_city_query, timeout=10
|
||||
)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(
|
||||
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
|
||||
)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
|
||||
# First try with default state
|
||||
location = rate_limited_nominatim_geocode_sync(
|
||||
self.bot, f"{city}, {self.default_state}, USA", timeout=10
|
||||
# Use shared geocode_city_sync function with address info
|
||||
default_country = self.bot.config.get('Weather', 'default_country', fallback='US')
|
||||
lat, lon, address_info = geocode_city_sync(
|
||||
self.bot, city, default_state=self.default_state,
|
||||
default_country=default_country,
|
||||
include_address_info=True, timeout=10
|
||||
)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city}, {self.default_state}, USA", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(
|
||||
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
|
||||
)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
|
||||
if lat and lon:
|
||||
return lat, lon, address_info or {}
|
||||
else:
|
||||
# Try without state as fallback
|
||||
location = rate_limited_nominatim_geocode_sync(
|
||||
self.bot, f"{city}, USA", timeout=10
|
||||
)
|
||||
if location:
|
||||
# Cache the result
|
||||
self.db_manager.cache_geocoding(f"{city}, USA", location.latitude, location.longitude)
|
||||
|
||||
# Use reverse geocoding to get detailed address info
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(
|
||||
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
|
||||
)
|
||||
if reverse_location:
|
||||
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
|
||||
except:
|
||||
pass
|
||||
return location.latitude, location.longitude, location.raw.get('address', {})
|
||||
else:
|
||||
return None, None, None
|
||||
return None, None, None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error geocoding city {city}: {e}")
|
||||
return None, None, None
|
||||
@@ -469,8 +357,12 @@ class WxCommand(BaseCommand):
|
||||
Tuple of (weather_string_or_periods, points_data)
|
||||
"""
|
||||
try:
|
||||
# Round coordinates to 4 decimal places to avoid API redirects
|
||||
lat_rounded = round(lat, 4)
|
||||
lon_rounded = round(lon, 4)
|
||||
|
||||
# Get weather data from NOAA
|
||||
weather_api = f"https://api.weather.gov/points/{lat},{lon}"
|
||||
weather_api = f"https://api.weather.gov/points/{lat_rounded},{lon_rounded}"
|
||||
|
||||
# Get the forecast URL
|
||||
weather_data = requests.get(weather_api, timeout=self.url_timeout)
|
||||
@@ -803,8 +695,12 @@ class WxCommand(BaseCommand):
|
||||
Tuple of (hourly_periods_list, points_data)
|
||||
"""
|
||||
try:
|
||||
# Round coordinates to 4 decimal places to avoid API redirects
|
||||
lat_rounded = round(lat, 4)
|
||||
lon_rounded = round(lon, 4)
|
||||
|
||||
# Get weather data from NOAA
|
||||
weather_api = f"https://api.weather.gov/points/{lat},{lon}"
|
||||
weather_api = f"https://api.weather.gov/points/{lat_rounded},{lon_rounded}"
|
||||
|
||||
# Get the forecast URL
|
||||
weather_data = requests.get(weather_api, timeout=self.url_timeout)
|
||||
@@ -1392,7 +1288,11 @@ class WxCommand(BaseCommand):
|
||||
def get_weather_alerts_noaa(self, lat: float, lon: float) -> tuple:
|
||||
"""Get weather alerts from NOAA"""
|
||||
try:
|
||||
alert_url = f"https://api.weather.gov/alerts/active.atom?point={lat},{lon}"
|
||||
# Round coordinates to 4 decimal places to avoid API redirects
|
||||
lat_rounded = round(lat, 4)
|
||||
lon_rounded = round(lon, 4)
|
||||
|
||||
alert_url = f"https://api.weather.gov/alerts/active.atom?point={lat_rounded},{lon_rounded}"
|
||||
|
||||
alert_data = requests.get(alert_url, timeout=self.url_timeout)
|
||||
if not alert_data.ok:
|
||||
|
||||
+466
-1
@@ -6,7 +6,7 @@ Shared helper functions used across multiple modules
|
||||
|
||||
import re
|
||||
import hashlib
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple, Dict
|
||||
|
||||
|
||||
def abbreviate_location(location: str, max_length: int = 20) -> str:
|
||||
@@ -28,6 +28,7 @@ def abbreviate_location(location: str, max_length: int = 20) -> str:
|
||||
|
||||
abbreviations = [
|
||||
('Central Business District', 'CBD'),
|
||||
('United States of America', 'USA'),
|
||||
('Business District', 'BD'),
|
||||
('British Columbia', 'BC'),
|
||||
('United States', 'USA'),
|
||||
@@ -84,6 +85,10 @@ def abbreviate_location(location: str, max_length: int = 20) -> str:
|
||||
('Wyoming', 'WY')
|
||||
]
|
||||
|
||||
# Sort by length (longest first) to ensure longer matches are checked before shorter ones
|
||||
# This prevents "United States" from matching before "United States of America"
|
||||
abbreviations.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
|
||||
# Apply abbreviations in order
|
||||
for full_term, abbrev in abbreviations:
|
||||
if full_term in abbreviated:
|
||||
@@ -498,3 +503,463 @@ def rate_limited_nominatim_reverse_sync(bot, coordinates: str, timeout: int = 10
|
||||
bot.nominatim_rate_limiter.record_request()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def geocode_zipcode(bot, zipcode: str, default_country: str = None, timeout: int = 10) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""
|
||||
Shared function to geocode a ZIP code to lat/lon coordinates.
|
||||
Checks cache first, then makes rate-limited API call if needed.
|
||||
|
||||
Args:
|
||||
bot: Bot instance (must have db_manager and nominatim_rate_limiter)
|
||||
zipcode: ZIP code string
|
||||
default_country: Default country code (e.g., "US"). If None, reads from bot.config
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Tuple of (latitude, longitude) or (None, None) if not found
|
||||
"""
|
||||
try:
|
||||
# Get default country from config if not provided
|
||||
if default_country is None:
|
||||
default_country = bot.config.get('Weather', 'default_country', fallback='US')
|
||||
|
||||
# Check cache first
|
||||
cache_query = f"{zipcode}, {default_country}"
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat is not None and cached_lon is not None:
|
||||
return cached_lat, cached_lon
|
||||
|
||||
# Use rate-limited Nominatim to geocode the zipcode
|
||||
location = await rate_limited_nominatim_geocode(bot, cache_query, timeout=timeout)
|
||||
if location:
|
||||
# Cache the result for future use
|
||||
bot.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
else:
|
||||
return None, None
|
||||
except Exception as e:
|
||||
bot.logger.error(f"Error geocoding zipcode {zipcode}: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def geocode_zipcode_sync(bot, zipcode: str, default_country: str = None, timeout: int = 10) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""
|
||||
Synchronous version of geocode_zipcode.
|
||||
|
||||
Args:
|
||||
bot: Bot instance (must have db_manager and nominatim_rate_limiter)
|
||||
zipcode: ZIP code string
|
||||
default_country: Default country code (e.g., "US"). If None, reads from bot.config
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Tuple of (latitude, longitude) or (None, None) if not found
|
||||
"""
|
||||
try:
|
||||
# Get default country from config if not provided
|
||||
if default_country is None:
|
||||
default_country = bot.config.get('Weather', 'default_country', fallback='US')
|
||||
|
||||
# Check cache first
|
||||
cache_query = f"{zipcode}, {default_country}"
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat is not None and cached_lon is not None:
|
||||
return cached_lat, cached_lon
|
||||
|
||||
# Use rate-limited Nominatim to geocode the zipcode
|
||||
location = rate_limited_nominatim_geocode_sync(bot, cache_query, timeout=timeout)
|
||||
if location:
|
||||
# Cache the result for future use
|
||||
bot.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
|
||||
return location.latitude, location.longitude
|
||||
else:
|
||||
return None, None
|
||||
except Exception as e:
|
||||
bot.logger.error(f"Error geocoding zipcode {zipcode}: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
async def geocode_city(bot, city: str, default_state: str = None,
|
||||
default_country: str = None,
|
||||
include_address_info: bool = False,
|
||||
timeout: int = 10) -> Tuple[Optional[float], Optional[float], Optional[Dict]]:
|
||||
"""
|
||||
Shared function to geocode a city name to lat/lon coordinates.
|
||||
Uses intelligent fallback logic with major city prioritization.
|
||||
|
||||
Args:
|
||||
bot: Bot instance (must have db_manager and nominatim_rate_limiter)
|
||||
city: City name (may include state/country, e.g., "Seattle, WA" or "Paris, France")
|
||||
default_state: Default state abbreviation (e.g., "WA"). If None, reads from bot.config
|
||||
default_country: Default country code (e.g., "US"). If None, reads from bot.config
|
||||
include_address_info: If True, also return address info via reverse geocoding
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Tuple of (latitude, longitude, address_info_dict) or (None, None, None) if not found
|
||||
address_info_dict is None if include_address_info is False
|
||||
"""
|
||||
try:
|
||||
# Get defaults from config if not provided
|
||||
if default_state is None:
|
||||
default_state = bot.config.get('Weather', 'default_state', fallback='WA')
|
||||
if default_country is None:
|
||||
default_country = bot.config.get('Weather', 'default_country', fallback='US')
|
||||
|
||||
city_clean = city.strip()
|
||||
state_abbr = None
|
||||
|
||||
# Parse city, state/country format if present
|
||||
if ',' in city_clean:
|
||||
parts = [p.strip() for p in city_clean.rsplit(',', 1)]
|
||||
if len(parts) == 2:
|
||||
city_clean = parts[0]
|
||||
state_abbr = parts[1].upper() if len(parts[1]) <= 2 else parts[1]
|
||||
|
||||
# Handle major cities with multiple locations (prioritize major cities)
|
||||
major_city_queries = get_major_city_queries(city_clean, state_abbr)
|
||||
if major_city_queries:
|
||||
# Try major city options first
|
||||
for major_city_query in major_city_queries:
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(major_city_query)
|
||||
if cached_lat and cached_lon:
|
||||
lat, lon = cached_lat, cached_lon
|
||||
else:
|
||||
location = await rate_limited_nominatim_geocode(bot, major_city_query, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
else:
|
||||
continue
|
||||
|
||||
# Get address info if requested
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = await rate_limited_nominatim_reverse(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
|
||||
return lat, lon, address_info
|
||||
|
||||
# If state abbreviation was parsed, use it
|
||||
if state_abbr:
|
||||
state_query = f"{city_clean}, {state_abbr}, {default_country}"
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(state_query)
|
||||
if cached_lat and cached_lon:
|
||||
lat, lon = cached_lat, cached_lon
|
||||
else:
|
||||
location = await rate_limited_nominatim_geocode(bot, state_query, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(state_query, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
else:
|
||||
lat, lon = None, None
|
||||
|
||||
if lat and lon:
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = await rate_limited_nominatim_reverse(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
# Try with default state
|
||||
cache_query = f"{city_clean}, {default_state}, {default_country}"
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat and cached_lon:
|
||||
lat, lon = cached_lat, cached_lon
|
||||
else:
|
||||
location = await rate_limited_nominatim_geocode(bot, cache_query, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
else:
|
||||
lat, lon = None, None
|
||||
|
||||
if lat and lon:
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = await rate_limited_nominatim_reverse(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
# Try without state
|
||||
location = await rate_limited_nominatim_geocode(bot, f"{city_clean}, {default_country}", timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(f"{city_clean}, {default_country}", location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = await rate_limited_nominatim_reverse(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
# Try international (no country suffix)
|
||||
location = await rate_limited_nominatim_geocode(bot, city_clean, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(city_clean, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = await rate_limited_nominatim_reverse(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
return None, None, None
|
||||
|
||||
except Exception as e:
|
||||
bot.logger.error(f"Error geocoding city {city}: {e}")
|
||||
return None, None, None
|
||||
|
||||
|
||||
def geocode_city_sync(bot, city: str, default_state: str = None,
|
||||
default_country: str = None,
|
||||
include_address_info: bool = False,
|
||||
timeout: int = 10) -> Tuple[Optional[float], Optional[float], Optional[Dict]]:
|
||||
"""
|
||||
Synchronous version of geocode_city.
|
||||
|
||||
Args:
|
||||
bot: Bot instance (must have db_manager and nominatim_rate_limiter)
|
||||
city: City name (may include state/country, e.g., "Seattle, WA" or "Paris, France")
|
||||
default_state: Default state abbreviation (e.g., "WA"). If None, reads from bot.config
|
||||
default_country: Default country code (e.g., "US"). If None, reads from bot.config
|
||||
include_address_info: If True, also return address info via reverse geocoding
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Tuple of (latitude, longitude, address_info_dict) or (None, None, None) if not found
|
||||
address_info_dict is None if include_address_info is False
|
||||
"""
|
||||
try:
|
||||
# Get defaults from config if not provided
|
||||
if default_state is None:
|
||||
default_state = bot.config.get('Weather', 'default_state', fallback='WA')
|
||||
if default_country is None:
|
||||
default_country = bot.config.get('Weather', 'default_country', fallback='US')
|
||||
|
||||
city_clean = city.strip()
|
||||
state_abbr = None
|
||||
|
||||
# Parse city, state/country format if present
|
||||
if ',' in city_clean:
|
||||
parts = [p.strip() for p in city_clean.rsplit(',', 1)]
|
||||
if len(parts) == 2:
|
||||
city_clean = parts[0]
|
||||
state_abbr = parts[1].upper() if len(parts[1]) <= 2 else parts[1]
|
||||
|
||||
# Handle major cities with multiple locations (prioritize major cities)
|
||||
major_city_queries = get_major_city_queries(city_clean, state_abbr)
|
||||
if major_city_queries:
|
||||
# Try major city options first
|
||||
for major_city_query in major_city_queries:
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(major_city_query)
|
||||
if cached_lat and cached_lon:
|
||||
lat, lon = cached_lat, cached_lon
|
||||
else:
|
||||
location = rate_limited_nominatim_geocode_sync(bot, major_city_query, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
else:
|
||||
continue
|
||||
|
||||
# Get address info if requested
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
|
||||
return lat, lon, address_info
|
||||
|
||||
# If state abbreviation was parsed, use it
|
||||
if state_abbr:
|
||||
state_query = f"{city_clean}, {state_abbr}, {default_country}"
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(state_query)
|
||||
if cached_lat and cached_lon:
|
||||
lat, lon = cached_lat, cached_lon
|
||||
else:
|
||||
location = rate_limited_nominatim_geocode_sync(bot, state_query, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(state_query, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
else:
|
||||
lat, lon = None, None
|
||||
|
||||
if lat and lon:
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
# Try with default state
|
||||
cache_query = f"{city_clean}, {default_state}, {default_country}"
|
||||
cached_lat, cached_lon = bot.db_manager.get_cached_geocoding(cache_query)
|
||||
if cached_lat and cached_lon:
|
||||
lat, lon = cached_lat, cached_lon
|
||||
else:
|
||||
location = rate_limited_nominatim_geocode_sync(bot, cache_query, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
else:
|
||||
lat, lon = None, None
|
||||
|
||||
if lat and lon:
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
# Try without state
|
||||
location = rate_limited_nominatim_geocode_sync(bot, f"{city_clean}, {default_country}", timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(f"{city_clean}, {default_country}", location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
# Try international (no country suffix)
|
||||
location = rate_limited_nominatim_geocode_sync(bot, city_clean, timeout=timeout)
|
||||
if location:
|
||||
bot.db_manager.cache_geocoding(city_clean, location.latitude, location.longitude)
|
||||
lat, lon = location.latitude, location.longitude
|
||||
|
||||
address_info = None
|
||||
if include_address_info:
|
||||
# Check cache for reverse geocoding result
|
||||
reverse_cache_key = f"reverse_{lat}_{lon}"
|
||||
cached_address = bot.db_manager.get_cached_json(reverse_cache_key, "geolocation")
|
||||
if cached_address:
|
||||
address_info = cached_address
|
||||
else:
|
||||
try:
|
||||
reverse_location = rate_limited_nominatim_reverse_sync(bot, f"{lat}, {lon}", timeout=timeout)
|
||||
if reverse_location:
|
||||
address_info = reverse_location.raw.get('address', {})
|
||||
# Cache the reverse geocoding result
|
||||
bot.db_manager.cache_json(reverse_cache_key, address_info, "geolocation", cache_hours=720)
|
||||
except:
|
||||
address_info = {}
|
||||
return lat, lon, address_info
|
||||
|
||||
return None, None, None
|
||||
|
||||
except Exception as e:
|
||||
bot.logger.error(f"Error geocoding city {city}: {e}")
|
||||
return None, None, None
|
||||
|
||||
Reference in New Issue
Block a user