123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 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)
|