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