Cope with the degenerate case of a GPX file with only a single point.
[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 GeoRSS
245     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
246       @doc = XML::Document.new
247       @doc.encoding = 'UTF-8' 
248       
249       rss = XML::Node.new 'rss'
250       @doc.root = rss
251       rss['version'] = "2.0"
252       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
253       @channel = XML::Node.new 'channel'
254       rss << @channel
255       title = XML::Node.new 'title'
256       title <<  feed_title
257       @channel << title
258       description_el = XML::Node.new 'description'
259       @channel << description_el
260
261       description_el << feed_description
262       link = XML::Node.new 'link'
263       link << feed_url
264       @channel << link
265       image = XML::Node.new 'image'
266       @channel << image
267       url = XML::Node.new 'url'
268       url << 'http://www.openstreetmap.org/feeds/mag_map-rss2.0.png'
269       image << url
270       title = XML::Node.new 'title'
271       title << "OpenStreetMap"
272       image << title
273       width = XML::Node.new 'width'
274       width << '100'
275       image << width
276       height = XML::Node.new 'height'
277       height << '100'
278       image << height
279       link = XML::Node.new 'link'
280       link << feed_url
281       image << link
282     end
283
284     def add(latitude=0, longitude=0, title_text='dummy title', url='http://www.example.com/', description_text='dummy description', timestamp=DateTime.now)
285       item = XML::Node.new 'item'
286
287       title = XML::Node.new 'title'
288       item << title
289       title << title_text
290       link = XML::Node.new 'link'
291       link << url
292       item << link
293
294       guid = XML::Node.new 'guid'
295       guid << url
296       item << guid
297
298       description = XML::Node.new 'description'
299       description << description_text
300       item << description
301
302       pubDate = XML::Node.new 'pubDate'
303       pubDate << timestamp.to_s(:rfc822)
304       item << pubDate
305
306       if latitude
307         lat_el = XML::Node.new 'geo:lat'
308         lat_el << latitude.to_s
309         item << lat_el
310       end
311
312       if longitude
313         lon_el = XML::Node.new 'geo:long'
314         lon_el << longitude.to_s
315         item << lon_el
316       end
317
318       @channel << item
319     end
320
321     def to_s
322       return @doc.to_s
323     end
324   end
325
326   class API
327     def get_xml_doc
328       doc = XML::Document.new
329       doc.encoding = 'UTF-8' 
330       root = XML::Node.new 'osm'
331       root['version'] = API_VERSION
332       root['generator'] = 'OpenStreetMap server'
333       doc.root = root
334       return doc
335     end
336   end
337 end