Sfoglia il codice sorgente

add clientside file reader

subDesTagesMitExtraKaese 2 anni fa
parent
commit
2656885ae1

+ 18 - 0
migrations/0002_censoredlocation_radius.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.13 on 2022-08-13 18:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('gps_logger', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='censoredlocation',
+            name='radius',
+            field=models.IntegerField(default=800),
+        ),
+    ]

+ 18 - 0
migrations/0003_rename_location_trip_name.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.13 on 2022-08-13 18:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('gps_logger', '0002_censoredlocation_radius'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='trip',
+            old_name='location',
+            new_name='name',
+        ),
+    ]

+ 23 - 0
static/gps_logger/GPS.js/GPS.min.js

@@ -0,0 +1,23 @@
+/*
+https://github.com/infusion/GPS.js
+
+GPS.js v0.6.1 26/01/2016
+
+Copyright (c) 2016, Robert Eisele (robert@xarg.org)
+Dual licensed under the MIT or GPL Version 2 licenses.
+*/
+(function(x){function k(b,a){if(""===b)return null;var d=new Date;if(a){var c=a.slice(4),f=a.slice(2,4)-1,h=a.slice(0,2);4===c.length?d.setUTCFullYear(Number(c),Number(f),Number(h)):d.setUTCFullYear(Number("20"+c),Number(f),Number(h))}d.setUTCHours(Number(b.slice(0,2)));d.setUTCMinutes(Number(b.slice(2,4)));d.setUTCSeconds(Number(b.slice(4,6)));c=b.slice(7);f=c.length;h=0;0!==f&&(h=parseFloat(c)*Math.pow(10,3-f));d.setUTCMilliseconds(Number(h));return d}function l(b,a){if(""===b)return null;var d=
+1;switch(a){case "S":d=-1;case "N":var c=2;break;case "W":d=-1;case "E":c=3}return d*(parseFloat(b.slice(0,c))+parseFloat(b.slice(c))/60)}function e(b){return""===b?null:parseFloat(b)}function q(b){return""===b?null:1.852*parseFloat(b)}function y(b){if(""===b)return null;switch(parseInt(b,10)){case 0:return null;case 1:return"fix";case 2:return"dgps-fix";case 3:return"pps-fix";case 4:return"rtk";case 5:return"rtk-float";case 6:return"estimated";case 7:return"manual";case 8:return"simulated"}throw Error("INVALID GGA FIX: "+
+b);}function r(b){switch(b){case "A":return"active";case "V":return"void";case "":return null}throw Error("INVALID RMC/GLL STATUS: "+b);}function n(b){switch(b){case "":return null;case "A":return"autonomous";case "D":return"differential";case "E":return"estimated";case "M":return"manual input";case "S":return"simulated";case "N":return"not valid";case "P":return"precise";case "R":return"rtk";case "F":return"rtk-float"}throw Error("INVALID FAA MODE: "+b);}function t(b,a){if("M"===a||""===a)return e(b);
+throw Error("Unknown unit: "+a);}function g(){if(!(this instanceof g))return new g;this.events={};this.state={errors:0,processed:0}}var m=Math.PI/180,p={},u={};g.prototype.events=null;g.prototype.state=null;g.mod={GGA:function(b,a){if(16!==a.length&&14!==a.length)throw Error("Invalid GGA length: "+b);return{time:k(a[1]),lat:l(a[2],a[3]),lon:l(a[4],a[5]),alt:t(a[9],a[10]),quality:y(a[6]),satellites:e(a[7]),hdop:e(a[8]),geoidal:t(a[11],a[12]),age:void 0===a[13]?null:e(a[13]),stationID:void 0===a[14]?
+null:e(a[14])}},GSA:function(b,a){if(19!==a.length&&20!==a.length)throw Error("Invalid GSA length: "+b);for(var d=[],c=3;15>c;c++)""!==a[c]&&d.push(parseInt(a[c],10));a:{c=a[1];switch(c){case "M":c="manual";break a;case "A":c="automatic";break a;case "":c=null;break a}throw Error("INVALID GSA MODE: "+c);}a:{var f=a[2];switch(f){case "1":case "":f=null;break a;case "2":f="2D";break a;case "3":f="3D";break a}throw Error("INVALID GSA FIX: "+f);}return{mode:c,fix:f,satellites:d,pdop:e(a[15]),hdop:e(a[16]),
+vdop:e(a[17]),systemId:19<a.length?e(a[18]):null}},RMC:function(b,a){if(13!==a.length&&14!==a.length&&15!==a.length)throw Error("Invalid RMC length: "+b);var d=k(a[1],a[9]),c=r(a[2]),f=l(a[3],a[4]),h=l(a[5],a[6]),z=q(a[7]),A=e(a[8]),v=a[10],w=a[11];return{time:d,status:c,lat:f,lon:h,speed:z,track:A,variation:""===v||""===w?null:parseFloat(v)*("W"===w?-1:1),faa:13<a.length?n(a[12]):null,navStatus:14<a.length?a[13]:null}},VTG:function(b,a){if(10!==a.length&&11!==a.length)throw Error("Invalid VTG length: "+
+b);if(""===a[2]&&""===a[8]&&""===a[6])return{track:null,trackMagetic:null,speed:null,faa:null};if("T"!==a[2])throw Error("Invalid VTG track mode: "+b);if("K"!==a[8]||"N"!==a[6])throw Error("Invalid VTG speed tag: "+b);return{track:e(a[1]),trackMagnetic:""===a[3]?null:e(a[3]),speed:q(a[5]),faa:11===a.length?n(a[9]):null}},GSV:function(b,a){if(0===a.length%4)throw Error("Invalid GSV length: "+b);for(var d=[],c=4;c<a.length-3;c+=4){var f=e(a[c]),h=e(a[c+3]);d.push({prn:f,elevation:e(a[c+1]),azimuth:e(a[c+
+2]),snr:h,status:null!==f?null!==h?"tracking":"in view":null})}return{msgNumber:e(a[2]),msgsTotal:e(a[1]),satsInView:e(a[3]),satellites:d,signalId:2===a.length%4?e(a[a.length-2]):null}},GLL:function(b,a){if(9!==a.length&&8!==a.length)throw Error("Invalid GLL length: "+b);return{time:k(a[5]),status:r(a[6]),lat:l(a[1],a[2]),lon:l(a[3],a[4]),faa:9===a.length?n(a[7]):null}},ZDA:function(b,a){return{time:k(a[1],a[2]+a[3]+a[4])}},GST:function(b,a){if(10!==a.length)throw Error("Invalid GST length: "+b);
+return{time:k(a[1]),rms:e(a[2]),ellipseMajor:e(a[3]),ellipseMinor:e(a[4]),ellipseOrientation:e(a[5]),latitudeError:e(a[6]),longitudeError:e(a[7]),heightError:e(a[8])}},HDT:function(b,a){if(4!==a.length)throw Error("Invalid HDT length: "+b);return{heading:parseFloat(a[1]),trueNorth:"T"===a[2]}},GRS:function(b,a){if(18!==a.length)throw Error("Invalid GRS length: "+b);for(var d=[],c=3;14>=c;c++){var f=e(a[c]);null!==f&&d.push(f)}return{time:k(a[1]),mode:e(a[2]),res:d}},GBS:function(b,a){if(10!==a.length&&
+12!==a.length)throw Error("Invalid GBS length: "+b);return{time:k(a[1]),errLat:e(a[2]),errLon:e(a[3]),errAlt:e(a[4]),failedSat:e(a[5]),probFailedSat:e(a[6]),biasFailedSat:e(a[7]),stdFailedSat:e(a[8]),systemId:12===a.length?e(a[9]):null,signalId:12===a.length?e(a[10]):null}},GNS:function(b,a){if(14!==a.length&&15!==a.length)throw Error("Invalid GNS length: "+b);return{time:k(a[1]),lat:l(a[2],a[3]),lon:l(a[4],a[5]),mode:a[6],satsUsed:e(a[7]),hdop:e(a[8]),alt:e(a[9]),sep:e(a[10]),diffAge:e(a[11]),diffStation:e(a[12]),
+navStatus:15===a.length?a[13]:null}}};g.Parse=function(b){if("string"!==typeof b)return!1;var a=b.split(","),d=a.pop();if(2>a.length||"$"!==b.charAt(0)||-1===d.indexOf("*"))return!1;d=d.split("*");a.push(d[0]);a.push(d[1]);a[0]=a[0].slice(3);if(void 0!==g.mod[a[0]]){d=this.mod[a[0]](b,a);d.raw=b;for(var c=0,f=1;f<b.length;f++){var h=b.charCodeAt(f);if(42===h)break;c^=h}d.valid=c===parseInt(a[a.length-1],16);d.type=a[0];return d}return!1};g.Heading=function(b,a,d,c){a=(c-a)*m;b*=m;d*=m;c=Math.cos(d);
+return(180*Math.atan2(Math.sin(a)*c,Math.cos(b)*Math.sin(d)-Math.sin(b)*c*Math.cos(a))/Math.PI+360)%360};g.Distance=function(b,a,d,c){var f=(d-b)*m*.5;a=(c-a)*m*.5;b*=m;d*=m;f=Math.sin(f);a=Math.sin(a);return 12745.6*Math.asin(Math.sqrt(f*f+Math.cos(b)*Math.cos(d)*a*a))};g.TotalDistance=function(b){if(2>b.length)return 0;for(var a=0,d=0;d<b.length-1;d++){var c=b[d],f=b[d+1];a+=g.Distance(c.lat,c.lon,f.lat,f.lon)}return a};g.prototype.update=function(b){b=g.Parse(b);this.state.processed++;if(!1===
+b)return this.state.errors++,!1;var a=this.state;if("RMC"===b.type||"GGA"===b.type||"GLL"===b.type||"GNS"===b.type)a.time=b.time,a.lat=b.lat,a.lon=b.lon;"ZDA"===b.type&&(a.time=b.time);"GGA"===b.type&&(a.alt=b.alt);"RMC"===b.type&&(a.speed=b.speed,a.track=b.track);"GSA"===b.type&&(a.satsActive=b.satellites,a.fix=b.fix,a.hdop=b.hdop,a.pdop=b.pdop,a.vdop=b.vdop);if("GSV"===b.type){for(var d=(new Date).getTime(),c=b.satellites,f=0;f<c.length;f++){var h=c[f].g;u[h]=d;p[h]=c[f]}c=[];for(h in p)3E3>d-u[h]&&
+c.push(p[h]);a.satsVisible=c}this.emit("data",b);this.emit(b.type,b);return!0};g.prototype.partial="";g.prototype.updatePartial=function(b){for(this.partial+=b;;){b=this.partial.indexOf("\r\n");if(-1===b)break;var a=this.partial.slice(0,b);if("$"===a.charAt(0))try{this.update(a)}catch(d){throw this.partial="",Error(d);}this.partial=this.partial.slice(b+2)}};g.prototype.on=function(b,a){return void 0===this.events[b]?(this.events[b]=a,this):null};g.prototype.off=function(b){void 0!==this.events[b]&&
+(this.events[b]=void 0);return this};g.prototype.emit=function(b,a){void 0!==this.events[b]&&this.events[b].call(this,a)};"object"===typeof exports?(Object.defineProperty(g,"__esModule",{value:!0}),g["default"]=g,g.GPS=g,module.exports=g):x.GPS=g})(this);

+ 21 - 0
static/gps_logger/GPS.js/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Robert Eisele
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 360 - 0
static/gps_logger/GPS.js/README.md

@@ -0,0 +1,360 @@
+
+![GPS.js](https://github.com/infusion/GPS.js/blob/master/res/logo.png?raw=true "Javascript GPS Parser")
+
+[![NPM Package](https://img.shields.io/npm/v/gps.svg?style=flat)](https://npmjs.org/package/gps "View this project on npm")
+[![Build Status](https://travis-ci.org/infusion/GPS.js.svg?branch=master)](https://travis-ci.org/infusion/GPS.js)
+[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
+
+GPS.js is an extensible parser for [NMEA](http://www.gpsinformation.org/dale/nmea.htm) sentences, given by any common GPS receiver. The output is tried to be as high-level as possible to make it more useful than simply splitting the information. The aim is, that you don't have to understand NMEA, just plug in your receiver and you're ready to go.
+
+
+Usage
+===
+
+The interface of GPS.js is as simple as the following few lines. You need to add an event-listener for the completion of the task and invoke the update method with a sentence you want to process. There are much more examples in the examples folder.
+
+```javascript
+const gps = new GPS;
+
+// Add an event listener on all protocols
+gps.on('data', parsed => {
+    console.log(parsed);
+});
+
+// Call the update routine directly with a NMEA sentence, which would
+// come from the serial port or stream-reader normally
+gps.update("$GPGGA,224900.000,4832.3762,N,00903.5393,E,1,04,7.8,498.6,M,48.0,M,,0000*5E");
+```
+
+It's also possible to add event-listeners only on one of the following protocols, by stating `gps.on('GGA', ...)` for example.
+
+State
+===
+
+The real advantage over other NMEA implementations is, that the GPS information is interpreted and normalized. The most high-level API is the state object, which changes with every new event. You can use this information with:
+
+```javascript
+gps.on('data', () => {
+  console.log(gps.state);
+});
+```
+
+Installation
+===
+Installing GPS.js is as easy as cloning this repo or use the following command:
+
+```
+npm install gps
+```
+
+Find the serial device
+===
+
+On Linux serial devices typically have names like `/dev/ttyS1`, on OSX `/dev/tty.usbmodem1411` after installing a USB to serial driver and on Windows, you're probably fine by using the highest COM device you can find in the device manager. Please note that if you have multiple USB ports on your computer and use them randomly, you have to lookup the path/device again.
+
+Register device on a BeagleBone
+---
+
+If you find yourself on a BeagleBone, the serial device must be registered manually. Luckily, this can be done within node quite easily using [octalbonescript](https://www.npmjs.com/package/octalbonescript):
+
+```javascript
+const obs = require('octalbonescript');
+obs.serial.enable('/dev/ttyS1', () => {  
+    console.log('serial device activated');
+});
+```
+
+Examples
+===
+
+GPS.js comes with some examples, like drawing the current latitude and longitude to Google Maps, displaying a persistent state and displaying the parsed raw data. In some cases you have to adjust the serial path to your own GPS receiver to make it work.
+
+Simple serial example
+---
+
+```javascript
+const SerialPort = require('serialport');
+const GPS = require('gps');
+
+const port = new SerialPort('/dev/tty.usbmodem11401', { // change path
+  baudRate: 9600,
+  parser: new SerialPort.parsers.Readline({
+    delimiter: '\r\n'
+  })
+});
+
+const gps = new GPS;
+
+gps.on('data', data => {
+  console.log(data, gps.state);
+})
+
+port.on('data', data => {
+  gps.updatePartial(data);
+})
+```
+
+Dashboard
+---
+Go into the folder `examples/dashboard` and start the server with
+
+```
+node server
+```
+
+After that you can open the browser and go to http://localhost:3000. The result should look like the following, which in principle is just a visualization of the state object `gps.state`
+
+![GPS TU Dresden](https://github.com/infusion/GPS.js/blob/master/res/dashboard.png?raw=true)
+
+Google Maps
+---
+Go into the folder `examples/maps` and start the server with
+
+```
+node server
+```
+
+After that you can open the browser and go to http://localhost:3000 The result should look like
+
+![GPS Google Maps Dresden](https://github.com/infusion/GPS.js/blob/master/res/maps.png?raw=true)
+
+Confluence
+---
+[Confluence](http://www.confluence.org/) is a project, which tries to travel to and document all integer GPS coordinates. GPS.js can assist on that goal. Go into the examples folder and run:
+
+```
+node confluence
+```
+
+You should see something like the following, updating as you move around
+
+```
+You are at (48.53, 9.05951),
+The closest confluence point (49, 9) is in 51.36 km.
+You have to go 355.2° N
+```
+
+Set Time
+---
+On systems without a RTC - like Raspberry PI - you need to update the time yourself at runtime. If the device has an internet connection, it's quite easy to use an NTP server. An alternative for disconnected projects with access to a GPS receiver can be the high-precision time signal, sent by satellites. Go to the examples folder and run the following to update the time:
+
+```
+node set-date
+```
+
+Available Methods
+===
+
+update(line)
+---
+The update method is the most important function, it parses a NMEA sentence and forces the callbacks to trigger
+
+updatePartial(chunk)
+---
+Will call `update()` when a full NMEA sentence has been arrived
+
+on(event, callback)
+---
+Adds an event listener for a protocol to occur (see implemented protocols, simply use the name - upper case) or for all sentences with `data`. Because GPS.js should be more general, it doesn't inherit `EventEmitter`, but simply invokes the callback.
+
+off(event)
+---
+Removes an event listener
+
+Implemented Protocols
+===
+
+GGA - Fix information
+---
+Gets the data, you're most probably looking for: *latitude and longitude*
+
+The parsed object will have the following attributes:
+
+- type: "GGA"
+- time: The time given as a JavaScript Date object
+- lat: The latitude
+- lon: The longitude
+- alt: The altitude
+- quality: Fix quality (either invalid, fix or diff)
+- satellites: Number of satellites being tracked
+- hdop: Horizontal [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS))
+- geoidal: Height of geoid in meters (mean sea level)
+- age: time in seconds since last DGPS update
+- stationID: DGPS station ID number
+- valid: Indicates if the checksum is okay
+
+RMC - NMEAs own version of essential GPS data
+---
+Similar to GGA but gives also delivers the velocity
+
+The parsed object will have the following attributes:
+
+- type: "RMC"
+- time: The time given as a JavaScript Date object
+- status: Status active or void
+- lat: The latitude
+- lon: The longitude
+- speed: Speed over the ground in km/h
+- track: Track angle in degrees
+- variation: Magnetic Variation
+- faa: The FAA mode, introduced with NMEA 2.3
+- valid: Indicates if the checksum is okay
+
+
+GSA - Active satellites
+---
+The parsed object will have the following attributes:
+
+- type: "GSA"
+- mode: Auto selection of 2D or 3D fix (either auto or manual)
+- fix: The selected fix mode (either 2D or 3D)
+- satellites: Array of satellite IDs
+- pdop: Position [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS))
+- vdop: Vertical [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS))
+- hdop: Horizontal [dilution of precision](https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS))
+- valid: Indicates if the checksum is okay
+
+GLL - Geographic Position - Latitude/Longitude
+---
+The parsed object will have the following attributes:
+
+- type: "GLL"
+- lat: The latitude
+- lon: The longitude
+- status: Status active or void
+- time: The time given as a JavaScript Date object
+- valid: Indicates if the checksum is okay
+
+GSV - List of Satellites in view
+---
+GSV messages are paginated. `msgNumber` indicates the current page and `msgsTotal` is the total number of pages.
+
+The parsed object will have the following attributes:
+
+- type: "GSV"
+- msgNumber: Current page
+- msgsTotal: Number of pages
+- satellites: Array of satellite objects with the following attributes:
+   - prn: Satellite PRN number
+   - elevation: Elevation in degrees
+   - azimuth: Azimuth in degrees
+   - snr: Signal to Noise Ratio (higher is better)
+- valid: Indicates if the checksum is okay
+
+
+VTG - vector track and speed over ground
+---
+
+The parsed object will have the following attributes:
+
+- type: "VTG"
+- track: Track in degrees
+- speed: Speed over ground in km/h
+- faa: The FAA mode, introduced with NMEA 2.3
+- valid: Indicates if the checksum is okay
+
+ZDA - UTC day, month, and year, and local time zone offset
+---
+
+The parsed object will have the following attributes:
+
+- type: "ZDA"
+- time: The time given as a JavaScript Date object
+
+HDT - Heading
+---
+
+The parsed object will have the following attributes:
+
+- type: "HDT"
+- heading: Heading in degrees
+- trueNorth: Indicates heading relative to True North
+- valid: Indicates if the checksum is okay
+
+GST - Position error statistics
+---
+
+The parsed object will have the following attributes:
+
+- type: "GST"
+- time: The time given as a JavaScript Date object
+- rms: RMS value of the pseudorange residuals; includes carrier phase residuals during periods of RTK (float) and RTK (fixed)
+- ellipseMajor: Error ellipse semi-major axis 1 sigma error, in meters
+- ellipseMinor: Error ellipse semi-minor axis 1 sigma error, in meters
+- ellipseOrientation: Error ellipse orientation, degrees from true north
+- latitudeError: Latitude 1 sigma error, in meters
+- longitudeError: Longitude 1 sigma error, in meters
+- heightError: Height 1 sigma error, in meters
+- valid: Indicates if the checksum is okay
+
+GPS State
+===
+If the streaming API is not needed, but a solid state of the system, the `gps.state` object can be used. It has the following properties:
+
+- time: Current time
+- lat: Latitude
+- lon: Longitude
+- alt: Altitude
+- satsActive: Array of active satellites
+- speed: Speed over ground in km/h
+- track: Track in degrees
+- satsVisible: Array of all visible satellites
+
+Adding new protocols is a matter of minutes. If you need a protocol which isn't implemented, I'm happy to see a pull request or a new ticket.
+
+
+Troubleshooting
+===
+If you don't get valid position information after turning on the receiver, chances are high you simply have to wait as it takes some [time to first fix](https://en.wikipedia.org/wiki/Time_to_first_fix).
+
+Functions
+===
+
+GPS.js comes with a few static functions, which helps working with geo-coordinates.
+
+GPS.Parse(line)
+---
+Parses a single line and returns the resulting object, in case the callback system isn't needed/wanted
+
+GPS.Distance(latFrom, lonFrom, latTo, lonTo)
+---
+Calculates the distance between two geo-coordinates using Haversine formula
+
+GPS.TotalDistance(points)
+---
+Calculates the length of a traveled route, given as an array of {lat: x, lon: y} point objects
+
+GPS.Heading(latFrom, lonFrom, latTo, lonTo)
+---
+Calculates the angle from one coordinate to another. Heading is represented as windrose coordinates (N=0, E=90, S=189, W=270). The result can be used as the argument of [angles](https://github.com/infusion/Angles.js) `compass()` method:
+
+```javascript
+const angles = require('angles');
+console.log(angles.compass(GPS.Heading(50, 10, 51, 9))); // will return x ∈ { N, S, E, W, NE, ... }
+```
+
+
+Using GPS.js with the browser
+===
+The use cases should be rare to parse NMEA directly inside the browser, but it works too.
+
+```html
+<script src="gps.js"></script>
+<script>
+   var gps = new GPS;
+   gps.update('...');
+</script>
+```
+
+Testing
+===
+If you plan to enhance the library, make sure you add test cases and all the previous tests are passing. You can test the library with
+
+```
+npm test
+```
+
+Copyright and licensing
+===
+Copyright (c) 2016-2022, [Robert Eisele](https://www.xarg.org/)
+Dual licensed under the MIT or GPL Version 2 licenses.

+ 206 - 0
static/gps_logger/js/preprocessor.js

@@ -0,0 +1,206 @@
+const options = {
+  legend: {
+    position: "nw",
+    show: true,
+    noColumns: 2,
+  },
+  zoom: {
+    interactive: true
+  },
+  pan: {
+    interactive: true,
+    cursor: "move",
+    frameRate: 60
+  },
+  series: {
+    lines: {
+      show: true,
+      lineWidth: 2
+    }
+  },
+  xaxis: {
+    tickDecimals: 2,
+    tickSize: 0.01
+  },
+  yaxis: {
+    tickDecimals: 2,
+    tickSize: 0.01
+  }
+}
+
+window.onload = function() {
+  const fileSelector = document.getElementById('file-selector')
+
+  fileSelector.addEventListener('change', (event) => {
+    const fileList = event.target.files;
+    console.log(fileList);
+    importFiles(fileList)
+  })
+
+  const dropArea = document.getElementsByClassName('content')[0];
+
+  dropArea.addEventListener('dragover', (event) => {
+    event.stopPropagation();
+    event.preventDefault();
+    // Style the drag-and-drop as a "copy file" operation.
+    event.dataTransfer.dropEffect = 'copy';
+  });
+
+  dropArea.addEventListener('drop', (event) => {
+    event.stopPropagation();
+    event.preventDefault();
+    const fileList = event.dataTransfer.files;
+    console.log(fileList);
+    importFiles(fileList)
+  });
+}
+function importFiles(fileList) {
+  d = []
+  plot = $.plot("#placeholder", d, options);
+  for (const file of fileList) {
+
+    if(file.name?.toLowerCase().endsWith(".txt"))
+      readTxtFile(file)
+    else if (file.name?.toLowerCase().endsWith(".nmea"))
+      parseNmeaFile(file)
+  }
+}
+
+function readTxtFile(file) {
+  const reader = new FileReader()
+  reader.addEventListener('load', (event) => {
+    const lines = event.target.result.split('\n')
+    let result = []
+    for(const line of lines) {
+      const fields = line.split(',')
+      if(fields.length != 8)
+        continue
+      
+      result.push({
+        timestamp: fields[0],
+        lat: parseFloat(fields[1]),
+        lng: parseFloat(fields[2]),
+        alt: parseFloat(fields[3]),
+        hdop: parseInt(fields[4]),
+        speed: parseInt(fields[5])
+      })
+    }
+    process(file.name, result)
+  });
+  reader.readAsText(file)
+}
+
+function parseNmeaFile(file) {
+  const reader = new FileReader()
+
+  reader.addEventListener('load', (event) => {
+    const lines = event.target.result.split('\n')
+    let result = []
+    let ggaObj, oldTime;
+  
+    for(const line of lines) {
+      const obj = GPS.Parse(line);
+      if (obj.type == "RMC") {
+        if(ggaObj.time != oldTime) {
+          oldTime = ggaObj.time
+          result.push({
+            timestamp: ggaObj.time,
+            lat: ggaObj.lat,
+            lng: ggaObj.lon,
+            alt: ggaObj.alt,
+            hdop: ggaObj.hdop,
+            speed: obj.speed
+          })
+        }
+        ggaObj = null
+      } else if(ggaObj) {
+        if(ggaObj.time != oldTime) {
+          oldTime = ggaObj.time
+          result.push({
+            timestamp: ggaObj.time,
+            lat: ggaObj.lat,
+            lng: ggaObj.lon,
+            alt: ggaObj.alt,
+            hdop: ggaObj.hdop,
+            speed: null
+          })
+        }
+        ggaObj = null
+      } 
+      if(obj.type == "GGA") {
+        ggaObj = obj;
+      }
+    }
+    process(file.name, result)
+  });
+  reader.readAsText(file)
+}
+
+function process(name, markers) {
+  d.push({
+    label: `${name} ${markers.length}`,
+    data: markers.map(m => [m.lat, m.lng])
+  })
+  markers = RamerDouglasPeucker2d(markers, 0.00001) //.0001 = 7m
+  d.push({
+    label: `RamerDouglasPeucker2d ${markers.length}`,
+    data: markers.map(m => [m.lat, m.lng])
+  })
+
+  plot.setData(d);
+  plot.setupGrid(); //only necessary if your new data will change the axes or grid
+  plot.draw();
+}
+
+function perpendicularDistance2d(ptX, ptY, l1x, l1y, l2x, l2y) {
+  if (l2x == l1x)
+  {
+    //vertical lines - treat this case specially to avoid dividing
+    //by zero
+    return Math.abs(ptX - l2x);
+  }
+  else
+  {
+    const slope = ((l2y-l1y) / (l2x-l1x));
+    const passThroughY = (0-l1x)*slope + l1y;
+    return (Math.abs((slope * ptX) - ptY + passThroughY)) /
+              (Math.sqrt(slope*slope + 1));
+  }
+}
+function RamerDouglasPeucker2d(pointList, epsilon) {
+  if (pointList.length < 2)
+  {
+    return pointList;
+  }
+  // Find the point with the maximum distance
+  let dmax = 0;
+  let index = 0;
+  let totalPoints = pointList.length;
+  for (let i = 1; i < (totalPoints - 1); i++)
+  {
+    let d = perpendicularDistance2d(
+          pointList[i].lat, pointList[i].lng,
+          pointList[0].lat, pointList[0].lng,
+          pointList[totalPoints-1].lat,
+          pointList[totalPoints-1].lng);
+    if (d > dmax)
+    {
+      index = i;
+      dmax = d;
+    }
+  }
+  
+  // If max distance is greater than epsilon, recursively simplify
+  if (dmax >= epsilon)
+  {
+    // Recursive call on each 'half' of the polyline
+    const recResults1 = RamerDouglasPeucker2d(pointList.slice(0, index + 1), epsilon);
+    const recResults2 = RamerDouglasPeucker2d(pointList.slice(index), epsilon);
+    // Build the result list
+    return recResults1.slice(0, recResults1.length - 1).concat(recResults2);
+  }
+  else
+  {
+    return [pointList[0], pointList[totalPoints-1]];
+  }
+}

+ 4 - 5
templates/gps_logger/index.html

@@ -53,14 +53,13 @@
   <ul id="tracks">
   </ul>
 </div>
-<script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.js' %}"></script>
-<script src="{% static 'gps_logger/cesium-1.60/Cesium.js' %}" type="text/javascript"></script>
-<script src="{% static 'gps_logger/js/tracks.js' %}" type="text/javascript"></script>
-<script src="{% static 'gps_logger/js/main.js' %}" type="text/javascript"></script>
-<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/excanvas.min.js' %}"></script><![endif]-->
 <script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.min.js' %}"></script>
+<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/excanvas.min.js' %}"></script><![endif]-->
 <script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.min.js' %}"></script>
 <script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.time.min.js' %}"></script>
 <script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.selection.min.js' %}"></script>
 <script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.navigate.min.js' %}"></script>
+<script src="{% static 'gps_logger/cesium-1.60/Cesium.js' %}" type="text/javascript"></script>
+<script src="{% static 'gps_logger/js/tracks.js' %}" type="text/javascript"></script>
+<script src="{% static 'gps_logger/js/main.js' %}" type="text/javascript"></script>
 {% endblock %}

+ 46 - 0
templates/gps_logger/upload.html

@@ -0,0 +1,46 @@
+
+{% extends "homepage/base.html" %}
+{% load static %}
+
+{% block title %}GPS Logging{% endblock %}
+{% block header %}GPS Logging{% endblock %}
+
+{% block status %}
+{% if user.is_authenticated %}
+<p style="text-align:center">
+  Hi {% firstof user.first_name user.username %}!<br/>
+  <a href="{% url 'logout' %}" class="link">Logout</a>
+</p>
+{% else %}
+<p style="text-align:center">
+  <a href="{% url 'login' %}" class="link">Login</a>
+</p>
+{% endif %}
+{% endblock %}
+
+{% block sidebar %}
+
+{% endblock %}
+
+
+{% block head %}
+<link href="{% static 'gps_logger/css/stylesheet.css' %}" rel="stylesheet" type="text/css">
+{% endblock %}
+
+{% block content %}
+<input type="file" id="file-selector" accept=".txt, .nmea" multiple>
+
+<div class="demo-container">
+  <div id="placeholder" class="demo-placeholder"></div>
+</div>
+{% endblock %}
+
+{% block body %}
+<script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.min.js' %}"></script>
+<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/excanvas.min.js' %}"></script><![endif]-->
+<script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.min.js' %}"></script>
+<script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.selection.min.js' %}"></script>
+<script language="javascript" type="text/javascript" src="{% static 'gps_logger/flot/jquery.flot.navigate.min.js' %}"></script>
+<script src="{% static 'gps_logger/js/preprocessor.js' %}" type="text/javascript"></script>
+<script src="{% static 'gps_logger/GPS.js/GPS.min.js' %}" type="text/javascript"></script>
+{% endblock %}