gcstats 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,50 @@
1
+ # gcstats #
2
+
3
+ A Ruby program that generates simple and clean statistics of your [Geocaching][gc] activity in HTML format ([sample][agorf]).
4
+
5
+ Also available online as a web app: <http://gcstats.heroku.com/>
6
+
7
+ [gc]: http://www.geocaching.com/
8
+ [agorf]: http://agorf.github.com/gcstats/agorf.html
9
+
10
+ ## Install ##
11
+
12
+ To install, change directory to where you have extracted the gcstats source files and issue:
13
+
14
+ gem build gcstats.gemspec
15
+ gem install gcstats-*.gem
16
+
17
+ ## Use ##
18
+
19
+ Visit the [Pocket Queries][pq] page and click the _Add to Queue_ button (under _My Finds_). After a while, you will receive an email informing you that your Pocket Query file is available for download. Visit the [Pocket Queries][pq] page again and download the file under the _Pocket Queries Ready for Download_ tab.
20
+
21
+ Assuming your Pocket Query file is _3627915.zip_, issue:
22
+
23
+ ruby gcstats.rb 3627915.zip
24
+
25
+ Open _3627915.html_ to view your statistics. It is also possible to specify the output filename:
26
+
27
+ ruby gcstats.rb 3627915.zip agorf.html
28
+
29
+ Will write to _agorf.html_.
30
+
31
+ **Note:** You need to be a [Premium Member][pm] at [Geocaching.com][gc] to use [Pocket Queries][pq].
32
+
33
+ [pq]: http://www.geocaching.com/pocket/
34
+ [pm]: https://www.geocaching.com/membership/
35
+
36
+ ## License ##
37
+
38
+ (The MIT License)
39
+
40
+ Copyright (c) 2010 Aggelos Orfanakos
41
+
42
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
43
+
44
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
47
+
48
+ ## Author ##
49
+
50
+ [Aggelos Orfanakos](http://agorf.gr/), with contributions from [Thomas Cyron](http://thcyron.de/).
data/bin/gcstats ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'zip/zip'
5
+ require 'gcstats/mapping'
6
+ require 'gcstats/template'
7
+
8
+ in_fn, out_fn = ARGV[0..1]
9
+
10
+ if in_fn.nil?
11
+ $stderr.puts "usage: #$0 <infile> [outfile]"
12
+ exit 1
13
+ end
14
+
15
+ if File.extname(in_fn) == '.zip'
16
+ data = nil
17
+
18
+ Zip::ZipFile.foreach(in_fn) {|entry|
19
+ if File.extname(entry.name) == '.gpx'
20
+ data = entry.get_input_stream.read
21
+ out_fn ||= File.basename(entry.name, '.gpx') + '.html'
22
+ break
23
+ end
24
+ }
25
+ else
26
+ data = File.read(in_fn)
27
+ out_fn ||= File.basename(in_fn, File.extname(in_fn)) + '.html'
28
+ end
29
+
30
+ lib_dir = File.join(File.dirname(__FILE__), '..', 'lib', 'gcstats')
31
+
32
+ rhtml = File.read(File.join(lib_dir, 'gcstats.rhtml'))
33
+ caches = GCStats::Caches.from_xml(data)
34
+ html = GCStats::Template.new(rhtml, :caches => caches).result
35
+ html.sub!('/* %css% */', "\n" + File.read(File.join(lib_dir, 'gcstats.css')))
36
+ html.sub!('/* %js% */', "\n" + File.read(File.join(lib_dir, 'gcstats.js')))
37
+ open(out_fn, 'w') {|f| f.write(html) }
38
+ puts "wrote #{out_fn}" if test(?s, out_fn)
data/config.ru ADDED
@@ -0,0 +1,3 @@
1
+ require 'gcstats/server'
2
+
3
+ run Sinatra::Application
@@ -0,0 +1,45 @@
1
+ body {
2
+ font-family: sans-serif;
3
+ }
4
+
5
+ table {
6
+ margin-bottom: 1em;
7
+ }
8
+
9
+ caption {
10
+ font-weight: bold;
11
+ }
12
+
13
+ th {
14
+ background-color: #9ab54f;
15
+ color: #240;
16
+ }
17
+
18
+ td {
19
+ background-color: #eee;
20
+ color: #333;
21
+ }
22
+
23
+ caption, th, td {
24
+ padding: 3px 5px;
25
+ }
26
+
27
+ .l {
28
+ text-align: left;
29
+ }
30
+
31
+ .c {
32
+ text-align: center;
33
+ }
34
+
35
+ .r {
36
+ text-align: right;
37
+ }
38
+
39
+ .hl {
40
+ background-color: #ddd;
41
+ }
42
+
43
+ .b {
44
+ font-weight: bold;
45
+ }
@@ -0,0 +1,78 @@
1
+ if (!GCStats) {
2
+ Array.prototype.max = function () {
3
+ return Math.max.apply(Math, this);
4
+ };
5
+
6
+ Array.prototype.min = function () {
7
+ return Math.min.apply(Math, this);
8
+ };
9
+
10
+ var GCStats = {
11
+ highlight_table: function (table) {
12
+ var finds, finds_max;
13
+
14
+ finds = $(table).find('td.finds');
15
+
16
+ finds_max = finds.map(function () {
17
+ return $(this).text() * 1;
18
+ }).get().max();
19
+
20
+ finds.filter(function () {
21
+ return $(this).text() * 1 === finds_max;
22
+ }).each(function () {
23
+ $(this).parent().find('td').addClass('hl b');
24
+ });
25
+ },
26
+ highlight_square_table: function (table) {
27
+ var total_row, row_max, total_col, col_max, inner_cells, table_max;
28
+
29
+ // total row
30
+
31
+ total_row = $(table).find('tr.total td');
32
+
33
+ row_max = total_row.slice(0, -1).map(function () {
34
+ return $(this).text() * 1;
35
+ }).get().max();
36
+
37
+ total_row.addClass('hl').filter(function () {
38
+ return $(this).text() * 1 === row_max;
39
+ }).addClass('b');
40
+
41
+ total_row.last().addClass('b');
42
+
43
+ // total column
44
+
45
+ total_col = $(table).find('td.total');
46
+
47
+ col_max = total_col.map(function () {
48
+ return $(this).text() * 1;
49
+ }).get().max();
50
+
51
+ total_col.addClass('hl').filter(function () {
52
+ return $(this).text() * 1 === col_max;
53
+ }).addClass('b');
54
+
55
+ // inner cells
56
+
57
+ inner_cells = $(table).find('td:not(.hl)');
58
+
59
+ table_max = inner_cells.filter(function () {
60
+ return ($(this).text() * 1).toString() === $(this).text();
61
+ }).map(function () {
62
+ return $(this).text() * 1;
63
+ }).get().max();
64
+
65
+ inner_cells.filter(function () {
66
+ return $(this).text() * 1 === table_max;
67
+ }).addClass('hl b');
68
+ },
69
+ };
70
+
71
+ $(document).ready(function () {
72
+ GCStats.highlight_table($('#finds_by_day_of_week'));
73
+ GCStats.highlight_table($('#finds_by_year'));
74
+ GCStats.highlight_table($('#finds_by_date'));
75
+ GCStats.highlight_square_table($('#difficulty_terrain_combinations'));
76
+ GCStats.highlight_square_table($('#month_day_combinations'));
77
+ });
78
+ }
@@ -0,0 +1,213 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
4
+ <head>
5
+ <title><%= geocacher_name %>'s Geocaching Stats</title>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
7
+ <link type="text/css" rel="stylesheet" href="gcstats.css" media="screen" />
8
+ <style type="text/css">/* %css% */</style>
9
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
10
+ <script type="text/javascript" src="gcstats.js"></script>
11
+ <script type="text/javascript">/* %js% */</script>
12
+ </head>
13
+ <body>
14
+
15
+ <%# {{{ cache finds %>
16
+ <table>
17
+ <caption>Cache Finds</caption>
18
+ <tr>
19
+ <th class="l">Geocacher</th>
20
+ <td class="hl b"><%= geocacher_name %></td>
21
+ </tr>
22
+ <tr>
23
+ <th class="l">Total</th>
24
+ <td><%= total_finds %></td>
25
+ </tr>
26
+ <tr>
27
+ <th class="l">Archived</th>
28
+ <td><%= total_archived %></td>
29
+ </tr>
30
+ <tr>
31
+ <th class="l">Caches/Day</th>
32
+ <td><%= finds_per_day %></td>
33
+ </tr>
34
+ <tr>
35
+ <th class="l">Days cached</th>
36
+ <td><%= finds_by_date.keys.size %></td>
37
+ </tr>
38
+ <tr>
39
+ <th class="l">Most in one day</th>
40
+ <td><%= '%d (%s)' % [most_finds_in_a_day, most_finds_in_a_day_dates.join(', ')] %></td>
41
+ </tr>
42
+ </table>
43
+ <%# }}} %>
44
+
45
+ <%# {{{ finds by day of week %>
46
+ <table id="finds_by_day_of_week">
47
+ <caption>Finds by Day of Week</caption>
48
+ <tr>
49
+ <th>Day</th>
50
+ <th>Finds</th>
51
+ <th>Percentage</th>
52
+ </tr>
53
+ <% finds_by_day_of_week.each_with_index {|finds, i| %>
54
+ <tr>
55
+ <td><%= DAY_NAME[i] %></td>
56
+ <td class="finds r"><%= finds %></td>
57
+ <td class="r"><%= '%.1f' % (finds / total_finds.to_f * 100) %>%</td>
58
+ </tr>
59
+ <% } %>
60
+ </table>
61
+ <%# }}} %>
62
+
63
+ <%# {{{ finds by year %>
64
+ <table id="finds_by_year">
65
+ <caption>Finds by Year</caption>
66
+ <tr>
67
+ <th>Year</th>
68
+ <th>Finds</th>
69
+ <th>Percentage</th>
70
+ </tr>
71
+ <% finds_by_year.keys.sort.reverse.each {|year| %>
72
+ <tr>
73
+ <td><%= year %></td>
74
+ <td class="finds r"><%= finds = finds_by_year[year] %></td>
75
+ <td class="r"><%= '%.1f' % (finds / total_finds.to_f * 100) %>%</td>
76
+ </tr>
77
+ <% } %>
78
+ </table>
79
+ <%# }}} %>
80
+
81
+ <%# {{{ finds by container size %>
82
+ <table>
83
+ <caption>Finds by Container Size</caption>
84
+ <tr>
85
+ <th>Size</th>
86
+ <th>Finds</th>
87
+ <th>Percentage</th>
88
+ </tr>
89
+ <% finds_by_size.each {|size, finds| %>
90
+ <tr>
91
+ <td><%= size %></td>
92
+ <td class="r"><%= finds %></td>
93
+ <td class="r"><%= '%.1f' % (finds / total_finds.to_f * 100) %>%</td>
94
+ </tr>
95
+ <% } %>
96
+ </table>
97
+ <%# }}} %>
98
+
99
+ <%# {{{ finds by cache type %>
100
+ <table>
101
+ <caption>Finds by Cache Type</caption>
102
+ <tr>
103
+ <th>Type</th>
104
+ <th>Finds</th>
105
+ <th>Percentage</th>
106
+ </tr>
107
+ <% finds_by_type.each {|type, finds| %>
108
+ <tr>
109
+ <td><%= type %></td>
110
+ <td class="r"><%= finds %></td>
111
+ <td class="r"><%= '%.1f' % (finds / total_finds.to_f * 100) %>%</td>
112
+ </tr>
113
+ <% } %>
114
+ </table>
115
+ <%# }}} %>
116
+
117
+ <%# {{{ difficulty/terrain combinations %>
118
+ <table id="difficulty_terrain_combinations" class="c">
119
+ <caption>Difficulty/Terrain Combinations</caption>
120
+ <tr>
121
+ <th>D/T</th>
122
+ <% (0..8).each {|i| %>
123
+ <th style="width: 35px"><%= ((i + 2) / 2.0).to_s.sub('.0', '') %></th>
124
+ <% } %>
125
+ <th>Total</th>
126
+ </tr>
127
+ <% (0..8).each {|i| %>
128
+ <tr>
129
+ <th><%= ((i + 2) / 2.0).to_s.sub('.0', '') %></th>
130
+ <% (0..8).each {|j| %>
131
+ <td><%= (n = difficulty_terrain_combinations[i][j]) == 0 ? '' : n %></td>
132
+ <% } %>
133
+ <td class="total"><%= difficulty_terrain_combinations[i].sum %></td>
134
+ </tr>
135
+ <% } %>
136
+ <tr class="total">
137
+ <th>Total</th>
138
+ <% (0..8).each {|i| %>
139
+ <td><%= difficulty_terrain_combinations.map {|e| e[i] }.sum %></td>
140
+ <% } %>
141
+ <td><%= total_finds %></td>
142
+ </tr>
143
+ </table>
144
+ <%# }}} %>
145
+
146
+ <%# {{{ month/day combinations %>
147
+ <table id="month_day_combinations" class="c">
148
+ <caption>Month/Day Combinations</caption>
149
+ <tr>
150
+ <th>M/D</th>
151
+ <% (1..31).each {|i| %>
152
+ <th><%= '%02d' % i %></th>
153
+ <% } %>
154
+ <th>Total</th>
155
+ </tr>
156
+ <% (0..11).each {|i| %>
157
+ <tr>
158
+ <th><%= Date::ABBR_MONTHNAMES[i + 1] %></th>
159
+ <% (0..30).each {|j| %>
160
+ <td><%= (n = month_day_combinations[i][j]) > 0 ? n : (n < 0 ? '-' : '') %></td>
161
+ <% } %>
162
+ <td class="total"><%= month_day_combinations[i].map {|e| [e, 0].max }.sum %></td>
163
+ </tr>
164
+ <% } %>
165
+ <tr class="total">
166
+ <th>Total</th>
167
+ <% (0..30).each {|i| %>
168
+ <td><%= month_day_combinations.map {|e| [e[i], 0].max }.sum %></td>
169
+ <% } %>
170
+ <td><%= total_finds %></td>
171
+ </tr>
172
+ </table>
173
+ <%# }}} %>
174
+
175
+ <%# {{{ finds by country %>
176
+ <table>
177
+ <caption>Finds by Country</caption>
178
+ <tr>
179
+ <th>Country</th>
180
+ <th>Finds</th>
181
+ <th>Percentage</th>
182
+ </tr>
183
+ <% finds_by_country.sort {|x, y| y[1] <=> x[1] }.each {|country, finds| %>
184
+ <tr>
185
+ <td><%= country %></td>
186
+ <td class="r"><%= finds %></td>
187
+ <td class="r"><%= '%.1f' % (finds / total_finds.to_f * 100) %>%</td>
188
+ </tr>
189
+ <% } %>
190
+ </table>
191
+ <%# }}} %>
192
+
193
+ <%# {{{ finds by date %>
194
+ <table id="finds_by_date">
195
+ <caption>Finds by Date</caption>
196
+ <tr>
197
+ <th>Date</th>
198
+ <th>Finds</th>
199
+ <th>Days Passed</th>
200
+ </tr>
201
+ <% finds_dates.each_with_index {|date, i| %>
202
+ <% days_passed = (date - (finds_dates[i + 1] || date)).to_i %>
203
+ <tr>
204
+ <td><%= date %></td>
205
+ <td class="finds r"><%= finds_by_date[date] %></td>
206
+ <td class="r"><%= days_passed == 0 ? '-' : days_passed %></td>
207
+ </tr>
208
+ <% } %>
209
+ </table>
210
+ <%# }}} %>
211
+
212
+ </body>
213
+ </html>
@@ -0,0 +1,187 @@
1
+ class Array
2
+ def sum
3
+ inject {|a, e| a + e }
4
+ end
5
+ end
6
+
7
+ module GCStats
8
+ module Helpers
9
+ LEAP_YEAR_MONTH_DAYS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
10
+ RFC2822_DAY_NAME = %w{Sun Mon Tue Wed Thu Fri Sat} # US, Canada, Japan
11
+ ISO8601_DAY_NAME = %w{Mon Tue Wed Thu Fri Sat Sun} # everywhere else
12
+ DAY_NAME = ISO8601_DAY_NAME
13
+
14
+ def total_finds
15
+ @total_finds ||= @caches.map {|c| c.find_dates.size }.sum
16
+ end
17
+
18
+ def total_archived
19
+ @caches.select {|c| c.archived? }.map {|c| c.find_dates.size }.sum
20
+ end
21
+
22
+ def days_cached
23
+ @days_cached ||= begin
24
+ dates = finds_by_date.keys.sort
25
+ (dates[-1] - dates[0]).to_i
26
+ end
27
+ end
28
+
29
+ def finds_per_day
30
+ sprintf('%.2f', total_finds / days_cached.to_f).to_f
31
+ end
32
+
33
+ def most_finds_in_a_day
34
+ @most_finds_in_a_day ||= finds_by_date.values.max
35
+ end
36
+
37
+ def most_finds_in_a_day_dates
38
+ finds_by_date.select {|d, f| f == most_finds_in_a_day }.map {|d, f| d }.sort
39
+ end
40
+
41
+ def finds_by_date
42
+ @finds_by_date ||= begin
43
+ finds = {}
44
+
45
+ @caches.each {|cache|
46
+ cache.find_dates.each {|find_date|
47
+ begin
48
+ finds[find_date] += 1
49
+ rescue
50
+ finds[find_date] = 1
51
+ end
52
+ }
53
+ }
54
+
55
+ finds
56
+ end
57
+ end
58
+
59
+ def finds_by_day_of_week
60
+ finds = Array.new(7, 0)
61
+
62
+ finds_by_date.each {|date, finds_on_date|
63
+ if DAY_NAME[0] == 'Mon'
64
+ wday = date.cwday - 1
65
+ else
66
+ wday = date.wday
67
+ end
68
+
69
+ begin
70
+ finds[wday] += finds_on_date
71
+ rescue
72
+ finds[wday] = finds_on_date
73
+ end
74
+ }
75
+
76
+ finds
77
+ end
78
+
79
+ def finds_by_year
80
+ @finds_by_year ||= begin
81
+ finds = {}
82
+
83
+ finds_by_date.each {|date, finds_on_date|
84
+ begin
85
+ finds[date.year] += finds_on_date
86
+ rescue
87
+ finds[date.year] = finds_on_date
88
+ end
89
+ }
90
+
91
+ # fill empty years with 0
92
+ (finds.keys.min + 1...finds.keys.max).each {|year|
93
+ finds[year] ||= 0
94
+ }
95
+
96
+ finds
97
+ end
98
+ end
99
+
100
+ def finds_by_size
101
+ finds = {}
102
+
103
+ @caches.each {|cache|
104
+ begin
105
+ finds[cache.container] += cache.find_dates.size
106
+ rescue
107
+ finds[cache.container] = cache.find_dates.size
108
+ end
109
+ }
110
+
111
+ finds.to_a.sort {|x, y| y[1] <=> x[1] }
112
+ end
113
+
114
+ def finds_by_type
115
+ finds = {}
116
+
117
+ @caches.each {|cache|
118
+ begin
119
+ finds[cache.type] += cache.find_dates.size
120
+ rescue
121
+ finds[cache.type] = cache.find_dates.size
122
+ end
123
+ }
124
+
125
+ finds.to_a.sort {|x, y| y[1] <=> x[1] }
126
+ end
127
+
128
+ def difficulty_terrain_combinations
129
+ @difficulty_terrain_combinations ||= begin
130
+ combinations = Array.new(9) { Array.new(9, 0) } # 9x9 array
131
+
132
+ @caches.each {|cache|
133
+ i = 2 * (cache.difficulty - 1)
134
+ j = 2 * (cache.terrain - 1)
135
+ combinations[i][j] += cache.find_dates.size
136
+ }
137
+
138
+ combinations
139
+ end
140
+ end
141
+
142
+ def month_day_combinations
143
+ @month_day_combinations ||= begin
144
+ combinations = Array.new(12) { Array.new(31, 0) } # 12x31 array
145
+
146
+ finds_by_date.each {|date, finds_on_date|
147
+ i = date.month - 1
148
+ j = date.day - 1
149
+ combinations[i][j] += finds_on_date
150
+ }
151
+
152
+ # mark erroneous combinations (e.g. Feb 30-31)
153
+ LEAP_YEAR_MONTH_DAYS.each_with_index {|days, i|
154
+ (days..30).each {|j|
155
+ combinations[i][j] = -1
156
+ }
157
+ }
158
+
159
+ combinations
160
+ end
161
+ end
162
+
163
+ def finds_by_country
164
+ @finds_by_country ||= begin
165
+ finds = {}
166
+
167
+ @caches.each {|cache|
168
+ begin
169
+ finds[cache.country] += cache.find_dates.size
170
+ rescue
171
+ finds[cache.country] = cache.find_dates.size
172
+ end
173
+ }
174
+
175
+ finds
176
+ end
177
+ end
178
+
179
+ def geocacher_name
180
+ @caches[0].logs[0].finder
181
+ end
182
+
183
+ def finds_dates
184
+ @finds_dates ||= finds_by_date.keys.sort.reverse
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,140 @@
1
+ require 'rexml/document'
2
+ require 'date'
3
+ require 'time'
4
+
5
+ module GCStats
6
+ module Caches
7
+ NS = 'groundspeak'
8
+
9
+ def self.from_xml(xml_data)
10
+ caches = []
11
+ doc = REXML::Document.new(xml_data)
12
+ doc.each_element('/gpx/wpt') {|wpt_node|
13
+ caches << Cache.new(wpt_node)
14
+ }
15
+ caches
16
+ end
17
+
18
+ class Cache
19
+ def initialize(wpt_node)
20
+ @wpt_node = wpt_node
21
+ @cache_node = wpt_node.elements["#{NS}:cache"]
22
+ end
23
+
24
+ def lat
25
+ @lat ||= @wpt_node.attributes['lat'].to_f
26
+ end
27
+
28
+ alias :latitude :lat
29
+
30
+ def lon
31
+ @lon ||= @wpt_node.attributes['lon'].to_f
32
+ end
33
+
34
+ alias :longitude :lon
35
+
36
+ def published
37
+ @published ||= Time.parse(@wpt_node.elements['time'].text)
38
+ end
39
+
40
+ alias :time :published
41
+
42
+ def code
43
+ @code ||= @wpt_node.elements['name'].text
44
+ end
45
+
46
+ def url
47
+ @url ||= @wpt_node.elements['url'].text
48
+ end
49
+
50
+ def available?
51
+ @available ||= @cache_node.attributes['available'].to_s.downcase == 'true'
52
+ end
53
+
54
+ def archived?
55
+ @archived ||= @cache_node.attributes['archived'].to_s.downcase == 'true'
56
+ end
57
+
58
+ def name
59
+ @name ||= @cache_node.elements["#{NS}:name"].text
60
+ end
61
+
62
+ def owner
63
+ @owner ||= @cache_node.elements["#{NS}:placed_by"].text
64
+ end
65
+
66
+ alias :placed_by :owner
67
+
68
+ def type
69
+ @type ||= @cache_node.elements["#{NS}:type"].text
70
+ end
71
+
72
+ def container
73
+ @container ||= @cache_node.elements["#{NS}:container"].text
74
+ end
75
+
76
+ alias :size :container
77
+
78
+ def difficulty
79
+ @difficulty ||= @cache_node.elements["#{NS}:difficulty"].text.to_f
80
+ end
81
+
82
+ def terrain
83
+ @terrain ||= @cache_node.elements["#{NS}:terrain"].text.to_f
84
+ end
85
+
86
+ def country
87
+ @country ||= @cache_node.elements["#{NS}:country"].text
88
+ end
89
+
90
+ def state
91
+ @state ||= @cache_node.elements["#{NS}:state"].text
92
+ end
93
+
94
+ def logs
95
+ @logs ||= begin
96
+ logs = []
97
+
98
+ @cache_node.each_element("#{NS}:logs/#{NS}:log") {|log_node|
99
+ logs << Log.new(log_node)
100
+ }
101
+
102
+ logs
103
+ end
104
+ end
105
+
106
+ def find_dates
107
+ @find_dates ||= begin
108
+ dates = []
109
+
110
+ logs.each {|log|
111
+ if log.type.downcase == 'found it' or
112
+ type.downcase == 'event cache' && log.type.downcase == 'attended'
113
+ dates << log.date
114
+ end
115
+ }
116
+
117
+ dates
118
+ end
119
+ end
120
+ end
121
+
122
+ class Log
123
+ def initialize(log_node)
124
+ @log_node = log_node
125
+ end
126
+
127
+ def date
128
+ @date ||= Date.parse(@log_node.elements["#{NS}:date"].text)
129
+ end
130
+
131
+ def type
132
+ @type ||= @log_node.elements["#{NS}:type"].text
133
+ end
134
+
135
+ def finder
136
+ @finder ||= @log_node.elements["#{NS}:finder"].text
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,62 @@
1
+ require 'gcstats/mapping'
2
+ require 'gcstats/template'
3
+ require 'rubygems'
4
+ require 'zip/zip'
5
+ require 'sinatra'
6
+
7
+ get '/' do
8
+ %{\
9
+ <html>
10
+ <head>
11
+ <title>gcstats</title>
12
+ <style type="text/css">body { font-family: sans-serif; }</style>
13
+ </head>
14
+ <body>
15
+ <form enctype="multipart/form-data" action="/generate_stats" method="post">
16
+ <label for="pq">Pocket Query (.zip or .gpx):</label>
17
+ <input type="file" id="pq" name="pq" />
18
+ <input type="submit" value="Generate Stats" />
19
+ </form>
20
+ <a href="http://github.com/agorf/gcstats"><img
21
+ style="position: absolute; top: 0; right: 0; border: 0;"
22
+ src="http://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png"
23
+ alt="Fork me on GitHub" /></a>
24
+ </body>
25
+ </html>
26
+ }
27
+ end
28
+
29
+ post '/generate_stats' do
30
+ begin
31
+ pq = params['pq']
32
+
33
+ if pq[:type] == 'application/zip'
34
+ data = nil
35
+
36
+ Zip::ZipFile.foreach(pq[:tempfile].path) {|entry|
37
+ if File.extname(entry.name) == '.gpx'
38
+ data = entry.get_input_stream.read
39
+ break
40
+ end
41
+ }
42
+ else
43
+ data = pq[:tempfile].read
44
+ end
45
+
46
+ lib_dir = File.dirname(__FILE__)
47
+
48
+ rhtml = open(File.join(lib_dir, 'gcstats.rhtml')).read
49
+ caches = GCStats::Caches.from_xml(data)
50
+ html = GCStats::Template.new(rhtml, :caches => caches).result
51
+ html.sub!('/* %css% */', "\n" + File.read(File.join(lib_dir, 'gcstats.css')))
52
+ html.sub!('/* %js% */', "\n" + File.read(File.join(lib_dir, 'gcstats.js')))
53
+
54
+ return html
55
+ rescue
56
+ halt 500, '<h1>Internal Server Error</h1>'
57
+ end
58
+ end
59
+
60
+ not_found do
61
+ '<h1>Not Found</h1>'
62
+ end
@@ -0,0 +1,19 @@
1
+ require 'erb'
2
+ require 'gcstats/helpers'
3
+
4
+ module GCStats
5
+ class Template
6
+ include Helpers
7
+
8
+ def initialize(text, data)
9
+ @text = text
10
+ data.each {|n, v| instance_variable_set("@#{n}", v) }
11
+ end
12
+
13
+ def result
14
+ ERB.new(@text).result(binding)
15
+ end
16
+
17
+ alias :to_s :result
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gcstats
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Aggelos Orfanakos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-08-30 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rubyzip
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Simple and clean statistics of your Geocaching activity
26
+ email: agorf@agorf.gr
27
+ executables:
28
+ - gcstats
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README.markdown
35
+ - config.ru
36
+ - bin/gcstats
37
+ - lib/gcstats/gcstats.css
38
+ - lib/gcstats/helpers.rb
39
+ - lib/gcstats/mapping.rb
40
+ - lib/gcstats/gcstats.js
41
+ - lib/gcstats/template.rb
42
+ - lib/gcstats/gcstats.rhtml
43
+ - lib/gcstats/server.rb
44
+ has_rdoc: true
45
+ homepage: http://github.com/agorf/gcstats
46
+ licenses: []
47
+
48
+ post_install_message:
49
+ rdoc_options: []
50
+
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.5
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Simple and clean statistics of your Geocaching activity
72
+ test_files: []
73
+