attack-barometer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +266 -0
- data/VERSION.yml +4 -0
- data/bin/barometer +63 -0
- data/lib/barometer/base.rb +52 -0
- data/lib/barometer/data/current.rb +93 -0
- data/lib/barometer/data/distance.rb +131 -0
- data/lib/barometer/data/forecast.rb +66 -0
- data/lib/barometer/data/geo.rb +98 -0
- data/lib/barometer/data/location.rb +20 -0
- data/lib/barometer/data/measurement.rb +161 -0
- data/lib/barometer/data/pressure.rb +133 -0
- data/lib/barometer/data/speed.rb +147 -0
- data/lib/barometer/data/sun.rb +35 -0
- data/lib/barometer/data/temperature.rb +164 -0
- data/lib/barometer/data/units.rb +55 -0
- data/lib/barometer/data/zone.rb +124 -0
- data/lib/barometer/data.rb +15 -0
- data/lib/barometer/extensions/graticule.rb +50 -0
- data/lib/barometer/extensions/httparty.rb +21 -0
- data/lib/barometer/query.rb +228 -0
- data/lib/barometer/services/google.rb +146 -0
- data/lib/barometer/services/noaa.rb +6 -0
- data/lib/barometer/services/service.rb +324 -0
- data/lib/barometer/services/weather_bug.rb +6 -0
- data/lib/barometer/services/weather_dot_com.rb +6 -0
- data/lib/barometer/services/wunderground.rb +285 -0
- data/lib/barometer/services/yahoo.rb +274 -0
- data/lib/barometer/services.rb +6 -0
- data/lib/barometer/weather.rb +187 -0
- data/lib/barometer.rb +52 -0
- data/spec/barometer_spec.rb +162 -0
- data/spec/data_current_spec.rb +225 -0
- data/spec/data_distance_spec.rb +336 -0
- data/spec/data_forecast_spec.rb +150 -0
- data/spec/data_geo_spec.rb +90 -0
- data/spec/data_location_spec.rb +59 -0
- data/spec/data_measurement_spec.rb +411 -0
- data/spec/data_pressure_spec.rb +336 -0
- data/spec/data_speed_spec.rb +374 -0
- data/spec/data_sun_spec.rb +76 -0
- data/spec/data_temperature_spec.rb +396 -0
- data/spec/data_zone_spec.rb +133 -0
- data/spec/fixtures/current_calgary_ab.xml +1 -0
- data/spec/fixtures/forecast_calgary_ab.xml +1 -0
- data/spec/fixtures/geocode_40_73.xml +1 -0
- data/spec/fixtures/geocode_90210.xml +1 -0
- data/spec/fixtures/geocode_T5B4M9.xml +1 -0
- data/spec/fixtures/geocode_calgary_ab.xml +1 -0
- data/spec/fixtures/geocode_newyork_ny.xml +1 -0
- data/spec/fixtures/google_calgary_ab.xml +1 -0
- data/spec/fixtures/yahoo_90210.xml +1 -0
- data/spec/query_spec.rb +469 -0
- data/spec/service_google_spec.rb +144 -0
- data/spec/service_wunderground_spec.rb +330 -0
- data/spec/service_yahoo_spec.rb +299 -0
- data/spec/services_spec.rb +1106 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/units_spec.rb +101 -0
- data/spec/weather_spec.rb +265 -0
- 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
|