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