import os import json import gzip import random from datetime import datetime, timedelta import colorsys from math import radians, cos, sin, asin, sqrt from django.contrib.gis.geos import Point, LineString import requests from ..models import Marker, Trip from website.settings import VIRTUALEARTH_API_KEY max_time_diff = timedelta(hours=6) max_distance = 3000 # m class TripConverter: trips: list[Trip] markers: list[Marker] created: int updated: int skipped: int def __init__(self, startDate: datetime=None): if startDate: self.markers = Marker.objects.filter(timestamp__gte=startDate).all() self.trips = list(Trip.objects.filter(endTime__gte=startDate).only("startTime", "endTime", "totalTime")) else: self.markers = Marker.objects.all() self.trips = list(Trip.objects.only("startTime", "endTime", "totalTime")) self.created = 0 self.updated = 0 self.skipped = 0 def run(self): tmp: list[Marker] = [] for prev, marker in zip(self.markers, self.markers[1:]): if marker.timestamp - prev.timestamp > max_time_diff or Distance(marker.location, prev.location) > max_distance: if len(tmp) > 1: self.trips.append(self.create_trip(tmp)) tmp = [] tmp.append(marker) if len(tmp) > 1: self.trips.append(self.create_trip(tmp)) def create_trip(self, markers: list[Marker]) -> Trip: print(len(markers), markers[0].timestamp, markers[-1].timestamp) trip = None for current in self.trips: if current.startTime <= markers[-1].timestamp and current.endTime >= markers[0].timestamp: trip = current self.trips.remove(current) break center = markers[len(markers)//2].location if not trip: trip = Trip( startTime = markers[0].timestamp, endTime = markers[-1].timestamp, name = f"Trip {markers[0].timestamp}", color = get_path_color(markers[0].timestamp), center = Point(center.x, center.y) ) self.created += 1 self.updated -= 1 elif trip.startTime == markers[0].timestamp and \ trip.endTime == markers[-1].timestamp and \ trip.totalTime == markers[-1].timestamp - markers[0].timestamp: print("Trip already exists") self.skipped += 1 return trip if not trip.name or "None" in trip.name or trip.name.startswith("Trip "): start = get_location_name(markers[0].location) end = get_location_name(markers[-1].location) if start != end: trip.name = f"{start} - {end}" else: trip.name = start trip.startTime = markers[0].timestamp trip.endTime = markers[-1].timestamp trip.totalTime = trip.endTime - trip.startTime trip.center = Point(center.x, center.y) total_distance = 0 # m topSpeed = 0 # km/h ascendHeight = 0 # m descendHeight = 0 # m movementTime = timedelta(0) minLat = minLon = maxLat = maxLon = None lastSpeed = 0 i = 1 while i < len(markers): point = markers[i] prev_point = markers[i-1] dist = Distance(point.location, prev_point.location) if point.speed is not None and point.speed > 0: speed = point.speed else: speed = dist / abs(point.timestamp - prev_point.timestamp).seconds * 3.6 if abs(speed - lastSpeed) / abs(point.timestamp - prev_point.timestamp).seconds > 10: # m/s² markers.remove(point) continue if abs(speed - lastSpeed) > 50: # m/s markers.remove(point) continue total_distance += dist topSpeed = max(topSpeed, speed) minLat = min(minLat, point.location.y) if minLat is not None else point.location.y minLon = min(minLon, point.location.x) if minLon is not None else point.location.x maxLat = max(maxLat, point.location.y) if maxLat is not None else point.location.y maxLon = max(maxLon, point.location.x) if maxLon is not None else point.location.x if speed > 2.0: # km/h movementTime += abs(point.timestamp - prev_point.timestamp) if point.alt is not None and prev_point.alt is not None: if point.alt > prev_point.alt: ascendHeight += point.alt - prev_point.alt else: descendHeight += prev_point.alt - point.alt i += 1 trip.distance = round(total_distance, 1) # m trip.topSpeed = round(topSpeed, 1) # km/h trip.avgSpeed = round(total_distance / (movementTime or trip.endTime - trip.startTime).total_seconds() * 3.6, 1) # km/h trip.ascendHeight = round(ascendHeight, 1) # m trip.descendHeight = round(descendHeight, 1) # m trip.movementTime = movementTime trip.line = LineString((minLon, minLat), (maxLon, maxLat)) trip.path = points_to_blob(markers) trip.save() self.updated += 1 return trip def Distance(point1: Point, point2: Point) -> float: lon1 = radians(point1.x) lon2 = radians(point2.x) lat1 = radians(point1.y) lat2 = radians(point2.y) # Haversine formula dlon = lon2 - lon1 dlat = lat2 - lat1 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. Use 3956 for miles return c * r def get_location_name(point: Point) -> str: url = f"https://dev.virtualearth.net/REST/v1/LocationRecog/{round(point.y, 6)},{round(point.x, 6)}?radius=2&top=1&c=de-DE&includeEntityTypes=address&key={VIRTUALEARTH_API_KEY}&output=json" response = requests.get(url) if response.status_code == 200: data = response.json() if data["resourceSets"][0]["estimatedTotal"] > 0: address = data["resourceSets"][0]["resources"][0]["addressOfLocation"][0] if address["locality"] and address["neighborhood"]: txt = f"{address['locality']} {address['neighborhood']}" else: txt = address["adminDivision"] if address["countryIso2"]: txt += f" {address['countryIso2']}" return txt return None def get_path_color(time: datetime) -> str: random.seed(int(time.timestamp())) hue = random.random() saturation = 0.5 + random.random() / 2 value = 0.5 + random.random() / 2 rgb = colorsys.hsv_to_rgb(hue, saturation, value) return f"#{int(rgb[0]*255):02x}{int(rgb[1]*255):02x}{int(rgb[2]*255):02x}" def points_to_blob(markers) -> bytes: arr = [] for marker in markers: arr.append({ "lat": marker.location.y, "lng": marker.location.x, "alt": marker.alt, "hdop": marker.hdop, "speed": marker.speed, "timestamp": marker.timestamp.timestamp(), }) data = json.dumps(arr).encode('utf-8') return gzip.compress(data)