attack-barometer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +266 -0
  3. data/VERSION.yml +4 -0
  4. data/bin/barometer +63 -0
  5. data/lib/barometer/base.rb +52 -0
  6. data/lib/barometer/data/current.rb +93 -0
  7. data/lib/barometer/data/distance.rb +131 -0
  8. data/lib/barometer/data/forecast.rb +66 -0
  9. data/lib/barometer/data/geo.rb +98 -0
  10. data/lib/barometer/data/location.rb +20 -0
  11. data/lib/barometer/data/measurement.rb +161 -0
  12. data/lib/barometer/data/pressure.rb +133 -0
  13. data/lib/barometer/data/speed.rb +147 -0
  14. data/lib/barometer/data/sun.rb +35 -0
  15. data/lib/barometer/data/temperature.rb +164 -0
  16. data/lib/barometer/data/units.rb +55 -0
  17. data/lib/barometer/data/zone.rb +124 -0
  18. data/lib/barometer/data.rb +15 -0
  19. data/lib/barometer/extensions/graticule.rb +50 -0
  20. data/lib/barometer/extensions/httparty.rb +21 -0
  21. data/lib/barometer/query.rb +228 -0
  22. data/lib/barometer/services/google.rb +146 -0
  23. data/lib/barometer/services/noaa.rb +6 -0
  24. data/lib/barometer/services/service.rb +324 -0
  25. data/lib/barometer/services/weather_bug.rb +6 -0
  26. data/lib/barometer/services/weather_dot_com.rb +6 -0
  27. data/lib/barometer/services/wunderground.rb +285 -0
  28. data/lib/barometer/services/yahoo.rb +274 -0
  29. data/lib/barometer/services.rb +6 -0
  30. data/lib/barometer/weather.rb +187 -0
  31. data/lib/barometer.rb +52 -0
  32. data/spec/barometer_spec.rb +162 -0
  33. data/spec/data_current_spec.rb +225 -0
  34. data/spec/data_distance_spec.rb +336 -0
  35. data/spec/data_forecast_spec.rb +150 -0
  36. data/spec/data_geo_spec.rb +90 -0
  37. data/spec/data_location_spec.rb +59 -0
  38. data/spec/data_measurement_spec.rb +411 -0
  39. data/spec/data_pressure_spec.rb +336 -0
  40. data/spec/data_speed_spec.rb +374 -0
  41. data/spec/data_sun_spec.rb +76 -0
  42. data/spec/data_temperature_spec.rb +396 -0
  43. data/spec/data_zone_spec.rb +133 -0
  44. data/spec/fixtures/current_calgary_ab.xml +1 -0
  45. data/spec/fixtures/forecast_calgary_ab.xml +1 -0
  46. data/spec/fixtures/geocode_40_73.xml +1 -0
  47. data/spec/fixtures/geocode_90210.xml +1 -0
  48. data/spec/fixtures/geocode_T5B4M9.xml +1 -0
  49. data/spec/fixtures/geocode_calgary_ab.xml +1 -0
  50. data/spec/fixtures/geocode_newyork_ny.xml +1 -0
  51. data/spec/fixtures/google_calgary_ab.xml +1 -0
  52. data/spec/fixtures/yahoo_90210.xml +1 -0
  53. data/spec/query_spec.rb +469 -0
  54. data/spec/service_google_spec.rb +144 -0
  55. data/spec/service_wunderground_spec.rb +330 -0
  56. data/spec/service_yahoo_spec.rb +299 -0
  57. data/spec/services_spec.rb +1106 -0
  58. data/spec/spec_helper.rb +14 -0
  59. data/spec/units_spec.rb +101 -0
  60. data/spec/weather_spec.rb +265 -0
  61. metadata +119 -0
@@ -0,0 +1,164 @@
1
+ module Barometer
2
+ #
3
+ # A simple Temperature class
4
+ #
5
+ # Think of this like the Integer class. Enhancement
6
+ # is that you can create a number (in a certain unit), then
7
+ # get that number back already converted to another unit.
8
+ #
9
+ # All comparison operations will be done in the absolute
10
+ # scale of Kelvin (K)
11
+ #
12
+ class Temperature < Barometer::Units
13
+
14
+ METRIC_UNITS = "C"
15
+ IMPERIAL_UNITS = "F"
16
+
17
+ attr_accessor :celsius, :fahrenheit, :kelvin
18
+
19
+ def initialize(metric=true)
20
+ @celsius = nil
21
+ @fahrenheit = nil
22
+ @kelvin = nil
23
+ super(metric)
24
+ end
25
+
26
+ def metric_default=(value); self.c = value; end
27
+ def imperial_default=(value); self.f = value; end
28
+
29
+ #
30
+ # CONVERTERS
31
+ #
32
+
33
+ def self.c_to_k(c)
34
+ return nil unless c && (c.is_a?(Integer) || c.is_a?(Float))
35
+ 273.15 + c.to_f
36
+ end
37
+
38
+ # Tf = (9/5)*Tc+32
39
+ def self.c_to_f(c)
40
+ return nil unless c && (c.is_a?(Integer) || c.is_a?(Float))
41
+ ((9.0/5.0)*c.to_f)+32.0
42
+ end
43
+
44
+ # Tc = (5/9)*(Tf-32)
45
+ def self.f_to_c(f)
46
+ return nil unless f && (f.is_a?(Integer) || f.is_a?(Float))
47
+ (5.0/9.0)*(f.to_f-32.0)
48
+ end
49
+
50
+ def self.f_to_k(f)
51
+ return nil unless f && (f.is_a?(Integer) || f.is_a?(Float))
52
+ c = self.f_to_c(f.to_f)
53
+ self.c_to_k(c)
54
+ end
55
+
56
+ def self.k_to_c(k)
57
+ return nil unless k && (k.is_a?(Integer) || k.is_a?(Float))
58
+ k.to_f - 273.15
59
+ end
60
+
61
+ def self.k_to_f(k)
62
+ return nil unless k && (k.is_a?(Integer) || k.is_a?(Float))
63
+ c = self.k_to_c(k.to_f)
64
+ self.c_to_f(c)
65
+ end
66
+
67
+ #
68
+ # ACCESSORS
69
+ #
70
+
71
+ # store celsius and kelvin
72
+ def c=(c)
73
+ return if !c || !(c.is_a?(Integer) || c.is_a?(Float))
74
+ @celsius = c.to_f
75
+ @kelvin = Temperature.c_to_k(c.to_f)
76
+ self.update_fahrenheit(c.to_f)
77
+ end
78
+
79
+ # store fahrenheit and kelvin
80
+ def f=(f)
81
+ return if !f || !(f.is_a?(Integer) || f.is_a?(Float))
82
+ @fahrenheit = f.to_f
83
+ @kelvin = Temperature.f_to_k(f.to_f)
84
+ self.update_celsius(f.to_f)
85
+ end
86
+
87
+ # store kelvin, convert to all
88
+ def k=(k)
89
+ return if !k || !(k.is_a?(Integer) || k.is_a?(Float))
90
+ @kelvin = k.to_f
91
+ @celsius = Temperature.k_to_c(k.to_f)
92
+ @fahrenheit = Temperature.k_to_f(k.to_f)
93
+ end
94
+
95
+ # return the stored celsius or convert from Kelvin
96
+ def c(as_integer=true)
97
+ c = (@celsius || Temperature.k_to_c(@kelvin))
98
+ c ? (as_integer ? c.to_i : (100*c).round/100.0) : nil
99
+ end
100
+
101
+ # return the stored fahrenheit or convert from Kelvin
102
+ def f(as_integer=true)
103
+ f = (@fahrenheit || Temperature.k_to_f(@kelvin))
104
+ f ? (as_integer ? f.to_i : (100*f).round/100.0) : nil
105
+ end
106
+
107
+ #
108
+ # OPERATORS
109
+ #
110
+
111
+ def <=>(other)
112
+ @kelvin <=> other.kelvin
113
+ end
114
+
115
+ #
116
+ # HELPERS
117
+ #
118
+
119
+ # will just return the value (no units)
120
+ def to_i(metric=nil)
121
+ (metric || (metric.nil? && self.metric?)) ? self.c : self.f
122
+ end
123
+
124
+ # will just return the value (no units) with more precision
125
+ def to_f(metric=nil)
126
+ (metric || (metric.nil? && self.metric?)) ? self.c(false) : self.f(false)
127
+ end
128
+
129
+ # will return the value with units
130
+ def to_s(metric=nil)
131
+ (metric || (metric.nil? && self.metric?)) ? "#{self.c} #{METRIC_UNITS}" : "#{self.f} #{IMPERIAL_UNITS}"
132
+ end
133
+
134
+ # will just return the units (no value)
135
+ def units(metric=nil)
136
+ (metric || (metric.nil? && self.metric?)) ? METRIC_UNITS : IMPERIAL_UNITS
137
+ end
138
+
139
+ # when we set fahrenheit, it is possible the a non-equivalent value of
140
+ # celsius remains. if so, clear it.
141
+ def update_celsius(f)
142
+ return unless @celsius
143
+ difference = Temperature.f_to_c(f.to_f) - @celsius
144
+ # only clear celsius if the stored celsius is off be more then 1 degree
145
+ # then the conversion of fahrenheit
146
+ @celsius = nil unless difference.abs <= 1.0
147
+ end
148
+
149
+ # when we set celsius, it is possible the a non-equivalent value of
150
+ # fahrenheit remains. if so, clear it.
151
+ def update_fahrenheit(c)
152
+ return unless @fahrenheit
153
+ difference = Temperature.c_to_f(c.to_f) - @fahrenheit
154
+ # only clear fahrenheit if the stored fahrenheit is off be more then 1 degree
155
+ # then the conversion of celsius
156
+ @fahrenheit = nil unless difference.abs <= 1.0
157
+ end
158
+
159
+ def nil?
160
+ (@celsius || @fahrenheit || @kelvin) ? false : true
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,55 @@
1
+ module Barometer
2
+ class Units
3
+ include Comparable
4
+
5
+ attr_accessor :metric
6
+
7
+ def initialize(metric=true)
8
+ @metric = metric
9
+ end
10
+
11
+ #
12
+ # HELPERS
13
+ #
14
+
15
+ def metric?; @metric; end
16
+ def metric!; @metric=true; end
17
+ def imperial!; @metric=false; end
18
+
19
+ # assigns a value to the right attribute based on metric setting
20
+ def <<(value)
21
+ return unless value
22
+
23
+ begin
24
+ if value.is_a?(Array)
25
+ value_m = value[0].to_f
26
+ value_i = value[1].to_f
27
+ value_b = nil
28
+ else
29
+ value_m = nil
30
+ value_i = nil
31
+ value_b = value.to_f
32
+ end
33
+ rescue
34
+ # do nothing
35
+ end
36
+
37
+ if self.metric?
38
+ self.metric_default = value_m || value_b
39
+ else
40
+ self.imperial_default = value_i || value_b
41
+ end
42
+ end
43
+
44
+ # STUB: define this method to actually retireve the metric_default
45
+ def metric_default=(value)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ # STUB: define this method to actually retireve the imperial_default
50
+ def imperial_default=(value)
51
+ raise NotImplementedError
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,124 @@
1
+ require 'rubygems'
2
+ require 'tzinfo'
3
+
4
+ module Barometer
5
+ #
6
+ # A simple Zone class
7
+ #
8
+ # Used for building and converting timezone aware date and times
9
+ # Really, these are just wrappers for TZInfo conversions.
10
+ #
11
+ class Zone
12
+
13
+ attr_accessor :timezone, :tz
14
+
15
+ def initialize(timezone)
16
+ @timezone = timezone
17
+ @tz = TZInfo::Timezone.get(timezone)
18
+ end
19
+
20
+ # what is the Timezone Short Code for the current timezone
21
+ def code
22
+ return "" unless @tz
23
+ @tz.period_for_utc(Time.now.utc).zone_identifier.to_s
24
+ end
25
+
26
+ # is the current timezone in daylights savings mode?
27
+ def dst?
28
+ return nil unless @tz
29
+ @tz.period_for_utc(Time.now.utc).dst?
30
+ end
31
+
32
+ # return Time.now.utc for the set timezone
33
+ def now
34
+ Barometer::Zone.now(@timezone)
35
+ end
36
+
37
+ # return Date.today for the set timezone
38
+ def today
39
+ Barometer::Zone.today(@timezone)
40
+ end
41
+
42
+ def local_to_utc(local_time)
43
+ @tz.local_to_utc(local_time)
44
+ end
45
+
46
+ def utc_to_local(utc_time)
47
+ @tz.utc_to_local(utc_time)
48
+ end
49
+
50
+ #
51
+ # Class Methods
52
+ #
53
+
54
+ # return the local current time, providing a timezone
55
+ # (ie 'Europe/Paris') will give the local time for the
56
+ # timezone, otherwise it will be Time.now
57
+ def self.now(timezone=nil)
58
+ if timezone
59
+ utc = Time.now.utc
60
+ tz = TZInfo::Timezone.get(timezone)
61
+ tz.utc_to_local(utc)
62
+ else
63
+ Time.now
64
+ end
65
+ end
66
+
67
+ # return the local current date, providing a timezone
68
+ # (ie 'Europe/Paris') will give the local date for the
69
+ # timezone, otherwise it will be Date.today
70
+ def self.today(timezone=nil)
71
+ if timezone
72
+ utc = Time.now.utc
73
+ tz = TZInfo::Timezone.get(timezone)
74
+ now = tz.utc_to_local(utc)
75
+ Date.new(now.year, now.month, now.day)
76
+ else
77
+ Date.today
78
+ end
79
+ end
80
+
81
+ # takes a time (any timezone), and a TimeZone Short Code (ie PST) and
82
+ # converts the time to UTC accorsing to that time_zone
83
+ # NOTE: No Tests
84
+ def self.code_to_utc(time, timezone_code)
85
+ raise ArgumentError unless time.is_a?(Time)
86
+ offset = Time.zone_offset(timezone_code) || 0
87
+
88
+ Time.utc(
89
+ time.year, time.month, time.day,
90
+ time.hour, time.min, time.sec, time.usec
91
+ ) - offset
92
+ end
93
+
94
+ # takes a string with TIME only information and merges it with a string that
95
+ # has DATE only information and creates a UTC TIME object with time and date
96
+ # info. if you supply the timezone code (ie PST), it will apply the timezone
97
+ # offset to the final time
98
+ def self.merge(time, date, timezone_code=nil)
99
+ raise ArgumentError unless (time.is_a?(Time) || time.is_a?(String))
100
+ raise ArgumentError unless (date.is_a?(Time) || date.is_a?(Date) || date.is_a?(String))
101
+
102
+ if time.is_a?(String)
103
+ reference_time = Time.parse(time)
104
+ elsif time.is_a?(Time)
105
+ reference_time = time
106
+ end
107
+
108
+ if date.is_a?(String)
109
+ reference_date = Date.parse(date)
110
+ elsif date.is_a?(Time)
111
+ reference_date = Date.new(date.year, date.month, date.day)
112
+ elsif date.is_a?(Date)
113
+ reference_date = date
114
+ end
115
+
116
+ new_time = Time.utc(
117
+ reference_date.year, reference_date.month, reference_date.day,
118
+ reference_time.hour, reference_time.min, reference_time.sec
119
+ )
120
+ timezone_code ? Barometer::Zone.code_to_utc(new_time,timezone_code) : new_time
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,15 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ # measurements and units
4
+ require 'data/measurement'
5
+ require 'data/current'
6
+ require 'data/forecast'
7
+ require 'data/zone'
8
+ require 'data/sun'
9
+ require 'data/geo'
10
+ require 'data/location'
11
+ require 'data/units'
12
+ require 'data/temperature'
13
+ require 'data/distance'
14
+ require 'data/speed'
15
+ require 'data/pressure'
@@ -0,0 +1,50 @@
1
+ module Graticule
2
+ class Location
3
+
4
+ attr_accessor :country_code
5
+
6
+ def attributes
7
+ [:latitude, :longitude, :street, :locality, :region, :postal_code, :country, :precision, :cuntry_code].inject({}) do |result,attr|
8
+ result[attr] = self.send(attr) unless self.send(attr).blank?
9
+ result
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ module Geocoder
16
+
17
+ class Google < Rest
18
+
19
+ # Locates +address+ returning a Location
20
+ # add ability to bias towards a country
21
+ def locate(address, country_bias=nil)
22
+ get :q => (address.is_a?(String) ? address : location_from_params(address).to_s),
23
+ :gl => country_bias
24
+ end
25
+
26
+ private
27
+
28
+ # Extracts a Location from +xml+.
29
+ def parse_response(xml) #:nodoc:
30
+ longitude, latitude, = xml.elements['/kml/Response/Placemark/Point/coordinates'].text.split(',').map { |v| v.to_f }
31
+ returning Location.new(:latitude => latitude, :longitude => longitude) do |l|
32
+ address = REXML::XPath.first(xml, '//xal:AddressDetails',
33
+ 'xal' => "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0")
34
+
35
+ if address
36
+ l.street = value(address.elements['.//ThoroughfareName/text()'])
37
+ l.locality = value(address.elements['.//LocalityName/text()'])
38
+ l.region = value(address.elements['.//AdministrativeAreaName/text()'])
39
+ l.postal_code = value(address.elements['.//PostalCodeNumber/text()'])
40
+ l.country = value(address.elements['.//CountryName/text()'])
41
+ l.country_code = value(address.elements['.//CountryNameCode/text()'])
42
+ l.precision = PRECISION[address.attribute('Accuracy').value.to_i] || :unknown
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ #
2
+ # extends HTTParty by adding configurable timeout support
3
+ #
4
+ module HTTParty
5
+ class Request
6
+
7
+ private
8
+
9
+ def http
10
+ http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport])
11
+ http.use_ssl = (uri.port == 443)
12
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
13
+ if options[:timeout] && options[:timeout].is_a?(Integer)
14
+ http.open_timeout = options[:timeout]
15
+ http.read_timeout = options[:timeout]
16
+ end
17
+ http
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,228 @@
1
+ module Barometer
2
+ #
3
+ # This class represents a query and can answer the
4
+ # questions that a Barometer will need to measure the weather
5
+ #
6
+ # Summary:
7
+ # When you create a new Query, you set the query string
8
+ # ie: "New York, NY" or "90210"
9
+ # The class will then determine the query string format
10
+ # ie: :zipcode, :postalcode, :geocode, :coordinates
11
+ # Now, when a Weather API driver asks for the query, it will prefer
12
+ # certain formats, and only permit certain formats. The Query class
13
+ # will attempt to either return the query string as-is if acceptable,
14
+ # or it will attempt to convert it to a format that is acceptable
15
+ # (most likely this conversion will in Googles geocoding service using
16
+ # the Graticule gem). Worst case scenario is that the Weather API will
17
+ # not accept the query string.
18
+ #
19
+ class Query
20
+
21
+ # OPTIONAL: key required by Google for geocoding
22
+ @@google_geocode_key = nil
23
+ def self.google_geocode_key; @@google_geocode_key || Barometer.google_geocode_key; end;
24
+ def self.google_geocode_key=(key); @@google_geocode_key = key; end;
25
+
26
+ attr_reader :format
27
+ attr_accessor :q, :preferred, :country_code, :geo
28
+
29
+ def initialize(query=nil)
30
+ @q = query
31
+ self.analyze!
32
+ end
33
+
34
+ # analyze the saved query to determine the format. for the format of
35
+ # :zipcode and :postalcode the country_code can also be set
36
+ def analyze!
37
+ return unless @q
38
+ if Barometer::Query.is_us_zipcode?(@q)
39
+ @format = :zipcode
40
+ elsif Barometer::Query.is_canadian_postcode?(@q)
41
+ @format = :postalcode
42
+ elsif Barometer::Query.is_coordinates?(@q)
43
+ @format = :coordinates
44
+ else
45
+ @format = :geocode
46
+ end
47
+ @country_code = Barometer::Query.format_to_country_code(@format)
48
+ end
49
+
50
+ # take a list of acceptable (and ordered by preference) formats and convert
51
+ # the current query (q) into the most preferred and acceptable format. as a
52
+ # side effect of some conversions, the country_code might be known, then save it
53
+ def convert!(preferred_formats=nil)
54
+ raise ArgumentError unless (preferred_formats && preferred_formats.size > 0)
55
+ # reset preferred
56
+ @preferred = nil
57
+
58
+ # go through each acceptable format and try to convert to that
59
+ converted = false
60
+ geocoded = false
61
+ preferred_formats.each do |preferred_format|
62
+ # we are already in this format, return this
63
+ if preferred_format == @format
64
+ converted = true
65
+ @preferred ||= @q
66
+ end
67
+
68
+ unless converted
69
+ case preferred_format
70
+ when :coordinates
71
+ geocoded = true
72
+ @preferred, @country_code, @geo = Barometer::Query.to_coordinates(@q, @format)
73
+ when :geocode
74
+ geocoded = true
75
+ @preferred, @country_code, @geo = Barometer::Query.to_geocode(@q, @format)
76
+ end
77
+ end
78
+ end
79
+
80
+ # if we haven't already geocoded and we are forcing it, do it now
81
+ if !geocoded && Barometer.force_geocode
82
+ not_used_coords, not_used_code, @geo = Barometer::Query.to_coordinates(@q, @format)
83
+ end
84
+
85
+ @preferred
86
+ end
87
+
88
+ #
89
+ # HELPERS
90
+ #
91
+
92
+ def zipcode?; @format == :zipcode; end
93
+ def postalcode?; @format == :postalcode; end
94
+ def coordinates?; @format == :coordinates; end
95
+ def geocode?; @format == :geocode; end
96
+
97
+ def self.is_us_zipcode?(query)
98
+ us_zipcode_regex = /(^[0-9]{5}$)|(^[0-9]{5}-[0-9]{4}$)/
99
+ return !(query =~ us_zipcode_regex).nil?
100
+ end
101
+
102
+ def self.is_canadian_postcode?(query)
103
+ # Rules: no D, F, I, O, Q, or U anywhere
104
+ # Basic validation: ^[ABCEGHJ-NPRSTVXY]{1}[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[ ]?[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[0-9]{1}$
105
+ # Extended validation: ^(A(0[ABCEGHJ-NPR]|1[ABCEGHK-NSV-Y]|2[ABHNV]|5[A]|8[A])|B(0[CEHJ-NPRSTVW]|1[ABCEGHJ-NPRSTV-Y]|2[ABCEGHJNRSTV-Z]|3[ABEGHJ-NPRSTVZ]|4[ABCEGHNPRV]|5[A]|6[L]|9[A])|C(0[AB]|1[ABCEN])|E(1[ABCEGHJNVWX]|2[AEGHJ-NPRSV]|3[ABCELNVYZ]|4[ABCEGHJ-NPRSTV-Z]|5[ABCEGHJ-NPRSTV]|6[ABCEGHJKL]|7[ABCEGHJ-NP]|8[ABCEGJ-NPRST]|9[ABCEGH])|G(0[ACEGHJ-NPRSTV-Z]|1[ABCEGHJ-NPRSTV-Y]|2[ABCEGJ-N]|3[ABCEGHJ-NZ]|4[ARSTVWXZ]|5[ABCHJLMNRTVXYZ]|6[ABCEGHJKLPRSTVWXZ]|7[ABGHJKNPSTXYZ]|8[ABCEGHJ-NPTVWYZ]|9[ABCHNPRTX])|H(0[HM]|1[ABCEGHJ-NPRSTV-Z]|2[ABCEGHJ-NPRSTV-Z]|3[ABCEGHJ-NPRSTV-Z]|4[ABCEGHJ-NPRSTV-Z]|5[AB]|7[ABCEGHJ-NPRSTV-Y]|8[NPRSTYZ]|9[ABCEGHJKPRSWX])|J(0[ABCEGHJ-NPRSTV-Z]|1[ACEGHJ-NRSTXZ]|2[ABCEGHJ-NRSTWXY]|3[ABEGHLMNPRTVXYZ]|4[BGHJ-NPRSTV-Z]|5[ABCJ-MRTV-Z]|6[AEJKNRSTVWYXZ]|7[ABCEGHJ-NPRTV-Z]|8[ABCEGHLMNPRTVXYZ]|9[ABEHJLNTVXYZ])|K(0[ABCEGHJ-M]|1[ABCEGHJ-NPRSTV-Z]|2[ABCEGHJ-MPRSTVW]|4[ABCKMPR]|6[AHJKTV]|7[ACGHK-NPRSV]|8[ABHNPRV]|9[AHJKLV])|L(0[[ABCEGHJ-NPRS]]|1[ABCEGHJ-NPRSTV-Z]|2[AEGHJMNPRSTVW]|3[BCKMPRSTVXYZ]|4[ABCEGHJ-NPRSTV-Z]|5[ABCEGHJ-NPRSTVW]|6[ABCEGHJ-MPRSTV-Z]|7[ABCEGJ-NPRST]|8[EGHJ-NPRSTVW]|9[ABCGHK-NPRSTVWYZ])|M(1[BCEGHJ-NPRSTVWX]|2[HJ-NPR]|3[ABCHJ-N]|4[ABCEGHJ-NPRSTV-Y]|5[ABCEGHJ-NPRSTVWX]|6[ABCEGHJ-NPRS]|7[AY]|8[V-Z]|9[ABCLMNPRVW])|N(0[ABCEGHJ-NPR]|1[ACEGHKLMPRST]|2[ABCEGHJ-NPRTVZ]|3[ABCEHLPRSTVWY]|4[BGKLNSTVWXZ]|5[ACHLPRV-Z]|6[ABCEGHJ-NP]|7[AGLMSTVWX]|8[AHMNPRSTV-Y]|9[ABCEGHJKVY])|P(0[ABCEGHJ-NPRSTV-Y]|1[ABCHLP]|2[ABN]|3[ABCEGLNPY]|4[NPR]|5[AEN]|6[ABC]|7[ABCEGJKL]|8[NT]|9[AN])|R(0[ABCEGHJ-M]|1[ABN]|2[CEGHJ-NPRV-Y]|3[ABCEGHJ-NPRSTV-Y]|4[AHJKL]|5[AGH]|6[MW]|7[ABCN]|8[AN]|9[A])|S(0[ACEGHJ-NP]|2[V]|3[N]|4[AHLNPRSTV-Z]|6[HJKVWX]|7[HJ-NPRSTVW]|9[AHVX])|T(0[ABCEGHJ-MPV]|1[ABCGHJ-MPRSV-Y]|2[ABCEGHJ-NPRSTV-Z]|3[ABCEGHJ-NPRZ]|4[ABCEGHJLNPRSTVX]|5[ABCEGHJ-NPRSTV-Z]|6[ABCEGHJ-NPRSTVWX]|7[AENPSVXYZ]|8[ABCEGHLNRSVWX]|9[ACEGHJKMNSVWX])|V(0[ABCEGHJ-NPRSTVWX]|1[ABCEGHJ-NPRSTV-Z]|2[ABCEGHJ-NPRSTV-Z]|3[ABCEGHJ-NRSTV-Y]|4[ABCEGK-NPRSTVWXZ]|5[ABCEGHJ-NPRSTV-Z]|6[ABCEGHJ-NPRSTV-Z]|7[ABCEGHJ-NPRSTV-Y]|8[ABCGJ-NPRSTV-Z]|9[ABCEGHJ-NPRSTV-Z])|X(0[ABCGX]|1[A])|Y(0[AB]|1[A]))[ ]?[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[0-9]{1}$
106
+ ca_postcode_regex = /^[A-Z]{1}[\d]{1}[A-Z]{1}[ ]?[\d]{1}[A-Z]{1}[\d]{1}$/
107
+ return !(query =~ ca_postcode_regex).nil?
108
+ end
109
+
110
+ def self.is_coordinates?(query)
111
+ coordinates_regex = /^[-]?[0-9\.]+[,]{1}[-]?[0-9\.]+$/
112
+ return !(query =~ coordinates_regex).nil?
113
+ end
114
+
115
+ #
116
+ # CONVERTERS
117
+ #
118
+
119
+ # this will take all query formats and convert them to coordinates
120
+ # accepts- :zipcode, :postalcode, :geocode
121
+ # returns- :coordinates
122
+ # if the conversion fails, return nil
123
+ def self.to_coordinates(query, format)
124
+ country_code = self.format_to_country_code(format)
125
+ geo = self.geocode(query, country_code)
126
+ country_code ||= geo.country_code if geo
127
+ return nil unless geo && geo.longitude && geo.latitude
128
+ ["#{geo.latitude},#{geo.longitude}", country_code, geo]
129
+ end
130
+
131
+ # this will take all query formats and convert them to coorinates
132
+ # accepts- :zipcode, :postalcode, :coordinates
133
+ # returns- :geocode
134
+ def self.to_geocode(query, format)
135
+ perform_geocode = false
136
+ perform_geocode = true if self.has_geocode_key?
137
+
138
+ # some formats can't convert, no need to geocode then
139
+ skip_formats = [:postalcode]
140
+ perform_geocode = false if skip_formats.include?(format)
141
+
142
+ country_code = self.format_to_country_code(format)
143
+ if perform_geocode
144
+ geo = self.geocode(query, country_code)
145
+ country_code ||= geo.country_code if geo
146
+ return nil unless geo && geo.locality && geo.region && geo.country
147
+ return ["#{geo.locality}, #{geo.region}, #{geo.country}", country_code, geo]
148
+ else
149
+ # without geocoding, the best we can do is just make use the given query as
150
+ # the query for the "geocode" format
151
+ return [query, country_code, nil]
152
+ end
153
+ return nil
154
+ end
155
+
156
+ #
157
+ # --- TODO ---
158
+ # The following methods need more coverage tests
159
+ #
160
+
161
+ def self.has_geocode_key?
162
+ # quick check to see that the Google API key exists for geocoding
163
+ self.google_geocode_key && !self.google_geocode_key.nil?
164
+ end
165
+
166
+ # if Graticule exists, use it, otherwise use HTTParty
167
+ def self.geocode(query, country_code=nil)
168
+ use_graticule = false
169
+ unless Barometer::skip_graticule
170
+ begin
171
+ require 'rubygems'
172
+ require 'graticule'
173
+ $:.unshift(File.dirname(__FILE__))
174
+ # load some changes to Graticule
175
+ # TODO: attempt to get changes into Graticule gem
176
+ require 'extensions/graticule'
177
+ use_graticule = true
178
+ rescue LoadError
179
+ # do nothing, we will use HTTParty
180
+ end
181
+ end
182
+
183
+ if use_graticule
184
+ geo = self.geocode_graticule(query, country_code)
185
+ else
186
+ geo = self.geocode_httparty(query, country_code)
187
+ end
188
+ geo
189
+ end
190
+
191
+ def self.geocode_graticule(query, country_code=nil)
192
+ return nil unless self.has_geocode_key?
193
+ geocoder = Graticule.service(:google).new(self.google_geocode_key)
194
+ location = geocoder.locate(query, country_code)
195
+ geo = Barometer::Geo.new(location)
196
+ end
197
+
198
+ def self.geocode_httparty(query, country_code=nil)
199
+ return nil unless self.has_geocode_key?
200
+ location = Barometer::Service.get(
201
+ "http://maps.google.com/maps/geo",
202
+ :query => {
203
+ :gl => country_code,
204
+ :key => self.google_geocode_key,
205
+ :output => "xml",
206
+ :q => query
207
+ },
208
+ :format => :xml
209
+ )['kml']['Response']
210
+ #puts location.inspect
211
+ geo = Barometer::Geo.new(location)
212
+ end
213
+
214
+ def self.format_to_country_code(format)
215
+ return nil unless format
216
+ case format
217
+ when :zipcode
218
+ country_code = "US"
219
+ when :postalcode
220
+ country_code = "CA"
221
+ else
222
+ country_code = nil
223
+ end
224
+ country_code
225
+ end
226
+
227
+ end
228
+ end