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