X-Git-Url: https://git.openstreetmap.org./rails.git/blobdiff_plain/127bb4523e111bca6b7bb1d2d0125d069a1f6b78..7058d836f75c845b7790c7ab6eb2f75844ce0c1c:/app/models/relation.rb?ds=sidebyside diff --git a/app/models/relation.rb b/app/models/relation.rb index dd2d1c7d5..be990e589 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -182,18 +182,52 @@ class Relation < ActiveRecord::Base def add_tag_keyval(k, v) @tags = Hash.new unless @tags + + # duplicate tags are now forbidden, so we can't allow values + # in the hash to be overwritten. + raise OSM::APIDuplicateTagsError.new if @tags.include? k + @tags[k] = v end def save_with_history! Relation.transaction do + # have to be a little bit clever here - to detect if any tags + # changed then we have to monitor their before and after state. + tags_changed = false + t = Time.now self.version += 1 self.timestamp = t self.save! tags = self.tags - RelationTag.delete_all(['id = ?', self.id]) + self.relation_tags.each do |old_tag| + key = old_tag.k + # if we can match the tags we currently have to the list + # of old tags, then we never set the tags_changed flag. but + # if any are different then set the flag and do the DB + # update. + if tags.has_key? key + # rails 2.1 dirty handling should take care of making this + # somewhat efficient... hopefully... + old_tag.v = tags[key] + tags_changed |= old_tag.changed? + old_tag.save! + + # remove from the map, so that we can expect an empty map + # at the end if there are no new tags + tags.delete key + + else + # this means a tag was deleted + tags_changed = true + RelationTag.delete_all ['id = ? and k = ?', self.id, old_tag.k] + end + end + # if there are left-over tags then they are new and will have to + # be added. + tags_changed |= (not tags.empty?) tags.each do |k,v| tag = RelationTag.new tag.k = k @@ -202,33 +236,97 @@ class Relation < ActiveRecord::Base tag.save! end - members = self.members - RelationMember.delete_all(['id = ?', self.id]) - members.each do |n| + # same pattern as before, but this time we're collecting the + # changed members in an array, as the bounding box updates for + # elements are per-element, not blanked on/off like for tags. + changed_members = Array.new + members = self.members_as_hash + relation_members.each do |old_member| + key = [old_member.member_id.to_s, old_member.member_type] + if members.has_key? key + # i'd love to rely on rails' dirty handling here, but the + # relation members are always dirty because of the member_class + # handling. + if members[key] != old_member.member_role + old_member.member_role = members[key] + changed_members << key + old_member.save! + end + members.delete key + + else + changed_members << key + RelationMember.delete_all ['id = ? and member_id = ? and member_type = ?', self.id, old_member.member_id, old_member.member_type] + end + end + # any remaining members must be new additions + changed_members += members.keys + members.each do |k,v| mem = RelationMember.new mem.id = self.id - mem.member_type = n[0]; - mem.member_id = n[1]; - mem.member_role = n[2]; + mem.member_type = k[1]; + mem.member_id = k[0]; + mem.member_role = v; mem.save! end old_relation = OldRelation.from_relation(self) old_relation.timestamp = t old_relation.save_with_dependencies! + + # update the bbox of the changeset and save it too. + # discussion on the mailing list gave the following definition for + # the bounding box update procedure of a relation: + # + # adding or removing nodes or ways from a relation causes them to be + # added to the changeset bounding box. adding a relation member or + # changing tag values causes all node and way members to be added to the + # bounding box. this is similar to how the map call does things and is + # reasonable on the assumption that adding or removing members doesn't + # materially change the rest of the relation. + any_relations = + changed_members.collect { |id,type| type == "relation" }. + inject(false) { |b,s| b or s } + + if tags_changed or any_relations + # add all non-relation bounding boxes to the changeset + # FIXME: check for tag changes along with element deletions and + # make sure that the deleted element's bounding box is hit. + self.members.each do |type, id, role| + if type != "relation" + update_changeset_element(type, id) + end + end + else + # add only changed members to the changeset + changed_members.each do |id, type| + update_changeset_element(type, id) + end + end + + # save the (maybe updated) changeset bounding box + changeset.save! end end - def delete_with_history(new_relation, user) + ## + # updates the changeset bounding box to contain the bounding box of + # the element with given +type+ and +id+. this only works with nodes + # and ways at the moment, as they're the only elements to respond to + # the :bbox call. + def update_changeset_element(type, id) + element = Kernel.const_get(type.capitalize).find(id) + changeset.update_bbox! element.bbox + end + + def delete_with_history!(new_relation, user) if self.visible check_consistency(self, new_relation, user) - if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", self.id ]) + if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='relation' and member_id=? ", true, self.id ]) raise OSM::APIPreconditionFailedError.new else - #self.user_id = user.id - # FIXME we need to deal with changeset here, which is probably already dealt with self.changeset_id = new_relation.changeset_id - self.tags = [] + self.tags = {} self.members = [] self.visible = false save_with_history! @@ -243,14 +341,22 @@ class Relation < ActiveRecord::Base if !new_relation.preconditions_ok? raise OSM::APIPreconditionFailedError.new end - # FIXME need to deal with changeset etc - #self.user_id = user.id self.changeset_id = new_relation.changeset_id self.tags = new_relation.tags self.members = new_relation.members self.visible = true save_with_history! end + + def create_with_history(user) + check_create_consistency(self, user) + if !self.preconditions_ok? + raise OSM::APIPreconditionFailedError.new + end + self.version = 0 + self.visible = true + save_with_history! + end def preconditions_ok? # These are hastables that store an id in the index of all @@ -305,8 +411,37 @@ class Relation < ActiveRecord::Base return false end + ## + # members in a hash table [id,type] => role + def members_as_hash + h = Hash.new + members.each do |m| + # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays + h[[m[1], m[0]]] = m[2] + end + return h + end + # Temporary method to match interface to nodes def tags_as_hash return self.tags end + + ## + # if any members are referenced by placeholder IDs (i.e: negative) then + # this calling this method will fix them using the map from placeholders + # to IDs +id_map+. + def fix_placeholders!(id_map) + self.members.map! do |type, id, role| + old_id = id.to_i + if old_id < 0 + new_id = id_map[type.to_sym][old_id] + raise "invalid placeholder" if new_id.nil? + [type, new_id, role] + else + [type, id, role] + end + end + end + end