Auto-update: Fri Jun 28 23:26:17 PDT 2024
This commit is contained in:
parent
6e960dca9e
commit
37600ce981
3 changed files with 76 additions and 47 deletions
sijapi
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
Loading…
Add table
Reference in a new issue