gcstats 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +50 -0
- data/bin/gcstats +38 -0
- data/config.ru +3 -0
- data/lib/gcstats/gcstats.css +45 -0
- data/lib/gcstats/gcstats.js +78 -0
- data/lib/gcstats/gcstats.rhtml +213 -0
- data/lib/gcstats/helpers.rb +187 -0
- data/lib/gcstats/mapping.rb +140 -0
- data/lib/gcstats/server.rb +62 -0
- data/lib/gcstats/template.rb +19 -0
- metadata +73 -0
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,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
|
+
|