forked from jl2/gpxtools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgpxtools.lisp
332 lines (275 loc) · 12.3 KB
/
gpxtools.lisp
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
;;;; gpxtools.lisp
(in-package #:gpxtools)
(defun meters-to-feet (val)
(* 3.281 val))
(defun meters-to-miles (val)
(* (/ 1.0 1609.344) val))
(defun string-to-float (str)
(if (string/= str "")
(with-input-from-string
(in str)
(coerce (read in) 'double-float))
0.0d0)) ; If string is empty, just return zero.
(defgeneric to-vec3 (pt))
(defstruct utm-pt
(northing 0.0 :type double-float)
(easting 0.0 :type double-float)
(zone 0 :type fixnum)
(ele 0.0 :type double-float))
(defstruct gpx-pt
(lat 0.0 :type double-float)
(lon 0.0 :type double-float)
(ele 0.0 :type double-float)
(time "" :type string))
(defmethod to-vec3 ((pt gpx-pt))
(with-slots (lat lon ele) pt
(values (vec3 lat lon ele))))
(defun to-utm (pt &key (zone nil))
(let ((utm (utm:lat-lon-to-utm (gpx-pt-lat pt) (gpx-pt-lon pt) :zone zone)))
(make-utm-pt :easting (car utm) :northing (cadr utm) :zone (caddr utm) :ele (gpx-pt-ele pt))))
(defstruct gpx-segment
(points () :type list)
(point-count 0 :type integer)
(max-lat -361.0d0 :type double-float)
(min-lat 361.0d0 :type double-float)
(max-lon -361.0d0 :type double-float)
(min-lon 361.0d0 :type double-float))
(defstruct gpx-track
(name "" :type string)
(segments () :type list))
(defstruct gpx-file
(tracks () :type list))
(defun format-iso (tm)
(multiple-value-bind (sec min hr day mon yr dow dst-p tz)
(decode-universal-time tm)
(declare (ignore dow dst-p tz))
(format nil "~4,'0d-~2,'0d-~2,'0dT~2,'0d:~2,'0d:~2,'0dZ" yr mon day hr min sec)))
(defgeneric write-gpx (el stream)
(:documentation "Write a GPX element to a file."))
(defmethod write-gpx ((pt gpx-pt) (stm stream))
(format stm "<trkpt lat=\"~,9f\" lon=\"~,9f\"><ele>~,9f</ele><time>~a</time></trkpt>~%"
(gpx-pt-lat pt)
(gpx-pt-lon pt)
(gpx-pt-ele pt)
(gpx-pt-time pt)))
(defmethod write-gpx ((seg gpx-segment) (stm stream))
(format stm "<trkseg>")
(loop for i in (gpx-segment-points seg) do
(write-gpx i stm))
(format stm "</trkseg>"))
(defmethod write-gpx ((track gpx-track) (stm stream))
(format stm "<trk><name>~a</name>" (gpx-track-name track))
(loop for seg in (gpx-track-segments track) do
(write-gpx seg stm))
(format stm "</trk>"))
(defmethod write-gpx ((file gpx-file) (file-name string))
(with-open-file
(stream file-name :direction :output)
(format stream "<?xml version=\"1.0\" encoding=\"UTF-8\"?><gpx version=\"1.0\" creator=\"gpxtools\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.topografix.com/GPX/1/0\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">")
(format stream "<time>~a</time>" (format-iso (get-universal-time)))
(loop for track in (gpx-file-tracks file) do
(write-gpx track stream))
(format stream "</gpx>")))
(defun read-gpx (file-name)
(let ((doc (cxml:parse-file file-name (cxml-dom:make-dom-builder)))
(rval ()))
(labels
((process-track
(track-node)
(let ((segs ())
(tname ""))
(xpath:do-node-set
(node (xpath:evaluate "gpx:trkseg" track-node))
(let ((name (xpath:evaluate "string(gpx:name)" track-node)))
(setf tname name))
(setf segs (cons (process-trackseg node) segs)))
(make-gpx-track :segments segs :name tname)))
(process-trackseg
(seg-node)
(let ((track (make-gpx-segment)))
(xpath:do-node-set
(node (xpath:evaluate "gpx:trkpt" seg-node))
(let ((np (process-trackpt node)))
(setf (gpx-segment-points track) (cons np (gpx-segment-points track)))
(setf (gpx-segment-max-lat track) (max (gpx-pt-lat np) (gpx-segment-max-lat track)))
(setf (gpx-segment-min-lat track) (min (gpx-pt-lat np) (gpx-segment-min-lat track)))
(setf (gpx-segment-max-lon track) (max (gpx-pt-lon np) (gpx-segment-max-lon track)))
(setf (gpx-segment-min-lon track) (min (gpx-pt-lon np) (gpx-segment-min-lon track)))
(incf (gpx-segment-point-count track))))
track))
(process-trackpt
(pt-node)
(let ((lat (string-to-float (xpath:evaluate "string(@lat)" pt-node)))
(lon (string-to-float (xpath:evaluate "string(@lon)" pt-node)))
(ele (string-to-float (xpath:evaluate "string(gpx:ele)" pt-node)))
(time (xpath:evaluate "string(gpx:time)" pt-node)))
(make-gpx-pt :lat lat :lon lon :ele ele :time time))))
(xpath:with-namespaces
(("gpx" (xpath:evaluate "namespace-uri(/*)" doc)))
(xpath:do-node-set
(node (xpath:evaluate "/gpx:gpx/gpx:trk" doc))
(setf rval (cons (process-track node) rval)))))
(make-gpx-file :tracks rval)))
(defun distance-between (p1 p2)
(let* ((pt1 (to-utm p1))
(pt2 (to-utm p2 :zone (utm-pt-zone pt1) ))
(ndiff (- (utm-pt-northing pt1) (utm-pt-northing pt2)))
(ediff (- (utm-pt-easting pt1) (utm-pt-easting pt2)))
(eldiff (- (utm-pt-ele pt1) (utm-pt-ele pt2)))
(ndiff2 (* ndiff ndiff))
(ediff2 (* ediff ediff))
(eldiff2 (* eldiff eldiff)))
(sqrt (+ ndiff2 ediff2 eldiff2))))
;; (defgeneric distance (el)
;; (:documentation "Compute the distance of the element."))
;; (defmethod distance ((seg gpx-segment))
;; (loop for i in (gpx-segment-points seg)
;; for j in (cdr (gpx-segment-points seg))
;; summing (distance-between i j) into total
;; finally (return total)))
;; (defmethod distance ((track gpx-track))
;; (loop for seg in (gpx-track-segments track)
;; summing (distance seg) into total
;; finally (return total)))
;; (defmethod distance ((file gpx-file))
;; (loop for track in (gpx-file-tracks file)
;; summing (distance track)))
(defun elevation-diff (p1 p2)
(- (gpx-pt-ele p1) (gpx-pt-ele p2)))
(defun ele-gain (p1 p2)
(let ((diff (elevation-diff p1 p2)))
(if (< 0.0 diff)
diff
0.0)))
(defun ele-loss (p1 p2)
(let ((diff (elevation-diff p1 p2)))
(if (> 0.0 diff)
diff
0.0)))
(defgeneric traverse2 (el func)
(:documentation "Traverse the GPX element and sum the results of (func point[i] point[i+1])"))
(defmethod traverse2 ((seg gpx-segment) func)
(loop for i in (gpx-segment-points seg)
for j in (cdr (gpx-segment-points seg))
summing (apply func (list i j)) into total
finally (return total)))
(defmethod traverse2 ((track gpx-track) func)
(loop for seg in (gpx-track-segments track)
summing (traverse2 seg func) into total
finally (return total)))
(defmethod traverse2 ((file gpx-file) func)
(loop for track in (gpx-file-tracks file)
summing (traverse2 track func)))
(defgeneric collect-points (el)
(:documentation "Traverse the GPX element and sum the results of (func point[i] point[i+1])"))
(defmethod collect-points ((seg gpx-segment))
(gpx-segment-points seg))
(defmethod collect-points ((track gpx-track))
(loop for seg in (gpx-track-segments track)
appending (collect-points seg) into rval
finally (return rval)))
(defmethod collect-points ((file gpx-file))
(loop for track in (gpx-file-tracks file)
appending (collect-points track) into rval
finally (return rval)))
(defun elevation-gain (el)
(traverse2 el #'ele-gain))
(defun elevation-loss (el)
(traverse2 el #'ele-loss))
(defun distance (el)
(traverse2 el #'distance-between))
(defun get-summary (gpx &key (units 'metric))
(let ((eg (elevation-gain gpx))
(el (elevation-loss gpx))
(dist (distance gpx))
(shortunit (if (eq units 'imperial) "feet" "meters"))
(longunit (if (eq units 'imperial) "miles" "kilometers"))
(start-end-time (time-range gpx)))
(list
(list 'total-elevation-gain (if (eq units 'imperial) (meters-to-feet eg) eg) shortunit)
(list 'total-elevation-lost (if (eq units 'imperial) (meters-to-feet el) el) shortunit)
(list 'total-distance (if (eq units 'imperial) (meters-to-miles dist) (/ dist 1000.0)) longunit)
(list 'start-time (first start-end-time))
(list 'end-time (car (last start-end-time))))))
(defun summarize (gpx &key (units 'metric))
(let ((eg (elevation-gain gpx))
(el (elevation-loss gpx))
(dist (distance gpx))
(shortunit (if (eq units 'imperial) "feet" "meters"))
(longunit (if (eq units 'imperial) "miles" "kilometers"))
(start-end-time (time-range gpx)))
(format t "Total elevation gain: ~a ~a~%" (if (eq units 'imperial) (meters-to-feet eg) eg) shortunit)
(format t "Total elevation loss: ~a ~a~%" (if (eq units 'imperial) (meters-to-feet el) el) shortunit)
(format t "Total distance: ~a ~a~%" (if (eq units 'imperial) (meters-to-miles dist) (/ dist 1000.0)) longunit)
(format t "Start time: ~a~%" (first start-end-time))
(format t "End time: ~a~%" (car (last start-end-time)))))
(defun time-range (gpx)
"Returns a two-member list consisting of the earliest timestamp
and the last timestamp in the GPX file"
(let ((all-pts (collect-points gpx))
(timepoints ()))
(loop for i in all-pts
do (push (gpx-pt-time i) timepoints))
(let ((sortedtimes (sort timepoints #'string-lessp)))
(list (first sortedtimes) (car (last sortedtimes))))))
(defun elevation-plot (gpx &key (file-name))
(let ((all-pts (collect-points gpx))
(total-distance 0.0)
(new-points ()))
(loop for i in all-pts
for j in (cdr all-pts)
do
(incf total-distance (distance-between i j))
(push (list (meters-to-miles total-distance) (meters-to-feet (gpx-pt-ele i))) new-points))
(adw-charting:with-chart (:line 1600 1200)
(adw-charting:add-series "Elevation" new-points)
(adw-charting:set-axis :y "Elevation (feet)")
(adw-charting:set-axis :x "Distance (miles)")
(adw-charting:save-file file-name))))
(defun find-loop (gpx &key (eps 0.001))
(let* ((all-pts (collect-points gpx))
(first-pt (car all-pts)))
(format t "Checking ~a points~%" (length all-pts))
(loop for i in (cdr all-pts)
for j upto (- (length all-pts) 1) do
(let ((this-dist (distance-between first-pt i)))
(if (< this-dist eps)
(format t "Point ~a is ~a meters away from the start!~%" j this-dist))))))
(defun simplify (gpx &key (dist 0.1))
(let* ((all-pts (collect-points gpx))
(cur-pt (car all-pts))
(new-pts (list cur-pt)))
(loop for i in (cdr all-pts) do
(let ((this-dist (distance-between cur-pt i)))
(cond ((> this-dist dist)
(setf cur-pt i)
(push cur-pt new-pts)))))
(make-gpx-file
:tracks (list (make-gpx-track
:segments (list (make-gpx-segment :points (reverse new-pts) :point-count (length new-pts)))
:name "Simplified")))))
(defun gpx-file-from-track (track)
(make-gpx-file :tracks (list track)))
(defun read-directory-to-points (directory)
(let* ((files (directory (format nil "~a/*.gpx" directory)))
(all-points (apply
(curry #'concatenate 'list)
(mapcar
(compose (curry #'mapcar #'to-vec3) #'gpxtools:collect-points #'gpxtools:read-gpx)
files))))
(make-array (length all-points) :element-type 'vec3 :initial-contents all-points :adjustable nil)))
(defun read-directory-to-segments (directory)
(let* ((files (directory (format nil "~a/*.gpx" directory)))
(all-segments (apply
(curry #'concatenate 'list)
(mapcar
(compose (lambda (points)
(loop
for (from goes-to) on points by #'cdr
when goes-to
collect (cons from goes-to)))
(curry #'mapcar #'to-vec3)
#'gpxtools:collect-points
#'gpxtools:read-gpx)
files))))
(make-array (length all-segments) :element-type 'vec3 :initial-contents all-segments :adjustable nil)))