Update bundle
[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     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   end
140
141   def extension_name
142     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
143     gzipped = filetype.include?("gzip compressed")
144     bzipped = filetype.include?("bzip2 compressed")
145     zipped = filetype.include?("Zip archive")
146     tarred = filetype.include?("tar archive")
147
148     if tarred && gzipped
149       ".tar.gz"
150     elsif tarred && bzipped
151       ".tar.bz2"
152     elsif tarred
153       ".tar"
154     elsif gzipped
155       ".gpx.gz"
156     elsif bzipped
157       ".gpx.bz2"
158     elsif zipped
159       ".zip"
160     else
161       ".gpx"
162     end
163   end
164
165   def update_from_xml(xml, create: false)
166     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
167     doc = p.parse
168     pt = doc.find_first("//osm/gpx_file")
169
170     if pt
171       update_from_xml_node(pt, :create => create)
172     else
173       raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
174     end
175   rescue LibXML::XML::Error, ArgumentError => e
176     raise OSM::APIBadXMLError.new("trace", xml, e.message)
177   end
178
179   def update_from_xml_node(pt, create: false)
180     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
181
182     self.visibility = pt["visibility"]
183
184     unless create
185       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
186
187       id = pt["id"].to_i
188       # .to_i will return 0 if there is no number that can be parsed.
189       # We want to make sure that there is no id with zero anyway
190       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
191       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
192     end
193
194     # We don't care about the time, as it is explicitly set on create/update/delete
195     # We don't care about the visibility as it is implicit based on the action
196     # and set manually before the actual delete
197     self.visible = true
198
199     description = pt.find("description").first
200     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
201
202     self.description = description.content
203
204     self.tags = pt.find("tag").collect do |tag|
205       Tracetag.new(:tag => tag.content)
206     end
207   end
208
209   def xml_file
210     filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp
211     gzipped = filetype.include?("gzip compressed")
212     bzipped = filetype.include?("bzip2 compressed")
213     zipped = filetype.include?("Zip archive")
214     tarred = filetype.include?("tar archive")
215
216     if gzipped || bzipped || zipped || tarred
217       file = Tempfile.new("trace.#{id}")
218
219       if tarred && gzipped
220         system("tar", "-zxOf", trace_name, :out => file.path)
221       elsif tarred && bzipped
222         system("tar", "-jxOf", trace_name, :out => file.path)
223       elsif tarred
224         system("tar", "-xOf", trace_name, :out => file.path)
225       elsif gzipped
226         system("gunzip", "-c", trace_name, :out => file.path)
227       elsif bzipped
228         system("bunzip2", "-c", trace_name, :out => file.path)
229       elsif zipped
230         system("unzip", "-p", trace_name, "-x", "__MACOSX/*", :out => file.path, :err => "/dev/null")
231       end
232
233       file.unlink
234     else
235       file = File.open(trace_name)
236     end
237
238     file
239   end
240
241   def import
242     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
243
244     gpx = GPX::File.new(trace_name)
245
246     f_lat = 0
247     f_lon = 0
248     first = true
249
250     # If there are any existing points for this trace then delete them
251     Tracepoint.where(:gpx_id => id).delete_all
252
253     gpx.points.each_slice(1_000) do |points|
254       # Gather the trace points together for a bulk import
255       tracepoints = []
256
257       points.each do |point|
258         if first
259           f_lat = point.latitude
260           f_lon = point.longitude
261           first = false
262         end
263
264         tp = Tracepoint.new
265         tp.lat = point.latitude
266         tp.lon = point.longitude
267         tp.altitude = point.altitude
268         tp.timestamp = point.timestamp
269         tp.gpx_id = id
270         tp.trackid = point.segment
271         tracepoints << tp
272       end
273
274       # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
275       tracepoints.each do |tp|
276         tp.run_callbacks(:save) { false }
277         tp.run_callbacks(:create) { false }
278       end
279
280       Tracepoint.import!(tracepoints)
281     end
282
283     if gpx.actual_points.positive?
284       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
285       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
286       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
287       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
288
289       max_lat = max_lat.to_f / 10000000
290       min_lat = min_lat.to_f / 10000000
291       max_lon = max_lon.to_f / 10000000
292       min_lon = min_lon.to_f / 10000000
293
294       self.latitude = f_lat
295       self.longitude = f_lon
296       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
297       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
298       self.size = gpx.actual_points
299       self.inserted = true
300       save!
301     end
302
303     logger.info "done trace #{id}"
304
305     gpx
306   end
307
308   private
309
310   def remove_files
311     FileUtils.rm_f(trace_name)
312     FileUtils.rm_f(icon_picture_name)
313     FileUtils.rm_f(large_picture_name)
314   end
315 end