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