geostats 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/LICENSE +13 -0
  2. data/README.md +58 -0
  3. data/bin/geostats +33 -0
  4. data/lib/geostats.rb +36 -0
  5. data/lib/geostats/commands/base.rb +73 -0
  6. data/lib/geostats/commands/console.rb +17 -0
  7. data/lib/geostats/commands/generate.rb +21 -0
  8. data/lib/geostats/commands/help.rb +20 -0
  9. data/lib/geostats/commands/init.rb +31 -0
  10. data/lib/geostats/commands/migrate.rb +21 -0
  11. data/lib/geostats/commands/push.rb +41 -0
  12. data/lib/geostats/commands/set.rb +35 -0
  13. data/lib/geostats/commands/update.rb +175 -0
  14. data/lib/geostats/console.rb +2 -0
  15. data/lib/geostats/core_ext.rb +5 -0
  16. data/lib/geostats/database.rb +15 -0
  17. data/lib/geostats/generator.rb +43 -0
  18. data/lib/geostats/grabber/cache.rb +95 -0
  19. data/lib/geostats/grabber/log.rb +27 -0
  20. data/lib/geostats/grabber/logs.rb +37 -0
  21. data/lib/geostats/grabber/user.rb +15 -0
  22. data/lib/geostats/http.rb +97 -0
  23. data/lib/geostats/migrations/001_create_caches.rb +27 -0
  24. data/lib/geostats/migrations/002_create_cache_types.rb +16 -0
  25. data/lib/geostats/migrations/003_create_logs.rb +13 -0
  26. data/lib/geostats/migrations/004_create_log_types.rb +15 -0
  27. data/lib/geostats/migrations/005_create_settings.rb +9 -0
  28. data/lib/geostats/migrations/006_add_cito_cache_type.rb +5 -0
  29. data/lib/geostats/models/cache.rb +47 -0
  30. data/lib/geostats/models/cache_type.rb +9 -0
  31. data/lib/geostats/models/log.rb +33 -0
  32. data/lib/geostats/models/log_type.rb +9 -0
  33. data/lib/geostats/models/setting.rb +16 -0
  34. data/lib/geostats/stats.rb +220 -0
  35. data/lib/geostats/templates/default.mustache +10 -0
  36. data/lib/geostats/templates/difficulty_terrain_matrix.mustache +40 -0
  37. data/lib/geostats/templates/founds_by_cache_type.mustache +17 -0
  38. data/lib/geostats/templates/milestones.mustache +19 -0
  39. data/lib/geostats/templates/monthly_founds.mustache +18 -0
  40. data/lib/geostats/templates/stylesheet.css +160 -0
  41. data/lib/geostats/utils.rb +42 -0
  42. data/lib/geostats/version.rb +3 -0
  43. data/lib/geostats/views/base.rb +19 -0
  44. data/lib/geostats/views/default.rb +13 -0
  45. data/lib/geostats/views/difficulty_terrain_matrix.rb +34 -0
  46. data/lib/geostats/views/founds_by_cache_type.rb +24 -0
  47. data/lib/geostats/views/milestones.rb +23 -0
  48. data/lib/geostats/views/monthly_founds.rb +24 -0
  49. metadata +165 -0
@@ -0,0 +1,2 @@
1
+ include Geostats
2
+ Database.connect!(ENV["GEOSTATS_DIR"])
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def avg
3
+ length.zero? ? 0 : sum.to_f / length.to_f
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module Geostats
2
+ class Database
3
+ def self.connect!(directory=nil)
4
+ directory ||= File.expand_path("~/.geostats")
5
+
6
+ file = File.join(directory, "database.sqlite3")
7
+ raise "file does not exist: #{file}" unless File.exists?(file)
8
+
9
+ ActiveRecord::Base.establish_connection(
10
+ :adapter => "sqlite3",
11
+ :database => file
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ require "hpricot"
2
+ require "css_parser"
3
+
4
+ require "geostats/views/base"
5
+ require "geostats/views/default"
6
+
7
+ module Geostats
8
+ class Generator
9
+ PARTS = %w(difficulty_terrain_matrix milestones monthly_founds
10
+ founds_by_cache_type)
11
+
12
+ def self.generate
13
+ new.generate
14
+ end
15
+
16
+ def initialize
17
+ @view = Views::Default.new
18
+ @css = CssParser::Parser.new
19
+
20
+ file = File.join(File.dirname(__FILE__), "templates", "stylesheet.css")
21
+ @css.add_block!(File.read(file))
22
+ end
23
+
24
+ def generate
25
+ PARTS.each do |part|
26
+ require "geostats/views/#{part}"
27
+ @view[part.to_sym] = "Geostats::Views::#{part.camelcase}".constantize.render
28
+ end
29
+
30
+ @doc = Hpricot(@view.render)
31
+
32
+ @css.each_selector do |selector, style|
33
+ @doc.search(selector) do |el|
34
+ if el.elem?
35
+ el["style"] = "#{el.attributes['style']} #{style}".strip
36
+ end
37
+ end
38
+ end
39
+
40
+ @doc.to_html
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,95 @@
1
+ module Geostats
2
+ module Grabber
3
+ class Cache
4
+ def initialize(uuid)
5
+ @resp, @data = HTTP.get("/seek/cache_details.aspx?guid=#{uuid}&log=y")
6
+ end
7
+
8
+ def code
9
+ if @data =~ /<div id="ctl00_cacheCodeWidget".*?>\s*<p>\s*([A-Z0-9]+)<\/p>\s*<\/div>/
10
+ $1
11
+ end
12
+ end
13
+
14
+ def name
15
+ if @data =~ /<span id="ctl00_ContentBody_CacheName">(.*?)<\/span>/
16
+ name = Utils.unescape($1)
17
+ name =~ /^<strike>(.*)<\/strike>$/ ? $1 : name
18
+ end
19
+ end
20
+
21
+ def type
22
+ if @data =~ /<h2.*?WptTypes\/(\d+)\.gif".*?h2>/
23
+ $1.to_i
24
+ end
25
+ end
26
+
27
+ def hidden_at
28
+ if @data =~ /<span id="ctl00_ContentBody_DateHidden">(\d{1,2})\/(\d{1,2})\/(\d{4})<\/span>/
29
+ Time.parse([$2, $1, $3].join("."))
30
+ end
31
+ end
32
+
33
+ def size
34
+ if @data =~ /<img src="\/images\/icons\/container\/(.*?)\.gif" alt="Size: .*?" \/>/
35
+ $1 if %w(micro small regular large other not_chosen).include?($1)
36
+ end
37
+ end
38
+
39
+ def owner
40
+ if @data =~ /<span id="ctl00_ContentBody_CacheOwner">by.*?\/profile\/\?guid=(.*?)&wid=.*<\/a><\/span>/
41
+ user = Grabber::User.new($1)
42
+ if name = user.username
43
+ name
44
+ end
45
+ end
46
+ end
47
+
48
+ def difficulty
49
+ if @data =~ /<span id="ctl00_ContentBody_Difficulty"><img src=".*?" alt="([0-9\.]+) out of 5" \/><\/span>/
50
+ $1.to_f
51
+ end
52
+ end
53
+
54
+ def terrain
55
+ if @data =~ /<span id="ctl00_ContentBody_Terrain"><img src=".*?" alt="([0-9\.]+) out of 5" \/><\/span>/
56
+ $1.to_f
57
+ end
58
+ end
59
+
60
+ def latitude
61
+ if @data =~ /\/wpt\/\?lat=(.*)&amp;lon=(.*)&amp;detail=1/
62
+ $1.to_f
63
+ end
64
+ end
65
+
66
+ def longitude
67
+ if @data =~ /\/wpt\/\?lat=(.*)&amp;lon=(.*)&amp;detail=1/
68
+ $2.to_f
69
+ end
70
+ end
71
+
72
+ def location
73
+ if @data =~ /<span id="ctl00_ContentBody_Location">In (.*?)<\/span>/
74
+ $1
75
+ end
76
+ end
77
+
78
+ def founds_count
79
+ @data.scan(/<strong><img src=".*?icon_smile\.gif" alt="" \/>&nbsp;/).length
80
+ end
81
+
82
+ def dnf_count
83
+ @data.scan(/<strong><img src=".*?icon_sad\.gif" alt="" \/>&nbsp;/).length
84
+ end
85
+
86
+ def archived?
87
+ !!(@data =~ /<li>This cache has been archived/)
88
+ end
89
+
90
+ def pmonly?
91
+ !!(@data =~ /<p class="Warning">Sorry, the owner of this listing has made it viewable to Premium Members only./)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ module Geostats
2
+ module Grabber
3
+ class Log
4
+ def initialize(uuid)
5
+ @resp, @data = HTTP.get("/seek/log.aspx?LUID=#{uuid}")
6
+ end
7
+
8
+ def icon
9
+ if @data =~ /<img id="ctl00_ContentBody_LogBookPanel1_LogImage".*?src="\/images\/icons\/(.*?).gif" .*? \/>/
10
+ $1
11
+ end
12
+ end
13
+
14
+ def message
15
+ if @data =~ /<p><span id="ctl00_ContentBody_LogBookPanel1_LogText">(.*)<\/span><\/p>/
16
+ Utils.replace_smilie_img_tags(Utils.unescape($1))
17
+ end
18
+ end
19
+
20
+ def logged_at
21
+ if @data =~ /<span id="ctl00_ContentBody_LogBookPanel1_LogDate">(.*?)<\/span>/
22
+ Time.parse($1) rescue nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ module Geostats
2
+ module Grabber
3
+ class Logs
4
+ class Log < Struct.new(:icon, :logged_at, :cache_uuid,
5
+ :cache_name, :location, :log_uuid)
6
+ end
7
+
8
+ REGEX = %r{<tr.*?>\s*<td><img src="/images/icons/([a-z_]+)\.gif" width="16" height="16" alt=".*?" /></td>\s*<td>([0-9\/]+)</td>\s*<td><a href="http://www\.geocaching\.com/seek/cache_details\.aspx\?guid=([a-f0-9\-]+)">(.*?)</a>&nbsp;</td>\s*<td>(.*?) &nbsp;</td>\s*<td><a href="http://www\.geocaching\.com/seek/log\.aspx\?LUID=([a-f0-9\-]+)" target="_blank" title="Visit Log">Visit Log</a></td>\s*</tr>}
9
+
10
+ def initialize
11
+ @resp, @data = HTTP.get("/my/logs.aspx?s=1")
12
+ end
13
+
14
+ def logs
15
+ @data.scan(REGEX).map do |match|
16
+ log = Log.new
17
+
18
+ log.icon = match[0]
19
+ log.cache_uuid = match[2]
20
+ log.cache_name = Utils.unescape(match[3])
21
+ log.location = Utils.unescape(match[4])
22
+ log.log_uuid = match[5]
23
+ log.logged_at = match[1]
24
+
25
+ log.cache_name = Utils.unescape($1) if log.cache_name =~ /^<span class="Strike Warning">(.*)<\/span>$/
26
+ log.cache_name = Utils.unescape($1) if log.cache_name =~ /^<strike>(.*)<\/strike>$/
27
+
28
+ if log.logged_at =~ /^(\d+)\/(\d+)\/(\d+)$/
29
+ log.logged_at = Time.parse([$2, $1, $3].join("."))
30
+ end
31
+
32
+ log
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ module Geostats
2
+ module Grabber
3
+ class User
4
+ def initialize(uuid)
5
+ @resp, @data = HTTP.get("/profile/?guid=#{uuid}")
6
+ end
7
+
8
+ def username
9
+ if @data =~ /<h2><span id="ctl00_ContentBody_lblUserProfile".*?>Profile for User: (.*?)<\/span><\/h2>/
10
+ Utils.unescape($1)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,97 @@
1
+ require "net/http"
2
+
3
+ module Geostats
4
+ class HTTP
5
+ attr_writer :username, :password
6
+
7
+ def self.instance
8
+ @instance ||= new
9
+ end
10
+
11
+ def self.login(username, password)
12
+ self.instance.username = username
13
+ self.instance.password = password
14
+ self.instance.login
15
+ end
16
+
17
+ def self.logout
18
+ self.instance.logout
19
+ end
20
+
21
+ def self.get(path)
22
+ self.instance.get(path)
23
+ end
24
+
25
+ def self.post(path, data={})
26
+ self.instance.post(path, data)
27
+ end
28
+
29
+ def login
30
+ raise ArgumentError, "Missing username" unless @username
31
+ raise ArgumentError, "Missing password" unless @password
32
+
33
+ resp, data = post("/login/default.aspx", {
34
+ "ctl00$ContentBody$myUsername" => @username,
35
+ "ctl00$ContentBody$myPassword" => @password,
36
+ "ctl00$ContentBody$Button1" => "Login",
37
+ "ctl00$ContentBody$cookie" => "on"
38
+ })
39
+
40
+ @cookie = resp.response["set-cookie"]
41
+ end
42
+
43
+ def logout
44
+ get("/login/default.aspx?RESET=Y")
45
+ @cookie = nil
46
+ end
47
+
48
+ def get(path)
49
+ header = default_header
50
+ header["Cookie"] = @cookie if @cookie
51
+
52
+ http.get(path, header)
53
+ end
54
+
55
+ def post(path, data={})
56
+ metadata(path).each do |key, value|
57
+ data[key] = value
58
+ end
59
+
60
+ header = default_header
61
+ header["Cookie"] = @cookie if @cookie
62
+
63
+ http.post(path, data.map { |k,v| "#{k}=#{v}" }.join("&"), header)
64
+ end
65
+
66
+ protected
67
+
68
+ def metadata(path)
69
+ resp, data = get(path)
70
+ meta = {}
71
+
72
+ data.scan(/<input type="hidden" name="__([A-Z]+)" id="__[A-Z]+" value="(.*?)" \/>/).each do |match|
73
+ meta["__#{match[0]}"] = CGI.escape(match[1])
74
+ end
75
+
76
+ meta
77
+ end
78
+
79
+ def user_agent
80
+ "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729)"
81
+ end
82
+
83
+ def default_header
84
+ {
85
+ "User-Agent" => user_agent
86
+ }
87
+ end
88
+
89
+ def http
90
+ @http ||= begin
91
+ @http = Net::HTTP.new("www.geocaching.com")
92
+ @http.open_timeout = 5
93
+ @http
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,27 @@
1
+ class CreateCaches < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :caches do |t|
4
+ t.integer :cache_type_id
5
+ t.string :uuid
6
+ t.string :code
7
+ t.string :name
8
+ t.float :latitude
9
+ t.float :longitude
10
+ t.float :difficulty
11
+ t.float :terrain
12
+ t.string :owner
13
+ t.string :size
14
+ t.string :location
15
+ t.datetime :hidden_at
16
+ t.boolean :is_pmonly
17
+ t.boolean :is_archived
18
+ t.integer :gcvote_votes
19
+ t.float :gcvote_average
20
+ t.float :gcvote_median
21
+ t.integer :founds_count
22
+ t.integer :dnf_count
23
+ t.datetime :synced_at
24
+ t.timestamps
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ class CreateCacheTypes < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :cache_types do |t|
4
+ t.string :name
5
+ end
6
+
7
+ cache_types = ["Traditional Cache", "Multi-Cache", "Mystery Cache",
8
+ "Letterbox Hybrid", "Wherigo Cache", "EarthCache", "Virtual Cache",
9
+ "Webcam Cache", "Reverse Cache", "Event Cache", "Mega-Event Cache",
10
+ "10 Years! Event Cache"]
11
+
12
+ cache_types.each do |cache_type|
13
+ Geostats::CacheType.create! :name => cache_type
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ class CreateLogs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :logs do |t|
4
+ t.integer :cache_id
5
+ t.integer :log_type_id
6
+ t.string :uuid
7
+ t.text :message
8
+ t.datetime :logged_at
9
+ t.datetime :synced_at
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class CreateLogTypes < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :log_types do |t|
4
+ t.string :name
5
+ end
6
+
7
+ log_types = ["Found it", "Didn't find it", "Note", "Needs archived",
8
+ "Needs maintenance", "Archive", "Enable Listing","Temporarily Disable Listing",
9
+ "Update Coordinates", "Owner Maintenance", "Will attend", "Attended"]
10
+
11
+ log_types.each do |log_type|
12
+ Geostats::LogType.create! :name => log_type
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ class CreateSettings < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :settings do |t|
4
+ t.string :key
5
+ t.string :value
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddCitoCacheType < ActiveRecord::Migration
2
+ def self.up
3
+ Geostats::CacheType.create! :name => "Cache In Trash Out Event"
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ module Geostats
2
+ class Cache < ActiveRecord::Base
3
+ belongs_to :cache_type, :class_name => "Geostats::CacheType"
4
+ has_many :logs, :class_name => "Geostats::Log"
5
+
6
+ scope :found_or_attended, lambda { where("logs.log_type_id IN (?)", [1, 12]) }
7
+
8
+ validates :uuid, :presence => true, :uniqueness => true
9
+ validates :code, :presence => true, :uniqueness => true
10
+ validates :name, :presence => true
11
+ validates :longitude, :presence => true, :numericality => true
12
+ validates :latitude, :presence => true, :numericality => true
13
+ validates :owner, :presence => true
14
+ validates :hidden_at, :presence => true
15
+ validates :size, :presence => true
16
+ validates :location, :presence => true
17
+ validates :founds_count, :presence => true, :numericality => true
18
+ validates :dnf_count, :presence => true, :numericality => true
19
+
20
+ def self.create_from_website(uuid)
21
+ cache = Cache.new(:uuid => uuid)
22
+ cache.update_from_website
23
+ cache
24
+ end
25
+
26
+ def update_from_website
27
+ info = Grabber::Cache.new(self.uuid)
28
+
29
+ [:name, :code, :size, :latitude, :longitude, :difficulty, :terrain,
30
+ :location, :owner, :hidden_at, :founds_count, :dnf_count
31
+ ].each do |attribute|
32
+ if value = info.send(attribute)
33
+ self.attributes = { attribute => value }
34
+ end
35
+ end
36
+
37
+ self.is_pmonly = info.pmonly?
38
+ self.is_archived = info.archived?
39
+
40
+ if type = info.type and index = CacheType::MAPPING.index(type)
41
+ self.cache_type = CacheType.where(:id => index + 1).first
42
+ end
43
+
44
+ self.synced_at = Time.now
45
+ end
46
+ end
47
+ end