Change to relation.rb to bring element selection in-line with that of way, so to return nil to be caught later, rather than raise an error. Change to way.rb to clarify which way on diff upload.
346 lines
9.5 KiB
Ruby
346 lines
9.5 KiB
Ruby
class Way < ActiveRecord::Base
|
|
require 'xml/libxml'
|
|
|
|
include ConsistencyValidations
|
|
|
|
set_table_name 'current_ways'
|
|
|
|
belongs_to :changeset
|
|
|
|
has_many :old_ways, :foreign_key => 'id', :order => 'version'
|
|
|
|
has_many :way_nodes, :foreign_key => 'id', :order => 'sequence_id'
|
|
has_many :nodes, :through => :way_nodes, :order => 'sequence_id'
|
|
|
|
has_many :way_tags, :foreign_key => 'id'
|
|
|
|
has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
|
|
has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
|
|
|
|
validates_presence_of :id, :on => :update
|
|
validates_presence_of :changeset_id,:version, :timestamp
|
|
validates_uniqueness_of :id
|
|
validates_inclusion_of :visible, :in => [ true, false ]
|
|
validates_numericality_of :changeset_id, :version, :integer_only => true
|
|
validates_numericality_of :id, :on => :update, :integer_only => true
|
|
validates_associated :changeset
|
|
|
|
def self.from_xml(xml, create=false)
|
|
begin
|
|
p = XML::Parser.string(xml)
|
|
doc = p.parse
|
|
|
|
doc.find('//osm/way').each do |pt|
|
|
return Way.from_xml_node(pt, create)
|
|
end
|
|
rescue LibXML::XML::Error, ArgumentError => ex
|
|
raise OSM::APIBadXMLError.new("way", xml, ex.message)
|
|
end
|
|
end
|
|
|
|
def self.from_xml_node(pt, create=false)
|
|
way = Way.new
|
|
|
|
if !create and pt['id'] != '0'
|
|
way.id = pt['id'].to_i
|
|
end
|
|
|
|
way.version = pt['version']
|
|
raise OSM::APIBadXMLError.new("node", pt, "Changeset is required") if pt['changeset'].nil?
|
|
way.changeset_id = pt['changeset']
|
|
|
|
# This next section isn't required for the create, update, or delete of ways
|
|
if create
|
|
way.timestamp = Time.now.getutc
|
|
way.visible = true
|
|
else
|
|
if pt['timestamp']
|
|
way.timestamp = Time.parse(pt['timestamp'])
|
|
end
|
|
# if visible isn't present then it defaults to true
|
|
way.visible = (pt['visible'] or true)
|
|
end
|
|
|
|
pt.find('tag').each do |tag|
|
|
way.add_tag_keyval(tag['k'], tag['v'])
|
|
end
|
|
|
|
pt.find('nd').each do |nd|
|
|
way.add_nd_num(nd['ref'])
|
|
end
|
|
|
|
return way
|
|
end
|
|
|
|
# Find a way given it's ID, and in a single SQL call also grab its nodes
|
|
#
|
|
|
|
# You can't pull in all the tags too unless we put a sequence_id on the way_tags table and have a multipart key
|
|
def self.find_eager(id)
|
|
way = Way.find(id, :include => {:way_nodes => :node})
|
|
#If waytag had a multipart key that was real, you could do this:
|
|
#way = Way.find(id, :include => [:way_tags, {:way_nodes => :node}])
|
|
end
|
|
|
|
# Find a way given it's ID, and in a single SQL call also grab its nodes and tags
|
|
def to_xml
|
|
doc = OSM::API.new.get_xml_doc
|
|
doc.root << to_xml_node()
|
|
return doc
|
|
end
|
|
|
|
def to_xml_node(visible_nodes = nil, changeset_cache = {}, user_display_name_cache = {})
|
|
el1 = XML::Node.new 'way'
|
|
el1['id'] = self.id.to_s
|
|
el1['visible'] = self.visible.to_s
|
|
el1['timestamp'] = self.timestamp.xmlschema
|
|
el1['version'] = self.version.to_s
|
|
el1['changeset'] = self.changeset_id.to_s
|
|
|
|
if changeset_cache.key?(self.changeset_id)
|
|
# use the cache if available
|
|
else
|
|
changeset_cache[self.changeset_id] = self.changeset.user_id
|
|
end
|
|
|
|
user_id = changeset_cache[self.changeset_id]
|
|
|
|
if user_display_name_cache.key?(user_id)
|
|
# use the cache if available
|
|
elsif self.changeset.user.data_public?
|
|
user_display_name_cache[user_id] = self.changeset.user.display_name
|
|
else
|
|
user_display_name_cache[user_id] = nil
|
|
end
|
|
|
|
if not user_display_name_cache[user_id].nil?
|
|
el1['user'] = user_display_name_cache[user_id]
|
|
el1['uid'] = user_id.to_s
|
|
end
|
|
|
|
# make sure nodes are output in sequence_id order
|
|
ordered_nodes = []
|
|
self.way_nodes.each do |nd|
|
|
if visible_nodes
|
|
# if there is a list of visible nodes then use that to weed out deleted nodes
|
|
if visible_nodes[nd.node_id]
|
|
ordered_nodes[nd.sequence_id] = nd.node_id.to_s
|
|
end
|
|
else
|
|
# otherwise, manually go to the db to check things
|
|
if nd.node and nd.node.visible?
|
|
ordered_nodes[nd.sequence_id] = nd.node_id.to_s
|
|
end
|
|
end
|
|
end
|
|
|
|
ordered_nodes.each do |nd_id|
|
|
if nd_id and nd_id != '0'
|
|
e = XML::Node.new 'nd'
|
|
e['ref'] = nd_id
|
|
el1 << e
|
|
end
|
|
end
|
|
|
|
self.way_tags.each do |tag|
|
|
e = XML::Node.new 'tag'
|
|
e['k'] = tag.k
|
|
e['v'] = tag.v
|
|
el1 << e
|
|
end
|
|
return el1
|
|
end
|
|
|
|
def nds
|
|
unless @nds
|
|
@nds = Array.new
|
|
self.way_nodes.each do |nd|
|
|
@nds += [nd.node_id]
|
|
end
|
|
end
|
|
@nds
|
|
end
|
|
|
|
def tags
|
|
unless @tags
|
|
@tags = {}
|
|
self.way_tags.each do |tag|
|
|
@tags[tag.k] = tag.v
|
|
end
|
|
end
|
|
@tags
|
|
end
|
|
|
|
def nds=(s)
|
|
@nds = s
|
|
end
|
|
|
|
def tags=(t)
|
|
@tags = t
|
|
end
|
|
|
|
def add_nd_num(n)
|
|
@nds = Array.new unless @nds
|
|
@nds << n.to_i
|
|
end
|
|
|
|
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("way", self.id, k) if @tags.include? k
|
|
|
|
@tags[k] = v
|
|
end
|
|
|
|
##
|
|
# the integer coords (i.e: unscaled) bounding box of the way, assuming
|
|
# straight line segments.
|
|
def bbox
|
|
lons = nodes.collect { |n| n.longitude }
|
|
lats = nodes.collect { |n| n.latitude }
|
|
[ lons.min, lats.min, lons.max, lats.max ]
|
|
end
|
|
|
|
def update_from(new_way, user)
|
|
check_consistency(self, new_way, user)
|
|
unless new_way.preconditions_ok?
|
|
raise OSM::APIPreconditionFailedError.new("Cannot update way #{self.id}: data is invalid.")
|
|
end
|
|
|
|
self.changeset_id = new_way.changeset_id
|
|
self.changeset = new_way.changeset
|
|
self.tags = new_way.tags
|
|
self.nds = new_way.nds
|
|
self.visible = true
|
|
save_with_history!
|
|
end
|
|
|
|
def create_with_history(user)
|
|
check_create_consistency(self, user)
|
|
unless self.preconditions_ok?
|
|
raise OSM::APIPreconditionFailedError.new("Cannot create way: data is invalid.")
|
|
end
|
|
self.version = 0
|
|
self.visible = true
|
|
save_with_history!
|
|
end
|
|
|
|
def preconditions_ok?
|
|
return false if self.nds.empty?
|
|
if self.nds.length > APP_CONFIG['max_number_of_way_nodes']
|
|
raise OSM::APITooManyWayNodesError.new(self.nds.length, APP_CONFIG['max_number_of_way_nodes'])
|
|
end
|
|
self.nds.each do |n|
|
|
node = Node.find(:first, :conditions => ["id = ?", n])
|
|
unless node and node.visible
|
|
raise OSM::APIPreconditionFailedError.new("Way #{self.id} requires the node with id #{n}, which either does not exist, or is not visible.")
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
def delete_with_history!(new_way, user)
|
|
unless self.visible
|
|
raise OSM::APIAlreadyDeletedError.new("way", new_way.id)
|
|
end
|
|
|
|
# need to start the transaction here, so that the database can
|
|
# provide repeatable reads for the used-by checks. this means it
|
|
# shouldn't be possible to get race conditions.
|
|
Way.transaction do
|
|
check_consistency(self, new_way, user)
|
|
rel = RelationMember.find(:first, :joins => :relation,
|
|
:conditions => [ "visible = ? AND member_type='Way' and member_id=? ", true, self.id])
|
|
raise OSM::APIPreconditionFailedError.new("Way #{self.id} still used by relation #{rel.relation.id}.") if rel
|
|
|
|
self.changeset_id = new_way.changeset_id
|
|
self.changeset = new_way.changeset
|
|
|
|
self.tags = []
|
|
self.nds = []
|
|
self.visible = false
|
|
save_with_history!
|
|
end
|
|
end
|
|
|
|
# Temporary method to match interface to nodes
|
|
def tags_as_hash
|
|
return self.tags
|
|
end
|
|
|
|
##
|
|
# if any referenced nodes are placeholder IDs (i.e: are 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.nds.map! do |node_id|
|
|
if node_id < 0
|
|
new_id = id_map[:node][node_id]
|
|
raise OSM::APIBadUserInput.new("Placeholder node not found for reference #{node_id} in way #{self.id.nil? ? placeholder_id : self.id}") if new_id.nil?
|
|
new_id
|
|
else
|
|
node_id
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def save_with_history!
|
|
t = Time.now.getutc
|
|
|
|
# update the bounding box, note that this has to be done both before
|
|
# and after the save, so that nodes from both versions are included in the
|
|
# bbox. we use a copy of the changeset so that it isn't reloaded
|
|
# later in the save.
|
|
cs = self.changeset
|
|
cs.update_bbox!(bbox) unless nodes.empty?
|
|
|
|
Way.transaction do
|
|
self.version += 1
|
|
self.timestamp = t
|
|
self.save!
|
|
|
|
tags = self.tags
|
|
WayTag.delete_all(['id = ?', self.id])
|
|
tags.each do |k,v|
|
|
tag = WayTag.new
|
|
tag.k = k
|
|
tag.v = v
|
|
tag.id = self.id
|
|
tag.save!
|
|
end
|
|
|
|
nds = self.nds
|
|
WayNode.delete_all(['id = ?', self.id])
|
|
sequence = 1
|
|
nds.each do |n|
|
|
nd = WayNode.new
|
|
nd.id = [self.id, sequence]
|
|
nd.node_id = n
|
|
nd.save!
|
|
sequence += 1
|
|
end
|
|
|
|
old_way = OldWay.from_way(self)
|
|
old_way.timestamp = t
|
|
old_way.save_with_dependencies!
|
|
|
|
# reload the way so that the nodes array points to the correct
|
|
# new set of nodes.
|
|
self.reload
|
|
|
|
# update and commit the bounding box, now that way nodes
|
|
# have been updated and we're in a transaction.
|
|
cs.update_bbox!(bbox) unless nodes.empty?
|
|
|
|
# tell the changeset we updated one element only
|
|
cs.add_changes! 1
|
|
|
|
cs.save!
|
|
end
|
|
end
|
|
|
|
end
|