Auto-update: Fri Jun 28 23:26:17 PDT 2024

This commit is contained in:
sanj 2024-06-28 23:26:17 -07:00
parent 6e960dca9e
commit 37600ce981
3 changed files with 76 additions and 47 deletions

View file

@ -12,7 +12,7 @@ from typing import List, Optional
import traceback import traceback
import logging import logging
from .logs import Logger from .logs import Logger
from .classes import AutoResponder, IMAPConfig, SMTPConfig, EmailAccount, EmailContact, IncomingEmail, TimezoneTracker, Database, Geocoder from .classes import AutoResponder, IMAPConfig, SMTPConfig, EmailAccount, EmailContact, IncomingEmail, Database, Geocoder
# from sijapi.config.config import load_config # from sijapi.config.config import load_config
# cfg = load_config() # cfg = load_config()

View file

@ -3,6 +3,8 @@ from typing import List, Optional, Any, Tuple, Dict, Union, Tuple
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import asyncio import asyncio
import json import json
import yaml
import math
from timezonefinder import TimezoneFinder from timezonefinder import TimezoneFinder
from pathlib import Path from pathlib import Path
import asyncpg import asyncpg
@ -46,6 +48,7 @@ class Location(BaseModel):
} }
class Geocoder: class Geocoder:
def __init__(self, named_locs: Union[str, Path] = None, cache_file: Union[str, Path] = 'timezone_cache.json'): def __init__(self, named_locs: Union[str, Path] = None, cache_file: Union[str, Path] = 'timezone_cache.json'):
self.tf = TimezoneFinder() self.tf = TimezoneFinder()
@ -56,10 +59,52 @@ class Geocoder:
self.last_update: Optional[datetime] = None self.last_update: Optional[datetime] = None
self.last_location: Optional[Tuple[float, float]] = None self.last_location: Optional[Tuple[float, float]] = None
self.executor = ThreadPoolExecutor() self.executor = ThreadPoolExecutor()
self.override_locations = self.load_override_locations()
def load_override_locations(self):
if self.named_locs and self.named_locs.exists():
with open(self.named_locs, 'r') as file:
return yaml.safe_load(file)
return []
def haversine(self, lat1, lon1, lat2, lon2):
R = 6371
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
return R * c
def find_override_location(self, lat: float, lon: float) -> Optional[str]:
closest_location = None
closest_distance = float('inf')
for location in self.override_locations:
loc_name = location.get("name")
loc_lat = location.get("latitude")
loc_lon = location.get("longitude")
loc_radius = location.get("radius")
distance = self.haversine(lat, lon, loc_lat, loc_lon)
if distance <= loc_radius:
if distance < closest_distance:
closest_distance = distance
closest_location = loc_name
return closest_location
async def location(self, lat: float, lon: float): async def location(self, lat: float, lon: float):
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
return await loop.run_in_executor(self.executor, rg.search, [(lat, lon)]) result = await loop.run_in_executor(self.executor, rg.search, [(lat, lon)])
override = self.find_override_location(lat, lon)
if override:
result[0]['override_name'] = override
return result
async def elevation(self, latitude: float, longitude: float, unit: str = "m") -> float: async def elevation(self, latitude: float, longitude: float, unit: str = "m") -> float:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
@ -107,12 +152,14 @@ class Geocoder:
coordinates = [(location.latitude, location.longitude) for location in processed_locations] coordinates = [(location.latitude, location.longitude) for location in processed_locations]
geocode_results = await self.location(*zip(*coordinates)) geocode_results = await asyncio.gather(*[self.location(lat, lon) for lat, lon in coordinates])
elevations = await asyncio.gather(*[self.elevation(lat, lon) for lat, lon in coordinates]) elevations = await asyncio.gather(*[self.elevation(lat, lon) for lat, lon in coordinates])
timezones = await asyncio.gather(*[self.timezone(lat, lon) for lat, lon in coordinates]) timezones = await asyncio.gather(*[self.timezone(lat, lon) for lat, lon in coordinates])
geocoded_locations = [] geocoded_locations = []
for location, result, elevation, timezone in zip(processed_locations, geocode_results, elevations, timezones): for location, result, elevation, timezone in zip(processed_locations, geocode_results, elevations, timezones):
result = result[0] # Unpack the first result
override_name = result.get('override_name')
geocoded_location = Location( geocoded_location = Location(
latitude=location.latitude, latitude=location.latitude,
longitude=location.longitude, longitude=location.longitude,
@ -123,8 +170,8 @@ class Geocoder:
state=result.get("admin1"), state=result.get("admin1"),
country=result.get("cc"), country=result.get("cc"),
context=location.context or {}, context=location.context or {},
name=result.get("name"), name=override_name or result.get("name"),
display_name=f"{result.get('name')}, {result.get('admin1')}, {result.get('cc')}", display_name=f"{override_name or result.get('name')}, {result.get('admin1')}, {result.get('cc')}",
country_code=result.get("cc"), country_code=result.get("cc"),
timezone=timezone timezone=timezone
) )
@ -176,44 +223,12 @@ class Geocoder:
timezone=await self.timezone(latitude, longitude) timezone=await self.timezone(latitude, longitude)
) )
def load_override_locations(self):
if self.named_locs and self.named_locs.exists():
with open(self.named_locs, 'r') as file:
return yaml.safe_load(file)
return []
def haversine(self, lat1, lon1, lat2, lon2): def round_coords(self, lat: float, lon: float, decimal_places: int = 2) -> Tuple[float, float]:
R = 6371 # Earth's radius in kilometers return (round(lat, decimal_places), round(lon, decimal_places))
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) def coords_equal(self, coord1: Tuple[float, float], coord2: Tuple[float, float], tolerance: float = 1e-5) -> bool:
dlat = lat2 - lat1 return math.isclose(coord1[0], coord2[0], abs_tol=tolerance) and math.isclose(coord1[1], coord2[1], abs_tol=tolerance)
dlon = lon2 - lon1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * atan2(sqrt(a), sqrt(1-a))
return R * c
async def find_override_location(self, lat: float, lon: float) -> Optional[str]:
closest_location = None
closest_distance = float('inf')
for location in self.override_locations:
loc_name = location.get("name")
loc_lat = location.get("latitude")
loc_lon = location.get("longitude")
loc_radius = location.get("radius")
distance = self.haversine(lat, lon, loc_lat, loc_lon)
if distance <= loc_radius:
if distance < closest_distance:
closest_distance = distance
closest_location = loc_name
return closest_location
async def refresh_timezone(self, location: Union[Location, Tuple[float, float]], force: bool = False) -> str: async def refresh_timezone(self, location: Union[Location, Tuple[float, float]], force: bool = False) -> str:
if isinstance(location, Location): if isinstance(location, Location):
@ -221,16 +236,20 @@ class Geocoder:
else: else:
lat, lon = location lat, lon = location
rounded_location = self.round_coords(lat, lon)
current_time = datetime.now() current_time = datetime.now()
if (force or if (force or
not self.last_update or not self.last_update or
current_time - self.last_update > timedelta(hours=1) or current_time - self.last_update > timedelta(hours=1) or
self.last_location != (lat, lon)): not self.coords_equal(rounded_location, self.round_coords(*self.last_location) if self.last_location else (None, None))):
new_timezone = await self.timezone(lat, lon) new_timezone = await self.timezone(lat, lon)
self.last_timezone = new_timezone self.last_timezone = new_timezone
self.last_update = current_time self.last_update = current_time
self.last_location = (lat, lon) self.last_location = (lat, lon) # Store the original, non-rounded coordinates
await self.tz_save() await self.tz_save()
return self.last_timezone return self.last_timezone
async def tz_save(self): async def tz_save(self):
@ -260,6 +279,17 @@ class Geocoder:
async def tz_last(self) -> Optional[str]: async def tz_last(self) -> Optional[str]:
await self.tz_cached() await self.tz_cached()
return self.last_timezone return self.last_timezone
async def tz_at(self, lat: float, lon: float) -> str:
"""
Get the timezone at a specific latitude and longitude without affecting the cache.
:param lat: Latitude
:param lon: Longitude
:return: Timezone string
"""
return await self.timezone(lat, lon)
def __del__(self): def __del__(self):
self.executor.shutdown() self.executor.shutdown()

View file

@ -30,11 +30,10 @@ from dateutil.parser import parse as dateutil_parse
from fastapi import HTTPException, status from fastapi import HTTPException, status
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Query, HTTPException from fastapi import APIRouter, Query, HTTPException
from sijapi import L, OBSIDIAN_VAULT_DIR, OBSIDIAN_RESOURCES_DIR, ARCHIVE_DIR, BASE_URL, OBSIDIAN_BANNER_SCENE, DEFAULT_11L_VOICE, DEFAULT_VOICE, TZ, DynamicTZ, GEO from sijapi import L, OBSIDIAN_VAULT_DIR, OBSIDIAN_RESOURCES_DIR, ARCHIVE_DIR, BASE_URL, OBSIDIAN_BANNER_SCENE, DEFAULT_11L_VOICE, DEFAULT_VOICE, GEO
from sijapi.routers import cal, loc, tts, llm, time, sd, weather, asr from sijapi.routers import cal, loc, tts, llm, time, sd, weather, asr
from sijapi.routers.loc import Location
from sijapi.utilities import assemble_journal_path, assemble_archive_path, convert_to_12_hour_format, sanitize_filename, convert_degrees_to_cardinal, HOURLY_COLUMNS_MAPPING from sijapi.utilities import assemble_journal_path, assemble_archive_path, convert_to_12_hour_format, sanitize_filename, convert_degrees_to_cardinal, HOURLY_COLUMNS_MAPPING
from sijapi.classes import Location
note = APIRouter() note = APIRouter()
@ -70,7 +69,7 @@ async def build_daily_note_endpoint(
date_str = dt_datetime.now().strftime("%Y-%m-%d") date_str = dt_datetime.now().strftime("%Y-%m-%d")
if location: if location:
lat, lon = map(float, location.split(',')) lat, lon = map(float, location.split(','))
tz = ZoneInfo(DynamicTZ.find(lat, lon)) tz = GEO.tz_at(lat, lon)
date_time = dateutil_parse(date_str).replace(tzinfo=tz) date_time = dateutil_parse(date_str).replace(tzinfo=tz)
else: else:
raise ValueError("Location is not provided or invalid.") raise ValueError("Location is not provided or invalid.")