points2trips.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import os
  2. import json
  3. import gzip
  4. import random
  5. from datetime import datetime, timedelta
  6. import colorsys
  7. from math import radians, cos, sin, asin, sqrt
  8. from django.contrib.gis.geos import Point, LineString
  9. import requests
  10. from ..models import Marker, Trip
  11. from website.settings import VIRTUALEARTH_API_KEY
  12. max_time_diff = timedelta(hours=6)
  13. max_distance = 3000 # m
  14. class TripConverter:
  15. trips: list[Trip]
  16. markers: list[Marker]
  17. created: int
  18. updated: int
  19. skipped: int
  20. def __init__(self, markers: list[Marker]):
  21. self.markers = markers
  22. self.trips = []
  23. self.created = 0
  24. self.updated = 0
  25. self.skipped = 0
  26. def run(self):
  27. first_index = 0
  28. for i, point in enumerate(self.markers[1:]):
  29. prev_point = self.markers[i-1]
  30. if point.timestamp - prev_point.timestamp > max_time_diff or \
  31. Distance(point.location, prev_point.location) > max_distance:
  32. if i - first_index > 2:
  33. self.trips.append(self.create_trip(self.markers[first_index:i]))
  34. first_index = i
  35. if first_index < len(self.markers) - 2:
  36. self.trips.append(self.create_trip(self.markers[first_index:]))
  37. def create_trip(self, markers: list[Marker]) -> Trip:
  38. print(len(markers), markers[0].timestamp, markers[-1].timestamp)
  39. trip = Trip.objects.filter(startTime__lte=markers[-1].timestamp, endTime__gte=markers[0].timestamp).first()
  40. center = markers[len(markers)//2].location
  41. if not trip:
  42. trip = Trip.objects.create(
  43. startTime = markers[0].timestamp,
  44. endTime = markers[-1].timestamp,
  45. name = f"Trip {markers[0].timestamp}",
  46. color = get_path_color(markers[0].timestamp),
  47. center = Point(center.x, center.y)
  48. )
  49. self.created += 1
  50. self.updated -= 1
  51. elif trip.startTime == markers[0].timestamp and \
  52. trip.endTime == markers[-1].timestamp and \
  53. trip.totalTime == markers[-1].timestamp - markers[0].timestamp:
  54. print("Trip already exists")
  55. self.skipped += 1
  56. return trip
  57. if not trip.name or "None" in trip.name or trip.name.startswith("Trip "):
  58. start = get_location_name(markers[0].location)
  59. end = get_location_name(markers[-1].location)
  60. if start != end:
  61. trip.name = f"{start} - {end}"
  62. else:
  63. trip.name = start
  64. trip.startTime = markers[0].timestamp
  65. trip.endTime = markers[-1].timestamp
  66. trip.totalTime = trip.endTime - trip.startTime
  67. trip.center = Point(center.x, center.y)
  68. total_distance = 0 # m
  69. topSpeed = 0 # km/h
  70. ascendHeight = 0 # m
  71. descendHeight = 0 # m
  72. movementTime = timedelta(0)
  73. minLat = minLon = maxLat = maxLon = None
  74. lastSpeed = 0
  75. i = 1
  76. while i < len(markers):
  77. point = markers[i]
  78. prev_point = markers[i-1]
  79. dist = Distance(point.location, prev_point.location)
  80. if point.speed is not None and point.speed > 0:
  81. speed = point.speed
  82. else:
  83. speed = dist / abs(point.timestamp - prev_point.timestamp).seconds * 3.6
  84. if abs(speed - lastSpeed) / abs(point.timestamp - prev_point.timestamp).seconds > 10: # m/s²
  85. markers.remove(point)
  86. continue
  87. if abs(speed - lastSpeed) > 50: # m/s
  88. markers.remove(point)
  89. continue
  90. total_distance += dist
  91. topSpeed = max(topSpeed, speed)
  92. minLat = min(minLat, point.location.y) if minLat is not None else point.location.y
  93. minLon = min(minLon, point.location.x) if minLon is not None else point.location.x
  94. maxLat = max(maxLat, point.location.y) if maxLat is not None else point.location.y
  95. maxLon = max(maxLon, point.location.x) if maxLon is not None else point.location.x
  96. if speed > 2.0: # km/h
  97. movementTime += abs(point.timestamp - prev_point.timestamp)
  98. if point.alt is not None and prev_point.alt is not None:
  99. if point.alt > prev_point.alt:
  100. ascendHeight += point.alt - prev_point.alt
  101. else:
  102. descendHeight += prev_point.alt - point.alt
  103. i += 1
  104. trip.distance = round(total_distance, 1) # m
  105. trip.topSpeed = round(topSpeed, 1) # km/h
  106. trip.avgSpeed = round(total_distance / (movementTime or trip.endTime - trip.startTime).total_seconds() * 3.6, 1) # km/h
  107. trip.ascendHeight = round(ascendHeight, 1) # m
  108. trip.descendHeight = round(descendHeight, 1) # m
  109. trip.movementTime = movementTime
  110. trip.line = LineString((minLon, minLat), (maxLon, maxLat))
  111. trip.path = points_to_blob(markers)
  112. trip.save()
  113. self.updated += 1
  114. return trip
  115. def Distance(point1: Point, point2: Point) -> float:
  116. lon1 = radians(point1.x)
  117. lon2 = radians(point2.x)
  118. lat1 = radians(point1.y)
  119. lat2 = radians(point2.y)
  120. # Haversine formula
  121. dlon = lon2 - lon1
  122. dlat = lat2 - lat1
  123. a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
  124. c = 2 * asin(sqrt(a))
  125. r = 6371000 # Radius of earth in meters. Use 3956 for miles
  126. return c * r
  127. def get_location_name(point: Point) -> str:
  128. 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"
  129. response = requests.get(url)
  130. if response.status_code == 200:
  131. data = response.json()
  132. if data["resourceSets"][0]["estimatedTotal"] > 0:
  133. address = data["resourceSets"][0]["resources"][0]["addressOfLocation"][0]
  134. if address["locality"] and address["neighborhood"]:
  135. txt = f"{address['locality']} {address['neighborhood']}"
  136. else:
  137. txt = address["adminDivision"]
  138. if address["countryIso2"]:
  139. txt += f" {address['countryIso2']}"
  140. return txt
  141. return None
  142. def get_path_color(time: datetime) -> str:
  143. random.seed(int(time.timestamp()))
  144. hue = random.random()
  145. saturation = 0.5 + random.random() / 2
  146. value = 0.5 + random.random() / 2
  147. rgb = colorsys.hsv_to_rgb(hue, saturation, value)
  148. return f"#{int(rgb[0]*255):02x}{int(rgb[1]*255):02x}{int(rgb[2]*255):02x}"
  149. def points_to_blob(markers) -> bytes:
  150. arr = []
  151. for marker in markers:
  152. arr.append({
  153. "lat": marker.location.y,
  154. "lng": marker.location.x,
  155. "alt": marker.alt,
  156. "hdop": marker.hdop,
  157. "speed": marker.speed,
  158. "timestamp": marker.timestamp.timestamp(),
  159. })
  160. data = json.dumps(arr).encode('utf-8')
  161. return gzip.compress(data)