Rename xml processing methods with an update_ prefix
[rails.git] / app / models / trace.rb
1 class Trace < ActiveRecord::Base
2   self.table_name = "gpx_files"
3
4   belongs_to :user, :counter_cache => true
5   has_many :tags, :class_name => "Tracetag", :foreign_key => "gpx_id", :dependent => :delete_all
6   has_many :points, :class_name => "Tracepoint", :foreign_key => "gpx_id", :dependent => :delete_all
7
8   scope :visible, -> { where(:visible => true) }
9   scope :visible_to, ->(u) { visible.where("visibility IN ('public', 'identifiable') OR user_id = ?", u) }
10   scope :visible_to_all, -> { where(:visibility => %w[public identifiable]) }
11   scope :tagged, ->(t) { joins(:tags).where(:gpx_file_tags => { :tag => t }) }
12
13   validates :user, :presence => true, :associated => true
14   validates :name, :presence => true, :length => 1..255
15   validates :description, :presence => { :on => :create }, :length => 1..255
16   validates :timestamp, :presence => true
17   validates :visibility, :inclusion => %w[private public trackable identifiable]
18
19   def destroy
20     super
21     FileUtils.rm_f(trace_name)
22     FileUtils.rm_f(icon_picture_name)
23     FileUtils.rm_f(large_picture_name)
24   end
25
26   def tagstring
27     tags.collect(&:tag).join(", ")
28   end
29
30   def tagstring=(s)
31     self.tags = if s.include? ","
32                   s.split(/\s*,\s*/).reject { |tag| tag =~ /^\s*$/ }.collect do |tag|
33                     tt = Tracetag.new
34                     tt.tag = tag
35                     tt
36                   end
37                 else
38                   # do as before for backwards compatibility:
39                   s.split.collect do |tag|
40                     tt = Tracetag.new
41                     tt.tag = tag
42                     tt
43                   end
44                 end
45   end
46
47   def public?
48     visibility == "public" || visibility == "identifiable"
49   end
50
51   def trackable?
52     visibility == "trackable" || visibility == "identifiable"
53   end
54
55   def identifiable?
56     visibility == "identifiable"
57   end
58
59   def large_picture=(data)
60     f = File.new(large_picture_name, "wb")
61     f.syswrite(data)
62     f.close
63   end
64
65   def icon_picture=(data)
66     f = File.new(icon_picture_name, "wb")
67     f.syswrite(data)
68     f.close
69   end
70
71   def large_picture
72     f = File.new(large_picture_name, "rb")
73     data = f.sysread(File.size(f.path))
74     f.close
75     data
76   end
77
78   def icon_picture
79     f = File.new(icon_picture_name, "rb")
80     data = f.sysread(File.size(f.path))
81     f.close
82     data
83   end
84
85   def large_picture_name
86     "#{GPX_IMAGE_DIR}/#{id}.gif"
87   end
88
89   def icon_picture_name
90     "#{GPX_IMAGE_DIR}/#{id}_icon.gif"
91   end
92
93   def trace_name
94     "#{GPX_TRACE_DIR}/#{id}.gpx"
95   end
96
97   def mime_type
98     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
99     gzipped = filetype =~ /gzip compressed/
100     bzipped = filetype =~ /bzip2 compressed/
101     zipped = filetype =~ /Zip archive/
102     tarred = filetype =~ /tar archive/
103
104     mimetype = if gzipped
105                  "application/x-gzip"
106                elsif bzipped
107                  "application/x-bzip2"
108                elsif zipped
109                  "application/x-zip"
110                elsif tarred
111                  "application/x-tar"
112                else
113                  "application/gpx+xml"
114                end
115
116     mimetype
117   end
118
119   def extension_name
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     extension = if tarred && gzipped
127                   ".tar.gz"
128                 elsif tarred && bzipped
129                   ".tar.bz2"
130                 elsif tarred
131                   ".tar"
132                 elsif gzipped
133                   ".gpx.gz"
134                 elsif bzipped
135                   ".gpx.bz2"
136                 elsif zipped
137                   ".zip"
138                 else
139                   ".gpx"
140                 end
141
142     extension
143   end
144
145   def to_xml
146     doc = OSM::API.new.get_xml_doc
147     doc.root << to_xml_node
148     doc
149   end
150
151   def to_xml_node
152     el1 = XML::Node.new "gpx_file"
153     el1["id"] = id.to_s
154     el1["name"] = name.to_s
155     el1["lat"] = latitude.to_s if inserted
156     el1["lon"] = longitude.to_s if inserted
157     el1["user"] = user.display_name
158     el1["visibility"] = visibility
159     el1["pending"] = inserted ? "false" : "true"
160     el1["timestamp"] = timestamp.xmlschema
161
162     el2 = XML::Node.new "description"
163     el2 << description
164     el1 << el2
165
166     tags.each do |tag|
167       el2 = XML::Node.new("tag")
168       el2 << tag.tag
169       el1 << el2
170     end
171
172     el1
173   end
174
175   def update_from_xml(xml, create = false)
176     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
177     doc = p.parse
178
179     doc.find("//osm/gpx_file").each do |pt|
180       return update_from_xml_node(pt, create)
181     end
182
183     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
184   rescue LibXML::XML::Error, ArgumentError => ex
185     raise OSM::APIBadXMLError.new("trace", xml, ex.message)
186   end
187
188   def update_from_xml_node(pt, create = false)
189     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
190     self.visibility = pt["visibility"]
191
192     unless create
193       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
194       id = pt["id"].to_i
195       # .to_i will return 0 if there is no number that can be parsed.
196       # We want to make sure that there is no id with zero anyway
197       raise OSM::APIBadUserInput.new("ID of trace cannot be zero when updating.") if id.zero?
198       raise OSM::APIBadUserInput.new("The id in the url (#{self.id}) is not the same as provided in the xml (#{id})") unless self.id == id
199     end
200
201     # We don't care about the time, as it is explicitly set on create/update/delete
202     # We don't care about the visibility as it is implicit based on the action
203     # and set manually before the actual delete
204     self.visible = true
205
206     description = pt.find("description").first
207     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
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 do |point|
263       if first
264         f_lat = point.latitude
265         f_lon = point.longitude
266         first = false
267       end
268
269       tp = Tracepoint.new
270       tp.lat = point.latitude
271       tp.lon = point.longitude
272       tp.altitude = point.altitude
273       tp.timestamp = point.timestamp
274       tp.gpx_id = id
275       tp.trackid = point.segment
276       tp.save!
277     end
278
279     if gpx.actual_points > 0
280       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
281       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
282       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
283       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
284
285       max_lat = max_lat.to_f / 10000000
286       min_lat = min_lat.to_f / 10000000
287       max_lon = max_lon.to_f / 10000000
288       min_lon = min_lon.to_f / 10000000
289
290       self.latitude = f_lat
291       self.longitude = f_lon
292       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
293       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
294       self.size = gpx.actual_points
295       self.inserted = true
296       save!
297     end
298
299     logger.info "done trace #{id}"
300
301     gpx
302   end
303 end