-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplottrack.py
375 lines (371 loc) · 12.4 KB
/
plottrack.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
#
# plot the track and its properties
# getGMapImage()
# doPlot()
# <- Last updated: Sat May 1 17:11:20 2021 -> SGK
#
import numpy as np
from math import cos,sin,atan,pi,sqrt
#
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
#
# extra function from my .py files
from utilslib import formatTime, formatTimeLabels, putID, saveFig
from dlsq_fit import dlsq_fit
#
# ------------------------------------------------------------------------
# get the Google Map image
# return the image and its boundaries (in miles)
def getGMapImage(useRoad,
lonRef = -71.3646464, # some lon/lat ref locations
latRef = 42.4358983):
"""
read a screen shot of a google map and return it as an image
plus the bounding box of the image in miles wrt to a ref lat/lon
useRoad is True then read gmap-road.jpg
False gmap-satellite.jpg
both jpg must be in the cwd
I saved such a pair of images using a screen shot of google map,
and calibrated it to get lon/lat at two positions
to derive the image scaling by dropping two markers,
saving these two extra images and measuring the markers
location in pixel units.
This is set for an area where I go biking
you can do the same to customize this for a different area.
"""
#
# grabbed a map and two known coords at zoom = 12
## https://www.google.com/maps/@42.4358983,-71.3646464,12z
## (latRef, lonRef) = (42.4358983, -71.3646464)
#
if useRoad:
## 1510 x 864, x/y pos of two markers
gmapImage = plt.imread("gmap-road.jpg")
(xps1, yps1) = ( 132.5, 740.5)
(xps2, yps2) = (1326.5, 117.5)
marker = '.b'
color = 'red'
else:
## 1514 x 867, x/y pos of two markers
gmapImage = plt.imread("gmap-satellite.jpg")
(xps1, yps1) = ( 132.5, 740.5+4)
(xps2, yps2) = (1326.5, 117.5+4)
marker = '.y'
color = 'yellow'
#
# lon/lat of these two markers
(lat1, lon1) = (42.523128, -71.579051)
(lat2, lon2) = (42.365291, -71.169124)
#
# cut/trim this image to remove ugly bits from the screen shot
(h, w, d) = gmapImage.shape
(xcut, xtrm) = ( 0, 0)
(ycut, ytrm) = (65, 0)
xps1 -= xcut
xps2 -= xcut
yps1 -= ytrm
yps2 -= ytrm
xmax = w-xtrm
ymax = h-ytrm
gmapImage = gmapImage[ycut:ymax, xcut:xmax, :]
(imageHeight, imageWidth, imageDepth) = gmapImage.shape
#
# conversion factors
deg2rad = pi/180.0
km2mi = 0.621371 # km to mi
#
# convert to miles
earthRad = 6367.449 # km
earthRad *= km2mi # mi
# some simple spherical trig
earthRho = earthRad*cos(latRef*deg2rad)
#
xVal1 = earthRho*atan((lon1-lonRef)*deg2rad)
yVal1 = earthRad*atan((lat1-latRef)*deg2rad)
#
xVal2 = earthRho*atan((lon2-lonRef)*deg2rad)
yVal2 = earthRad*atan((lat2-latRef)*deg2rad)
#
# get the x scaling xV = xOff + xScl * xP
xScl = (xVal2-xVal1)/(xps2-xps1)
xOff = xVal2 - xps2*xScl
#
# get the y scaling yV = yOff + yScl * yP
yScl = (yVal2-yVal1)/(yps2-yps1)
yOff = yVal2 - yps2*yScl
#
# boundingbox in miles relative to ref lat/lon
xMin = xOff
yMin = yOff
xMax = xOff + xScl*imageWidth
yMax = yOff + yScl*imageHeight
#
# return needed vars
return (gmapImage, xMin, xMax, yMin, yMax, marker, color)
#
# ------------------------------------------------------------------------
# plot the track
def doPlot(data, infos, stats,
plotType = 'pdf', # type of plot
noRoute = False, # don't show route
useRoad = False, # overplot on road or satellite image
plotVS = 'Time', # props vs time or distance
plotSize = (12, 8), # size of plot windows
velMin = 6.0, # define when moving etc
velMax = 100.0,
cadMin = 50):
"""
plot the data
fig1: route on top of a map or using google map -> html
fig2: ride properties vs time or distance
plotType type of plot like 'pdf' or 'x'
noRoute don't show route if True
useRoad overplot on road (True) or satellite image (False)
plotVS props vs time or distance
plotSize size of plot windows
velMin define when moving, etc
velMax
cadMin
"""
#
# decode infos -> index[] and units[]
nCols, nLines = data.shape
index = {}
units = {}
i = 0
for wx in infos.split():
w = wx.split(':')
index[w[0]] = i
units[w[0]] = w[1]
i += 1
#
# get the stats
avgMVel = stats['avgMVel']
maxMVel = stats['maxMVel']
avgHR = stats['avgHeartRate']
maxHR = stats['maxHeartRate']
avgCad = stats['avgCadence']
maxCad = stats['maxCadence']
#
# find the max(running mean moving velocity)
ix = index['MeanMVel']
mxxVel = max(data[ix, :])
#
# reject NaN postn values
ix = index['XPosition']
iy = index['YPosition']
mask1 = np.logical_not(np.isnan(data[ix, :]))
mask2 = np.logical_not(np.isnan(data[iy, :]))
mask0 = mask1 & mask2
#
# reject velocities outside [velMin, velMax]
iv = index['Velocity']
mask1 = data[iv, :] > velMin
mask2 = data[iv, :] < velMax
#
# final mask
mask = mask0 & mask1 & mask2 ## & mask3
#
fmtStr = 'Started {}\n' + \
'Time total {}, moving {}, paused {}\n' + \
'Distance traveled total {:.1f}, moving {:.1f} mi\n' + \
'Minimun moving velocity {:.1f} mph'
str = fmtStr.format(stats['startTime'],
formatTime(stats['totalTime']),
formatTime(stats['movingTime']),
formatTime(stats['totalTime']- stats['movingTime']),
stats['distance'], stats['mvgDistance'], velMin)
#
if not noRoute:
#
# draw first figure, unless noRoute is True
fig1 = plt.figure(figsize = plotSize)
#
# get x/y position arrays of the ride
ix = index['XPosition']
iy = index['YPosition']
xPos = data[ix, mask]
yPos = data[iy, mask]
#
# get the Google map and its bounding box (in miles)
# plus which marker and color to use
(gmapImage, xMin, xMax, yMin, yMax, \
marker, color) = getGMapImage(useRoad)
#
# display the image
plt.imshow(gmapImage, extent=[xMin, xMax, yMin, yMax])
#
## mark the border
## plt.plot([xMin,xMax], [yMin, yMax], '.b')
# plot the route w/ set markers
plt.plot(xPos, yPos, marker, markersize= 1.0 )
# title and labels
plt.title('Route')
plt.xlabel('x-position [mi]')
plt.ylabel('y-position [mi]')
# add some text
xx = xMin+0.5
yy = yMax-0.5
#
fmtStr = '\nVelocity average {:6.2f} max {:6.2f} mph\n' + \
'Cadence average {:6.2f} max {:6.2f} rpm\n' + \
'HR average {:6.2f} max {:6.2f} bpm'
strX = str + fmtStr.format(avgMVel, maxMVel,
avgCad, maxCad,
avgHR, maxHR)
plt.text(xx, yy, strX, color = color, va = 'top')
#
# add an ID on fig1 if not plotting on screen
if not ((plotType == 'x') or (plotType == 'w')):
putID(plt)
#
# draw second figure
fig2 = plt.figure(figsize = plotSize)
#
# what to plot?
# specify Var1-Var2
if (plotVS == 'Time'):
plotList = 'Time-Velocity Time-HeartRate Time-Altitude ' + \
'Time-Cadence Time-Grade Grade-Velocity'
else:
plotList = 'MovingDistance-Velocity MovingDistance-HeartRate ' + \
'MovingDistance-Altitude ' + \
'MovingDistance-Cadence MovingDistance-Grade Grade-Velocity'
#
# init frame index
k = 1
# loop on plot list, broken at spaces
for p in plotList.split():
v = p.split('-')
#
# index for variables
ix = index[v[0]]
iy = index[v[1]]
#
# set the subplot on a 3x2 grid
ax = plt.subplot(3, 2, k)
#
# do not plot low cadence values
if (v[1] == 'Cadence'):
m = data[iy, :] > cadMin
m = m & mask
else:
m = mask
#
# plot the data, using small dot (pixel) as marker
ax.plot(data[ix, m], data[iy, m], ',')
#
# if vs time, use my tick labels
# and set the x-label
if (v[0] == 'Time'):
ax.xaxis.set_major_formatter(FuncFormatter(formatTimeLabels))
plt.xlabel(v[0]+' [hh:mm]')
else:
plt.xlabel(v[0]+' ['+units[v[0]]+']')
#
# set y-label and title
plt.ylabel('['+units[v[1]]+']')
plt.title(v[1])
#
# set xp/yp as min/max of x/y
xp = np.empty(2)
yp = np.empty(2)
xp[0] = min(data[ix, mask])
xp[1] = max(data[ix, mask])
#
# add'l stuff depending on which var is being plotted
if (v[0] == 'Grade'):
#
# if plot vs grade, add a linear fit
(n, c) = dlsq_fit(data[ix, m], data[iy, m])
yp[0] = c[0] + c[1]*xp[0]
yp[1] = c[0] + c[1]*xp[1]
# draw the lin fit, w/ a green dot-dashed line
plt.plot(xp, yp, '-.g')
#
# where to put text, assume v[1] is velocity
xx = xp[0]
yy = min(data[iy, m])
plt.text(xx, yy, '{:.1f} mph/10%'.format(c[1]*10),color='g')
#
else:
if (v[1] == 'Velocity'):
#
# draw line at avg moving velocity
yp[0] = avgMVel
yp[1] = avgMVel
plt.plot(xp, yp, '-.r')
# draw the up-to-then mean moving velocity, in green
ix = index[v[0]]
iy = index['MeanMVel']
plt.plot(data[ix, mask], data[iy, mask], color='g')
#
# add the avg mvg vel, max(mean mvg vel) and max(vel)
# in red, green and blue
xx = xp[0]
yy = avgMVel+1
plt.text(xx, yy, '{:.1f}'.format(avgMVel), color = 'r')
#
xx = -(xp[1]-xp[0])*.05 + xp[1]
yy = maxMVel-4
plt.text(xx, yy, '{:.1f}'.format(maxMVel), color = 'b')
#
yy = mxxVel+1
plt.text(xx, yy, '{:.1f}'.format(mxxVel), color = 'g')
#
if (v[1] == 'HeartRate'):
#
# draw line at avg HR
yp[0] = avgHR
yp[1] = avgHR
plt.plot(xp, yp, '-.r')
#
# add the avg and max HR in red and blue
xx = xp[0]
yy = avgHR+2
plt.text(xx, yy, '{:.1f}'.format(avgHR), color = 'r')
#
xx = -(xp[1]-xp[0])*.075 + xp[1]
yy = maxHR-8
plt.text(xx, yy, '{:.1f}'.format(maxHR), color = 'b')
#
if (v[1] == 'Cadence'):
#
# draw line at avg cadence
yp[0] = avgCad
yp[1] = avgCad
plt.plot(xp, yp, '-.r')
#
# add avg and max cadence in red and blue
xx = xp[0]
yy = avgCad+5
plt.text(xx, yy, '{:.1f}'.format(avgCad), color = 'r')
#
xx = -(xp[1]-xp[0])*.075 + xp[1]
yy = maxCad-20
plt.text(xx, yy, '{:.1f}'.format(maxCad), color = 'b')
#
# next one
k += 1
#
# done, add the string str to last frame
# alignmt is va == vert aligmt set to 'top'
xx = min(data[ix, mask])
yy = max(data[iy, mask])
plt.text(xx, yy, str, fontsize = 6, va = 'top')
#
# use tight layout
fig2.tight_layout()
#
# either show the plot
if (plotType == 'x') or (plotType == 'w'):
plt.show()
# or save it to a file (pdf or png)
else:
# add the ID to the 2nd fig
putID(plt)
# save them to two files (unless noRoute == True)
if not noRoute:
saveFig(fig1, plotType, name='route')
saveFig(fig2, plotType, name='stats')