+
+ ##
+ # 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)
+ self.members.map! do |type, id, role|
+ old_id = id.to_i
+ if old_id < 0
+ new_id = id_map[type.downcase.to_sym][old_id]
+ raise OSM::APIBadUserInput.new("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!
+ 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.getutc
+ self.version += 1
+ self.timestamp = t
+ self.save!
+
+ tags = self.tags.clone
+ 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
+ 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
+
+ else
+ # this means a tag was deleted
+ tags_changed = true
+ end
+ end
+ # if there are left-over tags then they are new and will have to
+ # be added.
+ tags_changed |= (not tags.empty?)
+ RelationTag.delete_all(:id => self.id)
+ self.tags.each do |k,v|
+ tag = RelationTag.new
+ tag.k = k
+ tag.v = v
+ tag.id = self.id
+ tag.save!
+ end
+
+ # 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.clone
+ self.relation_members.each do |old_member|
+ key = [old_member.member_type, old_member.member_id, old_member.member_role]
+ i = members.index key
+ if i.nil?
+ changed_members << key
+ else
+ members.delete_at i
+ end
+ end
+ # any remaining members must be new additions
+ changed_members += members
+
+ # update the members. first delete all the old members, as the new
+ # members may be in a different order and i don't feel like implementing
+ # a longest common subsequence algorithm to optimise this.
+ members = self.members
+ RelationMember.delete_all(:id => self.id)
+ members.each_with_index do |m,i|
+ mem = RelationMember.new
+ mem.id = [self.id, i]
+ mem.member_type = m[0]
+ mem.member_id = m[1]
+ mem.member_role = m[2]
+ 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 }
+
+ update_members = 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
+ else
+ changed_members
+ end
+ update_members.each do |type, id, role|
+ if type != "Relation"
+ update_changeset_element(type, id)
+ end
+ end
+
+ # tell the changeset we updated one element only
+ changeset.add_changes! 1
+
+ # save the (maybe updated) changeset bounding box
+ changeset.save!
+ end
+ end
+