Merge pull request #1938 from jguthrie100/fix_no_trace_description_error
[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
42   validates :description, :presence => { :on => :create }, :length => 1..255
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     "#{GPX_IMAGE_DIR}/#{id}.gif"
114   end
115
116   def icon_picture_name
117     "#{GPX_IMAGE_DIR}/#{id}_icon.gif"
118   end
119
120   def trace_name
121     "#{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     self.visibility = pt["visibility"]
218
219     unless create
220       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
221       id = pt["id"].to_i
222       # .to_i will return 0 if there is no number that can be parsed.
223       # We want to make sure that there is no id with zero anyway
224       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
225       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
226     end
227
228     # We don't care about the time, as it is explicitly set on create/update/delete
229     # We don't care about the visibility as it is implicit based on the action
230     # and set manually before the actual delete
231     self.visible = true
232
233     description = pt.find("description").first
234     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
235     self.description = description.content
236
237     self.tags = pt.find("tag").collect do |tag|
238       Tracetag.new(:tag => tag.content)
239     end
240   end
241
242   def xml_file
243     # TODO: *nix specific, could do to work on windows... would be functionally inferior though - check for '.gz'
244     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
245     gzipped = filetype =~ /gzip compressed/
246     bzipped = filetype =~ /bzip2 compressed/
247     zipped = filetype =~ /Zip archive/
248     tarred = filetype =~ /tar archive/
249
250     if gzipped || bzipped || zipped || tarred
251       tmpfile = Tempfile.new("trace.#{id}")
252
253       if tarred && gzipped
254         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
255       elsif tarred && bzipped
256         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
257       elsif tarred
258         system("tar -xOf #{trace_name} > #{tmpfile.path}")
259       elsif gzipped
260         system("gunzip -c #{trace_name} > #{tmpfile.path}")
261       elsif bzipped
262         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
263       elsif zipped
264         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
265       end
266
267       tmpfile.unlink
268
269       file = tmpfile.file
270     else
271       file = File.open(trace_name)
272     end
273
274     file
275   end
276
277   def import
278     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
279
280     gpx = GPX::File.new(xml_file)
281
282     f_lat = 0
283     f_lon = 0
284     first = true
285
286     # If there are any existing points for this trace then delete them
287     Tracepoint.where(:gpx_id => id).delete_all
288
289     gpx.points do |point|
290       if first
291         f_lat = point.latitude
292         f_lon = point.longitude
293         first = false
294       end
295
296       tp = Tracepoint.new
297       tp.lat = point.latitude
298       tp.lon = point.longitude
299       tp.altitude = point.altitude
300       tp.timestamp = point.timestamp
301       tp.gpx_id = id
302       tp.trackid = point.segment
303       tp.save!
304     end
305
306     if gpx.actual_points > 0
307       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
308       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
309       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
310       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
311
312       max_lat = max_lat.to_f / 10000000
313       min_lat = min_lat.to_f / 10000000
314       max_lon = max_lon.to_f / 10000000
315       min_lon = min_lon.to_f / 10000000
316
317       self.latitude = f_lat
318       self.longitude = f_lon
319       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
320       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
321       self.size = gpx.actual_points
322       self.inserted = true
323       save!
324     end
325
326     logger.info "done trace #{id}"
327
328     gpx
329   end
330 end