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