+ ##
+ # 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)
+ raise OSM::APIAlreadyDeletedError.new("relation", new_relation.id) unless visible
+
+ # need to start the transaction here, so that the database can
+ # provide repeatable reads for the used-by checks. this means it
+ # shouldn't be possible to get race conditions.
+ Relation.transaction do
+ lock!
+ check_consistency(self, new_relation, user)
+ # This will check to see if this relation is used by another relation
+ rel = RelationMember.joins(:relation).find_by("visible = ? AND member_type = 'Relation' and member_id = ? ", true, id)
+ raise OSM::APIPreconditionFailedError, "The relation #{new_relation.id} is used in relation #{rel.relation.id}." unless rel.nil?
+
+ self.changeset_id = new_relation.changeset_id
+ self.tags = {}
+ self.members = []
+ self.visible = false
+ save_with_history!
+ end
+ end
+
+ def update_from(new_relation, user)
+ Relation.transaction do
+ lock!
+ check_consistency(self, new_relation, user)
+ raise OSM::APIPreconditionFailedError, "Cannot update relation #{id}: data or member data is invalid." unless new_relation.preconditions_ok?(members)
+
+ self.changeset_id = new_relation.changeset_id
+ self.changeset = new_relation.changeset
+ self.tags = new_relation.tags
+ self.members = new_relation.members
+ self.visible = true
+ save_with_history!
+ end
+ end
+
+ def create_with_history(user)
+ check_create_consistency(self, user)
+ raise OSM::APIPreconditionFailedError, "Cannot create relation: data or member data is invalid." unless preconditions_ok?
+
+ self.version = 0
+ self.visible = true
+ save_with_history!
+ end
+
+ def preconditions_ok?(good_members = [])
+ # These are hastables that store an id in the index of all
+ # the nodes/way/relations that have already been added.
+ # If the member is valid and visible then we add it to the
+ # relevant hash table, with the value true as a cache.
+ # Thus if you have nodes with the ids of 50 and 1 already in the
+ # relation, then the hash table nodes would contain:
+ # => {50=>true, 1=>true}
+ elements = { :node => {}, :way => {}, :relation => {} }
+
+ # pre-set all existing members to good
+ good_members.each { |m| elements[m[0].downcase.to_sym][m[1]] = true }
+
+ members.each do |m|
+ # find the hash for the element type or die
+ hash = elements[m[0].downcase.to_sym]
+ return false unless hash
+
+ # unless its in the cache already
+ next if hash.key? m[1]
+
+ # use reflection to look up the appropriate class
+ model = Kernel.const_get(m[0].capitalize)
+ # get the element with that ID. and, if found, lock the element to
+ # ensure it can't be deleted until after the current transaction
+ # commits.
+ element = model.lock("for share").find_by(:id => m[1])
+
+ # and check that it is OK to use.
+ raise OSM::APIPreconditionFailedError, "Relation with id #{id} cannot be saved due to #{m[0]} with id #{m[1]}" unless element&.visible? && element&.preconditions_ok?
+
+ hash[m[1]] = true
+ end
+
+ true
+ 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, placeholder_id = nil)
+ members.map! do |type, id, role|
+ old_id = id.to_i
+ if old_id.negative?
+ new_id = id_map[type.downcase.to_sym][old_id]
+ raise OSM::APIBadUserInput, "Placeholder #{type} not found for reference #{old_id} in relation #{self.id.nil? ? placeholder_id : self.id}." if new_id.nil?
+
+ [type, new_id, role]
+ else
+ [type, id, role]
+ end
+ end
+ end
+
+ private
+