]> git.openstreetmap.org Git - rails.git/blob - app/models/trace.rb
Merge branch 'master' into tag-colour-preview-rebase
[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 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     # TODO: *nix specific, could do to work on windows... would be functionally inferior though - check for '.gz'
212     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
213     gzipped = filetype =~ /gzip compressed/
214     bzipped = filetype =~ /bzip2 compressed/
215     zipped = filetype =~ /Zip archive/
216     tarred = filetype =~ /tar archive/
217
218     if gzipped || bzipped || zipped || tarred
219       tmpfile = Tempfile.new("trace.#{id}")
220
221       if tarred && gzipped
222         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
223       elsif tarred && bzipped
224         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
225       elsif tarred
226         system("tar -xOf #{trace_name} > #{tmpfile.path}")
227       elsif gzipped
228         system("gunzip -c #{trace_name} > #{tmpfile.path}")
229       elsif bzipped
230         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
231       elsif zipped
232         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
233       end
234
235       tmpfile.unlink
236
237       file = tmpfile.file
238     else
239       file = File.open(trace_name)
240     end
241
242     file
243   end
244
245   def import
246     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
247
248     gpx = GPX::File.new(trace_name)
249
250     f_lat = 0
251     f_lon = 0
252     first = true
253
254     # If there are any existing points for this trace then delete them
255     Tracepoint.where(:gpx_id => id).delete_all
256
257     gpx.points.each_slice(1_000) do |points|
258       # Gather the trace points together for a bulk import
259       tracepoints = []
260
261       points.each do |point|
262         if first
263           f_lat = point.latitude
264           f_lon = point.longitude
265           first = false
266         end
267
268         tp = Tracepoint.new
269         tp.lat = point.latitude
270         tp.lon = point.longitude
271         tp.altitude = point.altitude
272         tp.timestamp = point.timestamp
273         tp.gpx_id = id
274         tp.trackid = point.segment
275         tracepoints << tp
276       end
277
278       # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
279       tracepoints.each do |tp|
280         tp.run_callbacks(:save) { false }
281         tp.run_callbacks(:create) { false }
282       end
283
284       Tracepoint.import!(tracepoints)
285     end
286
287     if gpx.actual_points.positive?
288       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
289       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
290       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
291       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
292
293       max_lat = max_lat.to_f / 10000000
294       min_lat = min_lat.to_f / 10000000
295       max_lon = max_lon.to_f / 10000000
296       min_lon = min_lon.to_f / 10000000
297
298       self.latitude = f_lat
299       self.longitude = f_lon
300       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
301       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
302       self.size = gpx.actual_points
303       self.inserted = true
304       save!
305     end
306
307     logger.info "done trace #{id}"
308
309     gpx
310   end
311
312   private
313
314   def remove_files
315     FileUtils.rm_f(trace_name)
316     FileUtils.rm_f(icon_picture_name)
317     FileUtils.rm_f(large_picture_name)
318   end
319 end