ac4461f8e123efce46a265d193d99c8f91069a46
[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 LibXML::XML::Error => ex
38       raise OSM::APIBadXMLError.new("way", xml, ex.message)
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("way", self.id, k) 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 update_from(new_way, user)
200     check_consistency(self, new_way, user)
201     if !new_way.preconditions_ok?
202       raise OSM::APIPreconditionFailedError.new
203     end
204     self.changeset_id = new_way.changeset_id
205     self.tags = new_way.tags
206     self.nds = new_way.nds
207     self.visible = true
208     save_with_history!
209   end
210
211   def create_with_history(user)
212     check_create_consistency(self, user)
213     if !self.preconditions_ok?
214       raise OSM::APIPreconditionFailedError.new
215     end
216     self.version = 0
217     self.visible = true
218     save_with_history!
219   end
220
221   def preconditions_ok?
222     return false if self.nds.empty?
223     if self.nds.length > APP_CONFIG['max_number_of_way_nodes']
224       raise OSM::APITooManyWayNodesError.new(self.nds.count, APP_CONFIG['max_number_of_way_nodes'])
225     end
226     self.nds.each do |n|
227       node = Node.find(:first, :conditions => ["id = ?", n])
228       unless node and node.visible
229         return false
230       end
231     end
232     return true
233   end
234
235   def delete_with_history!(new_way, user)
236     unless self.visible
237       raise OSM::APIAlreadyDeletedError
238     end
239     
240     # need to start the transaction here, so that the database can 
241     # provide repeatable reads for the used-by checks. this means it
242     # shouldn't be possible to get race conditions.
243     Way.transaction do
244       check_consistency(self, new_way, user)
245       if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id",
246                              :conditions => [ "visible = ? AND member_type='way' and member_id=? ", true, self.id])
247         raise OSM::APIPreconditionFailedError
248       else
249         self.changeset_id = new_way.changeset_id
250         self.tags = []
251         self.nds = []
252         self.visible = false
253         save_with_history!
254       end
255     end
256   end
257
258   # delete a way and its nodes that aren't part of other ways, with history
259
260   # FIXME: merge the potlatch code to delete the relations
261   #        and refactor to use delete_with_history!
262   def delete_with_relations_and_nodes_and_history(changeset_id)
263     # delete the nodes not used by other ways
264     self.unshared_node_ids.each do |node_id|
265       n = Node.find(node_id)
266       n.changeset_id = changeset_id
267       n.visible = false
268       n.save_with_history!
269     end
270     
271     self.changeset_id = changeset_id
272     self.tags = []
273     self.nds = []
274     self.visible = false
275     self.save_with_history!
276   end
277
278   # Find nodes that belong to this way only
279   def unshared_node_ids
280     node_ids = self.nodes.collect { |node| node.id }
281
282     unless node_ids.empty?
283       way_nodes = WayNode.find(:all, :conditions => "node_id in (#{node_ids.join(',')}) and id != #{self.id}")
284       node_ids = node_ids - way_nodes.collect { |way_node| way_node.node_id }
285     end
286
287     return node_ids
288   end
289
290   # Temporary method to match interface to nodes
291   def tags_as_hash
292     return self.tags
293   end
294
295   ##
296   # if any referenced nodes are placeholder IDs (i.e: are negative) then
297   # this calling this method will fix them using the map from placeholders 
298   # to IDs +id_map+. 
299   def fix_placeholders!(id_map)
300     self.nds.map! do |node_id|
301       if node_id < 0
302         new_id = id_map[:node][node_id]
303         raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
304         new_id
305       else
306         node_id
307       end
308     end
309   end
310
311   private
312   
313   def save_with_history!
314     t = Time.now
315
316     # update the bounding box, but don't save it as the controller knows the 
317     # lifetime of the change better. note that this has to be done both before 
318     # and after the save, so that nodes from both versions are included in the 
319     # bbox.
320     changeset.update_bbox!(bbox) unless nodes.empty?
321
322     Way.transaction do
323       self.version += 1
324       self.timestamp = t
325       self.save!
326
327       tags = self.tags
328       WayTag.delete_all(['id = ?', self.id])
329       tags.each do |k,v|
330         tag = WayTag.new
331         tag.k = k
332         tag.v = v
333         tag.id = self.id
334         tag.save!
335       end
336
337       nds = self.nds
338       WayNode.delete_all(['id = ?', self.id])
339       sequence = 1
340       nds.each do |n|
341         nd = WayNode.new
342         nd.id = [self.id, sequence]
343         nd.node_id = n
344         nd.save!
345         sequence += 1
346       end
347
348       old_way = OldWay.from_way(self)
349       old_way.timestamp = t
350       old_way.save_with_dependencies!
351
352       # update and commit the bounding box, now that way nodes 
353       # have been updated and we're in a transaction.
354       changeset.update_bbox!(bbox) unless nodes.empty?
355
356       # tell the changeset we updated one element only
357       changeset.add_changes! 1
358
359       changeset.save!
360     end
361   end
362
363 end