rosemary 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,280 @@
1
+ module Rosemary
2
+ # This is a virtual parent class for the OSM objects Node, Way and Relation.
3
+ class Element
4
+ include ActiveModel::Validations
5
+
6
+ # Unique ID
7
+ attr_reader :id
8
+
9
+ # The version of this object (as read from file, it
10
+ # is not updated by operations to this object)
11
+ # API 0.6 and above only
12
+ attr_accessor :version
13
+
14
+ # The user who last edited this object (as read from file, it
15
+ # is not updated by operations to this object)
16
+ attr_accessor :user
17
+
18
+ # The user id of the user who last edited this object (as read from file, it
19
+ # is not updated by operations to this object)
20
+ # API 0.6 and above only
21
+ attr_accessor :uid
22
+
23
+ # Last change of this object (as read from file, it is not
24
+ # updated by operations to this object)
25
+ attr_reader :timestamp
26
+
27
+ # The changeset the last change of this object was made with.
28
+ attr_accessor :changeset
29
+
30
+ # Tags for this object
31
+ attr_reader :tags
32
+
33
+ # Get Rosemary::Element from API
34
+ def self.from_api(id, api=Rosemary::API.new) #:nodoc:
35
+ raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == Rosemary::Element
36
+ api.get_object(type, id)
37
+ end
38
+
39
+ def initialize(attrs = {}) #:nodoc:
40
+ raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == Rosemary::Element
41
+ attrs = {'version' => 1, 'uid' => 1}.merge(attrs.stringify_keys!)
42
+ @id = attrs['id'].to_i if attrs['id']
43
+ @version = attrs['version'].to_i
44
+ @uid = attrs['uid'].to_i
45
+ @user = attrs['user']
46
+ @timestamp = Time.parse(attrs['timestamp']) rescue nil
47
+ @changeset = attrs['changeset'].to_i
48
+ @tags = Tags.new
49
+ add_tags(attrs['tag']) if attrs['tag']
50
+ end
51
+
52
+ # Create an error when somebody tries to set the ID.
53
+ # (We need this here because otherwise method_missing will be called.)
54
+ def id=(id) # :nodoc:
55
+ raise NotImplementedError.new('id can not be changed once the object was created')
56
+ end
57
+
58
+ # Set timestamp for this object.
59
+ def timestamp=(timestamp)
60
+ @timestamp = _check_timestamp(timestamp)
61
+ end
62
+
63
+ # The list of attributes for this object
64
+ def attribute_list # :nodoc:
65
+ [:id, :version, :uid, :user, :timestamp, :tags]
66
+ end
67
+
68
+ # Returns a hash of all non-nil attributes of this object.
69
+ #
70
+ # Keys of this hash are <tt>:id</tt>, <tt>:user</tt>,
71
+ # and <tt>:timestamp</tt>. For a Node also <tt>:lon</tt>
72
+ # and <tt>:lat</tt>.
73
+ #
74
+ # call-seq: attributes -> Hash
75
+ #
76
+ def attributes
77
+ attrs = Hash.new
78
+ attribute_list.each do |attribute|
79
+ value = self.send(attribute)
80
+ attrs[attribute] = value unless value.nil?
81
+ end
82
+ attrs
83
+ end
84
+
85
+ # Get tag value
86
+ def [](key)
87
+ tags[key]
88
+ end
89
+
90
+ # Set tag
91
+ def []=(key, value)
92
+ tags[key] = value
93
+ end
94
+
95
+ # Add one or more tags to this object.
96
+ #
97
+ # call-seq: add_tags(Hash) -> OsmObject
98
+ #
99
+ def add_tags(new_tags)
100
+ case new_tags
101
+ when Array # Called with an array
102
+ # Call recursively for each entry
103
+ new_tags.each do |tag_hash|
104
+ add_tags(tag_hash)
105
+ end
106
+ when Hash # Called with a hash
107
+ #check if it is weird {'k' => 'key', 'v' => 'value'} syntax
108
+ if (new_tags.size == 2 && new_tags.keys.include?('k') && new_tags.keys.include?('v'))
109
+ # call recursively with values from k and v keys.
110
+ add_tags({new_tags['k'] => new_tags['v']})
111
+ else
112
+ # OK, this seems to be a proper ruby hash with a single entry
113
+ new_tags.each do |k,v|
114
+ self.tags[k] = v
115
+ end
116
+ end
117
+ end
118
+ self # return self so calls can be chained
119
+ end
120
+
121
+ def update_attributes(attribute_hash)
122
+ dirty = false
123
+ attribute_hash.each do |key,value|
124
+ if self.send(key).to_s != value.to_s
125
+ self.send("#{key}=", value.to_s)
126
+ dirty = true
127
+ end
128
+ end
129
+
130
+ puts "returning #{dirty}"
131
+ dirty
132
+ end
133
+
134
+
135
+ # Has this object any tags?
136
+ #
137
+ # call-seq: is_tagged?
138
+ #
139
+ def is_tagged?
140
+ ! @tags.empty?
141
+ end
142
+
143
+ # Create a new GeoRuby::Shp4r::ShpRecord with the geometry of
144
+ # this object and the given attributes.
145
+ #
146
+ # This only works if the GeoRuby library is included.
147
+ #
148
+ # geom:: Geometry
149
+ # attributes:: Hash with attributes
150
+ #
151
+ # call-seq: shape(attributes) -> GeoRuby::Shp4r::ShpRecord
152
+ #
153
+ # Example:
154
+ # require 'rubygems'
155
+ # require 'geo_ruby'
156
+ # node = Node(nil, nil, nil, 7.84, 54.34)
157
+ # g = node.point
158
+ # node.shape(g, :type => 'Pharmacy', :name => 'Hyde Park Pharmacy')
159
+ #
160
+ def shape(geom, attributes)
161
+ fields = Hash.new
162
+ attributes.each do |key, value|
163
+ fields[key.to_s] = value
164
+ end
165
+ GeoRuby::Shp4r::ShpRecord.new(geom, fields)
166
+ end
167
+
168
+ # Get all relations from the API that have his object as members.
169
+ #
170
+ # The optional parameter is an Rosemary::API object. If none is specified
171
+ # the default OSM API is used.
172
+ #
173
+ # Returns an array of Relation objects or an empty array.
174
+ #
175
+ def get_relations_from_api(api=Rosemary::API.new)
176
+ api.get_relations_referring_to_object(type, self.id.to_i)
177
+ end
178
+
179
+ # Get the history of this object from the API.
180
+ #
181
+ # The optional parameter is an Rosemary::API object. If none is specified
182
+ # the default OSM API is used.
183
+ #
184
+ # Returns an array of Rosemary::Node, Rosemary::Way, or Rosemary::Relation objects
185
+ # with all the versions.
186
+ def get_history_from_api(api=Rosemary::API.new)
187
+ api.get_history(type, self.id.to_i)
188
+ end
189
+
190
+ # All other methods are mapped so its easy to access tags: For
191
+ # instance obj.name is the same as obj.tags['name']. This works
192
+ # for getting and setting tags.
193
+ #
194
+ # node = Rosemary::Node.new
195
+ # node.add_tags( 'highway' => 'residential', 'name' => 'Main Street' )
196
+ # node.highway #=> 'residential'
197
+ # node.highway = 'unclassified' #=> 'unclassified'
198
+ # node.name #=> 'Main Street'
199
+ #
200
+ # In addition methods of the form <tt>key?</tt> are used to
201
+ # check boolean tags. For instance +oneway+ can be 'true' or
202
+ # 'yes' or '1', all meaning the same.
203
+ #
204
+ # way.oneway?
205
+ #
206
+ # will check this. It returns true if the value of this key is
207
+ # either 'true', 'yes' or '1'.
208
+ def method_missing(method, *args)
209
+ methodname = method.to_s
210
+ if methodname.slice(-1, 1) == '='
211
+ if args.size != 1
212
+ raise ArgumentError.new("wrong number of arguments (#{args.size} for 1)")
213
+ end
214
+ tags[methodname.chop] = args[0]
215
+ elsif methodname.slice(-1, 1) == '?'
216
+ if args.size != 0
217
+ raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)")
218
+ end
219
+ tags[methodname.chop] =~ /^(true|yes|1)$/
220
+ else
221
+ if args.size != 0
222
+ raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)")
223
+ end
224
+ tags[methodname]
225
+ end
226
+ end
227
+
228
+ def initialize_copy(from)
229
+ super
230
+ @tags = from.tags.dup
231
+ end
232
+
233
+ private
234
+
235
+ # Return next free ID
236
+ def _next_id
237
+ @@id -= 1
238
+ @@id
239
+ end
240
+
241
+ def _check_id(id)
242
+ if id.kind_of?(Integer)
243
+ return id
244
+ elsif id.kind_of?(String)
245
+ raise ArgumentError, "ID must be an integer" unless id =~ /^-?[0-9]+$/
246
+ return id.to_i
247
+ else
248
+ raise ArgumentError, "ID must be integer or string with integer"
249
+ end
250
+ end
251
+
252
+ def _check_timestamp(timestamp)
253
+ 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}))$/
254
+ raise ArgumentError, "Timestamp is in wrong format (must be 'yyyy-mm-ddThh:mm:ss(Z|[+-]mm:ss)')"
255
+ end
256
+ timestamp
257
+ end
258
+
259
+ def _check_lon(lon)
260
+ if lon.kind_of?(Numeric)
261
+ return lon.to_s
262
+ elsif lon.kind_of?(String)
263
+ return lon
264
+ else
265
+ raise ArgumentError, "'lon' must be number or string containing number"
266
+ end
267
+ end
268
+
269
+ def _check_lat(lat)
270
+ if lat.kind_of?(Numeric)
271
+ return lat.to_s
272
+ elsif lat.kind_of?(String)
273
+ return lat
274
+ else
275
+ raise ArgumentError, "'lat' must be number or string containing number"
276
+ end
277
+ end
278
+
279
+ end
280
+ end
@@ -0,0 +1,55 @@
1
+ module Rosemary
2
+ # Unspecified OSM API error.
3
+ class Error < StandardError
4
+ attr_reader :data
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ super
9
+ end
10
+ end
11
+
12
+ # This error occurs when Rosemary is instantiated without a client
13
+ class CredentialsMissing < StandardError; end
14
+
15
+ # This error occurs when Rosemary has no changeset.
16
+ class ChangesetMissing < StandardError; end
17
+
18
+ # An object was not found in the database.
19
+ class NotFound < Error; end
20
+
21
+ # The API returned HTTP 400 (Bad Request).
22
+ class BadRequest < Error; end # 400
23
+
24
+ # The API operation wasn't authorized. This happens if you didn't set the user and
25
+ # password for a write operation.
26
+ class Unauthorized < Error; end # 401
27
+
28
+ # The object was not found (HTTP 404). Generally means that the object doesn't exist
29
+ # and never has.
30
+ class NotFound < Error; end # 404
31
+
32
+ # If the request is not a HTTP PUT request
33
+ class MethodNotAllowed < Error; end # 405
34
+
35
+ # If the changeset in question has already been closed
36
+ class Conflict < Error; end # 409
37
+
38
+ # The object was not found (HTTP 410), but it used to exist. This generally means
39
+ # that the object existed at some point, but was deleted.
40
+ class Gone < Error; end # 410
41
+
42
+ # When a node is still used by a way
43
+ # When a node is still member of a relation
44
+ # When a way is still member of a relation
45
+ # When a relation is still member of another relation
46
+ class Precondition < Error; end # 412
47
+
48
+ # Unspecified API server error.
49
+ class ServerError < Error; end # 500
50
+
51
+ class Unavailable < Error; end # 503
52
+
53
+ class NotImplemented < Error; end # This method is not implemented yet.
54
+
55
+ end
@@ -0,0 +1,39 @@
1
+ require 'builder'
2
+ module Rosemary
3
+ # A member of an OpenStreetMap Relation.
4
+ class Member
5
+
6
+ # Role this member has in the relationship
7
+ attr_accessor :role
8
+
9
+ # Type of referenced object (can be 'node', 'way', or 'relation')
10
+ attr_reader :type
11
+
12
+ # ID of referenced object
13
+ attr_reader :ref
14
+
15
+ # Create a new Member object. Type can be one of 'node', 'way' or
16
+ # 'relation'. Ref is the ID of the corresponding Node, Way, or
17
+ # Relation. Role is a freeform string and can be empty.
18
+ def initialize(type, ref, role='')
19
+ if type !~ /^(node|way|relation)$/
20
+ raise ArgumentError.new("type must be 'node', 'way', or 'relation'")
21
+ end
22
+ if ref.to_s !~ /^[0-9]+$/
23
+ raise ArgumentError
24
+ end
25
+ @type = type
26
+ @ref = ref.to_i
27
+ @role = role
28
+ end
29
+
30
+ # Return XML for this way. This method uses the Builder library.
31
+ # The only parameter ist the builder object.
32
+ def to_xml(options = {})
33
+ xml = options[:builder] ||= Builder::XmlMarkup.new
34
+ xml.instruct! unless options[:skip_instruct]
35
+ xml.member(:type => type, :ref => ref, :role => role)
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ require 'builder'
2
+ module Rosemary
3
+ # OpenStreetMap Node.
4
+ #
5
+ # To create a new Rosemary::Node object:
6
+ # node = Rosemary::Node.new(:id => "123", :lat => "52.2", :lon => "13.4", :changeset => "12", :user => "fred", :uid => "123", :visible => true, :timestamp => "2005-07-30T14:27:12+01:00")
7
+ #
8
+ # To get a node from the API:
9
+ # node = Rosemary::Node.find(17)
10
+ #
11
+ class Node < Element
12
+ # Longitude in decimal degrees
13
+ attr_accessor :lon
14
+
15
+ # Latitude in decimal degrees
16
+ attr_accessor :lat
17
+
18
+ validates :lat, :presence => true, :numericality => {:greater_than_or_equal_to => -180, :less_than_or_equal_to => 180}
19
+ validates :lon, :presence => true, :numericality => {:greater_than_or_equal_to => -90, :less_than_or_equal_to => 90}
20
+
21
+ # Create new Node object.
22
+ #
23
+ # If +id+ is +nil+ a new unique negative ID will be allocated.
24
+ def initialize(attrs = {})
25
+ attrs.stringify_keys!
26
+ @lon = attrs['lon'].to_f rescue nil
27
+ @lat = attrs['lat'].to_f rescue nil
28
+ super(attrs)
29
+ end
30
+
31
+ def type
32
+ 'Node'
33
+ end
34
+
35
+ # List of attributes for a Node
36
+ def attribute_list
37
+ [:id, :version, :uid, :user, :timestamp, :lon, :lat, :changeset]
38
+ end
39
+
40
+ def to_xml(options = {})
41
+ xml = options[:builder] ||= Builder::XmlMarkup.new
42
+ xml.instruct! unless options[:skip_instruct]
43
+ xml.osm do
44
+ xml.node(attributes) do
45
+ tags.to_xml(:builder => xml, :skip_instruct => true)
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module Rosemary
2
+ class OauthClient
3
+
4
+ attr_reader :access_token
5
+
6
+ def initialize(access_token)
7
+ @access_token = access_token
8
+ end
9
+
10
+ def get(url, header={})
11
+ access_token.get(url, {'Content-Type' => 'application/xml' })
12
+ end
13
+
14
+ def put(url, options={}, header={})
15
+ body = options[:body]
16
+ access_token.put(url, body, {'Content-Type' => 'application/xml' })
17
+ end
18
+
19
+ def delete(url, options={}, header={})
20
+ raise NotImplemented.new("Delete with Oauth and OSM is not supported")
21
+ # body = options[:body]
22
+ # access_token.delete(url, {'Content-Type' => 'application/xml' })
23
+ end
24
+
25
+ def post(url, options={}, header={})
26
+ body = options[:body]
27
+ access_token.post(url, body, {'Content-Type' => 'application/xml' })
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,123 @@
1
+ require 'httparty'
2
+ require 'xml/libxml'
3
+ class Rosemary::Parser < HTTParty::Parser
4
+ include LibXML::XML::SaxParser::Callbacks
5
+
6
+ attr_accessor :context, :description, :lang, :collection
7
+
8
+ def parse
9
+ return nil if body.nil? || body.empty?
10
+ if supports_format?
11
+ self.send(format) # This is a hack, cause the xml format would not be recognized ways, but for nodes and relations
12
+ else
13
+ body
14
+ end
15
+ end
16
+
17
+ def xml
18
+ @parser = LibXML::XML::SaxParser.string(body)
19
+ @parser.callbacks = self
20
+ @parser.parse
21
+ @collection.empty? ? @context : @collection
22
+ end
23
+
24
+ def plain
25
+ body
26
+ end
27
+
28
+ def on_start_document # :nodoc:
29
+ @collection = []
30
+ start_document if respond_to?(:start_document)
31
+ end
32
+
33
+ def on_end_document # :nodoc:
34
+ end_document if respond_to?(:end_document)
35
+ end
36
+
37
+ def on_start_element(name, attr_hash) # :nodoc:
38
+ case name
39
+ when 'node' then _start_node(attr_hash)
40
+ when 'way' then _start_way(attr_hash)
41
+ when 'relation' then _start_relation(attr_hash)
42
+ when 'changeset' then _start_changeset(attr_hash)
43
+ when 'user' then _start_user(attr_hash)
44
+ when 'tag' then _tag(attr_hash)
45
+ when 'nd' then _nd(attr_hash)
46
+ when 'member' then _member(attr_hash)
47
+ when 'home' then _home(attr_hash)
48
+ when 'description' then @description = true
49
+ when 'lang' then @lang = true
50
+ end
51
+ end
52
+
53
+ def on_end_element(name) # :nodoc:
54
+ case name
55
+ when 'description' then @description = false
56
+ when 'lang' then @lang = false
57
+ when 'changeset' then _end_changeset
58
+ end
59
+ end
60
+
61
+ def on_characters(chars)
62
+ if @context.class.name == 'Rosemary::User'
63
+ if @description
64
+ @context.description = chars
65
+ end
66
+ if @lang
67
+ @context.languages << chars
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def _start_node(attr_hash)
75
+ @context = Rosemary::Node.new(attr_hash)
76
+ end
77
+
78
+ def _start_way(attr_hash)
79
+ @context = Rosemary::Way.new(attr_hash)
80
+ end
81
+
82
+ def _start_relation(attr_hash)
83
+ @context = Rosemary::Relation.new(attr_hash)
84
+ end
85
+
86
+ def _start_changeset(attr_hash)
87
+ @context = Rosemary::Changeset.new(attr_hash)
88
+ end
89
+
90
+ def _end_changeset
91
+ @collection << @context
92
+ end
93
+
94
+ def _start_user(attr_hash)
95
+ @context = Rosemary::User.new(attr_hash)
96
+ end
97
+
98
+ def _nd(attr_hash)
99
+ @context << attr_hash['ref']
100
+ end
101
+
102
+ def _tag(attr_hash)
103
+ if respond_to?(:tag)
104
+ return unless tag(@context, attr_hash['k'], attr_value['v'])
105
+ end
106
+ @context.tags.merge!(attr_hash['k'] => attr_hash['v'])
107
+ end
108
+
109
+ def _member(attr_hash)
110
+ new_member = Rosemary::Member.new(attr_hash['type'], attr_hash['ref'], attr_hash['role'])
111
+ if respond_to?(:member)
112
+ return unless member(@context, new_member)
113
+ end
114
+ @context.members << new_member
115
+ end
116
+
117
+ def _home(attr_hash)
118
+ @context.lat = attr_hash['lat'] if attr_hash['lat']
119
+ @context.lon = attr_hash['lon'] if attr_hash['lon']
120
+ @context.lon = attr_hash['zoom'] if attr_hash['zoom']
121
+ end
122
+
123
+ end
@@ -0,0 +1,52 @@
1
+ module Rosemary
2
+ # OpenStreetMap Relation.
3
+ #
4
+ # To create a new Rosemary::Relation object:
5
+ # relation = Rosemary::Relation.new()
6
+ #
7
+ # To get a relation from the API:
8
+ # relation = Rosemary::Relation.find(17)
9
+ #
10
+ class Relation < Element
11
+ # Array of Member objects
12
+ attr_reader :members
13
+
14
+ # Create new Relation object.
15
+ #
16
+ # If +id+ is +nil+ a new unique negative ID will be allocated.
17
+ def initialize(attrs)
18
+ attrs.stringify_keys!
19
+ @members = extract_member(attrs['member'])
20
+ super(attrs)
21
+ end
22
+
23
+ def type
24
+ 'relation'
25
+ end
26
+
27
+ # Return XML for this relation. This method uses the Builder library.
28
+ # The only parameter ist the builder object.
29
+ def to_xml(option = {})
30
+ xml = options[:builder] ||= Builder::XmlMarkup.new
31
+ xml.instruct! unless options[:skip_instruct]
32
+ xml.relation(attributes) do
33
+ members.each do |member|
34
+ member.to_xml(:builder => xml, :skip_instruct => true)
35
+ end
36
+ tags.to_xml(:builder => xml, :skip_instruct => true)
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ def extract_member(member_array)
43
+ return [] unless member_array && member_array.size > 0
44
+
45
+ member_array.inject([]) do |memo, member|
46
+ class_to_instantize = "Rosemary::#{member['type'].classify}".constantize
47
+ memo << class_to_instantize.new(:id => member['ref'])
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ module Rosemary
2
+ # A collection of OSM tags which can be attached to a Node, Way,
3
+ # or Relation.
4
+ # It is a subclass of Hash.
5
+ class Tags < Hash
6
+
7
+ # Return XML for these tags. This method uses the Builder library.
8
+ # The only parameter ist the builder object.
9
+ def to_xml(options = {})
10
+ xml = options[:builder] ||= Builder::XmlMarkup.new
11
+ xml.instruct! unless options[:skip_instruct]
12
+ each do |key, value|
13
+ xml.tag(:k => key, :v => value)
14
+ end unless empty?
15
+ end
16
+
17
+ # Return string with comma separated key=value pairs.
18
+ #
19
+ # call-seq: to_s -> String
20
+ #
21
+ def to_s
22
+ sort.collect{ |k, v| "#{k}=#{v}" }.join(', ')
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ require 'builder'
2
+ module Rosemary
3
+ class User
4
+ # Unique ID
5
+ attr_reader :id
6
+
7
+ # Display name
8
+ attr_reader :display_name
9
+
10
+ # When this user was created
11
+ attr_reader :account_created
12
+
13
+ # A little prosa about this user
14
+ attr_accessor :description
15
+
16
+ # All languages the user can speak
17
+ attr_accessor :languages
18
+
19
+ # Lat/Lon Coordinates of the users home.
20
+ attr_accessor :lat, :lon, :zoom
21
+
22
+ # A picture from this user
23
+ attr_accessor :img
24
+
25
+ def initialize(attrs = {})
26
+ attrs.stringify_keys!
27
+ @id = attrs['id'].to_i if attrs['id']
28
+ @display_name = attrs['display_name']
29
+ @account_created = Time.parse(attrs['account_created']) rescue nil
30
+ @languages = []
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Rosemary
2
+ VERSION = "0.2.2"
3
+ end