author | wim |
Wed, 30 Aug 2017 12:37:28 +0200 | |
changeset 10 | 89108adbc468 |
parent 6 | c808c4d02550 |
child 15 | cfb0607e5afc |
permissions | -rwxr-xr-x |
0 | 1 |
#!/usr/bin/env python |
2 |
# -*- coding: utf8 -*- |
|
3 |
||
4 |
''' |
|
5 |
gpx_reduce v1.8: removes points from gpx-files to reduce filesize and |
|
6 |
tries to keep introduced distortions to the track at a minimum. |
|
7 |
Copyright (C) 2011,2012,2013,2015,2016,2017 travelling_salesman on OpenStreetMap |
|
8 |
||
10 | 9 |
changelog: v1.2: clarity refractoring + speedup for identical points |
0 | 10 |
v1.3: new track weighting functions, progress display |
11 |
v1.4: new track weighting function, restructuring for memory saving |
|
12 |
v1.5: algorithm speedup by roughly a factor of 2 by eliminating some cases. |
|
13 |
v1.6: presets for train etc. |
|
14 |
v1.7: introduced weighting function for elevation errors |
|
15 |
v1.8: speed-dependent distance limit |
|
10 | 16 |
v1.9: removal of all dependencies (lxml, scipy, numpy, pylab) |
0 | 17 |
|
18 |
This program is free software: you can redistribute it and/or modify |
|
19 |
it under the terms of the GNU General Public License as published by |
|
20 |
the Free Software Foundation, either version 3 of the License, or |
|
21 |
(at your option) any later version. |
|
22 |
||
23 |
This program is distributed in the hope that it will be useful, |
|
24 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
25 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
26 |
GNU General Public License for more details. |
|
27 |
||
28 |
You should have received a copy of the GNU General Public License |
|
29 |
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
30 |
''' |
|
31 |
||
6 | 32 |
import datetime, sys, time, operator |
0 | 33 |
from math import * |
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
34 |
import xml.etree.ElementTree as etree |
0 | 35 |
from optparse import OptionParser |
36 |
||
37 |
||
38 |
parser = OptionParser('usage: %prog [options] input-file.gpx') |
|
39 |
parser.add_option('-v', '--verbose', action='store', type='int', |
|
40 |
dest='verbose', default=1, help='verbose=[0,1]') |
|
41 |
parser.add_option('-d', '--dist', action='store', type='float', dest='max_dist', |
|
42 |
default=0.5, help='Maximum distance of line from original points in meters') |
|
43 |
parser.add_option('-o', '--out', action='store', type='string', |
|
44 |
dest='ofname', default=None, help='Output file name') |
|
45 |
parser.add_option('-m', '--maxsep', action='store', type='float', dest='max_sep', |
|
46 |
default=200.0, help='Absolute maximum separation of points. No points will be deleted where the resulting distance would become greater than maxsep. Standard JOSM settings will not display points spaced more than 200m. -1 means no limit.') |
|
47 |
parser.add_option('-n', '--maxsep0', action='store', type='float', dest='max_sep0', |
|
48 |
default=4.0, help='Maximum separation of points at zero speed.') |
|
49 |
parser.add_option('-t', '--maxseptime', action='store', type='float', dest='max_sep_time', |
|
50 |
default=3.0, help='Maximum time separation of points, which will be added to maxsep0. Maximum allowed point separation will be min(m, n + t*v) where v is the speed in m/s.') |
|
51 |
parser.add_option('-e', '--ele-weight', action='store', type='float', |
|
52 |
dest='ele_weight', default=0.0, |
|
53 |
help='Weighting of elevation errors vs. horizontal errors. Default is 0.') |
|
54 |
parser.add_option('-b', '--bend', action='store', type='float', dest='bend', |
|
55 |
default=1.0, help='Penalty value for large bending angles at each trackpoint. Larger values (1 or more) make the track smoother.') |
|
56 |
parser.add_option('-w', '--weighting', action='store', type='string', |
|
57 |
dest='weighting', default='exp', |
|
58 |
help='''Weighting function to be minimized for track reduction: |
|
59 |
pnum (number of points), |
|
60 |
sqrdistsum (number of points plus sum of squared distances to leftout points), |
|
61 |
sqrdistmax (number of points plus sum of squared distances to each maximally separated leftout point per new line segment), |
|
62 |
sqrlength (number of points plus sum of squared new line segment lengths normalized by maxsep), |
|
63 |
mix (number of points plus sum of squared distances to each maximally separated leftout point per new line segment weighted with corresponding segment length), |
|
64 |
exp (number of points plus sum of squared distances to leftout points with exponential weighting of 1/2, 1/4, 1/8... from furthest to closest point). exp=standard''') |
|
4 | 65 |
parser.add_option ('-r', dest='remove', default='extensions,hdop', help='remove tags T1,T2,..,Tn from every trackpoint', metavar='T1 T2 Tn') |
0 | 66 |
|
4 | 67 |
options, args = parser.parse_args() |
0 | 68 |
|
69 |
if len(args) < 1: |
|
6 | 70 |
parser.print_help () |
71 |
sys.exit (2) |
|
0 | 72 |
|
73 |
# use the WGS-84 ellipsoid |
|
74 |
rE = 6356752.314245 # earth's radius |
|
75 |
a = 6378137.0 |
|
76 |
b = 6356752.314245179 |
|
77 |
||
78 |
timeformat = '%Y-%m-%dT%H:%M:%SZ' |
|
79 |
||
6 | 80 |
# the linear algebra with lists |
81 |
dot = lambda p1, p2: reduce (operator.add, [a * b for a, b in zip (p1, p2)]) |
|
82 |
vmin = lambda p1, p2: [a - b for a, b in zip (p1, p2)] |
|
83 |
smul = lambda p, c: [a * c for a in p] |
|
84 |
sdiv = lambda p, c: [a / c for a in p] |
|
85 |
norm = lambda p: sqrt (sum (a * a for a in p)) |
|
86 |
||
87 |
def distance (p1, pm, p2, ele_weight=1.0): |
|
0 | 88 |
# returns distance of pm from line between p1 and p2 |
89 |
||
6 | 90 |
h1, hm, h2 = norm (p1), norm (pm), norm (p2) |
0 | 91 |
if ele_weight != 1.0 and min(h1, hm, h2) > 0.0: |
92 |
hmean = (h1 + hm + h2) / 3.0 |
|
6 | 93 |
p1 = smul (p1, ele_weight + (1.0 - ele_weight) * hmean / h1) |
94 |
pm = smul (pm, ele_weight + (1.0 - ele_weight) * hmean / hm) |
|
95 |
p2 = smul (p2, ele_weight + (1.0 - ele_weight) * hmean / h2) |
|
96 |
line = vmin (p2, p1) |
|
97 |
linel = norm (line) |
|
98 |
vm = vmin (pm, p1) |
|
0 | 99 |
if linel == 0.0: |
6 | 100 |
return norm (vm) |
101 |
linem = sdiv (line, linel) |
|
0 | 102 |
|
6 | 103 |
position = dot (vm, linem) / linel |
0 | 104 |
if position < 0.0: |
6 | 105 |
return norm (vm) |
0 | 106 |
elif position > 1.0: |
6 | 107 |
return norm (vmin (pm, p2)) |
0 | 108 |
else: |
6 | 109 |
return norm (vmin (vm, smul (line, position))) |
0 | 110 |
|
6 | 111 |
def latlonele_to_xyz (lat, lon, ele): |
112 |
s = sin (radians (lat)) |
|
113 |
c = cos (radians (lat)) |
|
114 |
r = ele + a * b / norm ([s*a, c*b]) |
|
115 |
lon = radians (lon) |
|
116 |
return r * c * sin (lon), r * c * (-cos (lon)), r * s |
|
0 | 117 |
|
118 |
||
119 |
def reduced_track_indices(coordinate_list, timesteps=None): |
|
120 |
# returns a list of indices of trackpoints that constitute the reduced track |
|
121 |
# takes a list of kartesian coordinate tuples |
|
122 |
m = len(coordinate_list) |
|
123 |
if (m == 0): return [] |
|
124 |
if timesteps != None and len(timesteps) != len(coordinate_list): |
|
125 |
timesteps = None |
|
126 |
||
127 |
# number of dimensions |
|
128 |
d = len(coordinate_list[0]) |
|
129 |
||
130 |
# remove identical entries (can speed up algorithm considerably) |
|
131 |
original_indices = [0] |
|
132 |
points = [{'p': coordinate_list[0], 'weight':1}] |
|
133 |
if timesteps != None: points[0]['t'] = timesteps[0] |
|
134 |
for i in range(1, m): |
|
135 |
if False in [coordinate_list[i-1][j] == coordinate_list[i][j] for j in range(d)]: |
|
136 |
original_indices.append(i) |
|
137 |
points.append({'p': coordinate_list[i], 'weight':1}) |
|
138 |
if timesteps != None: points[-1]['t'] = timesteps[i] |
|
139 |
else: |
|
140 |
points[-1]['weight'] += 1 |
|
141 |
n = len(points) |
|
142 |
||
143 |
# progress printing initialisations |
|
144 |
progress_printed = False |
|
145 |
progress = None |
|
146 |
tprint = time.time() |
|
147 |
||
148 |
# execute Dijkstra-like algorithm on points |
|
149 |
points[0]['cost'] = 1.0 |
|
150 |
points[0]['prev'] = -1 |
|
151 |
||
152 |
for i2 in range(1, n): |
|
153 |
penalties = {} |
|
154 |
imin = None |
|
155 |
costmin = float('inf') |
|
156 |
for i1 in reversed(range(i2)): |
|
6 | 157 |
p1 = points[i1]['p'] |
158 |
p2 = points[i2]['p'] |
|
159 |
seglength = norm (vmin (p2, p1)) |
|
0 | 160 |
|
161 |
# estimate speed between p1 and p2 |
|
162 |
if timesteps != None: |
|
163 |
dt = (points[i2]['t'] - points[i1]['t']).total_seconds() |
|
164 |
v = seglength / max(0.1, dt) |
|
165 |
else: |
|
166 |
v = seglength / float(i2 - i1) # assume 1s time spacing |
|
167 |
||
168 |
max_sep = options.max_sep0 + v * options.max_sep_time |
|
169 |
if options.max_dist >= 0: |
|
170 |
max_sep = min(max_sep, options.max_sep) |
|
171 |
||
172 |
if (seglength >= max_sep and i1 != i2 - 1): |
|
173 |
# point separation is too far |
|
174 |
# but always accept direct predecessor i1 = i2 - 1 |
|
175 |
if (seglength >= max_sep + options.max_dist): |
|
176 |
# no chance to find a valid earlier predecessor point |
|
177 |
break |
|
178 |
else: |
|
179 |
continue |
|
180 |
||
181 |
if points[i1]['cost'] + 1.0 > costmin: |
|
182 |
# the possible predecessor i1 is already too bad. |
|
183 |
continue |
|
184 |
||
185 |
i1_i2_segment_valid = True |
|
186 |
lower_i1_possible = True |
|
187 |
distance_squaremax = 0.0 |
|
188 |
distance_squaresum = 0.0 |
|
189 |
distances_squared = [] |
|
190 |
# iterate all medium points between i1 and i2 |
|
191 |
for im in range(i1+1, i2): |
|
6 | 192 |
pm = points[im]['p'] |
193 |
d = distance (p1, pm, p2, options.ele_weight) |
|
0 | 194 |
if (d <= options.max_dist): |
195 |
d_sq = (d / options.max_dist) ** 2 |
|
196 |
distance_squaremax = max(distance_squaremax, d_sq) |
|
197 |
distance_squaresum += points[im]['weight'] * d_sq |
|
198 |
distances_squared.append(d_sq) |
|
199 |
else: |
|
200 |
i1_i2_segment_valid = False |
|
201 |
||
202 |
# check if connection to any further point i1 is impossible |
|
6 | 203 |
d1 = dot (vmin (p1, p2), vmin (p1, p2)) |
204 |
d2 = dot (vmin (pm, p2), vmin (pm, p2)) |
|
0 | 205 |
dd = options.max_dist ** 2 |
6 | 206 |
d1d2 = dot (vmin (p1, p2), vmin (pm, p2)) |
0 | 207 |
# formula from cosines of point separation angle and cone-opening angles around points |
208 |
if (d1 > dd and d2 > dd and (d1d2 + dd)**2 < (d2 - dd) * (d1 - dd)): |
|
209 |
lower_i1_possible = False |
|
210 |
break |
|
211 |
||
212 |
if (lower_i1_possible == False): |
|
213 |
break |
|
214 |
||
215 |
if i1_i2_segment_valid: |
|
216 |
if options.weighting == 'sqrdistmax': |
|
217 |
penalties[i1] = distance_squaremax |
|
218 |
elif options.weighting == 'sqrdistsum': |
|
219 |
penalties[i1] = distance_squaresum |
|
220 |
elif options.weighting == 'sqrlength': |
|
221 |
penalties[i1] = (seglength / max_sep) ** 2 |
|
222 |
elif options.weighting == 'mix': |
|
223 |
penalties[i1] = (distance_squaremax * (1.0 + seglength / max_sep)) |
|
224 |
elif options.weighting == 'exp': |
|
225 |
penalties[i1] = 0.5 * sum([0.5**i * d for i, d in |
|
226 |
enumerate(sorted(distances_squared, reverse=True))]) |
|
227 |
else: |
|
228 |
penalties[i1] = 0.0 |
|
229 |
||
230 |
# add a penalty for kinks |
|
231 |
if options.bend > 0.: |
|
232 |
if points[i1]['prev'] != -1: |
|
6 | 233 |
p0 = points [points [i1]['prev']] ['p'] |
234 |
v0 = vmin (p1, p0) |
|
235 |
v1 = vmin (p2, p1) |
|
236 |
if norm (v0) > 0. and norm (v1) > 0.: |
|
237 |
v0 = sdiv (v0, norm (v0)) |
|
238 |
v1 = sdiv (v1, norm (v1)) |
|
239 |
kink = (1.0 - dot (v0, v1)) / 2.0 |
|
0 | 240 |
penalties[i1] += options.bend * kink |
241 |
||
242 |
# find best predecessor |
|
243 |
imin = None |
|
244 |
costmin = float('inf') |
|
245 |
for prev, penalty in penalties.iteritems(): |
|
246 |
# cost function is sum of points used (1.0) plus penalties |
|
247 |
cost = points[prev]['cost'] + 1.0 + penalty |
|
248 |
if cost < costmin: |
|
249 |
imin = prev |
|
250 |
costmin = cost |
|
251 |
points[i2]['cost'] = costmin |
|
252 |
points[i2]['prev'] = imin |
|
253 |
||
254 |
# print progess |
|
255 |
if options.verbose == 1 and (100 * i2) / n > progress and time.time() >= tprint + 1: |
|
256 |
tprint = time.time() |
|
257 |
progress = (100 * i2) / n |
|
258 |
print '\r', progress, '%', |
|
259 |
sys.stdout.flush() |
|
260 |
progress_printed = True |
|
261 |
||
262 |
if progress_printed: |
|
263 |
print '\r', |
|
264 |
||
265 |
# trace route backwards to collect final points |
|
266 |
final_pnums = [] |
|
267 |
i = n-1 |
|
268 |
while i >= 0: |
|
269 |
final_pnums = [i] + final_pnums |
|
270 |
i = points[i]['prev'] |
|
271 |
||
272 |
return [original_indices[i] for i in final_pnums] |
|
273 |
||
274 |
||
275 |
||
276 |
############################## main function ################################# |
|
277 |
for fname in args: |
|
278 |
# initialisations |
|
5 | 279 |
ntot = 0 # total number of trackpoints (sum of segments) |
280 |
newtot = 0 # total number of trackpoints after removal |
|
0 | 281 |
|
282 |
# import xml data from files |
|
283 |
print 'opening file', fname |
|
284 |
infile = open(fname) |
|
285 |
tree = etree.parse(infile) |
|
286 |
infile.close() |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
287 |
|
0 | 288 |
gpx = tree.getroot() |
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
289 |
nsurl = gpx.tag.split ('}')[0][1:] # == 'http://www.topografix.com/GPX/1/1' |
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
290 |
etree.register_namespace('', nsurl) # default namespace -> xmlns:.... in the output |
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
291 |
nsmap = '{' + nsurl + '}' # prefix for all tags in the tree |
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
292 |
|
0 | 293 |
# extract data from xml |
5 | 294 |
for si, trkseg in enumerate (gpx.findall('.//' + nsmap + 'trkseg')): |
0 | 295 |
trkpts = trkseg.findall(nsmap + 'trkpt') |
296 |
n = len(trkpts) |
|
297 |
||
298 |
# extract coordinate values |
|
299 |
lats = [float(trkpt.get('lat')) for trkpt in trkpts] |
|
300 |
lons = [float(trkpt.get('lon')) for trkpt in trkpts] |
|
301 |
eles = [float(trkpt.find(nsmap + 'ele').text) for trkpt in trkpts] |
|
302 |
try: |
|
303 |
times = [datetime.datetime.strptime(trkpt.find(nsmap + 'time' |
|
304 |
).text, timeformat) for trkpt in trkpts] |
|
305 |
except Exception as e: |
|
5 | 306 |
print '-- trackpoint without time' |
0 | 307 |
times = None |
4 | 308 |
|
0 | 309 |
# calculate projected points to work on |
310 |
coords = [] |
|
311 |
for i in range(n): |
|
5 | 312 |
x, y, z = latlonele_to_xyz (lats[i], lons[i], eles[i]) |
0 | 313 |
coords.append((x, y, z)) |
314 |
||
315 |
# execute the reduction algorithm |
|
316 |
final_pnums = reduced_track_indices(coords, times) |
|
5 | 317 |
|
318 |
n_new = len (final_pnums) |
|
319 |
print 'segment %d, with %d - %d = %d points' % (si, n, n - n_new, n_new) |
|
320 |
ntot += n |
|
321 |
newtot += n_new |
|
322 |
||
0 | 323 |
# delete certain points from original data |
324 |
delete_pnums = [i for i in range(n) if i not in final_pnums] |
|
325 |
for i in reversed(delete_pnums): |
|
4 | 326 |
trkseg.remove (trkpts[i]) # remove from the xml-tree |
327 |
del trkpts [i] # also remove from the list |
|
328 |
||
329 |
# remove certain sub-elements from remaining points |
|
330 |
options.remove = options.remove.replace ('+','extensions,hdop,') |
|
331 |
taglist = options.remove.split (',') |
|
332 |
if taglist: print 'remove %s subelements from points' % ', '.join (taglist) |
|
333 |
for trkpnt in trkpts: |
|
334 |
for tag in taglist: |
|
335 |
e = trkpnt.find (nsmap + tag) |
|
336 |
if e != None: trkpnt.remove (e) |
|
337 |
||
5 | 338 |
print 'total number of points: %d - %d = %d' % (ntot, ntot - newtot, newtot) |
339 |
||
0 | 340 |
# export data to file |
341 |
if options.ofname != None: |
|
342 |
ofname = options.ofname |
|
343 |
elif fname.endswith('.gpx'): |
|
344 |
ofname = fname[:-4] + '_reduced.gpx' |
|
345 |
else: |
|
346 |
ofname = fname + '_reduced.gpx' |
|
347 |
outfile = open(ofname, 'w') |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
348 |
tree.write (outfile, encoding="utf-8", xml_declaration=True, default_namespace=None, method="xml") |
0 | 349 |
outfile.close() |
350 |
print 'modified copy written to', ofname |