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