lib/barometer/query.rb in attack-barometer-0.3.2 vs lib/barometer/query.rb in attack-barometer-0.5.0
- old
+ new
@@ -10,268 +10,97 @@
# ie: :zipcode, :postalcode, :geocode, :coordinates
# Now, when a Weather API driver asks for the query, it will prefer
# certain formats, and only permit certain formats. The Query class
# will attempt to either return the query string as-is if acceptable,
# or it will attempt to convert it to a format that is acceptable
- # (most likely this conversion will in Googles geocoding service using
+ # (most likely this conversion will use Googles geocoding service using
# the Graticule gem). Worst case scenario is that the Weather API will
# not accept the query string.
#
class Query
- # OPTIONAL: key required by Google for geocoding
- @@google_geocode_key = nil
- def self.google_geocode_key; @@google_geocode_key || Barometer.google_geocode_key; end;
- def self.google_geocode_key=(key); @@google_geocode_key = key; end;
+ # This array defines the order to check a query for the format
+ #
+ FORMATS = %w(
+ ShortZipcode Zipcode Postalcode WeatherID Coordinates Icao Geocode
+ )
+ FORMAT_MAP = {
+ :short_zipcode => "ShortZipcode", :zipcode => "Zipcode",
+ :postalcode => "Postalcode", :weather_id => "WeatherID",
+ :coordinates => "Coordinates", :icao => "Icao",
+ :geocode => "Geocode"
+ }
- attr_reader :format
- attr_accessor :q, :preferred, :country_code, :geo
+ attr_accessor :format, :q, :country_code, :geo
def initialize(query=nil)
+ return unless query
@q = query
self.analyze!
end
-
+
# analyze the saved query to determine the format.
+ # this delegates the detection to each formats class
+ # until th right one is found
+ #
def analyze!
return unless @q
- if Barometer::Query.is_us_zipcode?(@q)
- @format = :zipcode
- @country_code = Barometer::Query.format_to_country_code(@format)
- elsif Barometer::Query.is_canadian_postcode?(@q)
- @format = :postalcode
- @country_code = Barometer::Query.format_to_country_code(@format)
- elsif Barometer::Query.is_coordinates?(@q)
- @format = :coordinates
- elsif Barometer::Query.is_icao?(@q)
- @format = :icao
-# @country_code = Barometer::Query.icao_to_country_code(@q)
- else
- @format = :geocode
+ FORMATS.each do |format|
+ if Query::Format.const_get(format.to_s).is?(@q)
+ @format = Query::Format.const_get(format.to_s).format
+ @country_code = Query::Format.const_get(format.to_s).country_code(@q)
+ break
+ end
end
end
# take a list of acceptable (and ordered by preference) formats and convert
- # the current query (q) into the most preferred and acceptable format. as a
- # side effect of some conversions, the country_code might be known, then save it
+ # the current query (q) into the most preferred and acceptable format. a
+ # side effect of the conversions may reveal the country_code, if so save it
+ #
def convert!(preferred_formats=nil)
raise ArgumentError unless (preferred_formats && preferred_formats.size > 0)
- # reset preferred
- @preferred = nil
- # go through each acceptable format and try to convert to that
- converted = false
- geocoded = false
- preferred_formats.each do |preferred_format|
- # we are already in this format, return this
- if preferred_format == @format
- converted = true
- @preferred ||= @q
- end
-
- unless converted
- case preferred_format
- when :coordinates
- geocoded = true
- @preferred, @country_code, @geo = Barometer::Query.to_coordinates(@q, @format)
- when :geocode
- geocoded = true
- @preferred, @country_code, @geo = Barometer::Query.to_geocode(@q, @format)
+ # why convert if we are already there?
+ skip_conversion = false
+ if preferred_formats.include?(@format.to_sym)
+ skip_conversion = true
+ converted_query = self.dup
+ end
+
+ unless skip_conversion
+ # go through each acceptable format and try to convert to that
+ converted = false
+ converted_query = Barometer::Query.new
+ preferred_formats.each do |preferred_format|
+ klass = FORMAT_MAP[preferred_format.to_sym]
+ if preferred_format == @format
+ converted = true
+ converted_query = Barometer::Query.new(@q)
end
+ unless converted
+ converted_query = Query::Format.const_get(klass.to_s).to(self)
+ converted = true if converted_query
+ end
+ if converted
+ converted_query.country_code ||= Query::Format.const_get(klass.to_s).country_code(converted_query.q)
+ break
+ end
end
end
- # if we haven't already geocoded and we are forcing it, do it now
- if !geocoded && Barometer.force_geocode
- not_used_coords, not_used_code, @geo = Barometer::Query.to_coordinates(@q, @format)
- end
-
- @preferred
- end
-
- #
- # HELPERS
- #
-
- def zipcode?; @format == :zipcode; end
- def postalcode?; @format == :postalcode; end
- def coordinates?; @format == :coordinates; end
- def geocode?; @format == :geocode; end
- def icao?; @format == :icao; end
-
- def self.is_us_zipcode?(query)
- us_zipcode_regex = /(^[0-9]{5}$)|(^[0-9]{5}-[0-9]{4}$)/
- return !(query =~ us_zipcode_regex).nil?
- end
-
- def self.is_canadian_postcode?(query)
- # Rules: no D, F, I, O, Q, or U anywhere
- # Basic validation: ^[ABCEGHJ-NPRSTVXY]{1}[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[ ]?[0-9]{1}[ABCEGHJ-NPRSTV-Z]{1}[0-9]{1}$
- # 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}$
- ca_postcode_regex = /^[A-Z]{1}[\d]{1}[A-Z]{1}[ ]?[\d]{1}[A-Z]{1}[\d]{1}$/
- return !(query =~ ca_postcode_regex).nil?
- end
-
- def self.is_coordinates?(query)
- coordinates_regex = /^[-]?[0-9\.]+[,]{1}[-]?[0-9\.]+$/
- return !(query =~ coordinates_regex).nil?
- end
-
- def self.is_icao?(query)
- # allow any 3 or 4 letter word ... unfortunately this means some locations
- # (ie Utah, Goa, Kiev, etc) will be detected as ICAO. This won't matter for
- # returning weather results ... it will just effect what happens to the query.
- # For example, wunderground will accept :icao above :coordinates and :geocode,
- # which means that a city like Kiev would normally get converted to :coordinates
- # but in this case it will be detected as :icao so it will be passed as is.
- # Currently, only wunderground accepts ICAO, and they process ICAO the same as a
- # city name, so it doesn't matter.
- icao_regex = /^[A-Za-z]{3,4}$/
- return !(query =~ icao_regex).nil?
- end
-
- #
- # CONVERTERS
- #
-
- # this will take all query formats and convert them to coordinates
- # accepts- :zipcode, :postalcode, :geocode, :icao
- # returns- :coordinates
- # if the conversion fails, return nil
- def self.to_coordinates(query, format)
- country_code = self.format_to_country_code(format)
- geo = self.geocode(query, country_code)
- country_code ||= geo.country_code if geo
- return nil unless geo && geo.longitude && geo.latitude
- ["#{geo.latitude},#{geo.longitude}", country_code, geo]
- end
-
- # this will take all query formats and convert them to coorinates
- # accepts- :zipcode, :postalcode, :coordinates, :icao
- # returns- :geocode
- def self.to_geocode(query, format)
- perform_geocode = false
- perform_geocode = true if self.has_geocode_key?
-
- # some formats can't convert, no need to geocode then
- skip_formats = [:postalcode]
- perform_geocode = false if skip_formats.include?(format)
-
- country_code = self.format_to_country_code(format)
- if perform_geocode
- geo = self.geocode(query, country_code)
- country_code ||= geo.country_code if geo
- # different formats have different acceptance criteria
- q = nil
- case format
- when :icao
- return nil unless geo && geo.address && geo.country
- q = "#{geo.address}, #{geo.country}"
+ # force geocode?, unless we already did
+ #
+ if Barometer.force_geocode && !@geo
+ if converted_query && converted_query.geo
+ @geo = converted_query.geo
else
- return nil unless geo && geo.locality && geo.region && geo.country
- q = "#{geo.locality}, #{geo.region}, #{geo.country}"
+ geo_query = Query::Format::Coordinates.to(converted_query)
+ @geo = geo_query.geo if (geo_query && geo_query.geo)
end
- return [q, country_code, geo]
- else
- # without geocoding, the best we can do is just make use the given query as
- # the query for the "geocode" format
- return [query, country_code, nil]
end
- return nil
+
+ converted_query
end
-
- #
- # --- TODO ---
- # The following methods need more coverage tests
- #
-
- def self.has_geocode_key?
- # quick check to see that the Google API key exists for geocoding
- self.google_geocode_key && !self.google_geocode_key.nil?
- end
- # if Graticule exists, use it, otherwise use HTTParty
- def self.geocode(query, country_code=nil)
- use_graticule = false
- unless Barometer::skip_graticule
- begin
- require 'rubygems'
- require 'graticule'
- $:.unshift(File.dirname(__FILE__))
- # load some changes to Graticule
- # TODO: attempt to get changes into Graticule gem
- require 'extensions/graticule'
- use_graticule = true
- rescue LoadError
- # do nothing, we will use HTTParty
- end
- end
-
- if use_graticule
- geo = self.geocode_graticule(query, country_code)
- else
- geo = self.geocode_httparty(query, country_code)
- end
- geo
- end
-
- def self.geocode_graticule(query, country_code=nil)
- return nil unless self.has_geocode_key?
- geocoder = Graticule.service(:google).new(self.google_geocode_key)
- location = geocoder.locate(query, country_code)
- geo = Barometer::Geo.new(location)
- end
-
- def self.geocode_httparty(query, country_code=nil)
- return nil unless self.has_geocode_key?
- location = Barometer::Service.get(
- "http://maps.google.com/maps/geo",
- :query => {
- :gl => country_code,
- :key => self.google_geocode_key,
- :output => "xml",
- :q => query
- },
- :format => :xml
- )['kml']['Response']
- geo = Barometer::Geo.new(location)
- end
-
- def self.format_to_country_code(format)
- return nil unless format
- case format
- when :zipcode
- country_code = "US"
- when :postalcode
- country_code = "CA"
- else
- country_code = nil
- end
- country_code
- end
-
- # todo, the fist letter in a 4-letter icao can designate country:
- # c=canada
- # k=usa
- # etc...
- # def self.icao_to_country_code(icao_code)
- # return unless icao_code.is_a?(String)
- # country_code = nil
- # if icao_code.size == 4
- # case icao_code.first_letter
- # when "C"
- # country_code = "CA"
- # when "K"
- # country_code = "US"
- # end
- # if coutry_code.nil?
- # case icao_code.first_two_letters
- # when "ET"
- # country_code = "GERMANY"
- # end
- # end
- # end
- # country_code
- # end
-
end
end