points2trips.py 6.5 KB

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