]> git.openstreetmap.org Git - rails.git/blobdiff - app/models/relation.rb
cosmetic tweaks to 'login' and 'create account' screens
[rails.git] / app / models / relation.rb
index 081c44b25d13cc45e1fc0172bfda0411558bc90e..be990e589a2135b0c88b714927ce6824bd29d8cb 100644 (file)
@@ -77,7 +77,7 @@ class Relation < ActiveRecord::Base
     
     if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
+    elsif self.changeset.user.data_public?
       user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
       user_display_name_cache[self.changeset.user_id] = nil
@@ -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