6e9346bb4f0076df8361c9a52b52348a8a426747
[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     #init me with your bounding box and the size of your image
22
23     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
24       xsize = xsheet(max_lon) - xsheet(min_lon)
25       ysize = ysheet(max_lat) - ysheet(min_lat)
26       xscale = xsize / width
27       yscale = ysize / height
28       scale = [xscale, yscale].max
29
30       xpad = width * scale - xsize
31       ypad = height * scale - ysize
32
33       @width = width
34       @height = height
35
36       @tx = xsheet(min_lon) - xpad / 2
37       @ty = ysheet(min_lat) - ypad / 2
38
39       @bx = xsheet(max_lon) + xpad / 2
40       @by = ysheet(max_lat) + ypad / 2
41     end
42
43     #the following two functions will give you the x/y on the entire sheet
44
45     def ysheet(lat)
46       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
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(file)
72       @file = file
73     end
74
75     def points
76       @possible_points = 0
77       @actual_points = 0
78       @tracksegs = 0
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       @file.rewind
89
90       parser = REXML::Parsers::SAX2Parser.new(@file)
91
92       parser.listen( :start_element,  %w{ trkpt }) do |uri,localname,qname,attributes| 
93         lat = attributes['lat'].to_f
94         lon = attributes['lon'].to_f
95         gotlatlon = true
96         gotele = false
97         gotdate = false
98         @possible_points += 1
99       end
100
101       parser.listen( :characters, %w{ ele } ) do |text|
102         ele = text
103         gotele = true
104       end
105
106       parser.listen( :characters, %w{ time } ) do |text|
107         if text && text != ''
108           begin
109             date = DateTime.parse(text)
110             gotdate = true
111           rescue
112           end
113         end
114       end
115
116       parser.listen( :end_element, %w{ trkseg } ) do |uri, localname, qname|
117         @tracksegs += 1
118       end
119
120       parser.listen( :end_element, %w{ trkpt } ) do |uri,localname,qname|
121         if gotlatlon && gotdate
122           ele = '0' unless gotele
123           if lat < 90 && lat > -90 && lon > -180 && lon < 180
124             @actual_points += 1
125             yield Hash['latitude' => lat, 'longitude' => lon, 'timestamp' => date, 'altitude' => ele, 'segment' => @tracksegs]
126           end
127         end
128         gotlatlon = false
129         gotele = false
130         gotdate = false
131       end
132
133       parser.parse
134     end
135
136     def get_picture(min_lat, min_lon, max_lat, max_lon, num_points)
137       #puts "getting picfor bbox #{min_lat},#{min_lon} - #{max_lat},#{max_lon}"
138       frames = 10
139       width = 250
140       height = 250
141       proj = OSM::Mercator.new(min_lat, min_lon, max_lat, max_lon, 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       proj = OSM::Mercator.new(min_lat, min_lon, max_lat, max_lon, width, height)
217
218       gc = Magick::Draw.new
219       gc.stroke_linejoin('miter')
220       gc.stroke_width(1)
221       gc.stroke('#000000')
222       gc.fill('#000000')
223
224       image = Magick::Image.new(width, height) do |image|
225         image.background_color = 'white'
226         image.format = 'GIF'
227       end
228
229       oldpx = 0.0
230       oldpy = 0.0
231
232       first = true
233
234       points do |p|
235         px = proj.x(p['longitude'])
236         py = proj.y(p['latitude'])
237
238         gc.dup.line(px, py, oldpx, oldpy).draw(image) unless first
239
240         first = false
241         oldpy = py
242         oldpx = px
243       end
244
245       return image.to_blob
246     end
247
248   end
249
250   class GreatCircle
251     include Math
252
253     # initialise with a base position
254     def initialize(lat, lon)
255       @lat = lat * PI / 180
256       @lon = lon * PI / 180
257     end
258
259     # get the distance from the base position to a given position
260     def distance(lat, lon)
261       lat = lat * PI / 180
262       lon = lon * PI / 180
263       return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
264     end
265
266     # get the worst case bounds for a given radius from the base position
267     def bounds(radius)
268       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
269       lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
270       minlat = (@lat - latradius) * 180 / PI
271       maxlat = (@lat + latradius) * 180 / PI
272       minlon = (@lon - lonradius) * 180 / PI
273       maxlon = (@lon + lonradius) * 180 / PI
274       return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
275     end
276   end
277
278   class GeoRSS
279     def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
280       @doc = XML::Document.new
281       @doc.encoding = 'UTF-8' 
282       
283       rss = XML::Node.new 'rss'
284       @doc.root = rss
285       rss['version'] = "2.0"
286       rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
287       @channel = XML::Node.new 'channel'
288       rss << @channel
289       title = XML::Node.new 'title'
290       title <<  feed_title
291       @channel << title
292       description_el = XML::Node.new 'description'
293       @channel << description_el
294
295       description_el << feed_description
296       link = XML::Node.new 'link'
297       link << feed_url
298       @channel << link
299       image = XML::Node.new 'image'
300       @channel << image
301       url = XML::Node.new 'url'
302       url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
303       image << url
304       title = XML::Node.new 'title'
305       title << "OpenStreetMap"
306       image << title
307       width = XML::Node.new 'width'
308       width << '100'
309       image << width
310       height = XML::Node.new 'height'
311       height << '100'
312       image << height
313       link = XML::Node.new 'link'
314       link << feed_url
315       image << link
316     end
317
318     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)
319       item = XML::Node.new 'item'
320
321       title = XML::Node.new 'title'
322       item << title
323       title << title_text
324       link = XML::Node.new 'link'
325       link << url
326       item << link
327
328       guid = XML::Node.new 'guid'
329       guid << url
330       item << guid
331
332       description = XML::Node.new 'description'
333       description << description_text
334       item << description
335
336       author = XML::Node.new 'author'
337       author << author_text
338       item << author
339
340       pubDate = XML::Node.new 'pubDate'
341       pubDate << timestamp.to_s(:rfc822)
342       item << pubDate
343
344       if latitude
345         lat_el = XML::Node.new 'geo:lat'
346         lat_el << latitude.to_s
347         item << lat_el
348       end
349
350       if longitude
351         lon_el = XML::Node.new 'geo:long'
352         lon_el << longitude.to_s
353         item << lon_el
354       end
355
356       @channel << item
357     end
358
359     def to_s
360       return @doc.to_s
361     end
362   end
363
364   class API
365     def get_xml_doc
366       doc = XML::Document.new
367       doc.encoding = 'UTF-8' 
368       root = XML::Node.new 'osm'
369       root['version'] = API_VERSION
370       root['generator'] = 'OpenStreetMap server'
371       doc.root = root
372       return doc
373     end
374   end
375
376   def self.IPLocation(ip_address)
377     Timeout::timeout(4) do
378       Net::HTTP.start('api.hostip.info') do |http|
379         country = http.get("/country.php?ip=#{ip_address}").body
380         country = "GB" if country == "UK"
381         Net::HTTP.start('ws.geonames.org') do |http|
382           xml = REXML::Document.new(http.get("/countryInfo?country=#{country}").body)
383           xml.elements.each("geonames/country") do |ele|
384             minlon = ele.get_text("bBoxWest").to_s
385             minlat = ele.get_text("bBoxSouth").to_s
386             maxlon = ele.get_text("bBoxEast").to_s
387             maxlat = ele.get_text("bBoxNorth").to_s
388             return { :minlon => minlon, :minlat => minlat, :maxlon => maxlon, :maxlat => maxlat }
389           end
390         end
391       end
392     end
393
394     return nil
395   rescue Exception
396     return nil
397   end
398
399   # Construct a random token of a given length
400   def self.make_token(length = 30)
401     chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
402     token = ''
403
404     length.times do
405       token += chars[(rand * chars.length).to_i].chr
406     end
407
408     return token
409   end
410
411   # Return an encrypted version of a password
412   def self.encrypt_password(password, salt)
413     return Digest::MD5.hexdigest(password) if salt.nil?
414     return Digest::MD5.hexdigest(salt + password)
415   end
416
417   # Return an SQL fragment to select a given area of the globe
418   def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
419     tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
420     minlat = (minlat * 10000000).round
421     minlon = (minlon * 10000000).round
422     maxlat = (maxlat * 10000000).round
423     maxlon = (maxlon * 10000000).round
424
425     return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
426   end
427
428   class Potlatch # crazy shit
429
430     # ----- getpresets
431     #             in:   none
432     #             does: reads tag preset menus, colours, and autocomplete config files
433     #         out:  [0] presets, [1] presetmenus, [2] presetnames,
434     #                           [3] colours, [4] casing, [5] areas, [6] autotags
435     #                           (all hashes)
436     def self.get_presets
437       RAILS_DEFAULT_LOGGER.info("  Message: getpresets")
438
439       # Read preset menus
440       presets={}
441       presetmenus={}; presetmenus['point']=[]; presetmenus['way']=[]; presetmenus['POI']=[]
442       presetnames={}; presetnames['point']={}; presetnames['way']={}; presetnames['POI']={}
443       presettype=''
444       presetcategory=''
445       # StringIO.open(txt) do |file|
446       File.open("#{RAILS_ROOT}/config/potlatch/presets.txt") do |file|
447         file.each_line {|line|
448           t=line.chomp
449           if (t=~/(\w+)\/(\w+)/) then
450             presettype=$1
451             presetcategory=$2
452             presetmenus[presettype].push(presetcategory)
453             presetnames[presettype][presetcategory]=["(no preset)"]
454           elsif (t=~/^(.+):\s?(.+)$/) then
455             pre=$1; kv=$2
456             presetnames[presettype][presetcategory].push(pre)
457             presets[pre]={}
458             kv.split(',').each {|a|
459               if (a=~/^(.+)=(.*)$/) then presets[pre][$1]=$2 end
460             }
461           end
462         }
463       end
464
465       # Read colours/styling
466       colours={}; casing={}; areas={}
467       File.open("#{RAILS_ROOT}/config/potlatch/colours.txt") do |file|
468         file.each_line {|line|
469           t=line.chomp
470           if (t=~/(\w+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/) then
471             tag=$1
472             if ($2!='-') then colours[tag]=$2.hex end
473             if ($3!='-') then casing[tag]=$3.hex end
474             if ($4!='-') then areas[tag]=$4.hex end
475           end
476         }
477       end
478
479       # Read auto-complete
480       autotags={}; autotags['point']={}; autotags['way']={}; autotags['POI']={};
481       File.open("#{RAILS_ROOT}/config/potlatch/autocomplete.txt") do |file|
482         file.each_line {|line|
483           t=line.chomp
484           if (t=~/^(\w+)\/(\w+)\s+(.+)$/) then
485             tag=$1; type=$2; values=$3
486             if values=='-' then autotags[type][tag]=[]
487             else autotags[type][tag]=values.split(',').sort.reverse end
488           end
489         }
490       end
491
492       [presets,presetmenus,presetnames,colours,casing,areas,autotags]
493     end
494   end
495
496 end