Potlatch 1.0
[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}, server had: #{latest} of #{type} #{id}",
137         :status => :conflict, :content_type => "text/plain" }
138     end
139     
140     def to_s
141        "Version mismatch: Provided #{provided}, server had: #{latest} of #{type} #{id}"
142     end
143   end
144
145   # raised when a two tags have a duplicate key string in an element.
146   # this is now forbidden by the API.
147   class APIDuplicateTagsError < APIError
148     def initialize(type, id, tag_key)
149       @type, @id, @tag_key = type, id, tag_key
150     end
151
152     attr_reader :type, :id, :tag_key
153
154     def render_opts
155       { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.",
156         :status => :bad_request, :content_type => "text/plain" }
157     end
158   end
159   
160   # Raised when a way has more than the configured number of way nodes.
161   # This prevents ways from being to long and difficult to work with
162   class APITooManyWayNodesError < APIError
163     def initialize(provided, max)
164       @provided, @max = provided, max
165     end
166     
167     attr_reader :provided, :max
168     
169     def render_opts
170       { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed",
171         :status => :bad_request, :content_type => "text/plain" }
172     end
173   end
174
175   ##
176   # raised when user input couldn't be parsed
177   class APIBadUserInput < APIError
178     def initialize(message)
179       @message = message
180     end
181
182     def render_opts
183       { :text => @message, :content_type => "text/plain", :status => :bad_request }
184     end
185   end
186
187   ##
188   # raised when an API call is made using a method not supported on that URI
189   class APIBadMethodError < APIError
190     def initialize(supported_method)
191       @supported_method = supported_method
192     end
193
194     def render_opts
195       { :text => "Only method #{@supported_method} is supported on this URI.", :status => :method_not_allowed }
196     end
197   end
198
199   ##
200   # raised when an API call takes too long
201   class APITimeoutError < APIError
202     def render_opts
203       { :text => "Request timed out", :status => :request_timeout }
204     end
205
206     def to_s
207       "Request timed out"
208     end
209   end
210
211   # Helper methods for going to/from mercator and lat/lng.
212   class Mercator
213     include Math
214
215     #init me with your bounding box and the size of your image
216     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
217       xsize = xsheet(max_lon) - xsheet(min_lon)
218       ysize = ysheet(max_lat) - ysheet(min_lat)
219       xscale = xsize / width
220       yscale = ysize / height
221       scale = [xscale, yscale].max
222
223       xpad = width * scale - xsize
224       ypad = height * scale - ysize
225
226       @width = width
227       @height = height
228
229       @tx = xsheet(min_lon) - xpad / 2
230       @ty = ysheet(min_lat) - ypad / 2
231
232       @bx = xsheet(max_lon) + xpad / 2
233       @by = ysheet(max_lat) + ypad / 2
234     end
235
236     #the following two functions will give you the x/y on the entire sheet
237
238     def ysheet(lat)
239       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
240     end
241
242     def xsheet(lon)
243       lon
244     end
245
246     #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
247
248     def y(lat)
249       return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
250     end
251
252     def x(lon)
253       return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
254     end
255   end
256
257   class GreatCircle
258     include Math
259
260     # initialise with a base position
261     def initialize(lat, lon)
262       @lat = lat * PI / 180
263       @lon = lon * PI / 180
264     end
265
266     # get the distance from the base position to a given position
267     def distance(lat, lon)
268       lat = lat * PI / 180
269       lon = lon * PI / 180
270       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
271     end
272
273     # get the worst case bounds for a given radius from the base position
274     def bounds(radius)
275       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
276       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
277       minlat = (@lat - latradius) * 180 / PI
278       maxlat = (@lat + latradius) * 180 / PI
279       minlon = (@lon - lonradius) * 180 / PI
280       maxlon = (@lon + lonradius) * 180 / PI
281       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
282     end
283   end
284
285   class GeoRSS
286     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
287       @doc = XML::Document.new
288       @doc.encoding = XML::Encoding::UTF_8
289
290       rss = XML::Node.new 'rss'
291       @doc.root = rss
292       rss['version'] = "2.0"
293       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
294       @channel = XML::Node.new 'channel'
295       rss << @channel
296       title = XML::Node.new 'title'
297       title <<  feed_title
298       @channel << title
299       description_el = XML::Node.new 'description'
300       @channel << description_el
301
302       description_el << feed_description
303       link = XML::Node.new 'link'
304       link << feed_url
305       @channel << link
306       image = XML::Node.new 'image'
307       @channel << image
308       url = XML::Node.new 'url'
309       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
310       image << url
311       title = XML::Node.new 'title'
312       title << "OpenStreetMap"
313       image << title
314       width = XML::Node.new 'width'
315       width << '100'
316       image << width
317       height = XML::Node.new 'height'
318       height << '100'
319       image << height
320       link = XML::Node.new 'link'
321       link << feed_url
322       image << link
323     end
324
325     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)
326       item = XML::Node.new 'item'
327
328       title = XML::Node.new 'title'
329       item << title
330       title << title_text
331       link = XML::Node.new 'link'
332       link << url
333       item << link
334
335       guid = XML::Node.new 'guid'
336       guid << url
337       item << guid
338
339       description = XML::Node.new 'description'
340       description << description_text
341       item << description
342
343       author = XML::Node.new 'author'
344       author << author_text
345       item << author
346
347       pubDate = XML::Node.new 'pubDate'
348       pubDate << timestamp.to_s(:rfc822)
349       item << pubDate
350
351       if latitude
352         lat_el = XML::Node.new 'geo:lat'
353         lat_el << latitude.to_s
354         item << lat_el
355       end
356
357       if longitude
358         lon_el = XML::Node.new 'geo:long'
359         lon_el << longitude.to_s
360         item << lon_el
361       end
362
363       @channel << item
364     end
365
366     def to_s
367       return @doc.to_s
368     end
369   end
370
371   class API
372     def get_xml_doc
373       doc = XML::Document.new
374       doc.encoding = XML::Encoding::UTF_8
375       root = XML::Node.new 'osm'
376       root['version'] = API_VERSION
377       root['generator'] = GENERATOR
378       doc.root = root
379       return doc
380     end
381   end
382
383   def self.IPLocation(ip_address)
384     Timeout::timeout(4) do
385       Net::HTTP.start('api.hostip.info') do |http|
386         country = http.get("/country.php?ip=#{ip_address}").body
387         country = "GB" if country == "UK"
388         country = Country.find_by_code(country)
389         return { :minlon => country.min_lon, :minlat => country.min_lat, :maxlon => country.max_lon, :maxlat => country.max_lat }
390       end
391     end
392
393     return nil
394   rescue Exception
395     return nil
396   end
397
398   # Construct a random token of a given length
399   def self.make_token(length = 30)
400     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
401     token = ''
402
403     length.times do
404       token += chars[(rand * chars.length).to_i].chr
405     end
406
407     return token
408   end
409
410   # Return an encrypted version of a password
411   def self.encrypt_password(password, salt)
412     return Digest::MD5.hexdigest(password) if salt.nil?
413     return Digest::MD5.hexdigest(salt + password)
414   end
415
416   # Return an SQL fragment to select a given area of the globe
417   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
418     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
419     minlat = (minlat * 10000000).round
420     minlon = (minlon * 10000000).round
421     maxlat = (maxlat * 10000000).round
422     maxlon = (maxlon * 10000000).round
423
424     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
425   end
426
427
428 end