+ 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
+
+ def save_with_history!
+ t = Time.now.utc
+
+ self.version += 1
+ self.timestamp = t
+
+ 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
+
+ # clone the object before saving it so that the original is
+ # still marked as dirty if we retry the transaction
+ clone.save!
+
+ tags = self.tags.clone
+ 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.key? key
+ tags_changed |= (old_tag.v != tags[key])
+
+ # remove from the map, so that we can expect an empty map
+ # at the end if there are no new tags
+ tags.delete key
+