Use activerecord-import for bulk importing tracepoint records
[rails.git] / app / models / trace.rb
1 # == Schema Information
2 #
3 # Table name: gpx_files
4 #
5 #  id          :integer          not null, primary key
6 #  user_id     :integer          not null
7 #  visible     :boolean          default(TRUE), not null
8 #  name        :string           default(""), not null
9 #  size        :integer
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   def destroy
47     super
48     FileUtils.rm_f(trace_name)
49     FileUtils.rm_f(icon_picture_name)
50     FileUtils.rm_f(large_picture_name)
51   end
52
53   def tagstring
54     tags.collect(&:tag).join(", ")
55   end
56
57   def tagstring=(s)
58     self.tags = if s.include? ","
59                   s.split(/\s*,\s*/).reject { |tag| tag =~ /^\s*$/ }.collect do |tag|
60                     tt = Tracetag.new
61                     tt.tag = tag
62                     tt
63                   end
64                 else
65                   # do as before for backwards compatibility:
66                   s.split.collect do |tag|
67                     tt = Tracetag.new
68                     tt.tag = tag
69                     tt
70                   end
71                 end
72   end
73
74   def public?
75     visibility == "public" || visibility == "identifiable"
76   end
77
78   def trackable?
79     visibility == "trackable" || visibility == "identifiable"
80   end
81
82   def identifiable?
83     visibility == "identifiable"
84   end
85
86   def large_picture=(data)
87     f = File.new(large_picture_name, "wb")
88     f.syswrite(data)
89     f.close
90   end
91
92   def icon_picture=(data)
93     f = File.new(icon_picture_name, "wb")
94     f.syswrite(data)
95     f.close
96   end
97
98   def large_picture
99     f = File.new(large_picture_name, "rb")
100     data = f.sysread(File.size(f.path))
101     f.close
102     data
103   end
104
105   def icon_picture
106     f = File.new(icon_picture_name, "rb")
107     data = f.sysread(File.size(f.path))
108     f.close
109     data
110   end
111
112   def large_picture_name
113     "#{Settings.gpx_image_dir}/#{id}.gif"
114   end
115
116   def icon_picture_name
117     "#{Settings.gpx_image_dir}/#{id}_icon.gif"
118   end
119
120   def trace_name
121     "#{Settings.gpx_trace_dir}/#{id}.gpx"
122   end
123
124   def mime_type
125     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
126     gzipped = filetype =~ /gzip compressed/
127     bzipped = filetype =~ /bzip2 compressed/
128     zipped = filetype =~ /Zip archive/
129     tarred = filetype =~ /tar archive/
130
131     mimetype = if gzipped
132                  "application/x-gzip"
133                elsif bzipped
134                  "application/x-bzip2"
135                elsif zipped
136                  "application/x-zip"
137                elsif tarred
138                  "application/x-tar"
139                else
140                  "application/gpx+xml"
141                end
142
143     mimetype
144   end
145
146   def extension_name
147     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
148     gzipped = filetype =~ /gzip compressed/
149     bzipped = filetype =~ /bzip2 compressed/
150     zipped = filetype =~ /Zip archive/
151     tarred = filetype =~ /tar archive/
152
153     extension = if tarred && gzipped
154                   ".tar.gz"
155                 elsif tarred && bzipped
156                   ".tar.bz2"
157                 elsif tarred
158                   ".tar"
159                 elsif gzipped
160                   ".gpx.gz"
161                 elsif bzipped
162                   ".gpx.bz2"
163                 elsif zipped
164                   ".zip"
165                 else
166                   ".gpx"
167                 end
168
169     extension
170   end
171
172   def to_xml
173     doc = OSM::API.new.get_xml_doc
174     doc.root << to_xml_node
175     doc
176   end
177
178   def to_xml_node
179     el1 = XML::Node.new "gpx_file"
180     el1["id"] = id.to_s
181     el1["name"] = name.to_s
182     el1["lat"] = latitude.to_s if inserted
183     el1["lon"] = longitude.to_s if inserted
184     el1["user"] = user.display_name
185     el1["visibility"] = visibility
186     el1["pending"] = inserted ? "false" : "true"
187     el1["timestamp"] = timestamp.xmlschema
188
189     el2 = XML::Node.new "description"
190     el2 << description
191     el1 << el2
192
193     tags.each do |tag|
194       el2 = XML::Node.new("tag")
195       el2 << tag.tag
196       el1 << el2
197     end
198
199     el1
200   end
201
202   def update_from_xml(xml, create = false)
203     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
204     doc = p.parse
205
206     doc.find("//osm/gpx_file").each do |pt|
207       return update_from_xml_node(pt, create)
208     end
209
210     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
211   rescue LibXML::XML::Error, ArgumentError => ex
212     raise OSM::APIBadXMLError.new("trace", xml, ex.message)
213   end
214
215   def update_from_xml_node(pt, create = false)
216     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
217
218     self.visibility = pt["visibility"]
219
220     unless create
221       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
222
223       id = pt["id"].to_i
224       # .to_i will return 0 if there is no number that can be parsed.
225       # We want to make sure that there is no id with zero anyway
226       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
227       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
228     end
229
230     # We don't care about the time, as it is explicitly set on create/update/delete
231     # We don't care about the visibility as it is implicit based on the action
232     # and set manually before the actual delete
233     self.visible = true
234
235     description = pt.find("description").first
236     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
237
238     self.description = description.content
239
240     self.tags = pt.find("tag").collect do |tag|
241       Tracetag.new(:tag => tag.content)
242     end
243   end
244
245   def xml_file
246     # TODO: *nix specific, could do to work on windows... would be functionally inferior though - check for '.gz'
247     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
248     gzipped = filetype =~ /gzip compressed/
249     bzipped = filetype =~ /bzip2 compressed/
250     zipped = filetype =~ /Zip archive/
251     tarred = filetype =~ /tar archive/
252
253     if gzipped || bzipped || zipped || tarred
254       tmpfile = Tempfile.new("trace.#{id}")
255
256       if tarred && gzipped
257         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
258       elsif tarred && bzipped
259         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
260       elsif tarred
261         system("tar -xOf #{trace_name} > #{tmpfile.path}")
262       elsif gzipped
263         system("gunzip -c #{trace_name} > #{tmpfile.path}")
264       elsif bzipped
265         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
266       elsif zipped
267         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
268       end
269
270       tmpfile.unlink
271
272       file = tmpfile.file
273     else
274       file = File.open(trace_name)
275     end
276
277     file
278   end
279
280   def import
281     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
282
283     gpx = ::GPX::File.new(xml_file)
284
285     f_lat = 0
286     f_lon = 0
287     first = true
288
289     # If there are any existing points for this trace then delete them
290     Tracepoint.where(:gpx_id => id).delete_all
291
292     # Gather the trace points together for a bulk import
293     tracepoints = []
294     gpx.points do |point|
295       if first
296         f_lat = point.latitude
297         f_lon = point.longitude
298         first = false
299       end
300
301       tp = Tracepoint.new
302       tp.lat = point.latitude
303       tp.lon = point.longitude
304       tp.altitude = point.altitude
305       tp.timestamp = point.timestamp
306       tp.gpx_id = id
307       tp.trackid = point.segment
308       tracepoints << tp
309     end
310
311     # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
312     tracepoints.each do |tp|
313       tp.run_callbacks(:save) { false }
314       tp.run_callbacks(:create) { false }
315     end
316     Tracepoint.import(tracepoints)
317
318     if gpx.actual_points.positive?
319       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
320       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
321       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
322       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
323
324       max_lat = max_lat.to_f / 10000000
325       min_lat = min_lat.to_f / 10000000
326       max_lon = max_lon.to_f / 10000000
327       min_lon = min_lon.to_f / 10000000
328
329       self.latitude = f_lat
330       self.longitude = f_lon
331       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
332       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
333       self.size = gpx.actual_points
334       self.inserted = true
335       save!
336     end
337
338     logger.info "done trace #{id}"
339
340     gpx
341   end
342 end