Fixed some bugs in changeset query code. Added more test cases.
[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   # Helper methods for going to/from mercator and lat/lng.
145   class Mercator
146     include Math
147
148     #init me with your bounding box and the size of your image
149     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
150       xsize = xsheet(max_lon) - xsheet(min_lon)
151       ysize = ysheet(max_lat) - ysheet(min_lat)
152       xscale = xsize / width
153       yscale = ysize / height
154       scale = [xscale, yscale].max
155
156       xpad = width * scale - xsize
157       ypad = height * scale - ysize
158
159       @width = width
160       @height = height
161
162       @tx = xsheet(min_lon) - xpad / 2
163       @ty = ysheet(min_lat) - ypad / 2
164
165       @bx = xsheet(max_lon) + xpad / 2
166       @by = ysheet(max_lat) + ypad / 2
167     end
168
169     #the following two functions will give you the x/y on the entire sheet
170
171     def ysheet(lat)
172       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
173     end
174
175     def xsheet(lon)
176       lon
177     end
178
179     #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
180
181     def y(lat)
182       return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
183     end
184
185     def x(lon)
186       return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
187     end
188   end
189
190   class GreatCircle
191     include Math
192
193     # initialise with a base position
194     def initialize(lat, lon)
195       @lat = lat * PI / 180
196       @lon = lon * PI / 180
197     end
198
199     # get the distance from the base position to a given position
200     def distance(lat, lon)
201       lat = lat * PI / 180
202       lon = lon * PI / 180
203       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
204     end
205
206     # get the worst case bounds for a given radius from the base position
207     def bounds(radius)
208       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
209       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
210       minlat = (@lat - latradius) * 180 / PI
211       maxlat = (@lat + latradius) * 180 / PI
212       minlon = (@lon - lonradius) * 180 / PI
213       maxlon = (@lon + lonradius) * 180 / PI
214       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
215     end
216   end
217
218   class GeoRSS
219     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
220       @doc = XML::Document.new
221       @doc.encoding = 'UTF-8' 
222
223       rss = XML::Node.new 'rss'
224       @doc.root = rss
225       rss['version'] = "2.0"
226       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
227       @channel = XML::Node.new 'channel'
228       rss << @channel
229       title = XML::Node.new 'title'
230       title <<  feed_title
231       @channel << title
232       description_el = XML::Node.new 'description'
233       @channel << description_el
234
235       description_el << feed_description
236       link = XML::Node.new 'link'
237       link << feed_url
238       @channel << link
239       image = XML::Node.new 'image'
240       @channel << image
241       url = XML::Node.new 'url'
242       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
243       image << url
244       title = XML::Node.new 'title'
245       title << "OpenStreetMap"
246       image << title
247       width = XML::Node.new 'width'
248       width << '100'
249       image << width
250       height = XML::Node.new 'height'
251       height << '100'
252       image << height
253       link = XML::Node.new 'link'
254       link << feed_url
255       image << link
256     end
257
258     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)
259       item = XML::Node.new 'item'
260
261       title = XML::Node.new 'title'
262       item << title
263       title << title_text
264       link = XML::Node.new 'link'
265       link << url
266       item << link
267
268       guid = XML::Node.new 'guid'
269       guid << url
270       item << guid
271
272       description = XML::Node.new 'description'
273       description << description_text
274       item << description
275
276       author = XML::Node.new 'author'
277       author << author_text
278       item << author
279
280       pubDate = XML::Node.new 'pubDate'
281       pubDate << timestamp.to_s(:rfc822)
282       item << pubDate
283
284       if latitude
285         lat_el = XML::Node.new 'geo:lat'
286         lat_el << latitude.to_s
287         item << lat_el
288       end
289
290       if longitude
291         lon_el = XML::Node.new 'geo:long'
292         lon_el << longitude.to_s
293         item << lon_el
294       end
295
296       @channel << item
297     end
298
299     def to_s
300       return @doc.to_s
301     end
302   end
303
304   class API
305     def get_xml_doc
306       doc = XML::Document.new
307       doc.encoding = 'UTF-8' 
308       root = XML::Node.new 'osm'
309       root['version'] = API_VERSION
310       root['generator'] = GENERATOR
311       doc.root = root
312       return doc
313     end
314   end
315
316   def self.IPLocation(ip_address)
317     Timeout::timeout(4) do
318       Net::HTTP.start('api.hostip.info') do |http|
319         country = http.get("/country.php?ip=#{ip_address}").body
320         country = "GB" if country == "UK"
321         Net::HTTP.start('ws.geonames.org') do |http|
322           xml = REXML::Document.new(http.get("/countryInfo?country=#{country}").body)
323           xml.elements.each("geonames/country") do |ele|
324             minlon = ele.get_text("bBoxWest").to_s
325             minlat = ele.get_text("bBoxSouth").to_s
326             maxlon = ele.get_text("bBoxEast").to_s
327             maxlat = ele.get_text("bBoxNorth").to_s
328             return { :minlon => minlon, :minlat => minlat, :maxlon => maxlon, :maxlat => maxlat }
329           end
330         end
331       end
332     end
333
334     return nil
335   rescue Exception
336     return nil
337   end
338
339   # Construct a random token of a given length
340   def self.make_token(length = 30)
341     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
342     token = ''
343
344     length.times do
345       token += chars[(rand * chars.length).to_i].chr
346     end
347
348     return token
349   end
350
351   # Return an encrypted version of a password
352   def self.encrypt_password(password, salt)
353     return Digest::MD5.hexdigest(password) if salt.nil?
354     return Digest::MD5.hexdigest(salt + password)
355   end
356
357   # Return an SQL fragment to select a given area of the globe
358   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
359     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
360     minlat = (minlat * 10000000).round
361     minlon = (minlon * 10000000).round
362     maxlat = (maxlat * 10000000).round
363     maxlon = (maxlon * 10000000).round
364
365     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
366   end
367
368
369 end