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