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