app/models/georeference/geo_locate.rb
# A Georeference derived from a call to the Tulane GeoLocate API.
#
class Georeference::GeoLocate < Georeference
attr_accessor :api_response, :iframe_response
API_HOST = 'www.geo-locate.org'.freeze
API_PATH = '/webservices/geolocatesvcv2/glcwrap.aspx?'.freeze
EMBED_PATH = '/web/webgeoreflight.aspx?'.freeze
EMBED_HOST = 'www.geo-locate.org'.freeze
def dwc_georeference_attributes
h = {}
super(h)
h.merge!(
georeferenceSources: 'GEOLocate ',
georeferenceRemarks: 'Typically created by copy-pasting one or more values from a collecting event into a GEOLocate form.')
h[:georeferenceProtocol] = 'Generated via a query through the GEOLocate web interface' if h[:georeferenceProtocol].blank?
h
end
# @param [Response] response
# @return [RGeo object]
def api_response=(response)
self.geographic_item = make_geographic_point(response.coordinates[0], response.coordinates[1])
make_error_geographic_item(response.uncertainty_polygon, response.uncertainty_radius)
end
# @param [String] response_string
# @return [RGeo object]
def iframe_response=(response_string)
lat, long, response_radius, uncertainty_points = Georeference::GeoLocate.parse_iframe_result(response_string)
if response_radius.present?
response_radius = response_radius.to_f
else
response_radius = nil
end
self.geographic_item = make_geographic_point(long, lat, '0.0') unless (lat.blank? && long.blank?)
if uncertainty_points.nil?
# make a circle from the geographic_item
if response_radius.present?
# q1 = "SELECT ST_BUFFER('#{self.geographic_item.geo_object}', #{error_radius.to_f / Utilities::Geo::ONE_WEST_MEAN});"
self.error_radius = response_radius
# Why are we turning error radius into a polygon!?
q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['SELECT ST_Buffer(?, ?);',
self.geographic_item.geo_object.to_s,
((response_radius) / Utilities::Geo::ONE_WEST_MEAN)])
value = GeographicItem.connection.select_all(q2).first['st_buffer']
self.error_geographic_item = make_err_polygon(value)
end
else
make_error_geographic_item(uncertainty_points, response_radius)
end
self.geographic_item
end
# @return [Hash] of api request pieces
def request_hash
Hash[*self.api_request.split('&').collect { |a| a.split('=', 2) }.flatten]
end
# @param [String] wkb
# @return [GeographicItem::Polygon] GeographicItem::Polygon, either found, or created
def make_err_polygon(wkb)
polygon = Gis::FACTORY.parse_wkb(wkb)
# ActiveRecord::Base.send(:sanitize_sql_array, ['polygon = ST_GeographyFromText(?)', polygon.to_s])
test_grs = GeographicItem::Polygon.where(['polygon = ST_GeographyFromText(?)', polygon.to_s])
if test_grs.empty?
test_grs = [GeographicItem.new(polygon:)]
end
if test_grs.first.new_record?
test_grs.first.save
else
test_grs.first
end
test_grs.first
end
# @param [String] x = longitude
# @param [String] y = latitude
# @param [String] z = elevation, defaults to 0.0
# @return [Object] GeographicItem::Point, either found or created.
def make_geographic_point(x, y, z = '0.0')
if x.blank? || y.blank?
test_grs = []
else
test_grs = GeographicItem::Point
.where("point = ST_GeographyFromText('POINT(? ? ?)')", x.to_f, y.to_f, z.to_f)
# .where(['ST_Z(point::geometry) = ?', z.to_f])
end
if test_grs.empty? # put a new one in the array
test_grs = [GeographicItem.new(point: Gis::FACTORY.point(x, y, z))]
end
test_grs.first
end
# def make_error_geographic_item(result)
# # evaluate for error_radius only if called for (default)
# er = result['resultSet']['features'][0]['properties']['uncertaintyRadiusMeters']
# self.error_radius = (er ? er : 3.0)
# #evaluate for error polygon only if called for (non-default)
# if result['resultSet']['features'][0]['properties']['uncertaintyPolygon'] # @request[:doPoly]
# # Build the error geographic shape
# # isolate the array of points from the response, and build the polygon from a line_string
# # made out of the points
# p = result['resultSet']['features'][0]['properties']['uncertaintyPolygon']['coordinates'][0]
# # build an array of Gis::FACTORY.points from p
# # poly = 'MULTIPOLYGON(((' + p.collect{|a,b| "#{a} #{b}"}.join(',') + ')))'
# # parsed_poly = Gis::FACTORY.parse_wkt(poly)
# err_array = []
# # @todo get geoJson results and handle all this automatically?
# p.each { |point| err_array.push(Gis::FACTORY.point(point[0], point[1])) }
# self.error_geographic_item = GeographicItem.new
# self.error_geographic_item.polygon = Gis::FACTORY.polygon(Gis::FACTORY.line_string(err_array))
# end
# end
# @todo get geoJson results and handle all this automatically?
# @param [RGeo::Polygon] uncertainty_polygon
# @param [Integer] uncertainty_radius in meters
def make_error_geographic_item(uncertainty_polygon, uncertainty_radius)
self.error_radius = uncertainty_radius if !uncertainty_radius.nil?
unless uncertainty_polygon.nil?
err_array = []
uncertainty_polygon.each { |point| err_array.push(Gis::FACTORY.point(point[0], point[1])) }
self.error_geographic_item = GeographicItem.new(polygon: Gis::FACTORY.polygon(Gis::FACTORY.line_string(err_array)))
end
end
# @param [String] response_string from Tulane
# @return [Array]
# parsing the four possible bits of a response into an array
def self.parse_iframe_result(response_string)
s = unify_response_string(response_string)
lat, long, error_radius, uncertainty_polygon = s.split('|')
uncertainty_points = nil
unless uncertainty_polygon.nil?
if uncertainty_polygon =~ /unavailable/i # todo: there are many more possible error conditions
uncertainty_points = nil
else
uncertainty_points = uncertainty_polygon.split(',').reverse.in_groups_of(2)
end
end
[lat, long, error_radius, uncertainty_points]
end
# TODO: move ti iframe getter/setter
def self.unify_response_string(response_string)
response_string.gsub!(/[\t]/, '|')
response_string.gsub!(/\s+/, '|')
response_string
end
# Build a georeference starting with a set of request parameters.
# @param [ActionController::Parameters] request_params
# @return [GeoLocate]
def self.build(request_params)
g = self.new
# @todo write a Request.valid_params? method to use here
# @todo #1: Just what will be the validation criteria for the request?
# @todo #2: Why not judge validity from the response?
if request_params.nil?
g.errors.add(:base, 'invalid or no request parameters provided.')
return g
end
request = Request.new(request_params)
request.locate
if request.succeeded?
g.api_response = request.response
g.api_request = request.request_param_string
else
g.errors.add(:api_request, 'requested parameters did not succeed in returning a result')
end
g
end
# def self.default_options_string
# '&points=|||low|&georef=run|false|false|true|true|false|false|false|0&gc=Tester'
# end
#rubocop:disable Style/StringHashKeys
# This class is used to create the string which will be sent to Tulane
class RequestUI
REQUEST_PARAMS = {
'country' => nil, # name of a country 'USA', or Germany
'state' => nil, # 'IL', or 'illinois' (required in the United States)
'county' => nil, # supply as a parameter
'locality' => nil, # name of a place 'CHAMPAIGN' (or building, i.e. 'Eiffel Tower')
'Latitude' => nil, #
'Longitude' => nil, #
'Placename' => nil, #
'Score' => '0',
'Uncertainty' => '3',
'H20' => 'false',
'HwyX' => 'false',
'Uncert' => 'true',
'Poly' => 'true',
'DisplacePoly' => 'false',
'RestrictAdmin' => 'false',
'BG' => 'false',
'LanguageIndex' => '0',
'gc' => 'TaxonWorks'
}.freeze
#rubocop:enable Style/StringHashKeys
attr_reader :request_params, :request_params_string, :request_params_hash
# @param [ActionController::Parameters] request_params
def initialize(request_params)
@request_params_hash = REQUEST_PARAMS.merge(request_params)
build_param_string
@succeeded = nil
end
# "http://www.geo-locate.org/web/webgeoreflight.aspx?country=United States of
# America&state=Illinois&locality=Champaign&points=40.091622
# |-88.241179|Champaign|low|7000&georef=run|false|false|true|true|false|false|false|0&gc=Tester"
# @return [String] a string to invoke as an api call to hunt for a particular place.
def build_param_string
# @request_param_string ||= @request_params.collect { |key, value| "#{key}=#{value}" }.join('&')
ga = request_params_hash
params_string = 'http://' + EMBED_HOST + EMBED_PATH +
"country=#{ga['country']}&state=#{ga['state']}&county=#{ga['county']}&locality=#{ga['locality']}&points=" \
"#{ga['Latitude']}|#{ga['Longitude']}|#{ga['Placename']}|#{ga['Score']}|#{ga['Uncertainty']}" \
"&georef=run|#{ga['H20']}|#{ga['HwyX']}|#{ga['Uncert']}|#{ga['Poly']}|#{ga['DisplacePoly']}|" \
"#{ga['RestrictAdmin']}|#{ga['BG']}|#{ga['LanguageIndex']}" \
"&gc=#{ga['gc']}"
@request_params_string = params_string # URI.encode(params_string)
end
# def request_string
# build_param_string
# URI_PATH + @request_param_string
# end
#
# def request_hash
# @request_params_hash
# end
#
end
class Request
REQUEST_PARAMS = {
country: nil, # name of a country 'USA', or Germany
state: nil, # 'IL', or 'illinois' (required in the United States)
county: nil, # supply as a parameter, returned as 'Adm='
locality: nil, # name of a place 'CHAMPAIGN' (or building, i.e. 'Eiffel Tower')
enableH2O: 'false',
hwyX: 'false',
doUncert: 'true',
doPoly: 'false',
displacePoly: 'false',
languageKey: '0',
fmt: 'json' # or geojson ?
}.freeze
attr_accessor :succeeded
attr_reader :request_params, :response, :request_param_string
# @param [ActionController::Parameters] request_params
def initialize(request_params)
@request_params = REQUEST_PARAMS.merge(request_params)
@succeeded = nil
end
# sets the response attribute.
def locate
@response = Georeference::GeoLocate::Response.new(self)
end
# sets the @request_param_string attribute.
def build_param_string
@request_param_string ||= @request_params.collect { |key, value| "#{key}=#{value}" }.join('&')
end
# @return [String] api request string.
def request_string
build_param_string
API_PATH + @request_param_string
end
# @return [Boolean] true if request was successful
def succeeded?
@succeeded
end
# @return [Georeference::GeoLocate::Response]
def response
@response ||= locate
end
end
class Response
attr_accessor :result
# @param [JSON object] request
def initialize(request)
@result = JSON.parse(call_api(Georeference::GeoLocate::API_HOST, request))
request.succeeded = true if @result['numResults'].to_i > 0
end
# @return [String] coordinates from the response set.
def coordinates
@result['resultSet']['features'][0]['geometry']['coordinates']
end
# @return [String] uncertainty_radius from the response set.
def uncertainty_radius
retval = @result['resultSet']['features'][0]['properties']['uncertaintyRadiusMeters']
(retval == 'Unavailable') ? 3 : retval
end
# @return [String] uncertainty_polygon from the response set.
def uncertainty_polygon
retval = @result['resultSet']['features'][0]['properties']['uncertaintyPolygon']
(retval == 'Unavailable') ? nil : retval['coordinates'][0]
end
protected
# @param [String] host domain name
# @param [String] request string.
# @return [HTTP object]
def call_api(host, request)
Net::HTTP.get(host, request.request_string)
end
end
end