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