Make the search box load each set of results separately so that one
[rails.git] / app / controllers / geocoder_controller.rb
1 class GeocoderController < ApplicationController
2   require 'uri'
3   require 'net/http'
4   require 'rexml/document'
5
6   before_filter :set_locale
7
8   def search
9     @query = params[:query]
10     @sources = Array.new
11
12     @query.sub(/^\s+/, "")
13     @query.sub(/\s+$/, "")
14
15     if @query.match(/^[+-]?\d+(\.\d*)?\s*[\s,]\s*[+-]?\d+(\.\d*)?$/)
16       @sources.push "latlon"
17     elsif @query.match(/^\d{5}(-\d{4})?$/)
18       @sources.push "us_postcode"
19     elsif @query.match(/^(GIR 0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]|[A-HK-Y][0-9]([0-9]|[ABEHMNPRV-Y]))|[0-9][A-HJKS-UW])\s*[0-9][ABD-HJLNP-UW-Z]{2})$/i)
20       @sources.push "uk_postcode"
21       @sources.push "osm_namefinder"
22     elsif @query.match(/^[A-Z]\d[A-Z]\s*\d[A-Z]\d$/i)
23       @sources.push "ca_postcode"
24     else
25       @sources.push "osm_namefinder"
26       @sources.push "geonames"
27     end
28
29     render :update do |page|
30       page.replace_html :sidebar_content, :partial => "search"
31       page.call "openSidebar"
32     end
33   end
34
35   def search_latlon
36     # get query parameters
37     query = params[:query]
38
39     # create result array
40     @results = Array.new
41
42     # decode the location
43     if m = query.match(/^\s*([+-]?\d+(\.\d*)?)\s*[\s,]\s*([+-]?\d+(\.\d*)?)\s*$/)
44       lat = m[1].to_f
45       lon = m[3].to_f
46     end
47
48     # generate results
49     if lat < -90 or lat > 90
50       @error = "Latitude #{lat} out of range"
51       render :action => "error"
52     elsif lon < -180 or lon > 180
53       @error = "Longitude #{lon} out of range"
54       render :action => "error"
55     else
56       @results.push({:lat => lat, :lon => lon,
57                      :zoom => APP_CONFIG['postcode_zoom'],
58                      :name => "#{lat}, #{lon}"})
59
60       render :action => "results"
61     end
62   end
63
64   def search_us_postcode
65     # get query parameters
66     query = params[:query]
67
68     # create result array
69     @results = Array.new
70
71     # ask geocoder.us (they have a non-commercial use api)
72     response = fetch_text("http://rpc.geocoder.us/service/csv?zip=#{escape_query(query)}")
73
74     # parse the response
75     unless response.match(/couldn't find this zip/)
76       data = response.split(/\s*,\s+/) # lat,long,town,state,zip
77       @results.push({:lat => data[0], :lon => data[1],
78                      :zoom => APP_CONFIG['postcode_zoom'],
79                      :prefix => "#{data[2]}, #{data[3]}, ",
80                      :name => data[4]})
81     end
82
83     render :action => "results"
84   rescue Exception => ex
85     @error = "Error contacting rpc.geocoder.us: #{ex.to_s}"
86     render :action => "error"
87   end
88
89   def search_uk_postcode
90     # get query parameters
91     query = params[:query]
92
93     # create result array
94     @results = Array.new
95
96     # ask npemap.org.uk to do a combined npemap + freethepostcode search
97     response = fetch_text("http://www.npemap.org.uk/cgi/geocoder.fcgi?format=text&postcode=#{escape_query(query)}")
98
99     # parse the response
100     unless response.match(/Error/)
101       dataline = response.split(/\n/)[1]
102       data = dataline.split(/,/) # easting,northing,postcode,lat,long
103       postcode = data[2].gsub(/'/, "")
104       zoom = APP_CONFIG['postcode_zoom'] - postcode.count("#")
105       @results.push({:lat => data[3], :lon => data[4], :zoom => zoom,
106                      :name => postcode})
107     end
108
109     render :action => "results"
110   rescue Exception => ex
111     @error = "Error contacting www.npemap.org.uk: #{ex.to_s}"
112     render :action => "error"
113   end
114
115   def search_ca_postcode
116     # get query parameters
117     query = params[:query]
118     @results = Array.new
119
120     # ask geocoder.ca (note - they have a per-day limit)
121     response = fetch_xml("http://geocoder.ca/?geoit=XML&postal=#{escape_query(query)}")
122
123     # parse the response
124     if response.get_elements("geodata/error").empty?
125       @results.push({:lat => response.get_text("geodata/latt").to_s,
126                      :lon => response.get_text("geodata/longt").to_s,
127                      :zoom => APP_CONFIG['postcode_zoom'],
128                      :name => query.upcase})
129     end
130
131     render :action => "results"
132   rescue Exception => ex
133     @error = "Error contacting geocoder.ca: #{ex.to_s}"
134     render :action => "error"
135   end
136
137   def search_osm_namefinder
138     # get query parameters
139     query = params[:query]
140
141     # create result array
142     @results = Array.new
143
144     # ask OSM namefinder
145     response = fetch_xml("http://gazetteer.openstreetmap.org/namefinder/search.xml?find=#{escape_query(query)}")
146
147     # parse the response
148     response.elements.each("searchresults/named") do |named|
149       lat = named.attributes["lat"].to_s
150       lon = named.attributes["lon"].to_s
151       zoom = named.attributes["zoom"].to_s
152       place = named.elements["place/named"] || named.elements["nearestplaces/named"]
153       type = named.attributes["info"].to_s.capitalize
154       name = named.attributes["name"].to_s
155       description = named.elements["description"].to_s
156
157       if name.empty?
158         prefix = ""
159         name = type
160       else
161         prefix =  t "geocoder.search_osm_namefinder.prefix", :type => type
162       end
163
164       if place
165         distance = format_distance(place.attributes["approxdistance"].to_i)
166         direction = format_direction(place.attributes["direction"].to_i)
167         placename = format_name(place.attributes["name"].to_s)
168         suffix = t "geocoder.search_osm_namefinder.suffix_place", :distance => distance, :direction => direction, :placename => placename
169
170         if place.attributes["rank"].to_i <= 30
171           parent = nil
172           parentrank = 0
173           parentscore = 0
174
175           place.elements.each("nearestplaces/named") do |nearest|
176             nearestrank = nearest.attributes["rank"].to_i
177             nearestscore = nearestrank / nearest.attributes["distance"].to_f
178
179             if nearestrank > 30 and
180                ( nearestscore > parentscore or
181                  ( nearestscore == parentscore and nearestrank > parentrank ) )
182               parent = nearest
183               parentrank = nearestrank
184               parentscore = nearestscore
185             end
186           end
187
188           if parent
189             parentname = format_name(parent.attributes["name"].to_s)
190
191             if  place.attributes["info"].to_s == "suburb"
192               suffix = t "geocoder.search_osm_namefinder.suffix_suburb", :suffix => suffix, :parentname => parentname
193             else
194               parentdistance = format_distance(parent.attributes["approxdistance"].to_i)
195               parentdirection = format_direction(parent.attributes["direction"].to_i)
196               suffix = t "geocoder.search_osm_namefinder.suffix_parent", :suffix => suffix, :parentdistance => parentdistance, :parentdirection => parentdirection, :parentname => parentname
197             end
198           end
199         end
200       else
201         suffix = ""
202       end
203
204       @results.push({:lat => lat, :lon => lon, :zoom => zoom,
205                      :prefix => prefix, :name => name, :suffix => suffix,
206                      :description => description})
207     end
208
209     render :action => "results"
210   rescue Exception => ex
211     @error = "Error contacting gazetteer.openstreetmap.org: #{ex.to_s}"
212     render :action => "error"
213   end
214
215   def search_geonames
216     # get query parameters
217     query = params[:query]
218
219     # create result array
220     @results = Array.new
221
222     # ask geonames.org
223     response = fetch_xml("http://ws.geonames.org/search?q=#{escape_query(query)}&maxRows=20")
224
225     # parse the response
226     response.elements.each("geonames/geoname") do |geoname|
227       lat = geoname.get_text("lat").to_s
228       lon = geoname.get_text("lng").to_s
229       name = geoname.get_text("name").to_s
230       country = geoname.get_text("countryName").to_s
231       @results.push({:lat => lat, :lon => lon,
232                      :zoom => APP_CONFIG['geonames_zoom'],
233                      :name => name,
234                      :suffix => ", #{country}"})
235     end
236
237     render :action => "results"
238   rescue Exception => ex
239     @error = "Error contacting ws.geonames.org: #{ex.to_s}"
240     render :action => "error"
241   end
242   
243   def description
244     @sources = Array.new
245
246     @sources.push({ :name => "osm_namefinder", :types => "cities", :max => 2 })
247     @sources.push({ :name => "osm_namefinder", :types => "towns", :max => 4 })
248     @sources.push({ :name => "osm_namefinder", :types => "places", :max => 10 })
249     @sources.push({ :name => "geonames" })
250
251     render :update do |page|
252       page.replace_html :sidebar_content, :partial => "description"
253       page.call "openSidebar"
254     end
255   end
256
257   def description_osm_namefinder
258     # get query parameters
259     lat = params[:lat]
260     lon = params[:lon]
261     types = params[:types]
262     max = params[:max]
263
264     # create result array
265     @results = Array.new
266
267     # ask OSM namefinder
268     response = fetch_xml("http://gazetteer.openstreetmap.org/namefinder/search.xml?find=#{types}+near+#{lat},#{lon}&max=#{max}")
269
270     # parse the response
271     response.elements.each("searchresults/named") do |named|
272       lat = named.attributes["lat"].to_s
273       lon = named.attributes["lon"].to_s
274       zoom = named.attributes["zoom"].to_s
275       place = named.elements["place/named"] || named.elements["nearestplaces/named"]
276       type = named.attributes["info"].to_s
277       name = named.attributes["name"].to_s
278       description = named.elements["description"].to_s
279       distance = format_distance(place.attributes["approxdistance"].to_i)
280       direction = format_direction((place.attributes["direction"].to_i - 180) % 360)
281       prefix = "#{distance} #{direction} of #{type} "
282       @results.push({:lat => lat, :lon => lon, :zoom => zoom,
283                      :prefix => prefix.capitalize, :name => name,
284                      :description => description})
285     end
286
287     render :action => "results"
288   rescue Exception => ex
289     @error = "Error contacting gazetteer.openstreetmap.org: #{ex.to_s}"
290     render :action => "error"
291   end
292
293   def description_geonames
294     # get query parameters
295     lat = params[:lat]
296     lon = params[:lon]
297
298     # create result array
299     @results = Array.new
300
301     # ask geonames.org
302     response = fetch_xml("http://ws.geonames.org/countrySubdivision?lat=#{lat}&lng=#{lon}")
303
304     # parse the response
305     response.elements.each("geonames/countrySubdivision") do |geoname|
306       name = geoname.get_text("adminName1").to_s
307       country = geoname.get_text("countryName").to_s
308       @results.push({:prefix => "#{name}, #{country}"})
309     end
310
311     render :action => "results"
312   rescue Exception => ex
313     @error = "Error contacting ws.geonames.org: #{ex.to_s}"
314     render :action => "error"
315   end
316
317 private
318
319   def fetch_text(url)
320     return Net::HTTP.get(URI.parse(url))
321   end
322
323   def fetch_xml(url)
324     return REXML::Document.new(fetch_text(url))
325   end
326
327   def format_distance(distance)
328     return t("geocoder.distance", :count => distance)
329   end
330
331   def format_direction(bearing)
332     return t("geocoder.direction.south_west") if bearing >= 22.5 and bearing < 67.5
333     return t("geocoder.direction.south") if bearing >= 67.5 and bearing < 112.5
334     return t("geocoder.direction.south_east") if bearing >= 112.5 and bearing < 157.5
335     return t("geocoder.direction.east") if bearing >= 157.5 and bearing < 202.5
336     return t("geocoder.direction.north_east") if bearing >= 202.5 and bearing < 247.5
337     return t("geocoder.direction.north") if bearing >= 247.5 and bearing < 292.5
338     return t("geocoder.direction.north_west") if bearing >= 292.5 and bearing < 337.5
339     return t("geocoder.direction.west")
340   end
341
342   def format_name(name)
343     return name.gsub(/( *\[[^\]]*\])*$/, "")
344   end
345
346   def count_results(results)
347     count = 0
348
349     results.each do |source|
350       count += source[:results].length if source[:results]
351     end
352
353     return count
354   end
355
356   def escape_query(query)
357     return URI.escape(query, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]", false, 'N'))
358   end
359 end