Rework image generation to work in a fixed amount of memory. Closes #580.
[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       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       rat= Math.cos( ((max_lat + min_lat)/2.0) /  180.0 * 3.141592)
215       proj = OSM::Mercator.new((min_lat + max_lat) / 2, (max_lon + min_lon) / 2, (max_lat - min_lat) / width / rat, width, height)
216
217       gc = Magick::Draw.new
218       gc.stroke_linejoin('miter')
219       gc.stroke_width(1)
220       gc.stroke('#000000')
221       gc.fill('#000000')
222
223       image = Magick::Image.new(width, height) do |image|
224         image.background_color = 'white'
225         image.format = 'GIF'
226       end
227
228       oldpx = 0.0
229       oldpy = 0.0
230
231       first = true
232
233       points do |p|
234         px = proj.x(p['longitude'])
235         py = proj.y(p['latitude'])
236
237         gc.dup.line(px, py, oldpx, oldpy).draw(image) unless first
238
239         first = false
240         oldpy = py
241         oldpx = px
242       end
243
244       return image.to_blob
245     end
246
247   end
248
249   class GreatCircle
250     include Math
251
252     # initialise with a base position
253     def initialize(lat, lon)
254       @lat = lat * PI / 180
255       @lon = lon * PI / 180
256     end
257
258     # get the distance from the base position to a given position
259     def distance(lat, lon)
260       lat = lat * PI / 180
261       lon = lon * PI / 180
262       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
263     end
264
265     # get the worst case bounds for a given radius from the base position
266     def bounds(radius)
267       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
268       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
269       minlat = (@lat - latradius) * 180 / PI
270       maxlat = (@lat + latradius) * 180 / PI
271       minlon = (@lon - lonradius) * 180 / PI
272       maxlon = (@lon + lonradius) * 180 / PI
273       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
274     end
275   end
276
277   class GeoRSS
278     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
279       @doc = XML::Document.new
280       @doc.encoding = 'UTF-8' 
281       
282       rss = XML::Node.new 'rss'
283       @doc.root = rss
284       rss['version'] = "2.0"
285       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
286       @channel = XML::Node.new 'channel'
287       rss << @channel
288       title = XML::Node.new 'title'
289       title <<  feed_title
290       @channel << title
291       description_el = XML::Node.new 'description'
292       @channel << description_el
293
294       description_el << feed_description
295       link = XML::Node.new 'link'
296       link << feed_url
297       @channel << link
298       image = XML::Node.new 'image'
299       @channel << image
300       url = XML::Node.new 'url'
301       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
302       image << url
303       title = XML::Node.new 'title'
304       title << "OpenStreetMap"
305       image << title
306       width = XML::Node.new 'width'
307       width << '100'
308       image << width
309       height = XML::Node.new 'height'
310       height << '100'
311       image << height
312       link = XML::Node.new 'link'
313       link << feed_url
314       image << link
315     end
316
317     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)
318       item = XML::Node.new 'item'
319
320       title = XML::Node.new 'title'
321       item << title
322       title << title_text
323       link = XML::Node.new 'link'
324       link << url
325       item << link
326
327       guid = XML::Node.new 'guid'
328       guid << url
329       item << guid
330
331       description = XML::Node.new 'description'
332       description << description_text
333       item << description
334
335       author = XML::Node.new 'author'
336       author << author_text
337       item << author
338
339       pubDate = XML::Node.new 'pubDate'
340       pubDate << timestamp.to_s(:rfc822)
341       item << pubDate
342
343       if latitude
344         lat_el = XML::Node.new 'geo:lat'
345         lat_el << latitude.to_s
346         item << lat_el
347       end
348
349       if longitude
350         lon_el = XML::Node.new 'geo:long'
351         lon_el << longitude.to_s
352         item << lon_el
353       end
354
355       @channel << item
356     end
357
358     def to_s
359       return @doc.to_s
360     end
361   end
362
363   class API
364     def get_xml_doc
365       doc = XML::Document.new
366       doc.encoding = 'UTF-8' 
367       root = XML::Node.new 'osm'
368       root['version'] = API_VERSION
369       root['generator'] = 'OpenStreetMap server'
370       doc.root = root
371       return doc
372     end
373   end
374
375   def self.IPLocation(ip_address)
376     Timeout::timeout(4) do
377       Net::HTTP.start('api.hostip.info') do |http|
378         country = http.get("/country.php?ip=#{ip_address}").body
379         country = "GB" if country == "UK"
380         Net::HTTP.start('ws.geonames.org') do |http|
381           xml = REXML::Document.new(http.get("/countryInfo?country=#{country}").body)
382           xml.elements.each("geonames/country") do |ele|
383             minlon = ele.get_text("bBoxWest").to_s
384             minlat = ele.get_text("bBoxSouth").to_s
385             maxlon = ele.get_text("bBoxEast").to_s
386             maxlat = ele.get_text("bBoxNorth").to_s
387             return { :minlon => minlon, :minlat => minlat, :maxlon => maxlon, :maxlat => maxlat }
388           end
389         end
390       end
391     end
392
393     return nil
394   rescue Exception
395     return nil
396   end
397
398   # Construct a random token of a given length
399   def self.make_token(length = 30)
400     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
401     token = ''
402
403     length.times do
404       token += chars[(rand * chars.length).to_i].chr
405     end
406
407     return token
408   end
409
410   # Return an encrypted version of a password
411   def self.encrypt_password(password, salt)
412     return Digest::MD5.hexdigest(password) if salt.nil?
413     return Digest::MD5.hexdigest(salt + password)
414   end
415
416   # Return an SQL fragment to select a given area of the globe
417   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
418     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
419     minlat = (minlat * 10000000).round
420     minlon = (minlon * 10000000).round
421     maxlat = (maxlat * 10000000).round
422     maxlon = (maxlon * 10000000).round
423
424     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
425   end
426 end