]> git.openstreetmap.org Git - rails.git/blob - app/models/trace.rb
Merge remote-tracking branch 'upstream/pull/2741'
[rails.git] / app / models / trace.rb
1 # == Schema Information
2 #
3 # Table name: gpx_files
4 #
5 #  id          :bigint(8)        not null, primary key
6 #  user_id     :bigint(8)        not null
7 #  visible     :boolean          default(TRUE), not null
8 #  name        :string           default(""), not null
9 #  size        :bigint(8)
10 #  latitude    :float
11 #  longitude   :float
12 #  timestamp   :datetime         not null
13 #  description :string           default(""), not null
14 #  inserted    :boolean          not null
15 #  visibility  :enum             default("public"), not null
16 #
17 # Indexes
18 #
19 #  gpx_files_timestamp_idx           (timestamp)
20 #  gpx_files_user_id_idx             (user_id)
21 #  gpx_files_visible_visibility_idx  (visible,visibility)
22 #
23 # Foreign Keys
24 #
25 #  gpx_files_user_id_fkey  (user_id => users.id)
26 #
27
28 class Trace < ApplicationRecord
29   require "open3"
30
31   self.table_name = "gpx_files"
32
33   belongs_to :user, :counter_cache => true
34   has_many :tags, :class_name => "Tracetag", :foreign_key => "gpx_id", :dependent => :delete_all
35   has_many :points, :class_name => "Tracepoint", :foreign_key => "gpx_id", :dependent => :delete_all
36
37   scope :visible, -> { where(:visible => true) }
38   scope :visible_to, ->(u) { visible.where("visibility IN ('public', 'identifiable') OR user_id = ?", u) }
39   scope :visible_to_all, -> { where(:visibility => %w[public identifiable]) }
40   scope :tagged, ->(t) { joins(:tags).where(:gpx_file_tags => { :tag => t }) }
41
42   validates :user, :presence => true, :associated => true
43   validates :name, :presence => true, :length => 1..255, :characters => true
44   validates :description, :presence => { :on => :create }, :length => 1..255, :characters => true
45   validates :timestamp, :presence => true
46   validates :visibility, :inclusion => %w[private public trackable identifiable]
47
48   after_destroy :remove_files
49
50   def tagstring
51     tags.collect(&:tag).join(", ")
52   end
53
54   def tagstring=(s)
55     self.tags = if s.include? ","
56                   s.split(/\s*,\s*/).reject { |tag| tag =~ /^\s*$/ }.collect do |tag|
57                     tt = Tracetag.new
58                     tt.tag = tag
59                     tt
60                   end
61                 else
62                   # do as before for backwards compatibility:
63                   s.split.collect do |tag|
64                     tt = Tracetag.new
65                     tt.tag = tag
66                     tt
67                   end
68                 end
69   end
70
71   def public?
72     visibility == "public" || visibility == "identifiable"
73   end
74
75   def trackable?
76     visibility == "trackable" || visibility == "identifiable"
77   end
78
79   def identifiable?
80     visibility == "identifiable"
81   end
82
83   def large_picture=(data)
84     f = File.new(large_picture_name, "wb")
85     f.syswrite(data)
86     f.close
87   end
88
89   def icon_picture=(data)
90     f = File.new(icon_picture_name, "wb")
91     f.syswrite(data)
92     f.close
93   end
94
95   def large_picture
96     f = File.new(large_picture_name, "rb")
97     data = f.sysread(File.size(f.path))
98     f.close
99     data
100   end
101
102   def icon_picture
103     f = File.new(icon_picture_name, "rb")
104     data = f.sysread(File.size(f.path))
105     f.close
106     data
107   end
108
109   def large_picture_name
110     "#{Settings.gpx_image_dir}/#{id}.gif"
111   end
112
113   def icon_picture_name
114     "#{Settings.gpx_image_dir}/#{id}_icon.gif"
115   end
116
117   def trace_name
118     "#{Settings.gpx_trace_dir}/#{id}.gpx"
119   end
120
121   def mime_type
122     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
123     gzipped = filetype.include?("gzip compressed")
124     bzipped = filetype.include?("bzip2 compressed")
125     zipped = filetype.include?("Zip archive")
126     tarred = filetype.include?("tar archive")
127
128     mimetype = if gzipped
129                  "application/x-gzip"
130                elsif bzipped
131                  "application/x-bzip2"
132                elsif zipped
133                  "application/x-zip"
134                elsif tarred
135                  "application/x-tar"
136                else
137                  "application/gpx+xml"
138                end
139
140     mimetype
141   end
142
143   def extension_name
144     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
145     gzipped = filetype.include?("gzip compressed")
146     bzipped = filetype.include?("bzip2 compressed")
147     zipped = filetype.include?("Zip archive")
148     tarred = filetype.include?("tar archive")
149
150     extension = if tarred && gzipped
151                   ".tar.gz"
152                 elsif tarred && bzipped
153                   ".tar.bz2"
154                 elsif tarred
155                   ".tar"
156                 elsif gzipped
157                   ".gpx.gz"
158                 elsif bzipped
159                   ".gpx.bz2"
160                 elsif zipped
161                   ".zip"
162                 else
163                   ".gpx"
164                 end
165
166     extension
167   end
168
169   def update_from_xml(xml, create = false)
170     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
171     doc = p.parse
172
173     doc.find("//osm/gpx_file").each do |pt|
174       return update_from_xml_node(pt, create)
175     end
176
177     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
178   rescue LibXML::XML::Error, ArgumentError => e
179     raise OSM::APIBadXMLError.new("trace", xml, e.message)
180   end
181
182   def update_from_xml_node(pt, create = false)
183     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
184
185     self.visibility = pt["visibility"]
186
187     unless create
188       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
189
190       id = pt["id"].to_i
191       # .to_i will return 0 if there is no number that can be parsed.
192       # We want to make sure that there is no id with zero anyway
193       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
194       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
195     end
196
197     # We don't care about the time, as it is explicitly set on create/update/delete
198     # We don't care about the visibility as it is implicit based on the action
199     # and set manually before the actual delete
200     self.visible = true
201
202     description = pt.find("description").first
203     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
204
205     self.description = description.content
206
207     self.tags = pt.find("tag").collect do |tag|
208       Tracetag.new(:tag => tag.content)
209     end
210   end
211
212   def xml_file
213     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
214     gzipped = filetype.include?("gzip compressed")
215     bzipped = filetype.include?("bzip2 compressed")
216     zipped = filetype.include?("Zip archive")
217     tarred = filetype.include?("tar archive")
218
219     if gzipped || bzipped || zipped || tarred
220       file = Tempfile.new("trace.#{id}")
221
222       if tarred && gzipped
223         system("tar", "-zxOf", trace_name, :out => file.path)
224       elsif tarred && bzipped
225         system("tar", "-jxOf", trace_name, :out => file.path)
226       elsif tarred
227         system("tar", "-xOf", trace_name, :out => file.path)
228       elsif gzipped
229         system("gunzip", "-c", trace_name, :out => file.path)
230       elsif bzipped
231         system("bunzip2", "-c", trace_name, :out => file.path)
232       elsif zipped
233         system("unzip", "-p", trace_name, "-x", "__MACOSX/*", :out => file.path, :err => "/dev/null")
234       end
235
236       file.unlink
237     else
238       file = File.open(trace_name)
239     end
240
241     file
242   end
243
244   def import
245     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
246
247     gpx = GPX::File.new(trace_name)
248
249     f_lat = 0
250     f_lon = 0
251     first = true
252
253     # If there are any existing points for this trace then delete them
254     Tracepoint.where(:gpx_id => id).delete_all
255
256     gpx.points.each_slice(1_000) do |points|
257       # Gather the trace points together for a bulk import
258       tracepoints = []
259
260       points.each do |point|
261         if first
262           f_lat = point.latitude
263           f_lon = point.longitude
264           first = false
265         end
266
267         tp = Tracepoint.new
268         tp.lat = point.latitude
269         tp.lon = point.longitude
270         tp.altitude = point.altitude
271         tp.timestamp = point.timestamp
272         tp.gpx_id = id
273         tp.trackid = point.segment
274         tracepoints << tp
275       end
276
277       # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
278       tracepoints.each do |tp|
279         tp.run_callbacks(:save) { false }
280         tp.run_callbacks(:create) { false }
281       end
282
283       Tracepoint.import!(tracepoints)
284     end
285
286     if gpx.actual_points.positive?
287       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
288       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
289       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
290       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
291
292       max_lat = max_lat.to_f / 10000000
293       min_lat = min_lat.to_f / 10000000
294       max_lon = max_lon.to_f / 10000000
295       min_lon = min_lon.to_f / 10000000
296
297       self.latitude = f_lat
298       self.longitude = f_lon
299       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
300       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
301       self.size = gpx.actual_points
302       self.inserted = true
303       save!
304     end
305
306     logger.info "done trace #{id}"
307
308     gpx
309   end
310
311   private
312
313   def remove_files
314     FileUtils.rm_f(trace_name)
315     FileUtils.rm_f(icon_picture_name)
316     FileUtils.rm_f(large_picture_name)
317   end
318 end