geogov 0.0.1
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.
- data/Gemfile +5 -0
- data/Rakefile +15 -0
- data/lib/geogov.rb +53 -0
- data/lib/geogov/fuzzy_point.rb +23 -0
- data/lib/geogov/geo_stack.rb +140 -0
- data/lib/geogov/providers/dracos_gazetteer.rb +19 -0
- data/lib/geogov/providers/geonames.rb +48 -0
- data/lib/geogov/providers/google.rb +48 -0
- data/lib/geogov/providers/hostip.rb +34 -0
- data/lib/geogov/providers/mapit.rb +113 -0
- data/lib/geogov/providers/open_street_map.rb +51 -0
- data/lib/geogov/utils.rb +87 -0
- data/test/geogov_test.rb +64 -0
- data/test/test_helper.rb +21 -0
- metadata +129 -0
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
task :default => [:test_units]
|
8
|
+
|
9
|
+
desc "Run basic tests"
|
10
|
+
Rake::TestTask.new("test_units") { |t|
|
11
|
+
t.libs << "test"
|
12
|
+
t.pattern = 'test/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
t.warning = true
|
15
|
+
}
|
data/lib/geogov.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'geogov/providers/open_street_map'
|
4
|
+
require 'geogov/providers/geonames'
|
5
|
+
require 'geogov/providers/google'
|
6
|
+
require 'geogov/providers/mapit'
|
7
|
+
require 'geogov/utils'
|
8
|
+
require 'geogov/geo_stack'
|
9
|
+
require 'geogov/fuzzy_point'
|
10
|
+
require 'geogov/providers/hostip'
|
11
|
+
require 'geogov/providers/dracos_gazetteer'
|
12
|
+
|
13
|
+
module Geogov
|
14
|
+
|
15
|
+
def self.provider_for(method, instance)
|
16
|
+
caching_instance = SimpleCache.new(instance)
|
17
|
+
@@methods ||= {}
|
18
|
+
@@methods[method] = caching_instance
|
19
|
+
unless self.methods().include?(method)
|
20
|
+
dispatcher = <<-EOS
|
21
|
+
def #{method}(*args, &block)
|
22
|
+
@@methods[:#{method}].__send__(#{method.inspect}, *args, &block)
|
23
|
+
end
|
24
|
+
EOS
|
25
|
+
module_eval(dispatcher)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.configure
|
30
|
+
yield self
|
31
|
+
end
|
32
|
+
|
33
|
+
provider_for :nearest_place_name, DracosGazetteer.new()
|
34
|
+
|
35
|
+
provider_for :lat_lon_to_country, Geonames.new()
|
36
|
+
provider_for :centre_of_country, Geonames.new()
|
37
|
+
|
38
|
+
provider_for :centre_of_district, Mapit.new()
|
39
|
+
provider_for :areas_for_stack_from_postcode, Mapit.new()
|
40
|
+
provider_for :areas_for_stack_from_coords, Mapit.new()
|
41
|
+
provider_for :lat_lon_from_postcode, Mapit.new()
|
42
|
+
|
43
|
+
provider_for :remote_location, Hostip.new()
|
44
|
+
|
45
|
+
provider_for :map_img, Google.new()
|
46
|
+
provider_for :map_href, Google.new()
|
47
|
+
|
48
|
+
extend self
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Geogov
|
2
|
+
|
3
|
+
class FuzzyPoint
|
4
|
+
ACCURACIES = [:point,:postcode,:postcode_district,:ward,:council,:nation,:country,:planet]
|
5
|
+
attr_reader :lon, :lat, :accuracy
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(lat,lon,accuracy)
|
9
|
+
accuracy = accuracy.to_sym
|
10
|
+
raise ValueError unless ACCURACIES.include?(accuracy)
|
11
|
+
@lon,@lat,@accuracy = lon.to_f, lat.to_f, accuracy
|
12
|
+
if @accuracy == :point
|
13
|
+
@lon = @lon.round(2)
|
14
|
+
@lat = @lat.round(2)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
{"lon"=> self.lon.to_s,"lat"=>self.lat.to_s,"accuracy"=>accuracy.to_s}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Geogov
|
2
|
+
class GeoStack
|
3
|
+
|
4
|
+
attr_accessor :postcode,:ward,:council,:nation,:country,:wmc,:lat,:lon
|
5
|
+
attr_accessor :fuzzy_point
|
6
|
+
attr_accessor :friendly_name
|
7
|
+
|
8
|
+
def initialize()
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def calculate_fuzzy_point
|
13
|
+
if self.lat and self.lon
|
14
|
+
return FuzzyPoint.new(self.lat, self.lon, :point)
|
15
|
+
end
|
16
|
+
|
17
|
+
if self.postcode
|
18
|
+
district = postcode.split(" ")[0]
|
19
|
+
district_centre = Geogov.centre_of_district(district)
|
20
|
+
if district_centre
|
21
|
+
return FuzzyPoint.new(district_centre["lat"],district_centre["lon"],:postcode_district)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if self.country
|
26
|
+
country_centre = Geogov.centre_of_country(self.country)
|
27
|
+
if country_centre
|
28
|
+
return FuzzyPoint.new(country_centre["lat"],country_centre["lon"],:country)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
FuzzyPoint.new(0,0,:planet)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.new_from_ip(ip_address)
|
36
|
+
remote_location = Geogov.remote_location(ip_address)
|
37
|
+
new() do |gs|
|
38
|
+
if remote_location
|
39
|
+
gs.country = remote_location['country']
|
40
|
+
end
|
41
|
+
gs.fuzzy_point = gs.calculate_fuzzy_point
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.new_from_hash(hash)
|
46
|
+
new() do |gs|
|
47
|
+
gs.set_fields(hash)
|
48
|
+
unless hash['fuzzy_point']
|
49
|
+
raise ArgumentError, "fuzzy point required"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_hash
|
55
|
+
{
|
56
|
+
:fuzzy_point => self.fuzzy_point.to_hash,
|
57
|
+
:postcode => self.postcode,
|
58
|
+
:ward => self.ward,
|
59
|
+
:council => self.council,
|
60
|
+
:nation => self.nation,
|
61
|
+
:country => self.country,
|
62
|
+
:wmc => self.wmc,
|
63
|
+
:friendly_name => self.friendly_name
|
64
|
+
}.select {|k,v| !(v.nil?) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def update(hash)
|
68
|
+
self.class.new() do |empty|
|
69
|
+
full_postcode = hash['postcode']
|
70
|
+
empty.set_fields(hash)
|
71
|
+
if has_valid_lat_lon(hash)
|
72
|
+
empty.fetch_missing_fields_for_coords(hash['lat'], hash['lon'])
|
73
|
+
elsif full_postcode
|
74
|
+
empty.fetch_missing_fields_for_postcode(full_postcode)
|
75
|
+
end
|
76
|
+
empty.fuzzy_point = empty.calculate_fuzzy_point
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_valid_lat_lon(hash)
|
81
|
+
return (hash['lon'] and hash['lat'] and hash['lon'] != "" and hash['lat'] != "")
|
82
|
+
end
|
83
|
+
|
84
|
+
def fetch_missing_fields_for_postcode(postcode)
|
85
|
+
if matches = postcode.match(POSTCODE_REGEXP)
|
86
|
+
self.country = "UK"
|
87
|
+
fields = Geogov.areas_for_stack_from_postcode(postcode)
|
88
|
+
if fields
|
89
|
+
lat_lon = fields[:point]
|
90
|
+
if lat_lon
|
91
|
+
self.friendly_name = Geogov.nearest_place_name(lat_lon['lat'],lat_lon['lon'])
|
92
|
+
end
|
93
|
+
set_fields(fields.select {|k,v| k != :point})
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch_missing_fields_for_coords(lat, lon)
|
99
|
+
self.friendly_name = Geogov.nearest_place_name(lat, lon)
|
100
|
+
fields = Geogov.areas_for_stack_from_coords(lat, lon)
|
101
|
+
if ['England', 'Scotland', 'Northern Ireland', 'Wales'].include?(fields[:nation])
|
102
|
+
self.country = 'UK'
|
103
|
+
set_fields(fields.select {|k,v| k != :point})
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_fields(hash)
|
108
|
+
hash.each do |geo, value|
|
109
|
+
setter = (geo.to_s+"=").to_sym
|
110
|
+
if self.respond_to?(setter)
|
111
|
+
unless value == ""
|
112
|
+
self.send(setter,value)
|
113
|
+
end
|
114
|
+
else
|
115
|
+
raise ArgumentError, "geo type '#{geo}' is not a valid geo type"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
def fuzzy_point=(point)
|
122
|
+
if point.is_a?(Hash)
|
123
|
+
@fuzzy_point = FuzzyPoint.new(point["lat"],point["lon"],point["accuracy"])
|
124
|
+
else
|
125
|
+
@fuzzy_point = point
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
POSTCODE_REGEXP = /^([A-Z]{1,2}[0-9R][0-9A-Z]?)\s*([0-9])[ABD-HJLNP-UW-Z]{2}(:?\s+)?$/i
|
130
|
+
SECTOR_POSTCODE_REGEXP = /^([A-Z]{1,2}[0-9R][0-9A-Z]?)\s*([0-9])(:?\s+)?$/i
|
131
|
+
|
132
|
+
def postcode=(postcode)
|
133
|
+
if (matches = (postcode.match(POSTCODE_REGEXP) || postcode.match(SECTOR_POSTCODE_REGEXP)))
|
134
|
+
@postcode = matches[1]+" "+matches[2]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Geogov
|
2
|
+
class DracosGazetteer
|
3
|
+
def initialize(default_url = "http://gazetteer.dracos.vm.bytemark.co.uk")
|
4
|
+
@base = default_url
|
5
|
+
end
|
6
|
+
|
7
|
+
def nearest_place_name(lat,lon)
|
8
|
+
url = "#{@base}/point/#{lat},#{lon}.json"
|
9
|
+
results = Geogov.get_json(url)
|
10
|
+
if results && results["place"]
|
11
|
+
return results["place"][0]
|
12
|
+
else
|
13
|
+
return nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Geogov
|
2
|
+
class Geonames
|
3
|
+
def initialize(username = "username", url = "http://api.geonames.org")
|
4
|
+
@url = url
|
5
|
+
@username = username
|
6
|
+
end
|
7
|
+
|
8
|
+
def query(method,params)
|
9
|
+
params = {"username"=>@username}.merge(params)
|
10
|
+
Geogov.get_json("#{@url}/#{method}?"+Geogov.hash_to_params(params))
|
11
|
+
end
|
12
|
+
|
13
|
+
def nearest_place_name(lat,lon)
|
14
|
+
params = { "lat" => lat, "lng" => lon}
|
15
|
+
results = query("findNearbyPlaceNameJSON",params)
|
16
|
+
if results && results["geonames"]
|
17
|
+
return results["geonames"][0]["name"]
|
18
|
+
else
|
19
|
+
return nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def centre_of_country(country_code)
|
24
|
+
params = { "country" => country_code, "type" => "JSON" }
|
25
|
+
results = query("countryInfo",params)
|
26
|
+
if results && results["geonames"] && results["geonames"][0]
|
27
|
+
country = results["geonames"][0]
|
28
|
+
bbe, bbw = country["bBoxEast"],country["bBoxWest"]
|
29
|
+
bbn, bbs = country["bBoxNorth"],country["bBoxSouth"]
|
30
|
+
lon,lat = (bbe.to_f+bbw.to_f)/2,(bbn.to_f+bbs.to_f)/2
|
31
|
+
return { "lat" => lat, "lon" => lon }
|
32
|
+
else
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def lat_lon_to_country(lat,lon)
|
38
|
+
params = { "lat" => lat, "lng" => lon, 'type'=>"JSON"}
|
39
|
+
results = query("countryCode",params)
|
40
|
+
if results && results["countryCode"]
|
41
|
+
return results["countryCode"]
|
42
|
+
else
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Geogov
|
2
|
+
|
3
|
+
class Google
|
4
|
+
|
5
|
+
def dimension(l1,l2)
|
6
|
+
"#{l1}x#{l2}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def location(l1,l2)
|
10
|
+
"#{l1},#{l2}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def map_img(lat,lon,options= {})
|
14
|
+
g_options = {
|
15
|
+
:zoom => options[:z] || 14,
|
16
|
+
:size => dimension(options[:w],options[:h]),
|
17
|
+
:center => location(lat,lon),
|
18
|
+
:sensor => false
|
19
|
+
}
|
20
|
+
if options[:marker_lat] && options[:marker_lon]
|
21
|
+
location = location(options[:marker_lat],options[:marker_lon])
|
22
|
+
g_options[:markers] = ["color:blue",location].join("|")
|
23
|
+
end
|
24
|
+
|
25
|
+
params = Geogov.hash_to_params(g_options)
|
26
|
+
|
27
|
+
"http://maps.google.com/maps/api/staticmap?#{params}"
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def map_href(lat,lon,options = {})
|
32
|
+
g_options = {
|
33
|
+
:z => options[:z] || 14,
|
34
|
+
:ie => "UTF8",
|
35
|
+
:q => location(lat,lon)
|
36
|
+
}
|
37
|
+
if options[:marker_lat] && options[:marker_lon]
|
38
|
+
location = location(options[:marker_lat],options[:marker_lon])
|
39
|
+
g_options[:sll] = location
|
40
|
+
end
|
41
|
+
|
42
|
+
params = Geogov.hash_to_params(g_options)
|
43
|
+
"http://maps.google.com/maps?#{params}"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Geogov
|
4
|
+
|
5
|
+
class Hostip
|
6
|
+
def initialize
|
7
|
+
@url = 'http://api.hostip.info/get_html.php'
|
8
|
+
end
|
9
|
+
|
10
|
+
def remote_location(ip_address)
|
11
|
+
params = {:ip => ip_address, :position => true}
|
12
|
+
results = Geogov.get(@url + "?" + Geogov.hash_to_params(params))
|
13
|
+
return nil if results.nil?
|
14
|
+
response = YAML.load(results + "\n")
|
15
|
+
location = {}.tap do |h|
|
16
|
+
h["lat"] = response['Latitude']
|
17
|
+
h["lon"] = response['Longitude']
|
18
|
+
h["city"], h["county"] = response['City'].split(', ')
|
19
|
+
country = response['Country'].match(/\((\w+)\)$/)
|
20
|
+
h["country"] = country[1] if country
|
21
|
+
end
|
22
|
+
return nil if location['city'] =~ /Unknown City/
|
23
|
+
return nil if location['city'] =~ /Private Address/
|
24
|
+
|
25
|
+
# I found these very unreliable, so better they're
|
26
|
+
# not there to tempt anyone
|
27
|
+
location.delete("city")
|
28
|
+
location.delete("county")
|
29
|
+
return location
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Geogov
|
5
|
+
|
6
|
+
class Mapit
|
7
|
+
|
8
|
+
class Method
|
9
|
+
def initialize(url,params = [])
|
10
|
+
@url = url
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_url(base_url)
|
15
|
+
url = "/#{@url}" unless /^\//.match(@url)
|
16
|
+
params = @params.map {|p|
|
17
|
+
p = p.join(",") if p.is_a?(Array)
|
18
|
+
# Messy, but MapIt gets upset if you escape commas
|
19
|
+
CGI::escape(p).gsub('%2C', ',')
|
20
|
+
}
|
21
|
+
url_path = "#{base_url}#{url}"
|
22
|
+
url_path += "/#{params.join("/")}" if params.length > 0
|
23
|
+
return url_path
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(base_url)
|
27
|
+
Geogov.get_json(self.to_url(base_url))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(default_url = "http://mapit.mysociety.org")
|
32
|
+
@base = default_url
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid_mapit_methods
|
36
|
+
[:postcode,:areas,:area,:point,:generations]
|
37
|
+
end
|
38
|
+
|
39
|
+
def respond_to?(sym)
|
40
|
+
valid_mapit_methods.include?(sym) || super(sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Borrowed heavily from mapit's pylib/postcodes/views.py with some amendments based on
|
44
|
+
# pylib/mapit/areas/models.py
|
45
|
+
def translate_area_type_to_shortcut(area_type)
|
46
|
+
if ['COP','LBW','LGE','MTW','UTE','UTW','DIW'].include?(area_type)
|
47
|
+
return 'ward'
|
48
|
+
elsif ['CTY', 'CED'].include?(area_type)
|
49
|
+
return 'council' # county
|
50
|
+
elsif ['DIS', 'LBO'].include?(area_type)
|
51
|
+
return 'council' # district
|
52
|
+
elsif area_type == 'WMC' # XXX Also maybe 'EUR', 'NIE', 'SPC', 'SPE', 'WAC', 'WAE', 'OLF', 'OLG', 'OMF', 'OMG')
|
53
|
+
return 'WMC'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def areas_for_stack_from_coords(lat, lon)
|
58
|
+
query = self.point("4326", [lon,lat])
|
59
|
+
results = {:point => {'lat' => lat, 'lon' => lon}}
|
60
|
+
query.each do |id,area_info|
|
61
|
+
level = translate_area_type_to_shortcut(area_info['type'])
|
62
|
+
if level
|
63
|
+
level = level.downcase.to_sym
|
64
|
+
results[level] = [] unless results[level]
|
65
|
+
results[level] << area_info.select {|k,v| ["name","id","type"].include?(k) }
|
66
|
+
results[:nation] = area_info['country_name'] if results[:nation].nil?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
return results
|
70
|
+
end
|
71
|
+
|
72
|
+
def areas_for_stack_from_postcode(postcode)
|
73
|
+
query = self.postcode(postcode)
|
74
|
+
results = {}
|
75
|
+
if query && query['shortcuts'] && query['areas']
|
76
|
+
query['shortcuts'].each do |typ,i|
|
77
|
+
if i.is_a? Hash
|
78
|
+
ids = i.values()
|
79
|
+
else
|
80
|
+
ids = [i]
|
81
|
+
end
|
82
|
+
ids.each do |id|
|
83
|
+
area_info = query['areas'][id.to_s]
|
84
|
+
level = typ.downcase.to_sym
|
85
|
+
results[level] = [] unless results[level]
|
86
|
+
results[level] << area_info.select {|k,v| ["name","id","type"].include?(k) }
|
87
|
+
results[:nation] = area_info['country_name'] if results[:nation].nil?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
lat,lon = query['wgs84_lat'],query['wgs84_lon']
|
91
|
+
results[:point] = {'lat' => lat, 'lon' => lon}
|
92
|
+
end
|
93
|
+
return results
|
94
|
+
end
|
95
|
+
|
96
|
+
def centre_of_district(district_postcode)
|
97
|
+
query = self.postcode("partial",district_postcode)
|
98
|
+
if query
|
99
|
+
lat,lon = query['wgs84_lat'],query['wgs84_lon']
|
100
|
+
return {'lat' => lat, 'lon' => lon}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def method_missing(method, *args, &block)
|
105
|
+
if valid_mapit_methods.include?(method)
|
106
|
+
Mapit::Method.new(method.to_s,args).call(@base)
|
107
|
+
else
|
108
|
+
super(method, *args, &block)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Geogov
|
2
|
+
|
3
|
+
class OpenStreetMap
|
4
|
+
|
5
|
+
def initialize(url = "http://ojw.dev.openstreetmap.org")
|
6
|
+
@url = url
|
7
|
+
end
|
8
|
+
|
9
|
+
def map_img(lat,long,options = {})
|
10
|
+
options = {
|
11
|
+
:w => 200,
|
12
|
+
:h => 200,
|
13
|
+
:z => 14,
|
14
|
+
:mode => "export",
|
15
|
+
:lat => lat,
|
16
|
+
:lon => long,
|
17
|
+
:show => 1
|
18
|
+
}.merge(options)
|
19
|
+
|
20
|
+
if options[:marker_lat] && options[:marker_lon]
|
21
|
+
options[:mlat0] = options.delete(:marker_lat)
|
22
|
+
options[:mlon0] = options.delete(:marker_lon)
|
23
|
+
end
|
24
|
+
|
25
|
+
params = Geogov.hash_to_params(options)
|
26
|
+
|
27
|
+
"#{@url}/StaticMap?#{params}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def map_href(lat,long,options = {})
|
31
|
+
options = {
|
32
|
+
:zoom => options[:z] || 14,
|
33
|
+
:lat => lat,
|
34
|
+
:lon => long,
|
35
|
+
:layers => "M"
|
36
|
+
}.merge(options)
|
37
|
+
|
38
|
+
if options[:marker_lat] && options[:marker_lon]
|
39
|
+
options[:mlat0] = options.delete(:marker_lat)
|
40
|
+
options[:mlon0] = options.delete(:marker_lon)
|
41
|
+
end
|
42
|
+
|
43
|
+
params = Geogov.hash_to_params(options)
|
44
|
+
|
45
|
+
"http://www.openstreetmap.org/?#{params}"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
data/lib/geogov/utils.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Geogov
|
5
|
+
|
6
|
+
def get(url)
|
7
|
+
url = URI.parse(url) unless url.is_a? URI
|
8
|
+
response = Net::HTTP.start(url.host, url.port) { |http|
|
9
|
+
request = Net::HTTP::Get.new(url.request_uri)
|
10
|
+
http.request(request)
|
11
|
+
}
|
12
|
+
return nil if response.code != '200'
|
13
|
+
return response.body
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_json(url)
|
17
|
+
response = self.get(url)
|
18
|
+
if response
|
19
|
+
return JSON.parse(response)
|
20
|
+
else
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def hash_to_params(hash)
|
26
|
+
hash.map { |k,v| CGI.escape(k.to_s) + "=" + CGI.escape(v.to_s) }.join("&")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Dead simple and inefficient LRU cache
|
30
|
+
# In no way thread-safe
|
31
|
+
class LruCache
|
32
|
+
|
33
|
+
def initialize(size = 100)
|
34
|
+
@size = size
|
35
|
+
@bucket1 = {}
|
36
|
+
@bucket2 = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def []=(key,obj)
|
40
|
+
if @bucket1[key]
|
41
|
+
@bucket1[key] = obj
|
42
|
+
elsif @bucket2[key]
|
43
|
+
@bucket2[key] = obj
|
44
|
+
else
|
45
|
+
@bucket1[key] = obj
|
46
|
+
swizzle if @bucket1.size > @size
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](key)
|
51
|
+
if @bucket1[key]
|
52
|
+
return @bucket1[key]
|
53
|
+
elsif @bucket2[key]
|
54
|
+
obj = @bucket2.delete[key]
|
55
|
+
@bucket1[key] = obj
|
56
|
+
swizzle if @bucket1.size > @size
|
57
|
+
return obj
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def swizzle
|
62
|
+
@bucket2 = @bucket1
|
63
|
+
@bucket1 = {}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class SimpleCache
|
68
|
+
def initialize(delegate)
|
69
|
+
@delegate = delegate
|
70
|
+
@cache = LruCache.new(1000)
|
71
|
+
end
|
72
|
+
|
73
|
+
def method_missing(m, *args)
|
74
|
+
arg_key = args.inspect
|
75
|
+
cache_key = "#{m}--#{arg_key}"
|
76
|
+
if @cache[cache_key]
|
77
|
+
return @cache[cache_key]
|
78
|
+
else
|
79
|
+
result = @delegate.send(m,*args)
|
80
|
+
@cache[cache_key] = result
|
81
|
+
return result
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
end
|
data/test/geogov_test.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class GovspeakTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
test "IP-located stack should have country" do
|
6
|
+
|
7
|
+
Geogov.configure do |g|
|
8
|
+
g.provider_for :centre_of_country, stub(:centre_of_country => {"lat"=>37,"lon"=>-96})
|
9
|
+
g.provider_for :remote_location, stub(:remote_location => {'country' => 'US'})
|
10
|
+
end
|
11
|
+
|
12
|
+
stack = Geogov::GeoStack.new_from_ip('173.203.129.90')
|
13
|
+
assert_equal "US", stack.country
|
14
|
+
assert_in_delta stack.fuzzy_point.lon, -96, 0.5
|
15
|
+
assert_in_delta stack.fuzzy_point.lat, 37, 0.5
|
16
|
+
assert_equal :country, stack.fuzzy_point.accuracy
|
17
|
+
end
|
18
|
+
|
19
|
+
test "should be specific if no country available" do
|
20
|
+
|
21
|
+
Geogov.configure do |g|
|
22
|
+
g.provider_for :remote_location, stub(:remote_location => nil)
|
23
|
+
end
|
24
|
+
|
25
|
+
stack = Geogov::GeoStack.new_from_ip('127.0.0.1')
|
26
|
+
assert_nil stack.country
|
27
|
+
assert_equal 0, stack.fuzzy_point.lon
|
28
|
+
assert_equal 0, stack.fuzzy_point.lat
|
29
|
+
assert_equal :planet, stack.fuzzy_point.accuracy
|
30
|
+
end
|
31
|
+
|
32
|
+
test "reconstructed stack rejects unknown params" do
|
33
|
+
assert_raises(ArgumentError) {
|
34
|
+
Geogov::GeoStack.new_from_hash("galaxy" => "Andromeda")
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
test "reconstructed stack should refuse creation if no fuzzy point" do
|
39
|
+
assert_raises(ArgumentError) {
|
40
|
+
Geogov::GeoStack.new_from_hash("country" => "US")
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
test "reconstructed stack should always truncate postcode" do
|
45
|
+
stack = Geogov::GeoStack.new_from_hash("postcode"=>"SE10 8UG","country" => "UK","fuzzy_point" => {"lat"=>"37","lon"=>"-96","accuracy"=>"postcode"})
|
46
|
+
assert_equal "SE10 8", stack.postcode
|
47
|
+
end
|
48
|
+
|
49
|
+
test "stack should not consider a postcode with trailing whitespace invalid" do
|
50
|
+
stack = Geogov::GeoStack.new_from_hash("postcode"=>"SE10 8UG ","country" => "UK","fuzzy_point" => {"lat"=>"37","lon"=>"-96","accuracy"=>"postcode"})
|
51
|
+
assert_equal "SE10 8", stack.postcode
|
52
|
+
end
|
53
|
+
|
54
|
+
test "stack should ignore invalid postcodes" do
|
55
|
+
stack = Geogov::GeoStack.new_from_hash("postcode"=>"NOTAPOSTCODE","country" => "UK","fuzzy_point" => {"lat"=>"37","lon"=>"-96","accuracy"=>"postcode"})
|
56
|
+
assert_nil stack.postcode
|
57
|
+
end
|
58
|
+
|
59
|
+
test "stack with country should have country accuracy" do
|
60
|
+
stack = Geogov::GeoStack.new_from_hash("country" => "US","fuzzy_point" => {"lat"=>"37","lon"=>"-96","accuracy"=>"country"})
|
61
|
+
assert_equal :country, stack.fuzzy_point.accuracy
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$:.unshift(File.expand_path("../lib")) unless $:.include?(File.expand_path("../lib"))
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup :default, :development, :test
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
class Test::Unit::TestCase
|
9
|
+
class << self
|
10
|
+
def test(name, &block)
|
11
|
+
clean_name = name.gsub(/\s+/,'_')
|
12
|
+
method = "test_#{clean_name.gsub(/\s+/,'_')}".to_sym
|
13
|
+
already_defined = instance_method(method) rescue false
|
14
|
+
raise "#{method} exists" if already_defined
|
15
|
+
define_method(method, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'mocha'
|
21
|
+
require 'geogov'
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geogov
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ben Griffiths
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-08 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rake
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 63
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 8
|
33
|
+
- 0
|
34
|
+
version: 0.8.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: mocha
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 59
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 9
|
49
|
+
- 0
|
50
|
+
version: 0.9.0
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rspec
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 27
|
62
|
+
segments:
|
63
|
+
- 2
|
64
|
+
- 5
|
65
|
+
- 0
|
66
|
+
version: 2.5.0
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description: Geolocation and utilities for single domain
|
70
|
+
email:
|
71
|
+
- ben@alphagov.co.uk
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
files:
|
79
|
+
- lib/geogov/fuzzy_point.rb
|
80
|
+
- lib/geogov/geo_stack.rb
|
81
|
+
- lib/geogov/providers/dracos_gazetteer.rb
|
82
|
+
- lib/geogov/providers/geonames.rb
|
83
|
+
- lib/geogov/providers/google.rb
|
84
|
+
- lib/geogov/providers/hostip.rb
|
85
|
+
- lib/geogov/providers/mapit.rb
|
86
|
+
- lib/geogov/providers/open_street_map.rb
|
87
|
+
- lib/geogov/utils.rb
|
88
|
+
- lib/geogov.rb
|
89
|
+
- Gemfile
|
90
|
+
- Rakefile
|
91
|
+
- test/geogov_test.rb
|
92
|
+
- test/test_helper.rb
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: http://github.com/alphagov/geogov
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 3
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
hash: 3
|
117
|
+
segments:
|
118
|
+
- 0
|
119
|
+
version: "0"
|
120
|
+
requirements: []
|
121
|
+
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 1.3.7
|
124
|
+
signing_key:
|
125
|
+
specification_version: 3
|
126
|
+
summary: Geolocation and utilities for single domain
|
127
|
+
test_files:
|
128
|
+
- test/geogov_test.rb
|
129
|
+
- test/test_helper.rb
|