diff --git a/lib/noosfero/geo_ref.rb b/lib/noosfero/geo_ref.rb index f7cf2c4..d9ef265 100644 --- a/lib/noosfero/geo_ref.rb +++ b/lib/noosfero/geo_ref.rb @@ -1,6 +1,84 @@ module Noosfero::GeoRef - KM_LAT = 111.2 # aproximate distance in km for 1 degree latitude - KM_LNG = 85.3 # aproximate distance in km for 1 degree longitude + # May replace this module by http://www.postgresql.org/docs/9.3/static/earthdistance.html + + EARTH_RADIUS = 6378 # aproximate in km + + class << self + + def dist(lat1, lng1, lat2, lng2) + def deg2rad(d); (d*Math::PI)/180; end + def c(n); Math.cos(n); end + def s(n); Math.sin(n); end + lat1 = deg2rad lat1 + lat2 = deg2rad lat2 + dlng = deg2rad(lng2) - deg2rad(lng1) + EARTH_RADIUS * Math.atan2( + Math.sqrt( + ( c(lat2) * s(dlng) )**2 + + ( c(lat1) * s(lat2) - s(lat1) * c(lat2) * c(dlng) )**2 + ), + s(lat1) * s(lat2) + c(lat1) * c(lat2) * c(dlng) + ) + end + + # Write a SQL expression to return the distance from a profile to a + # reference point, in kilometers. + # http://www.plumislandmedia.net/mysql/vicenty-great-circle-distance-formula + def sql_dist(ref_lat, ref_lng) + "2*PI()*#{EARTH_RADIUS}*( + DEGREES( + ATAN2( + SQRT( + POW(COS(RADIANS(#{ref_lat}))*SIN(RADIANS(#{ref_lng}-lng)),2) + + POW( + COS(RADIANS(lat)) * SIN(RADIANS(#{ref_lat})) - ( + SIN(RADIANS(lat)) * COS(RADIANS(#{ref_lat})) * COS(RADIANS(#{ref_lng}-lng)) + ), 2 + ) + ), + SIN(RADIANS(lat)) * SIN(RADIANS(#{ref_lat})) + + COS(RADIANS(lat)) * COS(RADIANS(#{ref_lat})) * COS(RADIANS(#{ref_lng}-lng)) + ) + )/360 + )" + end + + # Asks Google for the georef of a location. + def location_to_georef(location) + key = location.downcase + ll = Rails.cache.read key + return ll + [:CACHE] if ll.kind_of? Array + resp = RestClient.get 'https://maps.googleapis.com/maps/api/geocode/json?' + + 'sensor=false&address=' + url_encode(location) + if resp.nil? || resp.code.to_i != 200 + if ENV['RAILS_ENV'] == 'test' + print " Google Maps API fail (code #{resp ? resp.code : :nil}) " + else + Rails.logger.warn "Google Maps API request information for " + + "\"#{location}\" fail. (code #{resp ? resp.code : :nil})" + end + return [ 0, 0, "HTTP_FAIL_#{resp.code}".to_sym ] # do not cache failed response + else + json = JSON.parse resp.body + if json && (r=json['results']) && (r=r[0]) && (r=r['geometry']) && + (r=r['location']) && r['lat'] + ll = [ r['lat'], r['lng'], :SUCCESS ] + else + status = json['status'] || 'Undefined Error' + message = "Google Maps API cant find \"#{location}\" (#{status})" + if ENV['RAILS_ENV'] == 'test' + print " #{message} " + else + Rails.logger.warn message + end + ll = [ 0, 0, status.to_sym ] + end + Rails.cache.write key, ll + end + ll + end + + end end diff --git a/test/unit/geo_ref_test.rb b/test/unit/geo_ref_test.rb new file mode 100644 index 0000000..246c5cc --- /dev/null +++ b/test/unit/geo_ref_test.rb @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +require File.dirname(__FILE__) + '/../test_helper' + +class GeoRefTest < ActiveSupport::TestCase + + ll = { + salvador: [-12.9, -38.5], + rio_de_janeiro: [-22.9, -43.1], + new_york: [ 40.7, -74.0], + tokyo: [ 35.6, 139.6] + } + + should 'calculate the distance between lat,lng points' do + assert_equal 1215, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:rio_de_janeiro])).round + assert_equal 6998, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:new_york])).round + assert_equal 17503, Noosfero::GeoRef.dist(*(ll[:salvador]+ll[:tokyo])).round + end + + should 'calculate the distance between a lat,lng points and a profile' do + env = fast_create Environment, name: 'SomeSite' + @acme = Enterprise.create! environment: env, identifier: 'acme', name: 'ACME', + city: 'Salvador', state: 'Bahia', country: 'BR', lat: -12.9, lng: -38.5 + def sql_dist_to(ll) + ActiveRecord::Base.connection.execute( + "SELECT #{Noosfero::GeoRef.sql_dist ll[0], ll[1]} as dist" + + " FROM profiles WHERE id = #{@acme.id};" + ).first['dist'].to_f.round + end + assert_equal 1215, sql_dist_to(ll[:rio_de_janeiro]) + assert_equal 6998, sql_dist_to(ll[:new_york]) + assert_equal 17503, sql_dist_to(ll[:tokyo]) + end + + def round_ll(ll) + ll.map{|n| n.is_a?(Float) ? n.to_i : n } + end + + should 'get lat/lng from address' do + Rails.cache.clear + ll = Noosfero::GeoRef.location_to_georef 'Salvador, Bahia, BR' + assert_equal [-12, -38, :SUCCESS], round_ll(ll) + end + + should 'get and cache lat/lng from address' do + Rails.cache.clear + ll = Noosfero::GeoRef.location_to_georef 'Curitiba, ParanĂ¡, BR' + assert_equal [-25, -49, :SUCCESS], round_ll(ll) + ll = Noosfero::GeoRef.location_to_georef 'Curitiba, ParanĂ¡, BR' + assert_equal [-25, -49, :SUCCESS, :CACHE], round_ll(ll) + end + + should 'notify a non existent address' do + Rails.cache.clear + orig_env = ENV['RAILS_ENV'] + ENV['RAILS_ENV'] = 'X' # cancel throw for test mode on process_rest_req. + ll = Noosfero::GeoRef.location_to_georef 'Nowhere, Nocountry, XYZ' + ENV['RAILS_ENV'] = orig_env # restore value to do not mess with other tests. + assert_equal [0, 0, :ZERO_RESULTS], round_ll(ll) + end + +end -- libgit2 0.21.2