geocaching 0.6.1 → 0.7.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.
- data/README.markdown +11 -57
- data/lib/geocaching.rb +13 -86
- data/lib/geocaching/cache.rb +79 -557
- data/lib/geocaching/cache_type.rb +39 -78
- data/lib/geocaching/container_type.rb +45 -0
- data/lib/geocaching/errors.rb +7 -0
- data/lib/geocaching/log.rb +49 -180
- data/lib/geocaching/log_type.rb +51 -81
- data/lib/geocaching/parsers/cache_gpx.rb +42 -0
- data/lib/geocaching/parsers/cache_gpx_additional_waypoint.rb +100 -0
- data/lib/geocaching/parsers/cache_gpx_cache_waypoint.rb +234 -0
- data/lib/geocaching/parsers/cache_simple.rb +219 -0
- data/lib/geocaching/parsers/log.rb +100 -0
- data/lib/geocaching/parsers/trackable.rb +107 -0
- data/lib/geocaching/session.rb +119 -0
- data/lib/geocaching/session/caches.rb +61 -0
- data/lib/geocaching/session/logs.rb +17 -0
- data/lib/geocaching/session/trackables.rb +26 -0
- data/lib/geocaching/trackable.rb +77 -0
- data/lib/geocaching/trackable_type.rb +17 -0
- data/lib/geocaching/user.rb +10 -202
- data/lib/geocaching/waypoint.rb +51 -0
- data/lib/geocaching/waypoint_type.rb +41 -0
- metadata +21 -25
- data/lib/geocaching/http.rb +0 -248
- data/lib/geocaching/my_logs.rb +0 -147
- data/lib/geocaching/version.rb +0 -5
- data/lib/geocaching/watchlist.rb +0 -93
data/README.markdown
CHANGED
@@ -1,57 +1,11 @@
|
|
1
|
-
Ruby API for
|
1
|
+
Ruby API for Geocaching.com
|
2
2
|
===========================
|
3
3
|
|
4
|
-
|
4
|
+
**ruby-geocaching** is a Ruby library providing an API for Geocaching.com.
|
5
|
+
It doesn’t scrape their website but rather uses the same API the
|
6
|
+
iPhone and Android geocaching apps use.
|
5
7
|
|
6
|
-
|
7
|
-
Usage
|
8
|
-
-----
|
9
|
-
|
10
|
-
require "geocaching"
|
11
|
-
|
12
|
-
# Logging in is not always necessary, but some information are only
|
13
|
-
# accessible when logged in.
|
14
|
-
Geocaching::HTTP.login("username", "password")
|
15
|
-
|
16
|
-
# Fetch the information for a cache by the cache’s GC code. You can also
|
17
|
-
# provide the cache’s GUID instead of the GC code.
|
18
|
-
cache = Geocaching::Cache.fetch(:code => "...")
|
19
|
-
|
20
|
-
# Print some cache information.
|
21
|
-
puts " Name: #{cache.name}"
|
22
|
-
puts "Difficulty: #{cache.difficulty}"
|
23
|
-
puts " Owner: #{cache.owner.username}"
|
24
|
-
|
25
|
-
# Print the number of logs.
|
26
|
-
puts " Logs: #{cache.logs.size}"
|
27
|
-
|
28
|
-
# Print the number of users that didn’t find the cache.
|
29
|
-
dnfs = cache.logs.select { |log| log.type == :dnf }.size
|
30
|
-
puts " DNFs: #{dnfs}"
|
31
|
-
|
32
|
-
# Fetch the information for a log by its GUID.
|
33
|
-
log = Geocaching::Log.fetch(:guid => "...")
|
34
|
-
|
35
|
-
# Print some log information.
|
36
|
-
puts "Username: #{log.user.name}"
|
37
|
-
puts " Words: #{log.message.split.size}"
|
38
|
-
puts " Cache: #{log.cache.name}"
|
39
|
-
|
40
|
-
Geocaching::HTTP.logout
|
41
|
-
|
42
|
-
The whole library may raise the following exceptions:
|
43
|
-
|
44
|
-
* `Geocaching::TimeoutError` when a timeout is hit.
|
45
|
-
* `Geocaching::LoginError` when calling a method that requires being
|
46
|
-
logged in and you’re not.
|
47
|
-
* `Geocaching::NotFetchedError` when accessing a method that requires the
|
48
|
-
`fetch` method to be called first.
|
49
|
-
* `Geocaching::ExtractError` when information could not be extracted
|
50
|
-
out of the website’s HTML code. This mostly happens after Groundspeak
|
51
|
-
changed their website.
|
52
|
-
* `Geocaching::HTTPError` when a HTTP request failed.
|
53
|
-
|
54
|
-
All exceptions are subclasses of `Geocaching::Error`.
|
8
|
+
See the [website](https://nano.github.com/ruby-geocaching) for more information.
|
55
9
|
|
56
10
|
|
57
11
|
Tests
|
@@ -60,11 +14,11 @@ Tests
|
|
60
14
|
Tests are written using [RSpec](http://relishapp.com/rspec).
|
61
15
|
You need [Bundler](http://gembundler.com/) to run the tests.
|
62
16
|
|
63
|
-
$ bundle
|
64
|
-
$
|
17
|
+
$ bundle
|
18
|
+
$ bundle exec rake test USERNAME=username PASSWORD=password
|
19
|
+
|
65
20
|
|
66
|
-
|
21
|
+
License
|
22
|
+
-------
|
67
23
|
|
68
|
-
|
69
|
-
* `GC_CACHE_TYPES` — A space-separated list of cache types you want to test.
|
70
|
-
* `GC_LOG_TYPES` — A space-separated list of log types you want to test.
|
24
|
+
**ruby-geocaching** is released under the terms of the MIT license.
|
data/lib/geocaching.rb
CHANGED
@@ -1,91 +1,18 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "geocaching/cache"
|
4
|
+
require "geocaching/cache_type"
|
5
|
+
require "geocaching/container_type"
|
6
|
+
require "geocaching/errors"
|
7
|
+
require "geocaching/log"
|
8
|
+
require "geocaching/log_type"
|
9
|
+
require "geocaching/session"
|
10
|
+
require "geocaching/user"
|
11
|
+
require "geocaching/trackable"
|
12
|
+
require "geocaching/trackable_type"
|
13
|
+
require "geocaching/waypoint"
|
14
|
+
require "geocaching/waypoint_type"
|
4
15
|
|
5
|
-
# This is a Ruby library to access information on geocaching.com. As
|
6
|
-
# Groundspeak does not provide a public API yet, one needs to parse the
|
7
|
-
# website content. That’s what this library does.
|
8
|
-
#
|
9
|
-
# The whole library may raise the following exceptions:
|
10
|
-
#
|
11
|
-
# * {Geocaching::LoginError}
|
12
|
-
# * {Geocaching::NotFetchedError}
|
13
|
-
# * {Geocaching::ParseError}
|
14
|
-
# * {Geocaching::TimeoutError}
|
15
|
-
# * {Geocaching::HTTPError}
|
16
|
-
#
|
17
|
-
# == Usage
|
18
|
-
#
|
19
|
-
# To have access to all information, you will need to provide the
|
20
|
-
# credentials for an account on geocaching.com and log in:
|
21
|
-
#
|
22
|
-
# Geocaching::HTTP.login("username", "password")
|
23
|
-
#
|
24
|
-
# Make sure to log out when you’re done:
|
25
|
-
#
|
26
|
-
# Geocaching::HTTP.logout
|
27
|
-
#
|
28
|
-
# You know have access to all information that are available for the
|
29
|
-
# account you provided.
|
30
|
-
#
|
31
|
-
# cache = Geocaching::Cache.fetch(:code => "GCTEST")
|
32
|
-
# p cache.difficulty #=> 3.5
|
33
|
-
# p cache.latitude #=> 49.17518
|
34
|
-
# p cache.pmonly? #=> false
|
35
|
-
# p cache.logs.size #=> 41
|
36
|
-
#
|
37
|
-
# log = Geocaching::Log.new(:guid => "07208985-f7b2-456a-b0a8-bbc26f28b5a9")
|
38
|
-
# log.fetch
|
39
|
-
# p log.cache #=> #<Geocaching::Cache:...>
|
40
|
-
# p log.username #=> Foobar
|
41
|
-
#
|
42
16
|
module Geocaching
|
43
|
-
|
44
|
-
# easily allows to rescue from all exceptions by catching Geocaching::Error.
|
45
|
-
class Error < Exception
|
46
|
-
end
|
47
|
-
|
48
|
-
# This exception is raised when a method that requires being
|
49
|
-
# logged in is called when not logged in.
|
50
|
-
class LoginError < Error
|
51
|
-
end
|
52
|
-
|
53
|
-
# This exception is raised when a timeout is hit.
|
54
|
-
class TimeoutError < Error
|
55
|
-
end
|
56
|
-
|
57
|
-
# This exception is raised if a request exceeds 500 results.
|
58
|
-
class TooManyResultsError < Error
|
59
|
-
def initialize
|
60
|
-
super "Your request exceeded 500 results"
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# This exception is raised when a method is called that requires
|
65
|
-
# the #fetch method to be called first.
|
66
|
-
class NotFetchedError < Error
|
67
|
-
def initialize
|
68
|
-
super "Need to call the #fetch method first"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# This exception is raised when information could not be
|
73
|
-
# extracted out of the website’s HTML code. For example,
|
74
|
-
# this may happen if Groundspeak changed their website.
|
75
|
-
class ParseError < Error
|
76
|
-
end
|
77
|
-
|
78
|
-
# This exception is raised when a HTTP request failed.
|
79
|
-
class HTTPError < Error
|
80
|
-
end
|
81
|
-
|
82
|
-
autoload :Cache, "geocaching/cache"
|
83
|
-
autoload :CacheType, "geocaching/cache_type"
|
84
|
-
autoload :HTTP, "geocaching/http"
|
85
|
-
autoload :Log, "geocaching/log"
|
86
|
-
autoload :LogType, "geocaching/log_type"
|
87
|
-
autoload :MyLogs, "geocaching/my_logs"
|
88
|
-
autoload :User, "geocaching/user"
|
89
|
-
autoload :VERSION, "geocaching/version"
|
90
|
-
autoload :Watchlist, "geocaching/watchlist"
|
17
|
+
VERSION = "0.7.0"
|
91
18
|
end
|
data/lib/geocaching/cache.rb
CHANGED
@@ -1,590 +1,112 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "
|
3
|
+
require "geocaching/parsers/cache_simple"
|
4
|
+
require "geocaching/parsers/cache_gpx"
|
5
5
|
|
6
6
|
module Geocaching
|
7
|
-
# The {LogsArray} class is a subclass of +Array+ and is used to store all
|
8
|
-
# logs that belong to a cache. It implements the {#fetch_all} method to
|
9
|
-
# fetch the information of all logs inside the array.
|
10
|
-
class LogsArray < Array
|
11
|
-
# Calls {Geocaching::Log#fetch} for each log inside the array.
|
12
|
-
#
|
13
|
-
# @return [Boolean] Whether all logs could be fetched successfully
|
14
|
-
def fetch_all
|
15
|
-
each { |log| log.fetch }
|
16
|
-
map { |log| log.fetched? }.all?
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# The {Cache} class represents a cache on geocaching.com. Altough some
|
21
|
-
# information are available without being logged in, most information
|
22
|
-
# will only be accessible after a successful login.
|
23
|
-
#
|
24
|
-
# == Usage
|
25
|
-
#
|
26
|
-
# cache = Geocaching::Cache.fetch(:code => "GCTEST")
|
27
|
-
#
|
28
|
-
# puts cache.difficulty #=> 3.5
|
29
|
-
# puts cache.latitude #=> 49.741541
|
30
|
-
# puts cache.archived? #=> false
|
31
|
-
#
|
32
7
|
class Cache
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
unless northeast.kind_of?(Array) and southwest.kind_of?(Array)
|
62
|
-
raise ArgumentError, "Arguments must be arrays"
|
63
|
-
end
|
64
|
-
|
65
|
-
unless northeast.size == 2 and southwest.size == 2
|
66
|
-
raise ArgumentError, "Arguments must have two elements"
|
67
|
-
end
|
68
|
-
|
69
|
-
unless northeast.map { |a| a.kind_of?(Numeric) }.all? \
|
70
|
-
and southwest.map { |a| a.kind_of?(Numeric) }.all?
|
71
|
-
raise ArgumentError, "Latitude and longitude must be given as numbers"
|
72
|
-
end
|
73
|
-
|
74
|
-
post_data = {
|
75
|
-
"dto" => {
|
76
|
-
"data" => {
|
77
|
-
"c" => 1,
|
78
|
-
"m" => "",
|
79
|
-
"d" => [northeast[0], southwest[0], northeast[1], southwest[1]].join("|")
|
80
|
-
},
|
81
|
-
"ut" => ""
|
82
|
-
}
|
83
|
-
}
|
84
|
-
|
85
|
-
headers = {
|
86
|
-
"Content-Type" => "application/json; charset=UTF-8"
|
87
|
-
}
|
88
|
-
|
89
|
-
resp, data = HTTP.post("/map/default.aspx/MapAction",
|
90
|
-
JSON.generate(post_data), headers)
|
91
|
-
|
92
|
-
begin
|
93
|
-
outer = JSON.parse(data)
|
94
|
-
raise ParseError, "Invalid JSON response" unless outer["d"]
|
95
|
-
results = JSON.parse(outer["d"])
|
96
|
-
rescue JSON::JSONError
|
97
|
-
raise ParseError, "Could not parse response JSON data"
|
98
|
-
end
|
99
|
-
|
100
|
-
raise ParseError, "Invalid JSON response" unless results["cs"]
|
101
|
-
raise TooManyResultsError if results["cs"]["count"] == 501
|
102
|
-
|
103
|
-
if results["cs"]["cc"].kind_of?(Array)
|
104
|
-
results["cs"]["cc"].map do |result|
|
105
|
-
Cache.new \
|
106
|
-
:name => result["nn"],
|
107
|
-
:code => result["gc"],
|
108
|
-
:latitude => result["lat"],
|
109
|
-
:longitude => result["lon"],
|
110
|
-
:type => CacheType.for_id(result["ctid"])
|
111
|
-
end
|
112
|
-
else
|
113
|
-
[]
|
114
|
-
end
|
8
|
+
attr_accessor :code
|
9
|
+
attr_accessor :guid
|
10
|
+
attr_accessor :name
|
11
|
+
attr_accessor :latitude
|
12
|
+
attr_accessor :longitude
|
13
|
+
attr_accessor :difficulty
|
14
|
+
attr_accessor :terrain
|
15
|
+
attr_accessor :hidden_at
|
16
|
+
attr_accessor :placed_by
|
17
|
+
attr_reader :owner
|
18
|
+
attr_reader :type
|
19
|
+
attr_reader :container_type
|
20
|
+
attr_accessor :country
|
21
|
+
attr_accessor :state
|
22
|
+
attr_accessor :is_available
|
23
|
+
attr_accessor :is_archived
|
24
|
+
attr_accessor :is_premium
|
25
|
+
attr_accessor :short_description
|
26
|
+
attr_accessor :long_description
|
27
|
+
attr_accessor :hint
|
28
|
+
attr_accessor :waypoints
|
29
|
+
|
30
|
+
def self.from_xml(doc)
|
31
|
+
Parsers::CacheSimple.new(doc).parse
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_gpx(doc)
|
35
|
+
Parsers::CacheGPX.new(doc).parse
|
115
36
|
end
|
116
37
|
|
117
|
-
# Creates a new instance. The following attributes may be specified
|
118
|
-
# as parameters:
|
119
|
-
#
|
120
|
-
# * +:code+ — The cache’s GC code
|
121
|
-
# * +:guid+ — The cache’s Globally Unique Identifier
|
122
|
-
#
|
123
|
-
# @param [Hash] attributes A hash of attributes
|
124
|
-
# @raise [ArgumentError] Trying to set an unknown attribute
|
125
38
|
def initialize(attributes = {})
|
126
|
-
@
|
39
|
+
@waypoints = []
|
127
40
|
|
128
41
|
attributes.each do |key, value|
|
129
|
-
if
|
130
|
-
|
131
|
-
raise TypeError, "Attribute `type' must be an instance of Geocaching::CacheType"
|
132
|
-
end
|
133
|
-
|
134
|
-
instance_variable_set("@#{key}", value)
|
135
|
-
else
|
136
|
-
raise ArgumentError, "Trying to set unknown attribute `#{key}'"
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# Fetches cache information from geocaching.com.
|
142
|
-
#
|
143
|
-
# @return [void]
|
144
|
-
# @raise [ArgumentError] Neither code nor GUID are given
|
145
|
-
def fetch
|
146
|
-
raise ArgumentError, "Neither code nor GUID given" unless @code or @guid
|
147
|
-
|
148
|
-
base_path = "/seek/cache_details.aspx?log=y&"
|
149
|
-
resp, @data = HTTP.get(base_path + (@code ? "wp=#{@code}" : "guid=#{@guid}"))
|
150
|
-
@doc = Nokogiri::HTML.parse(@data)
|
151
|
-
end
|
152
|
-
|
153
|
-
# Returns whether information have successfully been fetched
|
154
|
-
# from geocaching.com.
|
155
|
-
#
|
156
|
-
# @return [Boolean] Have cache information been fetched?
|
157
|
-
def fetched?
|
158
|
-
@data and @doc
|
159
|
-
end
|
160
|
-
|
161
|
-
# Returns the cache’s code.
|
162
|
-
#
|
163
|
-
# @return [String] Code
|
164
|
-
def code
|
165
|
-
@code ||= begin
|
166
|
-
raise NotFetchedError unless fetched?
|
167
|
-
elements = @doc.search("#ctl00_uxWaypointName.GCCode")
|
168
|
-
|
169
|
-
if elements.size == 1 and elements.first.content =~ /(GC[A-Z0-9]+)/
|
170
|
-
HTTP.unescape($1)
|
171
|
-
else
|
172
|
-
raise ParseError, "Could not extract code from website"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
# Returns the cache’s Globally Unique Identifier (GUID).
|
178
|
-
#
|
179
|
-
# @return [String] GUID
|
180
|
-
def guid
|
181
|
-
@guid ||= begin
|
182
|
-
raise NotFetchedError unless fetched?
|
183
|
-
|
184
|
-
elements = @doc.search("#ctl00_ContentBody_lnkPrintFriendly")
|
185
|
-
guid = nil
|
186
|
-
|
187
|
-
if elements.size == 1 and href = elements.first["href"]
|
188
|
-
guid = $1 if href =~ /guid=([0-9a-f-]{36})/
|
189
|
-
end
|
190
|
-
|
191
|
-
guid || begin
|
192
|
-
raise ParseError, "Could not extract GUID from website"
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
# Returns the cache’s ID (which is not the UUID).
|
198
|
-
#
|
199
|
-
# @return [Fixnum] ID
|
200
|
-
def id
|
201
|
-
@id ||= begin
|
202
|
-
raise NotFetchedError unless fetched?
|
203
|
-
|
204
|
-
elements = @doc.search("div.CacheDetailNavigationWidget li a[href^='/seek/log.aspx']")
|
205
|
-
|
206
|
-
if elements.size > 0 and elements.first["href"] =~ /ID=(\d+)/
|
207
|
-
id = $1.to_i
|
208
|
-
end
|
209
|
-
|
210
|
-
id || begin
|
211
|
-
raise ParseError, "Could not extract ID from website"
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
# Returns the cache’s type ID.
|
217
|
-
#
|
218
|
-
# @return [Fixnum] Type ID
|
219
|
-
def type_id
|
220
|
-
@type_id ||= begin
|
221
|
-
raise NotFetchedError unless fetched?
|
222
|
-
|
223
|
-
if @data =~ /<a.*?title="About Cache Types"><img.*?WptTypes\/(\d+)\.gif".*?<\/a>/
|
224
|
-
$1.to_i
|
225
|
-
else
|
226
|
-
raise ParseError, "Could not extract cache type ID from website"
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
# Returns the cache’s type.
|
232
|
-
#
|
233
|
-
# @return [Geocaching::CacheType] Type
|
234
|
-
def type
|
235
|
-
@type ||= CacheType.for_id(type_id)
|
236
|
-
end
|
237
|
-
|
238
|
-
# Returns the cache’s name.
|
239
|
-
#
|
240
|
-
# @return [String] Name
|
241
|
-
def name
|
242
|
-
@name ||= begin
|
243
|
-
raise NotFetchedError unless fetched?
|
244
|
-
elements = @doc.search("span#ctl00_ContentBody_CacheName")
|
245
|
-
|
246
|
-
if elements.size == 1
|
247
|
-
HTTP.unescape(elements.first.content)
|
248
|
-
else
|
249
|
-
raise ParseError, "Could not extract name from website"
|
250
|
-
end
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
# Returns the cache’s owner.
|
255
|
-
#
|
256
|
-
# @return [Geocaching::User] Owner
|
257
|
-
def owner
|
258
|
-
@owner ||= begin
|
259
|
-
raise NotFetchedError unless fetched?
|
260
|
-
elements = @doc.search("span.minorCacheDetails a[href*='/profile/?guid=']")
|
261
|
-
|
262
|
-
if elements.size == 1 and elements.first["href"] =~ /guid=([a-f0-9-]{36})/
|
263
|
-
@owner_display_name = HTTP.unescape(elements.first.content)
|
264
|
-
User.new(:guid => $1)
|
42
|
+
if respond_to?("#{key}=")
|
43
|
+
send("#{key}=", value)
|
265
44
|
else
|
266
|
-
raise
|
45
|
+
raise ArgumentError, "Unknown attribute `#{key}'"
|
267
46
|
end
|
268
47
|
end
|
269
48
|
end
|
270
49
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
def owner_display_name
|
275
|
-
owner unless @owner_display_name
|
276
|
-
@owner_display_name
|
277
|
-
end
|
278
|
-
|
279
|
-
# Returns the cache’s difficulty rating.
|
280
|
-
#
|
281
|
-
# @return [Float] Difficulty rating
|
282
|
-
def difficulty
|
283
|
-
@difficulty ||= begin
|
284
|
-
raise NotFetchedError unless fetched?
|
285
|
-
elements = @doc.search("#ctl00_ContentBody_uxLegendScale img")
|
286
|
-
|
287
|
-
if elements.size == 1 and elements.first["alt"] =~ /([\d\.]{1,3}) out of 5/
|
288
|
-
$1.to_f
|
289
|
-
else
|
290
|
-
raise ParseError, "Could not extract difficulty rating from website"
|
291
|
-
end
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
# Returns the cache’s terrain rating.
|
296
|
-
#
|
297
|
-
# @return [Float] Terrain rating
|
298
|
-
def terrain
|
299
|
-
@terrain ||= begin
|
300
|
-
raise NotFetchedError unless fetched?
|
301
|
-
elements = @doc.search("#ctl00_ContentBody_Localize6 img")
|
50
|
+
alias :available? :is_available
|
51
|
+
alias :archived? :is_archived
|
52
|
+
alias :premium? :is_premium
|
302
53
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
# Returns the date the cache has been hidden at.
|
312
|
-
#
|
313
|
-
# @return [Time] Hidden date
|
314
|
-
def hidden_at
|
315
|
-
@hidden_at ||= begin
|
316
|
-
raise NotFetchedError unless fetched?
|
317
|
-
|
318
|
-
if @data =~ /<span class="minorCacheDetails">Hidden\s*?:\s*?(\d{1,2})\/(\d{1,2})\/(\d{4})<\/span>/
|
319
|
-
Time.mktime($3, $1, $2)
|
320
|
-
else
|
321
|
-
raise ParseError, "Could not extract hidden date from website"
|
322
|
-
end
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
# Returns the date the event has been held.
|
327
|
-
#
|
328
|
-
# @return [Time] Event date
|
329
|
-
def event_date
|
330
|
-
@event_date ||= begin
|
331
|
-
raise NotFetchedError unless fetched?
|
332
|
-
return nil unless [:event, :megaevent, :cito, :lfevent].include?(type.to_sym)
|
333
|
-
|
334
|
-
if @data =~ /<span class="minorCacheDetails">\s*?Event Date:\s*?\w+, (\d+) (\w+) (\d{4})<\/span>/
|
335
|
-
Time.parse([$1, $2, $3].join)
|
336
|
-
else
|
337
|
-
raise ParseError, "Could not extract event date from website"
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
# Returns the cache’s container size. One of the following symbols
|
343
|
-
# is returned:
|
344
|
-
#
|
345
|
-
# * +:micro+
|
346
|
-
# * +:small+
|
347
|
-
# * +:regular+
|
348
|
-
# * +:large+
|
349
|
-
# * +:other+
|
350
|
-
# * +:not_chosen+
|
351
|
-
# * +:virtual+
|
352
|
-
#
|
353
|
-
# @return [Symbol] Cache container size
|
354
|
-
def size
|
355
|
-
@size ||= begin
|
356
|
-
raise NotFetchedError unless fetched?
|
357
|
-
size = nil
|
358
|
-
|
359
|
-
if @data =~ /<img src="\/images\/icons\/container\/(.*?)\.gif" alt="Size: .*?" \/>/
|
360
|
-
size = $1.to_sym if %w(micro small regular large other not_chosen virtual).include?($1)
|
361
|
-
end
|
362
|
-
|
363
|
-
size || begin
|
364
|
-
raise ParseError, "Could not extract cache container size from website"
|
365
|
-
end
|
366
|
-
end
|
367
|
-
end
|
368
|
-
|
369
|
-
# Returns the cache’s latitude.
|
370
|
-
#
|
371
|
-
# @return [Float] Latitude
|
372
|
-
def latitude
|
373
|
-
@latitude ||= begin
|
374
|
-
raise NotFetchedError unless fetched?
|
375
|
-
return nil if type == :locationless
|
376
|
-
|
377
|
-
latitude = nil
|
378
|
-
elements = @doc.search("a#ctl00_ContentBody_lnkConversions")
|
379
|
-
|
380
|
-
if elements.size == 1 and href = elements.first["href"]
|
381
|
-
latitude = $1.to_f if href =~ /lat=(-?[0-9\.]+)/
|
382
|
-
end
|
383
|
-
|
384
|
-
latitude || begin
|
385
|
-
raise ParseError, "Could not extract latitude from website"
|
386
|
-
end
|
387
|
-
end
|
388
|
-
end
|
389
|
-
|
390
|
-
# Returns the cache’s longitude.
|
391
|
-
#
|
392
|
-
# @return [Float] Longitude
|
393
|
-
def longitude
|
394
|
-
@longitude ||= begin
|
395
|
-
raise NotFetchedError unless fetched?
|
396
|
-
return nil if type == :locationless
|
397
|
-
|
398
|
-
longitude = nil
|
399
|
-
elements = @doc.search("a#ctl00_ContentBody_lnkConversions")
|
400
|
-
|
401
|
-
if elements.size == 1 and href = elements.first["href"]
|
402
|
-
longitude = $1.to_f if href =~ /lon=(-?[0-9\.]+)/
|
403
|
-
end
|
404
|
-
|
405
|
-
longitude || begin
|
406
|
-
raise ParseError, "Could not extract longitude from website"
|
407
|
-
end
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
# Returns the cache’s location name (State, Country).
|
412
|
-
#
|
413
|
-
# @return [String] Location name
|
414
|
-
def location
|
415
|
-
@location ||= begin
|
416
|
-
raise NotFetchedError unless fetched?
|
417
|
-
return nil if type == :locationless
|
418
|
-
|
419
|
-
location = nil
|
420
|
-
elements = @doc.search("span#ctl00_ContentBody_Location")
|
421
|
-
|
422
|
-
if elements.size == 1
|
423
|
-
text = @doc.search("span#ctl00_ContentBody_Location").inner_html
|
424
|
-
location = $1.strip if text =~ /In ([^<]+)/
|
425
|
-
end
|
426
|
-
|
427
|
-
location || begin
|
428
|
-
raise ParseError, "Could not extract location from website"
|
429
|
-
end
|
430
|
-
end
|
431
|
-
end
|
432
|
-
|
433
|
-
# Returns whether the cache is unpublished or not.
|
434
|
-
#
|
435
|
-
# @return [Boolean] Is cache unpublished?
|
436
|
-
def unpublished?
|
437
|
-
@is_unpublished ||= begin
|
438
|
-
raise NotFetchedError unless fetched?
|
439
|
-
!!(@data =~ /<h2>Cache is Unpublished<\/h2>/)
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
# Returns whether the cache has been archived or not.
|
444
|
-
#
|
445
|
-
# @return [Boolean] Has cache been archived?
|
446
|
-
def archived?
|
447
|
-
@is_archived ||= begin
|
448
|
-
raise NotFetchedError unless fetched?
|
449
|
-
!!(@data =~ /<li>This cache has been archived/)
|
450
|
-
end
|
451
|
-
end
|
452
|
-
|
453
|
-
# Returns whether the cache is only viewable to Premium Member only.
|
454
|
-
#
|
455
|
-
# @return [Boolean] Is cache PM-only?
|
456
|
-
def pmonly?
|
457
|
-
@is_pmonly ||= begin
|
458
|
-
raise NotFetchedError unless fetched?
|
459
|
-
|
460
|
-
@doc.search("#ctl00_ContentBody_basicMemberMsg").size == 1 ||
|
461
|
-
!!(@data =~ /<p class="Warning">This is a Premium Member Only cache\.<\/p>/)
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
# Returns whether the cache is currently in review.
|
466
|
-
#
|
467
|
-
# @return [Boolean] Is cache currently in review?
|
468
|
-
def in_review?
|
469
|
-
@in_review ||= begin
|
470
|
-
raise NotFetchedError unless fetched?
|
471
|
-
[!!@data.match(/<p class="Warning">Sorry, you cannot view this cache listing until it has been published/),
|
472
|
-
!!@data.match(/<p class="Warning">This cache listing has not been reviewed yet/)].any?
|
54
|
+
def hidden_at=(time)
|
55
|
+
if time.kind_of?(Time)
|
56
|
+
@hidden_at = time
|
57
|
+
else
|
58
|
+
raise TypeError, "`hidden_at' must be an instance of Time"
|
473
59
|
end
|
474
60
|
end
|
475
61
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
raise NotFetchedError unless fetched?
|
482
|
-
|
483
|
-
logs = LogsArray.new
|
484
|
-
tds = @doc.search("table.Table.LogsTable > tr > td")
|
485
|
-
|
486
|
-
if tds.size == 0
|
487
|
-
raise ParseError, "Could not extract logs from website"
|
488
|
-
end
|
489
|
-
|
490
|
-
tds.each do |td|
|
491
|
-
strongs = td.search("strong")
|
492
|
-
next unless strongs.size > 0
|
493
|
-
|
494
|
-
imgs = strongs.first.search("img")
|
495
|
-
user_as = strongs.first.search("a[href^='/profile/']")
|
496
|
-
log_as = td.search("small > a[href^='log.aspx']")
|
497
|
-
|
498
|
-
unless imgs.size == 1 and user_as.size == 1 and log_as.size == 1
|
499
|
-
raise ParseError, "Could not extract logs from website"
|
500
|
-
end
|
501
|
-
|
502
|
-
log_title = imgs.first["title"]
|
503
|
-
user_name = user_as.first.text.strip
|
504
|
-
user_guid = user_as.first["href"] =~ /guid=([a-f0-9-]{36})/ ? $1 : nil
|
505
|
-
log_guid = log_as.first["href"] =~ /LUID=([a-f0-9-]{36})/ ? $1 : nil
|
506
|
-
|
507
|
-
# XXX Fix log title as Groundspeak is unable to escape their
|
508
|
-
# HTML code properly.
|
509
|
-
log_title = "Didn't find it" if log_title == "Didn"
|
510
|
-
|
511
|
-
unless log_title and user_name and user_guid and log_guid
|
512
|
-
raise ParseError, "Could not extract logs from website"
|
513
|
-
end
|
514
|
-
|
515
|
-
user = User.new(:guid => user_guid, :name => user_name)
|
516
|
-
log = Log.new \
|
517
|
-
:guid => log_guid,
|
518
|
-
:title => log_title,
|
519
|
-
:cache => self,
|
520
|
-
:user => user
|
521
|
-
|
522
|
-
logs << log
|
523
|
-
end
|
524
|
-
|
525
|
-
logs
|
62
|
+
def owner=(user)
|
63
|
+
if user.kind_of?(Geocaching::User)
|
64
|
+
@owner = user
|
65
|
+
else
|
66
|
+
raise TypeError, "`owner' must be an instance of Geocaching::User"
|
526
67
|
end
|
527
68
|
end
|
528
69
|
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
unless HTTP.loggedin?
|
535
|
-
raise LoginError
|
70
|
+
def type=(cache_type)
|
71
|
+
if cache_type.kind_of?(Geocaching::CacheType)
|
72
|
+
@type = cache_type
|
73
|
+
else
|
74
|
+
raise TypeError, "`type' must be an instance of Geocaching::CacheType"
|
536
75
|
end
|
537
|
-
|
538
|
-
HTTP.get("/my/watchlist.aspx?w=#{id}")
|
539
76
|
end
|
540
77
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
def unwatch
|
547
|
-
unless HTTP.loggedin?
|
548
|
-
raise LoginError
|
78
|
+
def container_type=(container_type)
|
79
|
+
if container_type.kind_of?(Geocaching::ContainerType)
|
80
|
+
@container_type = container_type
|
81
|
+
else
|
82
|
+
raise TypeError, "`container_type' must be an instance of Geocaching::CacheType"
|
549
83
|
end
|
550
|
-
|
551
|
-
HTTP.post("/my/watchlist.aspx?ds=1&id=#{id}&action=rem", {
|
552
|
-
"ctl00$ContentBody$btnYes" => "Yes"
|
553
|
-
})
|
554
84
|
end
|
555
85
|
|
556
|
-
# Returns a hash representing this cache.
|
557
|
-
#
|
558
|
-
# @return [Hash] Hash representing this cache
|
559
86
|
def to_hash
|
560
|
-
|
561
|
-
:
|
562
|
-
:
|
563
|
-
:
|
564
|
-
:
|
565
|
-
:
|
566
|
-
:
|
567
|
-
:
|
568
|
-
:
|
569
|
-
:
|
570
|
-
:
|
571
|
-
:
|
572
|
-
:owner
|
573
|
-
:
|
574
|
-
:
|
575
|
-
:
|
576
|
-
:
|
577
|
-
:
|
578
|
-
:
|
87
|
+
{
|
88
|
+
:code => code,
|
89
|
+
:guid => guid,
|
90
|
+
:name => name,
|
91
|
+
:latitude => latitude,
|
92
|
+
:longitude => longitude,
|
93
|
+
:difficulty => difficulty,
|
94
|
+
:terrain => terrain,
|
95
|
+
:container_type => container_type,
|
96
|
+
:type => type,
|
97
|
+
:hidden_at => hidden_at,
|
98
|
+
:placed_by => placed_by,
|
99
|
+
:owner => owner,
|
100
|
+
:country => country,
|
101
|
+
:state => state,
|
102
|
+
:is_available => is_available,
|
103
|
+
:is_archived => is_archived,
|
104
|
+
:is_premium => is_premium,
|
105
|
+
:waypoints => waypoints,
|
106
|
+
:short_description => short_description,
|
107
|
+
:long_description => long_description,
|
108
|
+
:hint => hint
|
579
109
|
}
|
580
|
-
|
581
|
-
if [:event, :megaevent, :lfevent, :cito].include?(type.to_sym)
|
582
|
-
hash[:event_date] = event_date
|
583
|
-
else
|
584
|
-
hash[:hidden_at] = hidden_at
|
585
|
-
end
|
586
|
-
|
587
|
-
hash
|
588
110
|
end
|
589
111
|
end
|
590
112
|
end
|