gooby 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. data/README +242 -0
  2. data/bin/example_usage.rb +38 -0
  3. data/bin/tests_gen.rb +16 -0
  4. data/data/20050305_corporate_cup_hm.csv +251 -0
  5. data/data/20050305_corporate_cup_hm.xml +2208 -0
  6. data/data/20050430_nashville_marathon.csv +1208 -0
  7. data/data/20050430_nashville_marathon.xml +10043 -0
  8. data/data/20051119_dowd_ymca_hm.csv +251 -0
  9. data/data/20051119_dowd_ymca_hm.xml +2210 -0
  10. data/data/20051124_hyatt_turkey_trot_8K.csv +321 -0
  11. data/data/20051124_hyatt_turkey_trot_8K.xml +2651 -0
  12. data/data/2007_03_03.tcx +6207 -0
  13. data/data/forerunner_2007.xml +259014 -0
  14. data/data/geo_data.txt +171 -0
  15. data/data/phx.csv +1280 -0
  16. data/data/phx.xml +10620 -0
  17. data/data/run_2007_01_01_16_38_27.xml +2020 -0
  18. data/data/run_2007_01_06_15_27_31.xml +2020 -0
  19. data/data/run_2007_01_10_12_25_47.xml +820 -0
  20. data/data/run_2007_01_10_22_44_54.csv +112 -0
  21. data/data/run_2007_01_10_22_44_54.xml +908 -0
  22. data/data/run_2007_01_11_10_48_45.xml +1292 -0
  23. data/data/run_2007_01_13_15_37_06.xml +1964 -0
  24. data/data/run_2007_01_14_15_46_02.xml +1368 -0
  25. data/data/run_2007_01_15_14_01_48.xml +1868 -0
  26. data/data/run_2007_01_20_16_22_05.xml +1702 -0
  27. data/data/run_2007_01_27_17_32_13.xml +3626 -0
  28. data/data/run_2007_01_28_19_14_52.xml +2538 -0
  29. data/data/run_2007_02_03_14_30_20.xml +2016 -0
  30. data/data/run_2007_02_04_18_02_30.xml +1476 -0
  31. data/data/run_2007_02_17_16_29_35.xml +1236 -0
  32. data/data/run_2007_02_19_14_44_33.xml +1816 -0
  33. data/data/run_2007_02_23_15_53_55.xml +36 -0
  34. data/data/run_2007_02_23_15_55_20.xml +1296 -0
  35. data/data/run_2007_02_24_15_01_35.csv +484 -0
  36. data/data/run_2007_02_24_15_01_35.xml +3884 -0
  37. data/data/test1.txt +4 -0
  38. data/img/gicons/blank.png +0 -0
  39. data/img/gicons/dd-end.png +0 -0
  40. data/img/gicons/dd-start.png +0 -0
  41. data/img/gicons/marker.png +0 -0
  42. data/img/gicons/marker0.png +0 -0
  43. data/img/gicons/marker00.png +0 -0
  44. data/img/gicons/marker01.png +0 -0
  45. data/img/gicons/marker02.png +0 -0
  46. data/img/gicons/marker03.png +0 -0
  47. data/img/gicons/marker04.png +0 -0
  48. data/img/gicons/marker05.png +0 -0
  49. data/img/gicons/marker06.png +0 -0
  50. data/img/gicons/marker07.png +0 -0
  51. data/img/gicons/marker08.png +0 -0
  52. data/img/gicons/marker09.png +0 -0
  53. data/img/gicons/marker1.png +0 -0
  54. data/img/gicons/marker10.png +0 -0
  55. data/img/gicons/marker11.png +0 -0
  56. data/img/gicons/marker12.png +0 -0
  57. data/img/gicons/marker13.png +0 -0
  58. data/img/gicons/marker14.png +0 -0
  59. data/img/gicons/marker15.png +0 -0
  60. data/img/gicons/marker16.png +0 -0
  61. data/img/gicons/marker17.png +0 -0
  62. data/img/gicons/marker18.png +0 -0
  63. data/img/gicons/marker19.png +0 -0
  64. data/img/gicons/marker2.png +0 -0
  65. data/img/gicons/marker20.png +0 -0
  66. data/img/gicons/marker21.png +0 -0
  67. data/img/gicons/marker22.png +0 -0
  68. data/img/gicons/marker23.png +0 -0
  69. data/img/gicons/marker24.png +0 -0
  70. data/img/gicons/marker25.png +0 -0
  71. data/img/gicons/marker26.png +0 -0
  72. data/img/gicons/marker27.png +0 -0
  73. data/img/gicons/marker28.png +0 -0
  74. data/img/gicons/marker29.png +0 -0
  75. data/img/gicons/marker3.png +0 -0
  76. data/img/gicons/marker30.png +0 -0
  77. data/img/gicons/marker31.png +0 -0
  78. data/img/gicons/marker32.png +0 -0
  79. data/img/gicons/marker33.png +0 -0
  80. data/img/gicons/marker34.png +0 -0
  81. data/img/gicons/marker35.png +0 -0
  82. data/img/gicons/marker36.png +0 -0
  83. data/img/gicons/marker37.png +0 -0
  84. data/img/gicons/marker38.png +0 -0
  85. data/img/gicons/marker39.png +0 -0
  86. data/img/gicons/marker4.png +0 -0
  87. data/img/gicons/marker40.png +0 -0
  88. data/img/gicons/marker41.png +0 -0
  89. data/img/gicons/marker42.png +0 -0
  90. data/img/gicons/marker43.png +0 -0
  91. data/img/gicons/marker44.png +0 -0
  92. data/img/gicons/marker45.png +0 -0
  93. data/img/gicons/marker46.png +0 -0
  94. data/img/gicons/marker47.png +0 -0
  95. data/img/gicons/marker48.png +0 -0
  96. data/img/gicons/marker49.png +0 -0
  97. data/img/gicons/marker5.png +0 -0
  98. data/img/gicons/marker50.png +0 -0
  99. data/img/gicons/marker51.png +0 -0
  100. data/img/gicons/marker52.png +0 -0
  101. data/img/gicons/marker53.png +0 -0
  102. data/img/gicons/marker54.png +0 -0
  103. data/img/gicons/marker55.png +0 -0
  104. data/img/gicons/marker56.png +0 -0
  105. data/img/gicons/marker57.png +0 -0
  106. data/img/gicons/marker58.png +0 -0
  107. data/img/gicons/marker59.png +0 -0
  108. data/img/gicons/marker6.png +0 -0
  109. data/img/gicons/marker60.png +0 -0
  110. data/img/gicons/marker61.png +0 -0
  111. data/img/gicons/marker62.png +0 -0
  112. data/img/gicons/marker63.png +0 -0
  113. data/img/gicons/marker64.png +0 -0
  114. data/img/gicons/marker65.png +0 -0
  115. data/img/gicons/marker66.png +0 -0
  116. data/img/gicons/marker67.png +0 -0
  117. data/img/gicons/marker68.png +0 -0
  118. data/img/gicons/marker69.png +0 -0
  119. data/img/gicons/marker7.png +0 -0
  120. data/img/gicons/marker70.png +0 -0
  121. data/img/gicons/marker71.png +0 -0
  122. data/img/gicons/marker72.png +0 -0
  123. data/img/gicons/marker73.png +0 -0
  124. data/img/gicons/marker74.png +0 -0
  125. data/img/gicons/marker75.png +0 -0
  126. data/img/gicons/marker76.png +0 -0
  127. data/img/gicons/marker77.png +0 -0
  128. data/img/gicons/marker78.png +0 -0
  129. data/img/gicons/marker79.png +0 -0
  130. data/img/gicons/marker8.png +0 -0
  131. data/img/gicons/marker80.png +0 -0
  132. data/img/gicons/marker81.png +0 -0
  133. data/img/gicons/marker82.png +0 -0
  134. data/img/gicons/marker83.png +0 -0
  135. data/img/gicons/marker84.png +0 -0
  136. data/img/gicons/marker85.png +0 -0
  137. data/img/gicons/marker86.png +0 -0
  138. data/img/gicons/marker87.png +0 -0
  139. data/img/gicons/marker88.png +0 -0
  140. data/img/gicons/marker89.png +0 -0
  141. data/img/gicons/marker9.png +0 -0
  142. data/img/gicons/marker90.png +0 -0
  143. data/img/gicons/marker91.png +0 -0
  144. data/img/gicons/marker92.png +0 -0
  145. data/img/gicons/marker93.png +0 -0
  146. data/img/gicons/marker94.png +0 -0
  147. data/img/gicons/marker95.png +0 -0
  148. data/img/gicons/marker96.png +0 -0
  149. data/img/gicons/marker97.png +0 -0
  150. data/img/gicons/marker98.png +0 -0
  151. data/img/gicons/marker99.png +0 -0
  152. data/img/gicons/markerA.png +0 -0
  153. data/img/gicons/markerB.png +0 -0
  154. data/img/gicons/markerC.png +0 -0
  155. data/img/gicons/markerD.png +0 -0
  156. data/img/gicons/markerE.png +0 -0
  157. data/img/gicons/markerF.png +0 -0
  158. data/img/gicons/markerG.png +0 -0
  159. data/img/gicons/markerH.png +0 -0
  160. data/img/gicons/markerI.png +0 -0
  161. data/img/gicons/markerJ.png +0 -0
  162. data/img/gicons/mm_20_red.png +0 -0
  163. data/img/gicons/mm_20_shadow.png +0 -0
  164. data/img/gicons/readme.txt +11 -0
  165. data/img/gicons/shadow50.png +0 -0
  166. data/lib/gooby/cls_counter_hash.rb +78 -0
  167. data/lib/gooby/cls_delim_line.rb +35 -0
  168. data/lib/gooby/cls_dttm.rb +79 -0
  169. data/lib/gooby/cls_duration.rb +79 -0
  170. data/lib/gooby/cls_forerunner_xml_parser.rb +178 -0
  171. data/lib/gooby/cls_forerunner_xml_splitter.rb +109 -0
  172. data/lib/gooby/cls_geo_data.rb +181 -0
  173. data/lib/gooby/cls_gooby_command.rb +35 -0
  174. data/lib/gooby/cls_gooby_object.rb +18 -0
  175. data/lib/gooby/cls_google_map_generator.rb +363 -0
  176. data/lib/gooby/cls_history.rb +33 -0
  177. data/lib/gooby/cls_lap.rb +22 -0
  178. data/lib/gooby/cls_line.rb +75 -0
  179. data/lib/gooby/cls_options.rb +67 -0
  180. data/lib/gooby/cls_position.rb +44 -0
  181. data/lib/gooby/cls_run.rb +194 -0
  182. data/lib/gooby/cls_simple_xml_parser.rb +41 -0
  183. data/lib/gooby/cls_test_regen.rb +182 -0
  184. data/lib/gooby/cls_track.rb +47 -0
  185. data/lib/gooby/cls_trackpoint.rb +200 -0
  186. data/lib/gooby/mod_introspect.rb +26 -0
  187. data/lib/gooby/mod_io.rb +58 -0
  188. data/lib/gooby/mod_project_info.rb +80 -0
  189. data/lib/gooby/mod_string.rb +19 -0
  190. data/lib/gooby/mod_test_helper.rb +15 -0
  191. data/lib/gooby.rb +2265 -0
  192. data/pkg/code_header.txt +21 -0
  193. data/pkg/pkg.rb +236 -0
  194. data/pkg/test_header.txt +19 -0
  195. data/samples/20041113_richmond_marathon.html +532 -0
  196. data/samples/20050305_corporate_cup_hm.html +448 -0
  197. data/samples/20050430_nashville_marathon.html +530 -0
  198. data/samples/gps_point_capture.html +54 -0
  199. data/samples/phoenix_marathon.html +542 -0
  200. data/samples/run_2007_01_10_22_44_54.html +201 -0
  201. data/samples/run_2007_02_24_15_01_35.html +298 -0
  202. data/tests/tc_cls_counter_hash.rb +107 -0
  203. data/tests/tc_cls_delim_line.rb +74 -0
  204. data/tests/tc_cls_dttm.rb +131 -0
  205. data/tests/tc_cls_duration.rb +51 -0
  206. data/tests/tc_cls_forerunner_xml_parser.rb +70 -0
  207. data/tests/tc_cls_geo_data.xxx +71 -0
  208. data/tests/tc_cls_gooby_object.rb +26 -0
  209. data/tests/tc_cls_google_map_generator.rb +109 -0
  210. data/tests/tc_cls_history.rb +46 -0
  211. data/tests/tc_cls_lap.rb +38 -0
  212. data/tests/tc_cls_line.rb +110 -0
  213. data/tests/tc_cls_options.rb +79 -0
  214. data/tests/tc_cls_position.rb +66 -0
  215. data/tests/tc_cls_run.rb +142 -0
  216. data/tests/tc_cls_simple_xml_parser.rb +50 -0
  217. data/tests/tc_cls_track.rb +70 -0
  218. data/tests/tc_cls_trackpoint.rb +145 -0
  219. data/tests/tc_mod_introspect.rb +32 -0
  220. data/tests/tc_mod_io.rb +53 -0
  221. data/tests/tc_mod_project_info.rb +79 -0
  222. data/tests/tc_mod_string.rb +58 -0
  223. data/tests/ts_gooby.rb +1237 -0
  224. metadata +270 -0
data/lib/gooby.rb ADDED
@@ -0,0 +1,2265 @@
1
+ # Packaged on Sat Mar 03 16:56:38 EST 2007
2
+
3
+ =begin
4
+
5
+ This file contains the classes and modules for the Gooby project.
6
+ Gooby = Google APIs + Ruby
7
+
8
+ See file 'tests/ts_gooby.rb' for the regression test suite.
9
+
10
+ Gooby - Copyright 2007 by Chris Joakim.
11
+ Gooby is available under GNU General Public License (GPL) license.
12
+
13
+ =end
14
+
15
+ require 'date'
16
+ require 'find'
17
+ require 'rbconfig'
18
+ require 'rexml/document'
19
+ require 'rexml/streamlistener'
20
+ require 'time'
21
+ require 'yaml'
22
+
23
+ module Gooby
24
+
25
+ =begin rdoc
26
+ This module is used to embed information about the Gooby project and
27
+ version into the codebase.
28
+
29
+ Gooby - Copyright 2007 by Chris Joakim.
30
+ Gooby is available under GNU General Public License (GPL) license.
31
+ =end
32
+
33
+ module GoobyProjectInfo
34
+
35
+ # Return a String version number, like '1.0.0'.
36
+ def project_version_number
37
+ '0.9.3'
38
+ end
39
+
40
+ # Return a String date, like '2007/02/25'.
41
+ def project_date
42
+ '2007/03/03'
43
+ end
44
+
45
+ # Return a String year, like '2007'.
46
+ def project_year
47
+ project_date[0...4] # start, length
48
+ end
49
+
50
+ # Return a String containing copyright, year, and author.
51
+ def project_copyright
52
+ "Copyright (C) #{project_year} #{project_author}"
53
+ end
54
+
55
+ # Return a String containing GNU/GPL, and the gpl.html URL.
56
+ def project_license
57
+ 'GNU General Public License (GPL). See http://www.gnu.org/copyleft/gpl.html'
58
+ end
59
+
60
+ # Return a String containing the project author name.
61
+ def project_author
62
+ 'Chris Joakim'
63
+ end
64
+
65
+ def google_maps_api_level
66
+ '2'
67
+ end
68
+
69
+ def some_new_thing
70
+ '2'
71
+ end
72
+
73
+ def tested_files
74
+ array = Array.new
75
+ array << 'cls_counter_hash.rb'
76
+ array << 'cls_delim_line.rb'
77
+ array << 'cls_dttm.rb'
78
+ array << 'cls_duration.rb'
79
+ array << 'cls_forerunner_xml_parser.rb'
80
+ array << 'cls_geo_data.rb'
81
+ array << 'cls_gooby_object.rb'
82
+ array << 'cls_google_map_generator.rb'
83
+ array << 'cls_history.rb'
84
+ array << 'cls_lap.rb'
85
+ array << 'cls_line.rb'
86
+ array << 'cls_options.rb'
87
+ array << 'cls_position.rb'
88
+ array << 'cls_run.rb'
89
+ array << 'cls_simple_xml_parser.rb'
90
+ array << 'cls_track.rb'
91
+ array << 'cls_trackpoint.rb'
92
+ array << 'mod_constants.rb'
93
+ array << 'mod_introspect.rb'
94
+ array << 'mod_io.rb'
95
+ array << 'mod_project_info.rb'
96
+ array << 'mod_string.rb'
97
+ array
98
+ end
99
+
100
+ end
101
+
102
+
103
+ module Con
104
+
105
+ public
106
+
107
+ def default_delimiter
108
+ return '|'
109
+ end
110
+
111
+ def invalid_distance
112
+ return -99999999
113
+ end
114
+
115
+ def invalid_minutes
116
+ return -99999999
117
+ end
118
+
119
+ def invalid_time
120
+ return -99999999
121
+ end
122
+
123
+ def invalid_latitude
124
+ return -1
125
+ end
126
+
127
+ def invalid_longitude
128
+ return -1
129
+ end
130
+
131
+ def invalid_altitude
132
+ return -1
133
+ end
134
+
135
+ end
136
+
137
+
138
+ module Introspect
139
+
140
+ # Return a String w/instance variable and method info.
141
+ def introspect
142
+ c = self.class
143
+ s = "Class: #{c} "
144
+ iv = self.instance_variables.sort
145
+ s << " ivc=#{iv.size}"
146
+ iv.each { |v| s << " #{v}" }
147
+ meth = self.methods.sort
148
+ s << " mc=#{meth.size}"
149
+ meth.each { |m| s << " #{m}" }
150
+ s << ""
151
+ s
152
+ end
153
+
154
+ # Return 'self.instance_variables'.
155
+ def to_yaml_properties
156
+ self.instance_variables
157
+ end
158
+
159
+ end
160
+
161
+
162
+ module GoobyIO
163
+
164
+ # Return an Array of lines in file, optionally stripped.
165
+ def read_lines(filename, strip=false)
166
+
167
+ array = IO.readlines(filename)
168
+ if strip
169
+ array = strip_lines(array)
170
+ end
171
+ return array
172
+ end
173
+
174
+ # Return an Array of lines in file per the given delimeter, optionally stripped.
175
+ def read_as_ascii_lines(filename, delim=10, strip=false)
176
+
177
+ array = Array.new
178
+ file = File.new(filename)
179
+ currLine = ''
180
+ bytesRead = 0
181
+ linesRead = 0
182
+
183
+ file.each_byte { |b|
184
+ bytesRead = bytesRead + 1
185
+ if (b == delim) # delim is 13 for quicken, 10 for address book xml
186
+ array << currLine
187
+ currLine = ''
188
+ linesRead = linesRead + 1
189
+ else
190
+ if (b < 127)
191
+ currLine << "#{b.chr}"
192
+ end
193
+ end
194
+ }
195
+
196
+ if currLine.size > 0
197
+ array << currLine
198
+ end
199
+ if strip
200
+ array = strip_lines(array)
201
+ end
202
+ return array
203
+ end
204
+
205
+ # Strip the lines/Strings; return a new Array.
206
+ def strip_lines(array)
207
+
208
+ newArray = Array.new
209
+ if (array != nil)
210
+ array.each { |line| line.strip! ; newArray << line }
211
+ end
212
+ return newArray
213
+ end
214
+
215
+ end
216
+
217
+
218
+ module GoobyString
219
+
220
+ def tokenize(string, delim, strip=false)
221
+ if string
222
+ tokens = string.split(delim)
223
+ if strip
224
+ tokens.each { |tok| tok.strip! }
225
+ end
226
+ tokens
227
+ else
228
+ Array.new
229
+ end
230
+ end
231
+
232
+ end
233
+
234
+
235
+ module TestHelper
236
+
237
+ def setup
238
+ puts "test: #{name}"
239
+ end
240
+
241
+ def teardown
242
+ @debug = false
243
+ end
244
+
245
+ end
246
+
247
+
248
+ =begin rdoc
249
+ This is the abstract superclass of several Gooby classes.
250
+ Includes modules GoobyIO, Introspect, and GoobyProjectInfo.
251
+ =end
252
+
253
+ class GoobyObject
254
+
255
+ include Gooby::GoobyIO
256
+ include Gooby::GoobyString
257
+ include Gooby::Introspect
258
+ include Gooby::GoobyProjectInfo
259
+ include Gooby::Con
260
+
261
+ end
262
+
263
+
264
+ =begin rdoc
265
+ This class wrappers a Hash object and provides increment/decrement functionality
266
+ for a given key. It is used to sum the number of things (i.e. - xml tags) in a
267
+ collection.
268
+ =end
269
+
270
+ class CounterHash < GoobyObject
271
+
272
+ def initialize
273
+ @hash = Hash.new(0)
274
+ end
275
+
276
+ # Return the Integer count for the given key; zero default.
277
+ def value(key)
278
+ (@hash.has_key?(key)) ? @hash[key] : 0
279
+ end
280
+
281
+ # Increment the count for the given key.
282
+ def increment(key)
283
+ if key == nil
284
+ return
285
+ end
286
+ if (@hash.has_key?(key))
287
+ val = @hash[key]
288
+ @hash[key] = val + 1
289
+ else
290
+ @hash[key] = 1
291
+ end
292
+ end
293
+
294
+ # Decrement the count for the given key.
295
+ def decrement(key)
296
+ if key == nil
297
+ return
298
+ end
299
+ if (@hash.has_key?(key))
300
+ val = @hash[key]
301
+ @hash[key] = val - 1
302
+ else
303
+ @hash[key] = -1
304
+ end
305
+ end
306
+
307
+ # Return an Array of the sorted keys.
308
+ def sorted_keys
309
+ @hash.keys.sort
310
+ end
311
+
312
+ # Return a String containing all key=val pairs.
313
+ def to_s
314
+ s = "CHash:"
315
+ sorted_keys.each { |key|
316
+ val = @hash[key]
317
+ s << " key: [#{key}] val: [#{val}]"
318
+ }
319
+ s
320
+ end
321
+
322
+ # Return an XML String containing all key=val pairs, optionally aligned.
323
+ def to_xml(aligned=false)
324
+ s = "<CHash>"
325
+ sorted_keys.each { |key|
326
+ val = @hash[key]
327
+ (aligned) ? s << "\n " : s << ''
328
+ s << " <entry key='#{key}' value='#{val}'/>"
329
+ }
330
+ if aligned
331
+ s << "\n "
332
+ end
333
+ s << " </CHash>"
334
+ s
335
+ end
336
+
337
+ end
338
+
339
+
340
+ =begin rdoc
341
+ Instances of this class represent a delimited line of text, such as csv.
342
+ =end
343
+
344
+ class DelimLine < GoobyObject
345
+
346
+ attr_reader :line, :trim, :delim, :tokens
347
+
348
+ def initialize(line, trim=true, delim=default_delimiter)
349
+ @line = line
350
+ @trim = trim
351
+ @delim = delim
352
+ @tokens = @line.split(@delim)
353
+ if trim
354
+ @tokens.each { | token | token.strip! }
355
+ end
356
+ end
357
+
358
+ def as_trackpoint(num_idx, lat_idx, lng_idx, alt_idx, dttm_idx)
359
+ Trackpoint.new(@tokens[num_idx], @tokens[lat_idx], @tokens[lng_idx], @tokens[alt_idx], @tokens[dttm_idx])
360
+ end
361
+
362
+ def is_comment?
363
+ @line.strip.match('^#') ? true : false
364
+ end
365
+
366
+ def to_s
367
+ "DelimLine: length: #{@line.size} trim: #{@trim} delim: #{@delim} tokens: #{@tokens.size}"
368
+ end
369
+
370
+ end
371
+
372
+
373
+ =begin rdoc
374
+ Instances of this class represent a Date and Time as parsed from a value
375
+ such as '2006-01-15T13:41:40Z' in an XML file produced by a GPS device.
376
+ It wrappers both a DateTime and Time object.
377
+ =end
378
+
379
+ class DtTm < GoobyObject
380
+
381
+ attr_accessor :rawdata, :dateTime, :time, :valid
382
+
383
+ # Constructor; arg is a String like '2006-01-15T13:41:40Z'.
384
+ def initialize(raw)
385
+ if raw
386
+ @rawdata = raw.strip
387
+ if @rawdata.size > 18
388
+ @dateTime = DateTime.parse(@rawdata[0..18])
389
+ @time = Time.parse(@dateTime.to_s)
390
+ @valid = true
391
+ else
392
+ @valid = false
393
+ end
394
+ else
395
+ @rawdata = ''
396
+ @valid = false
397
+ end
398
+ end
399
+
400
+ public
401
+
402
+ # Return @time.to_i
403
+ def to_i()
404
+ (@time) ? @time.to_i : invalid_time
405
+ end
406
+
407
+ # Calculates and returns diff between another instance.
408
+ def seconds_diff(anotherDtTm)
409
+ if anotherDtTm
410
+ to_i - anotherDtTm.to_i
411
+ else
412
+ invalid_time
413
+ end
414
+ end
415
+
416
+ def yyyy_mm_dd
417
+ @time.strftime("%Y-%m-%d")
418
+ end
419
+
420
+ def yyyy_mm_dd_hh_mm_ss
421
+ @time.strftime("%Y-%m-%d %H:%M:%S")
422
+ end
423
+
424
+ def hh_mm_ss
425
+ @time.strftime("%H:%M:%S")
426
+ end
427
+
428
+ # Calculate and return time diff in 'hh:mm:ss' format.
429
+ def hhmmss_diff(anotherDtTm)
430
+ if anotherDtTm
431
+ t = @time - (anotherDtTm.to_i)
432
+ t.strftime("%H:%M:%S")
433
+ else
434
+ '??:??:??'
435
+ end
436
+ end
437
+
438
+ def to_s
439
+ "#{@rawdata}"
440
+ end
441
+
442
+ # Return a String with state values for debugging.
443
+ def print_string
444
+ "DtTm: #{yyyy_mm_dd_hh_mm_ss} #{to_i} #{@rawdata}"
445
+ end
446
+
447
+ end
448
+
449
+
450
+ =begin rdoc
451
+ Instances of this class represent the contents of a Forerunner extract
452
+ <Duration> tag, such as:
453
+ <Duration>PT507.870S</Duration>
454
+ =end
455
+
456
+ class Duration < GoobyObject
457
+
458
+ attr_accessor :rawdata, :seconds, :minutes, :mmss
459
+
460
+ # Constructor; arg is a String like 'PT507.870S'.
461
+ def initialize(raw)
462
+ if raw
463
+ @rawdata = scrub(raw)
464
+ @seconds = @rawdata.to_f
465
+ @minutes = @seconds / 60
466
+ @base_min = @minutes.floor
467
+ @frac_min = @minutes - @base_min
468
+ @frac_sec = @frac_min * 60
469
+
470
+ @mmss = ''
471
+ if (@base_min < 10)
472
+ @mmss << "0#{@base_min}:"
473
+ else
474
+ @mmss << "#{@base_min}:"
475
+ end
476
+ if (@frac_sec < 10)
477
+ @mmss << "0#{@frac_sec}"
478
+ else
479
+ @mmss << "#{@frac_sec}"
480
+ end
481
+ if (@mmss.size > 8)
482
+ @mmss = @mmss[0..8]
483
+ end
484
+ else
485
+ @rawdata = ''
486
+ @seconds = invalidDistance
487
+ @minutes = invalidMinutes
488
+ @mmss = '??:??.?'
489
+ end
490
+ end
491
+
492
+ private
493
+
494
+ def scrub(raw)
495
+ if (raw)
496
+ raw.strip!
497
+ newStr = ''
498
+ raw.each_byte { | b |
499
+ if ((b >= 48) && (b <= 57))
500
+ newStr << b
501
+ end
502
+ if (b == 46)
503
+ newStr << b
504
+ end
505
+ }
506
+ return newStr
507
+ else
508
+ ''
509
+ end
510
+ end
511
+
512
+ public
513
+
514
+ def to_s
515
+ "#{@mmss}"
516
+ end
517
+
518
+ # Return a String with state values for debugging.
519
+ def print_string
520
+ "Duration: #{@rawdata} sec: #{@seconds} min: #{@minutes} mmss: #{@mmss} bm: #{@base_min} fm: #{@frac_min} fs: #{@frac_sec}"
521
+ end
522
+
523
+ end
524
+
525
+
526
+ =begin rdoc
527
+ Instances of this class are used to parse a Forerunner XML file in a SAX-like
528
+ manner. Instances of the model classes - History, Run, Track, Trackpoint,
529
+ etc. are created in this parsing process.
530
+
531
+ See http://www.garmin.com/xmlschemas/ForerunnerLogbookv1.xsd for the XML Schema
532
+ Definition for the Garmin Forerunner XML. The Gooby object model mirrors this XSD.
533
+ =end
534
+
535
+ class ForerunnerXmlParser
536
+
537
+ DETAIL_TAGS = %w( Notes StartTime Duration Length Latitude Longitude Altitude Time BeginPosition EndPosition )
538
+
539
+ include REXML::StreamListener
540
+
541
+ attr_reader :history, :cvHash, :tagCount
542
+
543
+ def initialize
544
+ @cvHash = Hash.new("")
545
+ @tagCount = 0
546
+ @runCount = 0
547
+ @lapCount = 0
548
+ @trackCount = 0
549
+ @trackpoint_count = 0
550
+ @currText = "";
551
+ @history = History.new
552
+ @currRun = nil
553
+ @currLap = nil
554
+ @currTrack = nil
555
+ @currBeginPosition = nil
556
+ @currEndPosition = nil
557
+ end
558
+
559
+ public
560
+
561
+ # SAX API method; handles 'Run', 'Lap', 'Track'.
562
+ def tag_start(tagname, attrs)
563
+ @tagCount += 1
564
+ @currTag = tagname
565
+ @cvHash[tagname] = ''
566
+
567
+ if detail_tag?(tagname)
568
+ @inDetail = true
569
+ end
570
+
571
+ if is_tag?('Run', tagname)
572
+ @runCount = @runCount + 1
573
+ @lapCount = 0
574
+ @trackCount = 0
575
+ @currRun = Run.new(@runCount)
576
+ @history.add_run(@currRun)
577
+ @cvHash['Notes'] = ''
578
+ return
579
+ end
580
+
581
+ if is_tag?('Lap', tagname)
582
+ @lapCount = @lapCount + 1
583
+ @currLap = Lap.new(@lapCount)
584
+ return
585
+ end
586
+
587
+ if is_tag?('Track', tagname)
588
+ @trackCount = @trackCount + 1
589
+ @currTrack = Track.new(@trackCount)
590
+ @trackpoint_count = 0
591
+ return
592
+ end
593
+
594
+ end
595
+
596
+ # SAX API method; handles 'Position', 'Trackpoint', 'Track', 'Lap', 'Run'.
597
+ def tag_end(tagname)
598
+ if @inDetail
599
+ @cvHash[tagname] = @currText
600
+ else
601
+ if is_tag?('Position', tagname)
602
+ lat = @cvHash['Latitude']
603
+ long = @cvHash['Longitude']
604
+ @currBeginPosition = Position.new(lat.strip, long.strip, '')
605
+ @currEndPosition = Position.new(lat.strip, long.strip, '')
606
+ end
607
+
608
+ if is_tag?('BeginPosition', tagname)
609
+ lat = @cvHash['Latitude']
610
+ long = @cvHash['Longitude']
611
+ @currBeginPosition = Position.new(lat.strip, long.strip, '')
612
+ end
613
+
614
+ if is_tag?('EndPosition', tagname)
615
+ lat = @cvHash['Latitude']
616
+ long = @cvHash['Longitude']
617
+ @currEndPosition = Position.new(lat.strip, long.strip, '')
618
+ end
619
+
620
+ if is_tag?('Trackpoint', tagname)
621
+ @trackpoint_count = @trackpoint_count + 1
622
+ lat = @cvHash['Latitude']
623
+ long = @cvHash['Longitude']
624
+ alt = @cvHash['Altitude']
625
+ time = @cvHash['Time']
626
+ tp = Trackpoint.new(@trackpoint_count, lat, long, alt, time)
627
+ @currTrack.add_trackpoint(tp)
628
+ end
629
+
630
+ if is_tag?('Track', tagname)
631
+ if @currRun != nil
632
+ @currRun.add_track(@currTrack)
633
+ end
634
+ end
635
+
636
+ if is_tag?('Lap', tagname)
637
+ @currLap.startTime = @cvHash['StartTime']
638
+ @currLap.duration = Duration.new(@cvHash['Duration'])
639
+ @currLap.length = @cvHash['Length']
640
+ @currLap.beginPosition = @currBeginPosition
641
+ @currLap.endPosition = @currEndPosition
642
+ @currRun.add_lap(@currLap)
643
+ end
644
+
645
+ if is_tag?('Run', tagname)
646
+ @currRun.notes = @cvHash['Notes']
647
+ end
648
+ end
649
+
650
+ @inDetail = false
651
+ @currText = ""
652
+ @currTag = ""
653
+ end
654
+
655
+ # SAX API method.
656
+ def text(txt)
657
+ if @inDetail
658
+ @currText = @currText + txt
659
+ end
660
+ end
661
+
662
+ # Iterate all parsed Run objects and print each with to_s.
663
+ def gdump()
664
+ @history.runs().each { |run| puts run.to_s }
665
+ end
666
+
667
+ # Iterate all parsed Run objects and print each with to_s.
668
+ def dump()
669
+ @history.runs().each { |run| puts run.to_s }
670
+ end
671
+
672
+ # Iterate all parsed Run objects and print each with put_csv.
673
+ def put_run_csv()
674
+ @history.runs().each { |run| run.put_csv() }
675
+ end
676
+
677
+ # Iterate all parsed Run objects and print each with put_tkpt_csv.
678
+ def put_all_run_tkpt_csv(with_header_comment)
679
+ @history.runs.each { |run|
680
+ run.put_tkpt_csv(with_header_comment)
681
+ }
682
+ end
683
+
684
+ private
685
+
686
+ def is_tag?(tagname, value)
687
+ tagname == value
688
+ end
689
+
690
+ def detail_tag?(tagname)
691
+ DETAIL_TAGS.each { |typ|
692
+ if typ == tagname
693
+ return true
694
+ end
695
+ }
696
+ return false
697
+ end
698
+
699
+ end
700
+
701
+
702
+ =begin rdoc
703
+ Instances of this class are used to split a large ForerunnerLogbook
704
+ XML file into individual 'run_' files.
705
+ =end
706
+
707
+ class ForerunnerXmlSplitter < GoobyObject
708
+
709
+ attr_reader :out_dir, :forerunner_files, :out_files_hash
710
+
711
+ def initialize(xml_file, out_dir)
712
+ @out_dir = out_dir
713
+ @forerunner_files = Array.new
714
+ @forerunner_files << xml_file
715
+ @out_files_hash = Hash.new
716
+ end
717
+
718
+ def split
719
+ @forerunner_files.each { |f| process_file(f) }
720
+ write_files
721
+ end
722
+
723
+ private
724
+
725
+ def process_file(forerunnerXmlFile)
726
+ @file_name = forerunnerXmlFile
727
+ @xml_lines = read_lines(@file_name, false)
728
+ @line_num = 0
729
+ @run_num = 0
730
+ @curr_run_lines = Array.new
731
+ @curr_run_tkpts = 0
732
+ @start_line_num = 0
733
+ @end_line_num = 0
734
+ @first_start_time = nil
735
+
736
+ @xml_lines.each { |line|
737
+ @line_num = @line_num + 1
738
+ if (line.match(/<Run>/))
739
+ @run_num = @run_num + 1
740
+ @start_line_num = @line_num
741
+ @curr_run_lines = Array.new
742
+ @curr_run_lines << line
743
+ elsif (line.match(/<StartTime>/)) # <StartTime>2007-01-13T15:37:06Z</StartTime>
744
+ @curr_run_lines << line
745
+ if @first_start_time == nil
746
+ clone = String.new(line)
747
+ clone.gsub!(/[<>]/, ' ')
748
+ clone.gsub!(/[-:T]/, '_')
749
+ clone.gsub!(/[Z]/, '')
750
+ tokens = clone.split
751
+ @first_start_time = tokens[1]
752
+ end
753
+ elsif (line.match(/<Trackpoint>/))
754
+ @curr_run_tkpts = @curr_run_tkpts + 1
755
+ @curr_run_lines << line
756
+ elsif (line.match(/<\/Run>/))
757
+ @end_line_num = @line_num
758
+ @curr_run_lines << line
759
+ end_run
760
+ elsif (@curr_run_lines.size > 0)
761
+ @curr_run_lines << line
762
+ end
763
+ }
764
+ end
765
+
766
+ def end_run
767
+ out_file = "#{@out_dir}/run_#{@first_start_time}.xml"
768
+ comment = "<!-- file: #{out_file} lines: #{@curr_run_lines.size} (#{@start_line_num} to #{@end_line_num}) tkpts: #{@curr_run_tkpts} --> \n"
769
+ @curr_run_lines.insert(0, comment)
770
+
771
+ prev_entry = @out_files_hash[out_file]
772
+ if prev_entry
773
+ if (@curr_run_lines.size >= prev_entry.size)
774
+ puts "previous entry overlaid for #{out_file}. curr=#{@curr_run_lines.size} prev=#{prev_entry.size}"
775
+ @out_files_hash[out_file] = @curr_run_lines
776
+ else
777
+ puts "previous entry retained for #{out_file}. curr=#{@curr_run_lines.size} prev=#{prev_entry.size}"
778
+ end
779
+ else
780
+ puts "new entry for #{out_file}. curr=#{@curr_run_lines.size}"
781
+ @out_files_hash[out_file] = @curr_run_lines
782
+ end
783
+
784
+ @curr_run_lines = Array.new
785
+ @curr_run_tkpts = 0
786
+ @start_line_num = 0
787
+ @end_line_num = 0
788
+ @first_start_time = nil
789
+ end
790
+
791
+ def write_files
792
+ out_names = @out_files_hash.keys.sort
793
+ puts "Writing #{out_names.size} extract files..."
794
+ out_names.each { |out_name|
795
+ lines = @out_files_hash[out_name]
796
+ out = File.new out_name, "w+"
797
+ lines.each { |line| out.write line }
798
+ out.flush
799
+ out.close
800
+ puts "File written: #{out_name}"
801
+ }
802
+ puts "output files written."
803
+ end
804
+
805
+ end
806
+
807
+
808
+ =begin rdoc
809
+ Instances of this class represent a the set of Geographic data defined in file geo.txt
810
+ =end
811
+
812
+ class GeoData < GoobyObject
813
+
814
+ attr_reader :filename, :lines, :poi_hash, :poi_array, :track_hash, :track_array, :route_hash, :route_array
815
+
816
+ def initialize(filename)
817
+ @filename = filename
818
+ @filename = 'data/geo_data.txt' if @filename == nil
819
+ @lines = read_lines(@filename, true)
820
+ @poi_hash = Hash.new
821
+ @poi_array = Array.new
822
+ @track_hash = Hash.new
823
+ @track_array = Array.new
824
+ @route_hash = Hash.new
825
+ @route_array = Array.new
826
+ parse_poi
827
+ parse_tracks
828
+ parse_routes
829
+ end
830
+
831
+ private
832
+
833
+ def parse_poi
834
+ in_poi, poi_number = false, 0
835
+ @lines.each { |line|
836
+ line_obj = Line.new(line, nil, true)
837
+ tok_count = line_obj.token_count
838
+ is_point = line_obj.token_idx_equals(0, '.')
839
+
840
+ if line_obj.is_populated_non_comment
841
+ if line_obj.match('points_of_interest_start')
842
+ in_poi = true
843
+ elsif line_obj.match('points_of_interest_end')
844
+ in_poi = false
845
+ elsif in_poi && tok_count > 2 && is_point
846
+ poi_number = poi_number + 1
847
+ tkpt = Trackpoint.new(
848
+ poi_number, line_obj.tokens[1], line_obj.tokens[2],
849
+ '0', '', line_obj.concatinate_tokens(3))
850
+ add_poi(tkpt)
851
+ end
852
+ end
853
+ }
854
+ end
855
+
856
+ def parse_tracks
857
+ in_track, trk_number, tkpt_number = false, 0, 0
858
+ curr_trk, curr_run = nil, nil
859
+ @lines.each { |line|
860
+ line_obj = Line.new(line, nil, true)
861
+ tok_count = line_obj.token_count
862
+ is_point = line_obj.token_idx_equals(0, '.')
863
+
864
+ if line_obj.is_populated_non_comment
865
+ if line_obj.match('track_start')
866
+ in_track = true
867
+ trk_number = trk_number + 1
868
+ tkpt_number = 0
869
+ curr_trk = Track.new(0, line_obj.concatinate_tokens(1))
870
+ curr_run = Run.new(trk_number, line_obj.concatinate_tokens(1))
871
+ curr_run.add_track(curr_trk)
872
+ elsif line_obj.match('track_end')
873
+ in_track = false
874
+ curr_run.finish
875
+ add_track(curr_trk)
876
+ add_route(curr_run)
877
+ elsif in_track && tok_count > 2 && is_point
878
+ tkpt_number = tkpt_number + 1
879
+ tkpt = Trackpoint.new(
880
+ tkpt_number, line_obj.tokens[1], line_obj.tokens[2],
881
+ '0', '', line_obj.concatinate_tokens(3))
882
+ curr_trk.add_trackpoint(tkpt)
883
+ end
884
+ end
885
+ }
886
+ end
887
+
888
+ def parse_routes
889
+ in_route, route_number, trk_number, tkpt_number = false, 0, 0, 0
890
+ curr_trk, curr_run = nil, nil
891
+ @lines.each { |line|
892
+ line_obj = Line.new(line, nil, true)
893
+ tok_count = line_obj.token_count
894
+ is_point = line_obj.token_idx_equals(0, '.')
895
+
896
+ if line_obj.is_populated_non_comment
897
+ if line_obj.match('route_start')
898
+ in_route = true
899
+ trk_number = trk_number + 1
900
+ tkpt_number = 0
901
+ curr_trk = Track.new(0, line_obj.concatinate_tokens(1))
902
+ curr_run = Run.new(trk_number, line_obj.concatinate_tokens(1))
903
+ curr_run.add_track(curr_trk)
904
+ elsif line_obj.match('route_end')
905
+ in_route = false
906
+ curr_run.finish
907
+ add_route(curr_run)
908
+ elsif in_route && tok_count > 2 && is_point
909
+ tkpt_number = tkpt_number + 1
910
+ tkpt = Trackpoint.new(
911
+ tkpt_number, line_obj.tokens[1], line_obj.tokens[2],
912
+ '0', '', line_obj.concatinate_tokens(3))
913
+ curr_trk.add_trackpoint(tkpt)
914
+ elsif in_route && line_obj.token_idx_equals(0, 'track') && tok_count > 1
915
+ trk_desc = line_obj.concatinate_tokens(1)
916
+ trk = @track_hash[trk_desc]
917
+ if trk
918
+ trk.trackpoints.each { |tkpt| curr_trk.add_trackpoint(tkpt) }
919
+ end
920
+ elsif in_route && line_obj.token_idx_equals(0, 'track_rev') && tok_count > 1
921
+ trk_desc = line_obj.concatinate_tokens(1)
922
+ trk = @track_hash[trk_desc]
923
+ if trk
924
+ array = trk.trackpoints
925
+ trk.trackpoints.each { |tkpt| curr_trk.add_trackpoint(tkpt) }
926
+ end
927
+ end
928
+ end
929
+ }
930
+ end
931
+
932
+ def add_poi(tkpt)
933
+ if tkpt
934
+ descr = tkpt.descr
935
+ if @poi_hash.has_key? descr
936
+ puts "Duplicate POI key ignored - '#{descr}'"
937
+ else
938
+ #puts "Adding POI: #{tkpt.to_poi_csv}"
939
+ @poi_hash[descr] = tkpt
940
+ @poi_array << tkpt
941
+ end
942
+ end
943
+ end
944
+
945
+ def add_track(trk)
946
+ if trk
947
+ descr = trk.descr
948
+ if @track_hash.has_key? descr
949
+ puts "Duplicate Track key ignored - '#{descr}'"
950
+ else
951
+ @track_hash[descr] = trk
952
+ @track_array << trk
953
+ end
954
+ end
955
+ end
956
+
957
+ def add_route(run)
958
+ if run
959
+ descr = run.descr
960
+ if @route_hash.has_key? descr
961
+ puts "Duplicate Route key ignored - '#{descr}'"
962
+ else
963
+ @route_hash[descr] = run
964
+ @route_array << run
965
+ end
966
+ end
967
+ end
968
+
969
+ public
970
+
971
+ def to_s
972
+ return "GeoData lines: #{lines.size} poi: #{@poi_hash.size} tracks: #{@track_hash.size} routes: #{@route_hash.size} "
973
+ end
974
+
975
+ def dump
976
+ puts "#{self.class} dump:"
977
+ @poi_array.each { |tkpt| puts "POI: #{tkpt.to_geo_s}" }
978
+ @track_array.each { |trk| trk.dump }
979
+ @route_hash.keys.sort.each { |key|
980
+ puts "Route: '#{key}'"
981
+ }
982
+ end
983
+
984
+ end
985
+
986
+
987
+ =begin rdoc
988
+ Instances of this class represent a <Run> aggregate object from a
989
+ Forerunner XML file.
990
+
991
+ Additionally, there is distance, pace, and Google Map generation logic
992
+ in this class.
993
+ =end
994
+
995
+ class GoogleMapGenerator < GoobyObject
996
+
997
+ attr_reader :csv_file, :csv_lines, :dttm_idx, :num_idx, :lat_idx, :lng_idx, :alt_idx, :cdist_idx
998
+ attr_reader :run, :tkpts, :content_hash, :center_longitude, :center_latitude, :gpoint_array, :overlay_points, :notes
999
+ attr_reader :center_longitude, :center_latitude
1000
+
1001
+ # The default csv input file format is as follows:
1002
+ # 1 | 2006-01-15T18:31:10Z | 1279 | 33.42601 | -111.92927 | 347.654 | 26.3514930151813
1003
+ # 1 | 2004-11-13T13:05:20Z | 2 | 37.54318 | -77.43636 | -58.022 | 0.00297286231747969
1004
+
1005
+ def initialize(csv_file, dttm_idx=1, num_idx=2, lat_idx=3, lng_idx=4, alt_idx=5, cdist_idx=6)
1006
+ @csv_file = csv_file
1007
+ @dttm_idx = dttm_idx
1008
+ @num_idx = num_idx
1009
+ @lat_idx = lat_idx
1010
+ @lng_idx = lng_idx
1011
+ @alt_idx = alt_idx
1012
+ @cdist_idx = cdist_idx
1013
+ @options = Gooby::Options.new(nil)
1014
+ @content_hash = Hash.new('')
1015
+ @run = Gooby::Run.new(1)
1016
+ @track = Gooby::Track.new(1)
1017
+ @run.add_track(@track)
1018
+ @tkpts = Array.new
1019
+
1020
+ @csv_lines = read_as_ascii_lines(@csv_file, 10, true)
1021
+ @csv_lines.each { | line |
1022
+ dline = Gooby::DelimLine.new(line)
1023
+ if (!dline.is_comment?)
1024
+ tkpt = dline.as_trackpoint(@num_idx, @lat_idx, @lng_idx, @alt_idx, @dttm_idx)
1025
+ if tkpt
1026
+ @track.add_trackpoint(tkpt)
1027
+ end
1028
+ end
1029
+ }
1030
+ @run.finish
1031
+ end
1032
+
1033
+ =begin
1034
+ Returns a Hash with specific generated content at the following keys:
1035
+ =end
1036
+ def generate(options)
1037
+ if (options == nil)
1038
+ @options = Gooby::Options.new(nil)
1039
+ else
1040
+ @options = options
1041
+ end
1042
+ @content_hash['when_generated'] = Time.now
1043
+ @content_hash['title'] = @options.get('gmap_title')
1044
+ filter_trackpoints
1045
+ compute_center_point
1046
+ generate_key_js
1047
+ generate_map_div
1048
+ generate_messages_div
1049
+ generate_main_js_start
1050
+ generate_main_js_route_overlay
1051
+ generate_main_js_checkpoint_overlays
1052
+ generate_main_js_map_clicked_listeners
1053
+ generate_main_js_end
1054
+ @content_hash
1055
+ end
1056
+
1057
+ def filter_trackpoints
1058
+ count, @tkpts = 0, Array.new
1059
+ firstTkpt = @options.get('gmap_first_tkpt_number')
1060
+ lastTkpt = @options.get('gmap_last_tkpt_number')
1061
+ @run.tracks.each { |trk|
1062
+ trk.trackpoints.each { |tkpt|
1063
+ count = count + 1
1064
+ if ((count >= firstTkpt) && (count <= lastTkpt))
1065
+ @tkpts.push(tkpt)
1066
+ end
1067
+ }
1068
+ }
1069
+ end
1070
+
1071
+ =begin
1072
+ Returns a Hash with specific generated content at the following keys:
1073
+ =end
1074
+ def generate_page(options)
1075
+
1076
+ # puts "generate_page #{@csv_file} #{@csv_lines.size}"
1077
+ content_hash = generate(nil)
1078
+ s = String.new(@csv_file)
1079
+ s.gsub("/", " ")
1080
+ tokens = tokenize(s, nil)
1081
+ out_file = "#{tokens[-2]}.html"
1082
+ #content_hash.keys.sort.each { | key | puts key }
1083
+
1084
+ s = <<HERE
1085
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1086
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1087
+ <html xmlns="http://www.w3.org/1999/xhtml">
1088
+ <head>
1089
+ <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
1090
+ <title> Google Map by Gooby </title>
1091
+ #{content_hash['key_js']}
1092
+ #{content_hash['main_js_start']}
1093
+ #{content_hash['main_js_route_overlay']}
1094
+ #{content_hash['main_js_checkpoint_overlays']}
1095
+ #{content_hash['main_js_map_clicked_listeners']}
1096
+ #{content_hash['main_js_end']}
1097
+ </head>
1098
+ <body onload="load()" onunload="GUnload()">
1099
+ <center>
1100
+ <h3> #{content_hash['title']} </h3>
1101
+ <h5> Generated by Gooby #{content_hash['when_generated']} <br> Gooby = Google APIs + Ruby </h5>
1102
+ #{content_hash['map_div']}
1103
+ #{content_hash['messages_div']}
1104
+ </center>
1105
+ </body>
1106
+ </html>
1107
+ HERE
1108
+
1109
+ # html_file = File.new(out_file, "w+")
1110
+ # html_file.write(s)
1111
+ # html_file.flush
1112
+ # html_file.close
1113
+ puts s # Output is redirected by shell.
1114
+ end
1115
+
1116
+ private
1117
+
1118
+ def compute_center_point
1119
+ highLat = -999.0
1120
+ highLong = -999.0
1121
+ lowLat = 999.0
1122
+ lowLong = 999.0
1123
+ @tkpts.each { |tkpt|
1124
+ highLat = tkpt.latitude_as_float if tkpt.latitude_as_float > highLat
1125
+ lowLat = tkpt.latitude_as_float if tkpt.latitude_as_float < lowLat
1126
+ highLong = tkpt.longitude_as_float if tkpt.longitude_as_float > highLong
1127
+ lowLong = tkpt.longitude_as_float if tkpt.longitude_as_float < lowLong
1128
+ }
1129
+ @center_longitude = (highLong + lowLong) / 2
1130
+ @center_latitude = (highLat + lowLat) / 2
1131
+ @content_hash['center_longitude'] = @center_longitude
1132
+ @content_hash['center_latitude'] = @center_latitude
1133
+ end
1134
+
1135
+ def generate_key_js
1136
+ key = @options.get('gmap_key')
1137
+ key.strip!
1138
+ s = '<script src="http://maps.google.com/maps?file=api&v=2&key='
1139
+ s << key
1140
+ s << '" type="text/javascript"></script>'
1141
+ @content_hash['key_js'] = s
1142
+ end
1143
+
1144
+ def generate_map_div
1145
+ width = @options.get('gmap_width')
1146
+ height = @options.get('gmap_height')
1147
+ id = @options.get('gmap_map_element_id')
1148
+ s = '<div id="'
1149
+ s << id
1150
+ s << '" style="width: '
1151
+ s << width
1152
+ s << '; height: '
1153
+ s << height
1154
+ s << '"></div>'
1155
+ @content_hash['map_width'] = width
1156
+ @content_hash['map_height'] = height
1157
+ @content_hash['map_div'] = s
1158
+ end
1159
+
1160
+ def generate_messages_div
1161
+ s = "<div id=\"messages\"></div>"
1162
+ @content_hash['messages_div'] = s
1163
+ end
1164
+
1165
+ def generate_main_js_start
1166
+ id = @options.get('gmap_map_element_id')
1167
+ size = @options.get('gmap_size_control')
1168
+ type = @options.get('gmap_type')
1169
+ zoom = @options.get('gmap_zoom_level')
1170
+ title = @options.get('gmap_title')
1171
+ if size
1172
+ if size == 'smallmap'
1173
+ size = 'GSmallMapControl'
1174
+ elsif size == 'smallzoom'
1175
+ size = 'GSmallMapControl'
1176
+ else
1177
+ size = 'GLargeMapControl'
1178
+ end
1179
+ end
1180
+
1181
+ if type
1182
+ if type == 'satellite'
1183
+ type = 'G_SATELLITE_MAP'
1184
+ elsif type == 'hybrid'
1185
+ type = 'G_HYBRID_MAP'
1186
+ else
1187
+ type = 'G_NORMAL_MAP'
1188
+ end
1189
+ else
1190
+ type = 'G_NORMAL_MAP'
1191
+ end
1192
+
1193
+ s = '<script type="text/javascript">'
1194
+ s << "\n"
1195
+ s << "//<![CDATA[ \n"
1196
+ s << " function load() { \n"
1197
+ s << " if (GBrowserIsCompatible()) { \n"
1198
+ s << ' var map = new GMap2(document.getElementById("'
1199
+ s << id
1200
+ s << '")); '
1201
+ s << "\n"
1202
+
1203
+ if size
1204
+ s << ' map.addControl(new '
1205
+ s << size
1206
+ s << '());'
1207
+ s << "\n"
1208
+ end
1209
+
1210
+ if type
1211
+ s << ' map.addControl(new GMapTypeControl());'
1212
+ s << "\n"
1213
+ # s << ' map.setMapType('
1214
+ # s << type
1215
+ # s << ');'
1216
+ s << "\n"
1217
+ end
1218
+ s << " var centerPoint = new GLatLng(#{@center_latitude}, #{@center_longitude}); \n"
1219
+ s << " map.setCenter(centerPoint, #{zoom}); \n"
1220
+ s << "\n"
1221
+ @content_hash['main_js_start'] = s
1222
+ @content_hash['title'] = title
1223
+ end
1224
+
1225
+ def generate_main_js_route_overlay
1226
+ tkpt_count = @tkpts.size.to_f
1227
+ app_max = @options.get('gmap_approx_max_points').to_f
1228
+ comments = @options.get('gmap_gen_comments')
1229
+ ratio = tkpt_count / app_max
1230
+ @start_dttm = nil
1231
+ if ratio > 1.0
1232
+ increment = (tkpt_count / app_max).to_i
1233
+ else
1234
+ increment = 1
1235
+ end
1236
+ curr_idx, next_idx, gpoint_count, last_idx = -1, 0, 0, @tkpts.size - 1
1237
+ s = " var points = new Array(); "
1238
+ @tkpts.each { |tkpt|
1239
+ curr_idx = curr_idx + 1
1240
+ if curr_idx == 0
1241
+ @start_dttm = tkpt.dttm
1242
+ @start_pos = tkpt.position
1243
+ time = Time.parse(@start_dttm.dateTime().to_s)
1244
+ end
1245
+ if ((curr_idx == next_idx) || (curr_idx == last_idx) || (tkpt.is_split()))
1246
+ gpoint_count = gpoint_count + 1
1247
+ s << tkpt.as_glatlng(comments, tkpt_count, curr_idx, gpoint_count, @start_dttm)
1248
+ next_idx = curr_idx + increment
1249
+ end
1250
+ }
1251
+ s << "\n"
1252
+ s << "\n var routePolyline = new GPolyline(points); "
1253
+ s << "\n map.addOverlay(routePolyline); "
1254
+ @content_hash['main_js_route_overlay'] = s
1255
+ @content_hash['main_js_route_overlay_increment'] = increment
1256
+ end
1257
+
1258
+ def generate_main_js_checkpoint_overlays
1259
+ s = "\n // Create a base icon for all of our markers that specifies the "
1260
+ s << "\n // shadow, icon dimensions, etc."
1261
+ s << "\n var baseIcon = new GIcon();"
1262
+ s << "\n baseIcon.shadow = \"http://www.joakim-systems.com/gicons/shadow50.png\";"
1263
+ s << "\n baseIcon.iconSize = new GSize(20, 34);"
1264
+ s << "\n baseIcon.shadowSize = new GSize(37, 34);"
1265
+ s << "\n baseIcon.iconAnchor = new GPoint(9, 34);"
1266
+ s << "\n baseIcon.infoWindowAnchor = new GPoint(9, 2);"
1267
+ s << "\n baseIcon.infoShadowAnchor = new GPoint(18, 25);"
1268
+ s << "\n"
1269
+
1270
+ curr_idx = -1
1271
+ last_idx = @tkpts.size - 1
1272
+ next_checkpoint = 0.0
1273
+ @start_dttm = nil
1274
+ @tkpts.each { | tkpt |
1275
+ curr_idx = curr_idx + 1
1276
+ if curr_idx == 0
1277
+ @start_dttm = tkpt.dttm
1278
+ info_window_html = tkpt.as_info_window_html('Start', @start_dttm)
1279
+ s << "\n var iconStart = new GIcon(baseIcon); "
1280
+ s << "\n iconStart.image = \"http://www.joakim-systems.com/gicons/dd-start.png\";"
1281
+ s << "\n var pStart = new GPoint(#{tkpt.longitude_as_float}, #{tkpt.latitude_as_float});"
1282
+ s << "\n var mStart = new GMarker(pStart, iconStart);"
1283
+ s << "\n GEvent.addListener(mStart, \"click\", function() { "
1284
+ s << "\n mStart.openInfoWindowHtml(#{info_window_html});"
1285
+ s << "\n }); "
1286
+ s << "\n map.addOverlay(mStart);"
1287
+ s << "\n "
1288
+ next_checkpoint = 1.0
1289
+ elsif curr_idx == last_idx
1290
+ info_window_html = tkpt.as_info_window_html('Finish', @start_dttm)
1291
+ s << "\n var iconFinish = new GIcon(baseIcon); "
1292
+ s << "\n iconFinish.image = \"http://www.joakim-systems.com/gicons/dd-end.png\";"
1293
+ s << "\n var pFinish = new GPoint(#{tkpt.longitude_as_float}, #{tkpt.latitude_as_float});"
1294
+ s << "\n var mFinish = new GMarker(pFinish, iconFinish);"
1295
+ s << "\n GEvent.addListener(mFinish, \"click\", function() { "
1296
+ s << "\n mFinish.openInfoWindowHtml(#{info_window_html});"
1297
+ s << "\n }); "
1298
+ s << "\n map.addOverlay(mFinish);"
1299
+ s << "\n "
1300
+ next_checkpoint = 999999
1301
+ else
1302
+ if (tkpt.cumulativeDistance >= next_checkpoint)
1303
+ integer = next_checkpoint.to_i
1304
+ info_window_html = tkpt.as_info_window_html("#{integer}", @start_dttm)
1305
+ s << "\n var icon#{integer} = new GIcon(baseIcon); "
1306
+ s << "\n icon#{integer}.image = \"http://www.joakim-systems.com/gicons/marker#{integer}.png\";"
1307
+ s << "\n var p#{integer} = new GPoint(#{tkpt.longitude_as_float}, #{tkpt.latitude_as_float});"
1308
+ s << "\n var m#{integer} = new GMarker(p#{integer}, icon#{integer});"
1309
+ s << "\n GEvent.addListener(m#{integer}, \"click\", function() { "
1310
+ s << "\n m#{integer}.openInfoWindowHtml(#{info_window_html});"
1311
+ s << "\n }); "
1312
+ s << "\n map.addOverlay(m#{integer});"
1313
+ s << "\n "
1314
+ next_checkpoint = next_checkpoint + 1.0
1315
+ end
1316
+ end
1317
+ }
1318
+ s << "\n"
1319
+ @content_hash['main_js_checkpoint_overlays'] = s
1320
+
1321
+ end
1322
+
1323
+ def generate_main_js_map_clicked_listeners
1324
+ s = "\n"
1325
+ s << "\n GEvent.addListener(map, \"click\", function() { "
1326
+ s << "\n var center = map.getCenter(); \n"
1327
+ s << "\n document.getElementById(\"messages\").innerHTML = 'click: ' + center.toString(); "
1328
+ s << "\n });"
1329
+ s << "\n GEvent.addListener(map, \"moveend\", function() { "
1330
+ s << "\n var center = map.getCenter(); \n"
1331
+ s << "\n document.getElementById(\"messages\").innerHTML = 'moveend: ' + center.toString(); "
1332
+ s << "\n });"
1333
+ @content_hash['main_js_map_clicked_listeners'] = s
1334
+ end
1335
+
1336
+ def generate_main_js_end
1337
+ s = "\n } "
1338
+ s << "\n } "
1339
+ s << "\n//]]> \n"
1340
+ s << "\n</script>"
1341
+
1342
+ @content_hash['main_js_end'] = s
1343
+ end
1344
+
1345
+ end
1346
+
1347
+
1348
+ =begin rdoc
1349
+ Instances of this class represent a <History> aggregate object from a
1350
+ Forerunner XML file.
1351
+ =end
1352
+
1353
+ class History < GoobyObject
1354
+
1355
+ attr_reader :runs
1356
+
1357
+ def initialize
1358
+ @runs = Array.new
1359
+ end
1360
+
1361
+ # Adds a Run during XML parsing.
1362
+ def add_run(run)
1363
+ @runs.push(run)
1364
+ end
1365
+
1366
+ def to_s
1367
+ return "Hist: runs: #{@runs.size}"
1368
+ end
1369
+
1370
+ def print_string
1371
+ s = "History: run count=#{@runs.size} \n"
1372
+ runs.each { | run | s << run.print_string }
1373
+ s
1374
+ end
1375
+
1376
+ end
1377
+
1378
+
1379
+ =begin rdoc
1380
+ Instances of this class represent a <Lap> aggregate object from a
1381
+ Forerunner XML file.
1382
+ =end
1383
+
1384
+ class Lap < GoobyObject
1385
+
1386
+ attr_accessor :number, :startTime, :duration, :length, :beginPosition, :endPosition
1387
+
1388
+ def initialize(num)
1389
+ @number = num
1390
+ end
1391
+
1392
+ def to_s
1393
+ return "Lap: num: #{@number} start: #{@startTime} dur: #{@duration} len: #{@length} begin: #{@beginPosition.to_s} end: #{@endPosition.to_s}"
1394
+ end
1395
+
1396
+ end
1397
+
1398
+
1399
+ =begin rdoc
1400
+
1401
+ =end
1402
+
1403
+ class Line < GoobyObject
1404
+
1405
+ attr_accessor :raw_data, :tokens
1406
+
1407
+ def initialize(raw='', delim=nil, strip=false)
1408
+ if strip
1409
+ @raw_data = raw.strip
1410
+ else
1411
+ @raw_data = raw
1412
+ end
1413
+
1414
+ @tokens = tokenize(@raw_data, delim, strip=false)
1415
+ end
1416
+
1417
+ public
1418
+
1419
+ def token(idx)
1420
+ @tokens[idx]
1421
+ end
1422
+
1423
+ def token_count
1424
+ @tokens.size
1425
+ end
1426
+
1427
+ def token_idx_equals(idx, value)
1428
+ if idx < token_count
1429
+ if @tokens[idx] == value
1430
+ return true
1431
+ end
1432
+ end
1433
+ false
1434
+ end
1435
+
1436
+ def match(pattern)
1437
+ @raw_data.match(pattern)
1438
+ end
1439
+
1440
+ def is_comment
1441
+ s = @raw_data.strip
1442
+ (s.match('^#')) ? true : false
1443
+ end
1444
+
1445
+ def is_populated_non_comment
1446
+ s = @raw_data.strip
1447
+ if s.size == 0
1448
+ return false
1449
+ end
1450
+ if is_comment
1451
+ return false
1452
+ end
1453
+ return true
1454
+ end
1455
+
1456
+ def concatinate_tokens(start_idx = 0)
1457
+ s = ''
1458
+ idx = -1
1459
+ @tokens.each { |tok|
1460
+ idx = idx + 1
1461
+ if idx >= start_idx
1462
+ s << tok
1463
+ s << ' '
1464
+ end
1465
+ }
1466
+ s.strip!
1467
+ s
1468
+ end
1469
+ end
1470
+
1471
+
1472
+ class Options < GoobyObject
1473
+
1474
+ attr_reader :yamlFilename, :options
1475
+
1476
+ def initialize(filename) # Constructor.
1477
+ filename ? @yamlFilename = filename : @yamlFilename = 'gooby_options.yaml'
1478
+ loadFile()
1479
+ end
1480
+
1481
+ public
1482
+
1483
+ # Load the @yamlFilename
1484
+ def loadFile
1485
+ File.open("#{@yamlFilename}") { |fn|
1486
+ @options = YAML::load(fn)
1487
+ }
1488
+ end
1489
+
1490
+ def get(name)
1491
+ if name == nil
1492
+ return ''
1493
+ end
1494
+ s = @options["#{name}"]
1495
+
1496
+ # Provide "sensible defaults".
1497
+ if s == nil
1498
+ if (name == '')
1499
+ return ''
1500
+ elsif (name == 'gmap_first_tkpt_number')
1501
+ return 1
1502
+ elsif (name == 'gmap_last_tkpt_number')
1503
+ return 5000
1504
+ elsif (name == 'gmap_map_element_id')
1505
+ return 'map'
1506
+ elsif (name == 'gmap_height')
1507
+ return '600'
1508
+ elsif (name == 'gmap_key')
1509
+ return 'enter your Google Map Key here'
1510
+ elsif (name == 'gmap_type_control')
1511
+ return true
1512
+ elsif (name == 'gmap_approx_max_points')
1513
+ return '200'
1514
+ elsif (name == 'gmap_gen_comments')
1515
+ return true
1516
+ elsif (name == 'gmap_size_control')
1517
+ return nil
1518
+ elsif (name == 'gmap_type')
1519
+ return 'G_NORMAL_MAP'
1520
+ elsif (name == 'gmap_zoom_level')
1521
+ return 5
1522
+ else
1523
+ return ''
1524
+ end
1525
+ end
1526
+ s
1527
+ end
1528
+
1529
+ # Return a String containing yaml filename and entry count.
1530
+ def to_s
1531
+ return "Options: filename: #{@yamlFilename} entries: #{@options.size}"
1532
+ end
1533
+
1534
+ end
1535
+
1536
+
1537
+ =begin rdoc
1538
+ Instances of this class represent a <Position> aggregate object from a
1539
+ Forerunner XML file. Each contains a latitude and longitude.
1540
+ Instances within a Trackpoint will also contain an altitude.
1541
+ =end
1542
+
1543
+ class Position < GoobyObject
1544
+
1545
+ attr_accessor :latitude, :longitude, :altitude, :note
1546
+
1547
+ def initialize(lat, lng, alt='0', note='')
1548
+ @latitude = lat.to_s
1549
+ @longitude = lng.to_s
1550
+ @altitude = alt.to_s
1551
+ @note = note.to_s
1552
+ end
1553
+
1554
+ public
1555
+
1556
+ def to_s
1557
+ return "lat: #{@latitude} lng: #{@longitude} alt: #{@altitude} note: #{@note}"
1558
+ end
1559
+
1560
+ def to_csv
1561
+ return "#{@latitude} | #{@longitude} | #{@altitude}"
1562
+ end
1563
+
1564
+ def latitude_as_float
1565
+ @latitude ? @latitude.to_f : invalid_latitude
1566
+ end
1567
+
1568
+ def longitude_as_float
1569
+ @longitude ? @longitude.to_f : invalid_longitude
1570
+ end
1571
+
1572
+ def altitude_as_float
1573
+ @altitude ? @altitude.to_f : invalid_altitude
1574
+ end
1575
+
1576
+ end
1577
+
1578
+
1579
+ =begin rdoc
1580
+ Instances of this class represent a <Run> aggregate object from a
1581
+ Forerunner XML file.
1582
+
1583
+ Additionally, there is distance, pace, and Google Map generation logic
1584
+ in this class.
1585
+ =end
1586
+
1587
+ class Run < GoobyObject
1588
+
1589
+ attr_accessor :number, :descr, :notes, :tracks, :tkpts, :laps, :distance
1590
+
1591
+ def initialize(number=0, descr='')
1592
+ @number = number
1593
+ @descr = descr
1594
+ @notes = ''
1595
+ @tracks = Array.new
1596
+ @tkpts = Array.new
1597
+ @laps = Array.new
1598
+ @distance = 0
1599
+ @options = Hash.new
1600
+ @logProgress = true
1601
+ @finished = false
1602
+ end
1603
+
1604
+ public
1605
+
1606
+ # This method is invoked at end-of-parsing.
1607
+ def finish()
1608
+ @logProgress = false
1609
+ unless @finished
1610
+ @tracks.each { |trk|
1611
+ trk.trackpoints().each { |tkpt|
1612
+ tkpt.runNumber = @number
1613
+ @tkpts.push(tkpt)
1614
+ }
1615
+ }
1616
+ compute_distance_and_pace
1617
+ compute_splits
1618
+ @finished = true
1619
+ end
1620
+ end
1621
+
1622
+ public
1623
+
1624
+ def add_track(trk)
1625
+ if trk != nil
1626
+ @tracks.push(trk)
1627
+ end
1628
+ end
1629
+
1630
+ def trackpoint_count()
1631
+ @tkpts.size()
1632
+ end
1633
+
1634
+ def add_lap(lap)
1635
+ @laps.push(lap)
1636
+ end
1637
+
1638
+ def lapCount()
1639
+ @laps.size
1640
+ end
1641
+
1642
+ def start_dttm()
1643
+ count = 0
1644
+ @tracks.each { |trk|
1645
+ trk.trackpoints().each { |tkpt|
1646
+ return tkpt.dttm()
1647
+ }
1648
+ }
1649
+ return nil
1650
+ end
1651
+
1652
+ def end_dttm()
1653
+ lastOne = nil
1654
+ @tracks.each { |trk|
1655
+ trk.trackpoints().each { |tkpt|
1656
+ lastOne = tkpt.dttm()
1657
+ }
1658
+ }
1659
+ lastOne
1660
+ end
1661
+
1662
+ def duration()
1663
+ first = start_dttm()
1664
+ last = end_dttm()
1665
+ if first
1666
+ if last
1667
+ return last.hhmmss_diff(first)
1668
+ end
1669
+ end
1670
+ return "??:??:??"
1671
+ end
1672
+
1673
+ def start_yyyy_mm_dd
1674
+ if start_dttm()
1675
+ start_dttm().yyyy_mm_dd()
1676
+ else
1677
+ ""
1678
+ end
1679
+ end
1680
+
1681
+ def start_hh_mm_ss
1682
+ if start_dttm()
1683
+ start_dttm().hh_mm_ss()
1684
+ else
1685
+ ""
1686
+ end
1687
+ end
1688
+
1689
+ def end_hh_mm_ss
1690
+ if end_dttm()
1691
+ end_dttm().hh_mm_ss()
1692
+ else
1693
+ ""
1694
+ end
1695
+ end
1696
+
1697
+ def to_s
1698
+ finish() unless @finished
1699
+ s = "Run: #{@number} date: #{start_yyyy_mm_dd} distance: #{distance} duration: #{duration} "
1700
+ s << " tracks: #{@tracks.size} tkpts: #{trackpoint_count} laps: #{lapCount} "
1701
+ s << " notes: #{@notes} "
1702
+ s
1703
+ end
1704
+
1705
+ def print_string
1706
+ finish() unless @finished
1707
+ "Run number=#{@number} tracks=#{@tracks.size} tkpts=#{@tkpts.size} laps=#{@laps.size} distance=#{@distance} "
1708
+ end
1709
+
1710
+ def put_csv()
1711
+ finish() unless @finished
1712
+ puts "#{@number}|#{}|#{start_yyyy_mm_dd()}|#{start_hh_mm_ss()}|#{end_hh_mm_ss}|#{duration()}|#{@distance}|#{@tracks.size}|#{trackpoint_count()}|#{lapCount}|#{@notes.strip}"
1713
+ end
1714
+
1715
+ def put_tkpt_csv(with_header_comment=false)
1716
+ finish() unless @finished
1717
+ if with_header_comment
1718
+ puts "# Run: #{@number} date: #{start_yyyy_mm_dd} dist: #{distance} dur: #{duration} trks: #{@tracks.size} tkpts: #{trackpoint_count} laps: #{lapCount} "
1719
+ end
1720
+ @tkpts.each { | tkpt | puts tkpt.to_csv }
1721
+ end
1722
+
1723
+ def put_laps
1724
+ @laps.each { | lap | puts lap.to_s }
1725
+ end
1726
+
1727
+ private
1728
+
1729
+ def compute_distance_and_pace
1730
+ cumulative_dist = 0.to_f;
1731
+ curr_index = -1
1732
+ prev_tkpt = nil
1733
+ start_dttm = nil
1734
+ @tkpts.each { | tkpt |
1735
+ curr_index = curr_index + 1
1736
+ if curr_index == 0
1737
+ start_dttm = tkpt.dttm()
1738
+ prev_tkpt = tkpt
1739
+ else
1740
+ cumulative_dist = tkpt.compute_distance_and_pace(curr_index, start_dttm, cumulative_dist, prev_tkpt, 'M')
1741
+ prev_tkpt = tkpt
1742
+ end
1743
+ }
1744
+ @distance = cumulative_dist
1745
+ end
1746
+
1747
+ def compute_splits
1748
+ nextSplitDist = 1.00
1749
+ prevSplitTkpt = nil
1750
+ loop1Count = 0;
1751
+ @tkpts.each { |tkpt|
1752
+ loop1Count = loop1Count + 1
1753
+ if tkpt.cumulativeDistance() >= nextSplitDist
1754
+ tkpt.set_split(0 + nextSplitDist, prevSplitTkpt)
1755
+ nextSplitDist = nextSplitDist + 1.00
1756
+ prevSplitTkpt = tkpt
1757
+ end
1758
+ }
1759
+ # set first and last booleans
1760
+ count = 0
1761
+ @tkpts.each { |tkpt|
1762
+ count = count + 1
1763
+ tkpt.first = true if count == 1
1764
+ tkpt.last = true if count == loop1Count
1765
+ }
1766
+ end
1767
+
1768
+ end
1769
+
1770
+
1771
+ =begin rdoc
1772
+ Sample implementation of a REXML::StreamListener SAX parser.
1773
+ =end
1774
+
1775
+ class SimpleXmlParser
1776
+
1777
+ include REXML::StreamListener
1778
+
1779
+ attr_accessor :tag_count, :watched_tags
1780
+
1781
+ def initialize
1782
+ @tag_count = 0
1783
+ @counter_hash = CounterHash.new
1784
+ end
1785
+
1786
+ public
1787
+
1788
+ # SAX API method. Increments the tagname in the counter hash.
1789
+ def tag_start(tag_name, attrs)
1790
+ @tag_count = @tag_count + 1
1791
+ @counter_hash.increment(tag_name)
1792
+ end
1793
+
1794
+ # SAX API method. No impl.
1795
+ def tag_end(tagname)
1796
+ end
1797
+
1798
+ # SAX API method. No impl.
1799
+ def text(txt)
1800
+ end
1801
+
1802
+ # Prints the state of this object (the counter hash).
1803
+ def dump
1804
+ puts @counter_hash.to_s
1805
+ end
1806
+
1807
+ end
1808
+
1809
+
1810
+ =begin rdoc
1811
+ This class is used to generate, on an ongoing basis, the various Gooby test
1812
+ classes. Regeneration retains the current test methods, and adds stubs for
1813
+ new test methods. All methods, old and new, appear in the merged output in
1814
+ propper sort sequence.
1815
+ =end
1816
+
1817
+ class TestGenerator < GoobyObject
1818
+
1819
+ def initialize
1820
+ tested_files.each { | base_rb_file |
1821
+ test_file = "tc_#{base_rb_file}"
1822
+ if true
1823
+ @codeLines = read_lines("lib/#{base_rb_file}")
1824
+ @testLines = read_lines("tests/#{test_file}")
1825
+ @codeHash = Hash.new
1826
+ @testHash = Hash.new
1827
+ @mergedHash = Hash.new
1828
+ @excludeClasses = %w( TestHelper TestGenerator )
1829
+ puts "current codeLines = #{@codeLines.size}"
1830
+ puts "current testLines = #{@testLines.size}"
1831
+ parse_code_lines
1832
+ parse_test_lines
1833
+ merge_keys
1834
+ regenerate(test_file)
1835
+ end
1836
+ }
1837
+ end
1838
+
1839
+ private
1840
+
1841
+ # Produce a Hash with keys and values like the following:
1842
+ # test|class|Trackpoint|deg2rad Trackpoint|deg2rad(degrees)
1843
+
1844
+ def parse_code_lines
1845
+ type = ''
1846
+ type_name = ''
1847
+ meth_name = ''
1848
+ @codeLines.each { | line |
1849
+ line.strip!
1850
+ if (line.match(/^module /))
1851
+ type = 'module'
1852
+ tokens = line.split
1853
+ type_name = tokens[1]
1854
+ elsif (line.match(/^class /))
1855
+ type = 'class'
1856
+ tokens = line.split
1857
+ type_name = tokens[1]
1858
+ elsif (line.match(/^def /))
1859
+ signature = line[4...999]
1860
+ short_method = parse_meth_name("#{signature}")
1861
+ @codeHash["test_#{type}_#{type_name}"] = "#{type_name}"
1862
+ @codeHash["test_#{type}_#{type_name}_#{short_method}"] = "#{type_name}|#{signature}"
1863
+ end
1864
+ }
1865
+ end
1866
+
1867
+ def parse_meth_name(string)
1868
+ string.gsub!('(', ' ')
1869
+ string.gsub!(')', ' ')
1870
+ tokens = string.split
1871
+ tokens[0]
1872
+ end
1873
+
1874
+ def parse_test_lines
1875
+ in_method = true
1876
+ method_name = 'a_start'
1877
+ method_lines = Array.new
1878
+ line_num = 0
1879
+
1880
+ @testLines.each { | line |
1881
+ line_num = line_num + 1
1882
+ line.chomp!
1883
+ prefix = line[0...5] # ' def'
1884
+ prefix21 = line[0...21] # ' ### Start of tests.'
1885
+
1886
+ if ((prefix == ' def') || (prefix == "\tdef"))
1887
+ in_method = true
1888
+ tokens = line.split
1889
+ method_name = tokens[1]
1890
+ method_lines = Array.new
1891
+ end
1892
+ if in_method
1893
+ method_lines << "#{line}"
1894
+ end
1895
+ if prefix21 == ' ### Start of tests.'
1896
+ in_method = false
1897
+ @testHash["#{method_name}"] = method_lines
1898
+ end
1899
+ if ((prefix == ' end') || (prefix == "\tend"))
1900
+ in_method = false
1901
+ @testHash["#{method_name}"] = method_lines
1902
+ end
1903
+ }
1904
+ end
1905
+
1906
+ def merge_keys
1907
+ @codeHash.keys.sort.each { |key| @mergedHash["#{key}"] = "code" }
1908
+ @testHash.keys.sort.each { |key| @mergedHash["#{key}"] = "test" }
1909
+ end
1910
+
1911
+ def regenerate(test_file)
1912
+ code = ''
1913
+ @mergedHash.keys.sort.each { |key|
1914
+
1915
+ tokens = key.split('_')
1916
+ type, name, meth = tokens[1], tokens[2], tokens[3]
1917
+
1918
+ processThisKey = true
1919
+ @excludeClasses.each { |xc|
1920
+ if xc == name
1921
+ processThisKey = false
1922
+ end
1923
+ }
1924
+
1925
+ next if !processThisKey
1926
+
1927
+ if @testHash.has_key?(key)
1928
+ # We already have a test method written in the test class,
1929
+ # so keep this currently existing test code!
1930
+
1931
+ if @codeHash.has_key?(key)
1932
+ comment = nil
1933
+ else
1934
+ if key != 'a_start'
1935
+ comment = "# Warning: possible obsolete test method - #{key}"
1936
+ end
1937
+ end
1938
+
1939
+ code << "\n"
1940
+ if comment != nil
1941
+ code << "\n#{comment}"
1942
+ code << "\n"
1943
+ end
1944
+ array = @testHash["#{key}"]
1945
+ array.each { |line| code << "\n#{line}" }
1946
+ else
1947
+ # We don't have this test method in the current test class,
1948
+ # so generate a test method stub.
1949
+
1950
+ code << "\n"
1951
+ code << "\n def #{key}"
1952
+ code << "\n"
1953
+
1954
+ if @gen_impl_stub
1955
+ if type = 'class'
1956
+ code << "\n #obj = #{type}.new"
1957
+ code << "\n #result = obj.#{meth}"
1958
+ code << "\n #expected = ''"
1959
+ s = "\n"
1960
+ s << ' #assert_equal(expected, actual, "'
1961
+ s << "#{type}.#{meth} "
1962
+ s << 'values are not as expected; #{result} vs #{expected}")'
1963
+ code << s
1964
+ else
1965
+ code << "\n #result = #{type}.#{meth}"
1966
+ code << "\n #expected = ''"
1967
+ s = "\n"
1968
+ s << ' #assert_equal(expected, actual, "'
1969
+ s << "#{type}.#{meth} "
1970
+ s << 'values are not as expected; #{result} vs #{expected}")'
1971
+ code << s
1972
+ end
1973
+ end
1974
+ code << "\n end"
1975
+ end
1976
+ }
1977
+ code << "\nend" # end of class
1978
+ code << "\n"
1979
+ fn = "tests/#{test_file}"
1980
+ out = File.new fn, "w+"
1981
+ out.write code
1982
+ out.flush
1983
+ out.close
1984
+ puts "file written: #{fn}"
1985
+ end
1986
+
1987
+ end
1988
+
1989
+
1990
+ =begin rdoc
1991
+ Instances of this class represent a <Track> aggregate object from a Forerunner
1992
+ XML file. Note that a <Run> may contain more than one <Track> aggregates.
1993
+ =end
1994
+
1995
+ class Track < GoobyObject
1996
+
1997
+ attr_reader :number, :descr, :trackpoints
1998
+
1999
+ def initialize(num=0, descr='')
2000
+ @number = num
2001
+ @descr = descr
2002
+ @trackpoints = Array.new
2003
+ end
2004
+
2005
+ public
2006
+
2007
+ def add_trackpoint(tkpt)
2008
+ @trackpoints.push(tkpt)
2009
+ end
2010
+
2011
+ def size
2012
+ @trackpoints.size
2013
+ end
2014
+
2015
+ def first_tkpt
2016
+ @trackpoints.size > 0 ? @trackpoints[0] : nil
2017
+ end
2018
+
2019
+ def last_tkpt
2020
+ @trackpoints.size > 0 ? @trackpoints[-1] : nil
2021
+ end
2022
+
2023
+ def to_s
2024
+ return "Trk: #{@descr} tkpts: #{size}"
2025
+ end
2026
+
2027
+ def dump
2028
+ puts "Track: '#{@descr}' tkpts: #{size}"
2029
+ @trackpoints.each { |tkpt| puts tkpt.to_csv } # to_geo_s
2030
+ end
2031
+
2032
+ end
2033
+
2034
+
2035
+ =begin rdoc
2036
+ Instances of this class represent a <Trackpoint> aggregate from a Forerunner
2037
+ XML file. Additionally, there is distance, pace, and Google Map generation
2038
+ logic in this class.
2039
+
2040
+ <Trackpoint>
2041
+ <Position>
2042
+ <Latitude>35.49577</Latitude>
2043
+ <Longitude>-80.83281</Longitude>
2044
+ <Altitude>232.296</Altitude>
2045
+ </Position>
2046
+ <Time>2007-01-06T15:27:51Z</Time>
2047
+ </Trackpoint>
2048
+ =end
2049
+
2050
+ class Trackpoint < Position
2051
+
2052
+ attr_reader :first, :last, :number, :runNumber, :dttm, :prevTkpt, :descr
2053
+ attr_reader :cumulativeDistance, :cumulativePace, :incrementalDistance, :split, :prevSplit
2054
+ attr_writer :first, :last, :runNumber
2055
+
2056
+ def initialize(num, lat, lng, alt, time_string, descr='')
2057
+ @number = num
2058
+ @runNumber = 0
2059
+
2060
+ # initialize superclass variables:
2061
+ @latitude = lat.to_s
2062
+ @longitude = lng.to_s
2063
+ @altitude = alt.to_s
2064
+ @note = note.to_s
2065
+ @dttm = DtTm.new(time_string)
2066
+ @first = false
2067
+ @last = false
2068
+ @prevTkpt = nil
2069
+ @cumulativeDistance, @split = 0.0, 0.0
2070
+ @cumulativePace = ""
2071
+ @descr = descr
2072
+ end
2073
+
2074
+ public
2075
+
2076
+ def position
2077
+ Position.new(@latitude, @longitude, @altitude, @note)
2078
+ end
2079
+
2080
+ def to_s
2081
+ "Tkpt: #{@number} #{super.to_s} date: #{@dttm.to_s} cdist: #{@cumulativeDistance}"
2082
+ end
2083
+
2084
+ def to_csv
2085
+ ss = position.to_csv
2086
+ "#{@runNumber} | #{@dttm.to_s} | #{@number} | #{ss} | #{@cumulativeDistance} "
2087
+ end
2088
+
2089
+ def to_geo_s
2090
+ ss = position.to_csv
2091
+ "Tkpt: #{@number} | #{ss} | #{@descr}"
2092
+ end
2093
+
2094
+ def compute_distance_and_pace(curr_index, start_dttm, prev_cumulative_dist, prev_trackpoint, units)
2095
+ @prev_tkpt = prev_trackpoint
2096
+ @cumulativeDistance = prev_cumulative_dist.to_f
2097
+
2098
+ if @prev_tkpt
2099
+ arg1 = latitude().to_f
2100
+ arg2 = @prev_tkpt.latitude().to_f
2101
+ arg3 = latitude().to_f
2102
+ arg4 = @prev_tkpt.latitude().to_f
2103
+ theta = longitude().to_f - @prev_tkpt.longitude().to_f
2104
+
2105
+ res1 = Math.sin(deg2rad(arg1))
2106
+ res2 = Math.sin(deg2rad(arg2))
2107
+ res3 = Math.cos(deg2rad(arg3))
2108
+ res4 = Math.cos(deg2rad(arg4))
2109
+ res5 = Math.cos(deg2rad(theta.to_f))
2110
+
2111
+ incremental_distance = ((res1 * res2) + (res3 * res4 * res5)).to_f
2112
+
2113
+ if (!incremental_distance.nan?)
2114
+ incremental_distance = Math.acos(incremental_distance.to_f)
2115
+ if (!incremental_distance.nan?)
2116
+ incremental_distance = rad2deg(incremental_distance)
2117
+ if (!incremental_distance.nan?)
2118
+ incremental_distance = incremental_distance * 60 * 1.1515;
2119
+ if (!incremental_distance.nan?)
2120
+ if units == "K"
2121
+ incremental_distance = incremental_distance * 1.609344;
2122
+ end
2123
+ if units == "N"
2124
+ incremental_distance = incremental_distance * 0.8684;
2125
+ end
2126
+ @cumulativeDistance = @cumulativeDistance + incremental_distance.to_f
2127
+ end
2128
+ end
2129
+ end
2130
+ end
2131
+ compute_cumulative_pace(start_dttm)
2132
+ @cumulativeDistance
2133
+ else
2134
+ 0
2135
+ end
2136
+ end
2137
+
2138
+ def compute_cumulative_pace(start_dttm)
2139
+ if @cumulativeDistance > 0
2140
+ secsDiff = @dttm.seconds_diff(start_dttm)
2141
+ secsMile = ((secsDiff.to_f) / (@cumulativeDistance.to_f))
2142
+ minsMile = (secsMile / 60)
2143
+ wholeMins = minsMile.floor
2144
+ secsBal = secsMile - (wholeMins * 60)
2145
+ s1 = "#{secsDiff} #{secsMile} #{minsMile} #{wholeMins} #{secsBal} #{@cumulativeDistance} | "
2146
+ s2 = sprintf("%d:%2.1f", minsMile, secsBal)
2147
+ @cumulativePace = "#{s2}"
2148
+ else
2149
+ @cumulativePace = ""
2150
+ end
2151
+ end
2152
+
2153
+ def set_split(n, tkpt)
2154
+ @split, @prevSplit = n, tkpt
2155
+ end
2156
+
2157
+ def is_split()
2158
+ (@split >= 1)
2159
+ end
2160
+
2161
+ def is_first()
2162
+ @first
2163
+ end
2164
+
2165
+ def is_last()
2166
+ @last
2167
+ end
2168
+
2169
+ def split_info(dtTm)
2170
+ if is_split
2171
+ hhmmss = ''
2172
+ if @prevSplit
2173
+ return "#{@split} #{@dttm.hhmmss_diff(@prevSplit.dttm())}"
2174
+ else
2175
+ return "#{@split} #{@dttm.hhmmss_diff(dtTm)}"
2176
+ end
2177
+ else
2178
+ ""
2179
+ end
2180
+ end
2181
+
2182
+ private
2183
+
2184
+ def deg2rad(degrees)
2185
+ (((0 + degrees) * Math::PI) / 180)
2186
+ end
2187
+
2188
+ def rad2deg(radians)
2189
+ (((0 + radians) * 180) / Math::PI)
2190
+ end
2191
+
2192
+ public
2193
+
2194
+ def as_glatlng(comments, tkpt_count, curr_idx, gpoint_count, start_dttm)
2195
+ if comments
2196
+ secs_diff = @dttm.seconds_diff(start_dttm)
2197
+ fmt_time = @dttm.hhmmss_diff(start_dttm)
2198
+ "\n points.push(new GLatLng(#{latitude_as_float},#{longitude_as_float})); " +
2199
+ "// #{gpoint_count} (#{curr_idx + 1} of #{tkpt_count}) #{@dttm.to_s} #{secs_diff} #{fmt_time} #{@cumulativeDistance} #{split_info(start_dttm)} "
2200
+ else
2201
+ "\n points.push(new GLatLng(#{latitude_as_float},#{longitude_as_float})); "
2202
+ end
2203
+ end
2204
+
2205
+ def as_info_window_html(checkpoint, start_dttm)
2206
+ s = "\"<table align='left'>"
2207
+ if checkpoint
2208
+ secs_diff = @dttm.seconds_diff(start_dttm)
2209
+ fmt_time = @dttm.hhmmss_diff(start_dttm)
2210
+
2211
+ if checkpoint == 'Start'
2212
+ s << "<tr><td colspan='2'><b>Start!</b></td></tr>"
2213
+ elsif checkpoint == 'Finish'
2214
+ s << "<tr><td colspan='2'><b>Finish!</b></td></tr>"
2215
+ else
2216
+ s << "<tr><td colspan='2'><b>Checkpoint #{checkpoint}</b></td></tr>"
2217
+ end
2218
+ s << "<tr><td>Distance: </td><td>#{@cumulativeDistance}</td></tr>"
2219
+ s << "<tr><td>Time of Day: </td><td>#{@dttm.to_s} </td></tr>"
2220
+ s << "<tr><td>Elapsed Time: </td><td>#{fmt_time} </td></tr>"
2221
+ s << "<tr><td>Average Pace: </td><td>#{@cumulativePace} </td></tr>"
2222
+ s << "<tr><td>Lat/Lng: </td><td>#{latitude_as_float} , #{longitude_as_float} </td></tr>"
2223
+ s << "<tr><td>Altitude: </td><td>#{altitude_as_float}m </td></tr>"
2224
+ s
2225
+ end
2226
+ s << "</table>\""
2227
+ s
2228
+ end
2229
+
2230
+ end
2231
+
2232
+
2233
+ =begin rdoc
2234
+ This class provides "user friendly DSL" functionality for the use
2235
+ of Gooby.
2236
+ =end
2237
+
2238
+ class GoobyCommand < GoobyObject
2239
+
2240
+ def initialize
2241
+ end
2242
+
2243
+ def options(yaml_filename)
2244
+ Gooby::Options.new(yaml_filename)
2245
+ end
2246
+
2247
+ def split_forerunner_xml(xml_filename, out_dir)
2248
+ splitter = Gooby::ForerunnerXmlSplitter.new(xml_filename, out_dir)
2249
+ splitter.split
2250
+ end
2251
+
2252
+ def parse_forerunner_xml(xml_filename)
2253
+ handler = Gooby::ForerunnerXmlParser.new
2254
+ Document.parse_stream((File.new xml_filename), handler)
2255
+ handler.put_all_run_tkpt_csv(true)
2256
+ end
2257
+
2258
+ def generate_google_map(csv_filename, options_obj)
2259
+ generator = Gooby::GoogleMapGenerator.new(csv_filename)
2260
+ generator.generate_page(options_obj)
2261
+ end
2262
+
2263
+ end
2264
+
2265
+ end # end of module