[locations] Add distance and speed calculations
All checks were successful
build & deploy / test (push) Successful in 1m57s
build & deploy / deploy (push) Successful in 27s

This commit is contained in:
2026-05-01 11:50:24 -04:00
parent acf0c342bf
commit de733d5893
6 changed files with 340 additions and 2 deletions

View File

@ -0,0 +1,198 @@
import logging
from typing import Optional
from django.utils import timezone
from scrobbles.models import Scrobble
logger = logging.getLogger(__name__)
# Speed thresholds in meters per second
SPEED_THRESHOLDS = {
"at_rest": 0.5,
"walking": 1.5,
"running": 3.5,
"bicycling": 9.0,
"driving": float("inf"),
}
# Activity mapping from Android activity recognition
ACTIVITY_MAPPING = {
"still": "at_rest",
"on_foot": "walking",
"walking": "walking",
"running": "running",
"on_bicycle": "bicycling",
"in_vehicle": "driving",
"unknown": "unknown",
}
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Calculate the great-circle distance between two points in meters."""
from math import radians, cos, sin, asin, sqrt
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a))
r = 6371000 # Radius of earth in meters
return c * r
def detect_movement(
current_scrobble: Scrobble,
previous_scrobble: Optional[Scrobble] = None,
) -> dict:
"""
Analyze location scrobble to determine if moving and mode of transport.
Returns a dict shaped like GeoLocationLogData that can be stored in scrobble.log:
{
"is_moving": bool,
"movement_type": str,
"confidence": str,
"speed_mps": float,
"speed_accuracy": float,
"detection_method": str,
"activity": str,
"detected_at": str,
}
"""
result = {
"is_moving": False,
"movement_type": "unknown",
"confidence": "low",
"speed_mps": 0.0,
"speed_accuracy": 0.0,
"detection_method": "unknown",
"activity": "",
"detected_at": timezone.now().isoformat(),
}
if current_scrobble.media_type != Scrobble.MediaType.GEO_LOCATION:
return result
# Get GPS data from log
log_data = current_scrobble.log or {}
gps_data = log_data.get("gps_data", {})
# Method 1: Check Android activity recognition (highest confidence)
activity = gps_data.get("activity", "")
if activity and activity in ACTIVITY_MAPPING:
movement_type = ACTIVITY_MAPPING.get(activity, "unknown")
result.update(
{
"is_moving": movement_type != "at_rest",
"movement_type": movement_type,
"confidence": "high",
"detection_method": "activity_recognition",
"activity": activity,
}
)
return result
# Method 2: Use speed from GPS
speed = gps_data.get("speed")
accuracy = gps_data.get("accuracy", 100.0)
if speed is not None:
try:
speed = float(speed)
result["speed_mps"] = speed
result["speed_accuracy"] = float(accuracy)
# Determine movement type from speed
movement_type = "at_rest"
is_moving = False
if speed >= SPEED_THRESHOLDS["walking"]:
movement_type = "walking"
is_moving = True
if speed >= SPEED_THRESHOLDS["running"]:
movement_type = "running"
is_moving = True
if speed >= SPEED_THRESHOLDS["bicycling"]:
movement_type = "bicycling"
is_moving = True
if speed >= SPEED_THRESHOLDS["driving"]:
movement_type = "driving"
is_moving = True
# Confidence based on GPS accuracy
confidence = "low"
if accuracy <= 10:
confidence = "high"
elif accuracy <= 30:
confidence = "medium"
result.update(
{
"is_moving": is_moving,
"movement_type": movement_type,
"confidence": confidence,
"detection_method": "gps_speed",
}
)
return result
except (ValueError, TypeError):
pass
# Method 3: Calculate speed from distance/time between scrobbles
if not previous_scrobble:
# Try to get previous scrobble from the same user
previous_scrobble = (
Scrobble.objects.filter(
user=current_scrobble.user,
media_type=Scrobble.MediaType.GEO_LOCATION,
timestamp__lt=current_scrobble.timestamp,
)
.order_by("-timestamp")
.first()
)
if previous_scrobble and previous_scrobble.geo_location:
time_diff = (
current_scrobble.timestamp - previous_scrobble.timestamp
).total_seconds()
if time_diff > 0:
distance = haversine_distance(
previous_scrobble.geo_location.lat,
previous_scrobble.geo_location.lon,
current_scrobble.geo_location.lat,
current_scrobble.geo_location.lon,
)
speed = distance / time_diff
result["speed_mps"] = round(speed, 2)
# Determine movement type from calculated speed
movement_type = "at_rest"
is_moving = False
if speed >= SPEED_THRESHOLDS["walking"]:
movement_type = "walking"
is_moving = True
if speed >= SPEED_THRESHOLDS["running"]:
movement_type = "running"
is_moving = True
if speed >= SPEED_THRESHOLDS["bicycling"]:
movement_type = "bicycling"
is_moving = True
if speed >= SPEED_THRESHOLDS["driving"]:
movement_type = "driving"
is_moving = True
# Lower confidence for calculated speed
result.update(
{
"is_moving": is_moving,
"movement_type": movement_type,
"confidence": "low",
"detection_method": "calculated_speed",
}
)
return result
return result