Add a global timeout that is applied to most API requests.
[rails.git] / lib / osm.rb
1 # The OSM module provides support functions for OSM.
2 module OSM
3
4   require 'time'
5   require 'rexml/parsers/sax2parser'
6   require 'rexml/text'
7   require 'xml/libxml'
8   require 'digest/md5'
9   require 'RMagick'
10
11   # The base class for API Errors.
12   class APIError < RuntimeError
13     def render_opts
14       { :text => "Generic API Error", :status => :internal_server_error, :content_type => "text/plain" }
15     end
16
17     def to_s
18       "Generic API Error"
19     end
20   end
21
22   # Raised when an API object is not found.
23   class APINotFoundError < APIError
24     def render_opts
25       { :text => "The API wasn't found", :status => :not_found, :content_type => "text/plain" }
26     end
27   end
28
29   # Raised when a precondition to an API action fails sanity check.
30   class APIPreconditionFailedError < APIError
31     def initialize(message = "")
32       @message = message
33     end
34     
35     def render_opts
36       { :text => "Precondition failed: #{@message}", :status => :precondition_failed, :content_type => "text/plain" }
37     end
38
39     def to_s
40       "Precondition failed: #{@message}"
41     end
42   end
43
44   # Raised when to delete an already-deleted object.
45   class APIAlreadyDeletedError < APIError
46     def initialize(object = "object", object_id = "")
47       @object, @object_id = object, object_id
48     end
49     
50     attr_reader :object, :object_id
51     
52     def render_opts
53       { :text => "The #{object} with the id #{object_id} has already been deleted", :status => :gone, :content_type => "text/plain" }
54     end
55   end
56
57   # Raised when the user logged in isn't the same as the changeset
58   class APIUserChangesetMismatchError < APIError
59     def render_opts
60       { :text => "The user doesn't own that changeset", :status => :conflict, :content_type => "text/plain" }
61     end
62   end
63
64   # Raised when the changeset provided is already closed
65   class APIChangesetAlreadyClosedError < APIError
66     def initialize(changeset)
67       @changeset = changeset
68     end
69
70     attr_reader :changeset
71     
72     def render_opts
73       { :text => "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}.", :status => :conflict, :content_type => "text/plain" }
74     end
75   end
76   
77   # Raised when a change is expecting a changeset, but the changeset doesn't exist
78   class APIChangesetMissingError < APIError
79     def render_opts
80       { :text => "You need to supply a changeset to be able to make a change", :status => :conflict, :content_type => "text/plain" }
81     end
82     
83     def to_s
84        "You need to supply a changeset to be able to make a change"
85     end
86   end
87
88   # Raised when a diff is uploaded containing many changeset IDs which don't match
89   # the changeset ID that the diff was uploaded to.
90   class APIChangesetMismatchError < APIError
91     def initialize(provided, allowed)
92       @provided, @allowed = provided, allowed
93     end
94     
95     def render_opts
96       { :text => "Changeset mismatch: Provided #{@provided} but only " +
97       "#{@allowed} is allowed.", :status => :conflict, :content_type => "text/plain" }
98     end
99   end
100   
101   # Raised when a diff upload has an unknown action. You can only have create,
102   # modify, or delete
103   class APIChangesetActionInvalid < APIError
104     def initialize(provided)
105       @provided = provided
106     end
107     
108     def render_opts
109       { :text => "Unknown action #{@provided}, choices are create, modify, delete.",
110       :status => :bad_request, :content_type => "text/plain" }
111     end
112   end
113
114   # Raised when bad XML is encountered which stops things parsing as
115   # they should.
116   class APIBadXMLError < APIError
117     def initialize(model, xml, message="")
118       @model, @xml, @message = model, xml, message
119     end
120
121     def render_opts
122       { :text => "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}",
123       :status => :bad_request, :content_type => "text/plain" }
124     end
125   end
126
127   # Raised when the provided version is not equal to the latest in the db.
128   class APIVersionMismatchError < APIError
129     def initialize(id, type, provided, latest)
130       @id, @type, @provided, @latest = id, type, provided, latest
131     end
132
133     attr_reader :provided, :latest, :id, :type
134
135     def render_opts
136       { :text => "Version mismatch: Provided " + provided.to_s +
137         ", server had: " + latest.to_s + " of " + type + " " + id.to_s, 
138         :status => :conflict, :content_type => "text/plain" }
139     end
140     
141     def to_s
142        "Version mismatch: Provided " + provided.to_s + ", server had: " + latest.to_s + " of " + type + " " + id.to_s
143     end
144   end
145
146   # raised when a two tags have a duplicate key string in an element.
147   # this is now forbidden by the API.
148   class APIDuplicateTagsError < APIError
149     def initialize(type, id, tag_key)
150       @type, @id, @tag_key = type, id, tag_key
151     end
152
153     attr_reader :type, :id, :tag_key
154
155     def render_opts
156       { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.",
157         :status => :bad_request, :content_type => "text/plain" }
158     end
159   end
160   
161   # Raised when a way has more than the configured number of way nodes.
162   # This prevents ways from being to long and difficult to work with
163   class APITooManyWayNodesError < APIError
164     def initialize(provided, max)
165       @provided, @max = provided, max
166     end
167     
168     attr_reader :provided, :max
169     
170     def render_opts
171       { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed",
172         :status => :bad_request, :content_type => "text/plain" }
173     end
174   end
175
176   ##
177   # raised when user input couldn't be parsed
178   class APIBadUserInput < APIError
179     def initialize(message)
180       @message = message
181     end
182
183     def render_opts
184       { :text => @message, :content_type => "text/plain", :status => :bad_request }
185     end
186   end
187
188   ##
189   # raised when an API call is made using a method not supported on that URI
190   class APIBadMethodError < APIError
191     def initialize(supported_method)
192       @supported_method = supported_method
193     end
194
195     def render_opts
196       { :text => "Only method #{@supported_method} is supported on this URI.", :status => :method_not_allowed }
197     end
198   end
199
200   ##
201   # raised when an API call takes too long
202   class APITimeoutError < APIError
203     def render_opts
204       { :text => "Request timed out", :status => :request_timeout }
205     end
206
207     def to_s
208       "Request timed out"
209     end
210   end
211
212   # Helper methods for going to/from mercator and lat/lng.
213   class Mercator
214     include Math
215
216     #init me with your bounding box and the size of your image
217     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
218       xsize = xsheet(max_lon) - xsheet(min_lon)
219       ysize = ysheet(max_lat) - ysheet(min_lat)
220       xscale = xsize / width
221       yscale = ysize / height
222       scale = [xscale, yscale].max
223
224       xpad = width * scale - xsize
225       ypad = height * scale - ysize
226
227       @width = width
228       @height = height
229
230       @tx = xsheet(min_lon) - xpad / 2
231       @ty = ysheet(min_lat) - ypad / 2
232
233       @bx = xsheet(max_lon) + xpad / 2
234       @by = ysheet(max_lat) + ypad / 2
235     end
236
237     #the following two functions will give you the x/y on the entire sheet
238
239     def ysheet(lat)
240       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
241     end
242
243     def xsheet(lon)
244       lon
245     end
246
247     #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
248
249     def y(lat)
250       return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
251     end
252
253     def x(lon)
254       return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
255     end
256   end
257
258   class GreatCircle
259     include Math
260
261     # initialise with a base position
262     def initialize(lat, lon)
263       @lat = lat * PI / 180
264       @lon = lon * PI / 180
265     end
266
267     # get the distance from the base position to a given position
268     def distance(lat, lon)
269       lat = lat * PI / 180
270       lon = lon * PI / 180
271       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
272     end
273
274     # get the worst case bounds for a given radius from the base position
275     def bounds(radius)
276       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
277       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
278       minlat = (@lat - latradius) * 180 / PI
279       maxlat = (@lat + latradius) * 180 / PI
280       minlon = (@lon - lonradius) * 180 / PI
281       maxlon = (@lon + lonradius) * 180 / PI
282       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
283     end
284   end
285
286   class GeoRSS
287     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
288       @doc = XML::Document.new
289       @doc.encoding = XML::Encoding::UTF_8
290
291       rss = XML::Node.new 'rss'
292       @doc.root = rss
293       rss['version'] = "2.0"
294       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
295       @channel = XML::Node.new 'channel'
296       rss << @channel
297       title = XML::Node.new 'title'
298       title <<  feed_title
299       @channel << title
300       description_el = XML::Node.new 'description'
301       @channel << description_el
302
303       description_el << feed_description
304       link = XML::Node.new 'link'
305       link << feed_url
306       @channel << link
307       image = XML::Node.new 'image'
308       @channel << image
309       url = XML::Node.new 'url'
310       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
311       image << url
312       title = XML::Node.new 'title'
313       title << "OpenStreetMap"
314       image << title
315       width = XML::Node.new 'width'
316       width << '100'
317       image << width
318       height = XML::Node.new 'height'
319       height << '100'
320       image << height
321       link = XML::Node.new 'link'
322       link << feed_url
323       image << link
324     end
325
326     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)
327       item = XML::Node.new 'item'
328
329       title = XML::Node.new 'title'
330       item << title
331       title << title_text
332       link = XML::Node.new 'link'
333       link << url
334       item << link
335
336       guid = XML::Node.new 'guid'
337       guid << url
338       item << guid
339
340       description = XML::Node.new 'description'
341       description << description_text
342       item << description
343
344       author = XML::Node.new 'author'
345       author << author_text
346       item << author
347
348       pubDate = XML::Node.new 'pubDate'
349       pubDate << timestamp.to_s(:rfc822)
350       item << pubDate
351
352       if latitude
353         lat_el = XML::Node.new 'geo:lat'
354         lat_el << latitude.to_s
355         item << lat_el
356       end
357
358       if longitude
359         lon_el = XML::Node.new 'geo:long'
360         lon_el << longitude.to_s
361         item << lon_el
362       end
363
364       @channel << item
365     end
366
367     def to_s
368       return @doc.to_s
369     end
370   end
371
372   class API
373     def get_xml_doc
374       doc = XML::Document.new
375       doc.encoding = XML::Encoding::UTF_8
376       root = XML::Node.new 'osm'
377       root['version'] = API_VERSION
378       root['generator'] = GENERATOR
379       doc.root = root
380       return doc
381     end
382   end
383
384   def self.IPLocation(ip_address)
385     Timeout::timeout(4) do
386       Net::HTTP.start('api.hostip.info') do |http|
387         country = http.get("/country.php?ip=#{ip_address}").body
388         country = "GB" if country == "UK"
389         country = Country.find_by_code(country)
390         return { :minlon => country.min_lon, :minlat => country.min_lat, :maxlon => country.max_lon, :maxlat => country.max_lat }
391       end
392     end
393
394     return nil
395   rescue Exception
396     return nil
397   end
398
399   # Construct a random token of a given length
400   def self.make_token(length = 30)
401     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
402     token = ''
403
404     length.times do
405       token += chars[(rand * chars.length).to_i].chr
406     end
407
408     return token
409   end
410
411   # Return an encrypted version of a password
412   def self.encrypt_password(password, salt)
413     return Digest::MD5.hexdigest(password) if salt.nil?
414     return Digest::MD5.hexdigest(salt + password)
415   end
416
417   # Return an SQL fragment to select a given area of the globe
418   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
419     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
420     minlat = (minlat * 10000000).round
421     minlon = (minlon * 10000000).round
422     maxlat = (maxlat * 10000000).round
423     maxlon = (maxlon * 10000000).round
424
425     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
426   end
427
428
429 end