Ver Fonte

precalculate trips

subDesTagesMitExtraKaese há 2 anos atrás
pai
commit
c8c36aab3b

+ 2 - 1
admin.py

@@ -9,7 +9,8 @@ class MarkerAdmin(OSMGeoAdmin):
 
 @admin.register(Trip)
 class TripAdmin(admin.ModelAdmin):
-  list_display = ["startTime", "endTime", "name", "description"]
+  list_display = ["name", "description", "startTime", "endTime", "color", "distance", "topSpeed", "avgSpeed"]
+  readonly_fields = ["path", "distance", "topSpeed", "avgSpeed", "ascendHeight", "descendHeight", "movementTime"]
 
 @admin.register(CensoredLocation)
 class CensoredLocationAdmin(OSMGeoAdmin):

+ 5 - 5
migrations/0006_marker_alt_alter_marker_location.py

@@ -17,9 +17,9 @@ class Migration(migrations.Migration):
             field=models.FloatField(default=0, verbose_name='Altitude'),
             preserve_default=False,
         ),
-        migrations.AlterField(
-            model_name='marker',
-            name='location',
-            field=django.contrib.gis.db.models.fields.PointField(srid=4326),
-        ),
+        # migrations.AlterField(
+        #     model_name='marker',
+        #     name='location',
+        #     field=django.contrib.gis.db.models.fields.PointField(srid=4326),
+        # ),
     ]

+ 56 - 0
migrations/0008_trip_ascendheight_trip_avgspeed_trip_color_and_more.py

@@ -0,0 +1,56 @@
+# Generated by Django 4.1.2 on 2022-10-31 10:42
+
+import colorfield.fields
+import datetime
+import django.contrib.gis.db.models.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('gps_logger', '0007_alter_marker_speed'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='trip',
+            name='ascendHeight',
+            field=models.FloatField(default=0, verbose_name='Ascend height in m'),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='avgSpeed',
+            field=models.FloatField(default=0, verbose_name='Average speed in km/h'),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='color',
+            field=colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=18, samples=None),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='descendHeight',
+            field=models.FloatField(default=0, verbose_name='Descend height in m'),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='distance',
+            field=models.FloatField(default=0, verbose_name='Distance in km'),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='movementTime',
+            field=models.DurationField(default=datetime.timedelta(0), verbose_name='Movement time'),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='path',
+            field=models.BinaryField(default=b'', verbose_name='Path'),
+        ),
+        migrations.AddField(
+            model_name='trip',
+            name='topSpeed',
+            field=models.FloatField(default=0, verbose_name='Top speed in km/h'),
+        ),
+    ]

+ 22 - 0
migrations/0009_alter_marker_options_alter_trip_options_and_more.py

@@ -0,0 +1,22 @@
+# Generated by Django 4.1.2 on 2022-10-31 18:50
+
+import django.contrib.gis.db.models.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('gps_logger', '0008_trip_ascendheight_trip_avgspeed_trip_color_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='marker',
+            options={'ordering': ('timestamp',)},
+        ),
+        migrations.AlterModelOptions(
+            name='trip',
+            options={'ordering': ('startTime',)},
+        ),
+    ]

+ 19 - 1
models.py

@@ -1,8 +1,10 @@
 import math
+from datetime import datetime, timedelta
+
 from django.db import models
 from django.contrib.gis.db.models.fields import PointField
-from django.contrib.gis.db.models.functions import Distance
 from django.contrib.gis.measure import D
+from colorfield.fields import ColorField
 
 class Marker(models.Model):
   timestamp = models.DateTimeField(unique=True)
@@ -11,11 +13,27 @@ class Marker(models.Model):
   hdop = models.IntegerField(null=True, blank=True)
   speed = models.FloatField("Speed in km/h", null=True, blank=True)
 
+  class Meta:
+    ordering = ('timestamp',)
+
 class Trip(models.Model):
   startTime = models.DateTimeField()
   endTime = models.DateTimeField()
   name = models.CharField(max_length=255)
   description = models.TextField(null=True, blank=True)
+  color = ColorField()
+  distance = models.FloatField("Distance in m", default=0)
+  topSpeed = models.FloatField("Top speed in km/h", default=0)
+  avgSpeed = models.FloatField("Average speed in km/h", default=0)
+  ascendHeight = models.FloatField("Ascend height in m", default=0)
+  descendHeight = models.FloatField("Descend height in m", default=0)
+  movementTime = models.DurationField("Movement time", default=timedelta(0))
+
+  path = models.BinaryField("Path", default=bytes())
+
+  class Meta:
+    ordering = ('startTime',)
+
 
 class CensoredLocation(models.Model):
   location = PointField(dim=2)

+ 234 - 235
static/gps_logger/js/main.js

@@ -1,68 +1,68 @@
 const selColor = "#FF5C26";
 let d = [{
-  label:"Altitude,m",
-  data:[], 
-  yaxis: 2
+	label:"Altitude,m",
+	data:[], 
+	yaxis: 2
 },
 {
-  label:"Speed,km/h",
-  data:[]
+	label:"Speed,km/h",
+	data:[]
 }];
 
 let options = {
-  xaxis: {
-    mode: "time",
-    tickLength: 5,
-    zoomRange: [60 * 1000, 1000 * 3600 * 24 * 365]
-  },
-  /*selection: {
-    mode: "x",
-    color: selColor
-  },*/
-  yaxes: [{
-    min: 0,
-    zoomRange: false,
-    panRange: false
-  }, {
-    position: "right",
-    alignTicksWithAxis: 1,
-    min: 0,
-    zoomRange: false,
-    panRange: false
-  }],
-  zoom: {
-    interactive: true
-  },
-  pan: {
-    interactive: true,
-    cursor: "move",
-    frameRate: 60
-  }
+	xaxis: {
+		mode: "time",
+		tickLength: 5,
+		zoomRange: [60 * 1000, 1000 * 3600 * 24 * 365]
+	},
+	/*selection: {
+		mode: "x",
+		color: selColor
+	},*/
+	yaxes: [{
+		min: 0,
+		zoomRange: false,
+		panRange: false
+	}, {
+		position: "right",
+		alignTicksWithAxis: 1,
+		min: 0,
+		zoomRange: false,
+		panRange: false
+	}],
+	zoom: {
+		interactive: true
+	},
+	pan: {
+		interactive: true,
+		cursor: "move",
+		frameRate: 60
+	}
 };
 let optO = {
-  legend : {
-    show: false
-  },
-  series: {
-    lines: {
-      show: true,
-      lineWidth: 1
-    },
-    shadowSize: 0
-  },
-  xaxis: {
-    //ticks: [],
-    mode: "time"
-  },
-  yaxis: {
-    ticks: [],
-    autoscaleMargin: 0.1
-  },
-  selection: {
-    mode: "x",
-    color: selColor,
-    minSize: 0
-  }
+	legend : {
+		show: false
+	},
+	series: {
+		lines: {
+			show: true,
+			lineWidth: 1
+		},
+		shadowSize: 0
+	},
+	xaxis: {
+		//ticks: [],
+		mode: "time"
+	},
+	yaxis: {
+		ticks: [],
+		autoscaleMargin: 0.1
+	},
+	selection: {
+		mode: "x",
+		color: selColor,
+		minSize: 0
+	}
 };
 
 let gps = new GPS();
@@ -70,7 +70,7 @@ let plot, overview;
 
 window.onload = function() {
 	Cesium.Ion.defaultAccessToken = '<token>';
-  
+	
   var extent = {west: -0.2540382220862719, south: 0.6872565916005104, east: 0.6129855406042352, north: 0.9377043806513488};
 
   Cesium.Camera.DEFAULT_VIEW_RECTANGLE = extent;
@@ -83,15 +83,15 @@ window.onload = function() {
   });
 
   viewer = new Cesium.Viewer('map', {
-    fullscreenElement: "map",
-    terrainProvider : Cesium.createWorldTerrain({
-      requestVertexNormals: true,
-      requestWaterMask: true
-    }),
-    //shadows: true
-  });
-  viewer.scene.globe.enableLighting = true;
-  viewer.resolutionScale = 1.2;
+		fullscreenElement: "map",
+		terrainProvider : Cesium.createWorldTerrain({
+			requestVertexNormals: true,
+			requestWaterMask: true
+		}),
+		//shadows: true
+	});
+	viewer.scene.globe.enableLighting = true;
+	viewer.resolutionScale = 1.2;
   viewer.scene.screenSpaceCameraController.enableTilt = !('ontouchstart' in window);
   viewer.baseLayerPicker.viewModel.selectedImagery = new Cesium.ProviderViewModel({
     name: 'Bing Maps Aerial with Labels',
@@ -101,47 +101,46 @@ window.onload = function() {
       return bingMapsProvider
     }
   });
-  var paths = [];
+	var paths = [];
 
-  
-  fetch("markers")
-  .then(function(response) {
-    return response.json();
-  })
-  .then(function(jsonResponse) {
-    gps.parse(jsonResponse);
-    for(const t of gps.tracks) {
-      const i = t.info;
-      var desc = "Start: <b>" + timeFormat(i.startTime, "datetime") + 
-            "</b><br/>Finish: <b>" + timeFormat(i.endTime, "datetime") + "</b><br/>" +
-            "Track time (full): "+ timeFormat(i.totalTime) + "<br>" +
-            "Track time (mov.): "+ timeFormat(i.movTime) + "<br>" +
-            "Alt: " + i.totalAscend.toFixed(1) + "m &uarr;" + i.ascend.toFixed(1) + "m  &darr;" + i.descend.toFixed(1) + "m"+
-            "<br/>Distance: "+(i.distance/1000).toFixed(1)+"km<br/>" +
-            "top speed: "+i.topSpeed.toFixed(1)+
-            "km/h<br/>"+
-            "average speed: "+i.avgSpeed.toFixed(1)+"km/h<br/>";
+	
+	fetch("trips")
+	.then(function(response) {
+		return response.json();
+	})
+	.then(function(jsonResponse) {
+		gps.parse(jsonResponse);
+		for(const t of gps.trips) {
+			var desc = "Start: <b>" + timeFormat(t.startTime, "datetime") + 
+						"</b><br/>Finish: <b>" + timeFormat(t.endTime, "datetime") + "</b><br/>" +
+						"Track time (full): "+ timeFormat(t.totalTime) + "<br>" +
+						"Track time (mov.): "+ timeFormat(t.movementTime) + "<br>" +
+						"Alt: &uarr;" + t.ascendHeight.toFixed(1) + "m  &darr;" + t.descendHeight.toFixed(1) + "m"+
+						"<br/>Distance: "+(t.distance/1000).toFixed(1)+"km<br/>" +
+						"top speed: "+t.topSpeed.toFixed(1)+
+						"km/h<br/>"+
+						"average speed: "+t.avgSpeed.toFixed(1)+"km/h<br/>";
 
-      const col = Cesium.Color.fromCssColorString(i.color)
-      var path = viewer.entities.add({
-        label : "Track "+i.id,
-        id: "track_"+i.id,
+			const col = Cesium.Color.fromCssColorString(t.color)
+			var path = viewer.entities.add({
+				label : "Track "+t.id,
+        id: "track_"+t.id,
         description: desc,
-        polyline : {
-          positions : t.data.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat, p.alt)),
-          width : 5,
-          material : new Cesium.PolylineOutlineMaterialProperty({
-            color : col,
-          }),
-          //clampToGround: true,
-          depthFailMaterial: new Cesium.PolylineOutlineMaterialProperty({
-            color : Cesium.Color.fromAlpha(col, 0.6),
-          }),
-          shadows: Cesium.ShadowMode.ENABLED
-        }
-      });
-      paths.push(path);
-    }
+				polyline : {
+					positions : t.path.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat, p.alt)),
+					width : 5,
+					material : new Cesium.PolylineOutlineMaterialProperty({
+						color : col,
+					}),
+					//clampToGround: true,
+					depthFailMaterial: new Cesium.PolylineOutlineMaterialProperty({
+						color : Cesium.Color.fromAlpha(col, 0.6),
+					}),
+					shadows: Cesium.ShadowMode.ENABLED
+				}
+			});
+			paths.push(path);
+		}
     var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
     handler.setInputAction(function (movement) {
       var pick = viewer.scene.pick(movement.position);
@@ -153,138 +152,138 @@ window.onload = function() {
     }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
 
     viewer.flyTo(paths[paths.length - 1], { offset: new Cesium.HeadingPitchRange(0, -90, 0)});
-    let regionIndex = 0;
-    for(let r=0; r<gps.regions.length; r++) {
+		let regionIndex = 0;
+		for(let r=0; r<gps.regions.length; r++) {
       setTimeout(addInfo, (gps.regions.length-1-regionIndex) * 300, bingMapsProvider, viewer, regionIndex, gps.regions[r]);
-      regionIndex++;
-    }
-    const i = gps.getInfo();
-    var html =
-      "Time (full): "+ timeFormat(i.totalTime) + "<br>" +
-      "Time (mov.): "+ timeFormat(i.movTime) + "<br>" +
-      "Alt: &uarr;" + (i.ascend/1000).toFixed(1) + "km  &darr;" + (i.descend/1000).toFixed(1) + "km<br>"+
-      "Distance: <b>"+(i.distance/1000).toFixed(1)+"km</b><br/>" +
-      "Top speed: <b>"+i.topSpeed.toFixed(1) + "km/h</b><br/>"+
-      "Avg. speed: "+i.avgSpeed.toFixed(1)+"km/h<br/>"+
-      "Data points: "+i.points+"<br/>"+
-      "<a href='javascript:show(" + (gps.tracks.length-1) + ")'>Graph</a>";
-    
-    $(".status").append(html);
+			regionIndex++;
+		}
+		const i = gps.getInfo();
+		var html =
+			"Time (full): "+ timeFormat(i.totalTime) + "<br>" +
+			"Time (mov.): "+ timeFormat(i.movementTime) + "<br>" +
+			"Alt: &uarr;" + (i.ascendHeight).toFixed(1) + "m  &darr;" + (i.descendHeight).toFixed(1) + "m<br>"+
+			"Distance: <b>"+(i.distance/1000).toFixed(1)+"km</b><br/>" +
+			"Top speed: <b>"+i.topSpeed.toFixed(1) + "km/h</b><br/>"+
+			"Avg. speed: "+i.avgSpeed.toFixed(1)+"km/h<br/>"+
+			"Data points: "+i.points+"<br/>"+
+			"<a href='javascript:show(" + (gps.trips.length-1) + ")'>Graph</a>";
+		
+		$(".status").append(html);
 
-  });
+	});
 
-  plot = $.plot("#placeholder", d, options);
-  overview = $.plot("#overview", d, optO);
-  // now connect the two
+	plot = $.plot("#placeholder", d, options);
+	overview = $.plot("#overview", d, optO);
+	// now connect the two
 
-  $("#placeholder").bind("plotpan", updateOverview);
-  $("#placeholder").bind("plotzoom", updateOverview);
-  function updateOverview(event, ranges) {
-    var axes = plot.getAxes();
-    for(var axis in axes) {
-      axes[axis].from = axes[axis].min;
-      axes[axis].to = axes[axis].max;
-    }
-    overview.setSelection(axes, true);
-  }
-  $("#overview").bind("plotselected", function (event, ranges) {
-    $.each(plot.getXAxes(), function(_, axis) {
-      var opts = axis.options;
-      opts.min = ranges.xaxis.from;
-      opts.max = ranges.xaxis.to;
-    });
-    plot.setupGrid();
-    plot.draw();
-  });
-  
-  $("#whole").click(function () {
-    setZoom(false,false);
-  });
-  $("#right").click(function () {
-    var min = plot.getXAxes()[0].options.min;
-    var max = plot.getXAxes()[0].options.max;
-    if(min != null && max != null)
-      setZoom((min+max)/2, max * 1.5 - min/2);
-  });
-  $("#left").click(function () {
-    var min = plot.getXAxes()[0].options.min;
-    var max = plot.getXAxes()[0].options.max;
-    if(min != null && max != null)
-      setZoom(min * 1.5 - max/2, (min+max)/2);
-  });
+	$("#placeholder").bind("plotpan", updateOverview);
+	$("#placeholder").bind("plotzoom", updateOverview);
+	function updateOverview(event, ranges) {
+		var axes = plot.getAxes();
+		for(var axis in axes) {
+			axes[axis].from = axes[axis].min;
+			axes[axis].to = axes[axis].max;
+		}
+		overview.setSelection(axes, true);
+	}
+	$("#overview").bind("plotselected", function (event, ranges) {
+		$.each(plot.getXAxes(), function(_, axis) {
+			var opts = axis.options;
+			opts.min = ranges.xaxis.from;
+			opts.max = ranges.xaxis.to;
+		});
+		plot.setupGrid();
+		plot.draw();
+	});
+	
+	$("#whole").click(function () {
+		setZoom(false,false);
+	});
+	$("#right").click(function () {
+		var min = plot.getXAxes()[0].options.min;
+		var max = plot.getXAxes()[0].options.max;
+		if(min != null && max != null)
+			setZoom((min+max)/2, max * 1.5 - min/2);
+	});
+	$("#left").click(function () {
+		var min = plot.getXAxes()[0].options.min;
+		var max = plot.getXAxes()[0].options.max;
+		if(min != null && max != null)
+			setZoom(min * 1.5 - max/2, (min+max)/2);
+	});
 }
 
 let firstShow = true;
 
 function show(id) {
-  if(firstShow) {
-    firstShow = false;
-    
-    d[0].data = smoothOut(gps.data.map(v => v.alt), 0.85);
-    d[1].data = smoothOut(gps.data.map(v => v.speed), 0.85);
-    
-    for(let i=0; i<gps.data.length; i++) {
-      const cur = gps.data[i];
-      if((cur.timeDiff > 60 && cur.speed < 0.5) || cur.speed === null) {
-        d[0].data[i] = null;
-        d[1].data[i] = null;
-      }
-      d[0].data[i] = [Date.parse(cur.timestamp), d[0].data[i]];
-      d[1].data[i] = [Date.parse(cur.timestamp), d[1].data[i]];
-    }
-    let opts = plot.getXAxes()[0].options;
-    opts.panRange = [
-      Date.parse(gps.data[0].timestamp),
-      Date.parse(gps.data[gps.data.length-1].timestamp)
-    ];
-    
-    plot.setData(d);
-    plot.setupGrid(); //only necessary if your new data will change the axes or grid
-    plot.draw();
-    overview.setData(d);
-    overview.setupGrid(); //only necessary if your new data will change the axes or grid
-    overview.draw();
-    
-    for(let t of gps.tracks) {
-      $("#tracks").append("<li><input type='button' onclick='setZoom("+t.info.startTime*1000+","+t.info.endTime*1000+")' value='" + timeFormat(t.info.startTime, "date") + " " + (t.info.distance/1000).toFixed(1) + "km'</li>");
-    }
-  }
-  
-  $("#shadow").css("visibility", "visible");
-  $("#frame").css("visibility", "visible");
-  console.log(id);
-  
-  let start = gps.tracks[id].info.startTime*1000;
-  let end = gps.tracks[id].info.endTime*1000;
-  
-  setZoom(start,end);
-  
-  $("#track").click(function () {
-    setZoom(start,end);
-  });
+	if(firstShow) {
+		firstShow = false;
+		
+		d[0].data = smoothOut(gps.data.map(v => v.alt), 0.85);
+		d[1].data = smoothOut(gps.data.map(v => v.speed), 0.85);
+		
+		for(let i=0; i<gps.data.length; i++) {
+			const cur = gps.data[i];
+			if((cur.timeDiff > 60 && cur.speed < 0.5) || cur.speed === null) {
+				d[0].data[i] = null;
+				d[1].data[i] = null;
+			}
+			d[0].data[i] = [cur.timestamp*1000, d[0].data[i]];
+			d[1].data[i] = [cur.timestamp*1000, d[1].data[i]];
+		}
+		let opts = plot.getXAxes()[0].options;
+		opts.panRange = [
+			gps.data[0].timestamp*1000,
+			gps.data[gps.data.length-1].timestamp*1000
+		];
+		
+		plot.setData(d);
+		plot.setupGrid(); //only necessary if your new data will change the axes or grid
+		plot.draw();
+		overview.setData(d);
+		overview.setupGrid(); //only necessary if your new data will change the axes or grid
+		overview.draw();
+		
+		for(let t of gps.trips) {
+			$("#tracks").append("<li><input type='button' onclick='setZoom("+t.startTime*1000+","+t.endTime*1000+")' value='" + timeFormat(t.startTime, "date") + " " + (t.distance/1000).toFixed(1) + "km'</li>");
+		}
+	}
+	
+	$("#shadow").css("visibility", "visible");
+	$("#frame").css("visibility", "visible");
+	console.log(id);
+	
+	let start = gps.trips[id].startTime*1000;
+	let end = gps.trips[id].endTime*1000;
+	
+	setZoom(start,end);
+	
+	$("#track").click(function () {
+		setZoom(start,end);
+	});
 };
 function hide() {
-  $(".popup").css("visibility", "hidden");
+	$(".popup").css("visibility", "hidden");
 };
 function setZoom(Xmin, Xmax) {
-  var opts = plot.getXAxes()[0].options;
-  if(Xmax==false) {
-    Xmax = +new Date() - 60 * new Date().getTimezoneOffset() * 1000;
-  }
-  if(Xmin==false) {
-    Xmin = d[0].data[0][0];
-    overview.clearSelection();
-  } else {
-    overview.setSelection({ xaxis: { from: Xmin, to: Xmax}});
-  }
-  opts.min=Xmin;
-  opts.max=Xmax;
+	var opts = plot.getXAxes()[0].options;
+	if(Xmax==false) {
+		Xmax = +new Date() - 60 * new Date().getTimezoneOffset() * 1000;
+	}
+	if(Xmin==false) {
+		Xmin = d[0].data[0][0];
+		overview.clearSelection();
+	} else {
+		overview.setSelection({ xaxis: { from: Xmin, to: Xmax}});
+	}
+	opts.min=Xmin;
+	opts.max=Xmax;
 
-  plot.setupGrid();
-  plot.draw();
-  plot.clearSelection();
+	plot.setupGrid();
+	plot.draw();
+	plot.clearSelection();
 
-  return false;
+	return false;
 }
 
 function addInfo(bingMapsProvider, viewer, id, data) {
@@ -292,7 +291,7 @@ function addInfo(bingMapsProvider, viewer, id, data) {
   fetch(url).then(res=>res.json()).then(function(result) {
     if (result.statusDescription === 'OK') {
       const address = result?.resourceSets[0]?.resources[0]?.addressOfLocation[0];
-      if (address) {
+			if (address) {
 
         let txt = address.locality + " " + address.neighborhood;
         if (!address.locality && !address.neighborhood) {
@@ -302,22 +301,22 @@ function addInfo(bingMapsProvider, viewer, id, data) {
           txt += " " + address.countryIso2;
         }
 
-        $('.sidebar').append("<div class='info' id='info_" + id + "'>" + 
-          timeFormat(data.timestamp, "short") + "<br>" +
+				$('.sidebar').append("<div class='info' id='info_" + id + "'>" + 
+					timeFormat(data.timestamp, "short") + "<br>" +
           txt + "<br><b>" +
-          (data.distance/1000).toFixed(1) + " km</b></div>");
-          
-        $('#info_'+id).click(function() {
+					(data.distance/1000).toFixed(1) + " km</b></div>");
+					
+				$('#info_'+id).click(function() {
           viewer.camera.flyTo({
             destination: Cesium.Cartesian3.fromDegrees(data.lng, data.lat, data.distance),
             offset: new Cesium.HeadingPitchRange(0, -90, 0)
           });
-        });
-      } else {
-        console.log('No results found');
-      }
-    } else {
-      console.log('Geocoder failed due to: ' + status);
-    }
-  });
+				});
+			} else {
+				console.log('No results found');
+			}
+		} else {
+			console.log('Geocoder failed due to: ' + result.statusDescription);
+		}
+	});
 }

+ 22 - 1
static/gps_logger/js/preprocessor.js

@@ -78,7 +78,7 @@ window.onload = function() {
     xmlhttp.send(JSON.stringify(markersToUpload))
   })
 
-  const censorBtn = document.getElementById("censor-btn");
+  const censorBtn = document.getElementById("censor-btn")
   censorBtn.addEventListener('click', (event) => {
     censorBtn.disabled = true;
     responseP.textContent = "waiting for response..."
@@ -100,6 +100,27 @@ window.onload = function() {
     xmlhttp.send()
   })
 
+  const tripBtn = document.getElementById("trip-btn")
+  tripBtn.addEventListener('click', (event) => {
+    tripBtn.disabled = true;
+    responseP.textContent = "waiting for response..."
+    const xmlhttp = new XMLHttpRequest()
+    xmlhttp.open("GET", "markers/create-trips", true)
+    xmlhttp.onreadystatechange = () => {
+      if(xmlhttp.readyState == 4) {
+        if(xmlhttp.status < 300) {
+          responseP.textContent = "Trips created!"
+          const body = JSON.parse(xmlhttp.responseText)
+          for(const field in body)
+            responseP.innerHTML += `<br/>${field}: ${body[field]}`
+        }
+        else
+          responseP.textContent = `Error code ${xmlhttp.status}`
+          tripBtn.disabled = false
+      }
+    }
+    xmlhttp.send()
+  })
 }
 function importFiles(fileList) {
   d = []

+ 35 - 142
static/gps_logger/js/tracks.js

@@ -1,78 +1,39 @@
 
 class GPS {
-  parse(data) {
-    this.data = data;
-    this.index = 0;
-    
-    this.maxTimeDiff = 3600 * 6;
-    this.maxDist = 3000;
-    
-    this.tracks = [];
-    let t;
-    while(t = gps.next()) {
-      if (t.length > 1)
-        this.tracks.push(new Track(t));
+  parse(trips) {    
+    this.trips = trips
+    for(const trip of this.trips) {
+      trip.totalTime = Math.abs(trip.endTime - trip.startTime)
+      trip.minLat = trip.path.reduce((min, p) => p.lat < min ? p.lat : min, trip.path[0].lat)
+      trip.maxLat = trip.path.reduce((max, p) => p.lat > max ? p.lat : max, trip.path[0].lat)
+      trip.minLng = trip.path.reduce((min, p) => p.lng < min ? p.lng : min, trip.path[0].lng)
+      trip.maxLng = trip.path.reduce((max, p) => p.lng > max ? p.lng : max, trip.path[0].lng)
+      trip.center = trip.path[parseInt(trip.path.length/2)]
     }
-    this.regions = this.getRegions();
-  }
-  next() {
-    if(this.index >= this.data.length)
-      return;
-    let points = [];
-    while(this.index < this.data.length - 1) {
-      const cur = this.data[this.index];
-      const nxt = this.data[this.index+1];
-      
-      cur.timeDiff = Date.parse(nxt.timestamp) / 1000 - Date.parse(cur.timestamp) / 1000;
-      cur.distance = distance(cur, nxt);
-      
-      if(isNaN(cur.distance))
-        cur.distance = 0;
-      
-      if(cur.speed <= 0) {
-        cur.speed = cur.distance / cur.timeDiff * 3.6; // km/h
-      }
-      
-      if(cur.timeDiff > this.maxTimeDiff || cur.distance > this.maxDist) {
-        cur.distance = null;
-        cur.timeDiff = null;
-        cur.speed = null;
-        break;
-      }
-      
-      points.push(cur);
-      this.index++;
-    }
-    if(this.index == this.data.length - 1) {
-      this.data[this.index].distance = 0;
-      this.data[this.index].timeDiff = 0;
-      this.data[this.index].speed = 0;
-    }
-    points.push(this.data[this.index]);
-    this.index++;
-    
-    return points;
+    this.regions = this.getRegions()
+    this.data = this.trips.map(t => t.path).reduce((a, b) => a.concat(b), [])
   }
+
   getRegions() {
     var trips = [];
     var s = 0;
-    for(let i=0; i<=this.tracks.length; i++) {
-      while(i < this.tracks.length - 2) {
-        const cur = this.tracks[i].info;
-        const nxt = this.tracks[i+1].info;
+    for(let i=0; i<=this.trips.length; i++) {
+      while(i < this.trips.length - 2) {
+        const cur = this.trips[i];
+        const nxt = this.trips[i+1];
         if((nxt.startTime - cur.endTime) > 3600 * 24 * 7 || distance(cur.center, nxt.center) > 100000) {
           break;
         }
         i++;
       }
       
-      let trip = this.tracks.slice(s, i);
-      let dist = trip.reduce((sum, p) => p.info.distance + sum, 0);
+      let trip = this.trips.slice(s, i);
+      let dist = trip.reduce((sum, p) => p.distance + sum, 0);
       if (dist > 30000) {
         trips.push({
-          lat: median(trip.map(v => v.info.center.lat)),
-          lng: median(trip.map(v => v.info.center.lng)),
-          timestamp: median(trip.map(v => v.info.startTime)),
+          lat: median(trip.map(v => v.center.lat)),
+          lng: median(trip.map(v => v.center.lng)),
+          timestamp: median(trip.map(v => v.startTime)),
           distance: dist
         });
       }
@@ -82,89 +43,18 @@ class GPS {
   }
   getInfo() {
     let i = {
-      points: this.data.length,
-      distance: this.tracks.reduce((sum, p) => p.info.distance + sum, 0),
-      topSpeed: this.tracks.reduce((max, p) => p.info.topSpeed > max ? p.info.topSpeed : max, 0),
-      ascend: this.tracks.reduce((sum, p) => p.info.ascend + sum, 0),
-      descend: this.tracks.reduce((sum, p) => p.info.descend + sum, 0),
-      movTime: this.tracks.reduce((sum, p) => p.info.movTime + sum, 0),
-      totalTime: this.tracks.reduce((sum, p) => p.info.totalTime + sum, 0)
+      points: this.trips.reduce((sum, p) => p.path.length + sum, 0),
+      distance: this.trips.reduce((sum, p) => p.distance + sum, 0),
+      topSpeed: this.trips.reduce((max, p) => p.topSpeed > max ? p.topSpeed : max, 0),
+      ascendHeight: this.trips.reduce((sum, p) => p.ascendHeight + sum, 0),
+      descendHeight: this.trips.reduce((sum, p) => p.descendHeight + sum, 0),
+      movementTime: this.trips.reduce((sum, p) => p.movementTime + sum, 0),
+      totalTime: this.trips.reduce((sum, p) => p.totalTime + sum, 0)
     };
-    i.avgSpeed = i.distance / i.movTime * 3.6;
+    i.avgSpeed = i.distance / i.movementTime * 3.6;
     return i;
   }
 }
-let trackId = 0;
-
-class Track {
-  constructor(data) {
-    this.data = data;
-    this.id = trackId++;
-    
-    this.deleted = this.clean();
-    this.info = this.getInfo();
-  }
-  
-  clean() {
-    let last = this.data[this.data.length-2];
-    let speed = last.speed, alt = last.alt;
-    let res = [];
-    for(let i=this.data.length-2; i>=0; i--) {
-      let cur = this.data[i];
-      let lst = this.data[i+1];
-      if(Math.abs(cur.speed - lst.speed) / cur.timeDiff > 10 || Math.abs(cur.speed - speed) > 50) {
-        res.push(this.data.splice(i, 1)[0]);
-      } else
-      speed = speed * 0.9 + cur.speed * 0.1;
-      alt = alt * 0.9 + cur.alt * 0.1;
-    }
-    return res;
-  }
-  getInfo() {
-    let i = {
-      id: this.id,
-      points: this.data.length,
-      color: getRandomColor(this.data[0].timestamp),
-      distance: this.data.reduce((sum, p) => p.distance + sum, 0),
-      topSpeed: this.data.reduce((max, p) => p.speed > max ? p.speed : max, this.data[0].speed),
-      ascend: 0,
-      descend: 0,
-      startTime: Date.parse(this.data[0].timestamp) / 1000,
-      endTime: Date.parse(this.data[this.data.length-1].timestamp) / 1000,
-      movTime: this.data.reduce((sum, p) => p.speed > 2 ? p.timeDiff + sum : sum, 0),
-      minLat: this.data.reduce((min, p) => p.lat < min ? p.lat : min, this.data[0].lat),
-      maxLat: this.data.reduce((max, p) => p.lat > max ? p.lat : max, this.data[0].lat),
-      minLng: this.data.reduce((min, p) => p.lng < min ? p.lng : min, this.data[0].lng),
-      maxLng: this.data.reduce((max, p) => p.lng > max ? p.lng : max, this.data[0].lng),
-      
-    };
-    for(let n=1; n<this.data.length; n++) {
-      const cur = this.data[n].alt;
-      const lst = this.data[n-1].alt;
-      if(cur > lst)
-        i.ascend  += cur - lst;
-      else
-        i.descend += lst - cur;
-    }
-    i.totalAscend = i.ascend - i.descend;
-    i.avgSpeed = i.distance / i.movTime * 3.6;
-    i.center = this.data[parseInt(this.data.length/2)];
-    i.totalTime = i.endTime - i.startTime;
-    
-    return i;
-  }
-  
-}
-
-function getRandomColor(str) {
-  let arr = str.split('');
-  let i = Math.abs(arr.reduce(
-    (hashCode, currentVal) =>
-      (hashCode = currentVal.charCodeAt(0) + (hashCode << 6) + (hashCode << 16) - hashCode),
-    0
-  ));
-  return "hsl(" + (i * 2 % 360) + ", " + (i * 3 % 60 + 40) + "%, " + (i * 5 % 40 + 30) + "%)";
-}
 function distance (a, b) {
  
   // Convert degrees to radians
@@ -199,7 +89,10 @@ function distance (a, b) {
   return r * theta;
 }
 
-function timeFormat (sec, x = "time") {
+function timeFormat(sec, x = "time") {
+  if(isNaN(sec))
+    return "NaN";
+
   let d = new Date(sec*1000);
   let t = new Date(null,null,null,null,null,sec).toTimeString().match(/\d{2}:\d{2}:\d{2}/)[0];
   
@@ -239,11 +132,11 @@ function median(values) {
 
   return values[half];
 }
-function avg (v) {
+function avg(v) {
   return v.reduce((a,b) => a+b, 0)/v.length;
 }
 
-function smoothOut (vector, variance) {
+function smoothOut(vector, variance) {
   var t_avg = avg(vector)*variance;
   var ret = Array(vector.length);
   for (var i = 0; i < vector.length; i++) {

+ 1 - 0
templates/gps_logger/upload.html

@@ -41,6 +41,7 @@
   <p>
     <input type="submit" id="upload-btn" style="display: none"/>
     <input type="button" id="censor-btn" value="Delete censored"/>
+    <input type="button" id="trip-btn" value="Create trips"/>
   </p>
   <p id="response"></p>
 </div>

+ 3 - 1
urls.py

@@ -7,5 +7,7 @@ urlpatterns = [
   path('upload', views.upload, name='upload'),
   path('markers/create', views.MarkerCreateView.as_view(), name='marker-create'),
   path('markers/censor', views.DeleteCensoredView.as_view(), name='marker-censor'),
-  path('markers', views.marker_view, name='marker')
+  path('markers/create-trips', views.CreateTripsView.as_view(), name='marker-censor'),
+  path('markers', views.marker_view, name='marker'),
+  path('trips', views.trip_view, name='trips')
 ]

+ 30 - 0
views.py

@@ -1,5 +1,7 @@
 import json
+import gzip
 from datetime import datetime
+
 from django.shortcuts import render
 from django.http import JsonResponse, HttpResponse
 from django.core.serializers.json import DjangoJSONEncoder
@@ -10,6 +12,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.gis.geos import Point
 
 from .models import *
+from .workers.points2trips import TripConverter
 
 def index(request):
   return render(request, 'gps_logger/index.html')
@@ -63,6 +66,20 @@ class MarkerCreateView(PermissionRequiredMixin, View):
         'inserted': insert_count - censor_count
       }, status=201)
 
+class CreateTripsView(PermissionRequiredMixin, View):
+  permission_required = ("gps-logger.trips.change")
+
+  def get(self, request):
+    data = list(Marker.objects.all())
+
+    tc = TripConverter(data)
+    tc.save()
+    cache.delete('trips')
+    return JsonResponse({
+        'inserted': len(tc.trips)
+      }, status=201)
+
+
 def marker_view(request):
   data = cache.get('markers')
   if not data:
@@ -77,6 +94,19 @@ def marker_view(request):
     cache.set('markers', data, 3600*24*30)
   return HttpResponse(data, content_type='application/json')
 
+def trip_view(request):
+  data = cache.get('trips')
+  if not data:
+    values = list(Trip.objects.values())
+    for trip in values:
+      trip['path'] = json.loads(gzip.decompress(trip['path']))
+      trip['startTime'] = trip['startTime'].timestamp()
+      trip['endTime'] = trip['endTime'].timestamp()
+      trip['movementTime'] = trip['movementTime'].total_seconds()
+    data = json.dumps(values)
+    cache.set('trips', data, 3600*24*30)
+  return HttpResponse(data, content_type='application/json')
+
 class DeleteCensoredView(PermissionRequiredMixin, View):
   permission_required = ("gps-logger.markers.change",)
   def get(self, request):

+ 145 - 0
workers/points2trips.py

@@ -0,0 +1,145 @@
+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.point import Point
+
+from ..models import Marker, Trip
+
+
+max_time_diff = timedelta(hours=6)
+max_distance = 3000 # m
+
+class TripConverter:
+  trips: list[Trip]
+
+  def __init__(self, markers: list[Marker]):
+    self.trips = []
+    first_index = 0
+    for i, point in enumerate(markers[1:]):
+      prev_point = markers[i-1]
+      if point.timestamp - prev_point.timestamp > max_time_diff or \
+          Distance(point.location, prev_point.location) > max_distance:
+        if i - first_index > 2:
+          self.trips.append(create_trip(markers[first_index:i]))
+        first_index = i
+    
+    if first_index < len(markers) - 2:
+      self.trips.append(create_trip(markers[first_index:]))
+
+  def save(self):
+    for trip in self.trips:
+      trip.save()
+
+def create_trip(markers: list[Marker]) -> Trip:
+  print(len(markers), markers[0].timestamp, markers[-1].timestamp)
+  trip = Trip.objects.filter(startTime__lte=markers[-1].timestamp, endTime__gte=markers[0].timestamp).first()
+  if not trip:
+    trip = Trip.objects.create(
+      startTime = markers[0].timestamp,
+      endTime = markers[-1].timestamp,
+      name = f"Trip {markers[0].timestamp}",
+      color = get_path_color(markers[0].timestamp),
+    )
+  # elif trip.startTime == markers[0].timestamp and trip.endTime == markers[-1].timestamp:
+  #   data = json.loads(gzip.decompress(trip.path))
+  #   if len(data) == len(markers):
+  #     print("Trip already exists")
+  #     return trip
+
+  trip.startTime = markers[0].timestamp
+  trip.endTime = markers[-1].timestamp
+  total_distance = 0 # m
+  topSpeed = 0 # km/h
+  ascendHeight = 0 # m
+  descendHeight = 0 # m
+  movementTime = timedelta(0)
+
+  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)
+    
+    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.path = points_to_blob(markers)
+
+  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_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 convert_points_to_trips():
+  points = list(Marker.objects.all())
+  converter = TripConverter(points)
+  converter.save()
+
+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)