source: subversion/sites/rails_port/lib/osm.rb @ 14586

Last change on this file since 14586 was 14586, checked in by tomhughes, 11 years ago

Merge api06 branch to trunk.

File size: 11.5 KB
Line 
1# The OSM module provides support functions for OSM.
2module 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    def render_opts
14      { :text => "Generic API Error", :status => :internal_server_error, :content_type => "text/plain" }
15    end
16  end
17
18  # Raised when an API object is not found.
19  class APINotFoundError < APIError
20    def render_opts
21      { :text => "The API wasn't found", :status => :not_found, :content_type => "text/plain" }
22    end
23  end
24
25  # Raised when a precondition to an API action fails sanity check.
26  class APIPreconditionFailedError < APIError
27    def initialize(message = "")
28      @message = message
29    end
30   
31    def render_opts
32      { :text => "Precondition failed: #{@message}", :status => :precondition_failed, :content_type => "text/plain" }
33    end
34  end
35
36  # Raised when to delete an already-deleted object.
37  class APIAlreadyDeletedError < APIError
38    def render_opts
39      { :text => "The object has already been deleted", :status => :gone, :content_type => "text/plain" }
40    end
41  end
42
43  # Raised when the user logged in isn't the same as the changeset
44  class APIUserChangesetMismatchError < APIError
45    def render_opts
46      { :text => "The user doesn't own that changeset", :status => :conflict, :content_type => "text/plain" }
47    end
48  end
49
50  # Raised when the changeset provided is already closed
51  class APIChangesetAlreadyClosedError < APIError
52    def initialize(changeset)
53      @changeset = changeset
54    end
55
56    attr_reader :changeset
57   
58    def render_opts
59      { :text => "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}.", :status => :conflict, :content_type => "text/plain" }
60    end
61  end
62 
63  # Raised when a change is expecting a changeset, but the changeset doesn't exist
64  class APIChangesetMissingError < APIError
65    def render_opts
66      { :text => "You need to supply a changeset to be able to make a change", :status => :conflict, :content_type => "text/plain" }
67    end
68  end
69
70  # Raised when a diff is uploaded containing many changeset IDs which don't match
71  # the changeset ID that the diff was uploaded to.
72  class APIChangesetMismatchError < APIError
73    def initialize(provided, allowed)
74      @provided, @allowed = provided, allowed
75    end
76   
77    def render_opts
78      { :text => "Changeset mismatch: Provided #{@provided} but only " +
79      "#{@allowed} is allowed.", :status => :conflict, :content_type => "text/plain" }
80    end
81  end
82 
83  # Raised when a diff upload has an unknown action. You can only have create,
84  # modify, or delete
85  class APIChangesetActionInvalid < APIError
86    def initialize(provided)
87      @provided = provided
88    end
89   
90    def render_opts
91      { :text => "Unknown action #{@provided}, choices are create, modify, delete.",
92      :status => :bad_request, :content_type => "text/plain" }
93    end
94  end
95
96  # Raised when bad XML is encountered which stops things parsing as
97  # they should.
98  class APIBadXMLError < APIError
99    def initialize(model, xml, message="")
100      @model, @xml, @message = model, xml, message
101    end
102
103    def render_opts
104      { :text => "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}",
105      :status => :bad_request, :content_type => "text/plain" }
106    end
107  end
108
109  # Raised when the provided version is not equal to the latest in the db.
110  class APIVersionMismatchError < APIError
111    def initialize(id, type, provided, latest)
112      @id, @type, @provided, @latest = id, type, provided, latest
113    end
114
115    attr_reader :provided, :latest, :id, :type
116
117    def render_opts
118      { :text => "Version mismatch: Provided " + provided.to_s +
119        ", server had: " + latest.to_s + " of " + type + " " + id.to_s, 
120        :status => :conflict, :content_type => "text/plain" }
121    end
122  end
123
124  # raised when a two tags have a duplicate key string in an element.
125  # this is now forbidden by the API.
126  class APIDuplicateTagsError < APIError
127    def initialize(type, id, tag_key)
128      @type, @id, @tag_key = type, id, tag_key
129    end
130
131    attr_reader :type, :id, :tag_key
132
133    def render_opts
134      { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.",
135        :status => :bad_request, :content_type => "text/plain" }
136    end
137  end
138 
139  # Raised when a way has more than the configured number of way nodes.
140  # This prevents ways from being to long and difficult to work with
141  class APITooManyWayNodesError < APIError
142    def initialize(provided, max)
143      @provided, @max = provided, max
144    end
145   
146    attr_reader :provided, :max
147   
148    def render_opts
149      { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed",
150        :status => :bad_request, :content_type => "text/plain" }
151    end
152  end
153
154  ##
155  # raised when user input couldn't be parsed
156  class APIBadUserInput < APIError
157    def initialize(message)
158      @message = message
159    end
160
161    def render_opts
162      { :text => @message, :content_type => "text/plain", :status => :bad_request }
163    end
164  end
165
166  # Helper methods for going to/from mercator and lat/lng.
167  class Mercator
168    include Math
169
170    #init me with your bounding box and the size of your image
171    def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
172      xsize = xsheet(max_lon) - xsheet(min_lon)
173      ysize = ysheet(max_lat) - ysheet(min_lat)
174      xscale = xsize / width
175      yscale = ysize / height
176      scale = [xscale, yscale].max
177
178      xpad = width * scale - xsize
179      ypad = height * scale - ysize
180
181      @width = width
182      @height = height
183
184      @tx = xsheet(min_lon) - xpad / 2
185      @ty = ysheet(min_lat) - ypad / 2
186
187      @bx = xsheet(max_lon) + xpad / 2
188      @by = ysheet(max_lat) + ypad / 2
189    end
190
191    #the following two functions will give you the x/y on the entire sheet
192
193    def ysheet(lat)
194      log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
195    end
196
197    def xsheet(lon)
198      lon
199    end
200
201    #and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
202
203    def y(lat)
204      return @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
205    end
206
207    def x(lon)
208      return  ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
209    end
210  end
211
212  class GreatCircle
213    include Math
214
215    # initialise with a base position
216    def initialize(lat, lon)
217      @lat = lat * PI / 180
218      @lon = lon * PI / 180
219    end
220
221    # get the distance from the base position to a given position
222    def distance(lat, lon)
223      lat = lat * PI / 180
224      lon = lon * PI / 180
225      return 6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2) ** 2 + cos(@lat) * cos(lat) * sin((lon - @lon)/2) ** 2))
226    end
227
228    # get the worst case bounds for a given radius from the base position
229    def bounds(radius)
230      latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2))
231      lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2) ** 2 / cos(@lat) ** 2))
232      minlat = (@lat - latradius) * 180 / PI
233      maxlat = (@lat + latradius) * 180 / PI
234      minlon = (@lon - lonradius) * 180 / PI
235      maxlon = (@lon + lonradius) * 180 / PI
236      return { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
237    end
238  end
239
240  class GeoRSS
241    def initialize(feed_title='OpenStreetMap GPS Traces', feed_description='OpenStreetMap GPS Traces', feed_url='http://www.openstreetmap.org/traces/')
242      @doc = XML::Document.new
243      @doc.encoding = XML::Encoding::UTF_8
244
245      rss = XML::Node.new 'rss'
246      @doc.root = rss
247      rss['version'] = "2.0"
248      rss['xmlns:geo'] = "http://www.w3.org/2003/01/geo/wgs84_pos#"
249      @channel = XML::Node.new 'channel'
250      rss << @channel
251      title = XML::Node.new 'title'
252      title <<  feed_title
253      @channel << title
254      description_el = XML::Node.new 'description'
255      @channel << description_el
256
257      description_el << feed_description
258      link = XML::Node.new 'link'
259      link << feed_url
260      @channel << link
261      image = XML::Node.new 'image'
262      @channel << image
263      url = XML::Node.new 'url'
264      url << 'http://www.openstreetmap.org/images/mag_map-rss2.0.png'
265      image << url
266      title = XML::Node.new 'title'
267      title << "OpenStreetMap"
268      image << title
269      width = XML::Node.new 'width'
270      width << '100'
271      image << width
272      height = XML::Node.new 'height'
273      height << '100'
274      image << height
275      link = XML::Node.new 'link'
276      link << feed_url
277      image << link
278    end
279
280    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)
281      item = XML::Node.new 'item'
282
283      title = XML::Node.new 'title'
284      item << title
285      title << title_text
286      link = XML::Node.new 'link'
287      link << url
288      item << link
289
290      guid = XML::Node.new 'guid'
291      guid << url
292      item << guid
293
294      description = XML::Node.new 'description'
295      description << description_text
296      item << description
297
298      author = XML::Node.new 'author'
299      author << author_text
300      item << author
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 = XML::Encoding::UTF_8
330      root = XML::Node.new 'osm'
331      root['version'] = API_VERSION
332      root['generator'] = GENERATOR
333      doc.root = root
334      return doc
335    end
336  end
337
338  def self.IPLocation(ip_address)
339    Timeout::timeout(4) do
340      Net::HTTP.start('api.hostip.info') do |http|
341        country = http.get("/country.php?ip=#{ip_address}").body
342        country = "GB" if country == "UK"
343        Net::HTTP.start('ws.geonames.org') do |http|
344          xml = REXML::Document.new(http.get("/countryInfo?country=#{country}").body)
345          xml.elements.each("geonames/country") do |ele|
346            minlon = ele.get_text("bBoxWest").to_s
347            minlat = ele.get_text("bBoxSouth").to_s
348            maxlon = ele.get_text("bBoxEast").to_s
349            maxlat = ele.get_text("bBoxNorth").to_s
350            return { :minlon => minlon, :minlat => minlat, :maxlon => maxlon, :maxlat => maxlat }
351          end
352        end
353      end
354    end
355
356    return nil
357  rescue Exception
358    return nil
359  end
360
361  # Construct a random token of a given length
362  def self.make_token(length = 30)
363    chars = 'abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
364    token = ''
365
366    length.times do
367      token += chars[(rand * chars.length).to_i].chr
368    end
369
370    return token
371  end
372
373  # Return an encrypted version of a password
374  def self.encrypt_password(password, salt)
375    return Digest::MD5.hexdigest(password) if salt.nil?
376    return Digest::MD5.hexdigest(salt + password)
377  end
378
379  # Return an SQL fragment to select a given area of the globe
380  def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil)
381    tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix)
382    minlat = (minlat * 10000000).round
383    minlon = (minlon * 10000000).round
384    maxlat = (maxlat * 10000000).round
385    maxlon = (maxlon * 10000000).round
386
387    return "#{tilesql} AND #{prefix}latitude BETWEEN #{minlat} AND #{maxlat} AND #{prefix}longitude BETWEEN #{minlon} AND #{maxlon}"
388  end
389
390
391end
Note: See TracBrowser for help on using the repository browser.