source: subversion/sites/rails_port/app/models/node.rb @ 14586

Revision 14586, 8.7 KB checked in by tomhughes, 5 years ago (diff)

Merge api06 branch to trunk.

Line 
1class Node < ActiveRecord::Base
2  require 'xml/libxml'
3
4  include GeoRecord
5  include ConsistencyValidations
6
7  set_table_name 'current_nodes'
8
9  belongs_to :changeset
10
11  has_many :old_nodes, :foreign_key => :id
12
13  has_many :way_nodes
14  has_many :ways, :through => :way_nodes
15
16  has_many :node_tags, :foreign_key => :id
17 
18  has_many :old_way_nodes
19  has_many :ways_via_history, :class_name=> "Way", :through => :old_way_nodes, :source => :way
20
21  has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
22  has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
23
24  validates_presence_of :id, :on => :update
25  validates_presence_of :timestamp,:version,  :changeset_id
26  validates_uniqueness_of :id
27  validates_inclusion_of :visible, :in => [ true, false ]
28  validates_numericality_of :latitude, :longitude, :changeset_id, :version, :integer_only => true
29  validates_numericality_of :id, :on => :update, :integer_only => true
30  validate :validate_position
31  validates_associated :changeset
32
33  # Sanity check the latitude and longitude and add an error if it's broken
34  def validate_position
35    errors.add_to_base("Node is not in the world") unless in_world?
36  end
37
38  #
39  # Search for nodes matching tags within bounding_box
40  #
41  # Also adheres to limitations such as within max_number_of_nodes
42  #
43  def self.search(bounding_box, tags = {})
44    min_lon, min_lat, max_lon, max_lat = *bounding_box
45    # @fixme a bit of a hack to search for only visible nodes
46    # couldn't think of another to add to tags condition
47    #conditions_hash = tags.merge({ 'visible' => 1 })
48 
49    # using named placeholders http://www.robbyonrails.com/articles/2005/10/21/using-named-placeholders-in-ruby
50    #keys = []
51    #values = {}
52
53    #conditions_hash.each do |key,value|
54    #  keys <<  "#{key} = :#{key}"
55    #  values[key.to_sym] = value
56    #end
57    #conditions = keys.join(' AND ')
58 
59    find_by_area(min_lat, min_lon, max_lat, max_lon,
60                    :conditions => {:visible => true},
61                    :limit => APP_CONFIG['max_number_of_nodes']+1) 
62  end
63
64  # Read in xml as text and return it's Node object representation
65  def self.from_xml(xml, create=false)
66    begin
67      p = XML::Parser.string(xml)
68      doc = p.parse
69
70      doc.find('//osm/node').each do |pt|
71        return Node.from_xml_node(pt, create)
72      end
73    rescue LibXML::XML::Error, ArgumentError => ex
74      raise OSM::APIBadXMLError.new("node", xml, ex.message)
75    end
76  end
77
78  def self.from_xml_node(pt, create=false)
79    node = Node.new
80   
81    raise OSM::APIBadXMLError.new("node", pt, "lat missing") if pt['lat'].nil?
82    raise OSM::APIBadXMLError.new("node", pt, "lon missing") if pt['lon'].nil?
83    node.lat = pt['lat'].to_f
84    node.lon = pt['lon'].to_f
85    raise OSM::APIBadXMLError.new("node", pt, "changeset id missing") if pt['changeset'].nil?
86    node.changeset_id = pt['changeset'].to_i
87
88    raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world?
89
90    # version must be present unless creating
91    raise OSM::APIBadXMLError.new("node", pt, "Version is required when updating") unless create or not pt['version'].nil?
92    node.version = create ? 0 : pt['version'].to_i
93
94    unless create
95      if pt['id'] != '0'
96        node.id = pt['id'].to_i
97      end
98    end
99
100    # visible if it says it is, or as the default if the attribute
101    # is missing.
102    # Don't need to set the visibility, when it is set explicitly in the create/update/delete
103    #node.visible = pt['visible'].nil? or pt['visible'] == 'true'
104
105    # We don't care about the time, as it is explicitly set on create/update/delete
106
107    tags = []
108
109    pt.find('tag').each do |tag|
110      node.add_tag_key_val(tag['k'],tag['v'])
111    end
112
113    return node
114  end
115
116  ##
117  # the bounding box around a node, which is used for determining the changeset's
118  # bounding box
119  def bbox
120    [ longitude, latitude, longitude, latitude ]
121  end
122
123  # Should probably be renamed delete_from to come in line with update
124  def delete_with_history!(new_node, user)
125    unless self.visible
126      raise OSM::APIAlreadyDeletedError.new
127    end
128
129    # need to start the transaction here, so that the database can
130    # provide repeatable reads for the used-by checks. this means it
131    # shouldn't be possible to get race conditions.
132    Node.transaction do
133      check_consistency(self, new_node, user)
134      if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = ? AND current_way_nodes.node_id = ?", true, self.id ])
135        raise OSM::APIPreconditionFailedError.new
136      elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='Node' and member_id=? ", true, self.id])
137        raise OSM::APIPreconditionFailedError.new
138      else
139        self.changeset_id = new_node.changeset_id
140        self.visible = false
141       
142        # update the changeset with the deleted position
143        changeset.update_bbox!(bbox)
144       
145        save_with_history!
146      end
147    end
148  end
149
150  def update_from(new_node, user)
151    check_consistency(self, new_node, user)
152
153    # update changeset first
154    self.changeset_id = new_node.changeset_id
155    self.changeset = new_node.changeset
156
157    # update changeset bbox with *old* position first
158    changeset.update_bbox!(bbox);
159
160    # FIXME logic needs to be double checked
161    self.latitude = new_node.latitude
162    self.longitude = new_node.longitude
163    self.tags = new_node.tags
164    self.visible = true
165
166    # update changeset bbox with *new* position
167    changeset.update_bbox!(bbox);
168
169    save_with_history!
170  end
171 
172  def create_with_history(user)
173    check_create_consistency(self, user)
174    self.version = 0
175    self.visible = true
176
177    # update the changeset to include the new location
178    changeset.update_bbox!(bbox)
179
180    save_with_history!
181  end
182
183  def to_xml
184    doc = OSM::API.new.get_xml_doc
185    doc.root << to_xml_node()
186    return doc
187  end
188
189  def to_xml_node(user_display_name_cache = nil)
190    el1 = XML::Node.new 'node'
191    el1['id'] = self.id.to_s
192    el1['lat'] = self.lat.to_s
193    el1['lon'] = self.lon.to_s
194    el1['version'] = self.version.to_s
195    el1['changeset'] = self.changeset_id.to_s
196   
197    user_display_name_cache = {} if user_display_name_cache.nil?
198
199    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
200      # use the cache if available
201    elsif self.changeset.user.data_public?
202      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
203    else
204      user_display_name_cache[self.changeset.user_id] = nil
205    end
206
207    if not user_display_name_cache[self.changeset.user_id].nil?
208      el1['user'] = user_display_name_cache[self.changeset.user_id]
209      el1['uid'] = self.changeset.user_id.to_s
210    end
211
212    self.tags.each do |k,v|
213      el2 = XML::Node.new('tag')
214      el2['k'] = k.to_s
215      el2['v'] = v.to_s
216      el1 << el2
217    end
218
219    el1['visible'] = self.visible.to_s
220    el1['timestamp'] = self.timestamp.xmlschema
221    return el1
222  end
223
224  def tags_as_hash
225    return tags
226  end
227
228  def tags
229    unless @tags
230      @tags = {}
231      self.node_tags.each do |tag|
232        @tags[tag.k] = tag.v
233      end
234    end
235    @tags
236  end
237
238  def tags=(t)
239    @tags = t
240  end 
241
242  def add_tag_key_val(k,v)
243    @tags = Hash.new unless @tags
244
245    # duplicate tags are now forbidden, so we can't allow values
246    # in the hash to be overwritten.
247    raise OSM::APIDuplicateTagsError.new("node", self.id, k) if @tags.include? k
248
249    @tags[k] = v
250  end
251
252  ##
253  # are the preconditions OK? this is mainly here to keep the duck
254  # typing interface the same between nodes, ways and relations.
255  def preconditions_ok?
256    in_world?
257  end
258
259  ##
260  # dummy method to make the interfaces of node, way and relation
261  # more consistent.
262  def fix_placeholders!(id_map)
263    # nodes don't refer to anything, so there is nothing to do here
264  end
265 
266  private
267
268  def save_with_history!
269    t = Time.now.getutc
270    Node.transaction do
271      self.version += 1
272      self.timestamp = t
273      self.save!
274
275      # Create a NodeTag
276      tags = self.tags
277      NodeTag.delete_all(['id = ?', self.id])
278      tags.each do |k,v|
279        tag = NodeTag.new
280        tag.k = k
281        tag.v = v
282        tag.id = self.id
283        tag.save!
284      end 
285
286      # Create an OldNode
287      old_node = OldNode.from_node(self)
288      old_node.timestamp = t
289      old_node.save_with_dependencies!
290
291      # tell the changeset we updated one element only
292      changeset.add_changes! 1
293
294      # save the changeset in case of bounding box updates
295      changeset.save!
296    end
297  end
298 
299end
Note: See TracBrowser for help on using the repository browser.