├── .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 | [![Gem Version](https://badge.fury.io/rb/rosemary.svg)](http://badge.fury.io/rb/rosemary) 4 | [![Build Status](https://travis-ci.org/sozialhelden/rosemary.svg?branch=master)](https://travis-ci.org/sozialhelden/rosemary) 5 | [![Dependency Status](https://gemnasium.com/sozialhelden/rosemary.png)](https://gemnasium.com/sozialhelden/rosemary) 6 | [![Coverage Status](https://codeclimate.com/github/sozialhelden/rosemary/badges/coverage.svg)](https://codeclimate.com/github/sozialhelden/rosemary/coverage) 7 | [![Code Climate](https://codeclimate.com/github/sozialhelden/rosemary.svg)](https://codeclimate.com/github/sozialhelden/rosemary) 8 | [![License](https://img.shields.io/badge/license-MIT-green.svg) ](https://github.com/sozialhelden/rosemary/blob/master/LICENSE) 9 | [![Gittip ](https://img.shields.io/gittip/sozialhelden.svg)](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 --------------------------------------------------------------------------------