]> git.openstreetmap.org Git - rails.git/blob - app/models/trace.rb
Merge remote-tracking branch 'upstream/pull/2225'
[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   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 => e
212     raise OSM::APIBadXMLError.new("trace", xml, e.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     gpx.points.each_slice(1_000) do |points|
293       # Gather the trace points together for a bulk import
294       tracepoints = []
295
296       points.each do |point|
297         if first
298           f_lat = point.latitude
299           f_lon = point.longitude
300           first = false
301         end
302
303         tp = Tracepoint.new
304         tp.lat = point.latitude
305         tp.lon = point.longitude
306         tp.altitude = point.altitude
307         tp.timestamp = point.timestamp
308         tp.gpx_id = id
309         tp.trackid = point.segment
310         tracepoints << tp
311       end
312
313       # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import
314       tracepoints.each do |tp|
315         tp.run_callbacks(:save) { false }
316         tp.run_callbacks(:create) { false }
317       end
318
319       Tracepoint.import!(tracepoints)
320     end
321
322     if gpx.actual_points.positive?
323       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
324       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
325       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
326       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
327
328       max_lat = max_lat.to_f / 10000000
329       min_lat = min_lat.to_f / 10000000
330       max_lon = max_lon.to_f / 10000000
331       min_lon = min_lon.to_f / 10000000
332
333       self.latitude = f_lat
334       self.longitude = f_lon
335       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
336       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
337       self.size = gpx.actual_points
338       self.inserted = true
339       save!
340     end
341
342     logger.info "done trace #{id}"
343
344     gpx
345   end
346 end