├── .ruby-version
├── .rspec
├── Gemfile
├── .rvmrc
├── lib
├── rosemary
│ ├── version.rb
│ ├── client.rb
│ ├── note.rb
│ ├── permissions.rb
│ ├── basic_auth_client.rb
│ ├── user.rb
│ ├── tags.rb
│ ├── member.rb
│ ├── bounding_box.rb
│ ├── oauth_client.rb
│ ├── errors.rb
│ ├── node.rb
│ ├── relation.rb
│ ├── way.rb
│ ├── changeset.rb
│ ├── parser.rb
│ ├── element.rb
│ └── api.rb
└── rosemary.rb
├── .codeclimate.yml
├── .travis.yml
├── .gitignore
├── Rakefile
├── Manifest
├── spec
├── spec_helper.rb
├── integration
│ ├── way_spec.rb
│ ├── user_spec.rb
│ ├── note_spec.rb
│ ├── boundary_spec.rb
│ ├── changeset_spec.rb
│ └── node_spec.rb
└── models
│ ├── parser_spec.rb
│ ├── relation_spec.rb
│ ├── changeset_spec.rb
│ ├── way_spec.rb
│ └── node_spec.rb
├── LICENSE
├── CHANGELOG.md
├── rosemary.gemspec
└── README.md
/.ruby-version:
--------------------------------------------------------------------------------
1 | 1.9.3-p547
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 |
3 | gemspec
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | #rvm use 1.8.7@osm-client
2 | rvm use 1.9.2@osm-client
3 |
--------------------------------------------------------------------------------
/lib/rosemary/version.rb:
--------------------------------------------------------------------------------
1 | module Rosemary
2 | # The current version of this gem.
3 | VERSION = "0.4.4"
4 | end
5 |
--------------------------------------------------------------------------------
/lib/rosemary/client.rb:
--------------------------------------------------------------------------------
1 | # Superclass for all clients used to authenticate the user toward the OSM API.
2 | class Rosemary::Client
3 | end
4 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | # Save as .codeclimate.yml (note leading .) in project root directory
2 | languages:
3 | Ruby: true
4 | # exclude_paths:
5 | # - "foo/bar.rb"
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache:
3 | - bundler
4 | sudo: false
5 | rvm:
6 | - 1.9.3
7 | - 2.0.0
8 | - 2.1.0
9 | notifications:
10 | email:
11 | - info@christophbuente.de
12 | slack:
13 | secure: d2DsWIOCSVXX61HMzbOCzNpJN/mzXMowBHyupFGRx9NDg/cxtyza1mo9q+MgPrsoTODTNYV0G0+5YSeA88crzEto+Blf9gXC4PxJ1ifLla1DdcqSq3+mBBTx07bulomsO5++Isi7q5MYXYzr/X5fDxjret2NYaz/3AAQzYrkuZk=
14 | addons:
15 | code_climate:
16 | repo_token: 9b4adcf51537fa2b279a8e9ce948227f84c25288c6758c18f12f4f9e2dd3b0b6
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | *.sw[a-p]
4 | *.tmproj
5 | *.tmproject
6 | *.un~
7 | *~
8 | .DS_Store
9 | .Spotlight-V100
10 | .Trashes
11 | ._*
12 | .bundle
13 | .config
14 | .directory
15 | .elc
16 | .emacs.desktop
17 | .emacs.desktop.lock
18 | .redcar
19 | .yardoc
20 | Desktop.ini
21 | Gemfile.lock
22 | Icon?
23 | InstalledFiles
24 | Session.vim
25 | Thumbs.db
26 | \#*\#
27 | _yardoc
28 | auto-save-list
29 | coverage
30 | doc
31 | lib/bundler/man
32 | pkg
33 | pkg/*
34 | rdoc
35 | spec/reports
36 | test/tmp
37 | test/version_tmp
38 | tmp
39 | tmtags
40 | tramp
41 | vendor
42 |
--------------------------------------------------------------------------------
/lib/rosemary/note.rb:
--------------------------------------------------------------------------------
1 | require 'builder'
2 | module Rosemary
3 | # The note object
4 | class Note
5 | # Unique ID
6 | attr_accessor :id
7 |
8 | attr_accessor :lat
9 | attr_accessor :lon
10 | attr_accessor :text
11 | attr_accessor :user
12 | attr_accessor :action
13 |
14 | def initialize(attrs = {})
15 | attrs.stringify_keys!
16 | @lat = attrs['lat']
17 | @lon = attrs['lon']
18 | @text = attrs['text'] || ''
19 | @user = attrs['user']
20 | @action = attrs['action'] || ''
21 | end
22 |
23 | end
24 | end
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rspec/core'
2 | require 'rspec/core/rake_task'
3 | require 'webmock/rspec'
4 | RSpec::Core::RakeTask.new(:spec) do |spec|
5 | spec.pattern = FileList['spec/**/*_spec.rb']
6 | end
7 |
8 | desc "Open an irb session preloaded with this library"
9 | task :console do
10 | sh "irb -rubygems -I lib -r rosemary.rb"
11 | end
12 |
13 | task :c => :console
14 |
15 | task :default => :spec
16 |
17 | require 'yard'
18 | YARD::Rake::YardocTask.new do |t|
19 | t.files = ['lib/**/*.rb'] # optional
20 | # t.options = ['--any', '--extra', '--opts'] # optional
21 | end
22 |
23 | require "bundler/gem_tasks"
24 |
25 |
--------------------------------------------------------------------------------
/lib/rosemary/permissions.rb:
--------------------------------------------------------------------------------
1 | # The permissions granted to the API user.
2 | class Rosemary::Permissions
3 | include Enumerable
4 |
5 | attr_reader :raw
6 |
7 | def initialize
8 | @raw = []
9 | end
10 |
11 | # make sure we can add permissions and are "Enumerable" via delegation to the permissions array
12 | delegate :<<, :each, :to => :raw
13 |
14 | # some convenience helpers for permissions we already know:
15 | %w(allow_read_prefs allow_write_prefs allow_write_diary
16 | allow_write_api allow_read_gpx allow_write_gpx).each do |name|
17 | define_method("#{name}?") { raw.include?(name) }
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/rosemary.rb:
--------------------------------------------------------------------------------
1 | require "rosemary/version"
2 |
3 | require 'active_model'
4 | require 'rosemary/tags'
5 | require 'rosemary/element'
6 | require 'rosemary/node'
7 | require 'rosemary/note'
8 | require 'rosemary/way'
9 | require 'rosemary/changeset'
10 | require 'rosemary/relation'
11 | require 'rosemary/member'
12 | require 'rosemary/user'
13 | require 'rosemary/bounding_box'
14 | require 'rosemary/permissions'
15 | require 'rosemary/errors'
16 | require 'rosemary/client'
17 | require 'rosemary/basic_auth_client'
18 | require 'rosemary/oauth_client'
19 | require 'rosemary/parser'
20 | require 'rosemary/api'
21 | require 'oauth'
22 | require 'htmlentities'
23 |
24 | # The Rosemary module just handles class load.
25 | module Rosemary
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/Manifest:
--------------------------------------------------------------------------------
1 | CHANGELOG
2 | LICENSE
3 | Manifest
4 | README.md
5 | Rakefile
6 | lib/changeset_callbacks.rb
7 | lib/hash.rb
8 | lib/rosemary/api.rb
9 | lib/rosemary/basic_auth_client.rb
10 | lib/rosemary/changeset.rb
11 | lib/rosemary/element.rb
12 | lib/rosemary/errors.rb
13 | lib/rosemary/member.rb
14 | lib/rosemary/node.rb
15 | lib/rosemary/oauth_client.rb
16 | lib/rosemary/parser.rb
17 | lib/rosemary/relation.rb
18 | lib/rosemary/tags.rb
19 | lib/rosemary/user.rb
20 | lib/rosemary/way.rb
21 | lib/openstreetmap.rb
22 | openstreetmap.gemspec
23 | spec/api_spec.rb
24 | spec/rosemary/changeset_spec.rb
25 | spec/rosemary/node_spec.rb
26 | spec/rosemary/relation_spec.rb
27 | spec/rosemary/way_spec.rb
28 | spec/rosemary_changeset_spec.rb
29 | spec/rosemary_node_spec.rb
30 | spec/rosemary_way_spec.rb
31 |
32 |
--------------------------------------------------------------------------------
/lib/rosemary/basic_auth_client.rb:
--------------------------------------------------------------------------------
1 | class Rosemary::BasicAuthClient < Rosemary::Client
2 |
3 | # The username to be used to authenticate the user against the OMS API
4 | attr_reader :username
5 |
6 | # The password to be used to authenticate the user against the OMS API
7 | attr_reader :password
8 |
9 | def initialize(username, password)
10 | @username = username
11 | @password = password
12 | end
13 |
14 | # The username and password credentials as a Hash
15 | # @return [Hash] the credential hash.
16 | def credentials
17 | {:username => username, :password => password}
18 | end
19 |
20 | # Override inspect message to keep the password from showing up
21 | # in any logfile.
22 | def inspect
23 | "#<#{self.class.name}:#{self.object_id} @username='#{username}'>"
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "codeclimate-test-reporter"
2 | CodeClimate::TestReporter.start
3 | require 'webmock/rspec'
4 | require 'rosemary'
5 | require 'libxml'
6 |
7 | WebMock.disable_net_connect!(:allow => "codeclimate.com")
8 |
9 | RSpec::Matchers.define :have_xml do |xpath, text|
10 | match do |body|
11 | parser = LibXML::XML::Parser.string body
12 | doc = parser.parse
13 | nodes = doc.find(xpath)
14 | expect(nodes).not_to be_empty
15 | if text
16 | nodes.each do |node|
17 | node.content.should == text
18 | end
19 | end
20 | true
21 | end
22 |
23 | failure_message do |body|
24 | "expected to find xml tag #{xpath} in:\n#{body}"
25 | end
26 |
27 | failure_message_when_negated do |response|
28 | "expected not to find xml tag #{xpath} in:\n#{body}"
29 | end
30 |
31 | description do
32 | "have xml tag #{xpath}"
33 | end
34 | end
--------------------------------------------------------------------------------
/lib/rosemary/user.rb:
--------------------------------------------------------------------------------
1 | require 'builder'
2 | module Rosemary
3 | # The user object representing a registered OSM user.
4 | class User
5 | # Unique ID
6 | attr_reader :id
7 |
8 | # Display name
9 | attr_reader :display_name
10 |
11 | # When this user was created
12 | attr_reader :account_created
13 |
14 | # A little prosa about this user
15 | attr_accessor :description
16 |
17 | # All languages the user can speak
18 | attr_accessor :languages
19 |
20 | # Lat/Lon Coordinates of the users home.
21 | attr_accessor :lat, :lon, :zoom
22 |
23 | # A picture from this user
24 | attr_accessor :img
25 |
26 | def initialize(attrs = {})
27 | attrs.stringify_keys!
28 | @id = attrs['id'].to_i if attrs['id']
29 | @display_name = attrs['display_name']
30 | @account_created = Time.parse(attrs['account_created']) rescue nil
31 | @languages = []
32 | end
33 |
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Christoph Bünte
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | rosemary CHANGELOG
2 | ===================================
3 | This file is used to list changes made in each version.
4 |
5 | v0.4.4 (2015 May 29)
6 | ------
7 |
8 | * FIX: #14 Use htmlentities gem to sanitize input for tag values.
9 | * Remove coveralls in favor of codeclimate test coverage reporter
10 | * Exchange PNG badges in favor of SVG badges.
11 |
12 | v0.4.3 (2015 Feb 19)
13 | ------
14 |
15 | * Add "find elements within bounding box" functionality. Thanks to [Leo Roos Version](https://github.com/leoroos)
16 |
17 | v0.4.2 (2014 Aug 7)
18 | ------
19 |
20 | * Allow arbitrary tags to be attached to changeset
21 | * Upgrade all specs to 'expect' syntax instead of 'should'
22 |
23 |
24 | v0.4.1 (2014 May 27)
25 | ------
26 |
27 | * FIX: Return value of notes API call. Thanks to [Aleksandr Zykov](https://github.com/alexandrz)
28 |
29 | v0.4.0 (2014 April 23)
30 | ------
31 |
32 | * Add support for the notes API call. Thanks to [Aleksandr Zykov](https://github.com/alexandrz)
33 |
34 | v0.3.2 (2013 April 11)
35 | ------
36 |
37 | * Strip leading and trailing whitespace of xml tag values
38 |
39 |
40 | v0.1.0
41 | ------
42 |
43 | * First version
--------------------------------------------------------------------------------
/lib/rosemary/tags.rb:
--------------------------------------------------------------------------------
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 | def coder
8 | @coder ||= HTMLEntities.new
9 | end
10 |
11 | # Return XML for these tags. This method uses the Builder library.
12 | # The only parameter ist the builder object.
13 | def to_xml(options = {})
14 | xml = options[:builder] ||= Builder::XmlMarkup.new
15 | xml.instruct! unless options[:skip_instruct]
16 | each do |key, value|
17 | # Remove leading and trailing whitespace from tag values
18 | xml.tag(:k => key, :v => coder.decode(value.strip)) unless value.blank?
19 | end unless empty?
20 | end
21 |
22 | def []=(key, value)
23 | # Ignore empty values, cause we don't want them in the OSM anyways
24 | return if value.blank?
25 | super(key, value)
26 | end
27 |
28 | # Return string with comma separated key=value pairs.
29 | #
30 | # @return [String] string representation
31 | #
32 | def to_s
33 | sort.collect{ |k, v| "#{k}=#{v}" }.join(', ')
34 | end
35 |
36 | end
37 | end
--------------------------------------------------------------------------------
/lib/rosemary/member.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/spec/integration/way_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Way do
4 |
5 | let :osm do
6 | Api.new
7 | end
8 |
9 | def valid_fake_way
10 | way=<<-EOF
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | EOF
28 | end
29 |
30 | describe '#find:' do
31 |
32 | it "should build a Way from API response via get_way" do
33 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/way/1234").to_return(:status => 200, :body => valid_fake_way, :headers => {'Content-Type' => 'application/xml'})
34 | way = osm.find_way(1234)
35 | expect(way.class).to eql Way
36 | expect(way.nodes).to include(15735246)
37 | end
38 | end
39 |
40 | describe '#create:' do
41 | end
42 |
43 | describe '#update:' do
44 | end
45 |
46 | describe '#delete:' do
47 | end
48 | end
--------------------------------------------------------------------------------
/lib/rosemary/bounding_box.rb:
--------------------------------------------------------------------------------
1 | require 'builder'
2 |
3 | module Rosemary
4 | # OpenStreetMap Boundary Box.
5 | #
6 | class BoundingBox < Element
7 |
8 | attr_accessor :nodes, :ways, :relations, :minlat, :minlon, :maxlat, :maxlon
9 |
10 | # Create new Node object.
11 | #
12 | # If +id+ is +nil+ a new unique negative ID will be allocated.
13 | def initialize(attrs = {})
14 | attrs = attrs.dup.stringify_keys!
15 | @minlat = attrs['minlat'].to_f
16 | @minlon = attrs['minlon'].to_f
17 | @maxlat = attrs['maxlat'].to_f
18 | @maxlon = attrs['maxlon'].to_f
19 |
20 | @nodes = []
21 | @ways = []
22 | @relations = []
23 | end
24 |
25 |
26 | def type
27 | 'BoundingBoxBox'
28 | end
29 |
30 | # List of attributes for a bounds element
31 | def attribute_list
32 | [:minlat, :minlon, :maxlat, :maxlon]
33 | end
34 |
35 | def to_xml(options = {})
36 | xml = options[:builder] ||= Builder::XmlMarkup.new
37 | xml.instruct! unless options[:skip_instruct]
38 | xml.osm(:generator => "rosemary v#{Rosemary::VERSION}", :version => Rosemary::Api::API_VERSION) do
39 | xml.bounds(attributes)
40 | [ nodes, ways, relations].each do |elements|
41 | elements.each { |e| e.to_xml(:builder => xml, :skip_instruct => true) }
42 | end
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/rosemary/oauth_client.rb:
--------------------------------------------------------------------------------
1 | class Rosemary::OauthClient < Rosemary::Client
2 |
3 | # The access token to be used for all write access
4 | # @return [OAuth::AccessToken]
5 | attr_reader :access_token
6 |
7 | # @param [OAuth::AccessToken] access_token the access token to be used for write access.
8 | def initialize(access_token)
9 | @access_token = access_token
10 | end
11 |
12 | # Execute a signed OAuth GET request.
13 | # @param [String] url the url to be requested
14 | # @param [Hash] header optional header attributes
15 | def get(url, header={})
16 | access_token.get(url, {'Content-Type' => 'application/xml' })
17 | end
18 |
19 | # Execute a signed OAuth PUT request.
20 | # @param [String] url the url to be requested
21 | # @param [Hash] options optional option attributes
22 | # @param [Hash] header optional header attributes
23 | def put(url, options={}, header={})
24 | body = options[:body]
25 | access_token.put(url, body, {'Content-Type' => 'application/xml' })
26 | end
27 |
28 | # Execute a signed OAuth DELETE request.
29 | #
30 | # Unfortunately the OSM API requires to send an XML
31 | # representation of the Element to be delete in the body
32 | # of the request. The OAuth library does not support sending
33 | # any information in the request body.
34 | # If you know a workaround please fork and improve.
35 | def delete(url, options={}, header={})
36 | raise NotImplemented.new("Delete with Oauth and OSM is not supported")
37 | # body = options[:body]
38 | # access_token.delete(url, {'Content-Type' => 'application/xml' })
39 | end
40 |
41 | # Execute a signed OAuth POST request.
42 | # @param [String] url the url to be requested
43 | # @param [Hash] options optional option attributes
44 | # @param [Hash] header optional header attributes
45 | def post(url, options={}, header={})
46 | body = options[:body]
47 | access_token.post(url, body, {'Content-Type' => 'application/xml' })
48 | end
49 |
50 | end
--------------------------------------------------------------------------------
/lib/rosemary/errors.rb:
--------------------------------------------------------------------------------
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 the request send to the server could not be parsed.
13 | class ParseError < StandardError; end
14 |
15 | # This error occurs when Rosemary is instantiated without a client
16 | class CredentialsMissing < 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 | # You don't have sufficient permissions to make that request.
29 | class Forbidden < Error; end # 403
30 |
31 | # The object was not found (HTTP 404). Generally means that the object doesn't exist
32 | # and never has.
33 | class NotFound < Error; end # 404
34 |
35 | # If the request is not a HTTP PUT request
36 | class MethodNotAllowed < Error; end # 405
37 |
38 | # If the changeset in question has already been closed
39 | class Conflict < Error; end # 409
40 |
41 | # The object was not found (HTTP 410), but it used to exist. This generally means
42 | # that the object existed at some point, but was deleted.
43 | class Gone < Error; end # 410
44 |
45 | # When a node is still used by a way
46 | # When a node is still member of a relation
47 | # When a way is still member of a relation
48 | # When a relation is still member of another relation
49 | class Precondition < Error; end # 412
50 |
51 | # Unspecified API server error.
52 | class ServerError < Error; end # 500
53 |
54 | # When the API service times out or returns an HTTP 503 status.
55 | class Unavailable < Error; end # 503
56 |
57 | # This method is not implemented yet.
58 | class NotImplemented < Error; end
59 |
60 | end
--------------------------------------------------------------------------------
/spec/integration/user_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe User do
4 |
5 | let :consumer do
6 | OAuth::Consumer.new( 'a_key', 'a_secret',
7 | {
8 | :site => 'https://www.openstreetmap.org',
9 | :request_token_path => '/oauth/request_token',
10 | :access_token_path => '/oauth/access_token',
11 | :authorize_path => '/oauth/authorize'
12 | }
13 | )
14 | end
15 |
16 | let :access_token do
17 | OAuth::AccessToken.new(consumer, 'a_token', 'a_secret')
18 | end
19 |
20 | let :osm do
21 | Api.new(OauthClient.new(access_token))
22 | end
23 |
24 | def valid_fake_user
25 | way=<<-EOF
26 |
27 |
28 |
29 | The description of your profile
30 |
31 | de-DE
32 | de
33 | en-US
34 | en
35 |
36 |
37 |
38 | EOF
39 | end
40 |
41 | describe '#find:' do
42 |
43 | it "should build a User from API response via find_user" do
44 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/user/details").to_return(:status => 200, :body => valid_fake_user, :headers => {'Content-Type' => 'application/xml'})
45 | user = osm.find_user
46 | expect(user.class).to eql User
47 | end
48 |
49 | it "should raise error from api" do
50 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/user/details").to_return(:status => 403, :body => "OAuth token doesn't have that capability.", :headers => {'Content-Type' => 'plain/text'})
51 | expect {
52 | osm.find_user
53 | }.to raise_exception Forbidden
54 | end
55 | end
56 | end
57 |
58 |
--------------------------------------------------------------------------------
/lib/rosemary/node.rb:
--------------------------------------------------------------------------------
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 = attrs.dup.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 |
32 | def type
33 | 'Node'
34 | end
35 |
36 | # List of attributes for a Node
37 | def attribute_list
38 | [:id, :version, :uid, :user, :timestamp, :lon, :lat, :changeset]
39 | end
40 |
41 | def to_xml(options = {})
42 | xml = options[:builder] ||= Builder::XmlMarkup.new
43 | xml.instruct! unless options[:skip_instruct]
44 | xml.osm(:generator => "rosemary v#{Rosemary::VERSION}", :version => Rosemary::Api::API_VERSION) do
45 | xml.node(attributes) do
46 | tags.to_xml(:builder => xml, :skip_instruct => true)
47 | end
48 | end
49 | end
50 |
51 | def <=>(another_node)
52 | parent_compare = super(another_node)
53 | # don't bother to compare more stuff if parent comparison failed
54 | return parent_compare unless parent_compare == 0
55 |
56 | return -1 if self.send(:tags) != another_node.send(:tags)
57 |
58 | 0
59 | end
60 |
61 |
62 | end
63 | end
--------------------------------------------------------------------------------
/spec/integration/note_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Note do
4 |
5 | let(:osm) { Api.new }
6 |
7 | def valid_fake_note
8 | note=<<-EOF
9 |
10 |
11 | 174576
12 | https://www.openstreetmap.org/api/0.6/notes/174576
13 | https://www.openstreetmap.org/api/0.6/notes/174576/comment
14 | https://www.openstreetmap.org/api/0.6/notes/174576/close
15 | 2014-05-26 16:00:04 UTC
16 | open
17 |
18 |
19 | 2014-05-26 16:00:04 UTC
20 | 2044077
21 | osmthis
22 | https://www.openstreetmap.org/user/osmthis
23 | opened
24 | Test note
25 | <p>Test note</p>
26 |
27 |
28 |
29 |
30 | EOF
31 | end
32 |
33 | describe 'with BasicAuthClient' do
34 |
35 | let :osm do
36 | Api.new(BasicAuthClient.new('a_username', 'a_password'))
37 | end
38 |
39 | describe '#create_note:' do
40 |
41 | def request_url
42 | "https://a_username:a_password@www.openstreetmap.org/api/0.6/notes?lat=2.1059&lon=102.2205&text=Test%20note"
43 | end
44 |
45 | def stubbed_request
46 | stub_request(:post, request_url)
47 | end
48 |
49 | def valid_note
50 | {lon: 102.2205, lat: 2.1059, text: 'Test note'}
51 | end
52 |
53 | it "should create a new Note from given attributes" do
54 | stubbed_request.
55 | to_return(:status => 200, :body => valid_fake_note, :headers => {'Content-Type' => 'application/xml'})
56 |
57 | new_note = osm.create_note(valid_note)
58 | expect(new_note.id).to eql '174576'
59 | expect(new_note.lon).to eql '102.2205'
60 | expect(new_note.lat).to eql '2.1059'
61 | expect(new_note.text).to eql 'Test note'
62 | expect(new_note.user).to eql 'osmthis'
63 | expect(new_note.action).to eql 'opened'
64 | end
65 | end
66 |
67 | end
68 | end
--------------------------------------------------------------------------------
/lib/rosemary/relation.rb:
--------------------------------------------------------------------------------
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(options = {})
30 | xml = options[:builder] ||= Builder::XmlMarkup.new
31 | xml.instruct! unless options[:skip_instruct]
32 | xml.osm(:generator => "rosemary v#{Rosemary::VERSION}", :version => Rosemary::Api::API_VERSION) do
33 | xml.relation(attributes) do
34 | members.each do |member|
35 | member.to_xml(:builder => xml, :skip_instruct => true)
36 | end
37 | tags.to_xml(:builder => xml, :skip_instruct => true)
38 | end
39 | end
40 | end
41 |
42 | def <=>(another_relation)
43 | parent_compare = super(another_relation)
44 | # don't bother to compare more stuff if parent comparison failed
45 | return parent_compare unless parent_compare == 0
46 |
47 | return -1 if self.send(:tags) != another_relation.send(:tags)
48 |
49 | members_compare = self.send(:members).sort <=> another_relation.send(:members).sort
50 | # don't bother to compare more stuff if nodes comparison failed
51 | return members_compare unless members_compare == 0
52 |
53 | 0
54 | end
55 |
56 |
57 | protected
58 |
59 | def extract_member(member_array)
60 | return [] unless member_array && member_array.size > 0
61 |
62 | member_array.inject([]) do |memo, member|
63 | class_to_instantize = "Rosemary::#{member['type'].classify}".constantize
64 | memo << class_to_instantize.new(:id => member['ref'])
65 | end
66 | end
67 |
68 | end
69 | end
--------------------------------------------------------------------------------
/spec/models/parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Parser do
4 | context "xml" do
5 | it "parses ampersands correctly" do
6 | node_xml =<<-EOF
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | EOF
15 |
16 | n = Parser.call(node_xml, :xml)
17 | expect(n.name).to eql "The rose & the pony"
18 | end
19 |
20 | it "parses utf-8 encoded ampersands correctly" do
21 | node_xml =<<-EOF
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | EOF
30 |
31 | n = Parser.call(node_xml, :xml)
32 | expect(n.name).to eql "The rose & the pony"
33 | end
34 |
35 | it "parses double encoded ampersands correctly" do
36 | node_xml =<<-EOF
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | EOF
45 |
46 | n = Parser.call(node_xml, :xml)
47 | expect(n.name).to eql "The rose & the pony"
48 | end
49 |
50 | it "parses empty set of permissions" do
51 | permissions_xml =<<-EOF
52 |
53 |
54 |
55 |
56 | EOF
57 |
58 | permissions = Parser.call(permissions_xml, :xml)
59 | expect(permissions.raw).to be_empty
60 | end
61 |
62 | it "parses permissions" do
63 | permissions_xml =<<-EOF
64 |
65 |
66 |
67 |
68 |
69 |
70 | EOF
71 |
72 | permissions = Parser.call(permissions_xml, :xml)
73 | expect(permissions.raw.sort).to eql %w(allow_read_prefs allow_write_api)
74 |
75 | expect(permissions.allow_write_api?).to eql true
76 | expect(permissions.allow_read_prefs?).to eql true
77 | expect(permissions.allow_write_prefs?).to eql false
78 | end
79 | end
80 |
81 | end
--------------------------------------------------------------------------------
/rosemary.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "rosemary/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "rosemary"
7 | s.version = Rosemary::VERSION
8 |
9 | s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
10 | s.authors = ["Christoph Bünte, Enno Brehm"]
11 | s.date = Time.now
12 | s.description = "OpenStreetMap API client for ruby"
13 | s.email = ["info@christophbuente.de"]
14 | s.license = 'MIT'
15 | s.extra_rdoc_files = ["CHANGELOG.md", "LICENSE", "README.md"]
16 | s.homepage = "https://github.com/sozialhelden/rosemary"
17 | s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "OpenStreetMap", "--main", "README.md"]
18 | s.require_paths = ["lib"]
19 | s.rubyforge_project = "rosemary"
20 | s.rubygems_version = "1.8.25"
21 |
22 | s.summary = "OpenStreetMap API client for ruby"
23 |
24 | s.files = `git ls-files`.split("\n")
25 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27 | s.require_paths = ["lib"]
28 |
29 | if s.respond_to? :specification_version then
30 | s.specification_version = 3
31 |
32 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
33 | s.add_runtime_dependency(%q, ["~> 0.11.0"])
34 | s.add_runtime_dependency(%q, [">= 2.8.0"])
35 | s.add_runtime_dependency(%q, [">= 2.1.2"])
36 | s.add_runtime_dependency(%q, [">= 0.4.7"])
37 | s.add_runtime_dependency(%q, [">= 3.0.20"])
38 | s.add_runtime_dependency(%q)
39 | s.add_development_dependency(%q, [">= 3.2"])
40 | s.add_development_dependency(%q, [">= 1.21"])
41 | s.add_development_dependency(%q, [">= 10.4.2"])
42 | s.add_development_dependency(%q, [">= 0.8"])
43 | s.add_development_dependency(%q, [">= 3.2.3"])
44 | s.add_development_dependency(%q)
45 | else
46 | s.add_dependency(%q, ["~> 0.11.0"])
47 | s.add_dependency(%q, [">= 2.8.0"])
48 | s.add_dependency(%q, [">= 2.1.2"])
49 | s.add_dependency(%q, [">= 0.4.7"])
50 | s.add_dependency(%q, [">= 3.0.20"])
51 | s.add_dependency(%q)
52 | s.add_dependency(%q, [">= 3.2"])
53 | s.add_dependency(%q, [">= 1.21"])
54 | s.add_dependency(%q, [">= 10.4.2"])
55 | s.add_dependency(%q, [">= 0.8"])
56 | s.add_dependency(%q, [">= 3.2.3"])
57 | s.add_dependency(%q)
58 | end
59 | else
60 | s.add_dependency(%q, ["~> 0.11.0"])
61 | s.add_dependency(%q, [">= 2.8.0"])
62 | s.add_dependency(%q, [">= 2.1.2"])
63 | s.add_dependency(%q, [">= 0.4.7"])
64 | s.add_dependency(%q, [">= 3.0.20"])
65 | s.add_dependency(%q)
66 | s.add_dependency(%q, [">= 3.2"])
67 | s.add_dependency(%q, [">= 1.21"])
68 | s.add_dependency(%q, [">= 10.4.2"])
69 | s.add_dependency(%q, [">= 0.8"])
70 | s.add_dependency(%q, [">= 3.2.3"])
71 | s.add_dependency(%q)
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/rosemary/way.rb:
--------------------------------------------------------------------------------
1 | module Rosemary
2 | # OpenStreetMap Way.
3 | #
4 | # To create a new Rosemary::Way object:
5 | # way = Rosemary::Way.new()
6 | #
7 | # To get a way from the API:
8 | # way = Rosemary::Way.find_way(17)
9 | #
10 | class Way < Element
11 | # Array of node IDs in this way.
12 | # @return [Array]
13 | attr_reader :nodes
14 |
15 | # Create new Way object.
16 | #
17 | # id:: ID of this way. If +nil+ a new unique negative ID will be allocated.
18 | # user:: Username
19 | # timestamp:: Timestamp of last change
20 | # nodes:: Array of Node objects and/or node IDs
21 | def initialize(attrs = {})
22 | attrs.stringify_keys!
23 | @nodes = []
24 | super(attrs)
25 | end
26 |
27 | def type
28 | 'Way'
29 | end
30 |
31 | # Add one or more tags or nodes to this way.
32 | #
33 | # The argument can be one of the following:
34 | #
35 | # * If the argument is a Hash or an OSM::Tags object, those tags are added.
36 | # * If the argument is an OSM::Node object, its ID is added to the list of node IDs.
37 | # * If the argument is an Integer or String containing an Integer, this ID is added to the list of node IDs.
38 | # * If the argument is an Array the function is called recursively, i.e. all items in the Array are added.
39 | #
40 | # Returns the way to allow chaining.
41 | #
42 | # @return [Rosemary::Way] the way itself
43 | #
44 | def <<(stuff)
45 | case stuff
46 | when Array # call this method recursively
47 | stuff.each do |item|
48 | self << item
49 | end
50 | when Rosemary::Node
51 | nodes << stuff.id
52 | when String
53 | nodes << stuff.to_i
54 | when Integer
55 | nodes << stuff
56 | else
57 | tags.merge!(stuff)
58 | end
59 | self # return self to allow chaining
60 | end
61 |
62 |
63 | # The list of attributes for this Way
64 | def attribute_list # :nodoc:
65 | [:id, :version, :uid, :user, :timestamp, :changeset]
66 | end
67 |
68 | # Instantiate a way from an XML representation.
69 | # @param [String] xml_string the xml representing a way.
70 | # @return [Rosemary::Way] the way represented by the given XML string.
71 | def self.from_xml(xml_string)
72 | Parser.call(xml_string, :xml)
73 | end
74 |
75 | def to_xml(options = {})
76 | xml = options[:builder] ||= Builder::XmlMarkup.new
77 | xml.instruct! unless options[:skip_instruct]
78 | xml.osm(:generator => "rosemary v#{Rosemary::VERSION}", :version => Rosemary::Api::API_VERSION) do
79 | xml.way(attributes) do
80 | nodes.each do |node_id|
81 | xml.nd(:ref => node_id)
82 | end unless nodes.empty?
83 | tags.to_xml(:builder => xml, :skip_instruct => true)
84 | end
85 | end
86 | end
87 |
88 | def <=>(another_way)
89 | parent_compare = super(another_way)
90 | # don't bother to compare more stuff if parent comparison failed
91 | return parent_compare unless parent_compare == 0
92 |
93 | return -1 if self.send(:tags) != another_way.send(:tags)
94 |
95 | nodes_compare = self.send(:nodes).sort <=> another_way.send(:nodes).sort
96 | # don't bother to compare more stuff if nodes comparison failed
97 | return nodes_compare unless nodes_compare == 0
98 |
99 | 0
100 | end
101 | end
102 | end
--------------------------------------------------------------------------------
/lib/rosemary/changeset.rb:
--------------------------------------------------------------------------------
1 | require 'builder'
2 | module Rosemary
3 | # Changeset is used in OpenStreetMap to bundle several changes into a kind of "commit"
4 | class Changeset
5 | # Unique ID
6 | # @return [Fixnum]
7 | attr_reader :id
8 |
9 | # The user who last edited this object (as read from file, it is not updated by operations to this object)
10 | # @return [Rosemary::User] the user who last edited this object
11 | attr_accessor :user
12 |
13 | # The user id of the user who last edited this object(as read from file, it
14 | # is not updated by operations to this object) API 0.6 and above only
15 | # @return [Fixnum] the user id of the user who last edited this object
16 | attr_accessor :uid
17 |
18 | # @return [Boolean] is this changeset is still open.
19 | attr_accessor :open
20 |
21 | # @return [Date] creation date of this changeset
22 | attr_accessor :created_at
23 |
24 | # @return [Date] when the changeset was closed
25 | attr_accessor :closed_at
26 |
27 | # Bounding box surrounding all changes made in this changeset
28 | # @return [Float]
29 | attr_accessor :min_lat, :min_lon, :max_lat, :max_lon
30 |
31 | # Tags for this object
32 | # @return [Hash]
33 | attr_reader :tags
34 |
35 | def initialize(attrs = {}) #:nodoc:
36 | attrs = attrs.dup.stringify_keys!
37 | @id = attrs['id'].to_i if attrs['id']
38 | @uid = attrs['uid'].to_i
39 | @user = attrs['user']
40 | @created_at = Time.parse(attrs['created_at']) rescue nil
41 | @closed_at = Time.parse(attrs['closed_at']) rescue nil
42 | @open = attrs['open']
43 | tags = attrs['tags'] || {}
44 | @tags = Tags.new.merge(tags.dup.stringify_keys!)
45 | @tags['created_by'] = "rosemary v#{Rosemary::VERSION}"
46 | @min_lat = attrs['min_lat'].to_f
47 | @min_lon = attrs['min_lon'].to_f
48 | @max_lat = attrs['max_lat'].to_f
49 | @max_lon = attrs['max_lon'].to_f
50 |
51 | end
52 |
53 | # Set timestamp for this object.
54 | def created_at=(timestamp)
55 | @created_at = Time.parse(timestamp)
56 | end
57 |
58 | # Is this changeset still open?
59 | def open?
60 | ["yes", "1", "t", "true"].include?(open)
61 | end
62 |
63 | # List of attributes for a Changeset
64 | # @return [Array]
65 | def attribute_list
66 | [:id, :user, :uid, :open, :created_at, :closed_at, :min_lat, :max_lat, :min_lon, :max_lon]
67 | end
68 |
69 | # A hash of all non-nil attributes of this object.
70 | # Keys of this hash are :id, :user,
71 | # and :timestamp. For a Node also :lon
72 | # and :lat.
73 | #
74 | # @return [Hash] a hash of all non-nil attributes of this object.
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 | # Renders the object as an xml representation compatible to the OSM API
86 | # @return [String] XML
87 | def to_xml(options = {})
88 | xml = options[:builder] ||= Builder::XmlMarkup.new
89 | xml.instruct! unless options[:skip_instruct]
90 | xml.osm do
91 | xml.changeset(attributes) do
92 | tags.each do |k,v|
93 | xml.tag(:k => k, :v => v)
94 | end unless tags.empty?
95 | end
96 | end
97 | end
98 |
99 | end
100 | end
--------------------------------------------------------------------------------
/spec/models/relation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Relation do
4 |
5 | subject do
6 | Relation.new(:id => "123",
7 | :lat => "52.2",
8 | :lon => "13.4",
9 | :changeset => "12",
10 | :user => "fred",
11 | :uid => "123",
12 | :visible => true,
13 | :timestamp => "2005-07-30T14:27:12+01:00",
14 | :member => [
15 | {"type"=>"relation", "ref"=>"1628007", "role"=>"outer"},
16 | {"type"=>"way", "ref"=>"50197015", "role"=>""}
17 | ])
18 | end
19 |
20 | it { should be_valid }
21 |
22 | it "should have members" do
23 | expect(subject.members.size).to eql 2
24 | end
25 |
26 | it "should compare identity depending on tags and attributes" do
27 | first_relation = Relation.new('id' => 123, 'changeset' => '123', 'version' => 1, 'user' => 'horst', 'uid' => '123', 'timestamp' => '2005-07-30T14:27:12+01:00')
28 | first_relation.tags[:name] = 'Black horse'
29 | second_relation = Relation.new('id' => 123, 'changeset' => '123', 'version' => 1, 'user' => 'horst', 'uid' => '123', 'timestamp' => '2005-07-30T14:27:12+01:00')
30 | second_relation.tags[:name] = 'Black horse'
31 | expect(first_relation == second_relation).to eql true
32 | end
33 |
34 | it "should not be equal when id does not match" do
35 | first_relation = Relation.new('id' => 123)
36 | second_relation = Relation.new('id' => 234)
37 | expect(first_relation).not_to eql second_relation
38 | end
39 |
40 | it "should not be equal when changeset does not match" do
41 | first_relation = Relation.new('changeset' => 123)
42 | second_relation = Relation.new('changeset' => 234)
43 | expect(first_relation).not_to eql second_relation
44 | end
45 |
46 | it "should not be equal when version does not match" do
47 | first_relation = Relation.new('version' => 1)
48 | second_relation = Relation.new('version' => 2)
49 | expect(first_relation).not_to eql second_relation
50 | end
51 |
52 | it "should not be equal when user does not match" do
53 | first_relation = Relation.new('user' => 'horst')
54 | second_relation = Relation.new('user' => 'jack')
55 | expect(first_relation).not_to eql second_relation
56 | end
57 |
58 | it "should not be equal when uid does not match" do
59 | first_relation = Relation.new('uid' => 123)
60 | second_relation = Relation.new('uid' => 234)
61 | expect(first_relation).not_to eql second_relation
62 | end
63 |
64 | it "should not be equal when timestamp does not match" do
65 | first_relation = Relation.new('timestamp' => '2005-07-30T14:27:12+01:00')
66 | second_relation = Relation.new('timestamp' => '2006-07-30T14:27:12+01:00')
67 | expect(first_relation).not_to eql second_relation
68 | end
69 |
70 | it "should not be equal when members do not match" do
71 | first_relation = Relation.new('id' => 123)
72 | first_relation.members << 1
73 | first_relation.members << 2
74 | second_relation = Relation.new('id' => 123)
75 | second_relation.members << 1
76 | second_relation.members << 3
77 | expect(first_relation).not_to eql second_relation
78 | end
79 |
80 | it "should not be equal when tags do not match" do
81 | first_relation = Relation.new('id' => 123)
82 | first_relation.tags[:name] = 'black horse'
83 | second_relation = Relation.new('id' => 123)
84 | second_relation.tags[:name] = 'white horse'
85 | expect(first_relation).not_to eql second_relation
86 | end
87 |
88 | end
--------------------------------------------------------------------------------
/spec/models/changeset_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Changeset do
4 |
5 | let :changeset do
6 | Changeset.new( :id => "123",
7 | :user => "fred",
8 | :uid => "123",
9 | :created_at => "2008-11-08T19:07:39+01:00",
10 | :open => "true",
11 | :min_lat => "52.2",
12 | :max_lat => "52.3",
13 | :min_lon => "13.4",
14 | :max_lon => "13.5",
15 | :tags => { :comment => 'A bloody comment' })
16 | end
17 |
18 | context 'attributes' do
19 |
20 | subject { changeset }
21 |
22 | it "should have an id attribute set from attributes" do
23 | expect(subject.id).to eql(123)
24 | end
25 |
26 | it "should have a user attributes set from attributes" do
27 | expect(subject.user).to eql("fred")
28 | end
29 |
30 | it "should have an uid attribute set from attributes" do
31 | expect(subject.uid).to eql(123)
32 | end
33 |
34 | it "should have a changeset attributes set from attributes" do
35 | expect(subject).to be_open
36 | end
37 |
38 | it "should have a min_lat attribute set from attributes" do
39 | expect(subject.min_lat).to eql(52.2)
40 | end
41 |
42 | it "should have a min_lon attribute set from attributes" do
43 | expect(subject.min_lon).to eql(13.4)
44 | end
45 |
46 | it "should have a max_lat attribute set from attributes" do
47 | expect(subject.max_lat).to eql(52.3)
48 | end
49 |
50 | it "should have a max_lon attribute set from attributes" do
51 | expect(subject.max_lon).to eql(13.5)
52 | end
53 |
54 | it "should have a created_at attribute set from attributes" do
55 | expect(subject.created_at).to eql Time.parse('2008-11-08T19:07:39+01:00')
56 | end
57 |
58 | it "should have a comment attribute set from attributes" do
59 | expect(subject.tags['comment']).to eql 'A bloody comment'
60 | end
61 | end
62 |
63 | context 'xml representation' do
64 |
65 | subject { changeset.to_xml }
66 |
67 | it "should have an id attribute within xml representation" do
68 | expect(subject).to have_xml "//changeset[@id='123']"
69 | end
70 |
71 | it "should have a user attribute within xml representation" do
72 | expect(subject).to have_xml "//changeset[@user='fred']"
73 | end
74 |
75 | it "should have an uid attribute within xml representation" do
76 | expect(subject).to have_xml "//changeset[@uid='123']"
77 | end
78 |
79 | it "should have an open attribute within xml representation" do
80 | expect(subject).to have_xml "//changeset[@open='true']"
81 | end
82 |
83 | it "should have a min_lat attribute within xml representation" do
84 | expect(subject).to have_xml "//changeset[@min_lat='52.2']"
85 | end
86 |
87 | it "should have a min_lon attribute within xml representation" do
88 | expect(subject).to have_xml "//changeset[@min_lon='13.4']"
89 | end
90 |
91 | it "should have a max_lat attribute within xml representation" do
92 | expect(subject).to have_xml "//changeset[@max_lat='52.3']"
93 | end
94 |
95 | it "should have a max_lon attribute within xml representation" do
96 | expect(subject).to have_xml "//changeset[@max_lon='13.5']"
97 | end
98 |
99 | it "should have a created_at attribute within xml representation" do
100 | expect(subject).to have_xml "//changeset[@created_at=\'#{Time.parse('2008-11-08T19:07:39+01:00')}\']"
101 | end
102 |
103 | it "should have a comment tag within xml representation" do
104 | expect(subject).to have_xml "//tag[@k='comment'][@v='A bloody comment']"
105 | end
106 |
107 | end
108 | end
--------------------------------------------------------------------------------
/spec/models/way_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Way do
4 |
5 | def valid_fake_way
6 | way=<<-EOF
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | EOF
24 | end
25 |
26 | subject do
27 | @way ||= Way.from_xml(valid_fake_way)
28 | end
29 |
30 | it "should have 11 nodes" do
31 | expect(subject.nodes.size).to eql 11
32 | expect(subject.nodes.first).to eql 15735248
33 | end
34 |
35 | it "should have node referenzes in xml representation" do
36 | expect(subject.to_xml).to match /ref=\"15735248\"/
37 | end
38 |
39 |
40 | it "should have an id attribute set from attributes" do
41 | expect(subject.id).to eql(1234)
42 | end
43 |
44 | it "should have an id attribute within xml representation" do
45 | expect(subject.to_xml).to match /id=\"1234\"/
46 | end
47 |
48 | it "should have a user attributes set from attributes" do
49 | expect(subject.user).to eql("fred")
50 | end
51 |
52 | it "should have a user attribute within xml representation" do
53 | expect(subject.to_xml).to match /user=\"fred\"/
54 | end
55 |
56 | it "should have a changeset attributes set from attributes" do
57 | expect(subject.changeset).to eql(12)
58 | end
59 |
60 | it "should have a changeset attribute within xml representation" do
61 | expect(subject.to_xml).to match /changeset=\"12\"/
62 | end
63 |
64 | it "should have a uid attribute set from attributes" do
65 | expect(subject.uid).to eql(123)
66 | end
67 |
68 | it "should have a uid attribute within xml representation" do
69 | expect(subject.to_xml).to match /uid=\"123\"/
70 | end
71 |
72 | it "should have a version attribute for osm tag" do
73 | expect(subject.to_xml).to match /version=\"0.6\"/
74 | end
75 |
76 | it "should have a generator attribute for osm tag" do
77 | expect(subject.to_xml).to match /generator=\"rosemary v/
78 | end
79 |
80 | it "should produce xml" do
81 | subject.add_tags(:wheelchair => 'yes')
82 | expect(subject.to_xml).to match /k=\"wheelchair\"/
83 | expect(subject.to_xml).to match /v=\"yes\"/
84 | end
85 |
86 | it "should not add tags with empty value to xml" do
87 | subject.add_tags(:wheelchair => '')
88 | expect(subject.to_xml).not_to match /k=\"wheelchair\"/
89 | end
90 |
91 | it "should compare identity depending on tags and attributes" do
92 | first_way = Way.new('id' => 123, 'changeset' => '123', 'version' => 1, 'user' => 'horst', 'uid' => '123', 'timestamp' => '2005-07-30T14:27:12+01:00')
93 | first_way.tags[:name] = 'Black horse'
94 | second_way = Way.new('id' => 123, 'changeset' => '123', 'version' => 1, 'user' => 'horst', 'uid' => '123', 'timestamp' => '2005-07-30T14:27:12+01:00')
95 | second_way.tags[:name] = 'Black horse'
96 | expect(first_way == second_way).to eql true
97 | end
98 |
99 | it "should not be equal when id does not match" do
100 | first_way = Way.new('id' => 123)
101 | second_way = Way.new('id' => 234)
102 | expect(first_way == second_way).not_to eql true
103 | end
104 |
105 | it "should not be equal when changeset does not match" do
106 | first_way = Way.new('changeset' => 123)
107 | second_way = Way.new('changeset' => 234)
108 | expect(first_way == second_way).not_to eql true
109 | end
110 |
111 | it "should not be equal when version does not match" do
112 | first_way = Way.new('version' => 1)
113 | second_way = Way.new('version' => 2)
114 | expect(first_way == second_way).not_to eql true
115 | end
116 |
117 | it "should not be equal when user does not match" do
118 | first_way = Way.new('user' => 'horst')
119 | second_way = Way.new('user' => 'jack')
120 | expect(first_way == second_way).not_to eql true
121 | end
122 |
123 | it "should not be equal when uid does not match" do
124 | first_way = Way.new('uid' => 123)
125 | second_way = Way.new('uid' => 234)
126 | expect(first_way == second_way).not_to eql true
127 | end
128 |
129 | it "should not be equal when timestamp does not match" do
130 | first_way = Way.new('timestamp' => '2005-07-30T14:27:12+01:00')
131 | second_way = Way.new('timestamp' => '2006-07-30T14:27:12+01:00')
132 | expect(first_way == second_way).not_to eql true
133 | end
134 |
135 | it "should not be equal when nodes do not match" do
136 | first_way = Way.new('id' => 123)
137 | first_way.nodes << 1
138 | first_way.nodes << 2
139 | second_way = Way.new('id' => 123)
140 | second_way.nodes << 1
141 | second_way.nodes << 3
142 | expect(first_way == second_way).not_to eql true
143 | end
144 |
145 | it "should not be equal when tags do not match" do
146 | first_way = Way.new('id' => 123)
147 | first_way.tags[:name] = 'black horse'
148 | second_way = Way.new('id' => 123)
149 | second_way.tags[:name] = 'white horse'
150 | expect(first_way == second_way).not_to eql true
151 | end
152 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rosemary: OpenStreetMap for Ruby
2 |
3 | [](http://badge.fury.io/rb/rosemary)
4 | [](https://travis-ci.org/sozialhelden/rosemary)
5 | [](https://gemnasium.com/sozialhelden/rosemary)
6 | [](https://codeclimate.com/github/sozialhelden/rosemary/coverage)
7 | [](https://codeclimate.com/github/sozialhelden/rosemary)
8 | [ ](https://github.com/sozialhelden/rosemary/blob/master/LICENSE)
9 | [](https://gittip.com/sozialhelden)
10 |
11 | This ruby gem is an API client for the current OpenStreetMap [API v0.6](http://wiki.openstreetmap.org/wiki/API_v0.6). It provides easy access to OpenStreetMap (OSM) data.
12 |
13 | ## What is OpenStreetMap?
14 |
15 | OpenStreetMap (OSM) is a collaborative project to create a free editable map of the world. Two major driving forces behind the establishment and growth of OSM have been restrictions on use or availability of map information across much of the world and the advent of inexpensive portable GPS devices.
16 |
17 |
18 | ## The OpenStreetMap Database
19 |
20 | OpenStreetMap data is published under an open content license, with the intention of promoting free use and re-distribution of the data (both commercial and non-commercial). The license currently used is the [Creative Commons Attribution-Share Alike 2.0 licence](http://creativecommons.org/licenses/by-sa/2.0/); however, legal investigation work and community consultation is underway to relicense the project under the [Open Database License (ODbL)](http://opendatacommons.org/licenses/odbl/) from [Open Data Commons (ODC)](http://opendatacommons.org/), claimed to be more suitable for a map data set.
21 |
22 | ## Input Data
23 |
24 | All data added to the project need to have a license compatible with the Creative Commons Attribution-Share Alike license. This can include out of copyright information, public domain or other licenses. All contributors must register with the project and agree to provide data on a Creative Commons CC-BY-SA 2.0 licence, or determine that the licensing of the source data is suitable; this may involve examining licences for government data to establish whether they are compatible.
25 | Due to the license switch, data added in future must be compatible with both the Open Database License and the new Contributor Terms in order to be accepted.
26 |
27 | ## Installation
28 |
29 | Put this in your Gemfile
30 |
31 | ``` ruby
32 | # Gemfile
33 | gem 'rosemary'
34 | ```
35 |
36 | Then run
37 |
38 | ``` bash
39 | bundle install
40 | ```
41 |
42 | ## Getting started
43 |
44 | OK, gimme some code:
45 |
46 | ``` ruby
47 | require 'rosemary'
48 | api = Rosemary::Api.new
49 | node = api.find_node(123)
50 | => #
51 | ```
52 |
53 | ## Testing your code
54 |
55 | You should try your code on the OSM testing server first! You can change the url like this:
56 |
57 | ``` ruby
58 | require 'rosemary'
59 | Rosemary::Api.base_uri 'http://api06.dev.openstreetmap.org/'
60 | api = Rosemary::Api.new
61 | api.find_node(123)
62 | ```
63 |
64 | Modification of data is supported too. According to the OSM license every modification to the data has to be done by a registered OSM user account. The user can be authenticated with username and password. But see yourself:
65 |
66 | ``` ruby
67 | client = Rosemary::BasicAuthClient.new('osm_user_name', 'password')
68 |
69 | api = Rosemary::Api.new(client)
70 | changeset = api.create_changeset("Some meaningful comment")
71 | node = Rosemary::Node.new(:lat => 52.0, :lon => 13.4)
72 | api.save(node, changeset)
73 | api.close_changeset(changeset)
74 | ```
75 |
76 | Yeah, I can hear you sayin: 'Seriously, do I have to provide username and password? Is that secure?' Providing username and password is prone to some security issues. But OpenStreetMap supports secure HTTPS connections to hide basic auth headers. But wait, there is some more in store for you: [OAuth](http://oauth.net/) It's much more secure for the user and your OSM app. But it comes with a price: You have to register an application on http://www.openstreetmap.org. After you have your app registered you get an app key and secret. Keep it in a safe place.
77 |
78 | ``` ruby
79 | consumer = OAuth::Consumer.new( 'osm_app_key', 'osm_app_secret',
80 | :site => 'http://www.openstreetmap.org')
81 | access_token = OAuth::AccessToken.new(consumer, 'osm_user_token', 'osm_user_key')
82 | client = Rosemary::OauthClient.new(access_token)
83 |
84 | api = Rosemary::Api.new(client)
85 | changeset = api.create_changeset("Some meaningful comment")
86 | node = Rosemary::Node.new(:lat => 52.0, :lon => 13.4)
87 | api.save(node, changeset)
88 | api.close_changeset(changeset)
89 | ```
90 |
91 | Every request to the API is now handled by the OauthClient.
92 |
93 |
94 | ## Feedback and Contributions
95 |
96 | We appreciate your feedback and contributions. If you find a bug, feel free to to open a GitHub issue. Better yet, add a test that exposes the bug, fix it and send us a pull request.
97 |
--------------------------------------------------------------------------------
/lib/rosemary/parser.rb:
--------------------------------------------------------------------------------
1 | require 'httparty'
2 | require 'xml/libxml'
3 |
4 | # The XML parser capable of understanding the custom OSM XML format.
5 | class Rosemary::Parser < HTTParty::Parser
6 | include LibXML::XML::SaxParser::Callbacks
7 |
8 | attr_accessor :context, :description, :lang, :collection
9 |
10 | def parse
11 | return nil if body.nil? || body.empty?
12 | if supports_format?
13 | self.send(format) # This is a hack, cause the xml format would not be recognized ways, but for nodes and relations
14 | else
15 | body
16 | end
17 | end
18 |
19 | def coder
20 | @coder ||= HTMLEntities.new
21 | end
22 |
23 | def xml
24 | # instead of using
25 | # LibXML::XML::default_substitute_entities = true
26 | # we change the options of the xml context:
27 | ctx = XML::Parser::Context.string(body)
28 | ctx.encoding = XML::Encoding::UTF_8
29 | ctx.options = XML::Parser::Options::NOENT | XML::Parser::Options::NOBLANKS | XML::Parser::Options::RECOVER
30 | @parser = LibXML::XML::SaxParser.new(ctx)
31 |
32 | @parser.callbacks = self
33 | @parser.parse
34 |
35 | if @bounding_box
36 | @bounding_box
37 | else
38 | @collection.empty? ? @context : @collection
39 | end
40 | end
41 |
42 | def plain
43 | body
44 | end
45 |
46 | def on_start_document # :nodoc:
47 | @collection = []
48 | start_document if respond_to?(:start_document)
49 | end
50 |
51 | def on_end_document # :nodoc:
52 | end_document if respond_to?(:end_document)
53 | end
54 |
55 | def on_start_element(name, attr_hash) # :nodoc:
56 | case @context.class.name
57 | when 'Rosemary::User'
58 | case name
59 | when 'description' then @description = true
60 | when 'lang' then @lang = true
61 | end
62 | when 'Rosemary::Note'
63 | case name
64 | when 'id' then @id = true
65 | when 'text' then @text = true
66 | when 'user' then @user = true
67 | when 'action' then @action = true
68 | end
69 | else
70 | case name
71 | when 'node' then _start_node(attr_hash)
72 | when 'way' then _start_way(attr_hash)
73 | when 'relation' then _start_relation(attr_hash)
74 | when 'changeset' then _start_changeset(attr_hash)
75 | when 'user' then _start_user(attr_hash)
76 | when 'tag' then _tag(attr_hash)
77 | when 'nd' then _nd(attr_hash)
78 | when 'member' then _member(attr_hash)
79 | when 'home' then _home(attr_hash)
80 | when 'permissions' then _start_permissions(attr_hash)
81 | when 'permission' then _start_permission(attr_hash)
82 | when 'note' then _start_note(attr_hash)
83 | when 'bounds' then _start_bounds(attr_hash)
84 | end
85 | end
86 | end
87 |
88 | def on_end_element(name) # :nodoc:
89 | case name
90 | when 'description' then @description = false
91 | when 'lang' then @lang = false
92 | when 'id' then @id = false
93 | when 'text' then @text = false
94 | when 'action' then @action = false
95 | when 'user' then @user = false
96 | when 'changeset' then _end_changeset
97 | end
98 | end
99 |
100 | def on_characters(chars)
101 | case @context.class.name
102 | when 'Rosemary::User'
103 | @context.description = chars if @description
104 | @context.languages << chars if @lang
105 | when 'Rosemary::Note'
106 | @context.id = chars if @id
107 | @context.text << chars if @text
108 | @context.user = chars if @user
109 | @context.action = chars if @action
110 | end
111 | end
112 |
113 | private
114 | def _start_node(attr_hash)
115 | node = Rosemary::Node.new(attr_hash)
116 | @bounding_box.nodes << node if @bounding_box
117 | @context = node
118 | end
119 |
120 | def _start_way(attr_hash)
121 | way = Rosemary::Way.new(attr_hash)
122 | @bounding_box.ways << way if @bounding_box
123 | @context = way
124 | end
125 |
126 | def _start_relation(attr_hash)
127 | relation = Rosemary::Relation.new(attr_hash)
128 | @bounding_box.relations << relation if @bounding_box
129 | @context = relation
130 | end
131 |
132 | def _start_changeset(attr_hash)
133 | @context = Rosemary::Changeset.new(attr_hash)
134 | end
135 |
136 | def _start_note(attr_hash)
137 | @context = Rosemary::Note.new(attr_hash)
138 | end
139 |
140 | def _start_permissions(_)
141 | # just a few sanity checks: we can only parse permissions as a top level elem
142 | raise ParseError, "Unexpected element" unless @context.nil?
143 | @context = Rosemary::Permissions.new
144 | end
145 |
146 | def _start_permission(attr_hash)
147 | @context << attr_hash['name']
148 | end
149 |
150 | def _end_changeset
151 | @collection << @context
152 | end
153 |
154 | def _start_user(attr_hash)
155 | @context = Rosemary::User.new(attr_hash)
156 | end
157 |
158 | def _nd(attr_hash)
159 | @context << attr_hash['ref']
160 | end
161 |
162 | def _tag(attr_hash)
163 | if respond_to?(:tag)
164 | return unless tag(@context, attr_hash['k'], attr_value['v'])
165 | end
166 | @context.tags.merge!(attr_hash['k'] => coder.decode(attr_hash['v']))
167 | end
168 |
169 | def _member(attr_hash)
170 | new_member = Rosemary::Member.new(attr_hash['type'], attr_hash['ref'], attr_hash['role'])
171 | if respond_to?(:member)
172 | return unless member(@context, new_member)
173 | end
174 | @context.members << new_member
175 | end
176 |
177 | def _home(attr_hash)
178 | @context.lat = attr_hash['lat'] if attr_hash['lat']
179 | @context.lon = attr_hash['lon'] if attr_hash['lon']
180 | @context.lon = attr_hash['zoom'] if attr_hash['zoom']
181 | end
182 |
183 | def _start_bounds(attr_hash)
184 | @bounding_box = Rosemary::BoundingBox.new(attr_hash)
185 | end
186 |
187 | end
188 |
--------------------------------------------------------------------------------
/spec/models/node_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 |
4 | describe Node do
5 |
6 | subject do
7 | Node.new(:id => "123",
8 | :lat => "52.2",
9 | :lon => "13.4",
10 | :changeset => "12",
11 | :user => "fred",
12 | :uid => "123",
13 | :visible => true,
14 | :timestamp => "2005-07-30T14:27:12+01:00")
15 | end
16 |
17 | it { should be_valid }
18 |
19 | it "should be invalid without lat, lon" do
20 | subject.lat = nil
21 | subject.lon = nil
22 | expect(subject).not_to be_valid
23 | end
24 |
25 | it "does not modify the hash passed into constructor" do
26 | h = { :lat => 13.9, :lon => 54.1 }.freeze
27 | expect { Node.new(h) }.not_to raise_exception
28 | end
29 |
30 | it "should not be valid when using to large lat value" do
31 | subject.lat = 181
32 | expect(subject).not_to be_valid
33 | end
34 |
35 | it "should not be valid when using to large lat value" do
36 | subject.lon = 91
37 | expect(subject).not_to be_valid
38 | end
39 |
40 | it "should have an id attribute set from attributes" do
41 | expect(subject.id).to eql(123)
42 | end
43 |
44 | it "should have an id attribute within xml representation" do
45 | expect(subject.to_xml).to match /id=\"123\"/
46 | end
47 |
48 | it "should have a lat attribute set from attributes" do
49 | expect(subject.lat).to eql(52.2)
50 | end
51 |
52 | it "should have a lat attribute within xml representation" do
53 | expect(subject.to_xml).to match /lat=\"52.2\"/
54 | end
55 |
56 | it "should have a lon attribute set from attributes" do
57 | expect(subject.lon).to eql(13.4)
58 | end
59 |
60 | it "should have a lon attribute within xml representation" do
61 | expect(subject.to_xml).to match /lon=\"13.4\"/
62 | end
63 |
64 | it "should have a user attributes set from attributes" do
65 | expect(subject.user).to eql("fred")
66 | end
67 |
68 | it "should have a user attribute within xml representation" do
69 | expect(subject.to_xml).to match /user=\"fred\"/
70 | end
71 |
72 | it "should have a changeset attributes set from attributes" do
73 | expect(subject.changeset).to eql(12)
74 | end
75 |
76 | it "should have a changeset attribute within xml representation" do
77 | expect(subject.to_xml).to match /changeset=\"12\"/
78 | end
79 |
80 | it "should have a uid attribute set from attributes" do
81 | expect(subject.uid).to eql(123)
82 | end
83 |
84 | it "should have a uid attribute within xml representation" do
85 | expect(subject.to_xml).to match /uid=\"123\"/
86 | end
87 |
88 | it "should have a version attribute for osm tag" do
89 | expect(subject.to_xml).to match /version=\"0.6\"/
90 | end
91 |
92 | it "should have a generator attribute for osm tag" do
93 | expect(subject.to_xml).to match /generator=\"rosemary v/
94 | end
95 |
96 | it "should produce xml" do
97 | subject.add_tags(:wheelchair => 'yes')
98 | expect(subject.to_xml).to match /k=\"wheelchair\"/
99 | expect(subject.to_xml).to match /v=\"yes\"/
100 | end
101 |
102 | it "should not add tags with empty value to xml" do
103 | subject.add_tags(:wheelchair => '')
104 | expect(subject.to_xml).not_to match /k=\"wheelchair\"/
105 | end
106 |
107 | it "should properly encode ampersands" do
108 | subject.name = "foo & bar"
109 | expect(subject.to_xml).to match "foo & bar"
110 | end
111 |
112 | it "should not double encode ampersands" do
113 | subject.name = "foo & bar"
114 | expect(subject.to_xml).to match "foo & bar"
115 | end
116 |
117 | it "should fix double encoded ampersands" do
118 | subject.name = "foo & bar"
119 | expect(subject.to_xml).to match "foo & bar"
120 | end
121 |
122 |
123 | it "should properly strip leading and trailing whitespace" do
124 | subject.name = " Allice and Bob "
125 | expect(subject.to_xml).to match "\"Allice and Bob\""
126 | end
127 |
128 | it "should compare identity depending on tags and attributes" do
129 | first_node = Node.new('id' => 123, 'changeset' => '123', 'version' => 1, 'user' => 'horst', 'uid' => '123', 'timestamp' => '2005-07-30T14:27:12+01:00')
130 | first_node.tags[:name] = 'Black horse'
131 | second_node = Node.new('id' => 123, 'changeset' => '123', 'version' => 1, 'user' => 'horst', 'uid' => '123', 'timestamp' => '2005-07-30T14:27:12+01:00')
132 | second_node.tags[:name] = 'Black horse'
133 | expect(first_node <=> second_node).to eql 0
134 | end
135 |
136 | it "should not be equal when id does not match" do
137 | first_node = Node.new('id' => 123)
138 | second_node = Node.new('id' => 234)
139 | expect(first_node).not_to eql second_node
140 | end
141 |
142 | it "should not be equal when changeset does not match" do
143 | first_node = Node.new('changeset' => 123)
144 | second_node = Node.new('changeset' => 234)
145 | expect(first_node).not_to eql second_node
146 | end
147 |
148 | it "should not be equal when version does not match" do
149 | first_node = Node.new('version' => 1)
150 | second_node = Node.new('version' => 2)
151 | expect(first_node).not_to eql second_node
152 | end
153 |
154 | it "should not be equal when user does not match" do
155 | first_node = Node.new('user' => 'horst')
156 | second_node = Node.new('user' => 'jack')
157 | expect(first_node).not_to eql second_node
158 | end
159 |
160 | it "should not be equal when uid does not match" do
161 | first_node = Node.new('uid' => 123)
162 | second_node = Node.new('uid' => 234)
163 | expect(first_node).not_to eql second_node
164 | end
165 |
166 | it "should not be equal when timestamp does not match" do
167 | first_node = Node.new('timestamp' => '2005-07-30T14:27:12+01:00')
168 | second_node = Node.new('timestamp' => '2006-07-30T14:27:12+01:00')
169 | expect(first_node).not_to eql second_node
170 | end
171 |
172 | it "should not be equal when tags do not match" do
173 | first_node = Node.new('id' => 123)
174 | first_node.tags[:name] = 'black horse'
175 | second_node = Node.new('id' => 123)
176 | second_node.tags[:name] = 'white horse'
177 | expect(first_node).not_to eql second_node
178 | end
179 |
180 | it "should be ok to pass tags with emtpy value" do
181 | expect {
182 | subject.add_tags({"wheelchair_description"=>"", "type"=>"convenience",
183 | "street"=>nil, "name"=>"Kochhaus", "wheelchair"=>nil, "postcode"=>nil,
184 | "phone"=>nil, "city"=>nil, "website"=>nil, "lon"=>"13.35598468780518",
185 | "lat"=>"52.48627569798567", "housenumber"=>nil})
186 | }.not_to raise_exception
187 | end
188 | end
--------------------------------------------------------------------------------
/spec/integration/boundary_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe BoundingBox do
4 |
5 | let :osm do
6 | Api.new
7 | end
8 |
9 | def valid_fake_boundary
10 | boundary=<<-EOF
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | EOF
59 | end
60 |
61 | describe '#find:' do
62 | it "should find an array of Ways, Nodes and Relations from the API response via find_boundary" do
63 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/map?bbox=-122.035988,37.38554,-122.00948,37.411677").to_return(:status => 200, :body => valid_fake_boundary, :headers => {'Content-Type' => 'application/xml'})
64 | boundary = osm.find_bounding_box(-122.035988,37.38554,-122.00948,37.411677)
65 |
66 | expect(boundary.class).to eql BoundingBox
67 | expect(boundary.nodes).to include(Rosemary::Node.new({"id"=>"3147580094", "visible"=>"true", "version"=>"1", "changeset"=>"26302066", "timestamp"=>"2014-10-24T15:12:05Z", "user"=>"matthieun", "uid"=>"595221", "lat"=>"37.3872247", "lon"=>"-122.0216695"}))
68 | expect(boundary.ways.map { |it| it.id }).to include(309425054)
69 | expect(boundary.relations.map { |it| it.id }).to include(4133073)
70 |
71 | parsed_relation = boundary.relations.first
72 |
73 | expect(parsed_relation.members.length).to equal(2)
74 | expect(parsed_relation.tags.length).to equal(2)
75 | expect(boundary.minlat).to be_within(0.00001).of(37.3855400)
76 | expect(boundary.minlon).to be_within(0.00001).of(-122.0359880)
77 | expect(boundary.maxlat).to be_within(0.00001).of(37.4116770)
78 | expect(boundary.maxlon).to be_within(0.00001).of(-122.0094800)
79 | end
80 | end
81 |
82 | describe '#xml:' do
83 | it "should produce an xml that is equivalent to the parsed one" do
84 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/map?bbox=-122.035988,37.38554,-122.00948,37.411677").to_return(:status => 200, :body => valid_fake_boundary, :headers => {'Content-Type' => 'application/xml'})
85 | boundary = osm.find_bounding_box(-122.035988,37.38554,-122.00948,37.411677)
86 |
87 | xml = boundary.to_xml
88 | reparsed_boundary = Parser.call(xml, :xml)
89 |
90 | expect(reparsed_boundary.minlat).to eql(boundary.minlat)
91 | expect(reparsed_boundary.nodes.length).to eql(boundary.nodes.length)
92 | expect(reparsed_boundary.nodes.first.lat).to eql(boundary.nodes.first.lat)
93 | expect(reparsed_boundary.ways.length).to eql(boundary.ways.length)
94 | expect(reparsed_boundary.ways.first.nodes.first).to eql(boundary.ways.first.nodes.first)
95 | expect(reparsed_boundary.relations.length).to eql(boundary.relations.length)
96 | expect(reparsed_boundary.relations.first.tags.first).to eql(boundary.relations.first.tags.first)
97 | end
98 |
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/spec/integration/changeset_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 |
4 | describe Changeset do
5 |
6 | let :osm do
7 | Api.new
8 | end
9 |
10 | let :auth_osm do
11 | Api.new(BasicAuthClient.new('a_username', 'a_password'))
12 | end
13 |
14 | def valid_fake_user
15 | user=<<-EOF
16 |
17 |
18 |
19 | The description of your profile
20 |
21 | de-DE
22 | de
23 | en-US
24 | en
25 |
26 |
27 |
28 | EOF
29 | end
30 |
31 | def missing_changeset
32 | changeset=<<-EOF
33 |
34 | EOF
35 | end
36 |
37 | def single_changeset
38 | changeset=<<-EOF
39 |
40 |
41 |
42 |
43 |
44 |
45 | EOF
46 | end
47 |
48 | def multiple_changeset
49 | changeset=<<-EOF
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | EOF
61 | end
62 |
63 | describe '#find:' do
64 |
65 | def request_url
66 | "https://www.openstreetmap.org/api/0.6/changeset/10"
67 | end
68 |
69 | def stubbed_request
70 | stub_request(:get, request_url)
71 | end
72 |
73 | it "should build a Change from API response via find_changeset_object" do
74 | stubbed_request.to_return(:status => 200, :body => single_changeset, :headers => {'Content-Type' => 'application/xml'})
75 | changeset = osm.find_changeset(10)
76 | assert_requested :get, request_url, :times => 1
77 | expect(changeset.class).to eql Changeset
78 | end
79 |
80 | it "should raise an NotFound error, when a changeset cannot be found" do
81 | stubbed_request.to_return(:status => 404, :body => '', :headers => {'Content-Type' => 'text/plain'})
82 | node = osm.find_changeset(10)
83 | expect(node).to be_nil
84 | end
85 | end
86 |
87 | describe '#create' do
88 |
89 | def request_url
90 | "https://a_username:a_password@www.openstreetmap.org/api/0.6/changeset/create"
91 | end
92 |
93 | def stub_create_request
94 | stub_request(:put, request_url)
95 | end
96 |
97 | it "should post a new changeset with given comment" do
98 | body = Changeset.new(:tags => { :comment => 'New changeset' }).to_xml
99 |
100 | stub_create_request.with(:body => body).to_return(:status => 200, :body => "3", :headers => {'Content-Type' => 'plain/text'})
101 | expect(auth_osm).to receive(:find_changeset).with(3).and_return(cs = double())
102 | expect(auth_osm.create_changeset('New changeset')).to eql cs
103 | end
104 | end
105 |
106 | describe "#find_or_create_open_changeset" do
107 | it "returns an exisiting changeset if that exists and is open" do
108 | expect(auth_osm).to receive(:find_changeset).with(3).and_return(cs = double(:open? => true))
109 | expect(auth_osm).not_to receive(:create_changeset)
110 | expect(auth_osm.find_or_create_open_changeset(3, "some foo comment")).to eql cs
111 | end
112 |
113 | it "returns an new changeset if the requested one exists and is closed" do
114 | expect(auth_osm).to receive(:find_changeset).with(3).and_return(double(:open? => false))
115 | expect(auth_osm).to receive(:create_changeset).with("some foo comment", {}).and_return(cs = double())
116 | expect(auth_osm.find_or_create_open_changeset(3, "some foo comment")).to eql cs
117 | end
118 |
119 | it "returns an new changeset if the requested one doesn't exist" do
120 | expect(auth_osm).to receive(:find_changeset).with(3).and_return(nil)
121 | expect(auth_osm).to receive(:create_changeset).with("some foo comment", {}).and_return(cs = double())
122 | expect(auth_osm.find_or_create_open_changeset(3, "some foo comment")).to eql cs
123 | end
124 |
125 | it "appends arbitrary tags to the changeset itself" do
126 | expect(auth_osm).to receive(:find_changeset).with(3).and_return(nil)
127 | expect(auth_osm).to receive(:create_changeset).with("some foo comment", :source => 'http://example.com' ).and_return(cs = double())
128 | expect(auth_osm.find_or_create_open_changeset(3, "some foo comment", :source => 'http://example.com' )).to eql cs
129 | end
130 | end
131 |
132 | describe '#find_for_user' do
133 |
134 | def request_url
135 | "https://www.openstreetmap.org/api/0.6/changesets?user=1234"
136 | end
137 |
138 | def stubbed_request
139 | stub_request(:get, request_url)
140 | end
141 |
142 | let! :stub_user_lookup do
143 | stub_request(:get, "https://a_username:a_password@www.openstreetmap.org/api/0.6/user/details").to_return(:status => 200, :body => valid_fake_user, :headers => {'Content-Type' => 'application/xml'} )
144 | end
145 |
146 | it "should not find changeset for user if user has none" do
147 | stubbed_request.to_return(:status => 200, :body => missing_changeset, :headers => {'Content-Type' => 'application/xml'})
148 | changesets = auth_osm.find_changesets_for_user
149 | expect(changesets).to be_empty
150 | end
151 |
152 | it "should find a single changeset for user" do
153 | stubbed_request.to_return(:status => 200, :body => single_changeset, :headers => {'Content-Type' => 'application/xml'})
154 | changesets = auth_osm.find_changesets_for_user
155 | expect(changesets.size).to eql 1
156 | expect(changesets.first.class).to eql Changeset
157 | end
158 |
159 | it "should find a multiple changesets for a user" do
160 | stubbed_request.to_return(:status => 200, :body => multiple_changeset, :headers => {'Content-Type' => 'application/xml'})
161 | changesets = auth_osm.find_changesets_for_user
162 | expect(changesets.size).to eql 2
163 | expect(changesets.first.class).to eql Changeset
164 | end
165 | end
166 |
167 | describe '#update:' do
168 | end
169 |
170 | describe '#close' do
171 | end
172 | end
173 |
174 |
--------------------------------------------------------------------------------
/lib/rosemary/element.rb:
--------------------------------------------------------------------------------
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 | include Comparable
6 |
7 | # Unique ID
8 | # @return [Fixnum] id of this element
9 | attr_reader :id
10 |
11 | # The version of this object (as read from file, it
12 | # is not updated by operations to this object)
13 | # API 0.6 and above only
14 | # @return [Fixnum] the current version
15 | attr_accessor :version
16 |
17 | # The user who last edited this element (as read from file, it
18 | # is not updated by operations to this object)
19 | # @return [Rosemary::User] the user who last edititd this element
20 | attr_accessor :user
21 |
22 | # The user id of the user who last edited this object (as read from file, it
23 | # is not updated by operations to this object)
24 | # API 0.6 and above only
25 | attr_accessor :uid
26 |
27 | # Last change of this object (as read from file, it is not
28 | # updated by operations to this object)
29 | # @return [Time] last change of this object.
30 | attr_reader :timestamp
31 |
32 | # The changeset the last change of this object was made with.
33 | attr_accessor :changeset
34 |
35 | # Tags for this object
36 | attr_reader :tags
37 |
38 | # Get Rosemary::Element from API
39 | # @param [Fixnum] id the id of the element to load from the API
40 |
41 | def self.from_api(id, api=Rosemary::API.new) #:nodoc:
42 | raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == Rosemary::Element
43 | api.get_object(type, id)
44 | end
45 |
46 | def initialize(attrs = {}) #:nodoc:
47 | raise NotImplementedError.new('Element is a virtual base class for the Node, Way, and Relation classes') if self.class == Rosemary::Element
48 | attrs = {'version' => 1, 'uid' => 1}.merge(attrs.stringify_keys!)
49 | @id = attrs['id'].to_i if attrs['id']
50 | @version = attrs['version'].to_i
51 | @uid = attrs['uid'].to_i
52 | @user = attrs['user']
53 | @timestamp = Time.parse(attrs['timestamp']) rescue nil
54 | @changeset = attrs['changeset'].to_i
55 | @tags = Tags.new
56 | add_tags(attrs['tag']) if attrs['tag']
57 | end
58 |
59 | def <=>(another_element)
60 | attribute_list.each do |attrib|
61 | compare_value = self.send(attrib) <=> another_element.send(attrib)
62 | return compare_value unless compare_value == 0
63 | end
64 | 0
65 | end
66 |
67 | # Create an error when somebody tries to set the ID.
68 | # (We need this here because otherwise method_missing will be called.)
69 | def id=(id) # :nodoc:
70 | raise NotImplementedError.new('id can not be changed once the object was created')
71 | end
72 |
73 | # Set timestamp for this object.
74 | # @param [Time] timestamp the time this object was created
75 | def timestamp=(timestamp)
76 | @timestamp = _check_timestamp(timestamp)
77 | end
78 |
79 | # The list of attributes for this object
80 | def attribute_list # :nodoc:
81 | [:id, :version, :uid, :user, :timestamp, :changeset, :tags]
82 | end
83 |
84 | # Returns a hash of all non-nil attributes of this object.
85 | #
86 | # Keys of this hash are :id, :user,
87 | # and :timestamp. For a Node also :lon
88 | # and :lat.
89 | #
90 | # call-seq: attributes -> Hash
91 | #
92 | def attributes
93 | attrs = Hash.new
94 | attribute_list.each do |attribute|
95 | value = self.send(attribute)
96 | attrs[attribute] = value unless value.nil?
97 | end
98 | attrs
99 | end
100 |
101 | # Get tag value
102 | def [](key)
103 | tags[key]
104 | end
105 |
106 | # Set tag
107 | def []=(key, value)
108 | tags[key] = value
109 | end
110 |
111 | # Add one or more tags to this object.
112 | #
113 | # call-seq: add_tags(Hash) -> OsmObject
114 | #
115 | def add_tags(new_tags)
116 | case new_tags
117 | when Array # Called with an array
118 | # Call recursively for each entry
119 | new_tags.each do |tag_hash|
120 | add_tags(tag_hash)
121 | end
122 | when Hash # Called with a hash
123 | #check if it is weird {'k' => 'key', 'v' => 'value'} syntax
124 | if (new_tags.size == 2 && new_tags.keys.include?('k') && new_tags.keys.include?('v'))
125 | # call recursively with values from k and v keys.
126 | add_tags({new_tags['k'] => new_tags['v']})
127 | else
128 | # OK, this seems to be a proper ruby hash with a single entry
129 | new_tags.each do |k,v|
130 | self.tags[k] = v
131 | end
132 | end
133 | end
134 | self # return self so calls can be chained
135 | end
136 |
137 | def update_attributes(attribute_hash)
138 | dirty = false
139 | attribute_hash.each do |key,value|
140 | if self.send(key).to_s != value.to_s
141 | self.send("#{key}=", value.to_s)
142 | dirty = true
143 | end
144 | end
145 | dirty
146 | end
147 |
148 |
149 | # Has this object any tags?
150 | #
151 | # @return [Boolean] has any tags?
152 | #
153 | def is_tagged?
154 | ! @tags.empty?
155 | end
156 |
157 | # Create a new GeoRuby::Shp4r::ShpRecord with the geometry of
158 | # this object and the given attributes.
159 | #
160 | # This only works if the GeoRuby library is included.
161 | #
162 | # geom:: Geometry
163 | # attributes:: Hash with attributes
164 | #
165 | # call-seq: shape(attributes) -> GeoRuby::Shp4r::ShpRecord
166 | #
167 | # Example:
168 | # require 'rubygems'
169 | # require 'geo_ruby'
170 | # node = Node(nil, nil, nil, 7.84, 54.34)
171 | # g = node.point
172 | # node.shape(g, :type => 'Pharmacy', :name => 'Hyde Park Pharmacy')
173 | #
174 | def shape(geom, attributes)
175 | fields = Hash.new
176 | attributes.each do |key, value|
177 | fields[key.to_s] = value
178 | end
179 | GeoRuby::Shp4r::ShpRecord.new(geom, fields)
180 | end
181 |
182 | # Get all relations from the API that have his object as members.
183 | #
184 | # The optional parameter is an Rosemary::API object. If none is specified
185 | # the default OSM API is used.
186 | #
187 | # Returns an array of Relation objects or an empty array.
188 | #
189 | def get_relations_from_api(api=Rosemary::API.new)
190 | api.get_relations_referring_to_object(type, self.id.to_i)
191 | end
192 |
193 | # Get the history of this object from the API.
194 | #
195 | # The optional parameter is an Rosemary::API object. If none is specified
196 | # the default OSM API is used.
197 | #
198 | # Returns an array of Rosemary::Node, Rosemary::Way, or Rosemary::Relation objects
199 | # with all the versions.
200 | def get_history_from_api(api=Rosemary::API.new)
201 | api.get_history(type, self.id.to_i)
202 | end
203 |
204 | # All other methods are mapped so its easy to access tags: For
205 | # instance obj.name is the same as obj.tags['name']. This works
206 | # for getting and setting tags.
207 | #
208 | # node = Rosemary::Node.new
209 | # node.add_tags( 'highway' => 'residential', 'name' => 'Main Street' )
210 | # node.highway #=> 'residential'
211 | # node.highway = 'unclassified' #=> 'unclassified'
212 | # node.name #=> 'Main Street'
213 | #
214 | # In addition methods of the form key? are used to
215 | # check boolean tags. For instance +oneway+ can be 'true' or
216 | # 'yes' or '1', all meaning the same.
217 | #
218 | # way.oneway?
219 | #
220 | # will check this. It returns true if the value of this key is
221 | # either 'true', 'yes' or '1'.
222 | def method_missing(method, *args)
223 | methodname = method.to_s
224 | if methodname.slice(-1, 1) == '='
225 | if args.size != 1
226 | raise ArgumentError.new("wrong number of arguments (#{args.size} for 1)")
227 | end
228 | tags[methodname.chop] = args[0]
229 | elsif methodname.slice(-1, 1) == '?'
230 | if args.size != 0
231 | raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)")
232 | end
233 | tags[methodname.chop] =~ /^(true|yes|1)$/
234 | else
235 | if args.size != 0
236 | raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)")
237 | end
238 | tags[methodname]
239 | end
240 | end
241 |
242 | def initialize_copy(from)
243 | super
244 | @tags = from.tags.dup
245 | end
246 |
247 | def self.from_xml(xml)
248 | Parser.call(xml, :xml)
249 | end
250 |
251 | private
252 |
253 | # Return next free ID
254 | def _next_id
255 | @@id -= 1
256 | @@id
257 | end
258 |
259 | def _check_id(id)
260 | if id.kind_of?(Integer)
261 | return id
262 | elsif id.kind_of?(String)
263 | raise ArgumentError, "ID must be an integer" unless id =~ /^-?[0-9]+$/
264 | return id.to_i
265 | else
266 | raise ArgumentError, "ID must be integer or string with integer"
267 | end
268 | end
269 |
270 | def _check_timestamp(timestamp)
271 | 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}))$/
272 | raise ArgumentError, "Timestamp is in wrong format (must be 'yyyy-mm-ddThh:mm:ss(Z|[+-]mm:ss)')"
273 | end
274 | timestamp
275 | end
276 |
277 | def _check_lon(lon)
278 | if lon.kind_of?(Numeric)
279 | return lon.to_s
280 | elsif lon.kind_of?(String)
281 | return lon
282 | else
283 | raise ArgumentError, "'lon' must be number or string containing number"
284 | end
285 | end
286 |
287 | def _check_lat(lat)
288 | if lat.kind_of?(Numeric)
289 | return lat.to_s
290 | elsif lat.kind_of?(String)
291 | return lat
292 | else
293 | raise ArgumentError, "'lat' must be number or string containing number"
294 | end
295 | end
296 |
297 | end
298 | end
--------------------------------------------------------------------------------
/lib/rosemary/api.rb:
--------------------------------------------------------------------------------
1 | require 'httparty'
2 | module Rosemary
3 |
4 | # The Api class handles all calls to the OpenStreetMap API.
5 | #
6 | # Usage:
7 | # require 'rosemary'
8 | # auth_client = Rosemary::BasicAuthClient.new(:user_name => 'user', :password => 'a_password')
9 | # api = Rosemary::Api.new(auth_client)
10 | # @node = api.find_node(1234)
11 | # @node.tags << {:wheelchair => 'no'}
12 | # @changeset = api.create_changeset('Set the wheelchair tag')
13 | # api.save(@node, @changeset)
14 | class Api
15 | # Provide basic HTTP client behaviour.
16 | include HTTParty
17 |
18 | # The OSM API version supported by this gem.
19 | API_VERSION = "0.6".freeze
20 |
21 | # the default base URI for the API
22 | base_uri "https://www.openstreetmap.org"
23 | #base_uri "http://api06.dev.openstreetmap.org/api/#{API_VERSION}"
24 |
25 | # Make sure the request don't run forever
26 | default_timeout 5
27 |
28 | # Use a custom parser to handle the OSM XML format.
29 | parser Parser
30 |
31 |
32 | # @return [Rosemary::Client] the client to be used to authenticate the user towards the OSM API.
33 | attr_accessor :client
34 |
35 | # @return [Rosemary::Changeset] the current changeset to be used for all writing acces.
36 | attr_accessor :changeset
37 |
38 | # Creates an Rosemary::Api object with an optional client
39 | # @param [Rosemary::Client] client the client to authenticate the user for write access.
40 | def initialize(client=nil)
41 | @client = client
42 | end
43 |
44 | # Get a Node with specified ID from API.
45 | #
46 | # @param [Fixnum] id the id of the node to find.
47 | # @return [Rosemary::Node] the node with the given id.
48 | # @raise [Rosemary::Gone] in case the node has been deleted.
49 | def find_node(id)
50 | find_element('node', id)
51 | end
52 |
53 | # Get a Way with specified ID from API.
54 | #
55 | # @param [Fixnum] id the id of the node to find.
56 | # @return [Rosemary::Way] the way with the given id.
57 | #
58 | def find_way(id)
59 | find_element('way', id)
60 | end
61 |
62 | # Get a Relation with specified ID from API.
63 | #
64 | # call-seq: find_relation(id) -> Rosemary::Relation
65 | #
66 | def find_relation(id)
67 | find_element('relation', id)
68 | end
69 |
70 | # Get a Changeset with specified ID from API
71 | # if that changeset is missing, id is nil, or the changeset is closed
72 | # create a new one
73 | #
74 | # call-seq: find_or_create_open_changeset(id, comment) -> Rosemary::Changeset
75 | #
76 | def find_or_create_open_changeset(id, comment = nil, tags = {})
77 | find_open_changeset(id) || create_changeset(comment, tags)
78 | end
79 |
80 | def find_open_changeset(id)
81 | cs = find_changeset(id)
82 | (cs && cs.open?) ? cs : nil
83 | end
84 |
85 | # Get the user which represented by the Rosemary::Client
86 | #
87 | # @return: [Rosemary::User] user the user authenticated using the current client
88 | #
89 | def find_user
90 | raise CredentialsMissing if client.nil?
91 | resp = do_authenticated_request(:get, "/user/details")
92 | raise resp if resp.is_a? String
93 | resp
94 | end
95 |
96 | # Get the bounding box which is represented by the Rosemary::BoundingBox
97 | #
98 | # @param [Numeric] left is the longitude of the left (westernmost) side of the bounding box.
99 | # @param [Numeric] bottom is the latitude of the bottom (southernmost) side of the bounding box.
100 | # @param [Numeric] right is the longitude of the right (easternmost) side of the bounding box.
101 | # @param [Numeric] top is the latitude of the top (northernmost) side of the bounding box.
102 | # @return [Rosemary::BoundingBox] the bounding box containing all ways, nodes and relations inside the given coordinates
103 | #
104 | def find_bounding_box(left,bottom,right,top)
105 | do_request(:get, "/map?bbox=#{left},#{bottom},#{right},#{top}", {} )
106 | end
107 |
108 |
109 | def permissions
110 | if client.nil?
111 | get("/permissions")
112 | else
113 | do_authenticated_request(:get, "/permissions")
114 | end
115 | end
116 |
117 | # Deletes the given element using API write access.
118 | #
119 | # @param [Rosemary::Element] element the element to be created
120 | # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
121 | # @return [Fixnum] the new version of the deleted element.
122 | def destroy(element, changeset)
123 | element.changeset = changeset.id
124 | response = delete("/#{element.type.downcase}/#{element.id}", :body => element.to_xml) unless element.id.nil?
125 | response.to_i # New version number
126 | end
127 |
128 | # Creates or updates an element depending on the current state of persistance.
129 | #
130 | # @param [Rosemary::Element] element the element to be created
131 | # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
132 | def save(element, changeset)
133 | response = if element.id.nil?
134 | create(element, changeset)
135 | else
136 | update(element, changeset)
137 | end
138 | end
139 |
140 | # Create a new element using API write access.
141 | #
142 | # @param [Rosemary::Element] element the element to be created
143 | # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
144 | # @return [Fixnum] the id of the newly created element.
145 | def create(element, changeset)
146 | element.changeset = changeset.id
147 | put("/#{element.type.downcase}/create", :body => element.to_xml)
148 | end
149 |
150 | # Update an existing element using API write access.
151 | #
152 | # @param [Rosemary::Element] element the element to be created
153 | # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
154 | # @return [Fixnum] the versiom of the updated element.
155 | def update(element, changeset)
156 | element.changeset = changeset.id
157 | response = put("/#{element.type.downcase}/#{element.id}", :body => element.to_xml)
158 | response.to_i # New Version number
159 | end
160 |
161 | # Create a new changeset with an optional comment
162 | #
163 | # @param [String] comment a meaningful comment for this changeset
164 | # @return [Rosemary::Changeset] the changeset which was newly created
165 | # @raise [Rosemary::NotFound] in case the changeset could not be found
166 | def create_changeset(comment = nil, tags = {})
167 | tags.merge!(:comment => comment) { |key, v1, v2| v1 }
168 | changeset = Changeset.new(:tags => tags)
169 | changeset_id = put("/changeset/create", :body => changeset.to_xml).to_i
170 | find_changeset(changeset_id) unless changeset_id == 0
171 | end
172 |
173 | # Get a Changeset with specified ID from API.
174 | #
175 | # @param [Integer] id the ID for the changeset you look for
176 | # @return [Rosemary::Changeset] the changeset which was found with the id
177 | def find_changeset(id)
178 | find_element('changeset', id)
179 | end
180 |
181 | # Closes the given changeset.
182 | #
183 | # @param [Rosemary::Changeset] changeset the changeset to be closed
184 | def close_changeset(changeset)
185 | put("/changeset/#{changeset.id}/close")
186 | end
187 |
188 | def find_changesets_for_user(options = {})
189 | user_id = find_user.id
190 | changesets = get("/changesets", :query => options.merge({:user => user_id}))
191 | changesets.nil? ? [] : changesets
192 | end
193 |
194 | # Get an object ('node', 'way', or 'relation') with specified ID from API.
195 | #
196 | # call-seq: find_element('node', id) -> Rosemary::Element
197 | #
198 | def find_element(type, id)
199 | raise ArgumentError.new("type needs to be one of 'node', 'way', and 'relation'") unless type =~ /^(node|way|relation|changeset)$/
200 | return nil if id.nil?
201 | begin
202 | response = get("/#{type}/#{id}")
203 | response.is_a?(Array ) ? response.first : response
204 | rescue NotFound
205 | nil
206 | end
207 | end
208 |
209 | # Create a note
210 | #
211 | # call-seq: create_note(lat: 51.00, lon: 0.1, text: 'Test note') -> Rosemary::Note
212 | #
213 | def create_note(note)
214 | post("/notes", :query => note)
215 | end
216 |
217 | private
218 |
219 | # most GET requests are valid without authentication, so this is the standard
220 | def get(url, options = {})
221 | do_request(:get, url, options)
222 | end
223 |
224 | # all PUT requests need authorization, so this is the stanard
225 | def put(url, options = {})
226 | do_authenticated_request(:put, url, options)
227 | end
228 |
229 | # all POST requests need authorization, so this is the stanard
230 | def post(url, options = {})
231 | do_authenticated_request(:post, url, options)
232 | end
233 |
234 | # all DELETE requests need authorization, so this is the stanard
235 | def delete(url, options = {})
236 | do_authenticated_request(:delete, url, options)
237 | end
238 |
239 | def api_url(url)
240 | "/api/#{API_VERSION}" + url
241 | end
242 |
243 | # Do a API request without authentication
244 | def do_request(method, url, options = {})
245 | begin
246 | response = self.class.send(method, api_url(url), options)
247 | check_response_codes(response)
248 | response.parsed_response
249 | rescue Timeout::Error
250 | raise Unavailable.new('Service Unavailable')
251 | end
252 | end
253 |
254 | # Do a API request with authentication, using the given client
255 | def do_authenticated_request(method, url, options = {})
256 | begin
257 | response = case client
258 | when BasicAuthClient
259 | self.class.send(method, api_url(url), options.merge(:basic_auth => client.credentials))
260 | when OauthClient
261 | # We have to wrap the result of the access_token request into an HTTParty::Response object
262 | # to keep duck typing with HTTParty
263 | result = client.send(method, api_url(url), options)
264 | content_type = Parser.format_from_mimetype(result.content_type)
265 | parsed_response = Parser.call(result.body, content_type)
266 |
267 | HTTParty::Response.new(nil, result, lambda { parsed_response })
268 | else
269 | raise CredentialsMissing
270 | end
271 | check_response_codes(response)
272 | response.parsed_response
273 | rescue Timeout::Error
274 | raise Unavailable.new('Service Unavailable')
275 | end
276 | end
277 |
278 | def check_response_codes(response)
279 | body = response.body
280 | case response.code.to_i
281 | when 200 then return
282 | when 400 then raise BadRequest.new(body)
283 | when 401 then raise Unauthorized.new(body)
284 | when 403 then raise Forbidden.new(body)
285 | when 404 then raise NotFound.new(body)
286 | when 405 then raise MethodNotAllowed.new(body)
287 | when 409 then raise Conflict.new(body)
288 | when 410 then raise Gone.new(body)
289 | when 412 then raise Precondition.new(body)
290 | # when 414 then raise UriTooLarge.new(body)
291 | when 500 then raise ServerError, 'Internal Server Error'
292 | when 503 then raise Unavailable, 'Service Unavailable'
293 | else raise "Unknown response code: #{response.code}"
294 | end
295 | end
296 |
297 | end
298 | end
299 |
--------------------------------------------------------------------------------
/spec/integration/node_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | include Rosemary
3 | describe Node do
4 |
5 | let(:changeset) { Changeset.new(:id => 1) }
6 |
7 | let(:osm) { Api.new }
8 |
9 | def stub_changeset_lookup
10 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/changesets?open=true&user=1234").to_return(:status => 200, :body => valid_fake_changeset, :headers => {'Content-Type' => 'application/xml'} )
11 | end
12 |
13 | def stub_node_lookup
14 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => valid_fake_node, :headers => {'Content-Type' => 'application/xml'})
15 | end
16 |
17 | def valid_fake_node
18 | node=<<-EOF
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | EOF
27 | end
28 |
29 | def valid_fake_user
30 | user=<<-EOF
31 |
32 |
33 |
34 | The description of your profile
35 |
36 | de-DE
37 | de
38 | en-US
39 | en
40 |
41 |
42 |
43 | EOF
44 | end
45 |
46 | def valid_fake_changeset
47 | changeset=<<-EOF
48 |
49 |
50 |
51 |
52 |
53 |
54 | EOF
55 | end
56 |
57 | describe '#find:' do
58 |
59 | def request_url
60 | "https://www.openstreetmap.org/api/0.6/node/1234"
61 | end
62 |
63 | def stubbed_request
64 | stub_request(:get, request_url)
65 | end
66 |
67 | it "should build a Node from API response via get_object" do
68 | stubbed_request.to_return(:status => 200, :body => valid_fake_node, :headers => {'Content-Type' => 'application/xml'})
69 | node = osm.find_node 1234
70 | assert_requested :get, request_url, :times => 1
71 | expect(node.class).to eql Node
72 | expect(node.tags.size).to eql 3
73 | expect(node.tags['name']).to eql 'The rose'
74 | expect(node['name']).to eql 'The rose'
75 | node.add_tags('wheelchair' => 'yes')
76 | expect(node['wheelchair']).to eql 'yes'
77 | end
78 |
79 | it "should raise a Unavailable, when api times out" do
80 | stubbed_request.to_timeout
81 | expect {
82 | node = osm.find_node(1234)
83 | }.to raise_exception(Unavailable)
84 | end
85 |
86 | it "should raise an Gone error, when a node has been deleted" do
87 | stubbed_request.to_return(:status => 410, :body => '', :headers => {'Content-Type' => 'text/plain'})
88 | expect {
89 | node = osm.find_node(1234)
90 | }.to raise_exception(Gone)
91 | end
92 |
93 | it "should raise an NotFound error, when a node cannot be found" do
94 | stubbed_request.to_return(:status => 404, :body => '', :headers => {'Content-Type' => 'text/plain'})
95 | node = osm.find_node(1234)
96 | expect(node).to be_nil
97 | end
98 | end
99 |
100 | describe 'with BasicAuthClient' do
101 |
102 | let :osm do
103 | Api.new(BasicAuthClient.new('a_username', 'a_password'))
104 | end
105 |
106 | def stub_user_lookup
107 | stub_request(:get, "https://a_username:a_password@www.openstreetmap.org/api/0.6/user/details").to_return(:status => 200, :body => valid_fake_user, :headers => {'Content-Type' => 'application/xml'} )
108 | end
109 |
110 | describe '#create:' do
111 |
112 | let (:node) { Node.new }
113 |
114 | let (:expected_body) {
115 | expected_node = node.dup
116 | expected_node.changeset = changeset.id
117 | expected_node.to_xml
118 | }
119 |
120 | def request_url
121 | "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/create"
122 | end
123 |
124 | def stubbed_request
125 | stub_request(:put, request_url)
126 | end
127 |
128 | before do
129 | stub_user_lookup
130 | end
131 |
132 | it "should create a new Node from given attributes" do
133 | stubbed_request.with(:body => expected_body).
134 | to_return(:status => 200, :body => '123', :headers => {'Content-Type' => 'text/plain'})
135 |
136 | new_id = osm.create(node, changeset)
137 | end
138 |
139 | it "should raise a Unavailable, when api times out" do
140 | stubbed_request.to_timeout
141 | expect {
142 | new_id = osm.create(node, changeset)
143 | }.to raise_exception(Unavailable)
144 | end
145 |
146 | it "should not create a Node with invalid xml but raise BadRequest" do
147 | stubbed_request.to_return(:status => 400, :body => 'The given node is invalid', :headers => {'Content-Type' => 'text/plain'})
148 | expect {
149 | new_id = osm.save(node, changeset)
150 | }.to raise_exception(BadRequest)
151 | end
152 |
153 | it "should not allow to create a node when a changeset has been closed" do
154 | stubbed_request.to_return(:status => 409, :body => 'The given node is invalid', :headers => {'Content-Type' => 'text/plain'})
155 | expect {
156 | new_id = osm.save(node, changeset)
157 | }.to raise_exception(Conflict)
158 | end
159 |
160 | it "should not allow to create a node when no authentication client is given" do
161 | osm = Api.new
162 | expect {
163 | osm.save(node, changeset)
164 | }.to raise_exception(CredentialsMissing)
165 | end
166 |
167 | it "should set a changeset" do
168 | stubbed_request.to_return(:status => 200, :body => '123', :headers => {'Content-Type' => 'text/plain'})
169 | node.changeset = nil
170 | osm.save(node, changeset)
171 | expect(node.changeset).to eql changeset.id
172 | end
173 | end
174 |
175 | describe '#update:' do
176 |
177 | let :node do
178 | osm.find_node 123
179 | end
180 |
181 | before do
182 | stub_user_lookup
183 | stub_node_lookup
184 | end
185 |
186 | it "should save a edited node" do
187 | stub_request(:put, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => '43', :headers => {'Content-Type' => 'text/plain'})
188 | node.tags['amenity'] = 'restaurant'
189 | node.tags['name'] = 'Il Tramonto'
190 | expect(node).to receive(:changeset=)
191 | new_version = osm.save(node, changeset)
192 | expect(new_version).to eql 43
193 | end
194 |
195 | it "should set a changeset" do
196 | stub_request(:put, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => '43', :headers => {'Content-Type' => 'text/plain'})
197 | node.changeset = nil
198 | osm.save(node, changeset)
199 | expect(node.changeset).to eql changeset.id
200 | end
201 |
202 |
203 | end
204 |
205 | describe '#delete:' do
206 |
207 | let :node do
208 | osm.find_node 123
209 | end
210 |
211 | before do
212 | stub_changeset_lookup
213 | stub_user_lookup
214 | stub_node_lookup
215 | end
216 |
217 | it "should not delete an node with missing id" do
218 | node = Node.new
219 | osm.destroy(node, changeset)
220 | end
221 |
222 | it "should delete an existing node" do
223 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => '43', :headers => {'Content-Type' => 'text/plain'})
224 | expect(node).to receive(:changeset=)
225 | new_version = osm.destroy(node, changeset)
226 | expect(new_version).to eql 43 # new version number
227 | end
228 |
229 | it "should raise an error if node to be deleted is still part of a way" do
230 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 400, :body => 'Version does not match current database version', :headers => {'Content-Type' => 'text/plain'})
231 | expect {
232 | response = osm.destroy(node, changeset)
233 | expect(response).to eql "Version does not match current database version"
234 | }.to raise_exception BadRequest
235 | end
236 |
237 | it "should raise an error if node cannot be found" do
238 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 404, :body => 'Node cannot be found', :headers => {'Content-Type' => 'text/plain'})
239 | expect {
240 | response = osm.destroy(node, changeset)
241 | expect(response).to eql "Node cannot be found"
242 | }.to raise_exception NotFound
243 | end
244 |
245 | it "should raise an error if there is a conflict" do
246 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 409, :body => 'Node has been deleted in this changeset', :headers => {'Content-Type' => 'text/plain'})
247 | expect {
248 | response = osm.destroy(node, changeset)
249 | expect(response).to eql "Node has been deleted in this changeset"
250 | }.to raise_exception Conflict
251 | end
252 |
253 | it "should raise an error if the node is already delted" do
254 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 410, :body => 'Node has been deleted', :headers => {'Content-Type' => 'text/plain'})
255 | expect {
256 | response = osm.destroy(node, changeset)
257 | expect(response).to eql "Node has been deleted"
258 | }.to raise_exception Gone
259 | end
260 |
261 | it "should raise an error if the node is part of a way" do
262 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 412, :body => 'Node 123 is still used by way 456', :headers => {'Content-Type' => 'text/plain'})
263 | expect {
264 | response = osm.destroy(node, changeset)
265 | expect(response).to eql "Node 123 is still used by way 456"
266 | }.to raise_exception Precondition
267 | end
268 |
269 | it "should set the changeset an existing node" do
270 | stub_request(:delete, "https://a_username:a_password@www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => '43', :headers => {'Content-Type' => 'text/plain'})
271 | node.changeset = nil
272 | new_version = osm.destroy(node, changeset)
273 | expect(node.changeset).to eql changeset.id
274 | end
275 | end
276 | end
277 |
278 | describe 'with OauthClient' do
279 |
280 | let :consumer do
281 | OAuth::Consumer.new( 'a_key', 'a_secret',
282 | {
283 | :site => 'https://www.openstreetmap.org',
284 | :request_token_path => '/oauth/request_token',
285 | :access_token_path => '/oauth/access_token',
286 | :authorize_path => '/oauth/authorize'
287 | }
288 | )
289 | end
290 |
291 | let :access_token do
292 | OAuth::AccessToken.new(consumer, 'a_token', 'a_secret')
293 | end
294 |
295 | let :osm do
296 | Api.new(OauthClient.new(access_token))
297 | end
298 |
299 | def stub_user_lookup
300 | stub_request(:get, "https://www.openstreetmap.org/api/0.6/user/details").to_return(:status => 200, :body => valid_fake_user, :headers => {'Content-Type' => 'application/xml'} )
301 | end
302 |
303 | describe '#create:' do
304 | let :node do
305 | Node.new
306 | end
307 |
308 | def request_url
309 | "https://www.openstreetmap.org/api/0.6/node/create"
310 | end
311 |
312 | def stubbed_request
313 | stub_request(:put, request_url)
314 | end
315 |
316 | before do
317 | stub_changeset_lookup
318 | stub_user_lookup
319 | end
320 |
321 | it "should create a new Node from given attributes" do
322 | stubbed_request.to_return(:status => 200, :body => '123', :headers => {'Content-Type' => 'text/plain'})
323 | expect(node.id).to be_nil
324 | new_id = osm.save(node, changeset)
325 | end
326 |
327 | it "should raise a Unavailable, when api times out" do
328 | stubbed_request.to_timeout
329 | expect {
330 | new_id = osm.save(node, changeset)
331 | }.to raise_exception(Unavailable)
332 | end
333 |
334 |
335 | it "should not create a Node with invalid xml but raise BadRequest" do
336 | stubbed_request.to_return(:status => 400, :body => 'The given node is invalid', :headers => {'Content-Type' => 'text/plain'})
337 | expect {
338 | new_id = osm.save(node, changeset)
339 | }.to raise_exception(BadRequest)
340 | end
341 |
342 | it "should not allow to create a node when a changeset has been closed" do
343 | stubbed_request.to_return(:status => 409, :body => 'The given node is invalid', :headers => {'Content-Type' => 'text/plain'})
344 | expect {
345 | new_id = osm.save(node, changeset)
346 | }.to raise_exception(Conflict)
347 | end
348 |
349 | it "should not allow to create a node when no authentication client is given" do
350 | osm = Api.new
351 | expect {
352 | osm.save(node, changeset)
353 | }.to raise_exception(CredentialsMissing)
354 | end
355 |
356 | end
357 |
358 | describe '#update:' do
359 |
360 | let :node do
361 | osm.find_node 123
362 | end
363 |
364 | before do
365 | stub_changeset_lookup
366 | stub_user_lookup
367 | stub_node_lookup
368 | end
369 |
370 | it "should save a edited node" do
371 | stub_request(:put, "https://www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => '43', :headers => {'Content-Type' => 'text/plain'})
372 | node.tags['amenity'] = 'restaurant'
373 | node.tags['name'] = 'Il Tramonto'
374 | expect(node).to receive(:changeset=)
375 | new_version = osm.save(node, changeset)
376 | expect(new_version).to eql 43
377 | end
378 | end
379 |
380 | describe '#delete:' do
381 |
382 | let :node do
383 | osm.find_node 123
384 | end
385 |
386 | before do
387 | stub_changeset_lookup
388 | stub_user_lookup
389 | stub_node_lookup
390 | end
391 |
392 | it "should delete an existing node" do
393 | stub_request(:delete, "https://www.openstreetmap.org/api/0.6/node/123").to_return(:status => 200, :body => '43', :headers => {'Content-Type' => 'text/plain'})
394 | expect(node).to receive(:changeset=)
395 | expect {
396 | # Delete is not implemented using oauth
397 | new_version = osm.destroy(node, changeset)
398 | }.to raise_exception(NotImplemented)
399 | end
400 | end
401 | end
402 | end
--------------------------------------------------------------------------------