Using an around_handler for catching and rendering errors in most of the API controll...
[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   # Helper methods for going to/from mercator and lat/lng.
201   class Mercator
202     include Math
203
204     #init me with your bounding box and the size of your image
205     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
206       xsize = xsheet(max_lon) - xsheet(min_lon)
207       ysize = ysheet(max_lat) - ysheet(min_lat)
208       xscale = xsize / width
209       yscale = ysize / height
210       scale = [xscale, yscale].max
211
212       xpad = width * scale - xsize
213       ypad = height * scale - ysize
214
215       @width = width
216       @height = height
217
218       @tx = xsheet(min_lon) - xpad / 2
219       @ty = ysheet(min_lat) - ypad / 2
220
221       @bx = xsheet(max_lon) + xpad / 2
222       @by = ysheet(max_lat) + ypad / 2
223     end
224
225     #the following two functions will give you the x/y on the entire sheet
226
227     def ysheet(lat)
228       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
229     end
230
231     def xsheet(lon)
232       lon
233     end
234
235     #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
236
237     def y(lat)
238       return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
239     end
240
241     def x(lon)
242       return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
243     end
244   end
245
246   class GreatCircle
247     include Math
248
249     # initialise with a base position
250     def initialize(lat, lon)
251       @lat = lat * PI / 180
252       @lon = lon * PI / 180
253     end
254
255     # get the distance from the base position to a given position
256     def distance(lat, lon)
257       lat = lat * PI / 180
258       lon = lon * PI / 180
259       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
260     end
261
262     # get the worst case bounds for a given radius from the base position
263     def bounds(radius)
264       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
265       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
266       minlat = (@lat - latradius) * 180 / PI
267       maxlat = (@lat + latradius) * 180 / PI
268       minlon = (@lon - lonradius) * 180 / PI
269       maxlon = (@lon + lonradius) * 180 / PI
270       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
271     end
272   end
273
274   class GeoRSS
275     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
276       @doc = XML::Document.new
277       @doc.encoding = XML::Encoding::UTF_8
278
279       rss = XML::Node.new 'rss'
280       @doc.root = rss
281       rss['version'] = "2.0"
282       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
283       @channel = XML::Node.new 'channel'
284       rss << @channel
285       title = XML::Node.new 'title'
286       title <<  feed_title
287       @channel << title
288       description_el = XML::Node.new 'description'
289       @channel << description_el
290
291       description_el << feed_description
292       link = XML::Node.new 'link'
293       link << feed_url
294       @channel << link
295       image = XML::Node.new 'image'
296       @channel << image
297       url = XML::Node.new 'url'
298       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
299       image << url
300       title = XML::Node.new 'title'
301       title << "OpenStreetMap"
302       image << title
303       width = XML::Node.new 'width'
304       width << '100'
305       image << width
306       height = XML::Node.new 'height'
307       height << '100'
308       image << height
309       link = XML::Node.new 'link'
310       link << feed_url
311       image << link
312     end
313
314     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)
315       item = XML::Node.new 'item'
316
317       title = XML::Node.new 'title'
318       item << title
319       title << title_text
320       link = XML::Node.new 'link'
321       link << url
322       item << link
323
324       guid = XML::Node.new 'guid'
325       guid << url
326       item << guid
327
328       description = XML::Node.new 'description'
329       description << description_text
330       item << description
331
332       author = XML::Node.new 'author'
333       author << author_text
334       item << author
335
336       pubDate = XML::Node.new 'pubDate'
337       pubDate << timestamp.to_s(:rfc822)
338       item << pubDate
339
340       if latitude
341         lat_el = XML::Node.new 'geo:lat'
342         lat_el << latitude.to_s
343         item << lat_el
344       end
345
346       if longitude
347         lon_el = XML::Node.new 'geo:long'
348         lon_el << longitude.to_s
349         item << lon_el
350       end
351
352       @channel << item
353     end
354
355     def to_s
356       return @doc.to_s
357     end
358   end
359
360   class API
361     def get_xml_doc
362       doc = XML::Document.new
363       doc.encoding = XML::Encoding::UTF_8
364       root = XML::Node.new 'osm'
365       root['version'] = API_VERSION
366       root['generator'] = GENERATOR
367       doc.root = root
368       return doc
369     end
370   end
371
372   def self.IPLocation(ip_address)
373     Timeout::timeout(4) do
374       Net::HTTP.start('api.hostip.info') do |http|
375         country = http.get("/country.php?ip=#{ip_address}").body
376         country = "GB" if country == "UK"
377         country = Country.find_by_code(country)
378         return { :minlon => country.min_lon, :minlat => country.min_lat, :maxlon => country.max_lon, :maxlat => country.max_lat }
379       end
380     end
381
382     return nil
383   rescue Exception
384     return nil
385   end
386
387   # Construct a random token of a given length
388   def self.make_token(length = 30)
389     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
390     token = ''
391
392     length.times do
393       token += chars[(rand * chars.length).to_i].chr
394     end
395
396     return token
397   end
398
399   # Return an encrypted version of a password
400   def self.encrypt_password(password, salt)
401     return Digest::MD5.hexdigest(password) if salt.nil?
402     return Digest::MD5.hexdigest(salt + password)
403   end
404
405   # Return an SQL fragment to select a given area of the globe
406   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
407     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
408     minlat = (minlat * 10000000).round
409     minlon = (minlon * 10000000).round
410     maxlat = (maxlat * 10000000).round
411     maxlon = (maxlon * 10000000).round
412
413     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
414   end
415
416
417 end