gooby 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
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