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