geocaching 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/README.markdown +47 -21
  2. data/geocaching.gemspec +3 -0
  3. data/lib/geocaching.rb +20 -6
  4. data/lib/geocaching/cache.rb +237 -94
  5. data/lib/geocaching/cache_type.rb +17 -13
  6. data/lib/geocaching/http.rb +12 -16
  7. data/lib/geocaching/log.rb +67 -46
  8. data/lib/geocaching/log_type.rb +17 -13
  9. data/lib/geocaching/my_logs.rb +22 -16
  10. data/lib/geocaching/pocket_query.rb +213 -0
  11. data/lib/geocaching/user.rb +46 -69
  12. data/lib/geocaching/version.rb +3 -1
  13. data/lib/geocaching/watchlist.rb +106 -0
  14. data/spec/cache/ape.rb +4 -0
  15. data/spec/cache/cito.rb +4 -0
  16. data/spec/cache/earthcache.rb +4 -0
  17. data/spec/cache/event.rb +4 -0
  18. data/spec/cache/letterbox.rb +4 -0
  19. data/spec/cache/lfevent.rb +4 -0
  20. data/spec/cache/locationless.rb +4 -0
  21. data/spec/cache/megaevent.rb +4 -0
  22. data/spec/cache/multi.rb +4 -0
  23. data/spec/cache/mystery.rb +4 -0
  24. data/spec/cache/traditional.rb +4 -0
  25. data/spec/cache/virtual.rb +4 -0
  26. data/spec/cache/webcam.rb +4 -0
  27. data/spec/cache/wherigo.rb +4 -0
  28. data/spec/cache_spec.rb +7 -1
  29. data/spec/log/announcement.rb +1 -1
  30. data/spec/log/archive.rb +1 -1
  31. data/spec/log/attended.rb +1 -1
  32. data/spec/log/coords_update.rb +1 -1
  33. data/spec/log/disable.rb +1 -1
  34. data/spec/log/dnf.rb +1 -1
  35. data/spec/log/enable.rb +1 -1
  36. data/spec/log/found.rb +1 -1
  37. data/spec/log/needs_archived.rb +1 -1
  38. data/spec/log/needs_maintenance.rb +1 -1
  39. data/spec/log/note.rb +1 -1
  40. data/spec/log/owner_maintenance.rb +1 -1
  41. data/spec/log/publish.rb +1 -1
  42. data/spec/log/retract.rb +1 -1
  43. data/spec/log/reviewer_note.rb +1 -1
  44. data/spec/log/unarchive.rb +1 -1
  45. data/spec/log/webcam_photo_taken.rb +1 -1
  46. data/spec/log/will_attend.rb +1 -1
  47. data/spec/log_spec.rb +7 -1
  48. metadata +22 -6
  49. data/lib/geocaching/pq.rb +0 -72
@@ -1,44 +1,70 @@
1
1
  Ruby API for geocaching.com
2
2
  ===========================
3
3
 
4
- This Ruby library provides an API for geocaching.com. As Groundspeak
5
- doesn’t offer an official API yet, this library parses the website’s
6
- HTML code.
4
+ This Ruby library provides an API for geocaching.com.
7
5
 
8
- Documentation
9
- -------------
10
6
 
11
- Documentation is available at
12
- [rdoc.info](http://rdoc.info/projects/nano/ruby-geocaching).
13
-
14
- Example
15
- -------
7
+ Usage
8
+ -----
16
9
 
17
10
  require "geocaching"
18
11
 
12
+ # Logging in is not always necessary, but some information are only
13
+ # accessible when logged in.
19
14
  Geocaching::HTTP.login("username", "password")
20
15
 
21
- cache = Geocaching::Cache.fetch(:code => "GCF00")
22
- log = Geocaching::Log.fetch(:guid => "...")
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}"
23
31
 
24
- puts cache.name #=> "Bridge Over Troubled Waters"
25
- puts cache.difficulty #=> 2.5
26
- puts cache.logs.size #=> 194
32
+ # Fetch the information for a log by its GUID.
33
+ log = Geocaching::Log.fetch(:guid => "...")
27
34
 
28
- puts log.username #=> "Chris"
29
- puts log.message #=> "TFTC ..."
30
- puts log.cache #=> #<Geocaching::Cache:...>
35
+ # Print some log information.
36
+ puts "Username: #{log.user.name}"
37
+ puts " Words: #{log.message.split.size}"
38
+ puts " Cache: #{log.cache.name}"
31
39
 
32
40
  Geocaching::HTTP.logout
33
41
 
34
- Altough some cache information are available without being logged in,
35
- most information will only be accessible after a successful login.
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`.
55
+
36
56
 
37
57
  Tests
38
58
  -----
39
59
 
40
- Tests are written using RSpec.
60
+ Tests are written using [RSpec](http://relishapp.com/rspec).
41
61
 
42
62
  $ export GC_USERNAME="username"
43
63
  $ export GC_PASSWORD="password"
44
64
  $ rake test
65
+
66
+ Additional environment variables you may specify are:
67
+
68
+ * `GC_TIMEOUT` — HTTP timeout in seconds
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.
@@ -16,6 +16,9 @@ Gem::Specification.new do |s|
16
16
  s.files += Dir.glob("spec/**/*")
17
17
 
18
18
  s.has_rdoc = false
19
+
19
20
  s.add_dependency "nokogiri", ">= 1.4.2"
21
+ s.add_dependency "json", ">= 1.4.6"
22
+
20
23
  s.add_development_dependency "rspec", ">= 2.0.0"
21
24
  end
@@ -1,14 +1,18 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "nokogiri"
4
- require "geocaching/version"
5
4
 
6
5
  # This is a Ruby library to access information on geocaching.com. As
7
6
  # Groundspeak does not provide a public API yet, one needs to parse the
8
7
  # website content. That’s what this library does.
9
8
  #
10
- # * {Geocaching::Cache} Represents a cache
11
- # * {Geocaching::Log} — Represents a log
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}
12
16
  #
13
17
  # == Usage
14
18
  #
@@ -50,8 +54,15 @@ module Geocaching
50
54
  class TimeoutError < Error
51
55
  end
52
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
+
53
64
  # This exception is raised when a method is called that requires
54
- # the +#fetch+ method to be called first.
65
+ # the #fetch method to be called first.
55
66
  class NotFetchedError < Error
56
67
  def initialize
57
68
  super "Need to call the #fetch method first"
@@ -61,10 +72,10 @@ module Geocaching
61
72
  # This exception is raised when information could not be
62
73
  # extracted out of the website’s HTML code. For example,
63
74
  # this may happen if Groundspeak changed their website.
64
- class ExtractError < Error
75
+ class ParseError < Error
65
76
  end
66
77
 
67
- # This exception is raised when a HTTP request fails.
78
+ # This exception is raised when a HTTP request failed.
68
79
  class HTTPError < Error
69
80
  end
70
81
 
@@ -75,4 +86,7 @@ module Geocaching
75
86
  autoload :LogType, "geocaching/log_type"
76
87
  autoload :MyLogs, "geocaching/my_logs"
77
88
  autoload :User, "geocaching/user"
89
+ autoload :Watchlist, "geocaching/watchlist"
90
+ autoload :PocketQuery, "geocaching/pocket_query"
91
+ autoload :VERSION, "geocaching/version"
78
92
  end
@@ -1,10 +1,11 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "time"
4
+ require "json"
4
5
 
5
6
  module Geocaching
6
- # This class is subclass of Array and is used to store all logs
7
- # that belong to a cache. It implements the {#fetch_all} method to
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
8
9
  # fetch the information of all logs inside the array.
9
10
  class LogsArray < Array
10
11
  # Calls {Geocaching::Log#fetch} for each log inside the array.
@@ -16,11 +17,15 @@ module Geocaching
16
17
  end
17
18
  end
18
19
 
19
- # This class represents a cache on geocaching.com. Altough some
20
+ # The {Cache} class represents a cache on geocaching.com. Altough some
20
21
  # information are available without being logged in, most information
21
22
  # will only be accessible after a successful login.
22
23
  #
23
- # == Example
24
+ # This class does caching. That means that multiple calls of, for example,
25
+ # the {#latitude} method only do one HTTP request. To override the cached
26
+ # information, call the {#fetch} method manually.
27
+ #
28
+ # == Usage
24
29
  #
25
30
  # cache = Geocaching::Cache.fetch(:code => "GCTEST")
26
31
  #
@@ -29,7 +34,7 @@ module Geocaching
29
34
  # puts cache.archived? #=> false
30
35
  #
31
36
  class Cache
32
- # Create a new instance and call the {#fetch} methods afterwards.
37
+ # Creates a new instance and calls the {#fetch} methods afterwards.
33
38
  # One of +:code+ or +:guid+ must be provided as attributes.
34
39
  #
35
40
  # @param [Hash] attributes A hash of attributes, see {#initialize}
@@ -42,7 +47,78 @@ module Geocaching
42
47
  cache
43
48
  end
44
49
 
45
- # Create a new instance. The following attributes may be specified
50
+ # Returns an array of caches that are located within the given bounds.
51
+ # If the number of results exceeds 500 caches, a
52
+ # {Geocaching::TooManyResultsError} exception is raised.
53
+ #
54
+ # @param [Array<Numeric>] northeast
55
+ # An array containing latitude and longitude of the upper right bound
56
+ # @param [Array<Numeric>] southwest
57
+ # An array containing latitude and longitude of the lower left bound
58
+ # @return [Array<Geocaching::Cache>]
59
+ # Array of caches within given bounds
60
+ # @raise [ArgumentError]
61
+ # Invalid arguments
62
+ # @raise [Geocaching::TooManyResultsError]
63
+ # Number of results exceeds 500 caches
64
+ def self.within_bounds(northeast, southwest)
65
+ unless northeast.kind_of?(Array) and southwest.kind_of?(Array)
66
+ raise ArgumentError, "Arguments must be arrays"
67
+ end
68
+
69
+ unless northeast.size == 2 and southwest.size == 2
70
+ raise ArgumentError, "Arguments must have two elements"
71
+ end
72
+
73
+ unless northeast.map { |a| a.kind_of?(Numeric) }.all? \
74
+ and southwest.map { |a| a.kind_of?(Numeric) }.all?
75
+ raise ArgumentError, "Latitude and longitude must be given as numbers"
76
+ end
77
+
78
+ post_data = {
79
+ "dto" => {
80
+ "data" => {
81
+ "c" => 1,
82
+ "m" => "",
83
+ "d" => [northeast[0], southwest[0], northeast[1], southwest[1]].join("|")
84
+ },
85
+ "ut" => ""
86
+ }
87
+ }
88
+
89
+ headers = {
90
+ "Content-Type" => "application/json; charset=UTF-8"
91
+ }
92
+
93
+ resp, data = HTTP.post("/map/default.aspx/MapAction",
94
+ JSON.generate(post_data), headers)
95
+
96
+ begin
97
+ outer = JSON.parse(data)
98
+ raise ParseError, "Invalid JSON response" unless outer["d"]
99
+ results = JSON.parse(outer["d"])
100
+ rescue JSON::JSONError
101
+ raise ParseError, "Could not parse response JSON data"
102
+ end
103
+
104
+ raise ParseError, "Invalid JSON response" unless results["cs"]
105
+ raise TooManyResultsError if results["cs"]["count"] == 501
106
+
107
+ if results["cs"]["cc"].kind_of?(Array)
108
+ results["cs"]["cc"].map do |result|
109
+ Cache.new \
110
+ :name => result["nn"],
111
+ :code => result["gc"],
112
+ :latitude => result["lat"],
113
+ :longitude => result["lon"],
114
+ :type => CacheType.for_id(result["ctid"])
115
+ end
116
+ else
117
+ []
118
+ end
119
+ end
120
+
121
+ # Creates a new instance. The following attributes may be specified
46
122
  # as parameters:
47
123
  #
48
124
  # * +:code+ — The cache’s GC code
@@ -54,7 +130,7 @@ module Geocaching
54
130
  @data, @doc, @code, @guid = nil, nil, nil, nil
55
131
 
56
132
  attributes.each do |key, value|
57
- if [:code, :guid, :name, :type].include?(key)
133
+ if [:code, :guid, :name, :type, :latitude, :longitude].include?(key)
58
134
  if key == :type and not value.kind_of?(CacheType)
59
135
  raise TypeError, "Attribute `type' must be an instance of Geocaching::CacheType"
60
136
  end
@@ -66,12 +142,10 @@ module Geocaching
66
142
  end
67
143
  end
68
144
 
69
- # Fetche cache information from geocaching.com.
145
+ # Fetches cache information from geocaching.com.
70
146
  #
71
147
  # @return [void]
72
148
  # @raise [ArgumentError] Neither code nor GUID are given
73
- # @raise [Geocaching::TimeoutError] Timeout hit
74
- # @raise [Geocaching::HTTPError] HTTP request failed
75
149
  def fetch
76
150
  raise ArgumentError, "Neither code nor GUID given" unless @code or @guid
77
151
 
@@ -79,19 +153,17 @@ module Geocaching
79
153
  @doc = Nokogiri::HTML.parse(@data)
80
154
  end
81
155
 
82
- # Return whether information have successfully been fetched
156
+ # Returns whether information have successfully been fetched
83
157
  # from geocaching.com.
84
158
  #
85
- # @return [Boolean] Have information been fetched?
159
+ # @return [Boolean] Have cache information been fetched?
86
160
  def fetched?
87
161
  @data and @doc
88
162
  end
89
163
 
90
- # Return the cache’s code (GCXXXXXX).
164
+ # Returns the cache’s code.
91
165
  #
92
166
  # @return [String] Code
93
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
94
- # @raise [Geocaching::ExtractError] Could not extract code from website
95
167
  def code
96
168
  @code ||= begin
97
169
  raise NotFetchedError unless fetched?
@@ -100,16 +172,14 @@ module Geocaching
100
172
  if elements.size == 1 and elements.first.content =~ /(GC[A-Z0-9]+)/
101
173
  HTTP.unescape($1)
102
174
  else
103
- raise ExtractError, "Could not extract code from website"
175
+ raise ParseError, "Could not extract code from website"
104
176
  end
105
177
  end
106
178
  end
107
179
 
108
- # Return the cache’s Globally Unique Identifier (GUID).
180
+ # Returns the cache’s Globally Unique Identifier (GUID).
109
181
  #
110
182
  # @return [String] GUID
111
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
112
- # @raise [Geocaching::ExtractError] Could not extract GUID from website
113
183
  def guid
114
184
  @guid ||= begin
115
185
  raise NotFetchedError unless fetched?
@@ -122,16 +192,33 @@ module Geocaching
122
192
  end
123
193
 
124
194
  guid || begin
125
- raise ExtractError, "Could not extract GUID from website"
195
+ raise ParseError, "Could not extract GUID from website"
126
196
  end
127
197
  end
128
198
  end
129
199
 
130
- # Return the cache’s type ID.
200
+ # Returns the cache’s ID (which is not the UUID).
201
+ #
202
+ # @return [Fixnum] ID
203
+ def id
204
+ @id ||= begin
205
+ raise NotFetchedError unless fetched?
206
+
207
+ elements = @doc.search("div.CacheDetailNavigationWidget li a[href^='/seek/log.aspx']")
208
+
209
+ if elements.size > 0 and elements.first["href"] =~ /ID=(\d+)/
210
+ @id = $1.to_i
211
+ end
212
+
213
+ @id || begin
214
+ raise ParseError, "Could not extract ID from website"
215
+ end
216
+ end
217
+ end
218
+
219
+ # Returns the cache’s type ID.
131
220
  #
132
221
  # @return [Fixnum] Type ID
133
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
134
- # @raise [Geocaching::ExtractError] Could not extract cache type ID from website
135
222
  def type_id
136
223
  @type_id ||= begin
137
224
  raise NotFetchedError unless fetched?
@@ -139,25 +226,21 @@ module Geocaching
139
226
  if @data =~ /<a.*?title="About Cache Types"><img.*?WptTypes\/(\d+)\.gif".*?<\/a>/
140
227
  $1.to_i
141
228
  else
142
- raise ExtractError, "Could not extract cache type ID from website"
229
+ raise ParseError, "Could not extract cache type ID from website"
143
230
  end
144
231
  end
145
232
  end
146
233
 
147
- # Return the cache’s type.
234
+ # Returns the cache’s type.
148
235
  #
149
236
  # @return [Geocaching::CacheType] Type
150
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
151
- # @raise [Geocaching::ExtractError] Could not extract cache type ID from website
152
237
  def type
153
238
  @type ||= CacheType.for_id(type_id)
154
239
  end
155
240
 
156
- # Return the cache’s name.
241
+ # Returns the cache’s name.
157
242
  #
158
243
  # @return [String] Name
159
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
160
- # @raise [Geocaching::ExtractError] Could not extract name from website"
161
244
  def name
162
245
  @name ||= begin
163
246
  raise NotFetchedError unless fetched?
@@ -166,16 +249,14 @@ module Geocaching
166
249
  if elements.size == 1
167
250
  HTTP.unescape(elements.first.content)
168
251
  else
169
- raise ExtractError, "Could not extract name from website"
252
+ raise ParseError, "Could not extract name from website"
170
253
  end
171
254
  end
172
255
  end
173
256
 
174
- # Return the cache’s owner.
257
+ # Returns the cache’s owner.
175
258
  #
176
- # @return [Geocaching::User]
177
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
178
- # @raise [Geocaching::ExtractError] Could not extract info from website"
259
+ # @return [Geocaching::User] Owner
179
260
  def owner
180
261
  @owner ||= begin
181
262
  raise NotFetchedError unless fetched?
@@ -184,26 +265,22 @@ module Geocaching
184
265
  @owner_display_name = HTTP.unescape($2)
185
266
  User.new(:guid => $1)
186
267
  else
187
- raise ExtractError, "Could not extract owner from website"
268
+ raise ParseError, "Could not extract owner from website"
188
269
  end
189
270
  end
190
271
  end
191
272
 
192
- # Return the displayed cache owner name.
273
+ # Returns the displayed cache owner name.
193
274
  #
194
- # @return [String]
195
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
196
- # @raise [Geocaching::ExtractError] Could not extract info from website"
275
+ # @return [String] Displayed owner name
197
276
  def owner_display_name
198
277
  owner unless @owner_display_name
199
278
  @owner_display_name
200
279
  end
201
280
 
202
- # Return the cache’s difficulty rating.
281
+ # Returns the cache’s difficulty rating.
203
282
  #
204
283
  # @return [Float] Difficulty rating
205
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
206
- # @raise [Geocaching::ExtractError] Could not extract difficulty rating from website
207
284
  def difficulty
208
285
  @difficulty ||= begin
209
286
  raise NotFetchedError unless fetched?
@@ -211,16 +288,14 @@ module Geocaching
211
288
  if @data =~ /<strong>\s*?Difficulty:<\/strong>\s*?<img.*?alt="([\d\.]{1,3}) out of 5" \/>/
212
289
  $1.to_f
213
290
  else
214
- raise ExtractError, "Could not extract difficulty rating from website"
291
+ raise ParseError, "Could not extract difficulty rating from website"
215
292
  end
216
293
  end
217
294
  end
218
295
 
219
- # Return the cache’s terrain rating.
296
+ # Returns the cache’s terrain rating.
220
297
  #
221
298
  # @return [Float] Terrain rating
222
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
223
- # @raise [Geocaching::ExtractError] Could not extract terrain rating from website
224
299
  def terrain
225
300
  @terrain ||= begin
226
301
  raise NotFetchedError unless fetched?
@@ -228,16 +303,14 @@ module Geocaching
228
303
  if @data =~ /<strong>\s+?Terrain:<\/strong>\s+?<img.*?alt="([\d\.]{1,3}) out of 5" \/>/
229
304
  $1.to_f
230
305
  else
231
- raise ExtractError, "Could not extract terrain rating from website"
306
+ raise ParseError, "Could not extract terrain rating from website"
232
307
  end
233
308
  end
234
309
  end
235
310
 
236
- # Return the date the cache has been hidden at.
311
+ # Returns the date the cache has been hidden at.
237
312
  #
238
313
  # @return [Time] Hidden date
239
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
240
- # @raise [Geocaching::ExtractError] Could not extract hidden date from website
241
314
  def hidden_at
242
315
  @hidden_at ||= begin
243
316
  raise NotFetchedError unless fetched?
@@ -245,16 +318,14 @@ module Geocaching
245
318
  if @data =~ /<strong>\s*?Hidden\s*?:\s*?<\/strong>\s*?(\d{1,2})\/(\d{1,2})\/(\d{4})/
246
319
  Time.mktime($3, $1, $2)
247
320
  else
248
- raise ExtractError, "Could not extract hidden date from website"
321
+ raise ParseError, "Could not extract hidden date from website"
249
322
  end
250
323
  end
251
324
  end
252
325
 
253
- # Return the date the event has been held.
326
+ # Returns the date the event has been held.
254
327
  #
255
328
  # @return [Time] Event date
256
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
257
- # @raise [Geocaching::ExtractError] Could not extract event date from website
258
329
  def event_date
259
330
  @event_date ||= begin
260
331
  raise NotFetchedError unless fetched?
@@ -263,16 +334,23 @@ module Geocaching
263
334
  if @data =~ /<strong>\s*?Event Date:\s*?<\/strong>\s*?\w+, (\d+) (\w+) (\d{4})/
264
335
  Time.parse([$1, $2, $3].join)
265
336
  else
266
- raise ExtractError, "Could not extract event date from website"
337
+ raise ParseError, "Could not extract event date from website"
267
338
  end
268
339
  end
269
340
  end
270
341
 
271
- # Return the cache’s container size.
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+
272
352
  #
273
353
  # @return [Symbol] Cache container size
274
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
275
- # @raise [Geocaching::ExtractError] Could not extract cache container size from website
276
354
  def size
277
355
  @size ||= begin
278
356
  raise NotFetchedError unless fetched?
@@ -283,16 +361,14 @@ module Geocaching
283
361
  end
284
362
 
285
363
  size || begin
286
- raise ExtractError, "Could not extract cache container size from website"
364
+ raise ParseError, "Could not extract cache container size from website"
287
365
  end
288
366
  end
289
367
  end
290
368
 
291
- # Return the cache’s latitude.
369
+ # Returns the cache’s latitude.
292
370
  #
293
371
  # @return [Float] Latitude
294
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
295
- # @raise [Geocaching::ExtractError] Could not extract latitude from website
296
372
  def latitude
297
373
  @latitude ||= begin
298
374
  raise NotFetchedError unless fetched?
@@ -306,16 +382,14 @@ module Geocaching
306
382
  end
307
383
 
308
384
  latitude || begin
309
- raise ExtractError, "Could not extract latitude from website"
385
+ raise ParseError, "Could not extract latitude from website"
310
386
  end
311
387
  end
312
388
  end
313
389
 
314
- # Return the cache’s longitude.
390
+ # Returns the cache’s longitude.
315
391
  #
316
392
  # @return [Float] Longitude
317
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
318
- # @raise [Geocaching::ExtractError] Could not extract longitude from website
319
393
  def longitude
320
394
  @longitude ||= begin
321
395
  raise NotFetchedError unless fetched?
@@ -329,16 +403,14 @@ module Geocaching
329
403
  end
330
404
 
331
405
  longitude || begin
332
- raise ExtractError, "Could not extract longitude from website"
406
+ raise ParseError, "Could not extract longitude from website"
333
407
  end
334
408
  end
335
409
  end
336
410
 
337
- # Return the cache’s location name (State, Country).
411
+ # Returns the cache’s location name (State, Country).
338
412
  #
339
413
  # @return [String] Location name
340
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
341
- # @raise [Geocaching::ExtractError] Could not extract location from website
342
414
  def location
343
415
  @location ||= begin
344
416
  raise NotFetchedError unless fetched?
@@ -353,15 +425,14 @@ module Geocaching
353
425
  end
354
426
 
355
427
  location || begin
356
- raise ExtractError, "Could not extract location from website"
428
+ raise ParseError, "Could not extract location from website"
357
429
  end
358
430
  end
359
431
  end
360
432
 
361
- # Return whether the cache is unpublished or not.
433
+ # Returns whether the cache is unpublished or not.
362
434
  #
363
435
  # @return [Boolean] Is cache unpublished?
364
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
365
436
  def unpublished?
366
437
  @is_unpublished ||= begin
367
438
  raise NotFetchedError unless fetched?
@@ -369,10 +440,9 @@ module Geocaching
369
440
  end
370
441
  end
371
442
 
372
- # Return whether the cache has been archived or not.
443
+ # Returns whether the cache has been archived or not.
373
444
  #
374
445
  # @return [Boolean] Has cache been archived?
375
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
376
446
  def archived?
377
447
  @is_archived ||= begin
378
448
  raise NotFetchedError unless fetched?
@@ -380,10 +450,9 @@ module Geocaching
380
450
  end
381
451
  end
382
452
 
383
- # Return whether the cache is only viewable to Premium Member only.
453
+ # Returns whether the cache is only viewable to Premium Member only.
384
454
  #
385
455
  # @return [Boolean] Is cache PM-only?
386
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
387
456
  def pmonly?
388
457
  @is_pmonly ||= begin
389
458
  raise NotFetchedError unless fetched?
@@ -393,10 +462,9 @@ module Geocaching
393
462
  end
394
463
  end
395
464
 
396
- # Return whether the cache is currently in review.
465
+ # Returns whether the cache is currently in review.
397
466
  #
398
467
  # @return [Boolean] Is cache currently in review?
399
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
400
468
  def in_review?
401
469
  @in_review ||= begin
402
470
  raise NotFetchedError unless fetched?
@@ -405,12 +473,9 @@ module Geocaching
405
473
  end
406
474
  end
407
475
 
408
- # Return an array of logs for this cache. A log is an instance of
409
- # {Geocaching::Log}.
476
+ # Returns an array of the cache’s logs.
410
477
  #
411
478
  # @return [Geocaching::LogsArray<Geocaching::Log>] Array of logs
412
- # @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
413
- # @raise [Geocaching::ExtractError] Could not extract logs from website
414
479
  def logs
415
480
  @logs ||= begin
416
481
  raise NotFetchedError unless fetched?
@@ -419,36 +484,114 @@ module Geocaching
419
484
  tds = @doc.search("table.Table.LogsTable > tr > td")
420
485
 
421
486
  if tds.size == 0
422
- raise ExtractError, "Could not extract logs from website"
487
+ raise ParseError, "Could not extract logs from website"
423
488
  end
424
489
 
425
490
  tds.each do |td|
426
491
  strongs = td.search("strong")
427
492
  next unless strongs.size > 0
428
493
 
429
- imgs = strongs.first.search("img")
430
- as = strongs.first.search("a")
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']")
431
497
 
432
- unless imgs.size == 1 and as.size == 1
433
- raise ExtractError, "Could not extract logs from website"
498
+ unless imgs.size == 1 and user_as.size == 1 and log_as.size == 1
499
+ raise ParseError, "Could not extract logs from website"
434
500
  end
435
501
 
436
- title = imgs.first["title"]
437
- guid = as.first["href"] =~ /guid=([a-f0-9-]{36})/ ? $1 : nil
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"
438
510
 
439
- unless title and guid
440
- raise ExtractError, "Could not extract logs from website"
511
+ unless log_title and user_name and user_guid and log_guid
512
+ raise ParseError, "Could not extract logs from website"
441
513
  end
442
514
 
443
- logs << Log.new(:guid => guid, :title => title, :cache => self)
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
444
523
  end
445
524
 
446
525
  logs
447
526
  end
448
527
  end
449
528
 
529
+ # Puts the cache on the user’s watchlist. Obviously, you need to be logged
530
+ # in when calling this method.
531
+ #
532
+ # @return [void]
533
+ def watch
534
+ unless HTTP.loggedin?
535
+ raise LoginError
536
+ end
537
+
538
+ HTTP.get("/my/watchlist.aspx?w=#{id}")
539
+ end
540
+
541
+ # Removes the cache from the user’s watchlist. This method doesn’t check
542
+ # if the cache you want to remove from the watchlist actually is on your
543
+ # watchlist.
544
+ #
545
+ # @return [void]
546
+ def unwatch
547
+ unless HTTP.loggedin?
548
+ raise LoginError
549
+ end
550
+
551
+ HTTP.post("/my/watchlist.aspx?ds=1&id=#{id}&action=rem", {
552
+ "ctl00$ContentBody$btnYes" => "Yes"
553
+ })
554
+ end
555
+
556
+ # Returns a Hash representation of this class.
557
+ #
558
+ # @return [Hash] Hash representing this class
559
+ def to_hash
560
+ hash = {
561
+ :id => id,
562
+ :code => code,
563
+ :guid => guid,
564
+ :name => name,
565
+ :difficulty => difficulty,
566
+ :terrain => terrain,
567
+ :latitude => latitude,
568
+ :longitude => longitude,
569
+ :size => size,
570
+ :type => type,
571
+ :location => location,
572
+ :owner => owner,
573
+ :owner_display_name => owner_display_name,
574
+ :logs => logs,
575
+ :pmonly? => pmonly?,
576
+ :archived? => archived?,
577
+ :in_review? => in_review?,
578
+ :unpublished? => unpublished?
579
+ }
580
+
581
+ if [:event, :megaevent, :lfevent].include?(type)
582
+ hash[:event_date] = event_date
583
+ else
584
+ hash[:hidden_at] = hidden_at
585
+ end
586
+
587
+ hash
588
+ end
589
+
450
590
  private
451
591
 
592
+ # Returns the HTTP request path.
593
+ #
594
+ # @return [String] HTTP request path
452
595
  def path
453
596
  "/seek/cache_details.aspx?log=y&" + (@code ? "wp=#{@code}" : "guid=#{@guid}")
454
597
  end