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