api06: simplify exception handling and add exception handling to the diff
[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 { :text => "", :status => :internal_server_error } end
14   end
15
16   # Raised when an API object is not found.
17   class APINotFoundError < APIError
18   end
19
20   # Raised when a precondition to an API action fails sanity check.
21   class APIPreconditionFailedError < APIError
22     def render_opts { :text => "", :status => :precondition_failed } end
23   end
24
25   # Raised when to delete an already-deleted object.
26   class APIAlreadyDeletedError < APIError
27     def render_opts { :text => "", :status => :gone } end
28   end
29
30   # Raised when the provided version is not equal to the latest in the db.
31   class APIVersionMismatchError < APIError
32     def initialize(provided, latest)
33       @provided, @latest = provided, latest
34     end
35
36     attr_reader :provided, :latest
37
38     def render_opts
39       { :text => "Version mismatch: Provided " + ex.provided.to_s +
40         ", server had: " + ex.latest.to_s, :status => :bad_request }
41     end
42   end
43
44   # Helper methods for going to/from mercator and lat/lng.
45   class Mercator
46     include Math
47
48     #init me with your bounding box and the size of your image
49     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
50       xsize = xsheet(max_lon) - xsheet(min_lon)
51       ysize = ysheet(max_lat) - ysheet(min_lat)
52       xscale = xsize / width
53       yscale = ysize / height
54       scale = [xscale, yscale].max
55
56       xpad = width * scale - xsize
57       ypad = height * scale - ysize
58
59       @width = width
60       @height = height
61
62       @tx = xsheet(min_lon) - xpad / 2
63       @ty = ysheet(min_lat) - ypad / 2
64
65       @bx = xsheet(max_lon) + xpad / 2
66       @by = ysheet(max_lat) + ypad / 2
67     end
68
69     #the following two functions will give you the x/y on the entire sheet
70
71     def ysheet(lat)
72       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
73     end
74
75     def xsheet(lon)
76       lon
77     end
78
79     #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
80
81     def y(lat)
82       return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
83     end
84
85     def x(lon)
86       return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
87     end
88   end
89
90
91   # This piece of magic reads a GPX with SAX and spits out
92   # lat/lng and stuff
93   #
94   # This would print every latitude value:
95   #
96   # gpx = OSM::GPXImporter.new('somefile.gpx')
97   # gpx.points {|p| puts p['latitude']}
98   class GPXImporter
99     # FIXME swap REXML for libXML
100     attr_reader :possible_points
101     attr_reader :actual_points
102     attr_reader :tracksegs
103
104     def initialize(file)
105       @file = file
106     end
107
108     def points
109       @possible_points = 0
110       @actual_points = 0
111       @tracksegs = 0
112
113       lat = -1
114       lon = -1
115       ele = -1
116       date = DateTime.now();
117       gotlatlon = false
118       gotele = false
119       gotdate = false
120
121       @file.rewind
122
123       parser = REXML::Parsers::SAX2Parser.new(@file)
124
125       parser.listen( :start_element,  %w{ trkpt }) do |uri,localname,qname,attributes| 
126         lat = attributes['lat'].to_f
127         lon = attributes['lon'].to_f
128         gotlatlon = true
129         gotele = false
130         gotdate = false
131         @possible_points += 1
132       end
133
134       parser.listen( :characters, %w{ ele } ) do |text|
135         ele = text
136         gotele = true
137       end
138
139       parser.listen( :characters, %w{ time } ) do |text|
140         if text && text != ''
141           begin
142             date = DateTime.parse(text)
143             gotdate = true
144           rescue
145           end
146         end
147       end
148
149       parser.listen( :end_element, %w{ trkseg } ) do |uri, localname, qname|
150         @tracksegs += 1
151       end
152
153       parser.listen( :end_element, %w{ trkpt } ) do |uri,localname,qname|
154         if gotlatlon && gotdate
155           ele = '0' unless gotele
156           if lat < 90 && lat > -90 && lon > -180 && lon < 180
157             @actual_points += 1
158             yield Hash['latitude' => lat, 'longitude' => lon, 'timestamp' => date, 'altitude' => ele, 'segment' => @tracksegs]
159           end
160         end
161         gotlatlon = false
162         gotele = false
163         gotdate = false
164       end
165
166       parser.parse
167     end
168
169     def get_picture(min_lat, min_lon, max_lat, max_lon, num_points)
170       #puts "getting picfor bbox #{min_lat},#{min_lon} - #{max_lat},#{max_lon}"
171       frames = 10
172       width = 250
173       height = 250
174       proj = OSM::Mercator.new(min_lat, min_lon, max_lat, max_lon, width, height)
175
176       linegc = Magick::Draw.new
177       linegc.stroke_linejoin('miter')
178       linegc.stroke_width(1)
179       linegc.stroke('#BBBBBB')
180       linegc.fill('#BBBBBB')
181
182       highlightgc = Magick::Draw.new
183       highlightgc.stroke_linejoin('miter')
184       highlightgc.stroke_width(3)
185       highlightgc.stroke('#000000')
186       highlightgc.fill('#000000')
187
188       images = []
189
190       frames.times do
191         image = Magick::Image.new(width, height) do |image|
192           image.background_color = 'white'
193           image.format = 'GIF'
194         end
195
196         images << image
197       end
198
199       oldpx = 0.0
200       oldpy = 0.0
201
202       first = true
203
204       m = 0
205       mm = 0
206       points do |p|
207         px = proj.x(p['longitude'])
208         py = proj.y(p['latitude'])
209
210         if m > 0
211           frames.times do |n|
212             if n == mm
213               gc = highlightgc.dup
214             else
215               gc = linegc.dup
216             end
217
218             gc.line(px, py, oldpx, oldpy)
219
220             gc.draw(images[n])
221           end
222         end
223
224         m += 1
225         if m > num_points.to_f / frames.to_f * (mm+1)
226           mm += 1
227         end
228
229         oldpy = py
230         oldpx = px
231       end
232
233       il = Magick::ImageList.new
234
235       images.each do |f|
236         il << f
237       end
238
239       il.delay = 50
240       il.format = 'GIF'
241
242       return il.to_blob
243     end
244
245     def get_icon(min_lat, min_lon, max_lat, max_lon)
246       #puts "getting icon for bbox #{min_lat},#{min_lon} - #{max_lat},#{max_lon}"
247       width = 50
248       height = 50
249       proj = OSM::Mercator.new(min_lat, min_lon, max_lat, max_lon, width, height)
250
251       gc = Magick::Draw.new
252       gc.stroke_linejoin('miter')
253       gc.stroke_width(1)
254       gc.stroke('#000000')
255       gc.fill('#000000')
256
257       image = Magick::Image.new(width, height) do |image|
258         image.background_color = 'white'
259         image.format = 'GIF'
260       end
261
262       oldpx = 0.0
263       oldpy = 0.0
264
265       first = true
266
267       points do |p|
268         px = proj.x(p['longitude'])
269         py = proj.y(p['latitude'])
270
271         gc.dup.line(px, py, oldpx, oldpy).draw(image) unless first
272
273         first = false
274         oldpy = py
275         oldpx = px
276       end
277
278       return image.to_blob
279     end
280
281   end
282
283   class GreatCircle
284     include Math
285
286     # initialise with a base position
287     def initialize(lat, lon)
288       @lat = lat * PI / 180
289       @lon = lon * PI / 180
290     end
291
292     # get the distance from the base position to a given position
293     def distance(lat, lon)
294       lat = lat * PI / 180
295       lon = lon * PI / 180
296       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
297     end
298
299     # get the worst case bounds for a given radius from the base position
300     def bounds(radius)
301       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
302       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
303       minlat = (@lat - latradius) * 180 / PI
304       maxlat = (@lat + latradius) * 180 / PI
305       minlon = (@lon - lonradius) * 180 / PI
306       maxlon = (@lon + lonradius) * 180 / PI
307       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
308     end
309   end
310
311   class GeoRSS
312     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
313       @doc = XML::Document.new
314       @doc.encoding = 'UTF-8' 
315
316       rss = XML::Node.new 'rss'
317       @doc.root = rss
318       rss['version'] = "2.0"
319       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
320       @channel = XML::Node.new 'channel'
321       rss << @channel
322       title = XML::Node.new 'title'
323       title <<  feed_title
324       @channel << title
325       description_el = XML::Node.new 'description'
326       @channel << description_el
327
328       description_el << feed_description
329       link = XML::Node.new 'link'
330       link << feed_url
331       @channel << link
332       image = XML::Node.new 'image'
333       @channel << image
334       url = XML::Node.new 'url'
335       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
336       image << url
337       title = XML::Node.new 'title'
338       title << "OpenStreetMap"
339       image << title
340       width = XML::Node.new 'width'
341       width << '100'
342       image << width
343       height = XML::Node.new 'height'
344       height << '100'
345       image << height
346       link = XML::Node.new 'link'
347       link << feed_url
348       image << link
349     end
350
351     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)
352       item = XML::Node.new 'item'
353
354       title = XML::Node.new 'title'
355       item << title
356       title << title_text
357       link = XML::Node.new 'link'
358       link << url
359       item << link
360
361       guid = XML::Node.new 'guid'
362       guid << url
363       item << guid
364
365       description = XML::Node.new 'description'
366       description << description_text
367       item << description
368
369       author = XML::Node.new 'author'
370       author << author_text
371       item << author
372
373       pubDate = XML::Node.new 'pubDate'
374       pubDate << timestamp.to_s(:rfc822)
375       item << pubDate
376
377       if latitude
378         lat_el = XML::Node.new 'geo:lat'
379         lat_el << latitude.to_s
380         item << lat_el
381       end
382
383       if longitude
384         lon_el = XML::Node.new 'geo:long'
385         lon_el << longitude.to_s
386         item << lon_el
387       end
388
389       @channel << item
390     end
391
392     def to_s
393       return @doc.to_s
394     end
395   end
396
397   class API
398     def get_xml_doc
399       doc = XML::Document.new
400       doc.encoding = 'UTF-8' 
401       root = XML::Node.new 'osm'
402       root['version'] = API_VERSION
403       root['generator'] = 'OpenStreetMap server'
404       doc.root = root
405       return doc
406     end
407   end
408
409   def self.IPLocation(ip_address)
410     Timeout::timeout(4) do
411       Net::HTTP.start('api.hostip.info') do |http|
412         country = http.get("/country.php?ip=#{ip_address}").body
413         country = "GB" if country == "UK"
414         Net::HTTP.start('ws.geonames.org') do |http|
415           xml = REXML::Document.new(http.get("/countryInfo?country=#{country}").body)
416           xml.elements.each("geonames/country") do |ele|
417             minlon = ele.get_text("bBoxWest").to_s
418             minlat = ele.get_text("bBoxSouth").to_s
419             maxlon = ele.get_text("bBoxEast").to_s
420             maxlat = ele.get_text("bBoxNorth").to_s
421             return { :minlon => minlon, :minlat => minlat, :maxlon => maxlon, :maxlat => maxlat }
422           end
423         end
424       end
425     end
426
427     return nil
428   rescue Exception
429     return nil
430   end
431
432   # Construct a random token of a given length
433   def self.make_token(length = 30)
434     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
435     token = ''
436
437     length.times do
438       token += chars[(rand * chars.length).to_i].chr
439     end
440
441     return token
442   end
443
444   # Return an encrypted version of a password
445   def self.encrypt_password(password, salt)
446     return Digest::MD5.hexdigest(password) if salt.nil?
447     return Digest::MD5.hexdigest(salt + password)
448   end
449
450   # Return an SQL fragment to select a given area of the globe
451   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
452     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
453     minlat = (minlat * 10000000).round
454     minlon = (minlon * 10000000).round
455     maxlat = (maxlat * 10000000).round
456     maxlon = (maxlon * 10000000).round
457
458     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
459   end
460
461
462 end