Moved transaction boundary to cover used-by tests on deletion so that the database...
[rails.git] / app / models / way.rb
1 class Way < ActiveRecord::Base
2   require 'xml/libxml'
3   
4   include ConsistencyValidations
5
6   set_table_name 'current_ways'
7   
8   belongs_to :changeset
9
10   has_many :old_ways, :foreign_key => 'id', :order => 'version'
11
12   has_many :way_nodes, :foreign_key => 'id', :order => 'sequence_id'
13   has_many :nodes, :through => :way_nodes, :order => 'sequence_id'
14
15   has_many :way_tags, :foreign_key => 'id'
16
17   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
18   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
19
20   validates_presence_of :id, :on => :update
21   validates_presence_of :changeset_id,:version,  :timestamp
22   validates_uniqueness_of :id
23   validates_inclusion_of :visible, :in => [ true, false ]
24   validates_numericality_of :changeset_id, :version, :integer_only => true
25   validates_numericality_of :id, :on => :update, :integer_only => true
26   validates_associated :changeset
27
28   def self.from_xml(xml, create=false)
29     begin
30       p = XML::Parser.new
31       p.string = xml
32       doc = p.parse
33
34       doc.find('//osm/way').each do |pt|
35         return Way.from_xml_node(pt, create)
36       end
37     rescue
38       return nil
39     end
40   end
41
42   def self.from_xml_node(pt, create=false)
43     way = Way.new
44
45     if !create and pt['id'] != '0'
46       way.id = pt['id'].to_i
47     end
48     
49     way.version = pt['version']
50     way.changeset_id = pt['changeset']
51
52     if create
53       way.timestamp = Time.now
54       way.visible = true
55     else
56       if pt['timestamp']
57         way.timestamp = Time.parse(pt['timestamp'])
58       end
59       # if visible isn't present then it defaults to true
60       way.visible = (pt['visible'] or true)
61     end
62
63     pt.find('tag').each do |tag|
64       way.add_tag_keyval(tag['k'], tag['v'])
65     end
66
67     pt.find('nd').each do |nd|
68       way.add_nd_num(nd['ref'])
69     end
70
71     return way
72   end
73
74   # Find a way given it's ID, and in a single SQL call also grab its nodes
75   #
76   
77   # You can't pull in all the tags too unless we put a sequence_id on the way_tags table and have a multipart key
78   def self.find_eager(id)
79     way = Way.find(id, :include => {:way_nodes => :node})
80     #If waytag had a multipart key that was real, you could do this:
81     #way = Way.find(id, :include => [:way_tags, {:way_nodes => :node}])
82   end
83
84   # Find a way given it's ID, and in a single SQL call also grab its nodes and tags
85   def to_xml
86     doc = OSM::API.new.get_xml_doc
87     doc.root << to_xml_node()
88     return doc
89   end
90
91   def to_xml_node(visible_nodes = nil, user_display_name_cache = nil)
92     el1 = XML::Node.new 'way'
93     el1['id'] = self.id.to_s
94     el1['visible'] = self.visible.to_s
95     el1['timestamp'] = self.timestamp.xmlschema
96     el1['version'] = self.version.to_s
97     el1['changeset'] = self.changeset_id.to_s
98
99     user_display_name_cache = {} if user_display_name_cache.nil?
100
101     if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
102       # use the cache if available
103     elsif self.changeset.user.data_public?
104       user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
105     else
106       user_display_name_cache[self.changeset.user_id] = nil
107     end
108
109     if not user_display_name_cache[self.changeset.user_id].nil?
110       el1['user'] = user_display_name_cache[self.changeset.user_id]
111       el1['uid'] = self.changeset.user_id.to_s
112     end
113
114     # make sure nodes are output in sequence_id order
115     ordered_nodes = []
116     self.way_nodes.each do |nd|
117       if visible_nodes
118         # if there is a list of visible nodes then use that to weed out deleted nodes
119         if visible_nodes[nd.node_id]
120           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
121         end
122       else
123         # otherwise, manually go to the db to check things
124         if nd.node and nd.node.visible?
125           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
126         end
127       end
128     end
129
130     ordered_nodes.each do |nd_id|
131       if nd_id and nd_id != '0'
132         e = XML::Node.new 'nd'
133         e['ref'] = nd_id
134         el1 << e
135       end
136     end
137
138     self.way_tags.each do |tag|
139       e = XML::Node.new 'tag'
140       e['k'] = tag.k
141       e['v'] = tag.v
142       el1 << e
143     end
144     return el1
145   end 
146
147   def nds
148     unless @nds
149       @nds = Array.new
150       self.way_nodes.each do |nd|
151         @nds += [nd.node_id]
152       end
153     end
154     @nds
155   end
156
157   def tags
158     unless @tags
159       @tags = {}
160       self.way_tags.each do |tag|
161         @tags[tag.k] = tag.v
162       end
163     end
164     @tags
165   end
166
167   def nds=(s)
168     @nds = s
169   end
170
171   def tags=(t)
172     @tags = t
173   end
174
175   def add_nd_num(n)
176     @nds = Array.new unless @nds
177     @nds << n.to_i
178   end
179
180   def add_tag_keyval(k, v)
181     @tags = Hash.new unless @tags
182
183     # duplicate tags are now forbidden, so we can't allow values
184     # in the hash to be overwritten.
185     raise OSM::APIDuplicateTagsError.new if @tags.include? k
186
187     @tags[k] = v
188   end
189
190   ##
191   # the integer coords (i.e: unscaled) bounding box of the way, assuming
192   # straight line segments.
193   def bbox
194     lons = nodes.collect { |n| n.longitude }
195     lats = nodes.collect { |n| n.latitude }
196     [ lons.min, lats.min, lons.max, lats.max ]
197   end
198
199   def save_with_history!
200     t = Time.now
201
202     # update the bounding box, but don't save it as the controller knows the 
203     # lifetime of the change better. note that this has to be done both before 
204     # and after the save, so that nodes from both versions are included in the 
205     # bbox.
206     changeset.update_bbox!(bbox) unless nodes.empty?
207
208     Way.transaction do
209       self.version += 1
210       self.timestamp = t
211       self.save!
212
213       tags = self.tags
214       WayTag.delete_all(['id = ?', self.id])
215       tags.each do |k,v|
216         tag = WayTag.new
217         tag.k = k
218         tag.v = v
219         tag.id = self.id
220         tag.save!
221       end
222
223       nds = self.nds
224       WayNode.delete_all(['id = ?', self.id])
225       sequence = 1
226       nds.each do |n|
227         nd = WayNode.new
228         nd.id = [self.id, sequence]
229         nd.node_id = n
230         nd.save!
231         sequence += 1
232       end
233
234       old_way = OldWay.from_way(self)
235       old_way.timestamp = t
236       old_way.save_with_dependencies!
237
238       # update and commit the bounding box, now that way nodes 
239       # have been updated and we're in a transaction.
240       changeset.update_bbox!(bbox) unless nodes.empty?
241
242       # tell the changeset we updated one element only
243       changeset.add_changes! 1
244
245       changeset.save!
246     end
247   end
248
249   def update_from(new_way, user)
250     check_consistency(self, new_way, user)
251     if !new_way.preconditions_ok?
252       raise OSM::APIPreconditionFailedError.new
253     end
254     self.changeset_id = new_way.changeset_id
255     self.tags = new_way.tags
256     self.nds = new_way.nds
257     self.visible = true
258     save_with_history!
259   end
260
261   def create_with_history(user)
262     check_create_consistency(self, user)
263     if !self.preconditions_ok?
264       raise OSM::APIPreconditionFailedError.new
265     end
266     self.version = 0
267     self.visible = true
268     save_with_history!
269   end
270
271   def preconditions_ok?
272     return false if self.nds.empty?
273     if self.nds.length > APP_CONFIG['max_number_of_way_nodes']
274       raise OSM::APITooManyWayNodesError.new(self.nds.count, APP_CONFIG['max_number_of_way_nodes'])
275     end
276     self.nds.each do |n|
277       node = Node.find(:first, :conditions => ["id = ?", n])
278       unless node and node.visible
279         return false
280       end
281     end
282     return true
283   end
284
285   def delete_with_history!(new_way, user)
286     unless self.visible
287       raise OSM::APIAlreadyDeletedError
288     end
289     
290     # need to start the transaction here, so that the database can 
291     # provide repeatable reads for the used-by checks. this means it
292     # shouldn't be possible to get race conditions.
293     Way.transaction do
294       check_consistency(self, new_way, user)
295       if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id",
296                              :conditions => [ "visible = ? AND member_type='way' and member_id=? ", true, self.id])
297         raise OSM::APIPreconditionFailedError
298       else
299         self.changeset_id = new_way.changeset_id
300         self.tags = []
301         self.nds = []
302         self.visible = false
303         self.save_with_history!
304       end
305     end
306   end
307
308   # delete a way and its nodes that aren't part of other ways, with history
309
310   # FIXME: merge the potlatch code to delete the relations
311   #        and refactor to use delete_with_history!
312   def delete_with_relations_and_nodes_and_history(changeset_id)
313     # delete the nodes not used by other ways
314     self.unshared_node_ids.each do |node_id|
315       n = Node.find(node_id)
316       n.changeset_id = changeset_id
317       n.visible = false
318       n.save_with_history!
319     end
320     
321     self.changeset_id = changeset_id
322     self.tags = []
323     self.nds = []
324     self.visible = false
325     self.save_with_history!
326   end
327
328   # Find nodes that belong to this way only
329   def unshared_node_ids
330     node_ids = self.nodes.collect { |node| node.id }
331
332     unless node_ids.empty?
333       way_nodes = WayNode.find(:all, :conditions => "node_id in (#{node_ids.join(',')}) and id != #{self.id}")
334       node_ids = node_ids - way_nodes.collect { |way_node| way_node.node_id }
335     end
336
337     return node_ids
338   end
339
340   # Temporary method to match interface to nodes
341   def tags_as_hash
342     return self.tags
343   end
344
345   ##
346   # if any referenced nodes are placeholder IDs (i.e: are negative) then
347   # this calling this method will fix them using the map from placeholders 
348   # to IDs +id_map+. 
349   def fix_placeholders!(id_map)
350     self.nds.map! do |node_id|
351       if node_id < 0
352         new_id = id_map[:node][node_id]
353         raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
354         new_id
355       else
356         node_id
357       end
358     end
359   end
360
361 end