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