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