]> git.openstreetmap.org Git - rails.git/blob - app/models/trace.rb
Added tests for validators
[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 :name, :description, :invalid_chars => true
43   validates :description, :presence => { :on => :create }, :length => 1..255
44   validates :timestamp, :presence => true
45   validates :visibility, :inclusion => %w[private public trackable identifiable]
46
47   def destroy
48     super
49     FileUtils.rm_f(trace_name)
50     FileUtils.rm_f(icon_picture_name)
51     FileUtils.rm_f(large_picture_name)
52   end
53
54   def tagstring
55     tags.collect(&:tag).join(", ")
56   end
57
58   def tagstring=(s)
59     self.tags = if s.include? ","
60                   s.split(/\s*,\s*/).reject { |tag| tag =~ /^\s*$/ }.collect do |tag|
61                     tt = Tracetag.new
62                     tt.tag = tag
63                     tt
64                   end
65                 else
66                   # do as before for backwards compatibility:
67                   s.split.collect do |tag|
68                     tt = Tracetag.new
69                     tt.tag = tag
70                     tt
71                   end
72                 end
73   end
74
75   def public?
76     visibility == "public" || visibility == "identifiable"
77   end
78
79   def trackable?
80     visibility == "trackable" || visibility == "identifiable"
81   end
82
83   def identifiable?
84     visibility == "identifiable"
85   end
86
87   def large_picture=(data)
88     f = File.new(large_picture_name, "wb")
89     f.syswrite(data)
90     f.close
91   end
92
93   def icon_picture=(data)
94     f = File.new(icon_picture_name, "wb")
95     f.syswrite(data)
96     f.close
97   end
98
99   def large_picture
100     f = File.new(large_picture_name, "rb")
101     data = f.sysread(File.size(f.path))
102     f.close
103     data
104   end
105
106   def icon_picture
107     f = File.new(icon_picture_name, "rb")
108     data = f.sysread(File.size(f.path))
109     f.close
110     data
111   end
112
113   def large_picture_name
114     "#{GPX_IMAGE_DIR}/#{id}.gif"
115   end
116
117   def icon_picture_name
118     "#{GPX_IMAGE_DIR}/#{id}_icon.gif"
119   end
120
121   def trace_name
122     "#{GPX_TRACE_DIR}/#{id}.gpx"
123   end
124
125   def mime_type
126     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
127     gzipped = filetype =~ /gzip compressed/
128     bzipped = filetype =~ /bzip2 compressed/
129     zipped = filetype =~ /Zip archive/
130     tarred = filetype =~ /tar archive/
131
132     mimetype = if gzipped
133                  "application/x-gzip"
134                elsif bzipped
135                  "application/x-bzip2"
136                elsif zipped
137                  "application/x-zip"
138                elsif tarred
139                  "application/x-tar"
140                else
141                  "application/gpx+xml"
142                end
143
144     mimetype
145   end
146
147   def extension_name
148     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
149     gzipped = filetype =~ /gzip compressed/
150     bzipped = filetype =~ /bzip2 compressed/
151     zipped = filetype =~ /Zip archive/
152     tarred = filetype =~ /tar archive/
153
154     extension = if tarred && gzipped
155                   ".tar.gz"
156                 elsif tarred && bzipped
157                   ".tar.bz2"
158                 elsif tarred
159                   ".tar"
160                 elsif gzipped
161                   ".gpx.gz"
162                 elsif bzipped
163                   ".gpx.bz2"
164                 elsif zipped
165                   ".zip"
166                 else
167                   ".gpx"
168                 end
169
170     extension
171   end
172
173   def to_xml
174     doc = OSM::API.new.get_xml_doc
175     doc.root << to_xml_node
176     doc
177   end
178
179   def to_xml_node
180     el1 = XML::Node.new "gpx_file"
181     el1["id"] = id.to_s
182     el1["name"] = name.to_s
183     el1["lat"] = latitude.to_s if inserted
184     el1["lon"] = longitude.to_s if inserted
185     el1["user"] = user.display_name
186     el1["visibility"] = visibility
187     el1["pending"] = inserted ? "false" : "true"
188     el1["timestamp"] = timestamp.xmlschema
189
190     el2 = XML::Node.new "description"
191     el2 << description
192     el1 << el2
193
194     tags.each do |tag|
195       el2 = XML::Node.new("tag")
196       el2 << tag.tag
197       el1 << el2
198     end
199
200     el1
201   end
202
203   def update_from_xml(xml, create = false)
204     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
205     doc = p.parse
206
207     doc.find("//osm/gpx_file").each do |pt|
208       return update_from_xml_node(pt, create)
209     end
210
211     raise OSM::APIBadXMLError.new("trace", xml, "XML doesn't contain an osm/gpx_file element.")
212   rescue LibXML::XML::Error, ArgumentError => ex
213     raise OSM::APIBadXMLError.new("trace", xml, ex.message)
214   end
215
216   def update_from_xml_node(pt, create = false)
217     raise OSM::APIBadXMLError.new("trace", pt, "visibility missing") if pt["visibility"].nil?
218
219     self.visibility = pt["visibility"]
220
221     unless create
222       raise OSM::APIBadXMLError.new("trace", pt, "ID is required when updating.") if pt["id"].nil?
223
224       id = pt["id"].to_i
225       # .to_i will return 0 if there is no number that can be parsed.
226       # We want to make sure that there is no id with zero anyway
227       raise OSM::APIBadUserInput, "ID of trace cannot be zero when updating." if id.zero?
228       raise OSM::APIBadUserInput, "The id in the url (#{self.id}) is not the same as provided in the xml (#{id})" unless self.id == id
229     end
230
231     # We don't care about the time, as it is explicitly set on create/update/delete
232     # We don't care about the visibility as it is implicit based on the action
233     # and set manually before the actual delete
234     self.visible = true
235
236     description = pt.find("description").first
237     raise OSM::APIBadXMLError.new("trace", pt, "description missing") if description.nil?
238
239     self.description = description.content
240
241     self.tags = pt.find("tag").collect do |tag|
242       Tracetag.new(:tag => tag.content)
243     end
244   end
245
246   def xml_file
247     # TODO: *nix specific, could do to work on windows... would be functionally inferior though - check for '.gz'
248     filetype = `/usr/bin/file -Lbz #{trace_name}`.chomp
249     gzipped = filetype =~ /gzip compressed/
250     bzipped = filetype =~ /bzip2 compressed/
251     zipped = filetype =~ /Zip archive/
252     tarred = filetype =~ /tar archive/
253
254     if gzipped || bzipped || zipped || tarred
255       tmpfile = Tempfile.new("trace.#{id}")
256
257       if tarred && gzipped
258         system("tar -zxOf #{trace_name} > #{tmpfile.path}")
259       elsif tarred && bzipped
260         system("tar -jxOf #{trace_name} > #{tmpfile.path}")
261       elsif tarred
262         system("tar -xOf #{trace_name} > #{tmpfile.path}")
263       elsif gzipped
264         system("gunzip -c #{trace_name} > #{tmpfile.path}")
265       elsif bzipped
266         system("bunzip2 -c #{trace_name} > #{tmpfile.path}")
267       elsif zipped
268         system("unzip -p #{trace_name} -x '__MACOSX/*' > #{tmpfile.path} 2> /dev/null")
269       end
270
271       tmpfile.unlink
272
273       file = tmpfile.file
274     else
275       file = File.open(trace_name)
276     end
277
278     file
279   end
280
281   def import
282     logger.info("GPX Import importing #{name} (#{id}) from #{user.email}")
283
284     gpx = GPX::File.new(xml_file)
285
286     f_lat = 0
287     f_lon = 0
288     first = true
289
290     # If there are any existing points for this trace then delete them
291     Tracepoint.where(:gpx_id => id).delete_all
292
293     gpx.points do |point|
294       if first
295         f_lat = point.latitude
296         f_lon = point.longitude
297         first = false
298       end
299
300       tp = Tracepoint.new
301       tp.lat = point.latitude
302       tp.lon = point.longitude
303       tp.altitude = point.altitude
304       tp.timestamp = point.timestamp
305       tp.gpx_id = id
306       tp.trackid = point.segment
307       tp.save!
308     end
309
310     if gpx.actual_points.positive?
311       max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude)
312       min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude)
313       max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude)
314       min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude)
315
316       max_lat = max_lat.to_f / 10000000
317       min_lat = min_lat.to_f / 10000000
318       max_lon = max_lon.to_f / 10000000
319       min_lon = min_lon.to_f / 10000000
320
321       self.latitude = f_lat
322       self.longitude = f_lon
323       self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points)
324       self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon)
325       self.size = gpx.actual_points
326       self.inserted = true
327       save!
328     end
329
330     logger.info "done trace #{id}"
331
332     gpx
333   end
334 end