openstreetmap-website/app/controllers/geocoder_controller.rb
2016-02-05 13:35:26 +00:00

374 lines
13 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding: utf-8
class GeocoderController < ApplicationController
require "cgi"
require "uri"
require "rexml/document"
before_action :authorize_web
before_action :set_locale
before_action :require_oauth, :only => [:search]
def search
normalize_params
@sources = []
if params[:lat] && params[:lon]
@sources.push "latlon"
@sources.push "osm_nominatim_reverse"
@sources.push "geonames_reverse" if defined?(GEONAMES_USERNAME)
elsif params[:query]
if params[:query] =~ /^\d{5}(-\d{4})?$/
@sources.push "us_postcode"
@sources.push "osm_nominatim"
elsif params[:query] =~ /^(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
@sources.push "uk_postcode"
@sources.push "osm_nominatim"
elsif params[:query] =~ /^[A-Z]\d[A-Z]\s*\d[A-Z]\d$/i
@sources.push "ca_postcode"
@sources.push "osm_nominatim"
else
@sources.push "osm_nominatim"
@sources.push "geonames" if defined?(GEONAMES_USERNAME)
end
end
if @sources.empty?
render :text => "", :status => :bad_request
else
render :layout => map_layout
end
end
def search_latlon
lat = params[:lat].to_f
lon = params[:lon].to_f
if lat < -90 || lat > 90
@error = "Latitude #{lat} out of range"
render :action => "error"
elsif lon < -180 || lon > 180
@error = "Longitude #{lon} out of range"
render :action => "error"
else
@results = [{ :lat => lat, :lon => lon,
:zoom => params[:zoom],
:name => "#{lat}, #{lon}" }]
render :action => "results"
end
end
def search_us_postcode
# get query parameters
query = params[:query]
# create result array
@results = []
# ask geocoder.us (they have a non-commercial use api)
response = fetch_text("http://rpc.geocoder.us/service/csv?zip=#{escape_query(query)}")
# parse the response
unless response =~ /couldn't find this zip/
data = response.split(/\s*,\s+/) # lat,long,town,state,zip
@results.push(:lat => data[0], :lon => data[1],
:zoom => POSTCODE_ZOOM,
:prefix => "#{data[2]}, #{data[3]},",
:name => data[4])
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting rpc.geocoder.us: #{ex}"
render :action => "error"
end
def search_uk_postcode
# get query parameters
query = params[:query]
# create result array
@results = []
# ask npemap.org.uk to do a combined npemap + freethepostcode search
response = fetch_text("http://www.npemap.org.uk/cgi/geocoder.fcgi?format=text&postcode=#{escape_query(query)}")
# parse the response
unless response =~ /Error/
dataline = response.split(/\n/)[1]
data = dataline.split(/,/) # easting,northing,postcode,lat,long
postcode = data[2].delete("'")
zoom = POSTCODE_ZOOM - postcode.count("#")
@results.push(:lat => data[3], :lon => data[4], :zoom => zoom,
:name => postcode)
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting www.npemap.org.uk: #{ex}"
render :action => "error"
end
def search_ca_postcode
# get query parameters
query = params[:query]
@results = []
# ask geocoder.ca (note - they have a per-day limit)
response = fetch_xml("http://geocoder.ca/?geoit=XML&postal=#{escape_query(query)}")
# parse the response
if response.get_elements("geodata/error").empty?
@results.push(:lat => response.get_text("geodata/latt").to_s,
:lon => response.get_text("geodata/longt").to_s,
:zoom => POSTCODE_ZOOM,
:name => query.upcase)
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting geocoder.ca: #{ex}"
render :action => "error"
end
def search_osm_nominatim
# get query parameters
query = params[:query]
minlon = params[:minlon]
minlat = params[:minlat]
maxlon = params[:maxlon]
maxlat = params[:maxlat]
# get view box
if minlon && minlat && maxlon && maxlat
viewbox = "&viewbox=#{minlon},#{maxlat},#{maxlon},#{minlat}"
end
# get objects to excude
exclude = "&exclude_place_ids=#{params[:exclude]}" if params[:exclude]
# ask nominatim
response = fetch_xml("http:#{NOMINATIM_URL}search?format=xml&q=#{escape_query(query)}#{viewbox}#{exclude}&accept-language=#{http_accept_language.user_preferred_languages.join(',')}")
# extract the results from the response
results = response.elements["searchresults"]
# extract parameters from more_url
more_url_params = CGI.parse(URI.parse(results.attributes["more_url"]).query)
# create result array
@results = []
# create parameter hash for "more results" link
@more_params = params.merge(:exclude => more_url_params["exclude_place_ids"].first)
# parse the response
results.elements.each("place") do |place|
lat = place.attributes["lat"].to_s
lon = place.attributes["lon"].to_s
klass = place.attributes["class"].to_s
type = place.attributes["type"].to_s
name = place.attributes["display_name"].to_s
min_lat, max_lat, min_lon, max_lon = place.attributes["boundingbox"].to_s.split(",")
prefix_name = if type.empty?
""
else
t "geocoder.search_osm_nominatim.prefix.#{klass}.#{type}", :default => type.tr("_", " ").capitalize
end
if klass == "boundary" && type == "administrative"
rank = (place.attributes["place_rank"].to_i + 1) / 2
prefix_name = t "geocoder.search_osm_nominatim.admin_levels.level#{rank}", :default => prefix_name
end
prefix = t "geocoder.search_osm_nominatim.prefix_format", :name => prefix_name
object_type = place.attributes["osm_type"]
object_id = place.attributes["osm_id"]
@results.push(:lat => lat, :lon => lon,
:min_lat => min_lat, :max_lat => max_lat,
:min_lon => min_lon, :max_lon => max_lon,
:prefix => prefix, :name => name,
:type => object_type, :id => object_id)
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting nominatim.openstreetmap.org: #{ex}"
render :action => "error"
end
def search_geonames
# get query parameters
query = params[:query]
# get preferred language
lang = I18n.locale.to_s.split("-").first
# create result array
@results = []
# ask geonames.org
response = fetch_xml("http://api.geonames.org/search?q=#{escape_query(query)}&lang=#{lang}&maxRows=20&username=#{GEONAMES_USERNAME}")
# parse the response
response.elements.each("geonames/geoname") do |geoname|
lat = geoname.get_text("lat").to_s
lon = geoname.get_text("lng").to_s
name = geoname.get_text("name").to_s
country = geoname.get_text("countryName").to_s
@results.push(:lat => lat, :lon => lon,
:zoom => GEONAMES_ZOOM,
:name => name,
:suffix => ", #{country}")
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting api.geonames.org: #{ex}"
render :action => "error"
end
def search_osm_nominatim_reverse
# get query parameters
lat = params[:lat]
lon = params[:lon]
zoom = params[:zoom]
# create result array
@results = []
# ask nominatim
response = fetch_xml("http:#{NOMINATIM_URL}reverse?lat=#{lat}&lon=#{lon}&zoom=#{zoom}&accept-language=#{http_accept_language.user_preferred_languages.join(',')}")
# parse the response
response.elements.each("reversegeocode/result") do |result|
lat = result.attributes["lat"].to_s
lon = result.attributes["lon"].to_s
object_type = result.attributes["osm_type"]
object_id = result.attributes["osm_id"]
description = result.get_text.to_s
@results.push(:lat => lat, :lon => lon,
:zoom => zoom,
:name => description,
:type => object_type, :id => object_id)
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting nominatim.openstreetmap.org: #{ex}"
render :action => "error"
end
def search_geonames_reverse
# get query parameters
lat = params[:lat]
lon = params[:lon]
# get preferred language
lang = I18n.locale.to_s.split("-").first
# create result array
@results = []
# ask geonames.org
response = fetch_xml("http://api.geonames.org/countrySubdivision?lat=#{lat}&lng=#{lon}&lang=#{lang}&username=#{GEONAMES_USERNAME}")
# parse the response
response.elements.each("geonames/countrySubdivision") do |geoname|
name = geoname.get_text("adminName1").to_s
country = geoname.get_text("countryName").to_s
@results.push(:lat => lat, :lon => lon,
:zoom => GEONAMES_ZOOM,
:name => name,
:suffix => ", #{country}")
end
render :action => "results"
rescue StandardError => ex
@error = "Error contacting api.geonames.org: #{ex}"
render :action => "error"
end
private
def fetch_text(url)
response = OSM.http_client.get(URI.parse(url))
if response.success?
response.body
else
raise response.status.to_s
end
end
def fetch_xml(url)
REXML::Document.new(fetch_text(url))
end
def escape_query(query)
URI.escape(query, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]", false, "N"))
end
def normalize_params
query = params[:query]
return unless query
query.strip!
if latlon = query.match(/^([NS])\s*(\d{1,3}(\.\d*)?)\W*([EW])\s*(\d{1,3}(\.\d*)?)$/).try(:captures) # [NSEW] decimal degrees
params.merge!(nsew_to_decdeg(latlon)).delete(:query)
elsif latlon = query.match(/^(\d{1,3}(\.\d*)?)\s*([NS])\W*(\d{1,3}(\.\d*)?)\s*([EW])$/).try(:captures) # decimal degrees [NSEW]
params.merge!(nsew_to_decdeg(latlon)).delete(:query)
elsif latlon = query.match(/^([NS])\s*(\d{1,3})°?\s*(\d{1,3}(\.\d*)?)?[']?\W*([EW])\s*(\d{1,3})°?\s*(\d{1,3}(\.\d*)?)?[']?$/).try(:captures) # [NSEW] degrees, decimal minutes
params.merge!(ddm_to_decdeg(latlon)).delete(:query)
elsif latlon = query.match(/^(\d{1,3})°?\s*(\d{1,3}(\.\d*)?)?[']?\s*([NS])\W*(\d{1,3})°?\s*(\d{1,3}(\.\d*)?)?[']?\s*([EW])$/).try(:captures) # degrees, decimal minutes [NSEW]
params.merge!(ddm_to_decdeg(latlon)).delete(:query)
elsif latlon = query.match(/^([NS])\s*(\d{1,3})°?\s*(\d{1,2})[']?\s*(\d{1,3}(\.\d*)?)?["″]?\W*([EW])\s*(\d{1,3})°?\s*(\d{1,2})[']?\s*(\d{1,3}(\.\d*)?)?["″]?$/).try(:captures) # [NSEW] degrees, minutes, decimal seconds
params.merge!(dms_to_decdeg(latlon)).delete(:query)
elsif latlon = query.match(/^(\d{1,3})°?\s*(\d{1,2})[']?\s*(\d{1,3}(\.\d*)?)?["″]\s*([NS])\W*(\d{1,3})°?\s*(\d{1,2})[']?\s*(\d{1,3}(\.\d*)?)?["″]?\s*([EW])$/).try(:captures) # degrees, minutes, decimal seconds [NSEW]
params.merge!(dms_to_decdeg(latlon)).delete(:query)
elsif latlon = query.match(/^\s*([+-]?\d+(\.\d*)?)\s*[\s,]\s*([+-]?\d+(\.\d*)?)\s*$/)
params.merge!(:lat => latlon[1].to_f, :lon => latlon[3].to_f).delete(:query)
end
end
def nsew_to_decdeg(captures)
begin
Float(captures[0])
lat = !captures[2].casecmp("s").zero? ? captures[0].to_f : -captures[0].to_f
lon = !captures[5].casecmp("w").zero? ? captures[3].to_f : -captures[3].to_f
rescue
lat = !captures[0].casecmp("s").zero? ? captures[1].to_f : -captures[1].to_f
lon = !captures[3].casecmp("w").zero? ? captures[4].to_f : -captures[4].to_f
end
{ :lat => lat, :lon => lon }
end
def ddm_to_decdeg(captures)
begin
Float(captures[0])
lat = !captures[3].casecmp("s").zero? ? captures[0].to_f + captures[1].to_f / 60 : -(captures[0].to_f + captures[1].to_f / 60)
lon = !captures[7].casecmp("w").zero? ? captures[4].to_f + captures[5].to_f / 60 : -(captures[4].to_f + captures[5].to_f / 60)
rescue
lat = !captures[0].casecmp("s").zero? ? captures[1].to_f + captures[2].to_f / 60 : -(captures[1].to_f + captures[2].to_f / 60)
lon = !captures[4].casecmp("w").zero? ? captures[5].to_f + captures[6].to_f / 60 : -(captures[5].to_f + captures[6].to_f / 60)
end
{ :lat => lat, :lon => lon }
end
def dms_to_decdeg(captures)
begin
Float(captures[0])
lat = !captures[4].casecmp("s").zero? ? captures[0].to_f + (captures[1].to_f + captures[2].to_f / 60) / 60 : -(captures[0].to_f + (captures[1].to_f + captures[2].to_f / 60) / 60)
lon = !captures[9].casecmp("w").zero? ? captures[5].to_f + (captures[6].to_f + captures[7].to_f / 60) / 60 : -(captures[5].to_f + (captures[6].to_f + captures[7].to_f / 60) / 60)
rescue
lat = !captures[0].casecmp("s").zero? ? captures[1].to_f + (captures[2].to_f + captures[3].to_f / 60) / 60 : -(captures[1].to_f + (captures[2].to_f + captures[3].to_f / 60) / 60)
lon = !captures[5].casecmp("w").zero? ? captures[6].to_f + (captures[7].to_f + captures[8].to_f / 60) / 60 : -(captures[6].to_f + (captures[7].to_f + captures[8].to_f / 60) / 60)
end
{ :lat => lat, :lon => lon }
end
end