From 9769748939736946dde980ae119819a292caf8c1 Mon Sep 17 00:00:00 2001 From: Sean Breckenridge Date: Wed, 15 Feb 2023 21:25:25 -0800 Subject: [PATCH] estimate_from helper, via_home estimator, all.py --- my/location/fallback/all.py | 19 ++++++++++ my/location/fallback/common.py | 60 +++++++++++++++++++++++++++++--- my/location/fallback/via_home.py | 24 +++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/my/location/fallback/all.py b/my/location/fallback/all.py index e69de29..ae7b333 100644 --- a/my/location/fallback/all.py +++ b/my/location/fallback/all.py @@ -0,0 +1,19 @@ +# TODO: add config here which passes kwargs to estimate_from (under_accuracy) +# overwritable by passing the kwarg name here to the top-level estimate_location + +from typing import Union +from datetime import datetime + +from my.location.fallback.common import estimate_from, FallbackLocation + +def estimate_location(dt: Union[datetime, float, int]) -> FallbackLocation: + from my.location.fallback.via_home import estimate_location as via_home + + loc = estimate_from( + dt, + estimators=(via_home,) + ) + if loc is None: + raise ValueError("Could not estimate location") + return loc + diff --git a/my/location/fallback/common.py b/my/location/fallback/common.py index 49b9620..38c3181 100644 --- a/my/location/fallback/common.py +++ b/my/location/fallback/common.py @@ -1,17 +1,17 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional +from typing import Optional, Callable, Sequence, Iterator, List, Union from datetime import datetime, timedelta from ..common import LocationProtocol, Location - +DateIshExact = Union[datetime, float, int] @dataclass class FallbackLocation(LocationProtocol): lat: float lon: float dt: datetime - duration: float # time in seconds for how long this is valid + duration: Optional[float] = None # time in seconds for how long this is valid accuracy: Optional[float] = None elevation: Optional[float] = None datasource: Optional[str] = None # which module provided this, useful for debugging @@ -22,7 +22,7 @@ class FallbackLocation(LocationProtocol): If end is True, the start date + duration is used ''' dt: datetime = self.dt - if end: + if end and self.duration is not None: dt += timedelta(self.duration) return Location( lat=self.lat, @@ -48,7 +48,7 @@ class FallbackLocation(LocationProtocol): Create FallbackLocation from a start date and an end date ''' if end_dt < dt: - raise ValueError('end_date must be after dt') + raise ValueError("end_date must be after dt") duration = (end_dt - dt).total_seconds() return cls( lat=lat, @@ -61,4 +61,54 @@ class FallbackLocation(LocationProtocol): ) +LocationEstimator = Callable[[DateIshExact], Optional[FallbackLocation]] +LocationEstimators = Sequence[LocationEstimator] + +# helper function, instead of dealing with datetimes while comparing, just use epoch timestamps +def _datetime_timestamp(dt: DateIshExact) -> float: + if isinstance(dt, datetime): + return dt.timestamp() + return float(dt) + # TODO: create estimate location which uses other fallback_locations to estimate a location +def _iter_estimate_from( + dt: DateIshExact, + estimators: LocationEstimators, +) -> Iterator[FallbackLocation]: + for est in estimators: + loc = est(dt) + if loc is None: + continue + yield loc + + +def estimate_from( + dt: DateIshExact, + estimators: LocationEstimators, + *, + first_match: bool = False, + under_accuracy: Optional[int] = None, +) -> Optional[FallbackLocation]: + ''' + first_match: if True, return the first location found + under_accuracy: if set, only return locations with accuracy under this value + ''' + found: List[FallbackLocation] = [] + for loc in _iter_estimate_from(dt, estimators): + if under_accuracy is not None and loc.accuracy is not None and loc.accuracy > under_accuracy: + continue + if first_match: + return loc + found.append(loc) + + if not found: + return None + + # if all items have accuracy, return the one with the lowest accuracy + # otherwise, we should prefer the order that the estimators are passed in as + if all(loc.accuracy is not None for loc in found): + # return the location with the lowest accuracy + return min(has_accuracy, key=lambda loc: loc.accuracy) # type: ignore[union-attr] + else: + # return the first location + return found[0] diff --git a/my/location/fallback/via_home.py b/my/location/fallback/via_home.py index 3f09b13..6de8bea 100644 --- a/my/location/fallback/via_home.py +++ b/my/location/fallback/via_home.py @@ -10,6 +10,7 @@ from typing import Sequence, Tuple, Union, cast from my.config import location as user_config from my.location.common import LatLon, DateIsh +from my.location.fallback.common import FallbackLocation @dataclass class Config(user_config): @@ -70,3 +71,26 @@ def get_location(dt: datetime) -> LatLon: else: # I guess the most reasonable is to fallback on the first location return hist[-1][1] + + +def estimate_location(dt: Union[datetime, int, float]) -> FallbackLocation: + from my.location.fallback.common import _datetime_timestamp + d: float = _datetime_timestamp(dt) + # TODO: cache this? + hist = list(reversed(config._history)) + for pdt, (lat, lon) in hist: + if d >= pdt.timestamp(): + # TODO: add accuracy? + return FallbackLocation( + lat=lat, + lon=lon, + dt=datetime.fromtimestamp(d, timezone.utc), + datasource='via_home') + else: + # I guess the most reasonable is to fallback on the first location + lat, lon = hist[-1][1] + return FallbackLocation( + lat=lat, + lon=lon, + dt=datetime.fromtimestamp(d, timezone.utc), + datasource='via_home')