models.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. from decimal import Decimal, getcontext
  2. import logging
  3. from typing import Dict
  4. from uuid import uuid4
  5. from django.contrib.auth import get_user_model
  6. from django.conf import settings
  7. from django.db import models
  8. from django.urls import reverse
  9. from django_extensions.db.models import TimeStampedModel
  10. from scrobbles.mixins import ScrobblableMixin
  11. logger = logging.getLogger(__name__)
  12. BNULL = {"blank": True, "null": True}
  13. User = get_user_model()
  14. GEOLOC_ACCURACY = int(getattr(settings, "GEOLOC_ACCURACY", 4))
  15. GEOLOC_PROXIMITY = Decimal(getattr(settings, "GEOLOC_PROXIMITY", "0.0001"))
  16. class GeoLocation(ScrobblableMixin):
  17. COMPLETION_PERCENT = getattr(settings, "LOCATION_COMPLETION_PERCENT", 100)
  18. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  19. lat = models.FloatField()
  20. lon = models.FloatField()
  21. truncated_lat = models.FloatField(**BNULL)
  22. truncated_lon = models.FloatField(**BNULL)
  23. altitude = models.FloatField(**BNULL)
  24. class Meta:
  25. unique_together = [["lat", "lon", "altitude"]]
  26. def __str__(self):
  27. if self.title:
  28. return self.title
  29. return f"{self.lat} x {self.lon}"
  30. def get_absolute_url(self):
  31. return reverse(
  32. "locations:geo_location_detail", kwargs={"slug": self.uuid}
  33. )
  34. @classmethod
  35. def find_or_create(cls, data_dict: Dict) -> "GeoLocation":
  36. """Given a data dict from GPSLogger, does the heavy lifting of looking up
  37. the location, creating if if doesn't exist yet.
  38. """
  39. # TODO Add constants for all these data keys
  40. if "lat" not in data_dict.keys() or "lon" not in data_dict.keys():
  41. logger.error("No lat or lon keys in data dict")
  42. return
  43. int_lat, r_lat = str(data_dict.get("lat", "")).split(".")
  44. int_lon, r_lon = str(data_dict.get("lon", "")).split(".")
  45. try:
  46. trunc_lat = r_lat[0:GEOLOC_ACCURACY]
  47. except IndexError:
  48. trunc_lat = r_lat
  49. try:
  50. trunc_lon = r_lon[0:GEOLOC_ACCURACY]
  51. except IndexError:
  52. trunc_lon = r_lon
  53. data_dict["lat"] = float(f"{int_lat}.{trunc_lat}")
  54. data_dict["lon"] = float(f"{int_lon}.{trunc_lon}")
  55. int_alt, r_alt = str(data_dict.get("alt", "")).split(".")
  56. data_dict["altitude"] = float(int_alt)
  57. location = cls.objects.filter(
  58. lat=data_dict.get("lat"),
  59. lon=data_dict.get("lon"),
  60. ).first()
  61. if not location:
  62. location = cls.objects.create(
  63. lat=data_dict.get("lat"),
  64. lon=data_dict.get("lon"),
  65. altitude=data_dict.get("altitude"),
  66. )
  67. return location
  68. @property
  69. def subtitle(self) -> str:
  70. if self.title:
  71. return f"{self.lat} x {self.lon}"
  72. return ""
  73. def loc_diff(self, old_lat_lon: tuple) -> tuple:
  74. return (
  75. abs(Decimal(old_lat_lon[0]) - Decimal(self.lat)),
  76. abs(Decimal(old_lat_lon[1]) - Decimal(self.lon)),
  77. )
  78. def has_moved(self, previous_location: "GeoLocation") -> bool:
  79. has_moved = False
  80. loc_diff = self.loc_diff(
  81. (previous_location.lat, previous_location.lon)
  82. )
  83. if loc_diff[0] > GEOLOC_PROXIMITY or loc_diff[1] > GEOLOC_PROXIMITY:
  84. has_moved = True
  85. logger.debug(
  86. f"[locations] checked whether location has moved against proximity setting",
  87. extra={
  88. "location_id": self.id,
  89. "loc_diff": loc_diff,
  90. "has_moved": has_moved,
  91. "previous_location_id": previous_location.id,
  92. "geoloc_proximity": GEOLOC_PROXIMITY,
  93. },
  94. )
  95. return has_moved
  96. def in_proximity(self, named=False) -> models.QuerySet:
  97. lat_min = Decimal(self.lat) - GEOLOC_PROXIMITY
  98. lat_max = Decimal(self.lat) + GEOLOC_PROXIMITY
  99. lon_min = Decimal(self.lon) - GEOLOC_PROXIMITY
  100. lon_max = Decimal(self.lon) + GEOLOC_PROXIMITY
  101. is_title_null = not named
  102. close_locations = GeoLocation.objects.filter(
  103. title__isnull=is_title_null,
  104. lat__lte=lat_max,
  105. lat__gte=lat_min,
  106. lon__lte=lon_max,
  107. lon__gte=lon_min,
  108. ).exclude(id=self.id)
  109. return close_locations