openstreetmap-website/lib/osm.rb
Tom Hughes 5de81dc4b0 More work on optimisation the location of nearby users - it turns out
that getting the database to do the filtering is much better as it
avoids us constructing thousands of user objects only to then throw
most of them away again.
2009-06-05 09:30:31 +00:00

487 lines
12 KiB
Ruby

# The OSM module provides support functions for OSM.
module OSM
require 'time'
require 'rexml/parsers/sax2parser'
require 'rexml/text'
require 'xml/libxml'
require 'digest/md5'
require 'RMagick'
# The base class for API Errors.
class APIError < RuntimeError
def status
:internal_server_error
end
def to_s
"Generic API Error"
end
end
# Raised when an API object is not found.
class APINotFoundError < APIError
def status
:not_found
end
def to_s
"Object not found"
end
end
# Raised when a precondition to an API action fails sanity check.
class APIPreconditionFailedError < APIError
def initialize(message = "")
@message = message
end
def status
:precondition_failed
end
def to_s
"Precondition failed: #{@message}"
end
end
# Raised when to delete an already-deleted object.
class APIAlreadyDeletedError < APIError
def initialize(object = "object", object_id = "")
@object, @object_id = object, object_id
end
attr_reader :object, :object_id
def status
:gone
end
def to_s
"The #{object} with the id #{object_id} has already been deleted"
end
end
# Raised when the user logged in isn't the same as the changeset
class APIUserChangesetMismatchError < APIError
def status
:conflict
end
def to_s
"The user doesn't own that changeset"
end
end
# Raised when the changeset provided is already closed
class APIChangesetAlreadyClosedError < APIError
def initialize(changeset)
@changeset = changeset
end
attr_reader :changeset
def status
:conflict
end
def to_s
"The changeset #{@changeset.id} was closed at #{@changeset.closed_at}"
end
end
# Raised when a change is expecting a changeset, but the changeset doesn't exist
class APIChangesetMissingError < APIError
def status
:conflict
end
def to_s
"You need to supply a changeset to be able to make a change"
end
end
# Raised when a diff is uploaded containing many changeset IDs which don't match
# the changeset ID that the diff was uploaded to.
class APIChangesetMismatchError < APIError
def initialize(provided, allowed)
@provided, @allowed = provided, allowed
end
def status
:conflict
end
def to_s
"Changeset mismatch: Provided #{@provided} but only #{@allowed} is allowed"
end
end
# Raised when a diff upload has an unknown action. You can only have create,
# modify, or delete
class APIChangesetActionInvalid < APIError
def initialize(provided)
@provided = provided
end
def status
:bad_request
end
def to_s
"Unknown action #{@provided}, choices are create, modify, delete"
end
end
# Raised when bad XML is encountered which stops things parsing as
# they should.
class APIBadXMLError < APIError
def initialize(model, xml, message="")
@model, @xml, @message = model, xml, message
end
def status
:bad_request
end
def to_s
"Cannot parse valid #{@model} from xml string #{@xml}. #{@message}"
end
end
# Raised when the provided version is not equal to the latest in the db.
class APIVersionMismatchError < APIError
def initialize(id, type, provided, latest)
@id, @type, @provided, @latest = id, type, provided, latest
end
attr_reader :provided, :latest, :id, :type
def status
:conflict
end
def to_s
"Version mismatch: Provided #{provided}, server had: #{latest} of #{type} #{id}"
end
end
# raised when a two tags have a duplicate key string in an element.
# this is now forbidden by the API.
class APIDuplicateTagsError < APIError
def initialize(type, id, tag_key)
@type, @id, @tag_key = type, id, tag_key
end
attr_reader :type, :id, :tag_key
def status
:bad_request
end
def to_s
"Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}"
end
end
# Raised when a way has more than the configured number of way nodes.
# This prevents ways from being to long and difficult to work with
class APITooManyWayNodesError < APIError
def initialize(provided, max)
@provided, @max = provided, max
end
attr_reader :provided, :max
def status
:bad_request
end
def to_s
"You tried to add #{provided} nodes to the way, however only #{max} are allowed"
end
end
##
# raised when user input couldn't be parsed
class APIBadUserInput < APIError
def initialize(message)
@message = message
end
def status
:bad_request
end
def to_s
@message
end
end
##
# raised when bounding box is invalid
class APIBadBoundingBox < APIError
def initialize(message)
@message = message
end
def status
:bad_request
end
def to_s
@message
end
end
##
# raised when an API call is made using a method not supported on that URI
class APIBadMethodError < APIError
def initialize(supported_method)
@supported_method = supported_method
end
def status
:method_not_allowed
end
def to_s
"Only method #{@supported_method} is supported on this URI"
end
end
##
# raised when an API call takes too long
class APITimeoutError < APIError
def status
:request_timeout
end
def to_s
"Request timed out"
end
end
# Helper methods for going to/from mercator and lat/lng.
class Mercator
include Math
#init me with your bounding box and the size of your image
def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
xsize = xsheet(max_lon) - xsheet(min_lon)
ysize = ysheet(max_lat) - ysheet(min_lat)
xscale = xsize / width
yscale = ysize / height
scale = [xscale, yscale].max
xpad = width * scale - xsize
ypad = height * scale - ysize
@width = width
@height = height
@tx = xsheet(min_lon) - xpad / 2
@ty = ysheet(min_lat) - ypad / 2
@bx = xsheet(max_lon) + xpad / 2
@by = ysheet(max_lat) + ypad / 2
end
#the following two functions will give you the x/y on the entire sheet
def ysheet(lat)
log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
end
def xsheet(lon)
lon
end
#and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
def y(lat)
return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
end
def x(lon)
return ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
end
end
class GreatCircle
include Math
# initialise with a base position
def initialize(lat, lon)
@lat = lat * PI / 180
@lon = lon * PI / 180
end
# get the distance from the base position to a given position
def distance(lat, lon)
lat = lat * PI / 180
lon = lon * PI / 180
return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
end
# get the worst case bounds for a given radius from the base position
def bounds(radius)
latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
minlat = (@lat - latradius) * 180 / PI
maxlat = (@lat + latradius) * 180 / PI
minlon = (@lon - lonradius) * 180 / PI
maxlon = (@lon + lonradius) * 180 / PI
return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
end
# get the SQL to use to calculate distance
def sql_for_distance(lat_field, lon_field)
"6372.795 * 2 * asin(sqrt(power(sin((radians(#{lat_field}) - #{@lat}) / 2), 2) + cos(#{@lat}) * cos(radians(#{lat_field})) * power(sin((radians(#{lon_field}) - #{@lon})/2), 2)))"
end
end
class GeoRSS
def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
@doc = XML::Document.new
@doc.encoding = XML::Encoding::UTF_8
rss = XML::Node.new 'rss'
@doc.root = rss
rss['version'] = "2.0"
rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
@channel = XML::Node.new 'channel'
rss << @channel
title = XML::Node.new 'title'
title << feed_title
@channel << title
description_el = XML::Node.new 'description'
@channel << description_el
description_el << feed_description
link = XML::Node.new 'link'
link << feed_url
@channel << link
image = XML::Node.new 'image'
@channel << image
url = XML::Node.new 'url'
url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
image << url
title = XML::Node.new 'title'
title << "OpenStreetMap"
image << title
width = XML::Node.new 'width'
width << '100'
image << width
height = XML::Node.new 'height'
height << '100'
image << height
link = XML::Node.new 'link'
link << feed_url
image << link
end
def add(latitude=0, longitude=0, title_text='dummy title', author_text='anonymous', url='http://www.example.com/', description_text='dummy description', timestamp=DateTime.now)
item = XML::Node.new 'item'
title = XML::Node.new 'title'
item << title
title << title_text
link = XML::Node.new 'link'
link << url
item << link
guid = XML::Node.new 'guid'
guid << url
item << guid
description = XML::Node.new 'description'
description << description_text
item << description
author = XML::Node.new 'author'
author << author_text
item << author
pubDate = XML::Node.new 'pubDate'
pubDate << timestamp.to_s(:rfc822)
item << pubDate
if latitude
lat_el = XML::Node.new 'geo:lat'
lat_el << latitude.to_s
item << lat_el
end
if longitude
lon_el = XML::Node.new 'geo:long'
lon_el << longitude.to_s
item << lon_el
end
@channel << item
end
def to_s
return @doc.to_s
end
end
class API
def get_xml_doc
doc = XML::Document.new
doc.encoding = XML::Encoding::UTF_8
root = XML::Node.new 'osm'
root['version'] = API_VERSION
root['generator'] = GENERATOR
doc.root = root
return doc
end
end
def self.IPLocation(ip_address)
Timeout::timeout(4) do
Net::HTTP.start('api.hostip.info') do |http|
country = http.get("/country.php?ip=#{ip_address}").body
country = "GB" if country == "UK"
country = Country.find_by_code(country)
return { :minlon => country.min_lon, :minlat => country.min_lat, :maxlon => country.max_lon, :maxlat => country.max_lat }
end
end
return nil
rescue Exception
return nil
end
# Construct a random token of a given length
def self.make_token(length = 30)
chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
token = ''
length.times do
token += chars[(rand * chars.length).to_i].chr
end
return token
end
# Return an encrypted version of a password
def self.encrypt_password(password, salt)
return Digest::MD5.hexdigest(password) if salt.nil?
return Digest::MD5.hexdigest(salt + password)
end
# Return an SQL fragment to select a given area of the globe
def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
minlat = (minlat * 10000000).round
minlon = (minlon * 10000000).round
maxlat = (maxlat * 10000000).round
maxlon = (maxlon * 10000000).round
return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
end
end