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