Help_Me_3 / app /services /geocoding.py
giyos1212's picture
Upload 72 files
98b6d67 verified
# app/services/geocoding.py
"""
Geocoding Service - Manzilni koordinataga aylantirish
Nominatim API (OpenStreetMap) ishlatiladi
"""
import logging
from typing import Optional, Dict
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut, GeocoderServiceError
import time
from app.core.config import NOMINATIM_USER_AGENT, GEOCODING_TIMEOUT, TASHKENT_BOUNDS
logger = logging.getLogger(__name__)
# Nominatim client (rate limit: 1 request/second)
geolocator = Nominatim(user_agent=NOMINATIM_USER_AGENT, timeout=GEOCODING_TIMEOUT)
# Rate limiting
last_request_time = 0
MIN_REQUEST_INTERVAL = 1.0 # 1 second between requests
def _wait_for_rate_limit():
"""Rate limit uchun kutish (1 req/sec)"""
global last_request_time
current_time = time.time()
time_since_last = current_time - last_request_time
if time_since_last < MIN_REQUEST_INTERVAL:
wait_time = MIN_REQUEST_INTERVAL - time_since_last
logger.debug(f"⏳ Rate limit: {wait_time:.2f}s kutilmoqda...")
time.sleep(wait_time)
last_request_time = time.time()
def geocode_address(address_text: str) -> Optional[Dict]:
"""
Manzil matnini koordinataga aylantirish
Args:
address_text: Manzil matni (masalan: "Chilonzor tumani, Bunyodkor ko'chasi")
Returns:
{
"lat": 41.2856,
"lon": 69.2034,
"display_name": "To'liq manzil",
"address": {...}
}
yoki None (xatolik bo'lsa)
"""
if not address_text or len(address_text.strip()) < 3:
logger.warning("❌ Bo'sh yoki juda qisqa manzil")
return None
try:
# Toshkent qo'shib qidirish (aniqlik uchun)
search_query = f"{address_text}, Toshkent, O'zbekiston"
logger.info(f"πŸ” Geocoding: '{search_query}'")
# Rate limit
_wait_for_rate_limit()
# Nominatim API call
location = geolocator.geocode(
search_query,
exactly_one=True,
language='uz',
addressdetails=True
)
if not location:
logger.warning(f"⚠️ Manzil topilmadi: '{address_text}'")
return None
# Koordinatalarni tekshirish
if not validate_location_in_tashkent(location.latitude, location.longitude):
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {location.latitude}, {location.longitude}")
return None
result = {
"lat": location.latitude,
"lon": location.longitude,
"display_name": location.address,
"address": location.raw.get('address', {})
}
logger.info(f"βœ… Geocoding muvaffaqiyatli: {result['lat']}, {result['lon']}")
return result
except GeocoderTimedOut:
logger.error(f"⏱️ Geocoding timeout: '{address_text}'")
return None
except GeocoderServiceError as e:
logger.error(f"❌ Geocoding service xatoligi: {e}")
return None
except Exception as e:
logger.error(f"❌ Geocoding kutilmagan xatolik: {e}", exc_info=True)
return None
def reverse_geocode(lat: float, lon: float) -> Optional[Dict]:
"""
Koordinatalardan manzilni topish (reverse geocoding)
Args:
lat: Latitude
lon: Longitude
Returns:
{
"display_name": "To'liq manzil",
"address": {
"suburb": "Chilonzor",
"city": "Toshkent",
...
}
}
yoki None
"""
try:
# Koordinatalarni validatsiya qilish
if not validate_location_in_tashkent(lat, lon):
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}")
return None
logger.info(f"πŸ” Reverse geocoding: {lat}, {lon}")
# Rate limit
_wait_for_rate_limit()
# Nominatim API call
location = geolocator.reverse(
(lat, lon),
exactly_one=True,
language='uz',
addressdetails=True
)
if not location:
logger.warning(f"⚠️ Manzil topilmadi: {lat}, {lon}")
return None
result = {
"display_name": location.address,
"address": location.raw.get('address', {})
}
logger.info(f"βœ… Reverse geocoding muvaffaqiyatli: {result['display_name']}")
return result
except GeocoderTimedOut:
logger.error(f"⏱️ Reverse geocoding timeout: {lat}, {lon}")
return None
except GeocoderServiceError as e:
logger.error(f"❌ Reverse geocoding service xatoligi: {e}")
return None
except Exception as e:
logger.error(f"❌ Reverse geocoding kutilmagan xatolik: {e}", exc_info=True)
return None
def validate_location_in_tashkent(lat: float, lon: float) -> bool:
"""
Koordinatalarning Toshkent chegarasida ekanligini tekshirish
Args:
lat: Latitude
lon: Longitude
Returns:
True - Toshkent ichida, False - tashqarida
"""
try:
in_bounds = (
TASHKENT_BOUNDS["lat_min"] <= lat <= TASHKENT_BOUNDS["lat_max"] and
TASHKENT_BOUNDS["lon_min"] <= lon <= TASHKENT_BOUNDS["lon_max"]
)
if not in_bounds:
logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}")
return in_bounds
except Exception as e:
logger.error(f"❌ Koordinatalarni tekshirishda xatolik: {e}")
return False
def get_location_summary(geocoded_data: Dict) -> str:
"""
Geocoding natijasidan qisqacha manzil chiqarish
Args:
geocoded_data: geocode_address() natijasi
Returns:
"Chilonzor tumani, Toshkent" kabi qisqa manzil
"""
try:
address = geocoded_data.get('address', {})
# Tuman/mahalla
suburb = address.get('suburb', address.get('neighbourhood', ''))
# Ko'cha
road = address.get('road', '')
# Shahar
city = address.get('city', address.get('town', 'Toshkent'))
# Qisqacha manzil
parts = []
if suburb:
parts.append(suburb)
if road and suburb.lower() not in road.lower(): # Takrorlanishni oldini olish
parts.append(road)
if city and city != 'Toshkent':
parts.append(city)
summary = ', '.join(parts) if parts else geocoded_data.get('display_name', 'Noma\'lum manzil')
return summary
except Exception as e:
logger.error(f"❌ Location summary yaratishda xatolik: {e}")
return "Noma'lum manzil"
def extract_district_from_address(geocoded_data: Dict) -> Optional[str]:
"""
Geocoding natijasidan tuman nomini chiqarish
Args:
geocoded_data: geocode_address() natijasi
Returns:
"Chilonzor" kabi tuman nomi yoki None
"""
try:
address = geocoded_data.get('address', {})
# Suburb yoki neighbourhood maydonidan tuman nomini olish
district = address.get('suburb', address.get('neighbourhood', None))
if district:
# "tumani" so'zini olib tashlash
district = district.replace(' tumani', '').replace(' Tumani', '').strip()
logger.info(f"πŸ“ Tuman aniqlandi: {district}")
return district
except Exception as e:
logger.error(f"❌ Tumanni chiqarishda xatolik: {e}")
return None