author | wim |
Wed, 23 Aug 2017 21:25:58 +0200 | |
changeset 4 | 1b96bb9de1f3 |
parent 3 | f94099a0277a |
child 5 | 6be382f3be34 |
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 |
||
9 |
changelog: v1.2: clarity refractoring + speedup for identical points |
|
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 |
|
16 |
||
17 |
This program is free software: you can redistribute it and/or modify |
|
18 |
it under the terms of the GNU General Public License as published by |
|
19 |
the Free Software Foundation, either version 3 of the License, or |
|
20 |
(at your option) any later version. |
|
21 |
||
22 |
This program is distributed in the hope that it will be useful, |
|
23 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
24 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
25 |
GNU General Public License for more details. |
|
26 |
||
27 |
You should have received a copy of the GNU General Public License |
|
28 |
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
29 |
''' |
|
30 |
||
31 |
import datetime |
|
32 |
import sys |
|
33 |
import time |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
34 |
import numpy as np |
0 | 35 |
import numpy.linalg as la |
36 |
from math import * |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
37 |
import xml.etree.ElementTree as etree |
0 | 38 |
from optparse import OptionParser |
39 |
||
40 |
||
41 |
parser = OptionParser('usage: %prog [options] input-file.gpx') |
|
42 |
parser.add_option('-v', '--verbose', action='store', type='int', |
|
43 |
dest='verbose', default=1, help='verbose=[0,1]') |
|
44 |
parser.add_option('-p', '--plot', action='store_true', dest='plot', |
|
45 |
default=False, help='Show a plot of the result at the end.') |
|
46 |
parser.add_option('-d', '--dist', action='store', type='float', dest='max_dist', |
|
47 |
default=0.5, help='Maximum distance of line from original points in meters') |
|
48 |
parser.add_option('-o', '--out', action='store', type='string', |
|
49 |
dest='ofname', default=None, help='Output file name') |
|
50 |
parser.add_option('-m', '--maxsep', action='store', type='float', dest='max_sep', |
|
51 |
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.') |
|
52 |
parser.add_option('-n', '--maxsep0', action='store', type='float', dest='max_sep0', |
|
53 |
default=4.0, help='Maximum separation of points at zero speed.') |
|
54 |
parser.add_option('-t', '--maxseptime', action='store', type='float', dest='max_sep_time', |
|
55 |
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.') |
|
56 |
parser.add_option('-e', '--ele-weight', action='store', type='float', |
|
57 |
dest='ele_weight', default=0.0, |
|
58 |
help='Weighting of elevation errors vs. horizontal errors. Default is 0.') |
|
59 |
parser.add_option('-b', '--bend', action='store', type='float', dest='bend', |
|
60 |
default=1.0, help='Penalty value for large bending angles at each trackpoint. Larger values (1 or more) make the track smoother.') |
|
61 |
parser.add_option('-w', '--weighting', action='store', type='string', |
|
62 |
dest='weighting', default='exp', |
|
63 |
help='''Weighting function to be minimized for track reduction: |
|
64 |
pnum (number of points), |
|
65 |
sqrdistsum (number of points plus sum of squared distances to leftout points), |
|
66 |
sqrdistmax (number of points plus sum of squared distances to each maximally separated leftout point per new line segment), |
|
67 |
sqrlength (number of points plus sum of squared new line segment lengths normalized by maxsep), |
|
68 |
mix (number of points plus sum of squared distances to each maximally separated leftout point per new line segment weighted with corresponding segment length), |
|
69 |
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 | 70 |
parser.add_option ('-r', dest='remove', default='extensions,hdop', help='remove tags T1,T2,..,Tn from every trackpoint', metavar='T1 T2 Tn') |
0 | 71 |
|
4 | 72 |
options, args = parser.parse_args() |
0 | 73 |
|
74 |
if len(args) < 1: |
|
75 |
parser.print_usage() |
|
76 |
exit(2) |
|
77 |
||
78 |
# use the WGS-84 ellipsoid |
|
79 |
rE = 6356752.314245 # earth's radius |
|
80 |
a = 6378137.0 |
|
81 |
b = 6356752.314245179 |
|
82 |
||
83 |
timeformat = '%Y-%m-%dT%H:%M:%SZ' |
|
84 |
||
85 |
||
86 |
def distance(p1_, pm_, p2_, ele_weight=1.0): |
|
87 |
# returns distance of pm from line between p1 and p2 |
|
88 |
||
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
89 |
p1, pm, p2 = np.array(p1_), np.array(pm_), np.array(p2_) |
0 | 90 |
h1, hm, h2 = la.norm(p1), la.norm(pm), la.norm(p2) |
91 |
if ele_weight != 1.0 and min(h1, hm, h2) > 0.0: |
|
92 |
hmean = (h1 + hm + h2) / 3.0 |
|
93 |
p1 *= (ele_weight + (1.0 - ele_weight) * hmean / h1) |
|
94 |
pm *= (ele_weight + (1.0 - ele_weight) * hmean / hm) |
|
95 |
p2 *= (ele_weight + (1.0 - ele_weight) * hmean / h2) |
|
96 |
line = p2 - p1 |
|
97 |
linel = la.norm(line) |
|
98 |
vm = pm - p1 |
|
99 |
if linel == 0.0: |
|
100 |
return la.norm(vm) |
|
101 |
linem = line / linel |
|
102 |
||
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
103 |
position = np.dot(vm, linem) / linel |
0 | 104 |
if position < 0.0: |
105 |
return la.norm(vm) |
|
106 |
elif position > 1.0: |
|
107 |
return la.norm(pm - p2) |
|
108 |
else: |
|
109 |
return la.norm(vm - line * position) |
|
110 |
||
111 |
||
112 |
def rotate(x, y, phi): |
|
113 |
return x*cos(phi) - y*sin(phi), x*sin(phi) + y*cos(phi) |
|
114 |
||
115 |
||
116 |
def project_to_meters(lat, lon, latm, lonm): |
|
117 |
# azimuthal map projection centered at average track coordinate |
|
118 |
lon -= lonm |
|
119 |
xyz = latlonele_to_xyz(lat, lon, 0.0) |
|
120 |
zy = rotate(xyz[2], xyz[1], radians(90 - latm)) |
|
121 |
lat2 = atan2(zy[0], la.norm([zy[1], xyz[0]])) |
|
122 |
lon2 = atan2(xyz[0], -zy[1]) |
|
123 |
x_meters = rE * sin(lon2) * (pi / 2.0 - lat2) |
|
124 |
y_meters = -rE * cos(lon2) * (pi / 2.0 - lat2) |
|
125 |
return x_meters, y_meters |
|
126 |
||
127 |
||
128 |
def latlonele_to_xyz(lat, lon, ele): |
|
129 |
s = sin(radians(lat)) |
|
130 |
c = cos(radians(lat)) |
|
131 |
r = ele + a * b / la.norm([s*a, c*b]) |
|
132 |
lon = radians(lon) |
|
133 |
return r * c * sin(lon), r * c * (-cos(lon)), r * s |
|
134 |
||
135 |
||
136 |
def xyz_to_latlonele(x, y, z): |
|
137 |
r = la.norm([x, y, z]) |
|
138 |
if (r == 0): |
|
139 |
return 0.0, 0.0, 0.0 |
|
140 |
lat = degrees(atan2(z, la.norm([x, y]))) |
|
141 |
lon = degrees(atan2(x, -y)) |
|
142 |
ele = r * (1.0 - a * b / la.norm([a*z, b*x, b*y])) |
|
143 |
return lat, lon, ele |
|
144 |
||
145 |
||
146 |
def reduced_track_indices(coordinate_list, timesteps=None): |
|
147 |
# returns a list of indices of trackpoints that constitute the reduced track |
|
148 |
# takes a list of kartesian coordinate tuples |
|
149 |
m = len(coordinate_list) |
|
150 |
if (m == 0): return [] |
|
151 |
if timesteps != None and len(timesteps) != len(coordinate_list): |
|
152 |
timesteps = None |
|
153 |
||
154 |
# number of dimensions |
|
155 |
d = len(coordinate_list[0]) |
|
156 |
||
157 |
# remove identical entries (can speed up algorithm considerably) |
|
158 |
original_indices = [0] |
|
159 |
points = [{'p': coordinate_list[0], 'weight':1}] |
|
160 |
if timesteps != None: points[0]['t'] = timesteps[0] |
|
161 |
for i in range(1, m): |
|
162 |
if False in [coordinate_list[i-1][j] == coordinate_list[i][j] for j in range(d)]: |
|
163 |
original_indices.append(i) |
|
164 |
points.append({'p': coordinate_list[i], 'weight':1}) |
|
165 |
if timesteps != None: points[-1]['t'] = timesteps[i] |
|
166 |
else: |
|
167 |
points[-1]['weight'] += 1 |
|
168 |
n = len(points) |
|
169 |
||
170 |
# progress printing initialisations |
|
171 |
progress_printed = False |
|
172 |
progress = None |
|
173 |
tprint = time.time() |
|
174 |
||
175 |
# execute Dijkstra-like algorithm on points |
|
176 |
points[0]['cost'] = 1.0 |
|
177 |
points[0]['prev'] = -1 |
|
178 |
||
179 |
for i2 in range(1, n): |
|
180 |
penalties = {} |
|
181 |
imin = None |
|
182 |
costmin = float('inf') |
|
183 |
for i1 in reversed(range(i2)): |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
184 |
p1 = np.array(points[i1]['p']) |
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
185 |
p2 = np.array(points[i2]['p']) |
0 | 186 |
seglength = la.norm(p2 - p1) |
187 |
||
188 |
# estimate speed between p1 and p2 |
|
189 |
if timesteps != None: |
|
190 |
dt = (points[i2]['t'] - points[i1]['t']).total_seconds() |
|
191 |
v = seglength / max(0.1, dt) |
|
192 |
else: |
|
193 |
v = seglength / float(i2 - i1) # assume 1s time spacing |
|
194 |
||
195 |
max_sep = options.max_sep0 + v * options.max_sep_time |
|
196 |
if options.max_dist >= 0: |
|
197 |
max_sep = min(max_sep, options.max_sep) |
|
198 |
||
199 |
if (seglength >= max_sep and i1 != i2 - 1): |
|
200 |
# point separation is too far |
|
201 |
# but always accept direct predecessor i1 = i2 - 1 |
|
202 |
if (seglength >= max_sep + options.max_dist): |
|
203 |
# no chance to find a valid earlier predecessor point |
|
204 |
break |
|
205 |
else: |
|
206 |
continue |
|
207 |
||
208 |
if points[i1]['cost'] + 1.0 > costmin: |
|
209 |
# the possible predecessor i1 is already too bad. |
|
210 |
continue |
|
211 |
||
212 |
i1_i2_segment_valid = True |
|
213 |
lower_i1_possible = True |
|
214 |
distance_squaremax = 0.0 |
|
215 |
distance_squaresum = 0.0 |
|
216 |
distances_squared = [] |
|
217 |
# iterate all medium points between i1 and i2 |
|
218 |
for im in range(i1+1, i2): |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
219 |
pm = np.array(points[im]['p']) |
0 | 220 |
d = distance(p1, pm, p2, options.ele_weight) |
221 |
if (d <= options.max_dist): |
|
222 |
d_sq = (d / options.max_dist) ** 2 |
|
223 |
distance_squaremax = max(distance_squaremax, d_sq) |
|
224 |
distance_squaresum += points[im]['weight'] * d_sq |
|
225 |
distances_squared.append(d_sq) |
|
226 |
else: |
|
227 |
i1_i2_segment_valid = False |
|
228 |
||
229 |
# check if connection to any further point i1 is impossible |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
230 |
d1 = np.dot(p1 - p2, p1 - p2) |
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
231 |
d2 = np.dot(pm - p2, pm - p2) |
0 | 232 |
dd = options.max_dist ** 2 |
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
233 |
d1d2 = np.dot(p1 - p2, pm - p2) |
0 | 234 |
# formula from cosines of point separation angle and cone-opening angles around points |
235 |
if (d1 > dd and d2 > dd and (d1d2 + dd)**2 < (d2 - dd) * (d1 - dd)): |
|
236 |
lower_i1_possible = False |
|
237 |
break |
|
238 |
||
239 |
if (lower_i1_possible == False): |
|
240 |
break |
|
241 |
||
242 |
if i1_i2_segment_valid: |
|
243 |
if options.weighting == 'sqrdistmax': |
|
244 |
penalties[i1] = distance_squaremax |
|
245 |
elif options.weighting == 'sqrdistsum': |
|
246 |
penalties[i1] = distance_squaresum |
|
247 |
elif options.weighting == 'sqrlength': |
|
248 |
penalties[i1] = (seglength / max_sep) ** 2 |
|
249 |
elif options.weighting == 'mix': |
|
250 |
penalties[i1] = (distance_squaremax * (1.0 + seglength / max_sep)) |
|
251 |
elif options.weighting == 'exp': |
|
252 |
penalties[i1] = 0.5 * sum([0.5**i * d for i, d in |
|
253 |
enumerate(sorted(distances_squared, reverse=True))]) |
|
254 |
else: |
|
255 |
penalties[i1] = 0.0 |
|
256 |
||
257 |
# add a penalty for kinks |
|
258 |
if options.bend > 0.: |
|
259 |
if points[i1]['prev'] != -1: |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
260 |
p0 = np.array(points[points[i1]['prev']]['p']) |
0 | 261 |
v0 = p1 - p0 |
262 |
v1 = p2 - p1 |
|
263 |
if la.norm(v0) > 0. and la.norm(v1) > 0.: |
|
264 |
v0 /= la.norm(v0) |
|
265 |
v1 /= la.norm(v1) |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
266 |
kink = (1.0 - np.dot(v0, v1)) / 2.0 |
0 | 267 |
penalties[i1] += options.bend * kink |
268 |
||
269 |
# find best predecessor |
|
270 |
imin = None |
|
271 |
costmin = float('inf') |
|
272 |
for prev, penalty in penalties.iteritems(): |
|
273 |
# cost function is sum of points used (1.0) plus penalties |
|
274 |
cost = points[prev]['cost'] + 1.0 + penalty |
|
275 |
if cost < costmin: |
|
276 |
imin = prev |
|
277 |
costmin = cost |
|
278 |
points[i2]['cost'] = costmin |
|
279 |
points[i2]['prev'] = imin |
|
280 |
||
281 |
# print progess |
|
282 |
if options.verbose == 1 and (100 * i2) / n > progress and time.time() >= tprint + 1: |
|
283 |
tprint = time.time() |
|
284 |
progress = (100 * i2) / n |
|
285 |
print '\r', progress, '%', |
|
286 |
sys.stdout.flush() |
|
287 |
progress_printed = True |
|
288 |
||
289 |
if progress_printed: |
|
290 |
print '\r', |
|
291 |
||
292 |
# trace route backwards to collect final points |
|
293 |
final_pnums = [] |
|
294 |
i = n-1 |
|
295 |
while i >= 0: |
|
296 |
final_pnums = [i] + final_pnums |
|
297 |
i = points[i]['prev'] |
|
298 |
||
299 |
return [original_indices[i] for i in final_pnums] |
|
300 |
||
301 |
||
302 |
||
303 |
############################## main function ################################# |
|
304 |
for fname in args: |
|
305 |
# initialisations |
|
306 |
tracksegs_old = [] |
|
307 |
tracksegs_new = [] |
|
308 |
sumx, sumy, sumz = 0.0, 0.0, 0.0 |
|
309 |
||
310 |
# import xml data from files |
|
311 |
print 'opening file', fname |
|
312 |
infile = open(fname) |
|
313 |
tree = etree.parse(infile) |
|
314 |
infile.close() |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
315 |
|
0 | 316 |
gpx = tree.getroot() |
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
317 |
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
|
318 |
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
|
319 |
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
|
320 |
|
0 | 321 |
# extract data from xml |
322 |
for trkseg in gpx.findall('.//' + nsmap + 'trkseg'): |
|
323 |
trkpts = trkseg.findall(nsmap + 'trkpt') |
|
324 |
n = len(trkpts) |
|
325 |
||
326 |
# extract coordinate values |
|
327 |
lats = [float(trkpt.get('lat')) for trkpt in trkpts] |
|
328 |
lons = [float(trkpt.get('lon')) for trkpt in trkpts] |
|
329 |
eles = [float(trkpt.find(nsmap + 'ele').text) for trkpt in trkpts] |
|
330 |
try: |
|
331 |
times = [datetime.datetime.strptime(trkpt.find(nsmap + 'time' |
|
332 |
).text, timeformat) for trkpt in trkpts] |
|
333 |
except Exception as e: |
|
334 |
print e |
|
335 |
times = None |
|
4 | 336 |
|
0 | 337 |
# save original trackseg for plotting |
338 |
if options.plot: |
|
339 |
tracksegs_old.append([[lats[i], lons[i], eles[i]] for i in range(n)]) |
|
340 |
||
341 |
# calculate projected points to work on |
|
342 |
coords = [] |
|
343 |
for i in range(n): |
|
344 |
x, y, z = latlonele_to_xyz(lats[i], lons[i], eles[i]) |
|
345 |
coords.append((x, y, z)) |
|
346 |
sumx += x |
|
347 |
sumy += y |
|
348 |
sumz += z |
|
349 |
||
350 |
# execute the reduction algorithm |
|
351 |
final_pnums = reduced_track_indices(coords, times) |
|
352 |
||
353 |
n_new = len(final_pnums) |
|
354 |
print 'number of points:', n, '-', n - n_new, '=', n_new |
|
355 |
||
356 |
# delete certain points from original data |
|
357 |
delete_pnums = [i for i in range(n) if i not in final_pnums] |
|
358 |
for i in reversed(delete_pnums): |
|
4 | 359 |
trkseg.remove (trkpts[i]) # remove from the xml-tree |
360 |
del trkpts [i] # also remove from the list |
|
361 |
||
362 |
# remove certain sub-elements from remaining points |
|
363 |
options.remove = options.remove.replace ('+','extensions,hdop,') |
|
364 |
taglist = options.remove.split (',') |
|
365 |
if taglist: print 'remove %s subelements from points' % ', '.join (taglist) |
|
366 |
for trkpnt in trkpts: |
|
367 |
for tag in taglist: |
|
368 |
e = trkpnt.find (nsmap + tag) |
|
369 |
if e != None: trkpnt.remove (e) |
|
370 |
||
0 | 371 |
# save reduced trackseg for plotting |
372 |
if options.plot: |
|
4 | 373 |
tracksegs_new.append ([[float(trkpt.get('lat')), |
0 | 374 |
float(trkpt.get('lon')), float(trkpt.find(nsmap + 'ele').text)] |
375 |
for trkpt in trkseg.findall(nsmap + 'trkpt')]) |
|
376 |
||
377 |
# export data to file |
|
378 |
if options.ofname != None: |
|
379 |
ofname = options.ofname |
|
380 |
elif fname.endswith('.gpx'): |
|
381 |
ofname = fname[:-4] + '_reduced.gpx' |
|
382 |
else: |
|
383 |
ofname = fname + '_reduced.gpx' |
|
384 |
outfile = open(ofname, 'w') |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
385 |
tree.write (outfile, encoding="utf-8", xml_declaration=True, default_namespace=None, method="xml") |
0 | 386 |
outfile.close() |
387 |
print 'modified copy written to', ofname |
|
2
f93786d4f68e
- afhangelijkheid van lxml verwijderd door de gewone etree te gebruiken
wim
parents:
1
diff
changeset
|
388 |
|
0 | 389 |
# plot result to screen |
390 |
if options.plot: |
|
391 |
latm, lonm, elesum = xyz_to_latlonele(sumx, sumy, sumz) |
|
392 |
||
4 | 393 |
data_old = [] |
0 | 394 |
for trkseg in tracksegs_old: |
395 |
for trkpt in trkseg: |
|
396 |
xy = project_to_meters(trkpt[0], trkpt[1], latm, lonm) |
|
3 | 397 |
data_old.append (xy) |
0 | 398 |
|
4 | 399 |
data = [] |
0 | 400 |
for trkseg in tracksegs_new: |
401 |
for trkpt in trkseg: |
|
402 |
xy = project_to_meters(trkpt[0], trkpt[1], latm, lonm) |
|
3 | 403 |
data.append (xy) |
404 |
||
4 | 405 |
from subprocess import Popen, PIPE |
3 | 406 |
gnuPlotCmd = ['gnuplot'] |
407 |
plot = Popen (gnuPlotCmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) |
|
408 |
plot.stdin.write (b"plot '-' with linespoints lc rgb 'red' pt 4, '-' with linespoints pt 6\n") |
|
409 |
plot.stdin.write ("\n".join ('%f %f' % d for d in data).encode ()) |
|
410 |
plot.stdin.write (b'\ne\n') |
|
411 |
plot.stdin.write ("\n".join ('%f %f' % d for d in data_old).encode ()) |
|
412 |
plot.stdin.write (b'\ne\n') |
|
413 |
plot.stdin.flush () |
|
4 | 414 |
raw_input ('druk') |