]> git.openstreetmap.org Git - rails.git/blob - app/models/way.rb
Generate valid URLs for search results with no object attached
[rails.git] / app / models / way.rb
1 class Way < ActiveRecord::Base
2   require 'xml/libxml'
3   
4   include ConsistencyValidations
5   include NotRedactable
6
7   self.table_name = "current_ways"
8   
9   belongs_to :changeset
10
11   has_many :old_ways, -> { order(:version) }
12
13   has_many :way_nodes, -> { order(:sequence_id) }
14   has_many :nodes, -> { order("sequence_id") }, :through => :way_nodes
15
16   has_many :way_tags
17
18   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
19   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
20
21   validates_presence_of :id, :on => :update
22   validates_presence_of :changeset_id,:version,  :timestamp
23   validates_uniqueness_of :id
24   validates_inclusion_of :visible, :in => [ true, false ]
25   validates_numericality_of :changeset_id, :version, :integer_only => true
26   validates_numericality_of :id, :on => :update, :integer_only => true
27   validates_associated :changeset
28
29   scope :visible, -> { where(:visible => true) }
30     scope :invisible, -> { where(:visible => false) }
31
32   # Read in xml as text and return it's Way object representation
33   def self.from_xml(xml, create=false)
34     begin
35       p = XML::Parser.string(xml)
36       doc = p.parse
37
38       doc.find('//osm/way').each do |pt|
39         return Way.from_xml_node(pt, create)
40       end
41       raise OSM::APIBadXMLError.new("node", xml, "XML doesn't contain an osm/way element.")
42     rescue LibXML::XML::Error, ArgumentError => ex
43       raise OSM::APIBadXMLError.new("way", xml, ex.message)
44     end
45   end
46
47   def self.from_xml_node(pt, create=false)
48     way = Way.new
49
50     raise OSM::APIBadXMLError.new("way", pt, "Version is required when updating") unless create or not pt['version'].nil?
51     way.version = pt['version']
52     raise OSM::APIBadXMLError.new("way", pt, "Changeset id is missing") if pt['changeset'].nil?
53     way.changeset_id = pt['changeset']
54
55     unless create
56       raise OSM::APIBadXMLError.new("way", pt, "ID is required when updating") if pt['id'].nil?
57       way.id = pt['id'].to_i
58       # .to_i will return 0 if there is no number that can be parsed. 
59       # We want to make sure that there is no id with zero anyway
60       raise OSM::APIBadUserInput.new("ID of way cannot be zero when updating.") if way.id == 0
61     end
62
63     # We don't care about the timestamp nor the visibility as these are either
64     # set explicitly or implicit in the action. The visibility is set to true, 
65     # and manually set to false before the actual delete.
66     way.visible = true
67
68     # Start with no tags
69     way.tags = Hash.new
70
71     # Add in any tags from the XML
72     pt.find('tag').each do |tag|
73       raise OSM::APIBadXMLError.new("way", pt, "tag is missing key") if tag['k'].nil?
74       raise OSM::APIBadXMLError.new("way", pt, "tag is missing value") if tag['v'].nil?
75       way.add_tag_keyval(tag['k'], tag['v'])
76     end
77
78     pt.find('nd').each do |nd|
79       way.add_nd_num(nd['ref'])
80     end
81
82     return way
83   end
84
85   # Find a way given it's ID, and in a single SQL call also grab its nodes
86   #
87   
88   # 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
89   def self.find_eager(id)
90     way = Way.find(id, :include => {:way_nodes => :node})
91     #If waytag had a multipart key that was real, you could do this:
92     #way = Way.find(id, :include => [:way_tags, {:way_nodes => :node}])
93   end
94
95   # Find a way given it's ID, and in a single SQL call also grab its nodes and tags
96   def to_xml
97     doc = OSM::API.new.get_xml_doc
98     doc.root << to_xml_node()
99     return doc
100   end
101
102   def to_xml_node(visible_nodes = nil, changeset_cache = {}, user_display_name_cache = {})
103     el1 = XML::Node.new 'way'
104     el1['id'] = self.id.to_s
105     el1['visible'] = self.visible.to_s
106     el1['timestamp'] = self.timestamp.xmlschema
107     el1['version'] = self.version.to_s
108     el1['changeset'] = self.changeset_id.to_s
109
110     if changeset_cache.key?(self.changeset_id)
111       # use the cache if available
112     else
113       changeset_cache[self.changeset_id] = self.changeset.user_id
114     end
115
116     user_id = changeset_cache[self.changeset_id]
117
118     if user_display_name_cache.key?(user_id)
119       # use the cache if available
120     elsif self.changeset.user.data_public?
121       user_display_name_cache[user_id] = self.changeset.user.display_name
122     else
123       user_display_name_cache[user_id] = nil
124     end
125
126     if not user_display_name_cache[user_id].nil?
127       el1['user'] = user_display_name_cache[user_id]
128       el1['uid'] = user_id.to_s
129     end
130
131     # make sure nodes are output in sequence_id order
132     ordered_nodes = []
133     self.way_nodes.each do |nd|
134       if visible_nodes
135         # if there is a list of visible nodes then use that to weed out deleted nodes
136         if visible_nodes[nd.node_id]
137           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
138         end
139       else
140         # otherwise, manually go to the db to check things
141         if nd.node and nd.node.visible?
142           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
143         end
144       end
145     end
146
147     ordered_nodes.each do |nd_id|
148       if nd_id and nd_id != '0'
149         e = XML::Node.new 'nd'
150         e['ref'] = nd_id
151         el1 << e
152       end
153     end
154
155     self.way_tags.each do |tag|
156       e = XML::Node.new 'tag'
157       e['k'] = tag.k
158       e['v'] = tag.v
159       el1 << e
160     end
161     return el1
162   end 
163
164   def nds
165     unless @nds
166       @nds = Array.new
167       self.way_nodes.each do |nd|
168         @nds += [nd.node_id]
169       end
170     end
171     @nds
172   end
173
174   def tags
175     unless @tags
176       @tags = {}
177       self.way_tags.each do |tag|
178         @tags[tag.k] = tag.v
179       end
180     end
181     @tags
182   end
183
184   def nds=(s)
185     @nds = s
186   end
187
188   def tags=(t)
189     @tags = t
190   end
191
192   def add_nd_num(n)
193     @nds = Array.new unless @nds
194     @nds << n.to_i
195   end
196
197   def add_tag_keyval(k, v)
198     @tags = Hash.new unless @tags
199
200     # duplicate tags are now forbidden, so we can't allow values
201     # in the hash to be overwritten.
202     raise OSM::APIDuplicateTagsError.new("way", self.id, k) if @tags.include? k
203
204     @tags[k] = v
205   end
206
207   ##
208   # the integer coords (i.e: unscaled) bounding box of the way, assuming
209   # straight line segments.
210   def bbox
211     lons = nodes.collect { |n| n.longitude }
212     lats = nodes.collect { |n| n.latitude }
213     BoundingBox.new(lons.min, lats.min, lons.max, lats.max)
214   end
215
216   def update_from(new_way, user)
217     Way.transaction do
218       self.lock!
219       check_consistency(self, new_way, user)
220       unless new_way.preconditions_ok?(self.nds)
221         raise OSM::APIPreconditionFailedError.new("Cannot update way #{self.id}: data is invalid.")
222       end
223       
224       self.changeset_id = new_way.changeset_id
225       self.changeset = new_way.changeset
226       self.tags = new_way.tags
227       self.nds = new_way.nds
228       self.visible = true
229       save_with_history!
230     end
231   end
232
233   def create_with_history(user)
234     check_create_consistency(self, user)
235     unless self.preconditions_ok?
236       raise OSM::APIPreconditionFailedError.new("Cannot create way: data is invalid.")
237     end
238     self.version = 0
239     self.visible = true
240     save_with_history!
241   end
242
243   def preconditions_ok?(old_nodes = [])
244     return false if self.nds.empty?
245     if self.nds.length > MAX_NUMBER_OF_WAY_NODES
246       raise OSM::APITooManyWayNodesError.new(self.id, self.nds.length, MAX_NUMBER_OF_WAY_NODES)
247     end
248
249     # check only the new nodes, for efficiency - old nodes having been checked last time and can't
250     # be deleted when they're in-use.
251     new_nds = (self.nds - old_nodes).sort.uniq
252
253     unless new_nds.empty?
254       db_nds = Node.where(:id => new_nds, :visible => true)
255
256       if db_nds.length < new_nds.length
257         missing = new_nds - db_nds.collect { |n| n.id }
258         raise OSM::APIPreconditionFailedError.new("Way #{self.id} requires the nodes with id in (#{missing.join(',')}), which either do not exist, or are not visible.")
259       end
260     end
261
262     return true
263   end
264
265   def delete_with_history!(new_way, user)
266     unless self.visible
267       raise OSM::APIAlreadyDeletedError.new("way", new_way.id)
268     end
269     
270     # need to start the transaction here, so that the database can 
271     # provide repeatable reads for the used-by checks. this means it
272     # shouldn't be possible to get race conditions.
273     Way.transaction do
274       self.lock!
275       check_consistency(self, new_way, user)
276       rels = Relation.joins(:relation_members).where(:visible => true, :current_relation_members => { :member_type => "Way", :member_id => id }).order(:id)
277       raise OSM::APIPreconditionFailedError.new("Way #{self.id} is still used by relations #{rels.collect { |r| r.id }.join(",")}.") unless rels.empty?
278
279       self.changeset_id = new_way.changeset_id
280       self.changeset = new_way.changeset
281
282       self.tags = []
283       self.nds = []
284       self.visible = false
285       save_with_history!
286     end
287   end
288
289   # Temporary method to match interface to nodes
290   def tags_as_hash
291     return self.tags
292   end
293
294   ##
295   # if any referenced nodes are placeholder IDs (i.e: are negative) then
296   # this calling this method will fix them using the map from placeholders 
297   # to IDs +id_map+. 
298   def fix_placeholders!(id_map, placeholder_id = nil)
299     self.nds.map! do |node_id|
300       if node_id < 0
301         new_id = id_map[:node][node_id]
302         raise OSM::APIBadUserInput.new("Placeholder node not found for reference #{node_id} in way #{self.id.nil? ? placeholder_id : self.id}") if new_id.nil?
303         new_id
304       else
305         node_id
306       end
307     end
308   end
309
310   private
311   
312   def save_with_history!
313     t = Time.now.getutc
314
315     # update the bounding box, note that this has to be done both before 
316     # and after the save, so that nodes from both versions are included in the 
317     # bbox. we use a copy of the changeset so that it isn't reloaded
318     # later in the save.
319     cs = self.changeset
320     cs.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(:way_id => self.id)
329       tags.each do |k,v|
330         tag = WayTag.new
331         tag.way_id = self.id
332         tag.k = k
333         tag.v = v
334         tag.save!
335       end
336
337       nds = self.nds
338       WayNode.delete_all(:way_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       # reload the way so that the nodes array points to the correct
353       # new set of nodes.
354       self.reload
355
356       # update and commit the bounding box, now that way nodes 
357       # have been updated and we're in a transaction.
358       cs.update_bbox!(bbox) unless nodes.empty?
359
360       # tell the changeset we updated one element only
361       cs.add_changes! 1
362
363       cs.save!
364     end
365   end
366 end