]> git.openstreetmap.org Git - rails.git/blob - lib/osm.rb
Convert more changeset controller tests to use factories.
[rails.git] / lib / osm.rb
1 # The OSM module provides support functions for OSM.
2 module OSM
3   require "time"
4   require "rexml/parsers/sax2parser"
5   require "rexml/text"
6   require "xml/libxml"
7
8   if defined?(SystemTimer)
9     Timer = SystemTimer
10   else
11     require "timeout"
12     Timer = Timeout
13   end
14
15   # The base class for API Errors.
16   class APIError < RuntimeError
17     def status
18       :internal_server_error
19     end
20
21     def to_s
22       "Generic API Error"
23     end
24   end
25
26   # Raised when access is denied.
27   class APIAccessDenied < RuntimeError
28     def status
29       :forbidden
30     end
31
32     def to_s
33       "Access denied"
34     end
35   end
36
37   # Raised when an API object is not found.
38   class APINotFoundError < APIError
39     def status
40       :not_found
41     end
42
43     def to_s
44       "Object not found"
45     end
46   end
47
48   # Raised when a precondition to an API action fails sanity check.
49   class APIPreconditionFailedError < APIError
50     def initialize(message = "")
51       @message = message
52     end
53
54     def status
55       :precondition_failed
56     end
57
58     def to_s
59       "Precondition failed: #{@message}"
60     end
61   end
62
63   # Raised when to delete an already-deleted object.
64   class APIAlreadyDeletedError < APIError
65     def initialize(object = "object", object_id = "")
66       @object = object
67       @object_id = object_id
68     end
69
70     attr_reader :object, :object_id
71
72     def status
73       :gone
74     end
75
76     def to_s
77       "The #{object} with the id #{object_id} has already been deleted"
78     end
79   end
80
81   # Raised when the user logged in isn't the same as the changeset
82   class APIUserChangesetMismatchError < APIError
83     def status
84       :conflict
85     end
86
87     def to_s
88       "The user doesn't own that changeset"
89     end
90   end
91
92   # Raised when the changeset provided is already closed
93   class APIChangesetAlreadyClosedError < APIError
94     def initialize(changeset)
95       @changeset = changeset
96     end
97
98     attr_reader :changeset
99
100     def status
101       :conflict
102     end
103
104     def to_s
105       "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}"
106     end
107   end
108
109   # Raised when the changeset provided is not yet closed
110   class APIChangesetNotYetClosedError < APIError
111     def initialize(changeset)
112       @changeset = changeset
113     end
114
115     attr_reader :changeset
116
117     def status
118       :conflict
119     end
120
121     def to_s
122       "The changeset #{@changeset.id} is not yet closed."
123     end
124   end
125
126   # Raised when a user is already subscribed to the changeset
127   class APIChangesetAlreadySubscribedError < APIError
128     def initialize(changeset)
129       @changeset = changeset
130     end
131
132     attr_reader :changeset
133
134     def status
135       :conflict
136     end
137
138     def to_s
139       "You are already subscribed to changeset #{@changeset.id}."
140     end
141   end
142
143   # Raised when a user is not subscribed to the changeset
144   class APIChangesetNotSubscribedError < APIError
145     def initialize(changeset)
146       @changeset = changeset
147     end
148
149     attr_reader :changeset
150
151     def status
152       :not_found
153     end
154
155     def to_s
156       "You are not subscribed to changeset #{@changeset.id}."
157     end
158   end
159
160   # Raised when a change is expecting a changeset, but the changeset doesn't exist
161   class APIChangesetMissingError < APIError
162     def status
163       :conflict
164     end
165
166     def to_s
167       "You need to supply a changeset to be able to make a change"
168     end
169   end
170
171   # Raised when a diff is uploaded containing many changeset IDs which don't match
172   # the changeset ID that the diff was uploaded to.
173   class APIChangesetMismatchError < APIError
174     def initialize(provided, allowed)
175       @provided = provided
176       @allowed = allowed
177     end
178
179     def status
180       :conflict
181     end
182
183     def to_s
184       "Changeset mismatch: Provided #{@provided} but only #{@allowed} is allowed"
185     end
186   end
187
188   # Raised when a diff upload has an unknown action. You can only have create,
189   # modify, or delete
190   class APIChangesetActionInvalid < APIError
191     def initialize(provided)
192       @provided = provided
193     end
194
195     def status
196       :bad_request
197     end
198
199     def to_s
200       "Unknown action #{@provided}, choices are create, modify, delete"
201     end
202   end
203
204   # Raised when bad XML is encountered which stops things parsing as
205   # they should.
206   class APIBadXMLError < APIError
207     def initialize(model, xml, message = "")
208       @model = model
209       @xml = xml
210       @message = message
211     end
212
213     def status
214       :bad_request
215     end
216
217     def to_s
218       "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}"
219     end
220   end
221
222   # Raised when the provided version is not equal to the latest in the db.
223   class APIVersionMismatchError < APIError
224     def initialize(id, type, provided, latest)
225       @id = id
226       @type = type
227       @provided = provided
228       @latest = latest
229     end
230
231     attr_reader :provided, :latest, :id, :type
232
233     def status
234       :conflict
235     end
236
237     def to_s
238       "Version mismatch: Provided #{provided}, server had: #{latest} of #{type} #{id}"
239     end
240   end
241
242   # raised when a two tags have a duplicate key string in an element.
243   # this is now forbidden by the API.
244   class APIDuplicateTagsError < APIError
245     def initialize(type, id, tag_key)
246       @type = type
247       @id = id
248       @tag_key = tag_key
249     end
250
251     attr_reader :type, :id, :tag_key
252
253     def status
254       :bad_request
255     end
256
257     def to_s
258       "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}"
259     end
260   end
261
262   # Raised when a way has more than the configured number of way nodes.
263   # This prevents ways from being to long and difficult to work with
264   class APITooManyWayNodesError < APIError
265     def initialize(id, provided, max)
266       @id = id
267       @provided = provided
268       @max = max
269     end
270
271     attr_reader :id, :provided, :max
272
273     def status
274       :bad_request
275     end
276
277     def to_s
278       "You tried to add #{provided} nodes to way #{id}, however only #{max} are allowed"
279     end
280   end
281
282   ##
283   # raised when user input couldn't be parsed
284   class APIBadUserInput < APIError
285     def initialize(message)
286       @message = message
287     end
288
289     def status
290       :bad_request
291     end
292
293     def to_s
294       @message
295     end
296   end
297
298   ##
299   # raised when bounding box is invalid
300   class APIBadBoundingBox < APIError
301     def initialize(message)
302       @message = message
303     end
304
305     def status
306       :bad_request
307     end
308
309     def to_s
310       @message
311     end
312   end
313
314   ##
315   # raised when an API call is made using a method not supported on that URI
316   class APIBadMethodError < APIError
317     def initialize(supported_method)
318       @supported_method = supported_method
319     end
320
321     def status
322       :method_not_allowed
323     end
324
325     def to_s
326       "Only method #{@supported_method} is supported on this URI"
327     end
328   end
329
330   ##
331   # raised when an API call takes too long
332   class APITimeoutError < APIError
333     def status
334       :request_timeout
335     end
336
337     def to_s
338       "Request timed out"
339     end
340   end
341
342   ##
343   # raised when someone tries to redact a current version of
344   # an element - only historical versions can be redacted.
345   class APICannotRedactError < APIError
346     def status
347       :bad_request
348     end
349
350     def to_s
351       "Cannot redact current version of element, only historical versions may be redacted."
352     end
353   end
354
355   # Raised when the note provided is already closed
356   class APINoteAlreadyClosedError < APIError
357     def initialize(note)
358       @note = note
359     end
360
361     attr_reader :note
362
363     def status
364       :conflict
365     end
366
367     def to_s
368       "The note #{@note.id} was closed at #{@note.closed_at}"
369     end
370   end
371
372   # Raised when the note provided is already open
373   class APINoteAlreadyOpenError < APIError
374     def initialize(note)
375       @note = note
376     end
377
378     attr_reader :note
379
380     def status
381       :conflict
382     end
383
384     def to_s
385       "The note #{@note.id} is already open"
386     end
387   end
388
389   # raised when a two preferences have a duplicate key string.
390   class APIDuplicatePreferenceError < APIError
391     def initialize(key)
392       @key = key
393     end
394
395     attr_reader :key
396
397     def status
398       :bad_request
399     end
400
401     def to_s
402       "Duplicate preferences with key #{@key}"
403     end
404   end
405
406   # Helper methods for going to/from mercator and lat/lng.
407   class Mercator
408     include Math
409
410     # init me with your bounding box and the size of your image
411     def initialize(min_lat, min_lon, max_lat, max_lon, width, height)
412       xsize = xsheet(max_lon) - xsheet(min_lon)
413       ysize = ysheet(max_lat) - ysheet(min_lat)
414       xscale = xsize / width
415       yscale = ysize / height
416       scale = [xscale, yscale].max
417
418       xpad = width * scale - xsize
419       ypad = height * scale - ysize
420
421       @width = width
422       @height = height
423
424       @tx = xsheet(min_lon) - xpad / 2
425       @ty = ysheet(min_lat) - ypad / 2
426
427       @bx = xsheet(max_lon) + xpad / 2
428       @by = ysheet(max_lat) + ypad / 2
429     end
430
431     # the following two functions will give you the x/y on the entire sheet
432
433     def ysheet(lat)
434       log(tan(PI / 4 + (lat * PI / 180 / 2))) / (PI / 180)
435     end
436
437     def xsheet(lon)
438       lon
439     end
440
441     # and these two will give you the right points on your image. all the constants can be reduced to speed things up. FIXME
442
443     def y(lat)
444       @height - ((ysheet(lat) - @ty) / (@by - @ty) * @height)
445     end
446
447     def x(lon)
448       ((xsheet(lon) - @tx) / (@bx - @tx) * @width)
449     end
450   end
451
452   class GreatCircle
453     include Math
454
455     # initialise with a base position
456     def initialize(lat, lon)
457       @lat = lat * PI / 180
458       @lon = lon * PI / 180
459     end
460
461     # get the distance from the base position to a given position
462     def distance(lat, lon)
463       lat = lat * PI / 180
464       lon = lon * PI / 180
465       6372.795 * 2 * asin(sqrt(sin((lat - @lat) / 2)**2 + cos(@lat) * cos(lat) * sin((lon - @lon) / 2)**2))
466     end
467
468     # get the worst case bounds for a given radius from the base position
469     def bounds(radius)
470       latradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2)**2))
471
472       begin
473         lonradius = 2 * asin(sqrt(sin(radius / 6372.795 / 2)**2 / cos(@lat)**2))
474       rescue Errno::EDOM, Math::DomainError
475         lonradius = PI
476       end
477
478       minlat = (@lat - latradius) * 180 / PI
479       maxlat = (@lat + latradius) * 180 / PI
480       minlon = (@lon - lonradius) * 180 / PI
481       maxlon = (@lon + lonradius) * 180 / PI
482
483       { :minlat => minlat, :maxlat => maxlat, :minlon => minlon, :maxlon => maxlon }
484     end
485
486     # get the SQL to use to calculate distance
487     def sql_for_distance(lat_field, lon_field)
488       "6372.795 * 2 * asin(sqrt(power(sin((radians(#{lat_field}) - #{@lat}) / 2), 2) + cos(#{@lat}) * cos(radians(#{lat_field})) * power(sin((radians(#{lon_field}) - #{@lon})/2), 2)))"
489     end
490   end
491
492   class API
493     def get_xml_doc
494       doc = XML::Document.new
495       doc.encoding = XML::Encoding::UTF_8
496       root = XML::Node.new "osm"
497       root["version"] = API_VERSION.to_s
498       root["generator"] = GENERATOR
499       root["copyright"] = COPYRIGHT_OWNER
500       root["attribution"] = ATTRIBUTION_URL
501       root["license"] = LICENSE_URL
502       doc.root = root
503       doc
504     end
505   end
506
507   def self.ip_to_country(ip_address)
508     ipinfo = geoip_database.country(ip_address) if defined?(GEOIP_DATABASE)
509
510     if ipinfo
511       country = ipinfo.country_code2
512     else
513       country = http_client.get("http://api.hostip.info/country.php?ip=#{ip_address}").body
514       country = "GB" if country == "UK"
515     end
516
517     return country
518   rescue StandardError
519     return nil
520   end
521
522   def self.ip_location(ip_address)
523     code = OSM.ip_to_country(ip_address)
524
525     if code && country = Country.find(code)
526       return { :minlon => country.min_lon, :minlat => country.min_lat, :maxlon => country.max_lon, :maxlat => country.max_lat }
527     end
528
529     nil
530   end
531
532   # Parse a float, raising a specified exception on failure
533   def self.parse_float(str, klass, *args)
534     Float(str)
535   rescue
536     raise klass.new(*args)
537   end
538
539   # Construct a random token of a given length
540   def self.make_token(length = 30)
541     chars = "abcdefghijklmnopqrtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
542     token = ""
543
544     length.times do
545       token += chars[(rand * chars.length).to_i].chr
546     end
547
548     token
549   end
550
551   # Return an SQL fragment to select a given area of the globe
552   def self.sql_for_area(bbox, prefix = nil)
553     tilesql = QuadTile.sql_for_area(bbox, prefix)
554     bbox = bbox.to_scaled
555
556     "#{tilesql} AND #{prefix}latitude BETWEEN #{bbox.min_lat} AND #{bbox.max_lat} " +
557       "AND #{prefix}longitude BETWEEN #{bbox.min_lon} AND #{bbox.max_lon}"
558   end
559
560   # Return the terms and conditions text for a given country
561   def self.legal_text_for_country(country_code)
562     file_name = Rails.root.join("config", "legales", country_code.to_s + ".yml")
563     file_name = Rails.root.join("config", "legales", DEFAULT_LEGALE + ".yml") unless File.exist? file_name
564     YAML.load_file(file_name)
565   end
566
567   # Return the HTTP client to use
568   def self.http_client
569     @http_client ||= Faraday.new
570   end
571
572   # Return the GeoIP database handle
573   def self.geoip_database
574     @geoip_database ||= GeoIP.new(GEOIP_DATABASE) if defined?(GEOIP_DATABASE)
575   end
576 end