]> git.openstreetmap.org Git - rails.git/blob - app/models/changeset.rb
b76d0c5a7e1c09f333dc573d0f9e470c9a4860b3
[rails.git] / app / models / changeset.rb
1 class Changeset < ActiveRecord::Base
2   require 'xml/libxml'
3
4   belongs_to :user
5
6   has_many :changeset_tags
7
8   has_many :nodes
9   has_many :ways
10   has_many :relations
11   has_many :old_nodes
12   has_many :old_ways
13   has_many :old_relations
14
15   validates_presence_of :id, :on => :update
16   validates_presence_of :user_id, :created_at, :closed_at, :num_changes
17   validates_uniqueness_of :id
18   validates_numericality_of :id, :on => :update, :integer_only => true
19   validates_numericality_of :min_lat, :max_lat, :min_lon, :max_lat, :allow_nil => true, :integer_only => true
20   validates_numericality_of :user_id,  :integer_only => true
21   validates_numericality_of :num_changes, :integer_only => true, :greater_than_or_equal_to => 0
22
23   before_save :update_closed_at
24
25   # over-expansion factor to use when updating the bounding box
26   EXPAND = 0.1
27
28   # maximum number of elements allowed in a changeset
29   MAX_ELEMENTS = 50000
30
31   # maximum time a changeset is allowed to be open for.
32   MAX_TIME_OPEN = 1.day
33
34   # idle timeout increment, one hour seems reasonable.
35   IDLE_TIMEOUT = 1.hour
36
37   # Use a method like this, so that we can easily change how we
38   # determine whether a changeset is open, without breaking code in at
39   # least 6 controllers
40   def is_open?
41     # a changeset is open (that is, it will accept further changes) when
42     # it has not yet run out of time and its capacity is small enough.
43     # note that this may not be a hard limit - due to timing changes and
44     # concurrency it is possible that some changesets may be slightly
45     # longer than strictly allowed or have slightly more changes in them.
46     return ((closed_at > Time.now.getutc) and (num_changes <= MAX_ELEMENTS))
47   end
48
49   def set_closed_time_now
50     if is_open?
51       self.closed_at = Time.now.getutc
52     end
53   end
54
55   def self.from_xml(xml, create=false)
56     begin
57       p = XML::Parser.string(xml, :options => XML::Parser::Options::NOERROR)
58       doc = p.parse
59
60       doc.find('//osm/changeset').each do |pt|
61         return Changeset.from_xml_node(pt, create)
62       end
63       raise OSM::APIBadXMLError.new("changeset", xml, "XML doesn't contain an osm/changeset element.")
64     rescue LibXML::XML::Error, ArgumentError => ex
65       raise OSM::APIBadXMLError.new("changeset", xml, ex.message)
66     end
67   end
68
69   def self.from_xml_node(pt, create=false)
70     cs = Changeset.new
71     if create
72       cs.created_at = Time.now.getutc
73       # initial close time is 1h ahead, but will be increased on each
74       # modification.
75       cs.closed_at = cs.created_at + IDLE_TIMEOUT
76       # initially we have no changes in a changeset
77       cs.num_changes = 0
78     end
79
80     pt.find('tag').each do |tag|
81       raise OSM::APIBadXMLError.new("changeset", pt, "tag is missing key") if tag['k'].nil?
82       raise OSM::APIBadXMLError.new("changeset", pt, "tag is missing value") if tag['v'].nil?
83       cs.add_tag_keyval(tag['k'], tag['v'])
84     end
85
86     return cs
87   end
88
89   ##
90   # returns the bounding box of the changeset. it is possible that some
91   # or all of the values will be nil, indicating that they are undefined.
92   def bbox
93     @bbox ||= BoundingBox.new(min_lon, min_lat, max_lon, max_lat)
94   end
95
96   def has_valid_bbox?
97     bbox.complete?
98   end
99
100   ##
101   # expand the bounding box to include the given bounding box. also,
102   # expand a little bit more in the direction of the expansion, so that
103   # further expansions may be unnecessary. this is an optimisation
104   # suggested on the wiki page by kleptog.
105   def update_bbox!(bbox_update)
106     bbox.expand!(bbox_update, EXPAND)
107       
108     # update active record. rails 2.1's dirty handling should take care of
109     # whether this object needs saving or not.
110     self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox.to_a if bbox.complete?
111   end
112
113   ##
114   # the number of elements is also passed in so that we can ensure that
115   # a single changeset doesn't contain too many elements. this, of course,
116   # destroys the optimisation described in the bbox method above.
117   def add_changes!(elements)
118     self.num_changes += elements
119   end
120
121   def tags_as_hash
122     return tags
123   end
124
125   def tags
126     unless @tags
127       @tags = {}
128       self.changeset_tags.each do |tag|
129         @tags[tag.k] = tag.v
130       end
131     end
132     @tags
133   end
134
135   def tags=(t)
136     @tags = t
137   end
138
139   def add_tag_keyval(k, v)
140     @tags = Hash.new unless @tags
141
142     # duplicate tags are now forbidden, so we can't allow values
143     # in the hash to be overwritten.
144     raise OSM::APIDuplicateTagsError.new("changeset", self.id, k) if @tags.include? k
145
146     @tags[k] = v
147   end
148
149   def save_with_tags!
150     # do the changeset update and the changeset tags update in the
151     # same transaction to ensure consistency.
152     Changeset.transaction do
153       self.save!
154
155       tags = self.tags
156       ChangesetTag.delete_all(:changeset_id => self.id)
157
158       tags.each do |k,v|
159         tag = ChangesetTag.new
160         tag.changeset_id = self.id
161         tag.k = k
162         tag.v = v
163         tag.save!
164       end
165     end
166   end
167
168   ##
169   # set the auto-close time to be one hour in the future unless
170   # that would make it more than 24h long, in which case clip to
171   # 24h, as this has been decided is a reasonable time limit.
172   def update_closed_at
173     if self.is_open?
174       if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
175         self.closed_at = created_at + MAX_TIME_OPEN
176       else
177         self.closed_at = Time.now.getutc + IDLE_TIMEOUT
178       end
179     end
180   end
181
182   def to_xml
183     doc = OSM::API.new.get_xml_doc
184     doc.root << to_xml_node()
185     return doc
186   end
187
188   def to_xml_node(user_display_name_cache = nil)
189     el1 = XML::Node.new 'changeset'
190     el1['id'] = self.id.to_s
191
192     user_display_name_cache = {} if user_display_name_cache.nil?
193
194     if user_display_name_cache and user_display_name_cache.key?(self.user_id)
195       # use the cache if available
196     elsif self.user.data_public?
197       user_display_name_cache[self.user_id] = self.user.display_name
198     else
199       user_display_name_cache[self.user_id] = nil
200     end
201
202     el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
203     el1['uid'] = self.user_id.to_s if self.user.data_public?
204
205     self.tags.each do |k,v|
206       el2 = XML::Node.new('tag')
207       el2['k'] = k.to_s
208       el2['v'] = v.to_s
209       el1 << el2
210     end
211
212     el1['created_at'] = self.created_at.xmlschema
213     el1['closed_at'] = self.closed_at.xmlschema unless is_open?
214     el1['open'] = is_open?.to_s
215
216     if bbox.complete?
217       bbox.to_unscaled.add_bounds_to(el1, '_')
218     end
219
220     # NOTE: changesets don't include the XML of the changes within them,
221     # they are just structures for tagging. to get the osmChange of a
222     # changeset, see the download method of the controller.
223
224     return el1
225   end
226
227   ##
228   # update this instance from another instance given and the user who is
229   # doing the updating. note that this method is not for updating the
230   # bounding box, only the tags of the changeset.
231   def update_from(other, user)
232     # ensure that only the user who opened the changeset may modify it.
233     unless user.id == self.user_id
234       raise OSM::APIUserChangesetMismatchError.new
235     end
236
237     # can't change a closed changeset
238     unless is_open?
239       raise OSM::APIChangesetAlreadyClosedError.new(self)
240     end
241
242     # copy the other's tags
243     self.tags = other.tags
244
245     save_with_tags!
246   end
247 end