]> git.openstreetmap.org Git - rails.git/blob - app/models/trace.rb
Parse each GPX file in an archive separately
[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 < ActiveRecord::Base
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 = `/usr/bin/file -Lbz #{trace_name}`.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 = `/usr/bin/file -Lbz #{trace_name}`.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 to_xml
168     doc = OSM::API.new.get_xml_doc
169     doc.root << to_xml_node
170     doc
171   end
172
173   def to_xml_node
174     el1 = XML::Node.new "gpx_file"
175     el1["id"] = id.to_s
176     el1["name"] = name.to_s
177     el1["lat"] = latitude.to_s if inserted
178     el1["lon"] = longitude.to_s if inserted
179     el1["user"] = user.display_name
180     el1["visibility"] = visibility
181     el1["pending"] = inserted ? "false" : "true"
182     el1["timestamp"] = timestamp.xmlschema
183
184     el2 = XML::Node.new "description"
185     el2 << description
186     el1 << el2
187
188     tags.each do |tag|
189       el2 = XML::Node.new("tag")
190       el2 << tag.tag
191       el1 << el2
192     end
193
194     el1
195   end
196
197   def update_from_xml(xml, create = false)
198     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
199     doc = p.parse
200
201     doc.find("//osm/gpx_file").each do |pt|
202       return update_from_xml_node(pt, create)
203     end
204
205     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
206   rescue LibXML::XML::Error, ArgumentError => e
207     raise OSM::APIBadXMLError.new("trace", xml, e.message)
208   end
209
210   def update_from_xml_node(pt, create = false)
211     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
212
213     self.visibility = pt["visibility"]
214
215     unless create
216       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
217
218       id = pt["id"].to_i
219       # .to_i will return 0 if there is no number that can be parsed.
220       # We want to make sure that there is no id with zero anyway
221       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
222       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
223     end
224
225     # We don't care about the time, as it is explicitly set on create/update/delete
226     # We don't care about the visibility as it is implicit based on the action
227     # and set manually before the actual delete
228     self.visible = true
229
230     description = pt.find("description").first
231     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
232
233     self.description = description.content
234
235     self.tags = pt.find("tag").collect do |tag|
236       Tracetag.new(:tag => tag.content)
237     end
238   end
239
240   def xml_file
241     # TODO: *nix specific, could do to work on windows... would be functionally inferior though - check for '.gz'
242     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
243     gzipped = filetype =~ /gzip compressed/
244     bzipped = filetype =~ /bzip2 compressed/
245     zipped = filetype =~ /Zip archive/
246     tarred = filetype =~ /tar archive/
247
248     if gzipped || bzipped || zipped || tarred
249       tmpfile = Tempfile.new("trace.#{id}")
250
251       if tarred && gzipped
252         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
253       elsif tarred && bzipped
254         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
255       elsif tarred
256         system("tar -xOf #{trace_name} > #{tmpfile.path}")
257       elsif gzipped
258         system("gunzip -c #{trace_name} > #{tmpfile.path}")
259       elsif bzipped
260         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
261       elsif zipped
262         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
263       end
264
265       tmpfile.unlink
266
267       file = tmpfile.file
268     else
269       file = File.open(trace_name)
270     end
271
272     file
273   end
274
275   def import
276     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
277
278     gpx = ::GPX::File.new(trace_name)
279
280     f_lat = 0
281     f_lon = 0
282     first = true
283
284     # If there are any existing points for this trace then delete them
285     Tracepoint.where(:gpx_id => id).delete_all
286
287     gpx.points.each_slice(1_000) do |points|
288       # Gather the trace points together for a bulk import
289       tracepoints = []
290
291       points.each do |point|
292         if first
293           f_lat = point.latitude
294           f_lon = point.longitude
295           first = false
296         end
297
298         tp = Tracepoint.new
299         tp.lat = point.latitude
300         tp.lon = point.longitude
301         tp.altitude = point.altitude
302         tp.timestamp = point.timestamp
303         tp.gpx_id = id
304         tp.trackid = point.segment
305         tracepoints << tp
306       end
307
308       # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
309       tracepoints.each do |tp|
310         tp.run_callbacks(:save) { false }
311         tp.run_callbacks(:create) { false }
312       end
313
314       Tracepoint.import!(tracepoints)
315     end
316
317     if gpx.actual_points.positive?
318       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
319       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
320       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
321       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
322
323       max_lat = max_lat.to_f / 10000000
324       min_lat = min_lat.to_f / 10000000
325       max_lon = max_lon.to_f / 10000000
326       min_lon = min_lon.to_f / 10000000
327
328       self.latitude = f_lat
329       self.longitude = f_lon
330       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
331       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
332       self.size = gpx.actual_points
333       self.inserted = true
334       save!
335     end
336
337     logger.info "done trace #{id}"
338
339     gpx
340   end
341
342   private
343
344   def remove_files
345     FileUtils.rm_f(trace_name)
346     FileUtils.rm_f(icon_picture_name)
347     FileUtils.rm_f(large_picture_name)
348   end
349 end