module Rosemary
# This is a virtual parent class for the OSM objects Node, Way and Relation.
class Element
include ActiveModel::Validations
# Unique ID
attr_reader :id
# The version of this object (as read from file, it
# is not updated by operations to this object)
# API 0.6 and above only
attr_accessor :version
# The user who last edited this object (as read from file, it
# is not updated by operations to this object)
attr_accessor :user
# The user id of the user who last edited this object (as read from file, it
# is not updated by operations to this object)
# API 0.6 and above only
attr_accessor :uid
# Last change of this object (as read from file, it is not
# updated by operations to this object)
attr_reader :timestamp
# The changeset the last change of this object was made with.
attr_accessor :changeset
# Tags for this object
attr_reader :tags
# Get Rosemary::Element from API
def self.from_api(id, api=Rosemary::API.new) #:nodoc:
raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == Rosemary::Element
api.get_object(type, id)
end
def initialize(attrs = {}) #:nodoc:
raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == Rosemary::Element
attrs = {'version' => 1, 'uid' => 1}.merge(attrs.stringify_keys!)
@id = attrs['id'].to_i if attrs['id']
@version = attrs['version'].to_i
@uid = attrs['uid'].to_i
@user = attrs['user']
@timestamp = Time.parse(attrs['timestamp']) rescue nil
@changeset = attrs['changeset'].to_i
@tags = Tags.new
add_tags(attrs['tag']) if attrs['tag']
end
# Create an error when somebody tries to set the ID.
# (We need this here because otherwise method_missing will be called.)
def id=(id) # :nodoc:
raise NotImplementedError.new('id can not be changed once the object was created')
end
# Set timestamp for this object.
def timestamp=(timestamp)
@timestamp = _check_timestamp(timestamp)
end
# The list of attributes for this object
def attribute_list # :nodoc:
[:id, :version, :uid, :user, :timestamp, :tags]
end
# Returns a hash of all non-nil attributes of this object.
#
# Keys of this hash are :id, :user,
# and :timestamp. For a Node also :lon
# and :lat.
#
# call-seq: attributes -> Hash
#
def attributes
attrs = Hash.new
attribute_list.each do |attribute|
value = self.send(attribute)
attrs[attribute] = value unless value.nil?
end
attrs
end
# Get tag value
def [](key)
tags[key]
end
# Set tag
def []=(key, value)
tags[key] = value
end
# Add one or more tags to this object.
#
# call-seq: add_tags(Hash) -> OsmObject
#
def add_tags(new_tags)
case new_tags
when Array # Called with an array
# Call recursively for each entry
new_tags.each do |tag_hash|
add_tags(tag_hash)
end
when Hash # Called with a hash
#check if it is weird {'k' => 'key', 'v' => 'value'} syntax
if (new_tags.size == 2 && new_tags.keys.include?('k') && new_tags.keys.include?('v'))
# call recursively with values from k and v keys.
add_tags({new_tags['k'] => new_tags['v']})
else
# OK, this seems to be a proper ruby hash with a single entry
new_tags.each do |k,v|
self.tags[k] = v
end
end
end
self # return self so calls can be chained
end
def update_attributes(attribute_hash)
dirty = false
attribute_hash.each do |key,value|
if self.send(key).to_s != value.to_s
self.send("#{key}=", value.to_s)
dirty = true
end
end
dirty
end
# Has this object any tags?
#
# call-seq: is_tagged?
#
def is_tagged?
! @tags.empty?
end
# Create a new GeoRuby::Shp4r::ShpRecord with the geometry of
# this object and the given attributes.
#
# This only works if the GeoRuby library is included.
#
# geom:: Geometry
# attributes:: Hash with attributes
#
# call-seq: shape(attributes) -> GeoRuby::Shp4r::ShpRecord
#
# Example:
# require 'rubygems'
# require 'geo_ruby'
# node = Node(nil, nil, nil, 7.84, 54.34)
# g = node.point
# node.shape(g, :type => 'Pharmacy', :name => 'Hyde Park Pharmacy')
#
def shape(geom, attributes)
fields = Hash.new
attributes.each do |key, value|
fields[key.to_s] = value
end
GeoRuby::Shp4r::ShpRecord.new(geom, fields)
end
# Get all relations from the API that have his object as members.
#
# The optional parameter is an Rosemary::API object. If none is specified
# the default OSM API is used.
#
# Returns an array of Relation objects or an empty array.
#
def get_relations_from_api(api=Rosemary::API.new)
api.get_relations_referring_to_object(type, self.id.to_i)
end
# Get the history of this object from the API.
#
# The optional parameter is an Rosemary::API object. If none is specified
# the default OSM API is used.
#
# Returns an array of Rosemary::Node, Rosemary::Way, or Rosemary::Relation objects
# with all the versions.
def get_history_from_api(api=Rosemary::API.new)
api.get_history(type, self.id.to_i)
end
# All other methods are mapped so its easy to access tags: For
# instance obj.name is the same as obj.tags['name']. This works
# for getting and setting tags.
#
# node = Rosemary::Node.new
# node.add_tags( 'highway' => 'residential', 'name' => 'Main Street' )
# node.highway #=> 'residential'
# node.highway = 'unclassified' #=> 'unclassified'
# node.name #=> 'Main Street'
#
# In addition methods of the form key? are used to
# check boolean tags. For instance +oneway+ can be 'true' or
# 'yes' or '1', all meaning the same.
#
# way.oneway?
#
# will check this. It returns true if the value of this key is
# either 'true', 'yes' or '1'.
def method_missing(method, *args)
methodname = method.to_s
if methodname.slice(-1, 1) == '='
if args.size != 1
raise ArgumentError.new("wrong number of arguments (#{args.size} for 1)")
end
tags[methodname.chop] = args[0]
elsif methodname.slice(-1, 1) == '?'
if args.size != 0
raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)")
end
tags[methodname.chop] =~ /^(true|yes|1)$/
else
if args.size != 0
raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)")
end
tags[methodname]
end
end
def initialize_copy(from)
super
@tags = from.tags.dup
end
private
# Return next free ID
def _next_id
@@id -= 1
@@id
end
def _check_id(id)
if id.kind_of?(Integer)
return id
elsif id.kind_of?(String)
raise ArgumentError, "ID must be an integer" unless id =~ /^-?[0-9]+$/
return id.to_i
else
raise ArgumentError, "ID must be integer or string with integer"
end
end
def _check_timestamp(timestamp)
if timestamp !~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|([+-][0-9]{2}:[0-9]{2}))$/
raise ArgumentError, "Timestamp is in wrong format (must be 'yyyy-mm-ddThh:mm:ss(Z|[+-]mm:ss)')"
end
timestamp
end
def _check_lon(lon)
if lon.kind_of?(Numeric)
return lon.to_s
elsif lon.kind_of?(String)
return lon
else
raise ArgumentError, "'lon' must be number or string containing number"
end
end
def _check_lat(lat)
if lat.kind_of?(Numeric)
return lat.to_s
elsif lat.kind_of?(String)
return lat
else
raise ArgumentError, "'lat' must be number or string containing number"
end
end
end
end