gooby 1.1.0 → 1.2.0

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 (78) hide show
  1. data/README +200 -35
  2. data/bin/code_scan.rb +1 -3
  3. data/bin/gooby_been_there.rb +12 -14
  4. data/bin/gooby_config.rb +11 -3
  5. data/bin/gooby_csv_validation.rb +50 -0
  6. data/bin/gooby_first_trackpoints_as_poi.rb +31 -0
  7. data/bin/gooby_gen_gmap.rb +7 -3
  8. data/bin/gooby_parser.rb +7 -5
  9. data/bin/gooby_splitter.rb +7 -4
  10. data/bin/gooby_version.rb +7 -3
  11. data/bin/run_all.sh +12 -2
  12. data/bin/run_been_there.sh +4 -1
  13. data/bin/run_config.sh +12 -0
  14. data/bin/run_csv_validation.sh +15 -0
  15. data/bin/run_db_gen.sh +1 -1
  16. data/bin/run_db_load.sh +1 -1
  17. data/bin/run_first_trackpoints_as_poi.sh +16 -0
  18. data/bin/run_gen_gmaps.sh +7 -6
  19. data/bin/run_parse_full.sh +45 -0
  20. data/bin/run_parse_samples.sh +21 -0
  21. data/bin/run_split.sh +5 -4
  22. data/bin/run_version.sh +12 -0
  23. data/config/gooby_config.yaml +130 -131
  24. data/data/20050305_corporate_cup_hm.csv +251 -251
  25. data/data/20050430_nashville_marathon_km.csv +1208 -0
  26. data/data/20060115_phoenix_marathon.csv +1280 -1280
  27. data/data/20070101_davidson_11m.csv +251 -0
  28. data/data/{davidson_11m_20070101.xml → 20070101_davidson_11m.xml} +0 -0
  29. data/data/{davidson_5K_20070505.xml → 20070505_davidson_5k.xml} +0 -0
  30. data/data/20070505_davidson_5k_km.csv +286 -0
  31. data/data/hrm1.csv +5 -0
  32. data/lib/gooby.rb +27 -3144
  33. data/lib/gooby_code_scanner.rb +288 -0
  34. data/lib/gooby_command.rb +210 -0
  35. data/lib/gooby_configuration.rb +123 -0
  36. data/lib/gooby_counter_hash.rb +95 -0
  37. data/lib/gooby_course.rb +117 -0
  38. data/lib/gooby_csv_point.rb +71 -0
  39. data/lib/gooby_csv_reader.rb +71 -0
  40. data/lib/gooby_csv_run.rb +28 -0
  41. data/lib/gooby_delim_line.rb +42 -0
  42. data/lib/gooby_dttm.rb +87 -0
  43. data/lib/gooby_duration.rb +86 -0
  44. data/lib/gooby_forerunner_xml_parser.rb +191 -0
  45. data/lib/gooby_forerunner_xml_splitter.rb +115 -0
  46. data/lib/gooby_google_map_generator.rb +385 -0
  47. data/lib/gooby_history.rb +41 -0
  48. data/lib/gooby_kernel.rb +163 -0
  49. data/lib/gooby_lap.rb +30 -0
  50. data/lib/gooby_line.rb +80 -0
  51. data/lib/gooby_object.rb +22 -0
  52. data/lib/gooby_point.rb +172 -0
  53. data/lib/gooby_run.rb +213 -0
  54. data/lib/gooby_simple_xml_parser.rb +50 -0
  55. data/lib/gooby_test_helper.rb +23 -0
  56. data/lib/gooby_track.rb +47 -0
  57. data/lib/gooby_track_point.rb +229 -0
  58. data/lib/gooby_training_center_xml_parser.rb +224 -0
  59. data/lib/gooby_training_center_xml_splitter.rb +116 -0
  60. data/lib/split_code.sh +29 -0
  61. data/samples/20050305_corporate_cup_hm.html +269 -269
  62. data/samples/20050430_nashville_marathon.html +1410 -1266
  63. data/samples/20060115_phoenix_marathon.html +1311 -1311
  64. data/samples/{davidson_11m_20070101.html → 20070101_davidson_11m.html} +267 -267
  65. data/samples/20070505_davidson_5k.html +413 -0
  66. data/samples/been_there.txt +52 -704
  67. data/samples/hrm1.html +87 -0
  68. data/sql/gooby.ddl +20 -16
  69. data/sql/gooby_load.dml +36 -9
  70. metadata +48 -14
  71. data/bin/example_usage.txt +0 -55
  72. data/bin/run_parse.sh +0 -43
  73. data/bin/run_parse_named.sh +0 -19
  74. data/data/20050430_nashville_marathon.csv +0 -1208
  75. data/data/davidson_11m_20070101.csv +0 -251
  76. data/data/davidson_5K_20070505.csv +0 -286
  77. data/data/test1.txt +0 -4
  78. data/samples/davidson_5K_20070505.html +0 -395
data/data/hrm1.csv ADDED
@@ -0,0 +1,5 @@
1
+ #cols: primary_key|run_id|date|time|tkpt_num|latitude|longitude|altitude|heartbeat|run_distance|uom|run_elapsed|lap_tkpt_number|lap_distance|lap_elapsed
2
+ 2007-07-16T23:42:44Z.1|2007-07-16T23:42:44Z|2007-07-16|23:42:44|1|35.495497|-80.832159|241.908936|75|0.0|mi|00:00:00|1|0|00:00:00
3
+ 2007-07-16T23:42:44Z.2|2007-07-16T23:42:44Z|2007-07-16|23:42:46|2|35.495499|-80.832152|241.428223|75|0.000417102641276755|mi|00:00:02|2|0.000417102641276755|00:00:02
4
+ 2007-07-16T23:42:44Z.5|2007-07-16T23:42:44Z|2007-07-16|23:42:54|5|35.495522|-80.832229|242.870239|74|0.00503094417417941|mi|00:00:10|3|0.00503094417417941|00:00:10
5
+ 2007-07-16T23:42:44Z.6|2007-07-16T23:42:44Z|2007-07-16|23:47:46|6|35.495465|-80.832151|242.389648|87|0.0109261260004515|mi|00:05:02|4|0.0109261260004515|00:05:02
data/lib/gooby.rb CHANGED
@@ -1,43 +1,6 @@
1
1
  =begin
2
2
 
3
3
  Gooby = Google APIs + Ruby
4
- This file contains the classes and modules for the Gooby project.
5
-
6
- See file 'tests/ts_gooby.rb' for the regression test suite.
7
-
8
- Index of Modules and Classes in this file:
9
- line type Module or Class name
10
- ---- ------ ---------------------------
11
- 57 module Gooby
12
- 64 module GoobyKernel
13
- 191 module TestHelper
14
- 209 class GoobyObject
15
- 222 class CounterHash
16
- 305 class DelimLine
17
- 340 class DtTm
18
- 417 class Duration
19
- 496 class ForerunnerXmlParser
20
- 665 class ForerunnerXmlSplitter
21
- 775 class TrainingCenterXmlParser
22
- 962 class TrainingCenterXmlSplitter
23
- 1067 class GeoData
24
- 1250 class GoogleMapGenerator
25
- 1625 class History
26
- 1656 class Lap
27
- 1671 class Line
28
- 1749 class Configuration
29
- 1848 class Point
30
- 1987 class CsvPoint
31
- 2042 class CvsRun
32
- 2060 class CsvReader
33
- 2121 class Trackpoint
34
- 2319 class Run
35
- 2521 class SimpleXmlParser
36
- 2561 class Track
37
- 2593 class Course
38
- 2713 class CodeScanner
39
- 3003 class GoobyCommand
40
-
41
4
  Gooby - Copyright 2007 by Chris Joakim.
42
5
  Gooby is available under GNU General Public License (GPL) license.
43
6
 
@@ -52,3110 +15,30 @@ require 'singleton'
52
15
  require 'time'
53
16
  require 'yaml'
54
17
 
55
- # =============================================================================
56
-
57
- module Gooby
58
-
59
- =begin rdoc
60
- This module is used to embed information about the Gooby project and
61
- version into the codebase.
62
- =end
63
-
64
- module GoobyKernel
65
-
66
- # Return a String version number, like '1.1.0'.
67
-
68
- def project_name
69
- 'Gooby'
70
- end
71
-
72
- def project_version_number
73
- '1.1.0'
74
- end
75
-
76
- # Return a String date, like '2007/03/21'.
77
- def project_date
78
- '2007/06/10'
79
- end
80
-
81
- # Return a String containing the project author name.
82
- def project_author
83
- 'Chris Joakim'
84
- end
85
-
86
- # Return a String year, like '2007'.
87
- def project_year
88
- project_date[0...4] # start, length
89
- end
90
-
91
- # Return a String containing copyright, year, and author.
92
- def project_copyright
93
- "Copyright (C) #{project_year} #{project_author}"
94
- end
95
-
96
- def project_embedded_comment
97
- "#{project_name} #{project_version_number}"
98
- end
99
-
100
- # Return a String containing GNU/GPL, and the gpl.html URL.
101
- def project_license
102
- 'GNU General Public License (GPL). See http://www.gnu.org/copyleft/gpl.html'
103
- end
104
-
105
- # Return an Array of lines in file, optionally stripped.
106
- def read_lines(filename, strip=false)
107
-
108
- array = IO.readlines(filename)
109
- if strip
110
- array = strip_lines(array)
111
- end
112
- return array
113
- end
114
-
115
- # Return an Array of lines in file per the given delimeter, optionally stripped.
116
- def read_as_ascii_lines(filename, delim=10, strip=false)
117
-
118
- array = Array.new
119
- file = File.new(filename)
120
- currLine = ''
121
- bytesRead = 0
122
- linesRead = 0
123
-
124
- file.each_byte { |b|
125
- bytesRead = bytesRead + 1
126
- if (b == delim) # delim is 13 for quicken, 10 for address book xml
127
- array << currLine
128
- currLine = ''
129
- linesRead = linesRead + 1
130
- else
131
- if (b < 127)
132
- currLine << "#{b.chr}"
133
- end
134
- end
135
- }
136
-
137
- if currLine.size > 0
138
- array << currLine
139
- end
140
- if strip
141
- array = strip_lines(array)
142
- end
143
- return array
144
- end
145
-
146
- # Strip the lines/Strings; return a new Array.
147
- def strip_lines(array)
148
-
149
- newArray = Array.new
150
- if (array != nil)
151
- array.each { |line| line.strip! ; newArray << line }
152
- end
153
- return newArray
154
- end
155
-
156
- def tokenize(string, delim=nil, strip=false)
157
- if string
158
- tokens = string.split(delim)
159
- if strip
160
- tokens.each { |tok| tok.strip! }
161
- end
162
- tokens
163
- else
164
- Array.new
165
- end
166
- end
167
-
168
- def default_delimiter
169
- return '|'
170
- end
171
-
172
- def invalid_time
173
- return -99999999
174
- end
175
-
176
- def invalid_latitude
177
- return -1
178
- end
179
-
180
- def invalid_longitude
181
- return -1
182
- end
183
-
184
- def invalid_altitude
185
- return -1
186
- end
187
- end
188
-
189
- # =============================================================================
190
-
191
- module TestHelper
192
-
193
- def setup
194
- puts "test: #{name}"
195
- end
196
-
197
- def teardown
198
- @debug = false
199
- end
200
- end
201
-
202
- # =============================================================================
203
-
204
- =begin rdoc
205
- This is the abstract superclass of several Gooby classes.
206
- Includes modules GoobyIO, Introspect, and GoobyProjectInfo.
207
- =end
208
-
209
- class GoobyObject
210
-
211
- include Gooby::GoobyKernel
212
- end
213
-
214
- # =============================================================================
215
-
216
- =begin rdoc
217
- This class wrappers a Hash object and provides increment/decrement functionality
218
- for a given key. It is used to sum the number of things (i.e. - xml tags) in a
219
- collection.
220
- =end
221
-
222
- class CounterHash < GoobyObject
223
-
224
- def initialize
225
- @hash = Hash.new(0)
226
- end
227
-
228
- def size
229
- @hash.size
230
- end
231
-
232
- # Return the Integer count for the given key; zero default.
233
- def value(key)
234
- (@hash.has_key?(key)) ? @hash[key] : 0
235
- end
236
-
237
- # Increment the count for the given key.
238
- def increment(key)
239
- if key == nil
240
- return
241
- end
242
- if (@hash.has_key?(key))
243
- val = @hash[key]
244
- @hash[key] = val + 1
245
- else
246
- @hash[key] = 1
247
- end
248
- end
249
-
250
- def increment_tokens(text)
251
- tokens = tokenize(text)
252
- tokens.each { |token| increment(token) }
253
- end
254
-
255
- # Decrement the count for the given key.
256
- def decrement(key)
257
- if key == nil
258
- return
259
- end
260
- if (@hash.has_key?(key))
261
- val = @hash[key]
262
- @hash[key] = val - 1
263
- else
264
- @hash[key] = -1
265
- end
266
- end
267
-
268
- # Return an Array of the sorted keys.
269
- def sorted_keys
270
- @hash.keys.sort
271
- end
272
-
273
- # Return a String containing all key=val pairs.
274
- def to_s
275
- s = "CHash:"
276
- sorted_keys.each { |key|
277
- val = @hash[key]
278
- s << " key: [#{key}] val: [#{val}]\n"
279
- }
280
- s
281
- end
282
-
283
- # Return an XML String containing all key=val pairs, optionally aligned.
284
- def to_xml(aligned=false)
285
- s = "<CHash>"
286
- sorted_keys.each { |key|
287
- val = @hash[key]
288
- (aligned) ? s << "\n " : s << ''
289
- s << " <entry key='#{key}' value='#{val}'/>"
290
- }
291
- if aligned
292
- s << "\n "
293
- end
294
- s << " </CHash>"
295
- s
296
- end
297
- end
298
-
299
- # =============================================================================
300
-
301
- =begin rdoc
302
- Instances of this class represent a delimited line of text, such as csv.
303
- =end
304
-
305
- class DelimLine < GoobyObject
306
-
307
- attr_reader :line, :trim, :delim, :tokens
308
-
309
- def initialize(line, trim=true, delim=default_delimiter)
310
- @line = line
311
- @trim = trim
312
- @delim = delim
313
- @tokens = @line.split(@delim)
314
- if trim
315
- @tokens.each { | token | token.strip! }
316
- end
317
- end
318
-
319
- def as_trackpoint(num_idx, lat_idx, lng_idx, alt_idx, dttm_idx)
320
- Trackpoint.new(@tokens[num_idx], @tokens[lat_idx], @tokens[lng_idx], @tokens[alt_idx], @tokens[dttm_idx])
321
- end
322
-
323
- def is_comment?
324
- @line.strip.match('^#') ? true : false
325
- end
326
-
327
- def to_s
328
- "DelimLine: length: #{@line.size} trim: #{@trim} delim: #{@delim} tokens: #{@tokens.size}"
329
- end
330
- end
331
-
332
- # =============================================================================
333
-
334
- =begin rdoc
335
- Instances of this class represent a Date and Time as parsed from a value
336
- such as '2006-01-15T13:41:40Z' in an XML file produced by a GPS device.
337
- It wrappers both a DateTime and Time object.
338
- =end
339
-
340
- class DtTm < GoobyObject
341
-
342
- attr_accessor :rawdata, :dateTime, :time, :valid
343
-
344
- # Constructor; arg is a String like '2006-01-15T13:41:40Z'.
345
- def initialize(raw)
346
- if raw
347
- @rawdata = raw.strip
348
- if @rawdata.size > 18
349
- @date_time = DateTime.parse(@rawdata[0..18])
350
- @time = Time.parse(@date_time.to_s)
351
- @valid = true
352
- else
353
- @valid = false
354
- end
355
- else
356
- @rawdata = ''
357
- @valid = false
358
- end
359
- end
360
-
361
- public
362
-
363
- # Return @time.to_i
364
- def to_i()
365
- (@time) ? @time.to_i : invalid_time
366
- end
367
-
368
- # Calculates and returns diff between another instance.
369
- def seconds_diff(anotherDtTm)
370
- if anotherDtTm
371
- to_i - anotherDtTm.to_i
372
- else
373
- invalid_time
374
- end
375
- end
376
-
377
- def yyyy_mm_dd
378
- @time.strftime("%Y-%m-%d")
379
- end
380
-
381
- def yyyy_mm_dd_hh_mm_ss(delim=' ')
382
- @time.strftime("%Y-%m-%d#{delim}%H:%M:%S")
383
- end
384
-
385
- def hh_mm_ss
386
- @time.strftime("%H:%M:%S")
387
- end
388
-
389
- # Calculate and return time diff in 'hh:mm:ss' format.
390
- def hhmmss_diff(anotherDtTm)
391
- if anotherDtTm
392
- t = @time - (anotherDtTm.to_i)
393
- t.strftime("%H:%M:%S")
394
- else
395
- '00:00:00'
396
- end
397
- end
398
-
399
- def to_s
400
- "#{@rawdata}"
401
- end
402
-
403
- # Return a String with state values for debugging.
404
- def print_string
405
- "DtTm: #{yyyy_mm_dd_hh_mm_ss} #{to_i} #{@rawdata}"
406
- end
407
- end
408
-
409
- # =============================================================================
410
-
411
- =begin rdoc
412
- Instances of this class represent the contents of a Forerunner extract
413
- <Duration> tag, such as:
414
- <Duration>PT507.870S</Duration>
415
- =end
416
-
417
- class Duration < GoobyObject
418
-
419
- attr_accessor :rawdata, :seconds, :minutes, :mmss
420
-
421
- # Constructor; arg is a String like 'PT507.870S'.
422
- def initialize(raw)
423
- if raw
424
- @rawdata = scrub("#{raw}")
425
- @seconds = @rawdata.to_f
426
- @minutes = @seconds / 60
427
- @base_min = @minutes.floor
428
- @frac_min = @minutes - @base_min
429
- @frac_sec = @frac_min * 60
430
-
431
- @mmss = ''
432
- if (@base_min < 10)
433
- @mmss << "0#{@base_min}:"
434
- else
435
- @mmss << "#{@base_min}:"
436
- end
437
- if (@frac_sec < 10)
438
- @mmss << "0#{@frac_sec}"
439
- else
440
- @mmss << "#{@frac_sec}"
441
- end
442
- if (@mmss.size > 8)
443
- @mmss = @mmss[0..8]
444
- end
445
- else
446
- @rawdata = ''
447
- @seconds = invalidDistance
448
- @minutes = invalidMinutes
449
- @mmss = '??:??.?'
450
- end
451
- end
452
-
453
- private
454
-
455
- def scrub(raw)
456
- if (raw)
457
- raw.strip!
458
- newStr = ''
459
- raw.each_byte { | b |
460
- if ((b >= 48) && (b <= 57))
461
- newStr << b
462
- end
463
- if (b == 46)
464
- newStr << b
465
- end
466
- }
467
- return newStr
468
- else
469
- ''
470
- end
471
- end
472
-
473
- public
474
-
475
- def to_s
476
- "#{@mmss}"
477
- end
478
-
479
- # Return a String with state values for debugging.
480
- def print_string
481
- "Duration: #{@rawdata} sec: #{@seconds} min: #{@minutes} mmss: #{@mmss} bm: #{@base_min} fm: #{@frac_min} fs: #{@frac_sec}"
482
- end
483
- end
484
-
485
- # =============================================================================
486
-
487
- =begin rdoc
488
- Instances of this class are used to parse a Forerunner XML file in a SAX-like
489
- manner. Instances of the model classes - History, Run, Track, Trackpoint,
490
- etc. are created in this parsing process.
491
-
492
- See http://www.garmin.com/xmlschemas/ForerunnerLogbookv1.xsd for the XML Schema
493
- Definition for the Garmin Forerunner XML. The Gooby object model mirrors this XSD.
494
- =end
495
-
496
- class ForerunnerXmlParser
497
-
498
- DETAIL_TAGS = %w( Notes StartTime Duration Length Latitude Longitude Altitude Time BeginPosition EndPosition )
499
-
500
- include REXML::StreamListener
501
-
502
- attr_reader :history, :cvHash, :tagCount
503
-
504
- def initialize
505
- @cv_hash = Hash.new("")
506
- @tag_count = 0
507
- @run_count = 0
508
- @lap_count = 0
509
- @track_count = 0
510
- @trackpoint_count = 0
511
- @curr_text = "";
512
- @history = History.new
513
- @curr_run = nil
514
- @curr_lap = nil
515
- @curr_track = nil
516
- @curr_begin_position = nil
517
- @@curr_end_position = nil
518
- end
519
-
520
- public
521
-
522
- # SAX API method; handles 'Run', 'Lap', 'Track'.
523
- def tag_start(tagname, attrs)
524
- @tag_count += 1
525
- @currTag = tagname
526
- @cv_hash[tagname] = ''
527
-
528
- if detail_tag?(tagname)
529
- @inDetail = true
530
- end
531
-
532
- if is_tag?('Run', tagname)
533
- @run_count = @run_count + 1
534
- @lap_count = 0
535
- @track_count = 0
536
- @trackpoint_count = 0
537
- @curr_run = Run.new(@run_count)
538
- @history.add_run(@curr_run)
539
- @cv_hash['Notes'] = ''
540
- return
541
- end
542
-
543
- if is_tag?('Lap', tagname)
544
- @lap_count = @lap_count + 1
545
- @curr_lap = Lap.new(@lap_count)
546
- return
547
- end
548
-
549
- if is_tag?('Track', tagname)
550
- @track_count = @track_count + 1
551
- @curr_track = Track.new(@track_count)
552
- return
553
- end
554
- end
555
-
556
- # SAX API method; handles 'Position', 'Trackpoint', 'Track', 'Lap', 'Run'.
557
- def tag_end(tagname)
558
- if @inDetail
559
- @cv_hash[tagname] = @curr_text
560
- else
561
- if is_tag?('Position', tagname)
562
- lat = @cv_hash['Latitude']
563
- long = @cv_hash['Longitude']
564
- @curr_begin_position = Point.new(lat.strip, long.strip)
565
- @@curr_end_position = Point.new(lat.strip, long.strip)
566
- end
567
-
568
- if is_tag?('BeginPosition', tagname)
569
- lat = @cv_hash['Latitude']
570
- long = @cv_hash['Longitude']
571
- @curr_begin_position = Point.new(lat.strip, long.strip)
572
- end
573
-
574
- if is_tag?('EndPosition', tagname)
575
- lat = @cv_hash['Latitude']
576
- long = @cv_hash['Longitude']
577
- @@curr_end_position = Point.new(lat.strip, long.strip)
578
- end
579
-
580
- if is_tag?('Trackpoint', tagname)
581
- @trackpoint_count = @trackpoint_count + 1
582
- lat = @cv_hash['Latitude']
583
- long = @cv_hash['Longitude']
584
- alt = @cv_hash['Altitude']
585
- time = @cv_hash['Time']
586
- tp = Trackpoint.new(@trackpoint_count, lat, long, alt, time)
587
- @curr_track.add_trackpoint(tp)
588
- end
589
-
590
- if is_tag?('Track', tagname)
591
- if @curr_run != nil
592
- @curr_run.add_track(@curr_track)
593
- end
594
- end
595
-
596
- if is_tag?('Lap', tagname)
597
- @curr_lap.startTime = @cv_hash['StartTime']
598
- @curr_lap.duration = Duration.new(@cv_hash['Duration'])
599
- @curr_lap.length = @cv_hash['Length']
600
- @curr_lap.begin_position = @curr_begin_position
601
- @curr_lap.end_position = @@curr_end_position
602
- @curr_run.add_lap(@curr_lap)
603
- end
604
-
605
- if is_tag?('Run', tagname)
606
- @curr_run.notes = @cv_hash['Notes']
607
- end
608
- end
609
-
610
- @inDetail = false
611
- @curr_text = ""
612
- @currTag = ""
613
- end
614
-
615
- # SAX API method.
616
- def text(txt)
617
- if @inDetail
618
- @curr_text = @curr_text + txt
619
- end
620
- end
621
-
622
- # Iterate all parsed Run objects and print each with to_s.
623
- def gdump()
624
- @history.runs().each { |run| puts run.to_s }
625
- end
626
-
627
- # Iterate all parsed Run objects and print each with to_s.
628
- def dump()
629
- @history.runs().each { |run| puts run.to_s }
630
- end
631
-
632
- # Iterate all parsed Run objects and print each with put_csv.
633
- def put_run_csv()
634
- @history.runs().each { |run| run.put_csv() }
635
- end
636
-
637
- # Iterate all parsed Run objects and print each with put_tkpt_csv.
638
- def put_all_run_tkpt_csv()
639
- @history.runs.each { |run| run.put_tkpt_csv() }
640
- end
641
-
642
- private
643
-
644
- def is_tag?(tagname, value)
645
- tagname == value
646
- end
647
-
648
- def detail_tag?(tagname)
649
- DETAIL_TAGS.each { |typ|
650
- if typ == tagname
651
- return true
652
- end
653
- }
654
- return false
655
- end
656
- end
657
-
658
- # =============================================================================
659
-
660
- =begin rdoc
661
- Instances of this class are used to split a large ForerunnerLogbook
662
- XML file into individual 'run_' files.
663
- =end
664
-
665
- class ForerunnerXmlSplitter < GoobyObject
666
-
667
- attr_reader :out_dir, :forerunner_files, :out_files_hash
668
-
669
- def initialize(xml_file, out_dir)
670
- @out_dir = out_dir
671
- @forerunner_files = Array.new
672
- @forerunner_files << xml_file
673
- @out_files_hash = Hash.new
674
- end
675
-
676
- def split
677
- @forerunner_files.each { |f| process_file(f) }
678
- write_files
679
- end
680
-
681
- private
682
-
683
- def process_file(forerunnerXmlFile)
684
- @file_name = forerunnerXmlFile
685
- @xml_lines = read_lines(@file_name, false)
686
- @line_num = 0
687
- @run_num = 0
688
- @curr_run_lines = Array.new
689
- @curr_run_tkpts = 0
690
- @start_line_num = 0
691
- @end_line_num = 0
692
- @first_start_time = nil
693
-
694
- @xml_lines.each { |line|
695
- @line_num = @line_num + 1
696
- if (line.match(/<Run>/))
697
- @run_num = @run_num + 1
698
- @start_line_num = @line_num
699
- @curr_run_lines = Array.new
700
- @curr_run_lines << line
701
- elsif (line.match(/<StartTime>/)) # <StartTime>2007-01-13T15:37:06Z</StartTime>
702
- @curr_run_lines << line
703
- if @first_start_time == nil
704
- clone = String.new(line)
705
- clone.gsub!(/[<>]/, ' ')
706
- clone.gsub!(/[-:T]/, '_')
707
- clone.gsub!(/[Z]/, '')
708
- tokens = clone.split
709
- @first_start_time = tokens[1]
710
- end
711
- elsif (line.match(/<Trackpoint>/))
712
- @curr_run_tkpts = @curr_run_tkpts + 1
713
- @curr_run_lines << line
714
- elsif (line.match(/<\/Run>/))
715
- @end_line_num = @line_num
716
- @curr_run_lines << line
717
- end_run
718
- elsif (@curr_run_lines.size > 0)
719
- @curr_run_lines << line
720
- end
721
- }
722
- end
723
-
724
- def end_run
725
- out_file = "#{@out_dir}/run_#{@first_start_time}.xml"
726
- comment = "<!-- file: #{out_file} lines: #{@curr_run_lines.size} (#{@start_line_num} to #{@end_line_num}) tkpts: #{@curr_run_tkpts} --> \n"
727
- @curr_run_lines.insert(0, comment)
728
-
729
- prev_entry = @out_files_hash[out_file]
730
- if prev_entry
731
- if (@curr_run_lines.size >= prev_entry.size)
732
- puts "previous entry overlaid for #{out_file}. curr=#{@curr_run_lines.size} prev=#{prev_entry.size}"
733
- @out_files_hash[out_file] = @curr_run_lines
734
- else
735
- puts "previous entry retained for #{out_file}. curr=#{@curr_run_lines.size} prev=#{prev_entry.size}"
736
- end
737
- else
738
- puts "new entry for #{out_file}. curr=#{@curr_run_lines.size}"
739
- @out_files_hash[out_file] = @curr_run_lines
740
- end
741
-
742
- @curr_run_lines = Array.new
743
- @curr_run_tkpts = 0
744
- @start_line_num = 0
745
- @end_line_num = 0
746
- @first_start_time = nil
747
- end
748
-
749
- def write_files
750
- out_names = @out_files_hash.keys.sort
751
- puts "Writing #{out_names.size} extract files..."
752
- out_names.each { |out_name|
753
- lines = @out_files_hash[out_name]
754
- out = File.new out_name, "w+"
755
- lines.each { |line| out.write line }
756
- out.flush
757
- out.close
758
- puts "File written: #{out_name}"
759
- }
760
- puts "output files written."
761
- end
762
- end
763
-
764
- # =============================================================================
765
-
766
- =begin rdoc
767
- Instances of this class are used to parse a Garmin TrainingCenter XML(TCX) file
768
- in a SAX-like manner. Instances of the model classes - History, Run, Track,
769
- Trackpoint, etc. are created in this parsing process.
770
-
771
- See http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd for the XML
772
- Schema Definition for Garmin TrainingCenter XML.
773
- =end
774
-
775
- class TrainingCenterXmlParser
776
-
777
- DETAIL_TAGS = %w( Notes StartTime Duration Length Time
778
- TotalTimeSeconds DistanceMeters
779
- LatitudeDegrees LongitudeDegrees AltitudeMeters BeginPosition EndPosition )
780
-
781
- include REXML::StreamListener
782
-
783
- attr_reader :history, :cvHash, :tagCount
784
-
785
- def initialize
786
- @cv_hash = Hash.new("")
787
- @tag_count = 0
788
- @run_count = 0
789
- @lap_count = 0
790
- @track_count = 0
791
- @trackpoint_count = 0
792
- @curr_text = "";
793
- @history = History.new
794
- @curr_run = nil
795
- @curr_lap = nil
796
- @curr_track = nil
797
- @curr_begin_position = nil
798
- @@curr_end_position = nil
799
- @first_lap_start_time = nil
800
- @curr_lap_start_time = ''
801
- end
802
-
803
- public
804
-
805
- # SAX API method; handles 'Activity', 'Lap', 'Track'.
806
- def tag_start(tagname, attrs)
807
- @tag_count += 1
808
- @currTag = tagname
809
- @cv_hash[tagname] = ''
810
-
811
- if detail_tag?(tagname)
812
- @inDetail = true
813
- end
814
-
815
- if is_tag?('Activity', tagname)
816
- @run_count = @run_count + 1
817
- @lap_count = 0
818
- @track_count = 0
819
- @trackpoint_count = 0
820
- @curr_run = Run.new(@run_count)
821
- @history.add_run(@curr_run)
822
- @cv_hash['Notes'] = ''
823
- return
824
- end
825
-
826
- if is_tag?('Lap', tagname)
827
- @lap_count = @lap_count + 1
828
- @curr_lap = Lap.new(@lap_count)
829
-
830
- attrs.each { |attr|
831
- name = attr[0]
832
- val = attr[1]
833
- if (name && (name == 'StartTime'))
834
- if (@first_lap_start_time == nil)
835
- @first_lap_start_time = "#{val}"
836
- end
837
- @curr_lap_start_time = "#{val}"
838
- end
839
- }
840
- # TODO - capture value of 'StartTime' attribute.
841
- return
842
- end
843
-
844
- if is_tag?('Track', tagname)
845
- @track_count = @track_count + 1
846
- @curr_track = Track.new(@track_count)
847
- return
848
- end
849
-
850
- end
851
-
852
- # SAX API method; handles 'Position', 'Trackpoint', 'Track', 'Lap', 'Run'.
853
- def tag_end(tagname)
854
- if @inDetail
855
- @cv_hash[tagname] = @curr_text
856
- else
857
- if is_tag?('Position', tagname)
858
- lat = @cv_hash['LatitudeDegrees']
859
- long = @cv_hash['LongitudeDegrees']
860
- @curr_begin_position = Point.new(lat.strip, long.strip)
861
- @@curr_end_position = Point.new(lat.strip, long.strip)
862
- end
863
-
864
- if is_tag?('BeginPosition', tagname)
865
- lat = @cv_hash['LatitudeDegrees']
866
- long = @cv_hash['LongitudeDegrees']
867
- @curr_begin_position = Point.new(lat.strip, long.strip)
868
- end
869
-
870
- if is_tag?('EndPosition', tagname)
871
- lat = @cv_hash['LatitudeDegrees']
872
- long = @cv_hash['LongitudeDegrees']
873
- @@curr_end_position = Point.new(lat.strip, long.strip)
874
- end
875
-
876
- if is_tag?('Trackpoint', tagname)
877
- @trackpoint_count = @trackpoint_count + 1
878
- lat = @cv_hash['LatitudeDegrees']
879
- long = @cv_hash['LongitudeDegrees']
880
- alt = @cv_hash['AltitudeMeters']
881
- time = @cv_hash['Time']
882
-
883
- hash = Hash.new('')
884
- hash['lap_number'] = "#{@lap_count}"
885
- hash['first_lap_start_time'] = "#{@first_lap_start_time}"
886
- hash['curr_lap_start_time'] = "#{@curr_lap_start_time}"
887
-
888
- tp = Trackpoint.new(@trackpoint_count, lat, long, alt, time, hash)
889
- @curr_track.add_trackpoint(tp)
890
- end
891
-
892
- if is_tag?('Track', tagname)
893
- if @curr_run != nil
894
- @curr_run.add_track(@curr_track)
895
- end
896
- end
897
-
898
- if is_tag?('Lap', tagname)
899
- @curr_run.add_lap(@curr_lap)
900
- end
901
-
902
- if is_tag?('Activity', tagname)
903
- @curr_run.notes = @cv_hash['Notes']
904
- end
905
- end
906
-
907
- @inDetail = false
908
- @curr_text = ""
909
- @currTag = ""
910
- end
911
-
912
- # SAX API method.
913
- def text(txt)
914
- if @inDetail
915
- @curr_text = @curr_text + txt
916
- end
917
- end
918
-
919
- # Iterate all parsed Run objects and print each with to_s.
920
- def gdump()
921
- @history.runs().each { |run| puts run.to_s }
922
- end
923
-
924
- # Iterate all parsed Run objects and print each with to_s.
925
- def dump()
926
- @history.runs().each { |run| puts run.to_s }
927
- end
928
-
929
- # Iterate all parsed Run objects and print each with put_csv.
930
- def put_run_csv()
931
- @history.runs().each { |run| run.put_csv() }
932
- end
933
-
934
- # Iterate all parsed Run objects and print each with put_tkpt_csv.
935
- def put_all_run_tkpt_csv()
936
- @history.runs.each { |run| run.put_tkpt_csv() }
937
- end
938
-
939
- private
940
-
941
- def is_tag?(tagname, value)
942
- tagname == value
943
- end
944
-
945
- def detail_tag?(tagname)
946
- DETAIL_TAGS.each { |typ|
947
- if typ == tagname
948
- return true
949
- end
950
- }
951
- return false
952
- end
953
- end
954
-
955
- # =============================================================================
956
-
957
- =begin rdoc
958
- Instances of this class are used to split a large Garmin TrainingCenter
959
- *.tcx file into individual 'activity_' files.
960
- =end
961
-
962
- class TrainingCenterXmlSplitter < GoobyObject
963
-
964
- attr_reader :out_dir, :training_center_files, :out_files_hash
965
-
966
- def initialize(tcx_file, out_dir)
967
- @out_dir = out_dir
968
- @training_center_files = Array.new
969
- @training_center_files << tcx_file
970
- @out_files_hash = Hash.new
971
- end
972
-
973
- def split
974
- @training_center_files.each { |f| process_file(f) }
975
- write_files
976
- end
977
-
978
- private
979
-
980
- def process_file(training_center_tcx_file)
981
- @file_name = training_center_tcx_file
982
- @tcx_lines = read_lines(@file_name, false)
983
- @line_num = 0
984
- @activity_num = 0
985
- @curr_activity_lines = Array.new
986
- @curr_activity_tkpts = 0
987
- @start_line_num = 0
988
- @end_line_num = 0
989
- @activity_start_time = nil
990
-
991
- @tcx_lines.each { |line|
992
- @line_num = @line_num + 1
993
- if (line.match(/<Activity /))
994
- @activity_num = @activity_num + 1
995
- @start_line_num = @line_num
996
- @curr_activity_lines = Array.new
997
- @curr_activity_lines << line
998
- elsif (line.match(/<Id>/)) # <Id>2007-03-03T15:58:57Z</Id> <StartTime>2007-01-13T15:37:06Z</StartTime>
999
- @curr_activity_lines << line
1000
- if @activity_start_time == nil
1001
- clone = String.new(line)
1002
- clone.gsub!(/[<>]/, ' ')
1003
- clone.gsub!(/[-:T]/, '_')
1004
- clone.gsub!(/[Z]/, '')
1005
- tokens = clone.split
1006
- @activity_start_time = tokens[1]
1007
- end
1008
- elsif (line.match(/<Trackpoint>/))
1009
- @curr_activity_tkpts = @curr_activity_tkpts + 1
1010
- @curr_activity_lines << line
1011
- elsif (line.match(/<\/Activity/))
1012
- @end_line_num = @line_num
1013
- @curr_activity_lines << line
1014
- end_run
1015
- elsif (@curr_activity_lines.size > 0)
1016
- @curr_activity_lines << line
1017
- end
1018
- }
1019
- end
1020
-
1021
- def end_run
1022
- out_file = "#{@out_dir}/activity_#{@activity_start_time}.xml"
1023
- comment = "<!-- file: #{out_file} lines: #{@curr_activity_lines.size} (#{@start_line_num} to #{@end_line_num}) tkpts: #{@curr_activity_tkpts} --> \n"
1024
- @curr_activity_lines.insert(0, comment)
1025
-
1026
- prev_entry = @out_files_hash[out_file]
1027
- if prev_entry
1028
- if (@curr_activity_lines.size >= prev_entry.size)
1029
- puts "previous entry overlaid for #{out_file}. curr=#{@curr_activity_lines.size} prev=#{prev_entry.size}"
1030
- @out_files_hash[out_file] = @curr_activity_lines
1031
- else
1032
- puts "previous entry retained for #{out_file}. curr=#{@curr_activity_lines.size} prev=#{prev_entry.size}"
1033
- end
1034
- else
1035
- puts "new entry for #{out_file}. curr=#{@curr_activity_lines.size}"
1036
- @out_files_hash[out_file] = @curr_activity_lines
1037
- end
1038
-
1039
- @curr_activity_lines = Array.new
1040
- @curr_activity_tkpts = 0
1041
- @start_line_num = 0
1042
- @end_line_num = 0
1043
- @activity_start_time = nil
1044
- end
1045
-
1046
- def write_files
1047
- out_names = @out_files_hash.keys.sort
1048
- puts "Writing #{out_names.size} extract files..."
1049
- out_names.each { |out_name|
1050
- lines = @out_files_hash[out_name]
1051
- out = File.new out_name, "w+"
1052
- lines.each { |line| out.write line }
1053
- out.flush
1054
- out.close
1055
- puts "File written: #{out_name}"
1056
- }
1057
- puts "output files written."
1058
- end
1059
- end
1060
-
1061
- # =============================================================================
1062
-
1063
- =begin rdoc
1064
- Instances of this class represent a the set of Geographic data defined in file geo.txt
1065
- =end
1066
-
1067
- class GeoData < GoobyObject
1068
-
1069
- attr_reader :filename, :lines, :poi_hash, :poi_array, :track_hash, :track_array, :route_hash, :route_array
1070
-
1071
- def initialize(filename)
1072
- @filename = filename
1073
- @filename = 'data/geo_data.txt' if @filename == nil
1074
- @lines = read_lines(@filename, true)
1075
- @poi_hash = Hash.new
1076
- @poi_array = Array.new
1077
- @track_hash = Hash.new
1078
- @track_array = Array.new
1079
- @route_hash = Hash.new
1080
- @route_array = Array.new
1081
- parse_poi
1082
- parse_tracks
1083
- parse_routes
1084
- end
1085
-
1086
- private
1087
-
1088
- def parse_poi
1089
- in_poi, poi_number = false, 0
1090
- @lines.each { |line|
1091
- line_obj = Line.new(line, nil, true)
1092
- tok_count = line_obj.token_count
1093
- is_point = line_obj.token_idx_equals(0, '.')
1094
-
1095
- if line_obj.is_populated_non_comment
1096
- if line_obj.match('points_of_interest_start')
1097
- in_poi = true
1098
- elsif line_obj.match('points_of_interest_end')
1099
- in_poi = false
1100
- elsif in_poi && tok_count > 2 && is_point
1101
- poi_number = poi_number + 1
1102
- tkpt = Trackpoint.new(
1103
- poi_number, line_obj.tokens[1], line_obj.tokens[2],
1104
- '0', '', line_obj.concatinate_tokens(3))
1105
- add_poi(tkpt)
1106
- end
1107
- end
1108
- }
1109
- end
1110
-
1111
- def parse_tracks
1112
- in_track, trk_number, tkpt_number = false, 0, 0
1113
- curr_trk, curr_run = nil, nil
1114
- @lines.each { |line|
1115
- line_obj = Line.new(line, nil, true)
1116
- tok_count = line_obj.token_count
1117
- is_point = line_obj.token_idx_equals(0, '.')
1118
-
1119
- if line_obj.is_populated_non_comment
1120
- if line_obj.match('track_start')
1121
- in_track = true
1122
- trk_number = trk_number + 1
1123
- tkpt_number = 0
1124
- curr_trk = Track.new(0, line_obj.concatinate_tokens(1))
1125
- curr_run = Run.new(trk_number, line_obj.concatinate_tokens(1))
1126
- curr_run.add_track(curr_trk)
1127
- elsif line_obj.match('track_end')
1128
- in_track = false
1129
- curr_run.finish
1130
- add_track(curr_trk)
1131
- add_route(curr_run)
1132
- elsif in_track && tok_count > 2 && is_point
1133
- tkpt_number = tkpt_number + 1
1134
- tkpt = Trackpoint.new(
1135
- tkpt_number, line_obj.tokens[1], line_obj.tokens[2],
1136
- '0', '', line_obj.concatinate_tokens(3))
1137
- curr_trk.add_trackpoint(tkpt)
1138
- end
1139
- end
1140
- }
1141
- end
1142
-
1143
- def parse_routes
1144
- in_route, route_number, trk_number, tkpt_number = false, 0, 0, 0
1145
- curr_trk, curr_run = nil, nil
1146
- @lines.each { |line|
1147
- line_obj = Line.new(line, nil, true)
1148
- tok_count = line_obj.token_count
1149
- is_point = line_obj.token_idx_equals(0, '.')
1150
-
1151
- if line_obj.is_populated_non_comment
1152
- if line_obj.match('route_start')
1153
- in_route = true
1154
- trk_number = trk_number + 1
1155
- tkpt_number = 0
1156
- curr_trk = Track.new(0, line_obj.concatinate_tokens(1))
1157
- curr_run = Run.new(trk_number, line_obj.concatinate_tokens(1))
1158
- curr_run.add_track(curr_trk)
1159
- elsif line_obj.match('route_end')
1160
- in_route = false
1161
- curr_run.finish
1162
- add_route(curr_run)
1163
- elsif in_route && tok_count > 2 && is_point
1164
- tkpt_number = tkpt_number + 1
1165
- tkpt = Trackpoint.new(
1166
- tkpt_number, line_obj.tokens[1], line_obj.tokens[2],
1167
- '0', '', line_obj.concatinate_tokens(3))
1168
- curr_trk.add_trackpoint(tkpt)
1169
- elsif in_route && line_obj.token_idx_equals(0, 'track') && tok_count > 1
1170
- trk_desc = line_obj.concatinate_tokens(1)
1171
- trk = @track_hash[trk_desc]
1172
- if trk
1173
- trk.trackpoints.each { |tkpt| curr_trk.add_trackpoint(tkpt) }
1174
- end
1175
- elsif in_route && line_obj.token_idx_equals(0, 'track_rev') && tok_count > 1
1176
- trk_desc = line_obj.concatinate_tokens(1)
1177
- trk = @track_hash[trk_desc]
1178
- if trk
1179
- array = trk.trackpoints
1180
- trk.trackpoints.each { |tkpt| curr_trk.add_trackpoint(tkpt) }
1181
- end
1182
- end
1183
- end
1184
- }
1185
- end
1186
-
1187
- def add_poi(tkpt)
1188
- if tkpt
1189
- descr = tkpt.descr
1190
- if @poi_hash.has_key? descr
1191
- puts "Duplicate POI key ignored - '#{descr}'"
1192
- else
1193
- #puts "Adding POI: #{tkpt.to_poi_csv}"
1194
- @poi_hash[descr] = tkpt
1195
- @poi_array << tkpt
1196
- end
1197
- end
1198
- end
1199
-
1200
- def add_track(trk)
1201
- if trk
1202
- descr = trk.descr
1203
- if @track_hash.has_key? descr
1204
- puts "Duplicate Track key ignored - '#{descr}'"
1205
- else
1206
- @track_hash[descr] = trk
1207
- @track_array << trk
1208
- end
1209
- end
1210
- end
1211
-
1212
- def add_route(run)
1213
- if run
1214
- descr = run.descr
1215
- if @route_hash.has_key? descr
1216
- puts "Duplicate Route key ignored - '#{descr}'"
1217
- else
1218
- @route_hash[descr] = run
1219
- @route_array << run
1220
- end
1221
- end
1222
- end
1223
-
1224
- public
1225
-
1226
- def to_s
1227
- return "GeoData lines: #{lines.size} poi: #{@poi_hash.size} tracks: #{@track_hash.size} routes: #{@route_hash.size} "
1228
- end
1229
-
1230
- def dump
1231
- puts "#{self.class} dump:"
1232
- @poi_array.each { |tkpt| puts "POI: #{tkpt.to_geo_s}" }
1233
- @track_array.each { |trk| trk.dump }
1234
- @route_hash.keys.sort.each { |key|
1235
- puts "Route: '#{key}'"
1236
- }
1237
- end
1238
- end
1239
-
1240
- # =============================================================================
1241
-
1242
- =begin rdoc
1243
- Instances of this class represent a <Run> aggregate object from a
1244
- Forerunner XML file.
1245
-
1246
- Additionally, there is distance, pace, and Google Map generation logic
1247
- in this class.
1248
- =end
1249
-
1250
- class GoogleMapGenerator < GoobyObject
1251
-
1252
- attr_reader :csv_file, :csv_lines, :dttm_idx, :num_idx, :lat_idx, :lng_idx, :alt_idx, :cdist_idx
1253
- attr_reader :run, :tkpts, :content_hash, :center_longitude, :center_latitude, :gpoint_array, :overlay_points, :notes
1254
- attr_reader :center_longitude, :center_latitude
1255
-
1256
- # The default csv input file format is as follows:
1257
- # 1 | 2006-01-15T18:31:10Z | 1279 | 33.42601 | -111.92927 | 347.654 | 26.3514930151813
1258
- # 1 | 2004-11-13T13:05:20Z | 2 | 37.54318 | -77.43636 | -58.022 | 0.00297286231747969
1259
-
1260
- # primary_key|run_id|date|time|tkpt_num|latitude|longitude|altitude_ft|run_distance|run_elapsed|lap_tkpt_number|lap_distance|lap_elapsed
1261
- # 2005-03-05T13:00:29Z.2|2005-03-05T13:00:29Z|2005-03-05|13:00:49|2|35.22054|-80.84506|738.4161312|0.046918021941152|00:00:20|2|0.046918021941152|00:00:20
1262
-
1263
- def initialize(csv_file, dttm_idx=1, num_idx=4, lat_idx=5, lng_idx=6, alt_idx=7, cdist_idx=8)
1264
- @csv_file = csv_file
1265
- @dttm_idx = dttm_idx
1266
- @num_idx = num_idx
1267
- @lat_idx = lat_idx
1268
- @lng_idx = lng_idx
1269
- @alt_idx = alt_idx
1270
- @cdist_idx = cdist_idx
1271
-
1272
- # Override default csv value indices if specified in the configuration yaml file.
1273
- @configuration = Gooby::Configuration.get_config
1274
- @dttm_idx = @configuration.get('csv_dttm_idx') if @configuration.get('csv_dttm_idx')
1275
- @num_idx = @configuration.get('csv_num_idx') if @configuration.get('csv_num_idx')
1276
- @lat_idx = @configuration.get('csv_lat_idx') if @configuration.get('csv_lat_idx')
1277
- @lng_idx = @configuration.get('csv_lng_idx') if @configuration.get('csv_lng_idx')
1278
- @alt_idx = @configuration.get('csv_alt_idx') if @configuration.get('csv_alt_idx')
1279
- @title = @configuration.get("#{@csv_file}")
1280
-
1281
- @content_hash = Hash.new('')
1282
- @run = Gooby::Run.new(1)
1283
- @track = Gooby::Track.new(1)
1284
- @run.add_track(@track)
1285
- @tkpts = Array.new
1286
- @icon_url_base = @configuration.get('gmap_icon_url_base')
1287
-
1288
- list = Array.new
1289
- list << @csv_file
1290
- @cvs_reader = Gooby::CsvReader.new(list)
1291
- @cvs_points = @cvs_reader.read
1292
- @cvs_points.each { |cvs_point|
1293
- tkpt = cvs_point.as_trackpoint
1294
- if tkpt
1295
- @track.add_trackpoint(tkpt)
1296
- end
1297
- }
1298
- @run.finish
1299
- end
1300
-
1301
- =begin
1302
- Returns a Hash with specific generated content at the following keys:
1303
- =end
1304
- def generate(configuration)
1305
- if (configuration == nil)
1306
- @configuration = Gooby::Configuration.get_config
1307
- else
1308
- @configuration = configuration
1309
- end
1310
- @content_hash['when_generated'] = Time.now
1311
- @content_hash['title'] = @title
1312
- @icon_url_base = @configuration.get('gmap_icon_url_base')
1313
- filter_trackpoints
1314
- compute_center_point
1315
- generate_key_js
1316
- generate_map_div
1317
- generate_messages_div
1318
- generate_main_js_start
1319
- generate_main_js_route_overlay
1320
- generate_main_js_checkpoint_overlays
1321
- generate_main_js_map_clicked_listeners
1322
- generate_main_js_end
1323
- @content_hash
1324
- end
1325
-
1326
- def filter_trackpoints
1327
- count, @tkpts = 0, Array.new
1328
- firstTkpt = @configuration.get('gmap_first_tkpt_number')
1329
- lastTkpt = @configuration.get('gmap_last_tkpt_number')
1330
- @run.tracks.each { |trk|
1331
- trk.trackpoints.each { |tkpt|
1332
- count = count + 1
1333
- if ((count >= firstTkpt) && (count <= lastTkpt))
1334
- @tkpts.push(tkpt)
1335
- end
1336
- }
1337
- }
1338
- end
1339
-
1340
- =begin
1341
- Returns a Hash with specific generated content at the following keys:
1342
- =end
1343
- def generate_page(configuration)
1344
-
1345
- # puts "generate_page #{@csv_file} #{@csv_lines.size}"
1346
- content_hash = generate(nil)
1347
- s = String.new(@csv_file)
1348
- s.gsub("/", " ")
1349
- tokens = tokenize(s, nil)
1350
- out_file = "#{tokens[-2]}.html"
1351
- #content_hash.keys.sort.each { | key | puts key }
1352
-
1353
- s = <<HERE
1354
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
1355
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1356
- <html xmlns="http://www.w3.org/1999/xhtml">
1357
- <head>
1358
- <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
1359
- <meta name="description" content="Google Map generated with #{project_embedded_comment}">
1360
- <meta name="keywords" content="Google Map #{project_embedded_comment} GPS">
1361
- <title> Google Map by Gooby </title>
1362
- #{content_hash['key_js']}
1363
- #{content_hash['main_js_start']}
1364
- #{content_hash['main_js_route_overlay']}
1365
- #{content_hash['main_js_checkpoint_overlays']}
1366
- #{content_hash['main_js_map_clicked_listeners']}
1367
- #{content_hash['main_js_end']}
1368
- </head>
1369
- <body onload="load()" onunload="GUnload()">
1370
- <center>
1371
- <h3> #{@title} </h3>
1372
- <h5> Generated by Gooby #{content_hash['when_generated']} <br> Gooby = Google APIs + Ruby </h5>
1373
- #{content_hash['map_div']}
1374
- #{content_hash['messages_div']}
1375
- </center>
1376
- </body>
1377
- </html>
1378
- HERE
1379
- puts s # Redirect output via shell.
1380
- end
1381
-
1382
- private
1383
-
1384
- def compute_center_point
1385
- highLat = -999.0
1386
- highLong = -999.0
1387
- lowLat = 999.0
1388
- lowLong = 999.0
1389
- @tkpts.each { |tkpt|
1390
- highLat = tkpt.latitude_as_float if tkpt.latitude_as_float > highLat
1391
- lowLat = tkpt.latitude_as_float if tkpt.latitude_as_float < lowLat
1392
- highLong = tkpt.longitude_as_float if tkpt.longitude_as_float > highLong
1393
- lowLong = tkpt.longitude_as_float if tkpt.longitude_as_float < lowLong
1394
- }
1395
- @center_longitude = (highLong + lowLong) / 2
1396
- @center_latitude = (highLat + lowLat) / 2
1397
- @content_hash['center_longitude'] = @center_longitude
1398
- @content_hash['center_latitude'] = @center_latitude
1399
- end
1400
-
1401
- def generate_key_js
1402
- key = @configuration.get('gmap_key')
1403
- key.strip!
1404
- # <script src="http://maps.google.com/maps?file=api&v=2&key=<%= @gmap_key -%>" type="text/javascript"></script>
1405
- s = "<script src='http://maps.google.com/maps?file=api&v=2&key="
1406
- s << key
1407
- s << "' type='text/javascript'></script>"
1408
- @content_hash['key_js'] = s
1409
- end
1410
-
1411
- def generate_map_div
1412
- width = @configuration.get('gmap_width')
1413
- height = @configuration.get('gmap_height')
1414
- id = @configuration.get('gmap_map_element_id')
1415
- s = '<div id="'
1416
- s << id
1417
- s << '" style="width: '
1418
- s << width
1419
- s << '; height: '
1420
- s << height
1421
- s << '"></div>'
1422
- @content_hash['map_width'] = width
1423
- @content_hash['map_height'] = height
1424
- @content_hash['map_div'] = s
1425
- end
1426
-
1427
- def generate_messages_div
1428
- s = "<div id=\"messages\"></div>"
1429
- @content_hash['messages_div'] = s
1430
- end
1431
-
1432
- def generate_main_js_start
1433
- id = @configuration.get('gmap_map_element_id')
1434
- size = @configuration.get('gmap_size_control')
1435
- type = @configuration.get('gmap_type')
1436
- zoom = @configuration.get('gmap_zoom_level')
1437
- title = @configuration.get("#{@csv_file}")
1438
- title = '' if title == nil
1439
- zoom_tab = @configuration.get('gmap_zoom_tab')
1440
- if size
1441
- if size == 'smallmap'
1442
- size = 'GSmallMapControl'
1443
- elsif size == 'smallzoom'
1444
- size = 'GSmallMapControl'
1445
- else
1446
- size = 'GLargeMapControl'
1447
- end
1448
- end
1449
-
1450
- if type
1451
- if type == 'satellite'
1452
- type = 'G_SATELLITE_MAP'
1453
- elsif type == 'hybrid'
1454
- type = 'G_HYBRID_MAP'
1455
- else
1456
- type = 'G_NORMAL_MAP'
1457
- end
1458
- else
1459
- type = 'G_NORMAL_MAP'
1460
- end
1461
-
1462
- s = '<script type="text/javascript">'
1463
- s << "\n"
1464
- s << "//<![CDATA[ \n"
1465
- s << " function load() { \n"
1466
- s << " if (GBrowserIsCompatible()) { \n"
1467
- s << ' var map = new GMap2(document.getElementById("'
1468
- s << id
1469
- s << '")); '
1470
- s << "\n"
1471
-
1472
- if size
1473
- s << ' map.addControl(new '
1474
- s << size
1475
- s << '());'
1476
- s << "\n"
1477
- end
1478
-
1479
- if type
1480
- s << ' map.addControl(new GMapTypeControl());'
1481
- s << "\n"
1482
- # s << ' map.setMapType('
1483
- # s << type
1484
- # s << ');'
1485
- s << "\n"
1486
- end
1487
- s << " var centerPoint = new GLatLng(#{@center_latitude}, #{@center_longitude}); // #{project_embedded_comment} \n"
1488
- s << " map.setCenter(centerPoint, #{zoom}); \n"
1489
- s << "\n"
1490
- @content_hash['main_js_start'] = s
1491
- @content_hash['title'] = title
1492
- end
1493
-
1494
- def generate_main_js_route_overlay
1495
- tkpt_count = @tkpts.size.to_f
1496
- app_max = @configuration.get('gmap_approx_max_points').to_f
1497
- gen_comments = @configuration.get('gmap_gen_comments')
1498
- ratio = tkpt_count / app_max
1499
- @start_dttm = nil
1500
- if ratio > 1.0
1501
- increment = (tkpt_count / app_max).to_i
1502
- else
1503
- increment = 1
1504
- end
1505
- curr_idx, next_idx, gpoint_count, last_idx = -1, 0, 0, @tkpts.size - 1
1506
- s = " var points = new Array(); "
1507
- @tkpts.each { |tkpt|
1508
- curr_idx = curr_idx + 1
1509
- if curr_idx == 0
1510
- @start_dttm = tkpt.dttm
1511
- @start_pos = tkpt.point
1512
-
1513
- time = Time.parse(@start_dttm.dateTime().to_s)
1514
- end
1515
- if ((curr_idx == next_idx) || (curr_idx == last_idx) || (tkpt.is_split()))
1516
- gpoint_count = gpoint_count + 1
1517
- s << tkpt.as_glatlng(false, gen_comments, tkpt_count, curr_idx, @start_dttm)
1518
- next_idx = curr_idx + increment
1519
- else
1520
- s << tkpt.as_glatlng(true, gen_comments, tkpt_count, curr_idx, @start_dttm)
1521
- end
1522
- }
1523
- s << "\n"
1524
- s << "\n var routePolyline = new GPolyline(points); "
1525
- s << "\n map.addOverlay(routePolyline); "
1526
- @content_hash['main_js_route_overlay'] = s
1527
- @content_hash['main_js_route_overlay_increment'] = increment
1528
- end
1529
-
1530
- def generate_main_js_checkpoint_overlays
1531
- s = "\n // Create a base icon for all of our markers that specifies the "
1532
- s << "\n // shadow, icon dimensions, etc."
1533
- s << "\n var baseIcon = new GIcon();"
1534
- s << "\n baseIcon.shadow = \"#{@icon_url_base}shadow50.png\";"
1535
- s << "\n baseIcon.iconSize = new GSize(20, 34);"
1536
- s << "\n baseIcon.shadowSize = new GSize(37, 34);"
1537
- s << "\n baseIcon.iconAnchor = new GPoint(9, 34);"
1538
- s << "\n baseIcon.infoWindowAnchor = new GPoint(9, 2);"
1539
- s << "\n baseIcon.infoShadowAnchor = new GPoint(18, 25);"
1540
- s << "\n"
1541
-
1542
- curr_idx = -1
1543
- last_idx = @tkpts.size - 1
1544
- next_checkpoint = 0.0
1545
- @start_dttm = nil
1546
- @tkpts.each { | tkpt |
1547
- curr_idx = curr_idx + 1
1548
- if curr_idx == 0
1549
- @start_dttm = tkpt.dttm
1550
- info_window_html = tkpt.as_info_window_html('Start', @start_dttm)
1551
- s << "\n var iconStart = new GIcon(baseIcon); "
1552
- s << "\n iconStart.image = \"#{@icon_url_base}dd-start.png\";"
1553
- s << "\n var pStart = new GPoint(#{tkpt.longitude_as_float}, #{tkpt.latitude_as_float});"
1554
- s << "\n var mStart = new GMarker(pStart, iconStart);"
1555
- s << "\n GEvent.addListener(mStart, \"click\", function() { "
1556
- s << "\n mStart.openInfoWindowHtml(#{info_window_html});"
1557
- s << "\n }); "
1558
- s << "\n map.addOverlay(mStart);"
1559
- s << "\n "
1560
- next_checkpoint = 1.0
1561
- elsif curr_idx == last_idx
1562
- info_window_html = tkpt.as_info_window_html('Finish', @start_dttm)
1563
- s << "\n var iconFinish = new GIcon(baseIcon); "
1564
- s << "\n iconFinish.image = \"#{@icon_url_base}dd-end.png\";"
1565
- s << "\n var pFinish = new GPoint(#{tkpt.longitude_as_float}, #{tkpt.latitude_as_float});"
1566
- s << "\n var mFinish = new GMarker(pFinish, iconFinish);"
1567
- s << "\n GEvent.addListener(mFinish, \"click\", function() { "
1568
- s << "\n mFinish.openInfoWindowHtml(#{info_window_html});"
1569
- s << "\n }); "
1570
- s << "\n map.addOverlay(mFinish);"
1571
- s << "\n "
1572
- next_checkpoint = 999999
1573
- else
1574
- if (tkpt.cumulative_distance >= next_checkpoint)
1575
- integer = next_checkpoint.to_i
1576
- info_window_html = tkpt.as_info_window_html("#{integer}", @start_dttm)
1577
- s << "\n var icon#{integer} = new GIcon(baseIcon); "
1578
- s << "\n icon#{integer}.image = \"#{@icon_url_base}marker#{integer}.png\";"
1579
- s << "\n var p#{integer} = new GPoint(#{tkpt.longitude_as_float}, #{tkpt.latitude_as_float});"
1580
- s << "\n var m#{integer} = new GMarker(p#{integer}, icon#{integer});"
1581
- s << "\n GEvent.addListener(m#{integer}, \"click\", function() { "
1582
- s << "\n m#{integer}.openInfoWindowHtml(#{info_window_html});"
1583
- s << "\n }); "
1584
- s << "\n map.addOverlay(m#{integer});"
1585
- s << "\n "
1586
- next_checkpoint = next_checkpoint + 1.0
1587
- end
1588
- end
1589
- }
1590
- s << "\n"
1591
- @content_hash['main_js_checkpoint_overlays'] = s
1592
-
1593
- end
1594
-
1595
- def generate_main_js_map_clicked_listeners
1596
- s = "\n"
1597
- s << "\n GEvent.addListener(map, \"click\", function() { "
1598
- s << "\n var center = map.getCenter(); \n"
1599
- s << "\n document.getElementById(\"messages\").innerHTML = 'click: ' + center.toString(); "
1600
- s << "\n });"
1601
- s << "\n GEvent.addListener(map, \"moveend\", function() { "
1602
- s << "\n var center = map.getCenter(); \n"
1603
- s << "\n document.getElementById(\"messages\").innerHTML = 'moveend: ' + center.toString(); "
1604
- s << "\n });"
1605
- @content_hash['main_js_map_clicked_listeners'] = s
1606
- end
1607
-
1608
- def generate_main_js_end
1609
- s = "\n } "
1610
- s << "\n } "
1611
- s << "\n//]]> \n"
1612
- s << "\n</script>"
1613
-
1614
- @content_hash['main_js_end'] = s
1615
- end
1616
- end
1617
-
1618
- # =============================================================================
1619
-
1620
- =begin rdoc
1621
- Instances of this class represent a <History> aggregate object from a
1622
- Forerunner XML file.
1623
- =end
1624
-
1625
- class History < GoobyObject
1626
-
1627
- attr_reader :runs
1628
-
1629
- def initialize
1630
- @runs = Array.new
1631
- end
1632
-
1633
- # Adds a Run during XML parsing.
1634
- def add_run(run)
1635
- @runs.push(run)
1636
- end
1637
-
1638
- def to_s
1639
- return "Hist: runs: #{@runs.size}"
1640
- end
1641
-
1642
- def print_string
1643
- s = "History: run count=#{@runs.size} \n"
1644
- runs.each { | run | s << run.print_string }
1645
- s
1646
- end
1647
- end
1648
-
1649
- # =============================================================================
1650
-
1651
- =begin rdoc
1652
- Instances of this class represent a <Lap> aggregate object from a
1653
- Forerunner XML file.
1654
- =end
1655
-
1656
- class Lap < GoobyObject
1657
-
1658
- attr_accessor :number, :startTime, :duration, :length, :begin_position, :end_position
1659
-
1660
- def initialize(num)
1661
- @number = num
1662
- end
1663
-
1664
- def to_s
1665
- return "Lap: num: #{@number} start: #{@startTime} dur: #{@duration} len: #{@length} begin: #{@begin_position.to_s} end: #{@end_position.to_s}"
1666
- end
1667
- end
1668
-
1669
- # =============================================================================
1670
-
1671
- class Line < GoobyObject
1672
-
1673
- attr_accessor :raw_data, :tokens
1674
-
1675
- def initialize(raw='', delim=nil, strip=false)
1676
- if strip
1677
- @raw_data = raw.strip
1678
- else
1679
- @raw_data = raw
1680
- end
1681
-
1682
- @tokens = tokenize(@raw_data, delim, strip=false)
1683
- end
1684
-
1685
- public
1686
-
1687
- def token(idx)
1688
- @tokens[idx]
1689
- end
1690
-
1691
- def token_count
1692
- @tokens.size
1693
- end
1694
-
1695
- def token_idx_equals(idx, value)
1696
- if idx < token_count
1697
- if @tokens[idx] == value
1698
- return true
1699
- end
1700
- end
1701
- false
1702
- end
1703
-
1704
- def match(pattern)
1705
- @raw_data.match(pattern)
1706
- end
1707
-
1708
- def is_comment
1709
- s = @raw_data.strip
1710
- (s.match('^#')) ? true : false
1711
- end
1712
-
1713
- def is_populated_non_comment
1714
- s = @raw_data.strip
1715
- if s.size == 0
1716
- return false
1717
- end
1718
- if is_comment
1719
- return false
1720
- end
1721
- return true
1722
- end
1723
-
1724
- def concatinate_tokens(start_idx = 0)
1725
- s = ''
1726
- idx = -1
1727
- @tokens.each { |tok|
1728
- idx = idx + 1
1729
- if idx >= start_idx
1730
- s << tok
1731
- s << ' '
1732
- end
1733
- }
1734
- s.strip!
1735
- s
1736
- end
1737
- end
1738
-
1739
- # =============================================================================
1740
- =begin
1741
-
1742
- This is a singleton class whose values are loaded from a YAML file when your
1743
- GoobyCommand class is created. The default filename is 'gooby_config.yaml"'.
1744
-
1745
- The YAML file contains configuration parameters, such as your Google Map key,
1746
- map HTML options, points of interest, and courses.
1747
-
1748
- =end
1749
- class Configuration < GoobyObject
1750
-
1751
- @@singleton_instance = nil
1752
-
1753
- attr_reader :yaml_filename, :configuration
1754
-
1755
- private_class_method :new
1756
-
1757
- def Configuration.init(yaml_filename='gooby_config.yaml')
1758
- return @@singleton_instance if @@singleton_instance
1759
- @@singleton_instance = new(yaml_filename)
1760
- end
1761
-
1762
- def self.get_config
1763
- @@singleton_instance
1764
- end
1765
-
1766
- def initialize(yaml_filename)
1767
- @yaml_filename = yaml_filename
1768
- File.open("#{@yaml_filename}") { |fn| @configuration = YAML::load(fn) }
1769
- end
1770
-
1771
- def get(name)
1772
- if name == nil
1773
- return ''
1774
- end
1775
- s = @configuration["#{name}"]
1776
-
1777
- # Provide "sensible defaults".
1778
- if s == nil
1779
- if (name == '')
1780
- return ''
1781
- elsif (name == 'gmap_first_tkpt_number')
1782
- return 1
1783
- elsif (name == 'gmap_last_tkpt_number')
1784
- return 5000
1785
- elsif (name == 'gmap_map_element_id')
1786
- return 'map'
1787
- elsif (name == 'gmap_height')
1788
- return '600'
1789
- elsif (name == 'gmap_icon_url_base')
1790
- return 'http://www.your-web-site.com/gicons/'
1791
- elsif (name == 'gmap_key')
1792
- return 'enter your Google Map Key here'
1793
- elsif (name == 'gmap_type_control')
1794
- return true
1795
- elsif (name == 'gmap_approx_max_points')
1796
- return '200'
1797
- elsif (name == 'gmap_gen_comments')
1798
- return true
1799
- elsif (name == 'gmap_size_control')
1800
- return nil
1801
- elsif (name == 'gmap_type')
1802
- return 'G_NORMAL_MAP'
1803
- elsif (name == 'gmap_zoom_level')
1804
- return 5
1805
- else
1806
- return ''
1807
- end
1808
- end
1809
- s
1810
- end
1811
-
1812
- def print_all
1813
- @configuration.keys.sort.each { |key| puts "#{key}: #{@configuration["#{key}"]}" }
1814
- end
1815
-
1816
- def print_all_poi
1817
- @configuration.keys.sort.each { |key|
1818
- if (key.match(/poi[\.]/))
1819
- val = @configuration["#{key}"]
1820
- poi = Point.new(val)
1821
- puts poi.to_s
1822
- end
1823
- }
1824
- end
1825
-
1826
- def get_poi(number)
1827
- val = @configuration["poi.#{number}"]
1828
- (val) ? Point.new(val) : nil
1829
- end
1830
-
1831
- def get_course(number)
1832
- val = @configuration["course.#{number}"]
1833
- (val) ? Course.new(val) : nil
1834
- end
1835
-
1836
- def size
1837
- @configuration.size
1838
- end
1839
-
1840
- # Return a String containing yaml filename and entry count.
1841
- def to_s
1842
- return "# Configuration: filename: #{@yaml_filename} entries: #{@configuration.size}"
1843
- end
1844
- end
1845
-
1846
- # =============================================================================
1847
-
1848
- class Point < GoobyObject
1849
-
1850
- attr_accessor :number, :latitude, :longitude, :altitude, :note
1851
-
1852
- def initialize(*args)
1853
- @number, @latitude, @longitude, @altitude, @note = '', '', '', '', ''
1854
- if args
1855
- if args.size == 1
1856
- initialize_from_string(args[0]) # yaml
1857
- else
1858
- initialize_from_array(args)
1859
- end
1860
- end
1861
- end
1862
-
1863
- def csv_delim
1864
- '|'
1865
- end
1866
-
1867
- def initialize_from_array(args)
1868
- @latitude = args[0] if args.size > 0
1869
- @longitude = args[1] if args.size > 1
1870
- @altitude = args[2] if args.size > 2
1871
- @note = args[3] if args.size > 3
1872
- end
1873
-
1874
- def initialize_from_string(yaml_value_string)
1875
- tokens = yaml_value_string.split
1876
- if (tokens.size > 2)
1877
- @latitude = tokens[0]
1878
- @longitude = tokens[1]
1879
- @note = ''
1880
- count = 0
1881
- tokens.each { |tok|
1882
- count = count + 1
1883
- if (count > 2)
1884
- @note << tok
1885
- @note << ' '
1886
- end
1887
- }
1888
- end
1889
- end
1890
-
1891
- public
1892
-
1893
- def to_s
1894
- return "lat: #{@latitude} lng: #{@longitude} alt: #{@altitude} note: #{@note}"
1895
- end
1896
-
1897
- def to_formatted_string
1898
- s = "lat: #{@latitude.to_s.ljust(20)}"
1899
- s << " lng: #{@longitude.to_s.ljust(20)}"
1900
- s << " poi.#{@number.to_s.ljust(12)}" if @number
1901
- s << " #{@note}" if @note
1902
- s
1903
- end
1904
-
1905
- def to_csv
1906
- return "#{@latitude}#{csv_delim}#{@longitude}#{csv_delim}#{@altitude}"
1907
- end
1908
-
1909
- def latitude_as_float
1910
- @latitude ? @latitude.to_f : invalid_latitude
1911
- end
1912
-
1913
- def longitude_as_float
1914
- @longitude ? @longitude.to_f : invalid_longitude
1915
- end
1916
-
1917
- def altitude_as_float
1918
- @altitude ? @altitude.to_f : invalid_altitude
1919
- end
1920
-
1921
- def degrees_diff(another_point)
1922
- if (another_point)
1923
- puts "this: #{to_s}" if false
1924
- puts "other: #{another_point.to_s}" if false
1925
- puts "lats: #{latitude_as_float} #{another_point.latitude_as_float}" if false
1926
- puts "lngs: #{longitude_as_float} #{another_point.longitude_as_float}" if false
1927
- lat_diff = latitude_as_float - another_point.latitude_as_float
1928
- lng_diff = longitude_as_float - another_point.longitude_as_float
1929
- diff = lat_diff.abs + lng_diff.abs
1930
- puts "diff: #{diff} #{lat_diff} #{lng_diff}" if false
1931
- diff
1932
- else
1933
- 360
1934
- end
1935
- end
1936
-
1937
- def proximity(another_point, units)
1938
- if (another_point)
1939
- arg1 = latitude_as_float
1940
- arg2 = another_point.latitude_as_float
1941
- arg3 = latitude_as_float
1942
- arg4 = another_point.latitude_as_float
1943
- theta = longitude_as_float - another_point.longitude_as_float
1944
- res1 = Math.sin(deg2rad(arg1))
1945
- res2 = Math.sin(deg2rad(arg2))
1946
- res3 = Math.cos(deg2rad(arg3))
1947
- res4 = Math.cos(deg2rad(arg4))
1948
- res5 = Math.cos(deg2rad(theta.to_f))
1949
- dist = ((res1 * res2) + (res3 * res4 * res5)).to_f
1950
-
1951
- if (!dist.nan?)
1952
- dist = Math.acos(dist.to_f)
1953
- if (!dist.nan?)
1954
- dist = rad2deg(dist)
1955
- if (!dist.nan?)
1956
- dist = dist * 60 * 1.1515;
1957
- if (!dist.nan?)
1958
- if units == "K"
1959
- dist = dist * 1.609344;
1960
- end
1961
- if units == "N"
1962
- dist = dist * 0.8684;
1963
- end
1964
- end
1965
- end
1966
- end
1967
- return dist.to_f
1968
- else
1969
- return 0
1970
- end
1971
- else
1972
- return 0
1973
- end
1974
- end
1975
-
1976
- def deg2rad(degrees)
1977
- (((0 + degrees) * Math::PI) / 180)
1978
- end
1979
-
1980
- def rad2deg(radians)
1981
- (((0 + radians) * 180) / Math::PI)
1982
- end
1983
- end
1984
-
1985
- # =============================================================================
1986
-
1987
- class CsvPoint < Point
1988
-
1989
- attr_reader :rawdata, :tokens
1990
- attr_reader :id, :run_id, :date, :time, :tkpt_num, :distance, :elapsed
1991
- attr_reader :lap_number, :lap_distance, :lap_elapsed
1992
- attr_accessor :course_distance, :course_elapsed, :degrees_diff
1993
-
1994
- def initialize(csv_line)
1995
- @rawdata = "#{csv_line}"
1996
- @tokens = @rawdata.split(csv_delim)
1997
- if (tokens.size > 12)
1998
- @id = tokens[0] # <-- consists of @run_id.@tkpt_num for uniqueness and use as a DB table primary key.
1999
- @run_id = tokens[1]
2000
- @date = tokens[2]
2001
- @time = tokens[3]
2002
- @tkpt_num = tokens[4].strip.to_i
2003
- @latitude = tokens[5].to_f
2004
- @longitude = tokens[6].to_f
2005
- @altitude = tokens[7].strip.to_f
2006
- @distance = tokens[8].strip.to_f
2007
- @elapsed = tokens[9]
2008
- @lap_number = tokens[10].strip.to_i
2009
- @lap_distance = tokens[11].strip.to_f
2010
- @lap_elapsed = tokens[12]
2011
- end
2012
- end
2013
-
2014
- def as_trackpoint
2015
- tkpt = Trackpoint.new(@tkpt_num, @latitude, @longitude, @altitude, "#{date}T#{time}Z")
2016
- tkpt.lap_number = @lap_number
2017
- tkpt.lap_distance = @lap_distance
2018
- tkpt.lap_elapsed = @lap_elapsed
2019
- tkpt
2020
- end
2021
-
2022
- def to_s
2023
- return "lat: #{@latitude} lng: #{@longitude} alt: #{@altitude} note: #{@note}"
2024
- end
2025
-
2026
- def to_formatted_string
2027
- s = "lat: #{@latitude.to_s.ljust(20)}"
2028
- s << " lng: #{@longitude.to_s.ljust(20)}"
2029
- s << " time: #{@time}"
2030
- pad = ''.ljust(70)
2031
- s << "\n#{pad} course elapsed: #{@course_elapsed}"
2032
- s << "\n#{pad} distance: #{@distance}"
2033
- s << "\n#{pad} course distance: #{@course_distance}"
2034
- s << "\n#{pad} altitude: #{@altitude}"
2035
- s << "\n#{pad} degrees diff: #{@degrees_diff}"
2036
- s
2037
- end
2038
- end
2039
-
2040
- # =============================================================================
2041
-
2042
- class CvsRun < GoobyObject
2043
-
2044
- attr_reader :id, :points
2045
-
2046
- def initialize(id)
2047
- @id = "#{id}"
2048
- @points = Array.new
2049
- end
2050
-
2051
- def add_point(point)
2052
- if point
2053
- @points << point
2054
- end
2055
- end
2056
- end
2057
-
2058
- # =============================================================================
2059
-
2060
- class CsvReader < GoobyObject
2061
-
2062
- attr_reader :files, :cvs_points
2063
-
2064
- def initialize(array_of_filenames=nil)
2065
- @files = Array.new
2066
- @cvs_points = Array.new
2067
- if array_of_filenames
2068
- array_of_filenames.each { |filename| add_file(filename) }
2069
- end
2070
- end
2071
-
2072
- def add_file(filename)
2073
- if (filename)
2074
- if (File.exist?(filename))
2075
- @files << filename
2076
- end
2077
- end
2078
- end
2079
-
2080
- def read
2081
- @files.each { |filename|
2082
- lines = read_lines(filename, true)
2083
- lines.each { |line|
2084
- if (line.match('^#'))
2085
- if (line.match('^#cols: '))
2086
- col_names_header = line[7, line.size]
2087
- @col_names = col_names_header.split('|')
2088
- end
2089
- else
2090
- if (line.size > 50)
2091
- @cvs_points << Gooby::CsvPoint.new(line)
2092
- end
2093
- end
2094
- }
2095
- }
2096
- @cvs_points
2097
- end
2098
-
2099
- def display_formatted_record(record_index=2)
2100
- tokens = @cvs_points[record_index].rawdata.split('|')
2101
- puts "\nCsvReader.display_formatted_record hdr_cols=#{@col_names.size} data_cols=#{tokens.size}"
2102
- size = 0
2103
- @col_names.each { |col_name|
2104
- size = size + 1
2105
- if size <= tokens.size
2106
- value = tokens[size - 1]
2107
- puts "#{col_name.strip.ljust(20)} #{(size - 1).to_s.ljust(3)} #{value}"
2108
- end
2109
- }
2110
- end
2111
-
2112
- def to_s
2113
- s = "CsvReader - file count: #{files.size} total points: #{cvs_points.size}"
2114
- @files.each { |file| s << "\n file: #{file} "}
2115
- s
2116
- end
2117
- end
2118
-
2119
- # =============================================================================
2120
-
2121
- class Trackpoint < Point
2122
-
2123
- attr_accessor :first, :last, :number, :run_number, :dttm, :prev_tkpt, :lap_number, :lap_seq, :lap_distance, :lap_elapsed
2124
- attr_accessor :cumulative_distance, :cumulative_pace, :incremental_distance, :split, :prev_split
2125
- attr_accessor :first, :last, :run_id, :run_number
2126
- attr_accessor :run_start_dttm
2127
-
2128
- def initialize(num, lat, lng, alt, time_string, auxInfoHash=Hash.new(''))
2129
- @number = num
2130
- @run_number = 0
2131
- @auxInfoHash = auxInfoHash
2132
- @lap_seq = 1
2133
- @lap_distance = 0
2134
- lap_num = @auxInfoHash['lap_number']
2135
- if (lap_num && lap_num.size > 0)
2136
- @lap_number = lap_num.to_i
2137
- else
2138
- @lap_number = 0
2139
- end
2140
- # initialize superclass variables:
2141
- @latitude = lat.to_s
2142
- @longitude = lng.to_s
2143
- feet = alt.to_f * 3.2736 # Convert from meters (in the Garmin xml) to feet.
2144
- @altitude = feet.to_s
2145
- @note = note.to_s
2146
- @dttm = DtTm.new(time_string)
2147
- @first = false
2148
- @last = false
2149
- @prev_tkpt = nil
2150
- @cumulative_distance, @incremental_distance, @split = 0.0, 0.0, 0.0
2151
- @cumulative_pace = ""
2152
- end
2153
-
2154
- public
2155
-
2156
- def point
2157
- Point.new(@latitude, @longitude, @altitude, @note)
2158
- end
2159
-
2160
- def position
2161
- Point.new(@latitude, @longitude, @altitude, @note)
2162
- end
2163
-
2164
- def to_s
2165
- "Tkpt: #{@number} #{super.to_s} date: #{@dttm.to_s} cdist: #{@cumulative_distance}"
2166
- end
2167
-
2168
- def to_csv(prev_tkpt=nil)
2169
- first_lap_start_time_s = @auxInfoHash['first_lap_start_time']
2170
- curr_lap_start_time_s = @auxInfoHash['curr_lap_start_time']
2171
- lap_elapsed = ''
2172
- total_elapsed = ''
2173
-
2174
- if ((first_lap_start_time_s.size > 0) && (curr_lap_start_time_s.size > 0)) # garmin205 & 305
2175
- first_lap_start_time = DtTm.new(first_lap_start_time_s)
2176
- first_lap_start_time = @run_start_dttm
2177
- curr_lap_start_time = DtTm.new(curr_lap_start_time_s)
2178
- lap_elapsed = @dttm.hhmmss_diff(curr_lap_start_time)
2179
- total_elapsed = @dttm.hhmmss_diff(first_lap_start_time)
2180
- else # garmin 201
2181
- total_elapsed = @dttm.hhmmss_diff(@run_start_dttm)
2182
- lap_elapsed = total_elapsed
2183
- end
2184
-
2185
- delim = csv_delim
2186
- csv = "#{@run_id}.#{@number}" # <-- primary key
2187
- csv << "#{delim}#{@run_id}"
2188
- csv << "#{delim}#{@dttm.yyyy_mm_dd_hh_mm_ss('|')}"
2189
- csv << "#{delim}#{@number}"
2190
- csv << "#{delim}#{position.to_csv}"
2191
- csv << "#{delim}#{@cumulative_distance}"
2192
- csv << "#{delim}#{total_elapsed}"
2193
- csv << "#{delim}#{@lap_seq}"
2194
- csv << "#{delim}#{@lap_distance}"
2195
- csv << "#{delim}#{lap_elapsed}"
2196
- csv
2197
- end
2198
-
2199
- def self.csv_header
2200
- "#cols: primary_key|run_id|date|time|tkpt_num|latitude|longitude|altitude_ft|run_distance|run_elapsed|lap_tkpt_number|lap_distance|lap_elapsed"
2201
- end
2202
-
2203
- def to_geo_s
2204
- ss = position.to_csv
2205
- "Tkpt: #{@number} | #{ss} | #{@descr}"
2206
- end
2207
-
2208
- def compute_distance_and_pace(curr_index, start_dttm, prev_cumulative_dist, prev_trackpoint, units)
2209
- @prev_tkpt = prev_trackpoint
2210
- @cumulative_distance = prev_cumulative_dist.to_f
2211
- @run_start_dttm = start_dttm
2212
-
2213
- if @prev_tkpt
2214
- @incremental_distance = proximity(@prev_tkpt, units)
2215
- if (!@incremental_distance.nan?)
2216
- @cumulative_distance = @cumulative_distance + @incremental_distance.to_f
2217
- if (@lap_number == prev_trackpoint.lap_number)
2218
- @lap_seq = prev_trackpoint.lap_seq + 1
2219
- @lap_distance = prev_trackpoint.lap_distance + @incremental_distance
2220
- else
2221
- @lap_seq = 1
2222
- @lap_distance = @incremental_distance
2223
- end
2224
- end
2225
- compute_cumulative_pace(start_dttm)
2226
- @cumulative_distance
2227
- else
2228
- @lap_seq = 1
2229
- 0
2230
- end
2231
- end
2232
-
2233
- def compute_cumulative_pace(start_dttm)
2234
- if @cumulative_distance > 0
2235
- secsDiff = @dttm.seconds_diff(start_dttm)
2236
- secsMile = ((secsDiff.to_f) / (@cumulative_distance.to_f))
2237
- minsMile = (secsMile / 60)
2238
- wholeMins = minsMile.floor
2239
- secsBal = secsMile - (wholeMins * 60)
2240
- s1 = "#{secsDiff} #{secsMile} #{minsMile} #{wholeMins} #{secsBal} #{@cumulative_distance} | "
2241
- s2 = sprintf("%d:%2.1f", minsMile, secsBal)
2242
- @cumulative_pace = "#{s2}"
2243
- else
2244
- @cumulative_pace = ""
2245
- end
2246
- end
2247
-
2248
- def set_split(n, tkpt)
2249
- @split, @prev_split = n, tkpt
2250
- end
2251
-
2252
- def is_split()
2253
- (@split >= 1)
2254
- end
2255
-
2256
- def split_info(dtTm)
2257
- if is_split
2258
- hhmmss = ''
2259
- if @prev_split
2260
- return "#{@split} #{@dttm.hhmmss_diff(@prev_split.dttm())}"
2261
- else
2262
- return "#{@split} #{@dttm.hhmmss_diff(dtTm)}"
2263
- end
2264
- else
2265
- ""
2266
- end
2267
- end
2268
-
2269
- public
2270
-
2271
- def as_glatlng(comment_out, gen_comments, tkpt_count, curr_idx, start_dttm)
2272
- comment_out ? comment = '// ' : comment = ''
2273
- if gen_comments
2274
- secs_diff = @dttm.seconds_diff(start_dttm)
2275
- fmt_time = @dttm.hhmmss_diff(start_dttm)
2276
- "\n #{comment}points.push(new GLatLng(#{latitude_as_float},#{longitude_as_float})); " +
2277
- "// (#{curr_idx + 1} of #{tkpt_count}) #{@dttm.to_s} #{secs_diff} #{fmt_time} #{@cumulative_distance} #{split_info(start_dttm)} #{project_embedded_comment} "
2278
- else
2279
- "\n #{comment}points.push(new GLatLng(#{latitude_as_float},#{longitude_as_float})); // #{project_embedded_comment} "
2280
- end
2281
- end
2282
-
2283
- def as_info_window_html(checkpoint, start_dttm)
2284
- s = "\"<table align='left'>"
2285
- if checkpoint
2286
- secs_diff = @dttm.seconds_diff(start_dttm)
2287
- fmt_time = @dttm.hhmmss_diff(start_dttm)
2288
-
2289
- if checkpoint == 'Start'
2290
- s << "<tr><td colspan='2'><b>Start!</b></td></tr>"
2291
- elsif checkpoint == 'Finish'
2292
- s << "<tr><td colspan='2'><b>Finish!</b></td></tr>"
2293
- else
2294
- s << "<tr><td colspan='2'><b>Checkpoint #{checkpoint}</b></td></tr>"
2295
- end
2296
- s << "<tr><td>Distance: </td><td>#{@cumulative_distance}</td></tr>"
2297
- s << "<tr><td>Time of Day: </td><td>#{@dttm.to_s} </td></tr>"
2298
- s << "<tr><td>Elapsed Time: </td><td>#{fmt_time} </td></tr>"
2299
- s << "<tr><td>Average Pace: </td><td>#{@cumulative_pace} </td></tr>"
2300
- s << "<tr><td>Lat/Lng: </td><td>#{latitude_as_float} , #{longitude_as_float} </td></tr>"
2301
- #s << "<tr><td>Altitude: </td><td>#{altitude_as_float}m </td></tr>"
2302
- s
2303
- end
2304
- s << "</table>\""
2305
- s
2306
- end
2307
- end
2308
-
2309
- # =============================================================================
2310
-
2311
- =begin rdoc
2312
- Instances of this class represent a <Run> aggregate object from a
2313
- Forerunner XML file.
2314
-
2315
- Additionally, there is distance, pace, and Google Map generation logic
2316
- in this class.
2317
- =end
2318
-
2319
- class Run < GoobyObject
2320
-
2321
- attr_accessor :number, :run_id, :descr, :notes, :tracks, :tkpts, :laps, :distance
2322
-
2323
- def initialize(number=0, descr='')
2324
- @number = number
2325
- @run_id = nil
2326
- @descr = descr
2327
- @notes = ''
2328
- @tracks = Array.new
2329
- @tkpts = Array.new
2330
- @laps = Array.new
2331
- @distance = 0
2332
- @configuration = Hash.new
2333
- @logProgress = true
2334
- @finished = false
2335
- end
2336
-
2337
- public
2338
-
2339
- # This method is invoked at end-of-parsing.
2340
- def finish()
2341
- @logProgress = false
2342
- unless @finished
2343
- @tracks.each { |trk|
2344
- trk.trackpoints().each { |tkpt|
2345
- tkpt.run_number = @number
2346
- @tkpts.push(tkpt)
2347
- }
2348
- }
2349
- compute_distance_and_pace
2350
- compute_splits
2351
- set_run_ids
2352
- @finished = true
2353
- end
2354
- end
2355
-
2356
- public
2357
-
2358
- def add_track(trk)
2359
- if trk != nil
2360
- @tracks.push(trk)
2361
- end
2362
- end
2363
-
2364
- def trackpoint_count()
2365
- @tkpts.size()
2366
- end
2367
-
2368
- def add_lap(lap)
2369
- @laps.push(lap)
2370
- end
2371
-
2372
- def lap_count()
2373
- @laps.size
2374
- end
2375
-
2376
- def start_dttm()
2377
- count = 0
2378
- @tracks.each { |trk|
2379
- trk.trackpoints().each { |tkpt|
2380
- return tkpt.dttm()
2381
- }
2382
- }
2383
- return nil
2384
- end
2385
-
2386
- def end_dttm()
2387
- lastOne = nil
2388
- @tracks.each { |trk|
2389
- trk.trackpoints().each { |tkpt|
2390
- lastOne = tkpt.dttm()
2391
- }
2392
- }
2393
- lastOne
2394
- end
2395
-
2396
- def duration()
2397
- first = start_dttm()
2398
- last = end_dttm()
2399
- if first
2400
- if last
2401
- return last.hhmmss_diff(first)
2402
- end
2403
- end
2404
- return "00:00:00"
2405
- end
2406
-
2407
- def start_yyyy_mm_dd
2408
- if start_dttm()
2409
- start_dttm().yyyy_mm_dd()
2410
- else
2411
- ""
2412
- end
2413
- end
2414
-
2415
- def start_hh_mm_ss
2416
- if start_dttm()
2417
- start_dttm().hh_mm_ss()
2418
- else
2419
- ""
2420
- end
2421
- end
2422
-
2423
- def end_hh_mm_ss
2424
- if end_dttm()
2425
- end_dttm().hh_mm_ss()
2426
- else
2427
- ""
2428
- end
2429
- end
2430
-
2431
- def to_s
2432
- finish() unless @finished
2433
- s = "Run: #{@number} date: #{start_yyyy_mm_dd} distance: #{distance} duration: #{duration} "
2434
- s << " tracks: #{@tracks.size} tkpts: #{trackpoint_count} laps: #{lap_count} "
2435
- s << " notes: #{@notes} "
2436
- s
2437
- end
2438
-
2439
- def print_string
2440
- finish() unless @finished
2441
- "Run number=#{@number} tracks=#{@tracks.size} tkpts=#{@tkpts.size} laps=#{@laps.size} distance=#{@distance} "
2442
- end
2443
-
2444
- def put_csv()
2445
- finish() unless @finished
2446
- puts "#{@number}|#{}|#{start_yyyy_mm_dd()}|#{start_hh_mm_ss()}|#{end_hh_mm_ss}|#{duration()}|#{@distance}|#{@tracks.size}|#{trackpoint_count()}|#{lap_count}|#{@notes.strip}"
2447
- end
2448
-
2449
- def put_tkpt_csv()
2450
- finish() unless @finished
2451
- @tkpts.each { | tkpt |
2452
- if (@prev_tkpt == nil)
2453
- @prev_tkpt = tkpt
2454
- end
2455
- puts tkpt.to_csv(@prev_tkpt)
2456
- }
2457
- end
2458
-
2459
- def put_laps
2460
- @laps.each { | lap | puts lap.to_s }
2461
- end
2462
-
2463
- private
2464
-
2465
- def compute_distance_and_pace
2466
- cumulative_dist = 0.to_f;
2467
- curr_index = -1
2468
- prev_tkpt = nil
2469
- start_dttm = nil
2470
- @tkpts.each { | tkpt |
2471
- curr_index = curr_index + 1
2472
- if curr_index == 0
2473
- start_dttm = tkpt.dttm()
2474
- prev_tkpt = tkpt
2475
- else
2476
- cumulative_dist = tkpt.compute_distance_and_pace(curr_index, start_dttm, cumulative_dist, prev_tkpt, 'M')
2477
- prev_tkpt = tkpt
2478
- end
2479
- }
2480
- @distance = cumulative_dist
2481
- end
2482
-
2483
- def compute_splits
2484
- nextSplitDist = 1.00
2485
- prev_splitTkpt = nil
2486
- loop1Count = 0;
2487
- @tkpts.each { |tkpt|
2488
- loop1Count = loop1Count + 1
2489
- if tkpt.cumulative_distance() >= nextSplitDist
2490
- tkpt.set_split(0 + nextSplitDist, prev_splitTkpt)
2491
- nextSplitDist = nextSplitDist + 1.00
2492
- prev_splitTkpt = tkpt
2493
- end
2494
- }
2495
- # set first and last booleans
2496
- count = 0
2497
- @tkpts.each { |tkpt|
2498
- count = count + 1
2499
- tkpt.first = true if count == 1
2500
- tkpt.last = true if count == loop1Count
2501
- }
2502
- end
2503
-
2504
- def set_run_ids
2505
- @tkpts.each { |tkpt|
2506
- if (@run_id == nil)
2507
- @run_id = tkpt.dttm.rawdata
2508
- end
2509
- tkpt.run_id = @run_id
2510
- }
2511
- end
2512
- end
2513
-
2514
- # =============================================================================
2515
-
2516
- =begin rdoc
2517
- Sample implementation of a REXML::StreamListener SAX parser.
2518
- This class isn't actually used in Gooby, but is retained for future use.
2519
- =end
2520
-
2521
- class SimpleXmlParser
2522
-
2523
- include REXML::StreamListener
2524
-
2525
- attr_accessor :tag_count, :watched_tags
2526
-
2527
- def initialize
2528
- @tag_count = 0
2529
- @counter_hash = CounterHash.new
2530
- end
2531
-
2532
- public
2533
-
2534
- # SAX API method. Increments the tagname in the counter hash.
2535
- def tag_start(tag_name, attrs)
2536
- @tag_count = @tag_count + 1
2537
- @counter_hash.increment(tag_name)
2538
- end
2539
-
2540
- # SAX API method. No impl.
2541
- def tag_end(tagname)
2542
- end
2543
-
2544
- # SAX API method. No impl.
2545
- def text(txt)
2546
- end
2547
-
2548
- # Prints the state of this object (the counter hash).
2549
- def dump
2550
- puts @counter_hash.to_s
2551
- end
2552
- end
2553
-
2554
- # =============================================================================
2555
-
2556
- =begin rdoc
2557
- Instances of this class represent a <Track> aggregate object from a Forerunner
2558
- XML file. Note that a <Run> may contain more than one <Track> aggregates.
2559
- =end
2560
-
2561
- class Track < GoobyObject
2562
-
2563
- attr_reader :number, :descr, :trackpoints
2564
-
2565
- def initialize(num=0, descr='')
2566
- @number = num
2567
- @descr = descr
2568
- @trackpoints = Array.new
2569
- end
2570
-
2571
- public
2572
-
2573
- def add_trackpoint(tkpt)
2574
- @trackpoints.push(tkpt)
2575
- end
2576
-
2577
- def size
2578
- @trackpoints.size
2579
- end
2580
-
2581
- def to_s
2582
- return "Trk: #{@descr} tkpts: #{size}"
2583
- end
2584
-
2585
- def dump
2586
- puts "Track: '#{@descr}' tkpts: #{size}"
2587
- @trackpoints.each { |tkpt| puts tkpt.to_csv }
2588
- end
2589
- end
2590
-
2591
- # =============================================================================
2592
-
2593
- class Course < GoobyObject
2594
-
2595
- attr_accessor :name, :distance, :point_numbers, :points
2596
-
2597
- def initialize(yaml_csv)
2598
- @name, @distance = '', 0.0
2599
- @point_numbers, @points, @bad_points = Array.new, Array.new, Array.new
2600
- @points_hash, @matched_points = Hash.new, Hash.new
2601
- tokens = yaml_csv.split(',')
2602
- @name = tokens[0] if tokens.size > 0
2603
- @distance = tokens[1].to_f if tokens.size > 1
2604
- if tokens.size > 2
2605
- index = 0
2606
- tokens.each { |tok|
2607
- index = index + 1
2608
- if (index > 2)
2609
- poi = Configuration.get_config.get_poi(tok)
2610
- if (poi)
2611
- poi.number = "#{tok}"
2612
- @point_numbers << "#{tok}"
2613
- @points << poi
2614
- @points_hash["#{tok}"] = poi
2615
- else
2616
- @bad_points << tok
2617
- end
2618
- end
2619
- }
2620
- end
2621
- end
2622
-
2623
- def has_errors
2624
- (@bad_points.size > 0) ? true : false
2625
- end
2626
-
2627
- def matched(number, point)
2628
- @matched_points["#{number}"] = point if point
2629
- end
2630
-
2631
- def matched?
2632
- (@matched_points.size == @point_numbers.size) ? true : false
2633
- end
2634
-
2635
- def display_matches
2636
- puts ''
2637
- calculate_matches
2638
- @point_numbers.each { |num|
2639
- point = @points_hash["#{num}"]
2640
- mpoint = @matched_points["#{num}"]
2641
- puts ''
2642
- puts " Course Point: #{point.to_formatted_string}" if point
2643
- puts " Matched Point: #{mpoint.to_formatted_string}" if mpoint
2644
- }
2645
- puts ''
2646
- end
2647
-
2648
- def reset
2649
- @matched_points = Hash.new
2650
- end
2651
-
2652
- def to_s
2653
- "#{@name} #{@distance} points: #{@points.size} errors: #{has_errors}"
2654
- end
2655
-
2656
- def dump
2657
- puts "Course: #{@name}"
2658
- puts "Distance: #{@distance}"
2659
- points.each { |pt| puts pt } #{@points.size} errors: #{has_errors}"
2660
- end
2661
-
2662
- private
2663
-
2664
- def calculate_matches
2665
- # first, identify the high and low distance points and their indices.
2666
- idx, low_dist, low_dist_idx, low_time, high_dist, high_dist_idx = -1, 999999.0, 0, '', -1.0, 0
2667
- @point_numbers.each { |num|
2668
- idx = idx + 1
2669
- mpoint = @matched_points["#{num}"]
2670
- if mpoint && mpoint.distance < low_dist
2671
- low_dist = mpoint.distance
2672
- low_dist_idx = idx
2673
- low_time = mpoint.elapsed
2674
- end
2675
- if mpoint && mpoint.distance > high_dist
2676
- high_dist = mpoint.distance
2677
- high_dist_idx = idx
2678
- end
2679
- }
2680
- low_dttm = DtTm.new("2007-06-09T#{low_time}Z")
2681
-
2682
- # reorder the entries in @point_numbers if necessary - 'low-to-high distance'.
2683
- if (high_dist_idx < low_dist_idx)
2684
- @point_numbers.reverse!
2685
- end
2686
-
2687
- @point_numbers.each { |num|
2688
- mpoint = @matched_points["#{num}"]
2689
- if mpoint
2690
- mpoint.course_distance = mpoint.distance - low_dist
2691
- dttm = DtTm.new("2007-06-09T#{mpoint.elapsed}Z")
2692
- mpoint.course_elapsed = dttm.hhmmss_diff(low_dttm)
2693
- end
2694
- }
2695
- end
2696
- end
2697
-
2698
- # =============================================================================
2699
-
2700
- =begin rdoc
2701
- This class reads and scans the Gooby code for various purposes.
2702
-
2703
- Primarily, it is used to regenerate, on an ongoing basis, the various
2704
- regression tests in file 'ts_gooby.rb'. Regeneration retains the current
2705
- test methods, adds stubs for new test methods, and flags obsolete methods.
2706
-
2707
- It is also used to create a Gooby class, module, and method "quick reference"
2708
- - somewhat similar to the TextMate symbol list.
2709
-
2710
- TODO: Method indexing and where used" functionality.
2711
- =end
2712
-
2713
- class CodeScanner < GoobyObject
2714
-
2715
- def initialize(argv)
2716
-
2717
- function = 'outline'
2718
- if (argv.size > 0)
2719
- function = argv[0]
2720
- end
2721
-
2722
- @codebase_file = 'gooby.rb'
2723
- @testbase_file = 'ts_gooby.rb'
2724
- @code_lines = read_lines("lib/#{@codebase_file}")
2725
- @test_lines = read_lines("tests/#{@testbase_file}")
2726
-
2727
- puts "code lines = #{@code_lines.size}"
2728
- puts "test lines = #{@test_lines.size}"
2729
-
2730
- @tokens_hash = CounterHash.new
2731
- @module_names_hash = CounterHash.new
2732
- @class_names_hash = CounterHash.new
2733
- @method_names_hash = CounterHash.new
2734
- @mc_line_num_array = Array.new
2735
- @type_names = Hash.new
2736
- @code_hash = Hash.new
2737
- @test_hash = Hash.new
2738
- @api_hash = Hash.new
2739
- @merged_hash = Hash.new
2740
- @exclude_classes = Array.new
2741
-
2742
- regenerate_test_suite if (function == 'regenerate_test_suite')
2743
- regenerate_test_suite if (function == 'tests')
2744
-
2745
- model_class_outline if (function == 'model_class_outline')
2746
- model_class_outline if (function == 'outline')
2747
-
2748
- quick_reference_guide if (function == 'quick_reference_guide')
2749
- quick_reference_guide if (function == 'qrg')
2750
-
2751
- mcm_references if (function == 'mcm_references')
2752
- mcm_references if (function == 'mcm')
2753
- end
2754
-
2755
- private
2756
-
2757
- def regenerate_test_suite
2758
- @exclude_classes = %w( TestHelper CodeScanner )
2759
- parse_code_lines
2760
- parse_test_lines
2761
- merge_keys
2762
- regenerate(@testbase_file)
2763
- end
2764
-
2765
- def model_class_outline
2766
- parse_code_lines
2767
- @mc_line_num_array.each { |line|
2768
- tokens = tokenize(line)
2769
- type = tokens[0].ljust(6)
2770
- name = tokens[1]
2771
- count = tokens[2].rjust(6)
2772
- puts " #{count} #{type} #{name}"
2773
- }
2774
- end
2775
-
2776
- def quick_reference_guide
2777
- parse_code_lines
2778
- determine_longest_names
2779
- quick_reference_guide_report('module')
2780
- quick_reference_guide_report('class')
2781
- end
2782
-
2783
- def quick_reference_guide_report(obj_type)
2784
- @api_hash.keys.sort.each { |key|
2785
- val = @api_hash[key]
2786
- tokens = tokenize(key, '|')
2787
- if (tokens[0] == obj_type)
2788
- s1 = tokens[0].ljust(6)
2789
- s2 = tokens[1].ljust(@longest_classname + 1)
2790
- s3 = tokens[2]
2791
- # s = sprintf("%6s '%30s %s'", tokens[0], tokens[1], tokens[2])
2792
- puts "#{s1} #{s2} #{s3}"
2793
- end
2794
- }
2795
- end
2796
-
2797
- def determine_longest_names
2798
- @longest_classname = 0
2799
- @api_hash.keys.sort.each { |key|
2800
- tokens = tokenize(@api_hash[key], '|')
2801
- @longest_classname = tokens[1].size if tokens[1].size > @longest_classname
2802
- }
2803
- end
2804
-
2805
- def mcm_references
2806
- parse_code_lines
2807
- determine_longest_names
2808
- @module_names_hash.sorted_keys.each { |name|
2809
- count = @tokens_hash.value(name)
2810
- puts "module: #{name} (#{count})"
2811
- }
2812
- @class_names_hash.sorted_keys.each { |name|
2813
- count = @tokens_hash.value(name)
2814
- puts "class: #{name} (#{count})"
2815
- }
2816
- @method_names_hash.sorted_keys.each { |name|
2817
- count = @tokens_hash.value(name)
2818
- puts "method: #{name} (#{count})"
2819
- }
2820
- end
2821
-
2822
- def parse_code_lines
2823
- type = ''
2824
- type_name = ''
2825
- meth_name = ''
2826
- line_number = 0
2827
- @code_lines.each { | line |
2828
- line_number = line_number + 1
2829
- line.strip!
2830
- if (line.match(/^module /))
2831
- type = 'module'
2832
- tokens = line.split
2833
- type_name = tokens[1]
2834
- @module_names_hash.increment(type_name)
2835
- @mc_line_num_array << "module #{type_name} #{line_number}"
2836
- elsif (line.match(/^class /))
2837
- type = 'class'
2838
- tokens = line.split
2839
- type_name = tokens[1]
2840
- @class_names_hash.increment(type_name)
2841
- @mc_line_num_array << "class #{type_name} #{line_number}"
2842
- elsif (line.match(/^def /))
2843
- signature = line[4...999]
2844
- short_method = parse_meth_name("#{signature}")
2845
- @code_hash["test_#{type}_#{type_name}"] = "#{type_name}"
2846
- @code_hash["test_#{type}_#{type_name}_#{short_method}"] = "#{type_name}|#{signature}"
2847
- @api_hash["#{type}|#{type_name}|#{signature}" ] = "#{type}|#{type_name}|#{signature}"
2848
- @method_names_hash.increment(short_method)
2849
- end
2850
- increment_tokens(line)
2851
- }
2852
- end
2853
-
2854
- def increment_tokens(line)
2855
- # see http://www.asciitable.com/
2856
- s = ''
2857
- line.each_byte { |b|
2858
- keep = false
2859
- keep = true if ((b >= 65) && (b <= 90)) # A-Z
2860
- keep = true if ((b >= 97) && (b <= 122)) # a-z
2861
- keep = true if ((b >= 48) && (b <= 57)) # 0-9
2862
- keep = true if (b == 95) # _
2863
- keep = true if (b == 64) # @
2864
- keep = true if (b == 63) # ?
2865
- keep = true if (b == 33) # !
2866
- if keep
2867
- s << b.chr
2868
- else
2869
- s << ' '
2870
- end
2871
- }
2872
- @tokens_hash.increment_tokens(s)
2873
- end
2874
-
2875
- def parse_meth_name(string)
2876
- string.gsub!('(', ' ')
2877
- string.gsub!(')', ' ')
2878
- tokens = string.split
2879
- tokens[0]
2880
- end
2881
-
2882
- def parse_test_lines
2883
- in_zone = true
2884
- method_name = 'a_start'
2885
- method_lines = Array.new
2886
- line_num = 0
2887
-
2888
- @test_lines.each { | line |
2889
- line_num = line_num + 1
2890
- line.chomp!
2891
- prefix = line[0...5] # ' def' or ' end'
2892
- prefix42 = line [0..42]
2893
-
2894
- if ((prefix == ' def') || (prefix == "\tdef"))
2895
- in_zone = true
2896
- tokens = line.split
2897
- method_name = tokens[1]
2898
- method_lines = Array.new
2899
- end
2900
-
2901
- if in_zone
2902
- method_lines << "#{line}"
2903
- end
2904
- if (prefix42 == ' # beginning of tests - keep this marker')
2905
- in_zone = false
2906
- @test_hash["aaa"] = method_lines
2907
- end
2908
- if ((prefix == ' end') || (prefix == "\tend"))
2909
- in_zone = false
2910
- @test_hash["#{method_name}"] = method_lines
2911
- end
2912
- }
2913
- end
2914
-
2915
- def merge_keys
2916
- @merged_hash = Hash.new
2917
- @code_hash.keys.sort.each { |key| @merged_hash["#{key}"] = "code" }
2918
- @test_hash.keys.sort.each { |key| @merged_hash["#{key}"] = "test" }
2919
- end
2920
-
2921
- def regenerate(test_file)
2922
- code = ''
2923
- @merged_hash.keys.sort.each { |key|
2924
- tokens = key.split('_')
2925
- type, name, meth = tokens[1], tokens[2], tokens[3]
2926
-
2927
- processThisKey = true
2928
- @exclude_classes.each { |xc|
2929
- if xc == name
2930
- processThisKey = false
2931
- end
2932
- }
2933
-
2934
- next if !processThisKey
2935
-
2936
- if @test_hash.has_key?(key)
2937
- # We already have a test method written in the test class,
2938
- # so keep this currently existing test code!
2939
-
2940
- if @code_hash.has_key?(key)
2941
- comment = nil
2942
- else
2943
- if key != 'a_start'
2944
- comment = "# Warning: possible obsolete test method - #{key}"
2945
- end
2946
- end
2947
-
2948
- code << "\n"
2949
- if comment != nil
2950
- code << "\n#{comment}"
2951
- code << "\n"
2952
- end
2953
- array = @test_hash["#{key}"]
2954
- array.each { |line| code << "\n#{line}" }
2955
- else
2956
- # We don't have this test method in the current test class,
2957
- # so generate a test method stub.
2958
- code << "\n"
2959
- code << "\n def #{key}"
2960
- code << "\n"
2961
-
2962
- if @gen_impl_stub
2963
- if type = 'class'
2964
- code << "\n #obj = #{type}.new"
2965
- code << "\n #result = obj.#{meth}"
2966
- code << "\n #expected = ''"
2967
- s = "\n"
2968
- s << ' #assert_equal(expected, actual, "'
2969
- s << "#{type}.#{meth} "
2970
- s << 'values are not as expected; #{result} vs #{expected}")'
2971
- code << s
2972
- else
2973
- code << "\n #result = #{type}.#{meth}"
2974
- code << "\n #expected = ''"
2975
- s = "\n"
2976
- s << ' #assert_equal(expected, actual, "'
2977
- s << "#{type}.#{meth} "
2978
- s << 'values are not as expected; #{result} vs #{expected}")'
2979
- code << s
2980
- end
2981
- end
2982
- code << "\n end"
2983
- end
2984
- }
2985
- code << "\nend" # end of class
2986
- code << "\n"
2987
- fn = "tests/#{test_file}"
2988
- out = File.new fn, "w+"
2989
- out.write code
2990
- out.flush
2991
- out.close
2992
- puts "file written: #{fn}"
2993
- end
2994
- end
2995
-
2996
- # =============================================================================
2997
-
2998
- =begin rdoc
2999
- This class provides "user friendly DSL" functionality for the use
3000
- of Gooby. See 'bin/example_usage.rb' for examples using this class.
3001
- =end
3002
-
3003
- class GoobyCommand < GoobyObject
3004
-
3005
- attr_reader :configuration
3006
-
3007
- def initialize(gooby_yaml_filename=nil)
3008
- @configuration = Gooby::Configuration.init(gooby_yaml_filename)
3009
- end
3010
-
3011
- def display_version
3012
- s = "# #{project_name} #{project_version_number} #{project_date}. #{project_copyright}."
3013
- puts s
3014
- end
3015
-
3016
- def split_garmin_export_file(argv)
3017
- if (argv == nil)
3018
- puts "ERROR: no ARGV args passed."
3019
- elsif (argv.size < 3)
3020
- puts ""
3021
- puts "Invalid program args; three args required - format, input filename, output directory"
3022
- puts " the first arg, format, should be one of: garmin201, garmin205, garmin305, etc."
3023
- puts " the second arg is the input filename - which is a garmin export file"
3024
- puts " the third arg is the output directory where the split files are written to\n\n"
3025
- puts "Please correct the program arguments and try again. \n\n"
3026
- else
3027
- format = argv[0].downcase
3028
- filename = argv[1]
3029
- out_dir = argv[2]
3030
-
3031
- if (format == 'garmin201')
3032
- split_garmin_forerunner_logbook_xml(filename, out_dir)
3033
- else
3034
- split_garmin_training_center_xml(filename, out_dir)
3035
- end
3036
- end
3037
- end
3038
-
3039
- def split_garmin_forerunner_logbook_xml(xml_filename, out_dir)
3040
- splitter = Gooby::ForerunnerXmlSplitter.new(xml_filename, out_dir)
3041
- splitter.split
3042
- end
3043
-
3044
- def split_garmin_training_center_xml(tcx_filename, out_dir)
3045
- splitter = Gooby::TrainingCenterXmlSplitter.new(tcx_filename, out_dir)
3046
- splitter.split
3047
- end
3048
-
3049
- def parse_garmin_xml_file(argv)
3050
- if (argv == nil)
3051
- puts "ERROR: no ARGV args passed."
3052
- elsif (argv.size < 2)
3053
- puts ""
3054
- puts "Invalid program args; two args required - format, input (xml) filename, output directory"
3055
- puts " the first arg, format, should be one of: garmin201, garmin205, garmin305, etc."
3056
- puts " the second arg, input xml filename, was produced by the Gooby 'splitter.rb' program."
3057
- puts "Please correct the program arguments and try again. \n\n"
3058
- else
3059
- format = argv[0].downcase
3060
- filename = argv[1]
3061
- puts Trackpoint.csv_header
3062
- if (format == 'garmin201')
3063
- parse_garmin_forerunner_logbook_xml(filename)
3064
- else
3065
- parse_garmin_training_center_xml(filename)
3066
- end
3067
- end
3068
- end
3069
-
3070
- def parse_garmin_forerunner_logbook_xml(xml_filename)
3071
- handler = Gooby::ForerunnerXmlParser.new
3072
- Document.parse_stream((File.new xml_filename), handler)
3073
- handler.put_all_run_tkpt_csv()
3074
- end
3075
-
3076
- def parse_garmin_training_center_xml(tcx_filename)
3077
- handler = Gooby::TrainingCenterXmlParser.new
3078
- Document.parse_stream((File.new tcx_filename), handler)
3079
- handler.put_all_run_tkpt_csv()
3080
- end
3081
-
3082
- def read_csv_files(array_of_filenames, record_index=0)
3083
- @cvs_reader = Gooby::CsvReader.new(array_of_filenames)
3084
- @cvs_points = @cvs_reader.read
3085
- puts @cvs_reader.to_s
3086
- if (record_index > 0)
3087
- @cvs_reader.display_formatted_record(record_index)
3088
- end
3089
- @cvs_points
3090
- end
3091
-
3092
- def been_there(course_id, proximity=0.0070, uom='deg')
3093
- unless @cvs_points
3094
- puts "You must first invoke method 'read_csv_files' with a list of parsed CSV filenames."
3095
- return
3096
- end
3097
- course = configuration.get_course("#{course_id}")
3098
- unless course
3099
- puts "Unable to find course id '#{course_id}' in the gooby config yaml file."
3100
- return
3101
- end
3102
- puts ''
3103
- # collect the cvs_points into run arrays
3104
- @curr_run_id = 'x'
3105
- @cvs_runs = Array.new
3106
- @cvs_points.each { |cvs_point|
3107
- if (cvs_point.run_id == @curr_run_id)
3108
- @curr_run.add_point(cvs_point)
3109
- else
3110
- @curr_run_id = cvs_point.run_id
3111
- @curr_run = Gooby::CvsRun.new(@curr_run_id)
3112
- @curr_run.add_point(cvs_point)
3113
- @cvs_runs << @curr_run
3114
- end
3115
- }
3116
-
3117
- # iterate the runs - looking for a match vs the course
3118
- @cvs_runs.each { |cvs_run|
3119
- puts "Scanning run id '#{cvs_run.id}', point count= #{cvs_run.points.size}" if false
3120
- run_points = cvs_run.points
3121
- course.reset
3122
- course.points.each { |course_point|
3123
- closest_diff = 999
3124
- closest_point = nil
3125
- run_points.each { |run_point|
3126
- diff = course_point.degrees_diff(run_point)
3127
- if ((diff < proximity) && (diff < closest_diff))
3128
- closest_diff = diff
3129
- closest_point = run_point
3130
- closest_point.degrees_diff = diff
3131
- course.matched(course_point.number, run_point)
3132
- end
3133
- }
3134
- }
3135
- if course.matched?
3136
- puts "Course '#{course.name}' matched vs run id '#{cvs_run.id}'"
3137
- course.display_matches
3138
- else
3139
- puts "course not matched" if false
3140
- end
3141
- }
3142
- end
3143
-
3144
- def generate_google_map(argv)
3145
- if (argv == nil)
3146
- puts "ERROR: no ARGV args passed."
3147
- elsif (argv.size < 1)
3148
- puts ""
3149
- puts "Invalid program args; one required - input csv filename"
3150
- puts " the first arg, csv filename, was produced by the Gooby 'parser.rb' program."
3151
- puts "Please correct the program arguments and try again. \n\n"
3152
- else
3153
- csv_filename = argv[0]
3154
- configuration = Gooby::Configuration.get_config
3155
- generator = Gooby::GoogleMapGenerator.new(csv_filename)
3156
- generator.generate_page(configuration)
3157
- end
3158
-
3159
- end
3160
- end
3161
- end # end of module
18
+ require 'gooby_kernel'
19
+ require 'gooby_test_helper'
20
+ require 'gooby_object'
21
+ require 'gooby_counter_hash'
22
+ require 'gooby_delim_line'
23
+ require 'gooby_dttm'
24
+ require 'gooby_duration'
25
+ require 'gooby_forerunner_xml_parser'
26
+ require 'gooby_forerunner_xml_splitter'
27
+ require 'gooby_training_center_xml_parser'
28
+ require 'gooby_training_center_xml_splitter'
29
+ require 'gooby_google_map_generator'
30
+ require 'gooby_history'
31
+ require 'gooby_lap'
32
+ require 'gooby_line'
33
+ require 'gooby_configuration'
34
+ require 'gooby_point'
35
+ require 'gooby_csv_point'
36
+ require 'gooby_csv_run'
37
+ require 'gooby_csv_reader'
38
+ require 'gooby_track_point'
39
+ require 'gooby_run'
40
+ require 'gooby_simple_xml_parser'
41
+ require 'gooby_track'
42
+ require 'gooby_course'
43
+ require 'gooby_code_scanner'
44
+ require 'gooby_command'