Make "nearby users" show all those within 50km rather than all those
[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 'RMagick'
16
17   class Mercator
18     include Math
19
20     def initialize(lat, lon, degrees_per_pixel, width, height)
21       #init me with your centre lat/lon, the number of degrees per pixel and the size of your image
22       @clat = lat
23       @clon = lon
24       @degrees_per_pixel = degrees_per_pixel
25       @degrees_per_pixel = 0.0000000001 if @degrees_per_pixel < 0.0000000001
26       @width = width
27       @height = height
28       @dlon = width / 2 * @degrees_per_pixel
29       @dlat = height / 2 * @degrees_per_pixel  * cos(@clat * PI / 180)
30
31       @tx = xsheet(@clon - @dlon)
32       @ty = ysheet(@clat - @dlat)
33
34       @bx = xsheet(@clon + @dlon)
35       @by = ysheet(@clat + @dlat)
36
37     end
38
39     #the following two functions will give you the x/y on the entire sheet
40
41     def kilometerinpixels
42       return 40008.0  / 360.0 * @degrees_per_pixel
43     end
44
45     def ysheet(lat)
46       log(tan(PI / 4 +  (lat  * PI / 180 / 2)))
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       @possible_points = 0
73       @actual_points = 0
74       @tracksegs = 0
75       @points = []
76
77       file = File.new(filename)
78       parser = REXML::Parsers::SAX2Parser.new( file )
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.listen( :start_element,  %w{ trkpt }) do |uri,localname,qname,attributes| 
89         lat = attributes['lat'].to_f
90         lon = attributes['lon'].to_f
91         gotlatlon = true
92         @possible_points += 1
93       end
94
95       parser.listen( :characters, %w{ ele } ) do |text|
96         ele = text
97         gotele = true
98       end
99
100       parser.listen( :characters, %w{ time } ) do |text|
101         if text && text != ''
102           date = DateTime.parse(text)
103           gotdate = true
104         end
105       end
106
107       parser.listen( :end_element, %w{ trkseg } ) do |uri, localname, qname|
108         @tracksegs += 1
109       end
110
111       parser.listen( :end_element, %w{ trkpt } ) do |uri,localname,qname|
112         if gotlatlon && gotdate
113           ele = '0' unless gotele
114           if lat < 90 && lat > -90 && lon > -180 && lon < 180
115             @actual_points += 1
116             @points.push(Hash['latitude' => lat,'longitude' => lon,'timestamp' => date,'altitude' => ele,'segment' => @tracksegs])
117           end
118         end
119         gotlatlon = false
120         gotele = false
121         gotdate = false
122       end
123       parser.parse
124     end
125
126     def points
127       @points.each { |p| yield p }
128     end
129
130     def get_picture(min_lat, min_lon, max_lat, max_lon, num_points)
131       #puts "getting picfor bbox #{min_lat},#{min_lon} - #{max_lat},#{max_lon}"
132       frames = 10
133       width = 250
134       height = 250
135       rat= Math.cos( ((max_lat + min_lat)/2.0) /  180.0 * 3.141592)
136       proj = OSM::Mercator.new((min_lat + max_lat) / 2, (max_lon + min_lon) / 2, (max_lat - min_lat) / width / rat, width, height)
137
138       images = []
139
140       frames.times do
141         gc =  Magick::Draw.new
142         gc.stroke_linejoin('miter')
143         gc.stroke('#FFFFFF')
144         gc.fill('#FFFFFF')
145         gc.rectangle(0,0,width,height)
146         gc.stroke_width(1)
147         images << gc
148       end
149
150       oldpx = 0.0
151       oldpy = 0.0
152
153       first = true
154
155       m = 0
156       mm = 0
157       points do |p|
158         px = proj.x(p['longitude'])
159         py = proj.y(p['latitude'])
160         frames.times do |n|
161           images[n].stroke_width(1)
162           images[n].stroke('#BBBBBB')
163           images[n].fill('#BBBBBB')
164         #  puts "A #{px},#{py} - #{oldpx},#{oldpy}"
165           images[n].line(px, py, oldpx, oldpy ) unless first
166         end
167         images[mm].stroke_width(3)
168         images[mm].stroke('#000000')
169         images[mm].fill('#000000')
170         images[mm].line(px, py, oldpx, oldpy ) unless first
171       #  puts "B #{px},#{py} - #{oldpx},#{oldpy}"
172         m +=1
173         if m > num_points.to_f / frames.to_f * (mm+1)
174           mm += 1
175         end
176         first = false
177         oldpy = py
178         oldpx = px
179       end
180
181       il = Magick::ImageList.new
182
183       frames.times do |n|
184         canvas = Magick::Image.new(width, height) {
185           self.background_color = 'white'
186         }
187         begin
188           images[n].draw(canvas)
189         rescue ArgumentError
190         end
191         canvas.format = 'GIF'
192         il << canvas
193       end
194
195       il.delay = 50
196       il.format = 'GIF'
197       return il.to_blob
198     end
199
200     def get_icon(min_lat, min_lon, max_lat, max_lon)
201       #puts "getting icon for bbox #{min_lat},#{min_lon} - #{max_lat},#{max_lon}"
202       width = 50
203       height = 50
204       rat= Math.cos( ((max_lat + min_lat)/2.0) /  180.0 * 3.141592)
205       proj = OSM::Mercator.new((min_lat + max_lat) / 2, (max_lon + min_lon) / 2, (max_lat - min_lat) / width / rat, width, height)
206
207       images = []
208
209       gc =  Magick::Draw.new
210       gc.stroke_linejoin('miter')
211
212       oldpx = 0.0
213       oldpy = 0.0
214
215       first = true
216
217       gc.stroke_width(1)
218       gc.stroke('#000000')
219       gc.fill('#000000')
220
221       points do |p|
222         px = proj.x(p['longitude'])
223         py = proj.y(p['latitude'])
224         gc.line(px, py, oldpx, oldpy ) unless first
225        # puts "C #{px},#{py} - #{oldpx},#{oldpy}"
226         first = false
227         oldpy = py
228         oldpx = px
229       end
230
231       canvas = Magick::Image.new(width, height) {
232         self.background_color = 'white'
233       }
234       begin
235         gc.draw(canvas)
236       rescue ArgumentError
237       end
238       canvas.format = 'GIF'
239       return canvas.to_blob
240     end
241
242   end
243
244   class GreatCircle
245     include Math
246
247     # initialise with a base position
248     def initialize(lat, lon)
249       @lat = lat * PI / 180
250       @lon = lon * PI / 180
251     end
252
253     # get the distance from the base position to a given position
254     def distance(lat, lon)
255       lat = lat * PI / 180
256       lon = lon * PI / 180
257       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
258     end
259
260     # get the worst case bounds for a given radius from the base position
261     def bounds(radius)
262       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
263       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
264       minlat = (@lat - latradius) * 180 / PI
265       maxlat = (@lat + latradius) * 180 / PI
266       minlon = (@lon - lonradius) * 180 / PI
267       maxlon = (@lon + lonradius) * 180 / PI
268       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
269     end
270   end
271
272   class GeoRSS
273     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
274       @doc = XML::Document.new
275       @doc.encoding = 'UTF-8' 
276       
277       rss = XML::Node.new 'rss'
278       @doc.root = rss
279       rss['version'] = "2.0"
280       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
281       @channel = XML::Node.new 'channel'
282       rss << @channel
283       title = XML::Node.new 'title'
284       title <<  feed_title
285       @channel << title
286       description_el = XML::Node.new 'description'
287       @channel << description_el
288
289       description_el << feed_description
290       link = XML::Node.new 'link'
291       link << feed_url
292       @channel << link
293       image = XML::Node.new 'image'
294       @channel << image
295       url = XML::Node.new 'url'
296       url << 'http://www.openstreetmap.org/feeds/mag_map-rss2.0.png'
297       image << url
298       title = XML::Node.new 'title'
299       title << "OpenStreetMap"
300       image << title
301       width = XML::Node.new 'width'
302       width << '100'
303       image << width
304       height = XML::Node.new 'height'
305       height << '100'
306       image << height
307       link = XML::Node.new 'link'
308       link << feed_url
309       image << link
310     end
311
312     def add(latitude=0, longitude=0, title_text='dummy title', url='http://www.example.com/', description_text='dummy description', timestamp=DateTime.now)
313       item = XML::Node.new 'item'
314
315       title = XML::Node.new 'title'
316       item << title
317       title << title_text
318       link = XML::Node.new 'link'
319       link << url
320       item << link
321
322       guid = XML::Node.new 'guid'
323       guid << url
324       item << guid
325
326       description = XML::Node.new 'description'
327       description << description_text
328       item << description
329
330       pubDate = XML::Node.new 'pubDate'
331       pubDate << timestamp.to_s(:rfc822)
332       item << pubDate
333
334       if latitude
335         lat_el = XML::Node.new 'geo:lat'
336         lat_el << latitude.to_s
337         item << lat_el
338       end
339
340       if longitude
341         lon_el = XML::Node.new 'geo:long'
342         lon_el << longitude.to_s
343         item << lon_el
344       end
345
346       @channel << item
347     end
348
349     def to_s
350       return @doc.to_s
351     end
352   end
353
354   class API
355     def get_xml_doc
356       doc = XML::Document.new
357       doc.encoding = 'UTF-8' 
358       root = XML::Node.new 'osm'
359       root['version'] = API_VERSION
360       root['generator'] = 'OpenStreetMap server'
361       doc.root = root
362       return doc
363     end
364   end
365 end