openstreetmap 0.2.1
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.
- data/CHANGELOG +1 -0
- data/LICENSE +7 -0
- data/Manifest +29 -0
- data/README.md +70 -0
- data/Rakefile +39 -0
- data/lib/changeset_callbacks.rb +31 -0
- data/lib/hash.rb +19 -0
- data/lib/open_street_map/api.rb +214 -0
- data/lib/open_street_map/basic_auth_client.rb +15 -0
- data/lib/open_street_map/changeset.rb +93 -0
- data/lib/open_street_map/element.rb +280 -0
- data/lib/open_street_map/errors.rb +55 -0
- data/lib/open_street_map/member.rb +39 -0
- data/lib/open_street_map/node.rb +51 -0
- data/lib/open_street_map/oauth_client.rb +31 -0
- data/lib/open_street_map/parser.rb +123 -0
- data/lib/open_street_map/relation.rb +52 -0
- data/lib/open_street_map/tags.rb +26 -0
- data/lib/open_street_map/user.rb +34 -0
- data/lib/open_street_map/way.rb +84 -0
- data/lib/openstreetmap.rb +31 -0
- data/openstreetmap.gemspec +53 -0
- data/spec/open_street_map/changeset_spec.rb +90 -0
- data/spec/open_street_map/node_spec.rb +87 -0
- data/spec/open_street_map/relation_spec.rb +26 -0
- data/spec/open_street_map/way_spec.rb +77 -0
- data/spec/open_street_map_changeset_spec.rb +136 -0
- data/spec/open_street_map_node_spec.rb +388 -0
- data/spec/open_street_map_way_spec.rb +56 -0
- metadata +188 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
module OpenStreetMap
|
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 OpenStreetMap::Element from API
|
34
|
+
def self.from_api(id, api=OpenStreetMap::API.new) #:nodoc:
|
35
|
+
raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == OpenStreetMap::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 == OpenStreetMap::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 OpenStreetMap::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=OpenStreetMap::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 OpenStreetMap::API object. If none is specified
|
182
|
+
# the default OSM API is used.
|
183
|
+
#
|
184
|
+
# Returns an array of OpenStreetMap::Node, OpenStreetMap::Way, or OpenStreetMap::Relation objects
|
185
|
+
# with all the versions.
|
186
|
+
def get_history_from_api(api=OpenStreetMap::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 = OpenStreetMap::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 OpenStreetMap
|
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 OpenStreetMap is instantiated without a client
|
13
|
+
class CredentialsMissing < StandardError; end
|
14
|
+
|
15
|
+
# This error occurs when OpenStreetMap 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 OpenStreetMap
|
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 OpenStreetMap
|
3
|
+
# OpenStreetMap Node.
|
4
|
+
#
|
5
|
+
# To create a new OpenStreetMap::Node object:
|
6
|
+
# node = OpenStreetMap::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 = OpenStreetMap::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 OpenStreetMap
|
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
|