1 class Changeset < ActiveRecord::Base
 
   4   belongs_to :user, :counter_cache => true
 
   6   has_many :changeset_tags
 
  13   has_many :old_relations
 
  15   has_many :comments, -> { where(:visible => true).order(:created_at) }, :class_name => "ChangesetComment"
 
  16   has_and_belongs_to_many :subscribers, :class_name => "User", :join_table => "changesets_subscribers", :association_foreign_key => "subscriber_id"
 
  18   validates :id, :uniqueness => true, :presence => { :on => :update },
 
  19                  :numericality => { :on => :update, :integer_only => true }
 
  20   validates :user_id, :presence => true,
 
  21                       :numericality => { :integer_only => true }
 
  22   validates :num_changes, :presence => true,
 
  23                           :numericality => { :integer_only => true,
 
  24                                              :greater_than_or_equal_to => 0 }
 
  25   validates :created_at, :closed_at, :presence => true
 
  26   validates :min_lat, :max_lat, :min_lon, :max_lat, :allow_nil => true,
 
  27                                                     :numericality => { :integer_only => true }
 
  29   before_save :update_closed_at
 
  31   # over-expansion factor to use when updating the bounding box
 
  34   # maximum number of elements allowed in a changeset
 
  37   # maximum time a changeset is allowed to be open for.
 
  40   # idle timeout increment, one hour seems reasonable.
 
  43   # Use a method like this, so that we can easily change how we
 
  44   # determine whether a changeset is open, without breaking code in at
 
  47     # a changeset is open (that is, it will accept further changes) when
 
  48     # it has not yet run out of time and its capacity is small enough.
 
  49     # note that this may not be a hard limit - due to timing changes and
 
  50     # concurrency it is possible that some changesets may be slightly
 
  51     # longer than strictly allowed or have slightly more changes in them.
 
  52     ((closed_at > Time.now.getutc) && (num_changes <= MAX_ELEMENTS))
 
  55   def set_closed_time_now
 
  56     self.closed_at = Time.now.getutc if is_open?
 
  59   def self.from_xml(xml, create = false)
 
  60     p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
 
  63     doc.find("//osm/changeset").each do |pt|
 
  64       return Changeset.from_xml_node(pt, create)
 
  66     fail OSM::APIBadXMLError.new("changeset", xml, "XML doesn't contain an osm/changeset element.")
 
  67   rescue LibXML::XML::Error, ArgumentError => ex
 
  68     raise OSM::APIBadXMLError.new("changeset", xml, ex.message)
 
  71   def self.from_xml_node(pt, create = false)
 
  74       cs.created_at = Time.now.getutc
 
  75       # initial close time is 1h ahead, but will be increased on each
 
  77       cs.closed_at = cs.created_at + IDLE_TIMEOUT
 
  78       # initially we have no changes in a changeset
 
  82     pt.find("tag").each do |tag|
 
  83       fail OSM::APIBadXMLError.new("changeset", pt, "tag is missing key") if tag["k"].nil?
 
  84       fail OSM::APIBadXMLError.new("changeset", pt, "tag is missing value") if tag["v"].nil?
 
  85       cs.add_tag_keyval(tag["k"], tag["v"])
 
  92   # returns the bounding box of the changeset. it is possible that some
 
  93   # or all of the values will be nil, indicating that they are undefined.
 
  95     @bbox ||= BoundingBox.new(min_lon, min_lat, max_lon, max_lat)
 
 103   # expand the bounding box to include the given bounding box. also,
 
 104   # expand a little bit more in the direction of the expansion, so that
 
 105   # further expansions may be unnecessary. this is an optimisation
 
 106   # suggested on the wiki page by kleptog.
 
 107   def update_bbox!(bbox_update)
 
 108     bbox.expand!(bbox_update, EXPAND)
 
 110     # update active record. rails 2.1's dirty handling should take care of
 
 111     # whether this object needs saving or not.
 
 112     self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox.to_a if bbox.complete?
 
 116   # the number of elements is also passed in so that we can ensure that
 
 117   # a single changeset doesn't contain too many elements. this, of course,
 
 118   # destroys the optimisation described in the bbox method above.
 
 119   def add_changes!(elements)
 
 120     self.num_changes += elements
 
 126       changeset_tags.each do |tag|
 
 135   def add_tag_keyval(k, v)
 
 136     @tags = {} unless @tags
 
 138     # duplicate tags are now forbidden, so we can't allow values
 
 139     # in the hash to be overwritten.
 
 140     fail OSM::APIDuplicateTagsError.new("changeset", id, k) if @tags.include? k
 
 146     # do the changeset update and the changeset tags update in the
 
 147     # same transaction to ensure consistency.
 
 148     Changeset.transaction do
 
 152       ChangesetTag.delete_all(:changeset_id => id)
 
 155         tag = ChangesetTag.new
 
 156         tag.changeset_id = id
 
 165   # set the auto-close time to be one hour in the future unless
 
 166   # that would make it more than 24h long, in which case clip to
 
 167   # 24h, as this has been decided is a reasonable time limit.
 
 170       if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
 
 171         self.closed_at = created_at + MAX_TIME_OPEN
 
 173         self.closed_at = Time.now.getutc + IDLE_TIMEOUT
 
 178   def to_xml(include_discussion = false)
 
 179     doc = OSM::API.new.get_xml_doc
 
 180     doc.root << to_xml_node(nil, include_discussion)
 
 184   def to_xml_node(user_display_name_cache = nil, include_discussion = false)
 
 185     el1 = XML::Node.new "changeset"
 
 188     user_display_name_cache = {} if user_display_name_cache.nil?
 
 190     if user_display_name_cache && user_display_name_cache.key?(user_id)
 
 191       # use the cache if available
 
 192     elsif user.data_public?
 
 193       user_display_name_cache[user_id] = user.display_name
 
 195       user_display_name_cache[user_id] = nil
 
 198     el1["user"] = user_display_name_cache[user_id] unless user_display_name_cache[user_id].nil?
 
 199     el1["uid"] = user_id.to_s if user.data_public?
 
 202       el2 = XML::Node.new("tag")
 
 208     el1["created_at"] = created_at.xmlschema
 
 209     el1["closed_at"] = closed_at.xmlschema unless is_open?
 
 210     el1["open"] = is_open?.to_s
 
 212     bbox.to_unscaled.add_bounds_to(el1, "_") if bbox.complete?
 
 214     el1["comments_count"] = comments.count.to_s
 
 216     if include_discussion
 
 217       el2 = XML::Node.new("discussion")
 
 218       comments.includes(:author).each do |comment|
 
 219         el3 = XML::Node.new("comment")
 
 220         el3["date"] = comment.created_at.xmlschema
 
 221         el3["uid"] = comment.author.id.to_s if comment.author.data_public?
 
 222         el3["user"] = comment.author.display_name.to_s if comment.author.data_public?
 
 223         el4 = XML::Node.new("text")
 
 224         el4.content = comment.body.to_s
 
 231     # NOTE: changesets don't include the XML of the changes within them,
 
 232     # they are just structures for tagging. to get the osmChange of a
 
 233     # changeset, see the download method of the controller.
 
 239   # update this instance from another instance given and the user who is
 
 240   # doing the updating. note that this method is not for updating the
 
 241   # bounding box, only the tags of the changeset.
 
 242   def update_from(other, user)
 
 243     # ensure that only the user who opened the changeset may modify it.
 
 244     fail OSM::APIUserChangesetMismatchError.new unless user.id == user_id
 
 246     # can't change a closed changeset
 
 247     fail OSM::APIChangesetAlreadyClosedError.new(self) unless is_open?
 
 249     # copy the other's tags
 
 250     self.tags = other.tags