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