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