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

Last change on this file since 14733 was 14733, checked in by richard, 11 years ago

pass common API error text through to Potlatch so it can be shown to the user

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