Add README.
[gps-watch.git] / tools / gpxify
1 #!/usr/bin/env python3
2 #
3 # Copyright (c) 2020 Tilman Sauerbeck (tilman at code-monkey de)
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 from datetime import datetime
25 import struct
26 import sys
27
28 class Packet(object):
29     def __init__(self, unix_time, latitude, longitude):
30         self.unix_time = unix_time
31         self.latitude = latitude
32         self.longitude = longitude
33
34 class PacketExtractor(object):
35     POINTS_PER_GROUP = 7
36     PADDING_BYTE = 0xff
37
38     def __init__(self, source):
39         self._source = source
40         self._unix_time = None
41         self._latitude = 0
42         self._longitude = 0
43         self._flags = 0
44         self._num_points = 0
45
46     def run(self):
47         header = self._source.read(5)
48
49         format_version, self._unix_time = struct.unpack('<BI', header)
50
51         if format_version != 1:
52             s = 'Unexpected format version {}'.format(format_version)
53             raise RuntimeError(s)
54
55         while True:
56             shift = self._num_points % PacketExtractor.POINTS_PER_GROUP
57             self._num_points += 1
58
59             if shift == 0:
60                 self._flags = PacketExtractor.PADDING_BYTE
61
62                 # Skip padding bytes.
63                 while self._flags == PacketExtractor.PADDING_BYTE:
64                     self._flags, = struct.unpack('B', self._source.read(1))
65
66             if (self._flags & (1 << shift)) == 0:
67                 d_time = 1
68             else:
69                 d_time = self.read_uvarint()
70
71             # End-of-stream marker hit?
72             if d_time == 0xffffffff:
73                 break
74
75             d_lat = self.read_svarint()
76             d_lon = self.read_svarint()
77
78             yield self.process_deltas(d_time, d_lat, d_lon)
79
80     def process_deltas(self, d_time, d_lat, d_lon):
81         self._unix_time += d_time
82         self._latitude += d_lat
83         self._longitude += d_lon
84
85         q = 600000.0
86
87         return Packet(self._unix_time, self._latitude / q, self._longitude / q)
88
89     def read_uvarint(self):
90         shift = 7
91         mask = 1 << shift
92         total_shift = 0
93         v = 0
94
95         while True:
96             b, = struct.unpack('B', self._source.read(1))
97
98             c = (b & (mask - 1))
99
100             v |= c << total_shift
101             total_shift += shift
102
103             if (b & mask) == 0:
104                 break
105
106         return v
107
108     def read_svarint(self):
109         u = self.read_uvarint()
110
111         h = -(u & 1);
112         n = (u >> 1) ^ h;
113
114         return n
115
116 if __name__ == '__main__':
117     filename = sys.argv[1]
118
119     with open(filename, 'rb') as f:
120         print('<?xml version="1.0"?>')
121         print('<gpx version="1.1" creator="gpxify">')
122         print('\t<trk>')
123         print('\t\t<name>{}</name>'.format(filename))
124         print('\t\t<number>{}</number>'.format(1))
125         print('\t\t<trkseg>')
126
127         for packet in PacketExtractor(f).run():
128             print('\t\t\t<trkpt lat="{:.8f}" lon="{:.8f}">'.format(
129                    packet.latitude, packet.longitude))
130
131             dt = datetime.utcfromtimestamp(packet.unix_time)
132
133             print(dt.strftime('\t\t\t\t<time>%Y-%m-%dT%H:%M:%SZ</time>'))
134             print('\t\t\t</trkpt>')
135
136         print('\t\t</trkseg>')
137         print('\t</trk>')
138         print('</gpx>')