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