Link GPX fixture files instead of stubbing
[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*/).select { |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   # Read in xml as text and return it's Node object representation
176   def self.from_xml(xml, create = false)
177     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
178     doc = p.parse
179
180     doc.find("//osm/gpx_file").each do |pt|
181       return Trace.from_xml_node(pt, create)
182     end
183
184     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
185   rescue LibXML::XML::Error, ArgumentError => ex
186     raise OSM::APIBadXMLError.new("trace", xml, ex.message)
187   end
188
189   def self.from_xml_node(pt, create = false)
190     trace = Trace.new
191
192     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
193     trace.visibility = pt["visibility"]
194
195     unless create
196       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
197       trace.id = pt["id"].to_i
198       # .to_i will return 0 if there is no number that can be parsed.
199       # We want to make sure that there is no id with zero anyway
200       raise OSM::APIBadUserInput.new("ID of trace cannot be zero when updating.") if trace.id.zero?
201     end
202
203     # We don't care about the time, as it is explicitly set on create/update/delete
204     # We don't care about the visibility as it is implicit based on the action
205     # and set manually before the actual delete
206     trace.visible = true
207
208     description = pt.find("description").first
209     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
210     trace.description = description.content
211
212     pt.find("tag").each do |tag|
213       trace.tags.build(:tag => tag.content)
214     end
215
216     trace
217   end
218
219   def xml_file
220     # TODO: *nix specific, could do to work on windows... would be functionally inferior though - check for '.gz'
221     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
222     gzipped = filetype =~ /gzip compressed/
223     bzipped = filetype =~ /bzip2 compressed/
224     zipped = filetype =~ /Zip archive/
225     tarred = filetype =~ /tar archive/
226
227     if gzipped || bzipped || zipped || tarred
228       tmpfile = Tempfile.new("trace.#{id}")
229
230       if tarred && gzipped
231         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
232       elsif tarred && bzipped
233         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
234       elsif tarred
235         system("tar -xOf #{trace_name} > #{tmpfile.path}")
236       elsif gzipped
237         system("gunzip -c #{trace_name} > #{tmpfile.path}")
238       elsif bzipped
239         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
240       elsif zipped
241         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
242       end
243
244       tmpfile.unlink
245
246       file = tmpfile.file
247     else
248       file = File.open(trace_name)
249     end
250
251     file
252   end
253
254   def import
255     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
256
257     gpx = GPX::File.new(xml_file)
258
259     f_lat = 0
260     f_lon = 0
261     first = true
262
263     # If there are any existing points for this trace then delete them
264     Tracepoint.delete_all(:gpx_id => id)
265
266     gpx.points 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       tp.save!
281     end
282
283     if gpx.actual_points > 0
284       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
285       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
286       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
287       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
288
289       max_lat = max_lat.to_f / 10000000
290       min_lat = min_lat.to_f / 10000000
291       max_lon = max_lon.to_f / 10000000
292       min_lon = min_lon.to_f / 10000000
293
294       self.latitude = f_lat
295       self.longitude = f_lon
296       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
297       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
298       self.size = gpx.actual_points
299       self.inserted = true
300       save!
301     end
302
303     logger.info "done trace #{id}"
304
305     gpx
306   end
307 end