├── .rspec ├── History.txt ├── .gitignore ├── lib ├── postgis_adapter │ ├── railtie.rb │ ├── acts_as_geom.rb │ ├── functions │ │ ├── class.rb │ │ ├── bbox.rb │ │ └── common.rb │ ├── functions.rb │ └── common_spatial_adapter.rb └── postgis_adapter.rb ├── Guardfile ├── Gemfile ├── postgis_adapter.gemspec ├── spec ├── postgis_adapter │ ├── acts_as_geom_spec.rb │ ├── functions │ │ ├── bbox_spec.rb │ │ ├── class_spec.rb │ │ └── common_spec.rb │ ├── functions_spec.rb │ └── common_spatial_adapter_spec.rb ├── spec_helper.rb ├── db │ ├── models_postgis.rb │ └── schema_postgis.rb └── postgis_adapter_spec.rb ├── rails └── init.rb ├── Gemfile.lock ├── MIT-LICENSE ├── Rakefile └── README.rdoc /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 1.0.0 / 2008-12-10 2 | 3 | * 1 major enhancement 4 | 5 | * Birthday! 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | doc/* 3 | rdoc/* 4 | coverage/* 5 | spec/debug.log 6 | pkg/* 7 | *flymake.rb 8 | *.gem 9 | .rvmrc 10 | -------------------------------------------------------------------------------- /lib/postgis_adapter/railtie.rb: -------------------------------------------------------------------------------- 1 | module PostgisAdapter 2 | class Railtie < Rails::Railtie 3 | initializer "postgis adapter" do 4 | require "postgis_adapter" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { 'spec' } 7 | end 8 | 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | # Add dependencies required to use your gem here. 3 | gem 'pg' 4 | gem 'nofxx-georuby' 5 | 6 | # Add dependencies to develop your gem here. 7 | # Include everything needed to run rake, tests, features, etc. 8 | group :development do 9 | gem 'rake' 10 | gem 'rspec', '~> 2.3.0' 11 | gem 'bundler', '~> 1.0.0' 12 | gem 'rcov', '>= 0' 13 | gem 'guard-rspec' 14 | gem 'rb-fsevent' 15 | end 16 | -------------------------------------------------------------------------------- /postgis_adapter.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'postgis_adapter' 3 | spec.version = '0.8.1' 4 | spec.authors = ['Marcos Piccinini'] 5 | spec.summary = 'PostGIS Adapter for Active Record' 6 | spec.email = 'x@nofxx.com' 7 | spec.homepage = 'http://github.com/nofxx/postgis_adapter' 8 | 9 | spec.rdoc_options = ['--charset=UTF-8'] 10 | spec.rubyforge_project = 'postgis_adapter' 11 | 12 | spec.files = Dir['**/*'].reject{ |f| f.include?('git') } 13 | spec.test_files = Dir['spec/**/*.rb'] 14 | spec.extra_rdoc_files = ['README.rdoc'] 15 | 16 | spec.add_dependency 'nofxx-georuby' 17 | 18 | spec.description = 'Execute PostGIS functions on Active Record' 19 | end 20 | -------------------------------------------------------------------------------- /spec/postgis_adapter/acts_as_geom_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper.rb' 2 | 3 | class DiffColumn < ActiveRecord::Base 4 | acts_as_geom :ponto => :point 5 | end 6 | 7 | class NotInDb < ActiveRecord::Base 8 | acts_as_geom :geom 9 | end 10 | 11 | describe "ActsAsGeom" do 12 | 13 | it "should get the geom type" do 14 | City.connection.columns("cities").select { |c| c.name == "geom" }[0] 15 | City.get_geom_type(:geom).should eql(:polygon) 16 | end 17 | 18 | it "should get the geom type" do 19 | Position.get_geom_type(:geom).should eql(:point) 20 | end 21 | 22 | it "should not interfere with migrations" do 23 | NotInDb.get_geom_type(:geom).should be_nil 24 | end 25 | 26 | it "should query a diff column name" do 27 | # DiffColumn 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | # Patch Arel to support geometry type. 2 | module Arel 3 | module Attributes 4 | class << self 5 | alias original_for for 6 | 7 | def for(column) 8 | case column.type 9 | when :geometry then String 10 | else 11 | original_for(column) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | 18 | class SpatialAdapterNotCompatibleError < StandardError 19 | end 20 | 21 | unless ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql' 22 | error_message = "Database config file not set or it does not map to " 23 | error_message << "PostgreSQL.\nOnly PostgreSQL with PostGIS is supported " 24 | error_message << "by postgis_adapter.") 25 | raise SpatialAdapterNotCompatibleError.new(error_message) 26 | end 27 | 28 | require 'postgis_adapter' 29 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | diff-lcs (1.1.3) 5 | ffi (1.11.1) 6 | guard (1.0.0) 7 | ffi (>= 0.5.0) 8 | thor (~> 0.14.6) 9 | guard-rspec (0.6.0) 10 | guard (>= 0.10.0) 11 | nofxx-georuby (1.9.2) 12 | pg (0.12.2) 13 | rake (0.9.2.2) 14 | rb-fsevent (0.4.3.1) 15 | rcov (1.0.0) 16 | rspec (2.3.0) 17 | rspec-core (~> 2.3.0) 18 | rspec-expectations (~> 2.3.0) 19 | rspec-mocks (~> 2.3.0) 20 | rspec-core (2.3.1) 21 | rspec-expectations (2.3.0) 22 | diff-lcs (~> 1.1.2) 23 | rspec-mocks (2.3.0) 24 | thor (0.14.6) 25 | 26 | PLATFORMS 27 | ruby 28 | 29 | DEPENDENCIES 30 | bundler (~> 1.0.0) 31 | guard-rspec 32 | nofxx-georuby 33 | pg 34 | rake 35 | rb-fsevent 36 | rcov 37 | rspec (~> 2.3.0) 38 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Spatial Adapter Copyright (c) 2006 Guilhem Vellut 2 | PostGis Adapter Functions (c) 2008 Marcos Piccinini 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | SPEC_DB = { 2 | :adapter => "postgresql", 3 | :database => "postgis_adapter", 4 | :username => "postgres", 5 | :password => "" 6 | } 7 | 8 | require 'rubygems' 9 | require 'pg' 10 | $:.unshift((File.join(File.dirname(__FILE__), '..', 'lib'))) 11 | 12 | require "rspec" 13 | require "active_record" 14 | 15 | gem 'nofxx-georuby' 16 | require 'postgis_adapter' 17 | require 'logger' 18 | # GeoRuby::SimpleFeatures::DEFAULT_SRID = -1 19 | 20 | # Monkey patch Schema.define logger 21 | $logger = Logger.new(StringIO.new) 22 | def $logger.write(d); self.info(d); end 23 | # $stdout = $logger 24 | 25 | ActiveRecord::Base.logger = $logger 26 | 27 | begin 28 | ActiveRecord::Base.establish_connection(SPEC_DB) 29 | ActiveRecord::Migration.verbose = false 30 | PG_VERSION = ActiveRecord::Base.connection.select_value("SELECT version()").scan(/PostgreSQL ([\d\.]*)/)[0][0] 31 | 32 | puts "Running against PostgreSQL #{PG_VERSION}" 33 | 34 | require File.dirname(__FILE__) + '/db/schema_postgis.rb' 35 | require File.dirname(__FILE__) + '/db/models_postgis.rb' 36 | 37 | rescue PGError 38 | puts "Test DB not found, creating one for you..." 39 | `createdb -U #{SPEC_DB[:username]} #{SPEC_DB[:database]} -T template_postgis` 40 | puts "Done. Please run spec again." 41 | exit 42 | end 43 | -------------------------------------------------------------------------------- /lib/postgis_adapter/acts_as_geom.rb: -------------------------------------------------------------------------------- 1 | # 2 | # PostGIS Adapter 3 | # 4 | # http://github.com/nofxx/postgis_adapter 5 | # 6 | module PostgisAdapter 7 | module Functions 8 | def self.included(base) 9 | base.send :extend, ClassMethods 10 | end 11 | 12 | module ClassMethods 13 | 14 | # has_geom :db_field => :geom_type 15 | # Examples: 16 | # 17 | # has_geom :data => :point 18 | # has_geom :geom => :line_string 19 | # has_geom :geom => :polygon 20 | # 21 | def has_geom(*geom) 22 | cattr_accessor :postgis_geoms 23 | self.postgis_geoms = geom[0] # {:columns => column 24 | send :include, case geom[0].values[0] 25 | when :point then PointFunctions 26 | when :polygon then PolygonFunctions 27 | when :line_string, :multi_line_string then LineStringFunctions 28 | when :multi_polygon then MultiPolygonFunctions 29 | when :geometry then GeometryFunctions 30 | end unless geom[0].kind_of? Symbol 31 | end 32 | alias :acts_as_geom :has_geom 33 | 34 | def get_geom_type(column) 35 | self.postgis_geoms.values[0] rescue nil 36 | # self.columns.select { |c| c.name == column.to_s }[0].geometry_type 37 | # rescue ActiveRecord::StatementInvalid => e 38 | # nil 39 | end 40 | end 41 | end 42 | end 43 | 44 | ActiveRecord::Base.send :include, PostgisAdapter::Functions 45 | -------------------------------------------------------------------------------- /spec/db/models_postgis.rb: -------------------------------------------------------------------------------- 1 | class TablePoint < ActiveRecord::Base 2 | end 3 | 4 | class TableKeywordColumnPoint < ActiveRecord::Base 5 | end 6 | 7 | class TableLineString < ActiveRecord::Base 8 | end 9 | 10 | class TablePolygon < ActiveRecord::Base 11 | end 12 | 13 | class TableMultiPoint < ActiveRecord::Base 14 | end 15 | 16 | class TableMultiLineString < ActiveRecord::Base 17 | end 18 | 19 | class TableMultiPolygon < ActiveRecord::Base 20 | end 21 | 22 | class TableGeometry < ActiveRecord::Base 23 | end 24 | 25 | class TableGeometryCollection < ActiveRecord::Base 26 | end 27 | 28 | class Table3dzPoint < ActiveRecord::Base 29 | end 30 | 31 | class Table3dmPoint < ActiveRecord::Base 32 | end 33 | 34 | class Table4dPoint < ActiveRecord::Base 35 | end 36 | 37 | class TableSridLineString < ActiveRecord::Base 38 | end 39 | 40 | class TableSrid4dPolygon < ActiveRecord::Base 41 | end 42 | 43 | class City < ActiveRecord::Base 44 | acts_as_geom :geom => :polygon 45 | end 46 | 47 | class Position < ActiveRecord::Base 48 | acts_as_geom :geom => :point 49 | end 50 | 51 | class Street < ActiveRecord::Base 52 | acts_as_geom :geom => :line_string 53 | end 54 | 55 | class Road < ActiveRecord::Base 56 | acts_as_geom :geom => :multi_line_string 57 | end 58 | 59 | class CommonGeo < ActiveRecord::Base 60 | acts_as_geom :geom => :point 61 | end 62 | 63 | class DiffName < ActiveRecord::Base 64 | acts_as_geom :the_geom => :point 65 | end 66 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/clean' 4 | require 'rake/task' 5 | #require 'spec/rake/spectask' 6 | 7 | CLEAN.include('**/*.gem') 8 | 9 | namespace :gem do 10 | desc "Create the postgis_adapter gem" 11 | task :create => [:clean] do 12 | spec = eval(IO.read('postgis_adapter.gemspec')) 13 | Gem::Builder.new(spec).build 14 | end 15 | 16 | desc "Install the postgis_adapter gem" 17 | task :install => [:create] do 18 | file = Dir['*.gem'].first 19 | sh "gem install #{file}" 20 | end 21 | end 22 | 23 | # Spec::Rake::SpecTask.new(:spec) do |spec| 24 | # spec.libs << 'lib' << 'spec' 25 | # spec.spec_files = FileList['spec/**/*_spec.rb'] 26 | # end 27 | 28 | # Spec::Rake::SpecTask.new(:rcov) do |spec| 29 | # spec.libs << 'lib' << 'spec' 30 | # spec.pattern = 'spec/**/*_spec.rb' 31 | # spec.rcov = true 32 | # end 33 | 34 | Rake::Task.new do |rdoc| 35 | version = File.exist?('VERSION') ? File.read('VERSION').chomp : "" 36 | rdoc.rdoc_dir = 'rdoc' 37 | rdoc.title = "postgis_adapter #{version}" 38 | rdoc.rdoc_files.include('README*') 39 | rdoc.rdoc_files.include('lib/**/*.rb') 40 | end 41 | 42 | task :default => :spec 43 | 44 | # 45 | # Reek & Roodi 46 | # 47 | begin 48 | require 'reek/rake_task' 49 | Reek::RakeTask.new do |t| 50 | t.fail_on_error = true 51 | t.verbose = false 52 | t.source_files = 'lib/**/*.rb' 53 | end 54 | rescue LoadError 55 | task :reek do 56 | abort "Reek is not available. In order to run reek, you must: sudo gem install reek" 57 | end 58 | end 59 | 60 | begin 61 | require 'roodi' 62 | require 'roodi_task' 63 | RoodiTask.new do |t| 64 | t.verbose = false 65 | end 66 | rescue LoadError 67 | task :roodi do 68 | abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi" 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/postgis_adapter/functions/class.rb: -------------------------------------------------------------------------------- 1 | module PostgisAdapter 2 | module Functions 3 | 4 | # 5 | # Class Methods 6 | # 7 | module ClassMethods 8 | 9 | # 10 | # Returns the closest record 11 | def closest_to(p, opts = {}) 12 | srid = opts.delete(:srid) || 4326 13 | opts.merge!(:order => "ST_Distance(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))") 14 | find(:first, opts) 15 | end 16 | 17 | # 18 | # Order by distance 19 | def close_to(p, opts = {}) 20 | srid = opts.delete(:srid) || 4326 21 | opts.merge!(:order => "ST_Distance(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))") 22 | find(:all, opts) 23 | end 24 | 25 | def by_length opts = {} 26 | sort = opts.delete(:sort) || 'asc' 27 | opts.merge!(:order => "ST_length(geom) #{sort}") 28 | find(:all, opts) 29 | end 30 | 31 | def longest 32 | find(:first, :order => "ST_length(geom) DESC") 33 | end 34 | 35 | def contains(p, srid=4326) 36 | find(:all, :conditions => ["ST_Contains(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))"]) 37 | end 38 | 39 | def contain(p, srid=4326) 40 | find(:first, :conditions => ["ST_Contains(geom, GeomFromText('POINT(#{p.x} #{p.y})', #{srid}))"]) 41 | end 42 | 43 | def by_area sort='asc' 44 | find(:all, :order => "ST_Area(geom) #{sort}" ) 45 | end 46 | 47 | def by_perimeter sort='asc' 48 | find(:all, :order => "ST_Perimeter(geom) #{sort}" ) 49 | end 50 | 51 | def all_dwithin(other, margin=1) 52 | # find(:all, :conditions => "ST_DWithin(geom, ST_GeomFromEWKB(E'#{other.as_ewkt}'), #{margin})") 53 | find(:all, :conditions => "ST_DWithin(geom, ST_GeomFromEWKT(E'#{other.as_hex_ewkb}'), #{margin})") 54 | end 55 | 56 | def all_within(other) 57 | find(:all, :conditions => "ST_Within(geom, ST_GeomFromEWKT(E'#{other.as_hex_ewkb}'))") 58 | end 59 | 60 | def by_boundaries sort='asc' 61 | find(:all, :order => "ST_Boundary(geom) #{sort}" ) 62 | end 63 | 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/postgis_adapter/functions/bbox_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper.rb' 2 | 3 | describe "Point" do 4 | 5 | before(:all) do 6 | @c1 ||= City.create!(:data => "City1", :geom => Polygon.from_coordinates([[[12,45],[45,41],[4,1],[12,45]],[[2,5],[5,1],[14,1],[2,5]]],4326)) 7 | @c2 ||= City.create!(:data => "City1", :geom => Polygon.from_coordinates([[[22,66],[65,65],[20,10],[22,66]],[[10,15],[15,11],[34,14],[10,15]]],4326)) 8 | @c3 ||= City.create!(:data => "City3", :geom => Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]],4326)) 9 | @s1 ||= Street.create!(:data => "Street1", :geom => LineString.from_coordinates([[1,1],[2,2]],4326)) 10 | @s2 ||= Street.create!(:data => "Street2", :geom => LineString.from_coordinates([[4,4],[7,7]],4326)) 11 | @s3 ||= Street.create!(:data => "Street3", :geom => LineString.from_coordinates([[8,8],[18,18],[20,20],[25,25],[30,30],[38,38]],4326)) 12 | @s4 ||= Street.create!(:data => "Street3", :geom => LineString.from_coordinates([[10,8],[15,18]],4326)) 13 | @p1 ||= Position.create!(:data => "Point1", :geom => Point.from_x_y(1,1,4326)) 14 | @p2 ||= Position.create!(:data => "Point2", :geom => Point.from_x_y(5,5,4326)) 15 | @p3 ||= Position.create!(:data => "Point3", :geom => Point.from_x_y(8,8,4326)) 16 | end 17 | 18 | describe "BBox operations" do 19 | 20 | it "should check stricly left" do 21 | @p1.bbox("<<", @c1).should be_true 22 | end 23 | 24 | it "should check stricly right" do 25 | @p1.bbox(">>", @c1).should be_false 26 | end 27 | 28 | it { @p1.should be_strictly_left_of(@c1) } 29 | it { @p1.should_not be_strictly_right_of(@c1) } 30 | it { @p1.should_not be_overlaps_or_right_of(@c1) } 31 | it { @p1.should be_overlaps_or_left_of(@c1) } 32 | it { @p1.should_not be_completely_contained_by(@c1) } 33 | it { @c2.completely_contains?(@p1).should be_false } 34 | it { @p1.should be_overlaps_or_above(@c1) } 35 | it { @p1.should be_overlaps_or_below(@c1) } 36 | it { @p1.should_not be_strictly_above(@c1) } 37 | it { @p1.should_not be_strictly_below(@c1) } 38 | it { @p1.interacts_with?(@c1).should be_false } 39 | 40 | it { @p1.binary_equal?(@c1).should be_false } 41 | it { @p1.same_as?(@c1).should be_false } 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /spec/postgis_adapter/functions_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper.rb' 2 | 3 | describe "PostgisFunctions" do 4 | before(:all) do 5 | @c1 ||= City.create!(:data => "City1", :geom => Polygon.from_coordinates([[[12,45],[45,42],[4,1],[12,45]],[[2,5],[5,1],[14,1],[2,5]]],4326)) 6 | @s1 ||= Street.create!(:data => "Street1", :geom => LineString.from_coordinates([[-43,-20],[-42,-28]],4326)) 7 | @p1 ||= Position.create!(:data => "Point1", :geom => Point.from_x_y(-43,-22,4326)) 8 | @cg ||= CommonGeo.create!(:data => "Point1", :geom => Point.from_x_y(-43,-22,4326)) 9 | @px = DiffName.create!(:data => "Hey", :the_geom => Point.from_x_y(10,20, 4326)) 10 | end 11 | 12 | describe "Common Mix" do 13 | 14 | it "should calculate distance point to line" do 15 | @p1.distance_to(@s1).should be_within(0.00000001).of(0.248069469178417) 16 | end 17 | 18 | it "should calculate distance point to line" do 19 | @cg.distance_to(@s1).should be_within(0.00000001).of(0.248069469178417) 20 | end 21 | 22 | it "should calculate distance point to line" do 23 | @p1.geom.as_kml.should eql("\n-43,-22\n\n") 24 | end 25 | 26 | it "should calculate inside a city" do 27 | @p1.should_not be_inside(@c1) 28 | end 29 | 30 | it "should find the distance from a unsaved point" do 31 | @p1.distance_to(Point.from_x_y(5,5,4326)).should be_within(0.00001).of(55.0726792520575) 32 | end 33 | 34 | it "should work with unsaved objects" do 35 | ss = Street.new(:data => "Street1", :geom => LineString.from_coordinates([[-44,-21],[-43,-29]],4326)) 36 | ss.length_spheroid.should be_within(0.01).of(891908.39) 37 | end 38 | 39 | it { @c1.area(32640).should be_within(0.01).of(9165235788987.37) } 40 | 41 | it { @c1.area.should be_within(0.1).of(720.0) } 42 | 43 | it "should be strictly left of city" do 44 | @p1.should be_strictly_left_of(@c1) 45 | end 46 | 47 | it { @s1.length.should be_within(0.0001).of(8.06225774829855) } 48 | 49 | it { @s1.length_spheroid.should be_within(0.0001).of(891883.597963462) } 50 | 51 | it "should work with a diff column name" do 52 | px2 = DiffName.create!(:data => "Hey 2", :the_geom => Point.from_x_y(20,20, 4326)) 53 | @px.distance_to(px2).should be_within(0.01).of(10.0) 54 | end 55 | 56 | it "should work with mixed column names" do 57 | @px.distance_to(@s1).should be_within(0.1).of(66.4) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/postgis_adapter/functions/class_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper.rb' 2 | 3 | describe "ClassMethods" do 4 | before(:all) do 5 | @c1 ||= City.create!(:data => "CityClass", :geom => Polygon.from_coordinates([[[12,45],[45,41],[4,1],[12,45]],[[2,5],[5,1],[14,1],[2,5]]],4326)) 6 | @c2 ||= City.create!(:data => "CityClass", :geom => Polygon.from_coordinates([[[10,10],[10,50],[50,50],[10,10]],[[2,5],[5,1],[14,1],[2,5]]],4326)) 7 | @s1 ||= Street.create!(:data => "StreetClass", :geom => LineString.from_coordinates([[1,1],[99,88]],4326)) 8 | @s2 ||= Street.create!(:data => "StreetClassTiny", :geom => LineString.from_coordinates([[1,1],[1.1,1.1]],4326)) 9 | @p1 ||= Position.create!(:data => "PointClass", :geom => Point.from_x_y(99,99,4326)) 10 | @p2 ||= Position.create!(:data => "PointClassClose", :geom => Point.from_x_y(99.9,99.9,4326)) 11 | @p3 ||= Position.create!(:data => "PointInsideCity", :geom => Point.from_x_y(15.0,15.0,4326)) 12 | end 13 | 14 | after(:all) do 15 | [City, Street, Position].each { |m| m.delete_all } 16 | end 17 | 18 | it "should find the closest other point" do 19 | Position.close_to(Point.from_x_y(99,99,4326), :srid => 4326)[0].data.should == @p1.data 20 | end 21 | 22 | it "should find the closest other point and limit" do 23 | Position.close_to(Point.from_x_y(99,99,4326), :limit => 2).should have(2).positions 24 | end 25 | 26 | it "should find the closest other point" do 27 | Position.closest_to(Point.from_x_y(99,99,4326)).data.should == @p1.data 28 | end 29 | 30 | it "should sort by size" do 31 | Street.by_length.first.data.should == "StreetClassTiny" 32 | Street.by_length.last.data.should == "StreetClass" 33 | end 34 | 35 | it "largest" do 36 | Street.longest.data.should == "StreetClass" 37 | end 38 | 39 | it "should sort by linestring length" do 40 | Street.by_length.should be_instance_of(Array) 41 | end 42 | 43 | it "should sort by linestring length" do 44 | Street.by_length(:limit => 2).should have(2).streets 45 | end 46 | 47 | it "should find the longest" do 48 | Street.longest.should == @s1 49 | end 50 | 51 | it "should find all dwithin one" do 52 | Position.all_within(@s1.geom).should be_instance_of(Array) 53 | end 54 | 55 | it "should find all dwithin one" do 56 | City.by_perimeter.should be_instance_of(Array) 57 | end 58 | 59 | it "should sort by polygon area" do 60 | City.by_area.should be_instance_of(Array) 61 | end 62 | 63 | it "should sort by all dwithin" do 64 | City.all_dwithin(@s1.geom).should eql([@c1, @c2]) 65 | end 66 | 67 | it "should find all within polygon" do 68 | Position.all_within(@c1.geom).should eql([@p3])#Array) 69 | end 70 | 71 | it "should find all within polygon 2" do 72 | Position.all_within(@c2.geom).should eql([])#Array) 73 | end 74 | 75 | it "should sort by all within" do 76 | City.by_boundaries.should be_instance_of(Array) 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /lib/postgis_adapter/functions/bbox.rb: -------------------------------------------------------------------------------- 1 | ### 2 | ## 3 | # 4 | # BBox 5 | # 6 | # 7 | module PostgisAdapter 8 | module Functions 9 | 10 | # 11 | # These operators utilize indexes. They compare geometries by bounding boxes. 12 | # 13 | # You can use the literal forms or call directly using the 'bbox' method. eg.: 14 | # 15 | # @point.bbox(">>", @area) 16 | # @point.bbox("|&>", @area) 17 | # 18 | # 19 | # Cheatsheet: 20 | # 21 | # A &< B => A overlaps or is to the left of B 22 | # A &> B => A overlaps or is to the right of B 23 | # A << B => A is strictly to the left of B 24 | # A >> B => A is strictly to the right of B 25 | # A &<| B => A overlaps B or is below B 26 | # A |&> B => A overlaps or is above B 27 | # A <<| B => A strictly below B 28 | # A |>> B => A strictly above B 29 | # A = B => A bbox same as B bbox 30 | # A @ B => A completely contained by B 31 | # A ~ B => A completely contains B 32 | # A && B => A and B bboxes interact 33 | # A ~= B => A and B geometries are binary equal? 34 | # 35 | def bbox(operator, other) 36 | postgis_calculate(:bbox, [self, other], operator) 37 | end 38 | 39 | # 40 | # bbox literal method. 41 | # 42 | def completely_contained_by? other 43 | bbox("@", other) 44 | end 45 | 46 | # 47 | # bbox literal method. 48 | # 49 | def completely_contains? other 50 | bbox("~", other) 51 | end 52 | 53 | # 54 | # bbox literal method. 55 | # 56 | def overlaps_or_above? other 57 | bbox("|&>", other) 58 | end 59 | 60 | # 61 | # bbox literal method. 62 | # 63 | def overlaps_or_below? other 64 | bbox("&<|", other) 65 | end 66 | 67 | # 68 | # bbox literal method. 69 | # 70 | def overlaps_or_left_of? other 71 | bbox("&<", other) 72 | end 73 | 74 | # 75 | # bbox literal method. 76 | # 77 | def overlaps_or_right_of? other 78 | bbox("&>", other) 79 | end 80 | 81 | # 82 | # bbox literal method. 83 | # 84 | def strictly_above? other 85 | bbox("|>>", other) 86 | end 87 | 88 | # 89 | # bbox literal method. 90 | # 91 | def strictly_below? other 92 | bbox("<<|", other) 93 | end 94 | 95 | # 96 | # bbox literal method. 97 | # 98 | def strictly_left_of? other 99 | bbox("<<", other) 100 | end 101 | 102 | # 103 | # bbox literal method. 104 | # 105 | def strictly_right_of? other 106 | bbox(">>", other) 107 | end 108 | 109 | # 110 | # bbox literal method. 111 | # 112 | def interacts_with? other 113 | bbox("&&", other) 114 | end 115 | 116 | # 117 | # bbox literal method. 118 | # 119 | def binary_equal? other 120 | bbox("~=", other) 121 | end 122 | 123 | # 124 | # bbox literal method. 125 | # 126 | def same_as? other 127 | bbox("=", other) 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/db/schema_postgis.rb: -------------------------------------------------------------------------------- 1 | #add some postgis specific tables 2 | ActiveRecord::Schema.define() do 3 | 4 | create_table "table_points", :force => true do |t| 5 | t.string "data" 6 | t.point "geom", :null=>false, :srid => 4326 7 | end 8 | 9 | create_table "table_keyword_column_points", :force => true do |t| 10 | t.point "location", :null => false, :srid => 4326 11 | end 12 | 13 | create_table "table_line_strings", :force => true do |t| 14 | t.integer "value" 15 | t.line_string "geom", :null=>false, :srid => 4326 16 | end 17 | 18 | create_table "table_polygons", :force => true do |t| 19 | t.polygon "geom", :null=>false, :srid => 4326 20 | end 21 | 22 | create_table "table_multi_points", :force => true do |t| 23 | t.multi_point "geom", :null=>false, :srid => 4326 24 | end 25 | 26 | create_table "table_multi_line_strings", :force => true do |t| 27 | t.multi_line_string "geom", :null=>false, :srid => 4326 28 | end 29 | 30 | create_table "table_multi_polygons", :force => true do |t| 31 | t.multi_polygon "geom", :null=>false, :srid => 4326 32 | end 33 | 34 | create_table "table_geometries", :force => true do |t| 35 | t.geometry "geom", :null=>false, :srid => 4326 36 | end 37 | 38 | create_table "table_geometry_collections", :force => true do |t| 39 | t.geometry_collection "geom", :null=>false, :srid => 4326 40 | end 41 | 42 | create_table "table3dz_points", :force => true do |t| 43 | t.column "data", :string 44 | t.point "geom", :null => false , :with_z => true, :srid => 4326 45 | end 46 | 47 | create_table "table3dm_points", :force => true do |t| 48 | t.point "geom", :null => false , :with_m => true, :srid => 4326 49 | end 50 | 51 | create_table "table4d_points", :force => true do |t| 52 | t.point "geom", :null => false, :with_m => true, :with_z => true, :srid => 4326 53 | end 54 | 55 | create_table "table_srid_line_strings", :force => true do |t| 56 | t.line_string "geom", :null => false , :srid => 4326 57 | end 58 | 59 | create_table "table_srid4d_polygons", :force => true do |t| 60 | t.polygon "geom", :with_m => true, :with_z => true, :srid => 4326 61 | end 62 | 63 | create_table :cities, :force => true do |t| 64 | t.string :data, :limit => 100 65 | t.integer :value 66 | t.polygon :geom, :null => false, :srid => 4326 67 | end 68 | 69 | create_table :positions, :force => true do |t| 70 | t.string :data, :limit => 100 71 | t.integer :value 72 | t.point :geom, :null => false, :srid => 4326 73 | end 74 | 75 | create_table :streets, :force => true do |t| 76 | t.string :data, :limit => 100 77 | t.integer :value 78 | t.line_string :geom, :null => false, :srid => 4326 79 | end 80 | 81 | create_table :roads, :force => true do |t| 82 | t.string :data, :limit => 100 83 | t.integer :value 84 | t.multi_line_string :geom, :null => false, :srid => 4326 85 | end 86 | 87 | create_table :common_geos, :force => true do |t| 88 | t.string :data, :limit => 100 89 | t.integer :value 90 | t.point :geom, :null => false, :srid => 4326 91 | end 92 | 93 | create_table :diff_names, :force => true do |t| 94 | t.string :data 95 | t.point :the_geom, :null => false, :srid => 4326 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/postgis_adapter/functions.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PostGIS Adapter - http://github.com/nofxx/postgis_adapter 4 | # 5 | # Hope you enjoy this plugin. 6 | # 7 | # 8 | # Post any bugs/suggestions to GitHub issues tracker: 9 | # http://github.com/nofxx/postgis_adapter/issues 10 | # 11 | # 12 | # Some links: 13 | # 14 | # PostGis Manual - http://postgis.refractions.net/documentation/manual-svn 15 | # Earth Spheroid - http://en.wikipedia.org/wiki/Figure_of_the_Earth 16 | # 17 | 18 | module PostgisAdapter 19 | module Functions 20 | # WGS84 Spheroid 21 | EARTH_SPHEROID = "'SPHEROID[\"GRS-80\",6378137,298.257222101]'" # SRID => 4326 22 | 23 | def postgis_calculate(operation, subjects, options = {}) 24 | subjects = [subjects] unless subjects.respond_to?(:map) 25 | execute_geometrical_calculation(operation, subjects, options) 26 | end 27 | 28 | def geo_columns 29 | @geo_columns ||= postgis_geoms[:columns] 30 | end 31 | 32 | private 33 | 34 | # 35 | # Construct the PostGIS SQL query 36 | # 37 | # Returns: 38 | # Area/Distance/DWithin/Length/Perimeter => projected units 39 | # DistanceSphere/Spheroid => meters 40 | # 41 | def construct_geometric_sql(type,geoms,options) 42 | not_db, on_db = geoms.partition { |g| g.is_a?(Geometry) || g.new_record? } 43 | not_db.map! {|o| o.respond_to?(:new_record?) ? o.geom : o } 44 | 45 | tables = on_db.map do |t| { 46 | :name => t.class.table_name, 47 | :column => t.postgis_geoms.keys[0], 48 | :uid => unique_identifier, 49 | :primary_key => t.class.primary_key, 50 | :id => t[:id] } 51 | end 52 | 53 | # Implement a better way for options? 54 | if options.instance_of? Hash 55 | transform = options.delete(:transform) 56 | stcollect = options.delete(:stcollect) 57 | options = nil 58 | end 59 | 60 | fields = tables.map { |f| "#{f[:uid]}.#{f[:column]}" } # W1.geom 61 | fields << not_db.map { |g| "'#{g.as_hex_ewkb}'::geometry"} unless not_db.empty? 62 | fields.map! { |f| "ST_Transform(#{f}, #{transform})" } if transform # ST_Transform(W1.geom,x) 63 | fields.map! { |f| "ST_Union(#{f})" } if stcollect # ST_Transform(W1.geom,x) 64 | conditions = tables.map {|f| "#{f[:uid]}.#{f[:primary_key]} = #{f[:id]}" } # W1.id = 5 65 | 66 | tables.map! { |f| "#{f[:name]} #{f[:uid]}" } # streets W1 67 | 68 | # 69 | # Data => SELECT Func(A,B) 70 | # BBox => SELECT (A <=> B) 71 | # Func => SELECT Func(Func(A)) 72 | # 73 | if type != :bbox 74 | opcode = type.to_s 75 | opcode = "ST_#{opcode}" unless opcode =~ /th3d|pesinter/ 76 | fields << options if options 77 | fields = fields.join(",") 78 | else 79 | fields = fields.join(" #{options} ") 80 | end 81 | 82 | 83 | sql = "SELECT #{opcode}(#{fields}) " 84 | sql << "FROM #{tables.join(",")} " unless tables.empty? 85 | sql << "WHERE #{conditions.join(" AND ")}" unless conditions.empty? 86 | sql 87 | end 88 | 89 | # 90 | # Execute the query and parse the return. 91 | # We may receive: 92 | # 93 | # "t" or "f" for boolean queries 94 | # BIGHASH for geometries 95 | # HASH for ST_Relate 96 | # Rescue a float 97 | # 98 | def execute_geometrical_calculation(operation, subject, options) #:nodoc: 99 | value = connection.select_value(construct_geometric_sql(operation, subject, options)) 100 | return nil unless value 101 | # TODO: bench case vs if here 102 | if value =~ /^[tf]$/ 103 | {"f" => false, "t" => true}[value] 104 | elsif value =~ /^\{/ 105 | value 106 | else 107 | GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(value) rescue value 108 | end 109 | rescue Exception => e 110 | raise StandardError, e.to_s #+ e.backtrace.inspect 111 | end 112 | 113 | # Get a unique ID for tables 114 | def unique_identifier 115 | @u_id ||= "T1" 116 | @u_id = @u_id.succ 117 | end 118 | 119 | end 120 | end 121 | # 122 | # POINT(0 0) 123 | # LINESTRING(0 0,1 1,1 2) 124 | # POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1)) 125 | # MULTIPOINT(0 0,1 2) 126 | # MULTILINESTRING((0 0,1 1,1 2),(2 3,3 2,5 4)) 127 | # MULTIPOLYGON(((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1)), ..) 128 | # GEOMETRYCOLLECTION(POINT(2 3),LINESTRING((2 3,3 4))) 129 | # 130 | #Accessors 131 | # 132 | #ST_Dump 133 | #ST_ExteriorRing 134 | #ST_GeometryN 135 | #ST_GeometryType 136 | #ST_InteriorRingN 137 | #ST_IsEmpty 138 | #ST_IsRing 139 | #ST_IsSimple 140 | #ST_IsValid 141 | #ST_mem_size 142 | #ST_M 143 | #ST_NumGeometries 144 | #ST_NumInteriorRings 145 | #ST_PointN 146 | #ST_SetSRID 147 | #ST_Summary1 148 | #ST_X 149 | #ST_XMin,ST_XMax 150 | #ST_Y 151 | #YMin,YMax 152 | #ST_Z 153 | #ZMin,ZMax 154 | 155 | #OUTPUT 156 | 157 | #ST_AsBinary 158 | #ST_AsText 159 | #ST_AsEWKB 160 | #ST_AsEWKT 161 | #ST_AsHEXEWKB 162 | #ST_AsGML 163 | #ST_AsKML 164 | #ST_AsSVG 165 | # #EARTH_SPHEROID = "'SPHEROID[\"IERS_2003\",6378136.6,298.25642]'" # SRID => 166 | # def distance_convert(value, unit, from = nil) 167 | # factor = case unit 168 | # when :km, :kilo then 1 169 | # when :miles,:mile then 0.62137119 170 | # when :cm, :cent then 0.1 171 | # when :nmi, :nmile then 0.5399568 172 | # end 173 | # factor *= 1e3 if from 174 | # value * factor 175 | # end #use all commands in lowcase form 176 | #opcode = opcode.camelize unless opcode =~ /spher|max|npoints/ 177 | -------------------------------------------------------------------------------- /lib/postgis_adapter/common_spatial_adapter.rb: -------------------------------------------------------------------------------- 1 | # 2 | # PostGIS Adapter 3 | # 4 | # Common Spatial Adapter for ActiveRecord 5 | # 6 | # Code from 7 | # http://georuby.rubyforge.org Spatial Adapter 8 | # 9 | 10 | #Addition of a flag indicating if the index is spatial 11 | ActiveRecord::ConnectionAdapters::IndexDefinition.class_eval do 12 | attr_accessor :spatial 13 | 14 | def initialize(table, name, unique, spatial,columns) 15 | super(table,name,unique,columns) 16 | @spatial = spatial 17 | end 18 | 19 | end 20 | 21 | ActiveRecord::SchemaDumper.class_eval do 22 | def table(table, stream) 23 | 24 | columns = @connection.columns(table) 25 | begin 26 | tbl = StringIO.new 27 | 28 | if @connection.respond_to?(:pk_and_sequence_for) 29 | pk, pk_seq = @connection.pk_and_sequence_for(table) 30 | end 31 | pk ||= 'id' 32 | 33 | tbl.print " create_table #{table.inspect}" 34 | if columns.detect { |c| c.name == pk } 35 | if pk != 'id' 36 | tbl.print %Q(, :primary_key => "#{pk}") 37 | end 38 | else 39 | tbl.print ", :id => false" 40 | end 41 | 42 | if @connection.respond_to?(:options_for) 43 | res = @connection.options_for(table) 44 | tbl.print ", :options=>'#{res}'" if res 45 | end 46 | 47 | tbl.print ", :force => true" 48 | tbl.puts " do |t|" 49 | 50 | columns.each do |column| 51 | 52 | raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}' in table '#{table}'" if @types[column.type].nil? 53 | next if column.name == pk 54 | #need to use less_simplified_type here or have each specific geometry type be simplified to a specific simplified type in Column and each one treated separately in the Column methods 55 | if column.is_a?(SpatialColumn) 56 | tbl.print " t.column #{column.name.inspect}, #{column.geometry_type.inspect}" 57 | tbl.print ", :srid => #{column.srid.inspect}" if column.srid != -1 58 | tbl.print ", :with_z => #{column.with_z.inspect}" if column.with_z 59 | tbl.print ", :with_m => #{column.with_m.inspect}" if column.with_m 60 | else 61 | tbl.print " t.column #{column.name.inspect}, #{column.type.inspect}" 62 | end 63 | tbl.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit] && column.precision.blank? && column.scale.blank? 64 | tbl.print ", :precision => #{column.precision.inspect}" if column.precision != @types[column.type][:precision] 65 | tbl.print ", :scale => #{column.scale.inspect}" if column.scale != @types[column.type][:scale] 66 | tbl.print ", :default => #{default_string(column.default)}" if !column.default.nil? 67 | tbl.print ", :null => false" if !column.null 68 | tbl.puts 69 | end 70 | 71 | tbl.puts " end" 72 | tbl.puts 73 | indexes(table, tbl) 74 | tbl.rewind 75 | stream.print tbl.read 76 | rescue => e 77 | stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" 78 | stream.puts "# #{e.message} #{e.backtrace}" 79 | stream.puts 80 | end 81 | 82 | stream end 83 | 84 | def indexes(table, stream) 85 | indexes = @connection.indexes(table) 86 | indexes.each do |index| 87 | stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}" 88 | stream.print ", :unique => true" if index.unique 89 | stream.print ", :spatial=> true " if index.spatial 90 | stream.puts 91 | end 92 | 93 | stream.puts unless indexes.empty? 94 | end 95 | end 96 | 97 | 98 | 99 | 100 | module SpatialAdapter 101 | #Translation of geometric data types 102 | def geometry_data_types 103 | { 104 | :point => { :name => "POINT" }, 105 | :line_string => { :name => "LINESTRING" }, 106 | :polygon => { :name => "POLYGON" }, 107 | :geometry_collection => { :name => "GEOMETRYCOLLECTION" }, 108 | :multi_point => { :name => "MULTIPOINT" }, 109 | :multi_line_string => { :name => "MULTILINESTRING" }, 110 | :multi_polygon => { :name => "MULTIPOLYGON" }, 111 | :geometry => { :name => "GEOMETRY"} 112 | } 113 | end 114 | 115 | end 116 | 117 | 118 | #using a mixin instead of subclassing Column since each adapter can have a specific subclass of Column 119 | module SpatialColumn 120 | attr_reader :geometry_type, :srid, :with_z, :with_m 121 | 122 | def initialize(name, default, sql_type = nil, null = true,srid=-1,with_z=false,with_m=false) 123 | super(name,default,sql_type,null) 124 | @geometry_type = geometry_simplified_type(@sql_type) 125 | @srid = srid 126 | @with_z = with_z 127 | @with_m = with_m 128 | end 129 | 130 | 131 | #Redefines type_cast to add support for geometries 132 | def type_cast(value) 133 | return nil if value.nil? 134 | case type 135 | when :geometry then self.class.string_to_geometry(value) 136 | else super 137 | end 138 | end 139 | 140 | #Redefines type_cast_code to add support for geometries. 141 | # 142 | #WARNING : Since ActiveRecord keeps only the string values directly returned from the database, it translates from these to the correct types everytime an attribute is read (using the code returned by this method), which is probably ok for simple types, but might be less than efficient for geometries. Also you cannot modify the geometry object returned directly or your change will not be saved. 143 | def type_cast_code(var_name) 144 | case type 145 | when :geometry then "#{self.class.name}.string_to_geometry(#{var_name})" 146 | else super 147 | end 148 | end 149 | 150 | 151 | #Redefines klass to add support for geometries 152 | def klass 153 | case type 154 | when :geometry then GeoRuby::SimpleFeatures::Geometry 155 | else super 156 | end 157 | end 158 | 159 | private 160 | 161 | #Redefines the simplified_type method to add behaviour for when a column is of type geometry 162 | def simplified_type(field_type) 163 | case field_type 164 | when /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i then :geometry 165 | else super 166 | end 167 | end 168 | 169 | #less simlpified geometric type to be use in migrations 170 | def geometry_simplified_type(field_type) 171 | case field_type 172 | when /^point$/i then :point 173 | when /^linestring$/i then :line_string 174 | when /^polygon$/i then :polygon 175 | when /^geometry$/i then :geometry 176 | when /multipoint/i then :multi_point 177 | when /multilinestring/i then :multi_line_string 178 | when /multipolygon/i then :multi_polygon 179 | when /geometrycollection/i then :geometry_collection 180 | end 181 | end 182 | 183 | 184 | end 185 | -------------------------------------------------------------------------------- /spec/postgis_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require File.dirname(__FILE__) + '/spec_helper.rb' 3 | 4 | describe "PostgisAdapter" do 5 | 6 | describe "Point" do 7 | it "should record a point nicely" do 8 | pt = TablePoint.new(:data => "Test", :geom => Point.from_x_y(1.2,4.5)) 9 | pt.save.should be_true 10 | end 11 | 12 | it "should find a point nicely" do 13 | find = TablePoint.find(:last) 14 | find.should be_instance_of(TablePoint) 15 | find.geom.should be_instance_of(Point) 16 | end 17 | 18 | it "should find`em all for hellsake..." do 19 | find = TablePoint.all 20 | find.should be_instance_of(Array) 21 | find.last.geom.x.should eql(1.2) 22 | end 23 | 24 | it "should est_3dz_points" do 25 | pt = Table3dzPoint.create!(:data => "Hello!", 26 | :geom => Point.from_x_y_z(-1.6,2.8,-3.4)) 27 | pt = Table3dzPoint.find(:first) 28 | pt.geom.should be_instance_of(Point) 29 | pt.geom.z.should eql(-3.4) 30 | end 31 | 32 | it "should est_3dm_points" do 33 | pt = Table3dmPoint.create!(:geom => Point.from_x_y_m(-1.6,2.8,-3.4)) 34 | pt = Table3dmPoint.find(:first) 35 | pt.geom.should == Point.from_x_y_m(-1.6,2.8,-3.4) 36 | pt.geom.m.should eql(-3.4) 37 | end 38 | 39 | it "should est_4d_points" do 40 | pt = Table4dPoint.create!(:geom => Point.from_x_y_z_m(-1,2.8,-3.4,15)) 41 | pt = Table4dPoint.find(:first) 42 | pt.geom.should be_instance_of(Point) 43 | pt.geom.z.should eql(-3.4) 44 | pt.geom.m.should eql(15.0) 45 | end 46 | 47 | it "should test_keyword_column_point" do 48 | pt = TableKeywordColumnPoint.create!(:location => Point.from_x_y(1.2,4.5)) 49 | find = TableKeywordColumnPoint.find(:first) 50 | find.location.should == Point.from_x_y(1.2,4.5) 51 | end 52 | 53 | it "should test multipoint" do 54 | mp = TableMultiPoint.create!(:geom => MultiPoint.from_coordinates([[12.4,-4326.3],[-65.1,4326.4],[4326.55555555,4326]])) 55 | find = TableMultiPoint.find(:first) 56 | find.geom.should == MultiPoint.from_coordinates([[12.4,-4326.3],[-65.1,4326.4],[4326.55555555,4326]]) 57 | end 58 | 59 | end 60 | 61 | describe "LineString" do 62 | it "should record a linestring nicely" do 63 | @ls = TableLineString.new(:value => 3, 64 | :geom => LineString.from_coordinates([[1.4,2.5],[1.5,6.7]])) 65 | @ls.save.should be_true 66 | end 67 | 68 | it "should find" do 69 | find = TableLineString.find(:first) 70 | find.geom.should be_instance_of(LineString) 71 | find.geom.points.first.y.should eql(2.5) 72 | end 73 | 74 | it "should test_srid_line_string" do 75 | ls = TableSridLineString.create!( 76 | :geom => LineString.from_coordinates([[1.4,2.5],[1.5,6.7]],4326)) 77 | ls = TableSridLineString.find(:first) 78 | ls_e = LineString.from_coordinates([[1.4,2.5],[1.5,6.7]],4326) 79 | ls.geom.should be_instance_of(LineString) 80 | ls.geom.srid.should eql(4326) 81 | end 82 | 83 | it "hsould test_multi_line_string" do 84 | ml = TableMultiLineString.create!(:geom => MultiLineString.from_line_strings([LineString.from_coordinates([[1.5,45.2],[-54.432612,-0.012]]),LineString.from_coordinates([[1.5,45.2],[-54.432612,-0.012],[45.4326,4326.3]])])) 85 | find = TableMultiLineString.find(:first) 86 | find.geom.should == MultiLineString.from_line_strings([LineString.from_coordinates([[1.5,45.2],[-54.432612,-0.012]]),LineString.from_coordinates([[1.5,45.2],[-54.432612,-0.012],[45.4326,4326.3]])]) 87 | end 88 | end 89 | 90 | describe "Polygon" do 91 | 92 | it "should create" do 93 | pg = TablePolygon.new(:geom => Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]])) 94 | pg.save.should be_true 95 | end 96 | 97 | it "should get it back" do 98 | pg = TablePolygon.find(:first) 99 | pg.geom.should == Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]]) 100 | end 101 | 102 | it "should test_multi_polygon" do 103 | mp = TableMultiPolygon.create!( :geom => MultiPolygon.from_polygons([Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]]),Polygon.from_coordinates([[[0,0],[4,0],[4,4],[0,4],[0,0]],[[1,1],[3,1],[3,3],[1,3],[1,1]]])])) 104 | find = TableMultiPolygon.find(:first) 105 | find.geom.should == MultiPolygon.from_polygons([Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]]),Polygon.from_coordinates([[[0,0],[4,0],[4,4],[0,4],[0,0]],[[1,1],[3,1],[3,3],[1,3],[1,1]]])]) 106 | end 107 | 108 | it "should test_srid_4d_polygon" do 109 | pg = TableSrid4dPolygon.create(:geom => Polygon.from_coordinates([[[0,0,2,-45.1],[4,0,2,5],[4,4,2,4.67],[0,4,2,1.34],[0,0,2,-45.1]],[[1,1,2,12.3],[3,1,2,4326],[3,3,2,12.2],[1,3,2,12],[1,1,2,12.3]]],4326,true,true)) 110 | find = TableSrid4dPolygon.find(:first) 111 | pg_e = Polygon.from_coordinates([[[0,0,2,-45.1],[4,0,2,5],[4,4,2,4.67],[0,4,2,1.34],[0,0,2,-45.1]],[[1,1,2,12.3],[3,1,2,4326],[3,3,2,12.2],[1,3,2,12],[1,1,2,12.3]]],4326,true,true) 112 | pg.geom.should == pg_e 113 | pg.geom.srid.should eql(4326) 114 | end 115 | end 116 | 117 | describe "Geometry" do 118 | 119 | it "should test_geometry" do 120 | gm = TableGeometry.create!(:geom => LineString.from_coordinates([[12.4,-45.3],[45.4,41.6],[4.456,1.0698]])) 121 | find = TableGeometry.find(:first) 122 | find.geom.should == LineString.from_coordinates([[12.4,-45.3],[45.4,41.6],[4.456,1.0698]]) 123 | end 124 | 125 | it "should test_geometry_collection" do 126 | gc = TableGeometryCollection.create!(:geom => GeometryCollection.from_geometries([Point.from_x_y(4.67,45.4),LineString.from_coordinates([[5.7,12.45],[67.55,54]])])) 127 | find = TableGeometryCollection.find(:first) 128 | find.geom.should == GeometryCollection.from_geometries([Point.from_x_y(4.67,45.4),LineString.from_coordinates([[5.7,12.45],[67.55,54]])]) 129 | end 130 | 131 | end 132 | 133 | describe "Find" do 134 | 135 | ActiveRecord::Schema.define() do 136 | create_table :areas, :force => true do |t| 137 | t.string :data, :limit => 100 138 | t.integer :value 139 | t.point :geom, :null => false, :srid => 4326 140 | end 141 | add_index :areas, :geom, :spatial => true, :name => "areas_spatial_index" 142 | end 143 | 144 | class Area < ActiveRecord::Base 145 | end 146 | 147 | it "should create some points" do 148 | Area.create!(:data => "Point1", :geom => Point.from_x_y(1.2,0.75,4326)) 149 | Area.create!(:data => "Point2", :geom => Point.from_x_y(0.6,1.3,4326)) 150 | Area.create!(:data => "Point3", :geom => Point.from_x_y(2.5,2,4326)) 151 | end 152 | 153 | it "should find by geom" do 154 | pts = Area.find_all_by_geom(LineString.from_coordinates([[0,0],[2,2]],4326)) 155 | pts.should be_instance_of(Array) 156 | pts.length.should eql(2) 157 | pts[0].data.should match(/Point/) 158 | pts[1].data.should match(/Point/) 159 | end 160 | 161 | it "should find by geom again" do 162 | pts = Area.find_all_by_geom(LineString.from_coordinates([[2.49,1.99],[2.51,2.01]],4326)) 163 | pts[0].data.should eql("Point3") 164 | end 165 | 166 | it "should find by geom column bbox condition" do 167 | pts = Area.find_all_by_geom([[0,0],[2,2],4326]) 168 | pts.should be_instance_of(Array) 169 | pts.length.should eql(2) 170 | pts[0].data.should match(/Point/) 171 | pts[1].data.should match(/Point/) 172 | end 173 | 174 | it "should not mess with rails finder" do 175 | pts = Area.find_all_by_data "Point1" 176 | pts.should have(1).park 177 | end 178 | 179 | end 180 | 181 | # Verify that a non-NULL column with a default value is handled correctly. # Additionally, if the database uses UTF8, set the binary (bytea) 182 | # column value to an illegal UTF8 string; it should be stored as the 183 | # specified binary string and not as a text string. (The binary data test 184 | # cannot fail if the database uses SQL_ASCII or LATIN1 encoding.) 185 | describe "PostgreSQL-specific types and default values" do 186 | 187 | ActiveRecord::Schema.define() do 188 | create_table :binary_defaults, :force => true do |t| 189 | t.string :name, :null => false 190 | t.string :data, :null => false, :default => '' 191 | t.binary :value 192 | end 193 | end 194 | 195 | class BinaryDefault < ActiveRecord::Base 196 | end 197 | 198 | it "should create some records" do 199 | if BinaryDefault.connection.encoding == "UTF8" 200 | # fôo as ISO-8859-1 (i.e., not valid UTF-8 data) 201 | BinaryDefault.create!(:name => "foo", :data => "baz", 202 | :value => "f\xf4o") 203 | # data value not specified, should use default 204 | # bår as ISO-8859-1 (i.e., not valid UTF-8 data) 205 | BinaryDefault.create!(:name => "bar", 206 | :value => "b\xe5r") 207 | else 208 | BinaryDefault.create!(:name => "foo", :data => "baz") 209 | BinaryDefault.create!(:name => "bar") 210 | end 211 | end 212 | 213 | it "should find the records" do 214 | foo = BinaryDefault.find_by_name("foo") 215 | bar = BinaryDefault.find_by_name("bar") 216 | 217 | foo.data.should eql("baz") 218 | bar.data.should eql("") 219 | 220 | if BinaryDefault.connection.encoding == "UTF8" 221 | foo.value.encode("UTF-8", "ISO-8859-1").should eql("fôo") 222 | bar.value.encode("UTF-8", "ISO-8859-1").should eql("bår") 223 | end 224 | end 225 | 226 | end 227 | 228 | describe "Extras" do 229 | it "should disable referencial integrity" do 230 | lambda do 231 | Area.connection.disable_referential_integrity do 232 | Area.delete_all 233 | end 234 | end.should_not raise_error 235 | end 236 | end 237 | 238 | end 239 | -------------------------------------------------------------------------------- /spec/postgis_adapter/common_spatial_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper.rb' 2 | 3 | describe "CommonSpatialAdapter" do 4 | 5 | class Park < ActiveRecord::Base; end 6 | class Viewpark < ActiveRecord::Base; end 7 | 8 | describe "Migration" do 9 | 10 | before(:all) do 11 | @connection = ActiveRecord::Base.connection 12 | ActiveRecord::Schema.define do 13 | create_table "parks", :force => true do |t| 14 | t.string "data", :limit => 100 15 | t.integer "value" 16 | t.polygon "geom", :null => false, :srid => 4326 , :with_z => true, :with_m => true 17 | end 18 | end 19 | end 20 | 21 | it "should test_creation_modification" do 22 | @connection.columns("parks").length.should eql(4) # the 3 defined + id 23 | end 24 | 25 | it "should test columns" do 26 | @connection.columns("parks").each do |col| 27 | if col.name == "geom" 28 | col.class.should eql(ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn) 29 | col.geometry_type.should eql(:polygon) 30 | col.type.should eql(:geometry) 31 | col.null.should be_false 32 | col.srid.should eql(4326) 33 | col.with_z.should be_true 34 | col.with_m.should be_true 35 | end 36 | end 37 | end 38 | 39 | describe "Add" do 40 | 41 | before(:all)do 42 | ActiveRecord::Schema.define do 43 | add_column "parks","geom2", :multi_point, :srid => 4326 44 | end 45 | end 46 | 47 | it "should test_creation_modification" do 48 | @connection.columns("parks").length.should eql(5) # the 3 defined + id 49 | end 50 | 51 | it "should test columns" do 52 | @connection.columns("parks").each do |col| 53 | if col.name == "geom2" 54 | col.class.should eql(ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn) 55 | col.geometry_type.should eql(:multi_point) 56 | col.type.should eql(:geometry) 57 | col.null.should be_true 58 | col.srid.should eql(4326) 59 | col.with_z.should be_false 60 | col.with_m.should be_false 61 | end 62 | end 63 | end 64 | end 65 | 66 | describe "remove" do 67 | before(:all) do 68 | ActiveRecord::Schema.define do 69 | remove_column "parks","geom2" 70 | end 71 | end 72 | 73 | it "should test_creation_modification" do 74 | @connection.columns("parks").length.should eql(4) # the 3 defined + id 75 | end 76 | 77 | it "should get rid of the right one" do 78 | @connection.columns("parks").each do |col| 79 | violated if col.name == "geom2" 80 | end 81 | end 82 | end 83 | 84 | describe "indexes" do 85 | 86 | it "should have 0 indexes" do 87 | @connection.indexes("parks").length.should eql(0) 88 | end 89 | 90 | it "should create one" do 91 | ActiveRecord::Schema.define do 92 | add_index "parks","geom",:spatial=>true 93 | end 94 | @connection.indexes("parks").length.should eql(1) 95 | @connection.indexes("parks")[0].spatial.should be_true 96 | end 97 | 98 | it "should remove too" do 99 | ActiveRecord::Schema.define do 100 | remove_index "parks", "geom" 101 | end 102 | @connection.indexes("parks").length.should eql(0) 103 | end 104 | 105 | it "should work with points" do 106 | ActiveRecord::Schema.define do 107 | remove_column "parks","geom2" 108 | add_column "parks","geom2", :point 109 | add_index "parks","geom2",:spatial=>true,:name => "example_spatial_index" 110 | end 111 | @connection.indexes("parks").length.should eql(1) 112 | @connection.indexes("parks")[0].spatial.should be_true 113 | @connection.indexes("parks")[0].name.should eql("example_spatial_index") 114 | end 115 | 116 | end 117 | 118 | end 119 | 120 | describe "Keywords" do 121 | 122 | before(:all) do 123 | ActiveRecord::Schema.define do 124 | create_table "parks", :force => true do |t| 125 | t.string "data", :limit => 100 126 | t.integer "value" 127 | #location is a postgreSQL keyword and is surrounded by double-quotes ("") when appearing in constraint descriptions ; tests a bug corrected in version 39 128 | t.point "location", :null=>false,:srid => -1, :with_m => true, :with_z => true 129 | end 130 | end 131 | 132 | @connection = ActiveRecord::Base.connection 133 | @columns = @connection.columns("parks") 134 | end 135 | 136 | it "should get the columsn length" do 137 | @connection.indexes("parks").length.should eql(0) # the 3 defined + id 138 | end 139 | 140 | it "should get the columns too" do 141 | @connection.columns("parks").each do |col| 142 | if col.name == "geom2" 143 | col.class.should eql(ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn) 144 | col.geometry_type.should eql(:point) 145 | col.type.should eql(:geometry) 146 | col.null.should be_true 147 | col.srid.should eql(-1) 148 | col.with_z.should be_false 149 | col.with_m.should be_false 150 | end 151 | end 152 | end 153 | end 154 | 155 | describe "Views" do 156 | 157 | before(:all) do 158 | ActiveRecord::Schema.define do 159 | create_table "parks", :force => true do |t| 160 | t.column "data" , :string, :limit => 100 161 | t.column "value", :integer 162 | t.column "geom", :point,:null=>false, :srid => 4326 163 | end 164 | end 165 | 166 | Park.create!(:data => "Test", :geom => Point.from_x_y(1.2,4.5)) 167 | 168 | ActiveRecord::Base.connection.execute('CREATE VIEW viewparks as SELECT * from parks') 169 | #if not ActiveRecord::Base.connection.execute("select * from geometry_columns where f_table_name = 'viewparks' and f_geometry_column = 'geom'") #do not add if already there 170 | #mark the geom column in the view as geometric 171 | #ActiveRecord::Base.connection.execute("insert into geometry_columns values ('','public','viewparks','geom',2,-1,'POINT')") 172 | #end 173 | end 174 | 175 | it "should works" do 176 | pt = Viewpark.find(:first) 177 | pt.data.should eql("Test") 178 | pt.geom.should == Point.from_x_y(1.2,4.5) 179 | end 180 | 181 | after(:all) do 182 | ActiveRecord::Base.connection.execute('DROP VIEW viewparks') 183 | end 184 | end 185 | 186 | describe "Dump" do 187 | before(:all) do 188 | #Force the creation of a table 189 | ActiveRecord::Schema.define do 190 | create_table "parks", :force => true do |t| 191 | t.string "data" , :limit => 100 192 | t.integer "value" 193 | t.multi_polygon "geom", :null=>false,:srid => -1, :with_m => true, :with_z => true 194 | end 195 | add_index "parks","geom",:spatial=>true,:name => "example_spatial_index" 196 | end 197 | #dump it : tables from other tests will be dumped too but not a problem 198 | File.open('schema.rb', "w") do |file| 199 | ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) 200 | end 201 | #load it again 202 | load('schema.rb') 203 | #delete the schema file 204 | File.delete('schema.rb') 205 | @connection = ActiveRecord::Base.connection 206 | @columns = @connection.columns("parks") 207 | end 208 | 209 | it "should create" do 210 | @columns.length.should eql(4) # the 3 defined + id 211 | end 212 | 213 | it "should get the same stuff bakc" do 214 | @columns.each do |col| 215 | if col.name == "geom" 216 | col.class.should eql(ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn) 217 | col.geometry_type.should eql(:multi_polygon) 218 | col.type.should eql(:geometry) 219 | col.null.should be_false 220 | col.srid.should eql(-1) 221 | col.with_z.should be_true 222 | col.with_m.should be_true 223 | end 224 | end 225 | end 226 | 227 | it "should get the indexes back too" do 228 | @connection.indexes("parks").length.should eql(1) 229 | @connection.indexes("parks")[0].spatial.should be_true 230 | @connection.indexes("parks")[0].name.should eql("example_spatial_index") 231 | end 232 | end 233 | 234 | 235 | describe "Fixtures" do 236 | 237 | it "should test_long_fixture" do 238 | Polygon.from_coordinates([[[144.857742,13.598263], 239 | [144.862362,13.589922],[144.865169,13.587336],[144.862927,13.587665], 240 | [144.861292,13.587321],[144.857597,13.585299],[144.847845,13.573858], 241 | [144.846225,13.571014],[144.843605,13.566047],[144.842157,13.563831], 242 | [144.841202,13.561991],[144.838305,13.556465],[144.834645,13.549919], 243 | [144.834352,13.549395],[144.833825,13.548454],[144.831839,13.544451], 244 | [144.830845,13.54081],[144.821543,13.545695],[144.8097993,13.55186285], 245 | [144.814753,13.55755],[144.816744,13.56176944],[144.818862,13.566258], 246 | [144.819402,13.568565],[144.822373,13.572223],[144.8242032,13.57381149], 247 | [144.82634,13.575666],[144.83416,13.590365],[144.83514,13.595657], 248 | [144.834284,13.59652],[144.834024,13.598031],[144.83719,13.598061], 249 | [144.857742,13.598263]]]).to_fixture_format.split(/\s+/).should eql(["0103000020E61000000100000020000000FBCC599F721B62404ED026874F322B40056A3178981B6240BF61A2410A2E2B406B10E676AF1B6240E486DF4DB72C2B40BC7A15199D1B6240F701486DE22C2B40CE893DB48F1B62400E828E56B52C2B40BA84436F711B624054C37E4FAC2B2B407862D68B211B62408F183DB7D0252B40D8817346141B6240C51D6FF25B242B40BFB7E9CFFE1A624071FF91E9D0212B401EA33CF3F21A624024F1F274AE202B408EEA7420EB1A6240ED4ACB48BD1F2B4058E20165D31A6240BEBC00FBE81C2B40A3586E69B51A6240E6E5B0FB8E192B40452BF702B31A6240FE2B2B4D4A192B40CA32C4B1AE1A624084B872F6CE182B403291D26C9E1A62408B8C0E48C2162B4072E14048961A624014B35E0CE5142B403FA88B144A1A6240732EC55565172B405CBA38E0E9196240344179C48D1A2B402C2AE274121A624005C58F31771D2B40892650C4221A62406D62793EA01F2B40FDBD141E341A62405D328E91EC212B40DD088B8A381A6240EC4CA1F31A232B40A1832EE1501A6240BB09BE69FA242B4046A863DF5F1A6240F23C9F9ECA252B400D6C9560711A624099D6A6B1BD262B4034F44F70B11A6240F5673F52442E2B409B728577B91A624056444DF4F9302B406FF25B74B21A6240E1D1C6116B312B4088821953B01A624005FD851E31322B4039D1AE42CA1A6240AF06280D35322B40FBCC599F721B62404ED026874F322B40"]) 250 | end 251 | 252 | end 253 | 254 | end 255 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = PostgisAdapter 2 | 3 | *WARN* 4 | 5 | I no longer maintain this lib. Please find a fork, or: 6 | 7 | Stay with PostGIS: 8 | 9 | https://github.com/dazuma/activerecord-postgis-adapter 10 | 11 | Go with MongoDB: 12 | 13 | https://github.com/nofxx/mongoid-geospatial 14 | 15 | 16 | 17 | A plugin for ActiveRecord which manages the PostGIS geometric columns 18 | in a transparent way (that is like the other base data type columns). 19 | It also provides a way to manage these columns in migrations. 20 | 21 | This fork adds handy methods to make geometrical calculations on postgis. 22 | Based on http://georuby.rubyforge.org Spatial Adapter 23 | 24 | RDocs - http://docs.github.com/nofxx/postgis_adapter 25 | Postgis Online reference - http://postgis.refractions.net 26 | Postgis Manual - http://postgis.refractions.net/documentation/manual-svn 27 | 28 | 29 | == Install 30 | 31 | If you are using Spatial Adapter, *remove it first*. 32 | 33 | gem install postgis_adapter 34 | 35 | 36 | === Dependencies 37 | 38 | - georuby gem 39 | - postgres 8.3+ 40 | - postgis 1.3+ 41 | 42 | 43 | === Rails 3+ 44 | 45 | Add dependency to Gemfile: 46 | 47 | gem "postgis_adapter" 48 | 49 | 50 | Or, to use latest from repository: 51 | 52 | gem "postgis_adapter", :git => 'git://github.com/nofxx/postgis_adapter.git' 53 | 54 | 55 | === Rails 2 56 | 57 | gem install postgis_adapter -v 0.7.8 58 | 59 | 60 | == How to Use 61 | 62 | Geometric columns in your ActiveRecord models now appear just like 63 | any other column of other basic data types. They can also be dumped 64 | in ruby schema mode and loaded in migrations the same way as columns 65 | of basic types. 66 | 67 | 68 | === Example App 69 | 70 | Simple rails app to demonstrate, check it out: 71 | 72 | http://github.com/nofxx/postgis_example 73 | 74 | 75 | === Model 76 | 77 | class TablePoint < ActiveRecord::Base 78 | end 79 | 80 | That was easy! As you see, there is no need to declare a column as geometric. 81 | The plugin will get this information by itself. 82 | 83 | Here is an example of PostGIS row creation and access, using the 84 | model and the table defined above : 85 | 86 | pt = TablePoint.new(:data => "Hello!",:geom => Point.from_x_y(1,2)) 87 | pt.save 88 | pt = TablePoint.first 89 | puts pt.geom.x 90 | => 1 91 | 92 | 93 | == PostGIS Functions 94 | 95 | Here are this fork additions. To use it: 96 | 97 | acts_as_geom [column_name] => [geom_type] 98 | 99 | 100 | Examples: 101 | 102 | class POI < ActiveRecord::Base 103 | acts_as_geom :geom => :point 104 | end 105 | 106 | class Street < ActiveRecord::Base 107 | acts_as_geom :line => :line_string 108 | end 109 | 110 | ... 111 | 112 | 113 | 114 | == Play! 115 | 116 | @place = Poi.new( :geom => Point.from_x_y(10,20) ) 117 | @park = Park.new( :area => **Polygon** ) 118 | @street = Street.new( :line => **LineString** ) 119 | 120 | @place.inside?(@park) 121 | => true 122 | 123 | @place.in_bounds?(@park, 0.5) # margin 124 | => false 125 | 126 | @place.outside?(@park) 127 | @street.crosses?(@park) 128 | @area.contains?(@place) 129 | ... 130 | 131 | 132 | === Polygons: 133 | 134 | @park.area 135 | => 1345 136 | 137 | @park.contains?(@point) 138 | => true 139 | 140 | @park.overlaps?(@other_park) 141 | => false 142 | 143 | Supports transform (useful to transform SRID to UTM for area in Km^2) 144 | 145 | @park.area(SRID) 146 | => Area with new SRID 147 | 148 | 149 | === LineStrings: 150 | 151 | @street_east.intersects?(@street_west) 152 | => false 153 | 154 | @street_central.length 155 | => 4508.53636 156 | 157 | @street.length_spheroid 158 | => 4.40853636 159 | 160 | 161 | === Class Methods 162 | 163 | City.close_to(@point) 164 | => [Array of cities in order by distance... 165 | 166 | Street.close_to(@point) 167 | => [Array streets in order by distance... 168 | 169 | Country.contain(@point) 170 | => The Conutry that contains the point 171 | 172 | Area.contains(@point) 173 | => [Array of areas contains the point... 174 | 175 | 176 | === BBox Support 177 | 178 | @area.strictly_left_of? @point 179 | 180 | @area.overlaps_or_above? @street 181 | 182 | ... 183 | 184 | completely_contained_by? 185 | completely_contains? 186 | overlaps_or_above? 187 | overlaps_or_below? 188 | overlaps_or_left_of? 189 | overlaps_or_right_of? 190 | strictly_above? 191 | strictly_below? 192 | strictly_left_of? 193 | strictly_right_of? 194 | interacts_with? 195 | binary_equal? 196 | same_as? 197 | 198 | 199 | Or use a (almost) PostGIS like notation: 200 | 201 | @area.bbox "<<", @point 202 | 203 | @area.bbox "|>>", @point 204 | 205 | @area.bbox "@", @park 206 | 207 | 208 | === Warning 209 | 210 | *To be fixed:* 211 | 212 | This only supports one geom column per model. Still looking for the best way to 213 | implement a multi geom. 214 | 215 | http://nofxx.lighthouseapp.com/projects/20712/tickets/3-multiple-geoms-in-model 216 | 217 | 218 | === Find_by 219 | 220 | find_by_*column* has been redefined when column is of a geometric type. 221 | Instead of using the Rails default '=' operator, for which I can't see 222 | a definition for MySql spatial datatypes and which performs a bounding 223 | box equality test in PostGIS, it uses a bounding box intersection: 224 | && in PostGIS and MBRIntersects in MySQL, which can both make use 225 | of a spatial index if one is present to speed up the queries. 226 | You could use this query, for example, if you need to display data 227 | from the database: You would want only the geometries which are in 228 | the screen rectangle and you could use a bounding box query for that. 229 | Since this is a common case, it is the default. You have 2 ways to use 230 | the find_by_*geom_column*: Either by passing a geometric object directly, 231 | or passing an array with the 2 opposite corners of a bounding box 232 | (with 2 or 3 coordinates depending of the dimension of the data). 233 | 234 | Park.find_by_geom(LineString.from_coordinates([[1.4,5.6],[2.7,8.9],[1.6,5.6]])) 235 | 236 | or 237 | 238 | Park.find_by_geom([[3,5.6],[19.98,5.9]]) 239 | 240 | In PostGIS, since you can only use operations with geometries with the same SRID, you can add a third element representing the SRID of the bounding box to the array. It is by default set to -1: 241 | 242 | Park.find_by_geom([[3,5.6],[19.98,5.9],123]) 243 | 244 | 245 | 246 | == Database Tools 247 | 248 | === Migrations 249 | 250 | Here is an example of code for the creation of a table with a 251 | geometric column in PostGIS, along with the addition of a spatial 252 | index on the column : 253 | 254 | ActiveRecord::Schema.define do 255 | create_table :places do |t| 256 | t.string :name 257 | t.point :geom, :srid => 4326, :with_z => true, :null => false 258 | 259 | t.timestamps 260 | end 261 | 262 | add_index :places, :geom, :spatial => true 263 | end 264 | 265 | 266 | Types: 267 | 268 | point 269 | polygon 270 | line_string 271 | multi_point 272 | multi_polygon 273 | multi_line_string 274 | geometry 275 | geometry_collection 276 | 277 | 278 | === PostGIS Helper Scripts 279 | 280 | Optional, this will create postgis enabled database automatically for you. 281 | 282 | Helpers to create postgis template database. At time of writing, 283 | postgis.sql and spatial_ref_sys.sql are used. 284 | 285 | 286 | ==== System wide 287 | 288 | 289 | Find where your OS put those sql files and: 290 | 291 | rake postgis:template path/to/sqls/folder 292 | 293 | 294 | ==== Vendorize 295 | 296 | Place the following scripts in a folder named 'spatial' under the 'db' folder; For example: 297 | 298 | RAILS_ROOT/db/spatial/lwpostgis.sql 299 | RAILS_ROOT/db/spatial/spatial_ref_sys 300 | 301 | These will be used when creating the Test database when running the Rake Test tasks. 302 | These scripts should have been installed when the PostGIS libraries were installed. 303 | Online reference: http://postgis.refractions.net/ 304 | 305 | 306 | === Fixtures 307 | 308 | If you use fixtures for your unit tests, at some point, 309 | you will want to input a geometry. You could transform your 310 | geometries to a form suitable for YAML yourself everytime but 311 | the spatial adapter provides a method to do it for you: +to_yaml+. 312 | It works for both MySQL and PostGIS (although the string returned 313 | is different for each database). You would use it like this, if 314 | the geometric column is a point: 315 | 316 | fixture: 317 | id: 1 318 | data: HELLO 319 | geom: <%= Point.from_x_y(123.5,321.9).to_yaml %> 320 | 321 | 322 | === Annotate 323 | 324 | If you are using annotate_models, check out this fork which adds geometrical annotations for PostgisAdapter and SpatialAdapter: 325 | 326 | http://github.com/nofxx/annotate_models 327 | 328 | 329 | == Geometric data types 330 | 331 | Ruby geometric datatypes are currently made available only through 332 | the GeoRuby library (http://georuby.rubyforge.org): This is where the 333 | *Point.from_x_y* in the example above comes from. It is a goal 334 | of a future release of the Spatial Adapter to support additional 335 | geometric datatype libraries, such as Ruby/GEOS, as long as they 336 | can support reading and writing of EWKB. 337 | 338 | 339 | 340 | === Warning 341 | 342 | - Since ActiveRecord seems to keep only the string values directly 343 | returned from the database, it translates from these to the correct 344 | types everytime an attribute is read, which is probably ok for simple 345 | types, but might be less than efficient for geometries, since the EWKB 346 | string has to be parsed everytime. Also it means you cannot modify the 347 | geometry object returned from an attribute directly : 348 | 349 | place = Place.first 350 | place.the_geom.y=123456.7 351 | 352 | - Since the translation to a geometry is performed everytime the_geom 353 | is read, the change to y will not be saved! You would have to do 354 | something like this : 355 | 356 | place = Place.first 357 | the_geom = place.the_geom 358 | the_geom.y=123456.7 359 | place.the_geom = the_geom 360 | 361 | 362 | == Postgis Adapter 363 | 364 | Marcos Piccinini (nofxx) 365 | Ying Tsen Hong (tsenying) 366 | Simon Tokumine (tokumine) 367 | Fernando Blat (ferblape) 368 | Shoaib Burq (sabman) 369 | 370 | (in order of appearance) 371 | 372 | 373 | == License 374 | 375 | Spatial Adapter for Rails is released under the MIT license. 376 | Postgis Adapter is released under the MIT license. 377 | 378 | 379 | == Support 380 | 381 | Tested using activerecord 3+ / postgresql 8.5+ / postgis 1.5+ / linux / osx 382 | 383 | Any questions, enhancement proposals, bug notifications or corrections: 384 | 385 | 386 | === PostgisAdapter 387 | 388 | http://github.com/nofxx/postgis_adapter/issues 389 | 390 | 391 | === SpatialAdapter 392 | 393 | http://georuby.rubyforge.org 394 | guilhem.vellut+georuby@gmail.com. 395 | -------------------------------------------------------------------------------- /spec/postgis_adapter/functions/common_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../spec_helper.rb' 2 | 3 | describe "Common Functions" do 4 | 5 | before(:all) do 6 | @poly = Polygon.from_coordinates([[[12,45],[45,41],[4,1],[12,45]],[[2,5],[5,1],[14,1],[2,5]]], 4326) 7 | @c1 ||= City.create!(:data => "City1", :geom => @poly) 8 | @c2 ||= City.create!(:data => "City1", :geom => Polygon.from_coordinates([[[22,66],[65,65],[20,10],[22,66]],[[10,15],[15,11],[34,14],[10,15]]],4326)) 9 | @c3 ||= City.create!(:data => "City3", :geom => Polygon.from_coordinates([[[12.4,-45.3],[45.4,41.6],[4.456,1.0698],[12.4,-45.3]],[[2.4,5.3],[5.4,1.4263],[14.46,1.06],[2.4,5.3]]],4326)) 10 | @s1 ||= Street.create!(:data => "Street1", :geom => LineString.from_coordinates([[1,1],[2,2]],4326)) 11 | @sx ||= Street.create!(:data => "Street1", :geom => LineString.from_coordinates([[2,2],[4,4]],4326)) 12 | @s2 ||= Street.create!(:data => "Street2", :geom => LineString.from_coordinates([[4,4],[7,7]],4326)) 13 | @s3 ||= Street.create!(:data => "Street3", :geom => LineString.from_coordinates([[8,8],[18,18],[20,20],[25,25],[30,30],[38,38]],4326)) 14 | @s4 ||= Street.create!(:data => "Street4", :geom => LineString.from_coordinates([[10,8],[15,18]],4326)) 15 | @p1 ||= Position.create!(:data => "Point1", :geom => Point.from_x_y(1,1,4326)) 16 | @p2 ||= Position.create!(:data => "Point2", :geom => Point.from_x_y(5,5,4326)) 17 | @p3 ||= Position.create!(:data => "Point3", :geom => Point.from_x_y(8,8,4326)) 18 | @p4 ||= Position.create!(:data => "Point4", :geom => Point.from_x_y(18.1,18,4326)) 19 | @p5 ||= Position.create!(:data => "Point5", :geom => Point.from_x_y(30,30,4326)) 20 | @m1 ||= Road.create(:data => "MG-050", :geom => MultiLineString.from_line_strings([@s1.geom, @sx.geom])) 21 | @m2 ||= Road.create(:data => "MG-050", :geom => MultiLineString.from_line_strings([@s3.geom, @s4.geom])) 22 | @p6 ||= Position.create!(:data => "Point6", :geom => Point.from_x_y(30.9999,30.9999,4326)) 23 | end 24 | 25 | describe "Point" do 26 | 27 | it "should find the closest other point" do 28 | Position.close_to(@p1.geom)[0].data.should == @p1.data 29 | end 30 | 31 | it "should find the closest other point" do 32 | Position.closest_to(@p1.geom).data.should == @p1.data 33 | end 34 | 35 | it { @p1.distance_to(@s2).should be_close(4.24264068711928, 0.0001) } 36 | it { @p1.distance_to(@s3).should be_close(9.89949493661167, 0.0001) } 37 | it { @p2.distance_to(@s3).should be_close(4.24264068711928, 0.0001) } 38 | it { @p1.distance_to(@p2).should be_close(5.65685424949238, 0.0001) } 39 | it { @p1.distance_to(@c1).should be_close(3.0, 0.0001) } 40 | it { @p1.distance_to(@c2).should be_close(21.0237960416286, 0.000001) } 41 | it { @p1.distance_to(@s2).should be_close(4.24264068711928, 0.000001) } 42 | it { @p1.distance_sphere_to(@p2).should be_close(628519.033787529, 0.0001) } 43 | it { @p1.distance_sphere_to(@p3).should be_close(1098730.38927754, 0.00001) } 44 | it { @p1.distance_spheroid_to(@p2).should be_close(627129.50,0.01) } 45 | it { @p1.distance_spheroid_to(@p2).should be_close(627129.502561203, 0.000001) } 46 | it { @p1.distance_spheroid_to(@p3).should be_close(1096324.48105195, 0.000001) } 47 | 48 | it "should find the distance from a unsaved point" do 49 | @p1.distance_to(@p2).should be_close(5.65685424949238,0.001) 50 | @p1.distance_to(Point.from_x_y(5,5,4326)).should be_close(5.65685424949238,0.001) 51 | end 52 | 53 | it { @p1.should_not be_inside(@c1) } 54 | it { @p1.should be_outside(@c1) } 55 | it { @p1.should be_inside_circle(2.0,2.0,20.0) } 56 | it { @p1.should_not be_inside_circle(50,50,2) } 57 | it { @p1.should be_in_bounds(@s1) } 58 | it { @p3.should_not be_in_bounds(@s1, 1) } 59 | it { @p4.in_bounds?(@s3, 0.01).should be_false } 60 | 61 | it { @p1.azimuth(@p2).should be_close(0.785398163397448,0.000001) } 62 | it { @p1.azimuth(@s2).should raise_error } 63 | it { @p1.disjoint?(@s2).should be_true } 64 | it { @p3.polygonize.geometries.should be_empty } 65 | it { @p4.where_on_line(@s3).should be_close(0.335, 0.0001) } 66 | it { @s3.locate_point(@p4).should be_close(0.335, 0.1)} 67 | it { @s3.interpolate_point(0.335).x.should be_close(18.05, 0.01) } 68 | 69 | it { @p1.relate?(@s3, "T*T***FF*").should be_false } 70 | it { @p1.relate?(@s3).should eql("FF0FFF102") } 71 | 72 | it "should transform srid" do 73 | @p1.geom = @p1.transform(29101) 74 | @p1.geom.srid.should eql(29101) 75 | end 76 | 77 | it "should transform non saved srid geoms" do 78 | pt = Point.from_x_y(11121381.4586196,10161852.0494475, 29101) 79 | pos = Position.new(:geom => pt) 80 | pos.transform!(4326) 81 | pos.geom.x.should be_close(1.00000000000005, 0.00001) 82 | pos.geom.y.should be_close(1.00000000000005, 0.00001) 83 | end 84 | 85 | it "should see in what fraction of the ls it is" do 86 | @p1.where_on_line(@s1).should eql(0.0) 87 | end 88 | 89 | it "should see in what fraction of the ls it is" do 90 | @p2.where_on_line(@s2).should be_close(0.3333, 0.1) 91 | end 92 | 93 | it "should have a srid getter" do 94 | @p1.srid.should eql(29101) 95 | end 96 | 97 | it "should calculate the UTM srid" do 98 | @p2.utm_zone.should eql(32731) 99 | end 100 | 101 | it "should convert to utm zone" do 102 | lambda { @p2.to_utm! }.should change(@p2, :srid) 103 | end 104 | 105 | if PG_VERSION >= "8.4.0" 106 | it "should export as GeoJSON" do 107 | @p1.as_geo_json.should eql("{\"type\":\"Point\",\"coordinates\":[1,1]}") 108 | end 109 | 110 | it "should export as GeoJSON with variable precision" do 111 | @p6.as_geo_json(1).should eql("{\"type\":\"Point\",\"coordinates\":[31,31]}") 112 | end 113 | 114 | it "should export as GeoJSON with variable precision and bounding box" do 115 | @p6.as_geo_json(1,1).should eql("{\"type\":\"Point\",\"bbox\":[31.0,31.0,31.0,31.0],\"coordinates\":[31,31]}") 116 | end 117 | end 118 | 119 | 120 | 121 | # it { @p3.x.should be_close(8.0, 0.1) } 122 | # it { @p3.y.should be_close(8.0, 0.1) } 123 | # it { @p3.z.should be_close(0.0, 0.1) } 124 | 125 | end 126 | 127 | describe "Polygon" do 128 | 129 | it { City.first.data.should eql("City1") } 130 | 131 | it "sort by area size" do 132 | City.by_area.first.data.should == "City1" #[@c1, @c2, @c3] 133 | end 134 | 135 | it "find all cities that contains a point" do 136 | City.contains(@p1.geom, 4326).should eql([]) 137 | end 138 | 139 | it "should find one city (first) that contains a point" do 140 | City.contain(@p4.geom, 4326).data.should eql("City1") 141 | end 142 | 143 | it { @c2.should be_closed } 144 | it { @c2.dimension.should eql(2) } 145 | 146 | it { @c3.area.should be_close(1093.270089, 0.1) } 147 | it { @c2.area.should be_close(1159.5, 0.1) } 148 | it { @c2.area(32640).should be_close(5852791139841.2, 0.01) } 149 | 150 | it { @c2.perimeter.should be_close(219.770013855493, 0.1) } 151 | it { @c2.perimeter(32640).should be_close(23061464.4268903, 0.1) } 152 | it { @c2.perimeter3d.should be_close(219.770013855493, 0.1) } 153 | 154 | it { @c1.contains?(@p1).should be_false } 155 | it { @c1.contains?(@p4).should be_true } 156 | 157 | it { @c1.should_not be_spatially_equal(@c2) } 158 | 159 | it { @c1.covers?(@p1).should be_false } 160 | it { @c1.covers?(@p4).should be_true } 161 | it { @c1.within?(@c2).should be_false } 162 | 163 | it "city overlaps point?" do 164 | lambda { @c3.overlaps?(@c2) }.should raise_error # WHY?? 165 | end 166 | 167 | it "should get a polygon for envelope" do 168 | @c2.envelope.should be_instance_of(Polygon) 169 | end 170 | 171 | it "should get a polygon for envelope" do 172 | @c2.envelope.rings[0].points[0].should be_instance_of(Point) 173 | end 174 | 175 | it "should get the center" do 176 | @c2.centroid.x.should be_close(36.2945235015093,0.00001) 177 | @c2.centroid.y.should be_close(48.3211154233146,0.00001) 178 | end 179 | 180 | it "should get the center with the correct srid" do 181 | @c1.centroid.srid.should eql(4326) 182 | end 183 | 184 | it "distance from another" do 185 | @c1.distance_to(@c3).should eql(0.0) 186 | end 187 | 188 | it "distance to a linestring" do 189 | @c1.distance_to(@s1).should be_close(2.146,0.001) 190 | end 191 | 192 | it "should simplify me" do 193 | @c3.simplify.should be_instance_of(Polygon) 194 | end 195 | 196 | it "should simplify me number of points" do 197 | @c3.simplify[0].length.should eql(4) 198 | end 199 | 200 | #Strange again.... s2 s3 ... error 201 | it { @c3.touches?(@s1).should be_false } 202 | it { @c2.should be_simple } 203 | it { @c2.disjoint?(@p2).should be_true } 204 | it { @c3.polygonize.should have(2).geometries } 205 | 206 | it "should acts as jack" do 207 | @c2.segmentize(0.1).should be_instance_of(Polygon) 208 | end 209 | 210 | 211 | # weird... 212 | # it do 213 | # @c1.disjoint?(@p2).should be_true 214 | # end 215 | 216 | it "should check overlaps" do 217 | @c2.contains?(@c1).should be_false 218 | end 219 | 220 | it "should check overlaps non saved" do 221 | @c2.contains?(@poly).should be_false 222 | end 223 | 224 | it "should find the UTM zone" do 225 | @c2.utm_zone.should eql(32737) 226 | end 227 | 228 | if PG_VERSION >= "8.4.0" 229 | it "should export as GeoJSON" do 230 | @c1.as_geo_json.should eql("{\"type\":\"Polygon\",\"coordinates\":[[[12,45],[45,41],[4,1],[12,45]],[[2,5],[5,1],[14,1],[2,5]]]}") 231 | end 232 | end 233 | 234 | end 235 | 236 | describe "LineString" do 237 | 238 | describe "Length" do 239 | it { @s1.length.should be_close(1.4142135623731, 0.000001) } 240 | it { @s2.length.should be_close(4.2, 0.1) } 241 | it { @s3.length.should be_close(42.4264068, 0.001) } 242 | it { @s1.length_spheroid.should be_close(156876.1494,0.0001) } 243 | it { @s1.length_3d.should be_close(1.4142135623731,0.0001) } 244 | end 245 | 246 | it { @s1.crosses?(@s2).should be_false } 247 | it { @s4.crosses?(@s3).should be_true } 248 | it { @s1.touches?(@s2).should be_false } 249 | it { @s4.touches?(@s3).should be_false } 250 | 251 | if PG_VERSION >= "8.4.0" 252 | it "should calculate crossing direction" do 253 | @s4.line_crossing_direction(@s3).should eql("1") 254 | end 255 | end 256 | 257 | it "should have 1 geometry" do 258 | @s1.should_not respond_to(:geometries) 259 | end 260 | 261 | it "should intersect with linestring" do 262 | @s4.intersects?(@s3).should be_true 263 | end 264 | 265 | it "should not intersect with this linestring" do 266 | @s4.intersects?(@s1).should be_false 267 | end 268 | 269 | it "intersection with a point" do 270 | @s1.intersection(@p2).should be_instance_of(GeometryCollection) 271 | end 272 | 273 | it "have a point on surface" do 274 | @s3.point_on_surface.should be_a GeoRuby::SimpleFeatures::Point 275 | end 276 | 277 | describe "Self" do 278 | 279 | it do 280 | @s1.envelope.should be_instance_of(Polygon) 281 | end 282 | 283 | it "should get a polygon for envelope" do 284 | @s1.envelope.rings[0].points[0].should be_instance_of(Point) 285 | end 286 | 287 | it "should get the center" do 288 | @s1.centroid.x.should be_close(1.5,0.01) 289 | @s1.centroid.y.should be_close(1.5,0.01) 290 | end 291 | 292 | it "should get the center with the correct srid" do 293 | @s1.centroid.srid.should eql(4326) 294 | end 295 | 296 | it "number of points" do 297 | @s3.num_points.should eql(6) 298 | end 299 | 300 | it "startpoint" do 301 | @s3.start_point.should be_instance_of(Point) 302 | @s3.start_point.x.should be_close(8.0, 0.1) 303 | end 304 | 305 | it "endpoint" do 306 | @s2.end_point.should be_instance_of(Point) 307 | @s2.end_point.x.should be_close(7.0, 0.1) 308 | end 309 | 310 | it { @s1.should_not be_envelopes_intersect(@s2) } 311 | it { @s1.boundary.should be_instance_of(MultiPoint) } 312 | 313 | 314 | if PG_VERSION >= "8.4.0" 315 | it "should export as GeoJSON" do 316 | @s1.as_geo_json.should eql("{\"type\":\"LineString\",\"coordinates\":[[1,1],[2,2]]}") 317 | end 318 | end 319 | end 320 | 321 | describe "More Distance" do 322 | 323 | it { @s1.distance_to(@p3).should be_close(8.48528137423857,0.0001) } 324 | it { @s1.distance_to(@p3).should be_close(8.48,0.01) } 325 | it { @p1.distance_spheroid_to(@c3).should be_close(384735.205204477, 1) } 326 | it { @p3.distance_spheroid_to(@s1).should be_close(939450.671670147,1) } 327 | 328 | end 329 | 330 | it do @s1.locate_point(@p1).should eql(0.0) end 331 | it do @s1.locate_point(@p2).should eql(1.0) end 332 | 333 | it "should simplify a line" do 334 | @s3.simplify.points.length.should eql(2) 335 | end 336 | 337 | it "should simplify the first correcty" do 338 | @s3.simplify.points[0].y.should be_close(8.0, 0.1) 339 | end 340 | 341 | it "should simplify the last correcty" do 342 | @s3.simplify.points[1].y.should be_close(38.0, 0.1) 343 | end 344 | 345 | it { @s1.overlaps?(@c2).should be_false } 346 | it { @s1.overlaps?(@s2).should be_false } 347 | it { @s1.convex_hull.should be_instance_of(LineString) } 348 | it { @s1.line_substring(0.2,0.5).should be_instance_of(LineString) } 349 | 350 | it do 351 | @s1.interpolate_point(0.7).should be_instance_of(Point) 352 | @s1.interpolate_point(0.7).x.should be_close(1.7,0.1) 353 | end 354 | 355 | it { @s1.should be_simple } 356 | it { @s1.disjoint?(@s2).should be_true } 357 | it { @s1.polygonize.should be_instance_of(GeometryCollection) } 358 | it { @s3.polygonize.geometries.should be_empty } 359 | 360 | # TODO: Starting with Pgis 1.5 this fail.. need to check 361 | it do 362 | lambda { @s2.locate_along_measure(1.6) }.should raise_error 363 | end 364 | 365 | it do 366 | lambda { @s2.locate_between_measures(0.1,0.3).should be_nil }.should raise_error 367 | end 368 | 369 | it "should build area" do 370 | @s2.build_area.should be_nil 371 | end 372 | 373 | it "should acts as jack" do 374 | @s2.segmentize(0.1).should be_instance_of(LineString) 375 | end 376 | 377 | it "should find the UTM zone" do 378 | @s2.utm_zone.should eql(32731) 379 | end 380 | 381 | it "should find the UTM zone" do 382 | @s2.transform!(29101) 383 | @s2.utm_zone.should eql(32732) 384 | end 385 | 386 | it "should transform non saved" do 387 | ls = LineString.from_coordinates([[11435579.3992231,10669620.8116516],[11721337.4281638,11210714.9524106]],29101) 388 | str = Street.new(:geom => ls) 389 | str.transform!(4326) 390 | str.geom[0].x.should be_close(4,0.0000001) 391 | str.geom[0].y.should be_close(4,0.0000001) 392 | str.geom[1].x.should be_close(7,0.0000001) 393 | str.geom[1].y.should be_close(7,0.0000001) 394 | end 395 | 396 | describe "MultiLineString" do 397 | 398 | it "should write nicely" do 399 | @m1.geom.should be_instance_of(MultiLineString) 400 | end 401 | 402 | it "should have 2 geometries" do 403 | @m1.geom.should have(2).geometries 404 | end 405 | 406 | it "should have 2 points on the geometry" do 407 | @m1.geom.geometries[0].length.should eql(2) 408 | end 409 | 410 | it "should calculate multi line string length" do 411 | @m1.length_spheroid.should be_close(470464.54, 0.01) 412 | end 413 | 414 | it "should line merge!" do 415 | merged = @m1.line_merge 416 | merged.should be_instance_of(LineString) 417 | merged.length.should eql(3) 418 | end 419 | 420 | it "should line merge collect" do 421 | pending 422 | co = @m2.line_merge 423 | co.should be_instance_of(LineString) 424 | end 425 | end 426 | end 427 | 428 | end 429 | -------------------------------------------------------------------------------- /lib/postgis_adapter.rb: -------------------------------------------------------------------------------- 1 | # 2 | # PostGIS Adapter 3 | # 4 | # 5 | # Code from 6 | # http://georuby.rubyforge.org Spatial Adapter 7 | # 8 | require 'active_record' 9 | require 'active_record/connection_adapters/postgresql_adapter' 10 | require 'geo_ruby' 11 | require 'postgis_adapter/common_spatial_adapter' 12 | require 'postgis_adapter/functions' 13 | require 'postgis_adapter/functions/common' 14 | require 'postgis_adapter/functions/class' 15 | require 'postgis_adapter/functions/bbox' 16 | require 'postgis_adapter/acts_as_geom' 17 | 18 | include GeoRuby::SimpleFeatures 19 | include SpatialAdapter 20 | 21 | module PostgisAdapter 22 | IGNORE_TABLES = %w{ spatial_ref_sys geometry_columns geography_columns } 23 | end 24 | #tables to ignore in migration : relative to PostGIS management of geometric columns 25 | ActiveRecord::SchemaDumper.ignore_tables.concat PostgisAdapter::IGNORE_TABLES 26 | 27 | #add a method to_yaml to the Geometry class which will transform a geometry in a form suitable to be used in a YAML file (such as in a fixture) 28 | GeoRuby::SimpleFeatures::Geometry.class_eval do 29 | def to_fixture_format 30 | as_hex_ewkb 31 | end 32 | end 33 | 34 | ActiveRecord::Base.class_eval do 35 | 36 | #Vit Ondruch & Tilmann Singer 's patch 37 | def self.get_conditions(attrs) 38 | attrs.map do |attr, value| 39 | attr = attr.to_s 40 | column_name = connection.quote_column_name(attr) 41 | if columns_hash[attr].is_a?(SpatialColumn) 42 | if value.is_a?(Array) 43 | attrs[attr.to_sym]= "BOX3D(" + value[0].join(" ") + "," + value[1].join(" ") + ")" 44 | "#{table_name}.#{column_name} && SetSRID(?::box3d, #{value[2] || @@default_srid || DEFAULT_SRID} ) " 45 | elsif value.is_a?(Envelope) 46 | attrs[attr.to_sym]= "BOX3D(" + value.lower_corner.text_representation + "," + value.upper_corner.text_representation + ")" 47 | "#{table_name}.#{column_name} && SetSRID(?::box3d, #{value.srid} ) " 48 | else 49 | "#{table_name}.#{column_name} && ? " 50 | end 51 | else 52 | attribute_condition("#{table_name}.#{column_name}", value) 53 | end 54 | end.join(' AND ') 55 | end 56 | 57 | #For Rails >= 2 58 | if method(:sanitize_sql_hash_for_conditions).arity == 1 59 | # Before Rails 2.3.3, the method had only one argument 60 | def self.sanitize_sql_hash_for_conditions(attrs) 61 | conditions = get_conditions(attrs) 62 | replace_bind_variables(conditions, expand_range_bind_variables(attrs.values)) 63 | end 64 | elsif method(:sanitize_sql_hash_for_conditions).arity == -2 65 | # After Rails 2.3.3, the method had only two args, the last one optional 66 | def self.sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name) 67 | attrs = expand_hash_conditions_for_aggregates(attrs) 68 | 69 | conditions = attrs.map do |attr, value| 70 | unless value.is_a?(Hash) 71 | attr = attr.to_s 72 | 73 | # Extract table name from qualified attribute names. 74 | if attr.include?('.') 75 | table_name, attr = attr.split('.', 2) 76 | table_name = connection.quote_table_name(table_name) 77 | end 78 | 79 | if columns_hash[attr].is_a?(SpatialColumn) 80 | if value.is_a?(Array) 81 | attrs[attr.to_sym]= "BOX3D(" + value[0].join(" ") + "," + value[1].join(" ") + ")" 82 | "#{table_name}.#{connection.quote_column_name(attr)} && SetSRID(?::box3d, #{value[2] || DEFAULT_SRID} ) " 83 | elsif value.is_a?(Envelope) 84 | attrs[attr.to_sym]= "BOX3D(" + value.lower_corner.text_representation + "," + value.upper_corner.text_representation + ")" 85 | "#{table_name}.#{connection.quote_column_name(attr)} && SetSRID(?::box3d, #{value.srid} ) " 86 | else 87 | "#{table_name}.#{connection.quote_column_name(attr)} && ? " 88 | end 89 | else 90 | attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value) 91 | end 92 | else 93 | sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s)) 94 | end 95 | end.join(' AND ') 96 | 97 | replace_bind_variables(conditions, expand_range_bind_variables(attrs.values)) 98 | end 99 | else 100 | raise "Spatial Adapter will not work with this version of Rails" 101 | end 102 | end 103 | 104 | ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do 105 | 106 | include SpatialAdapter 107 | 108 | # SCHEMA STATEMENTS ======================================== 109 | # 110 | # Use :template on database.yml seems a better practice. 111 | # 112 | # alias :original_recreate_database :recreate_database 113 | # def recreate_database(configuration, enc_option) 114 | # `dropdb -U "#{configuration["test"]["username"]}" #{configuration["test"]["database"]}` 115 | # `createdb #{enc_option} -U "#{configuration["test"]["username"]}" #{configuration["test"]["database"]}` 116 | # `createlang -U "#{configuration["test"]["username"]}" plpgsql #{configuration["test"]["database"]}` 117 | # `psql -d #{configuration["test"]["database"]} -f db/spatial/postgis.sql` 118 | # `psql -d #{configuration["test"]["database"]} -f db/spatial/spatial_ref_sys.sql` 119 | # end 120 | 121 | # alias :original_create_database :create_database 122 | # def create_database(name, options = {}) 123 | # original_create_database(name, options = {}) 124 | # `createlang plpgsql #{name}` 125 | # `psql -d #{name} -f db/spatial/postgis.sql` 126 | # `psql -d #{name} -f db/spatial/spatial_ref_sys.sql` 127 | # end 128 | 129 | alias :original_native_database_types :native_database_types 130 | def native_database_types 131 | original_native_database_types.update(geometry_data_types) 132 | end 133 | 134 | # Hack to make it works with Rails 3.1 135 | alias :original_type_cast :type_cast rescue nil 136 | def type_cast(value, column) 137 | return value.as_hex_ewkb if value.is_a?(GeoRuby::SimpleFeatures::Geometry) 138 | original_type_cast(value,column) 139 | end 140 | 141 | alias :original_quote :quote 142 | #Redefines the quote method to add behaviour for when a Geometry is encountered 143 | def quote(value, column = nil) 144 | if value.kind_of?(GeoRuby::SimpleFeatures::Geometry) 145 | "'#{value.as_hex_ewkb}'" 146 | else 147 | original_quote(value,column) 148 | end 149 | end 150 | 151 | alias :original_tables :tables 152 | def tables(name = nil) #:nodoc: 153 | original_tables(name) + views(name) 154 | end 155 | 156 | def views(name = nil) #:nodoc: 157 | schemas = schema_search_path.split(/,/).map { |p| quote(p.strip) }.join(',') 158 | query(<<-SQL, name).map { |row| row[0] } 159 | SELECT viewname 160 | FROM pg_views 161 | WHERE schemaname IN (#{schemas}) 162 | SQL 163 | end 164 | 165 | def create_table(name, options = {}) 166 | table_definition = ActiveRecord::ConnectionAdapters::PostgreSQLTableDefinition.new(self) 167 | table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false 168 | 169 | yield table_definition 170 | 171 | if options[:force] 172 | drop_table(name) rescue nil 173 | end 174 | 175 | create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " 176 | create_sql << "#{name} (" 177 | create_sql << table_definition.to_sql 178 | create_sql << ") #{options[:options]}" 179 | execute create_sql 180 | 181 | #added to create the geometric columns identified during the table definition 182 | unless table_definition.geom_columns.nil? 183 | table_definition.geom_columns.each do |geom_column| 184 | execute geom_column.to_sql(name) 185 | end 186 | end 187 | end 188 | 189 | alias :original_remove_column :remove_column 190 | def remove_column(table_name,column_name, options = {}) 191 | columns(table_name).each do |col| 192 | if col.name == column_name.to_s 193 | #check if the column is geometric 194 | unless geometry_data_types[col.type].nil? or 195 | (options[:remove_using_dropgeometrycolumn] == false) 196 | execute "SELECT DropGeometryColumn('#{table_name}','#{column_name}')" 197 | else 198 | original_remove_column(table_name,column_name) 199 | end 200 | end 201 | end 202 | end 203 | 204 | alias :original_add_column :add_column 205 | def add_column(table_name, column_name, type, options = {}) 206 | unless geometry_data_types[type].nil? or (options[:create_using_addgeometrycolumn] == false) 207 | geom_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumnDefinition. 208 | new(self,column_name, type, nil,nil,options[:null],options[:srid] || -1 , 209 | options[:with_z] || false , options[:with_m] || false) 210 | 211 | execute geom_column.to_sql(table_name) 212 | else 213 | original_add_column(table_name,column_name,type,options) 214 | end 215 | end 216 | 217 | # Adds a GIST spatial index to a column. Its name will be 218 | # __spatial_index unless 219 | # the key :name is present in the options hash, in which case its 220 | # value is taken as the name of the index. 221 | def add_index(table_name, column_name, options = {}) 222 | index_name = options[:name] || index_name(table_name,:column => Array(column_name)) 223 | if options[:spatial] 224 | execute "CREATE INDEX #{index_name} ON #{table_name} USING GIST (#{Array(column_name).join(", ")} GIST_GEOMETRY_OPS)" 225 | else 226 | super 227 | end 228 | end 229 | 230 | def indexes(table_name, name = nil) #:nodoc: 231 | result = query(<<-SQL, name) 232 | SELECT i.relname, d.indisunique, a.attname , am.amname 233 | FROM pg_class t, pg_class i, pg_index d, pg_attribute a, pg_am am 234 | WHERE i.relkind = 'i' 235 | AND d.indexrelid = i.oid 236 | AND d.indisprimary = 'f' 237 | AND t.oid = d.indrelid 238 | AND i.relam = am.oid 239 | AND t.relname = '#{table_name}' 240 | AND a.attrelid = t.oid 241 | AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum 242 | OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum 243 | OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum 244 | OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum 245 | OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum ) 246 | ORDER BY i.relname 247 | SQL 248 | 249 | current_index = nil 250 | indexes = [] 251 | 252 | result.each do |row| 253 | if current_index != row[0] 254 | #index type gist indicates a spatial index (probably not totally true but let's simplify!) 255 | indexes << ActiveRecord::ConnectionAdapters::IndexDefinition. 256 | new(table_name, row[0], row[1] == "t", row[3] == "gist" ,[]) 257 | 258 | current_index = row[0] 259 | end 260 | indexes.last.columns << row[2] 261 | end 262 | indexes 263 | end 264 | 265 | def columns(table_name, name = nil) #:nodoc: 266 | raw_geom_infos = column_spatial_info(table_name) 267 | 268 | column_definitions(table_name).collect do |name, type, default, notnull| 269 | if type =~ /geometry/i 270 | raw_geom_info = raw_geom_infos[name] 271 | if raw_geom_info.nil? 272 | ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_simplified(name, default, notnull == "f") 273 | else 274 | ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.new(name, default,raw_geom_info.type, notnull == "f", raw_geom_info.srid, raw_geom_info.with_z, raw_geom_info.with_m) 275 | end 276 | else 277 | ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(name, default, type, notnull == "f") 278 | end 279 | end 280 | end 281 | 282 | # For version of Rails where exists disable_referential_integrity 283 | if self.instance_methods.include? :disable_referential_integrity 284 | #Pete Deffendol's patch 285 | alias :original_disable_referential_integrity :disable_referential_integrity 286 | def disable_referential_integrity(&block) #:nodoc: 287 | execute(tables.reject { |name| PostgisAdapter::IGNORE_TABLES.include?(name) }.map { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) 288 | yield 289 | ensure 290 | execute(tables.reject { |name| PostgisAdapter::IGNORE_TABLES.include?(name) }.map { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) 291 | end 292 | end 293 | 294 | private 295 | 296 | def column_spatial_info(table_name) 297 | constr = query <<-end_sql 298 | SELECT * FROM geometry_columns WHERE f_table_name = '#{table_name}' 299 | end_sql 300 | 301 | raw_geom_infos = {} 302 | constr.each do |constr_def_a| 303 | raw_geom_infos[constr_def_a[3]] ||= ActiveRecord::ConnectionAdapters::RawGeomInfo.new 304 | raw_geom_infos[constr_def_a[3]].type = constr_def_a[6] 305 | raw_geom_infos[constr_def_a[3]].dimension = constr_def_a[4].to_i 306 | raw_geom_infos[constr_def_a[3]].srid = constr_def_a[5].to_i 307 | 308 | if raw_geom_infos[constr_def_a[3]].type[-1] == ?M 309 | raw_geom_infos[constr_def_a[3]].with_m = true 310 | raw_geom_infos[constr_def_a[3]].type.chop! 311 | else 312 | raw_geom_infos[constr_def_a[3]].with_m = false 313 | end 314 | end 315 | 316 | raw_geom_infos.each_value do |raw_geom_info| 317 | #check the presence of z and m 318 | raw_geom_info.convert! 319 | end 320 | 321 | raw_geom_infos 322 | rescue => e 323 | nil 324 | end 325 | 326 | end 327 | 328 | module ActiveRecord 329 | module ConnectionAdapters 330 | class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc: 331 | def convert! 332 | self.type = "geometry" if self.type.nil? #if geometry the geometrytype constraint is not present : need to set the type here then 333 | 334 | if dimension == 4 335 | self.with_m = true 336 | self.with_z = true 337 | elsif dimension == 3 338 | if with_m 339 | self.with_z = false 340 | self.with_m = true 341 | else 342 | self.with_z = true 343 | self.with_m = false 344 | end 345 | else 346 | self.with_z = false 347 | self.with_m = false 348 | end 349 | end 350 | end 351 | end 352 | end 353 | 354 | 355 | module ActiveRecord 356 | module ConnectionAdapters 357 | class PostgreSQLTableDefinition < TableDefinition 358 | attr_reader :geom_columns 359 | 360 | def column(name, type, options = {}) 361 | unless (@base.geometry_data_types[type.to_sym].nil? or 362 | (options[:create_using_addgeometrycolumn] == false)) 363 | 364 | geom_column = PostgreSQLColumnDefinition.new(@base,name, type) 365 | geom_column.null = options[:null] 366 | geom_column.srid = options[:srid] || -1 367 | geom_column.with_z = options[:with_z] || false 368 | geom_column.with_m = options[:with_m] || false 369 | 370 | @geom_columns = [] if @geom_columns.nil? 371 | @geom_columns << geom_column 372 | else 373 | super(name,type,options) 374 | end 375 | end 376 | 377 | SpatialAdapter.geometry_data_types.keys.each do |column_type| 378 | class_eval <<-EOV 379 | def #{column_type}(*args) 380 | options = args.extract_options! 381 | column_names = args 382 | 383 | column_names.each { |name| column(name, '#{column_type}', options) } 384 | end 385 | EOV 386 | end 387 | end 388 | 389 | class PostgreSQLColumnDefinition < ColumnDefinition 390 | attr_accessor :srid, :with_z,:with_m 391 | attr_reader :spatial 392 | 393 | def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil,null=nil,srid=-1,with_z=false,with_m=false) 394 | super(base, name, type, limit, default,null) 395 | @spatial=true 396 | @srid=srid 397 | @with_z=with_z 398 | @with_m=with_m 399 | end 400 | 401 | def to_sql(table_name) 402 | if @spatial 403 | type_sql = type_to_sql(type.to_sym) 404 | type_sql += "M" if with_m and !with_z 405 | if with_m and with_z 406 | dimension = 4 407 | elsif with_m or with_z 408 | dimension = 3 409 | else 410 | dimension = 2 411 | end 412 | 413 | column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})" 414 | column_sql += ";ALTER TABLE #{table_name} ALTER #{name} SET NOT NULL" if null == false 415 | column_sql 416 | else 417 | super 418 | end 419 | end 420 | 421 | 422 | private 423 | def type_to_sql(name, limit=nil) 424 | base.type_to_sql(name, limit) rescue name 425 | end 426 | 427 | end 428 | 429 | end 430 | end 431 | 432 | # Would prefer creation of a PostgreSQLColumn type instead but I would 433 | # need to reimplement methods where Column objects are instantiated so 434 | # I leave it like this 435 | module ActiveRecord 436 | module ConnectionAdapters 437 | class SpatialPostgreSQLColumn < Column 438 | 439 | include SpatialColumn 440 | 441 | #Transforms a string to a geometry. PostGIS returns a HexEWKB string. 442 | def self.string_to_geometry(string) 443 | return string unless string.is_a?(String) 444 | GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(string) rescue nil 445 | end 446 | 447 | def self.create_simplified(name,default,null = true) 448 | new(name,default,"geometry",null,nil,nil,nil) 449 | end 450 | 451 | end 452 | end 453 | end 454 | -------------------------------------------------------------------------------- /lib/postgis_adapter/functions/common.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # 3 | # 4 | # COMMON GEOMETRICAL FUNCTIONS 5 | # 6 | # The methods here can be used by all geoms. 7 | # 8 | 9 | module PostgisAdapter 10 | module Functions 11 | 12 | # 13 | # Test if a geometry is well formed. 14 | # 15 | def valid_geom? 16 | postgis_calculate(:isvalid, self) 17 | end 18 | 19 | # 20 | # True if the given geometries represent the same geometry. 21 | # Directionality is ignored. 22 | # 23 | # Returns TRUE if the given Geometries are "spatially equal". 24 | # Use this for a 'better' answer than '='. Note by spatially equal we 25 | # mean ST_Within(A,B) = true and ST_Within(B,A) = true and also mean ordering 26 | # of points can be different but represent the same geometry structure. 27 | # To verify the order of points is consistent, use ST_OrderingEquals 28 | # (it must be noted ST_OrderingEquals is a little more stringent than 29 | # simply verifying order of points are the same). 30 | # 31 | # This function will return false if either geometry is invalid even 32 | # if they are binary equal. 33 | # 34 | # Returns Boolean ST_Equals(geometry A, geometry B); 35 | # 36 | def spatially_equal?(other) 37 | postgis_calculate(:equals, [self, other]) 38 | end 39 | 40 | # 41 | # Returns the minimum bounding box for the supplied geometry, as a geometry. 42 | # The polygon is defined by the corner points of the bounding box 43 | # ((MINX, MINY), (MINX, MAXY), (MAXX, MAXY), (MAXX, MINY), (MINX, MINY)). 44 | # PostGIS will add a ZMIN/ZMAX coordinate as well/ 45 | # 46 | # Degenerate cases (vertical lines, points) will return a geometry of 47 | # lower dimension than POLYGON, ie. POINT or LINESTRING. 48 | # 49 | # In PostGIS, the bounding box of a geometry is represented internally using 50 | # float4s instead of float8s that are used to store geometries. The bounding 51 | # box coordinates are floored, guarenteeing that the geometry is contained 52 | # entirely within its bounds. This has the advantage that a geometry's 53 | # bounding box is half the size as the minimum bounding rectangle, 54 | # which means significantly faster indexes and general performance. 55 | # But it also means that the bounding box is NOT the same as the minimum 56 | # bounding rectangle that bounds the geome. 57 | # 58 | # Returns GeometryCollection ST_Envelope(geometry g1); 59 | # 60 | def envelope 61 | postgis_calculate(:envelope, self) 62 | end 63 | 64 | # 65 | # Computes the geometric center of a geometry, or equivalently, 66 | # the center of mass of the geometry as a POINT. For [MULTI]POINTs, this is 67 | # computed as the arithmetric mean of the input coordinates. 68 | # For [MULTI]LINESTRINGs, this is computed as the weighted length of each 69 | # line segment. For [MULTI]POLYGONs, "weight" is thought in terms of area. 70 | # If an empty geometry is supplied, an empty GEOMETRYCOLLECTION is returned. 71 | # If NULL is supplied, NULL is returned. 72 | # 73 | # The centroid is equal to the centroid of the set of component Geometries of 74 | # highest dimension (since the lower-dimension geometries contribute zero 75 | # "weight" to the centroid). 76 | # 77 | # Computation will be more accurate if performed by the GEOS module (enabled at compile time). 78 | # 79 | # http://postgis.refractions.net/documentation/manual-svn/ST_Centroid.html 80 | # 81 | # Returns Geometry ST_Centroid(geometry g1); 82 | # 83 | def centroid 84 | postgis_calculate(:centroid, self) 85 | end 86 | 87 | # 88 | # Returns the closure of the combinatorial boundary of this Geometry. 89 | # The combinatorial boundary is defined as described in section 3.12.3.2 of the 90 | # OGC SPEC. Because the result of this function is a closure, and hence topologically 91 | # closed, the resulting boundary can be represented using representational 92 | # geometry primitives as discussed in the OGC SPEC, section 3.12.2. 93 | # 94 | # Do not call with a GEOMETRYCOLLECTION as an argument. 95 | # 96 | # Performed by the GEOS module. 97 | # 98 | # Returns Geometry ST_Boundary(geometry geomA); 99 | # 100 | def boundary 101 | postgis_calculate(:boundary, self) 102 | end 103 | 104 | # 105 | # 2D minimum cartesian distance between two geometries in projected units. 106 | # 107 | # Returns Float ST_Distance(geometry g1, geometry g2); 108 | # 109 | def distance_to(other) 110 | postgis_calculate(:distance, [self, other]).to_f 111 | end 112 | 113 | # 114 | # True if geometry A is completely inside geometry B. 115 | # 116 | # For this function to make sense, the source geometries must both be of the same 117 | # coordinate projection, having the same SRID. It is a given that 118 | # if ST_Within(A,B) is true and ST_Within(B,A) is true, then the 119 | # two geometries are considered spatially equal. 120 | # 121 | # This function call will automatically include a bounding box comparison that will 122 | # make use of any indexes that are available on the geometries. To avoid index use, 123 | # use the function _ST_Within. 124 | # 125 | # Do not call with a GEOMETRYCOLLECTION as an argument 126 | # Do not use this function with invalid geometries. You will get unexpected results. 127 | # 128 | # Performed by the GEOS module. 129 | # 130 | # Returns Boolean ST_Within(geometry A, geometry B); 131 | # 132 | def within? other 133 | postgis_calculate(:within, [self, other]) 134 | end 135 | 136 | # 137 | # True if the geometries are within the specified distance of one another. 138 | # The distance is specified in units defined by the spatial reference system 139 | # of the geometries. For this function to make sense, the source geometries 140 | # must both be of the same coorindate projection, having the same SRID. 141 | # 142 | # Returns boolean ST_DWithin(geometry g1, geometry g2, double precision distance); 143 | # 144 | def d_within?(other, margin=0.1) 145 | postgis_calculate(:dwithin, [self, other], margin) 146 | end 147 | alias_method "in_bounds?", "d_within?" 148 | 149 | # 150 | # True if geometry B is completely inside geometry A. 151 | # 152 | # For this function to make sense, the source geometries must both be of the same 153 | # coordinate projection, having the same SRID. 'contains?' is the inverse of 'within?'. 154 | # 155 | # So a.contains?(b) is like b.within?(a) except in the case of invalid 156 | # geometries where the result is always false regardless or not defined. 157 | # 158 | # Do not call with a GEOMETRYCOLLECTION as an argument 159 | # Do not use this function with invalid geometries. You will get unexpected results. 160 | # 161 | # Performed by the GEOS module 162 | # 163 | # Returns Boolean ST_Contains(geometry geomA, geometry geomB); 164 | # 165 | def contains? other 166 | postgis_calculate(:contains, [self, other]) 167 | end 168 | 169 | # 170 | # True if no point in Geometry A is outside Geometry B 171 | # 172 | # This function call will automatically include a bounding box comparison that 173 | # will make use of any indexes that are available on the geometries. To avoid 174 | # index use, use the function _ST_CoveredBy. 175 | # 176 | # Do not call with a GEOMETRYCOLLECTION as an argument. 177 | # Do not use this function with invalid geometries. You will get unexpected results. 178 | # 179 | # Performed by the GEOS module. 180 | # 181 | # Aliased as 'inside?' 182 | # 183 | # Returns Boolean ST_CoveredBy(geometry geomA, geometry geomB); 184 | # 185 | def covered_by? other 186 | postgis_calculate(:coveredby, [self, other]) 187 | end 188 | alias_method "inside?", "covered_by?" 189 | 190 | # 191 | # Eye-candy. See 'covered_by?'. 192 | # 193 | # Returns !(Boolean ST_CoveredBy(geometry geomA, geometry geomB);) 194 | # 195 | def outside? other 196 | !covered_by? other 197 | end 198 | 199 | # 200 | # True if the Geometries do not "spatially intersect" - if they 201 | # do not share any space together. 202 | # 203 | # Overlaps, Touches, Within all imply geometries are not spatially disjoint. 204 | # If any of the aforementioned returns true, then the geometries are not 205 | # spatially disjoint. Disjoint implies false for spatial intersection. 206 | # 207 | # Do not call with a GEOMETRYCOLLECTION as an argument. 208 | # 209 | # Returns boolean ST_Disjoint( geometry A , geometry B ); 210 | # 211 | def disjoint? other 212 | postgis_calculate(:disjoint, [self, other]) 213 | end 214 | 215 | # 216 | # How many dimensions the geom is made of (2, 3 or 4) 217 | # 218 | # Returns Integer ST_Dimension(geom g1) 219 | # 220 | def dimension 221 | postgis_calculate(:dimension, self).to_i 222 | end 223 | 224 | # 225 | # Returns a "simplified" version of the given geometry using the Douglas-Peuker 226 | # algorithm. Will actually do something only with (multi)lines and (multi)polygons 227 | # but you can safely call it with any kind of geometry. Since simplification 228 | # occurs on a object-by-object basis you can also feed a GeometryCollection to this 229 | # function. 230 | # 231 | # Note that returned geometry might loose its simplicity (see 'is_simple?'). 232 | # Topology may not be preserved and may result in invalid geometries. 233 | # Use 'simplify_preserve_topology' to preserve topology. 234 | # 235 | # Performed by the GEOS Module. 236 | # 237 | # Returns Geometry ST_Simplify(geometry geomA, float tolerance); 238 | # 239 | def simplify(tolerance=0.1) 240 | postgis_calculate(:simplify, self, tolerance) 241 | end 242 | 243 | def simplify!(tolerance=0.1) 244 | #FIXME: not good.. 245 | self.update_attribute(geo_columns.first, simplify) 246 | end 247 | 248 | # 249 | # 250 | def buffer(width=0.1) 251 | postgis_calculate(:buffer, self, width) 252 | end 253 | 254 | # 255 | # Returns a "simplified" version of the given geometry using the Douglas-Peuker 256 | # algorithm. Will avoid creating derived geometries (polygons in particular) that 257 | # are invalid. Will actually do something only with (multi)lines and (multi)polygons 258 | # but you can safely call it with any kind of geometry. Since simplification occurs 259 | # on a object-by-object basis you can also feed a GeometryCollection to this function. 260 | # 261 | # Performed by the GEOS module. Requires GEOS 3.0.0+ 262 | # 263 | # Returns Geometry ST_SimplifyPreserveTopology(geometry geomA, float tolerance); 264 | # 265 | def simplify_preserve_topology(tolerance=0.1) 266 | postgis_calculate(:simplifypreservetopology, self, tolerance) 267 | end 268 | 269 | # 270 | # True if Geometries "spatially intersect", share any portion of space. 271 | # False if they don't (they are Disjoint). 272 | # 273 | # 'overlaps?', 'touches?', 'within?' all imply spatial intersection. 274 | # If any of the aforementioned returns true, then the geometries also 275 | # spatially intersect. 'disjoint?' implies false for spatial intersection. 276 | # 277 | # Returns Boolean ST_Intersects(geometry geomA, geometry geomB); 278 | # 279 | def intersects? other 280 | postgis_calculate(:intersects, [self, other]) 281 | end 282 | 283 | # 284 | # True if a Geometry`s Envelope "spatially intersect", share any portion of space. 285 | # 286 | # It`s 'intersects?', for envelopes. 287 | # 288 | # Returns Boolean SE_EnvelopesIntersect(geometry geomA, geometry geomB); 289 | # 290 | def envelopes_intersect? other 291 | postgis_calculate(:se_envelopesintersect, [self, other]) 292 | end 293 | 294 | # 295 | # Geometry that represents the point set intersection of the Geometries. 296 | # In other words - that portion of geometry A and geometry B that is shared between 297 | # the two geometries. If the geometries do not share any space (are disjoint), 298 | # then an empty geometry collection is returned. 299 | # 300 | # 'intersection' in conjunction with intersects? is very useful for clipping 301 | # geometries such as in bounding box, buffer, region queries where you only want 302 | # to return that portion of a geometry that sits in a country or region of interest. 303 | # 304 | # Do not call with a GEOMETRYCOLLECTION as an argument. 305 | # Performed by the GEOS module. 306 | # 307 | # Returns Geometry ST_Intersection(geometry geomA, geometry geomB); 308 | # 309 | def intersection other 310 | postgis_calculate(:intersection, [self, other]) 311 | end 312 | 313 | # 314 | # True if the Geometries share space, are of the same dimension, but are 315 | # not completely contained by each other. They intersect, but one does not 316 | # completely contain another. 317 | # 318 | # Do not call with a GeometryCollection as an argument 319 | # This function call will automatically include a bounding box comparison that 320 | # will make use of any indexes that are available on the geometries. To avoid 321 | # index use, use the function _ST_Overlaps. 322 | # 323 | # Performed by the GEOS module. 324 | # 325 | # Returns Boolean ST_Overlaps(geometry A, geometry B); 326 | # 327 | def overlaps? other 328 | postgis_calculate(:overlaps, [self, other]) 329 | end 330 | 331 | # True if the geometries have at least one point in common, 332 | # but their interiors do not intersect. 333 | # 334 | # If the only points in common between g1 and g2 lie in the union of the 335 | # boundaries of g1 and g2. The 'touches?' relation applies to all Area/Area, 336 | # Line/Line, Line/Area, Point/Area and Point/Line pairs of relationships, 337 | # but not to the Point/Point pair. 338 | # 339 | # Returns Boolean ST_Touches(geometry g1, geometry g2); 340 | # 341 | def touches? other 342 | postgis_calculate(:touches, [self, other]) 343 | end 344 | 345 | def st_collect(other=nil) 346 | postgis_calculate(:collect, [self, other]) 347 | end 348 | # 349 | # The convex hull of a geometry represents the minimum closed geometry that 350 | # encloses all geometries within the set. 351 | # 352 | # It is usually used with MULTI and Geometry Collections. Although it is not 353 | # an aggregate - you can use it in conjunction with ST_Collect to get the convex 354 | # hull of a set of points. ST_ConvexHull(ST_Collect(somepointfield)). 355 | # It is often used to determine an affected area based on a set of point observations. 356 | # 357 | # Performed by the GEOS module. 358 | # 359 | # Returns Geometry ST_ConvexHull(geometry geomA); 360 | # 361 | def convex_hull 362 | postgis_calculate(:convexhull, self) 363 | end 364 | 365 | # 366 | # Creates an areal geometry formed by the constituent linework of given geometry. 367 | # The return type can be a Polygon or MultiPolygon, depending on input. 368 | # If the input lineworks do not form polygons NULL is returned. The inputs can 369 | # be LINESTRINGS, MULTILINESTRINGS, POLYGONS, MULTIPOLYGONS, and GeometryCollections. 370 | # 371 | # Returns Boolean ST_BuildArea(geometry A); 372 | # 373 | def build_area 374 | postgis_calculate(:buildarea, self) 375 | end 376 | 377 | # 378 | # Returns true if this Geometry has no anomalous geometric points, such as 379 | # self intersection or self tangency. 380 | # 381 | # Returns boolean ST_IsSimple(geometry geomA); 382 | # 383 | def is_simple? 384 | postgis_calculate(:issimple, self) 385 | end 386 | alias_method "simple?", "is_simple?" 387 | 388 | # 389 | # Aggregate. Creates a GeometryCollection containing possible polygons formed 390 | # from the constituent linework of a set of geometries. 391 | # 392 | # Geometry Collections are often difficult to deal with with third party tools, 393 | # so use ST_Polygonize in conjunction with ST_Dump to dump the polygons out into 394 | # individual polygons. 395 | # 396 | # Returns Geometry ST_Polygonize(geometry set geomfield); 397 | # 398 | def polygonize 399 | postgis_calculate(:polygonize, self) 400 | end 401 | 402 | # 403 | # Returns true if this Geometry is spatially related to anotherGeometry, 404 | # by testing for intersections between the Interior, Boundary and Exterior 405 | # of the two geometries as specified by the values in the 406 | # intersectionPatternMatrix. If no intersectionPatternMatrix is passed in, 407 | # then returns the maximum intersectionPatternMatrix that relates the 2 geometries. 408 | # 409 | # 410 | # Version 1: Takes geomA, geomB, intersectionMatrix and Returns 1 (TRUE) if 411 | # this Geometry is spatially related to anotherGeometry, by testing for 412 | # intersections between the Interior, Boundary and Exterior of the two 413 | # geometries as specified by the values in the intersectionPatternMatrix. 414 | # 415 | # This is especially useful for testing compound checks of intersection, 416 | # crosses, etc in one step. 417 | # 418 | # Do not call with a GeometryCollection as an argument 419 | # 420 | # This is the "allowable" version that returns a boolean, not an integer. 421 | # This is defined in OGC spec. 422 | # This DOES NOT automagically include an index call. The reason for that 423 | # is some relationships are anti e.g. Disjoint. If you are using a relationship 424 | # pattern that requires intersection, then include the && index call. 425 | # 426 | # Version 2: Takes geomA and geomB and returns the DE-9IM 427 | # (dimensionally extended nine-intersection matrix) 428 | # 429 | # Do not call with a GeometryCollection as an argument 430 | # Not in OGC spec, but implied. see s2.1.13.2 431 | # 432 | # Both Performed by the GEOS module 433 | # 434 | # Returns: 435 | # 436 | # String ST_Relate(geometry geomA, geometry geomB); 437 | # Boolean ST_Relate(geometry geomA, geometry geomB, text intersectionPatternMatrix); 438 | # 439 | def relate?(other, m = nil) 440 | # Relate is case sentitive....... 441 | m = "'#{m}'" if m 442 | postgis_calculate("Relate", [self, other], m) 443 | end 444 | 445 | # 446 | # Transform the geometry into a different spatial reference system. 447 | # The destination SRID must exist in the SPATIAL_REF_SYS table. 448 | # 449 | # This method implements the OpenGIS Simple Features Implementation Specification for SQL. 450 | # This method supports Circular Strings and Curves (PostGIS 1.3.4+) 451 | # 452 | # Requires PostGIS be compiled with Proj support. 453 | # 454 | # Return Geometry ST_Transform(geometry g1, integer srid); 455 | # 456 | def transform!(new_srid) 457 | self[postgis_geoms.keys[0]] = postgis_calculate("Transform", self.new_record? ? self.geom : self, new_srid) 458 | end 459 | 460 | def transform(new_srid) 461 | dup.transform!(new_srid) 462 | end 463 | 464 | # 465 | # Returns a modified geometry having no segment longer than the given distance. 466 | # Distance computation is performed in 2d only. 467 | # 468 | # This will only increase segments. It will not lengthen segments shorter than max length 469 | # 470 | # Return Geometry ST_Segmentize(geometry geomA, float max_length); 471 | # 472 | def segmentize(max_length=1.0) 473 | postgis_calculate("segmentize", self, max_length) 474 | end 475 | 476 | # 477 | # Returns the instance`s geom srid 478 | # 479 | def srid 480 | self[postgis_geoms.keys.first].srid 481 | end 482 | 483 | # 484 | # Return UTM Zone for a geom 485 | # 486 | # Return Integer 487 | def utm_zone 488 | if srid == 4326 489 | geom = centroid 490 | else 491 | geomdup = transform(4326) 492 | mezzo = geomdup.length / 2 493 | geom = case geomdup 494 | when Point then geomdup 495 | when LineString then geomdup[mezzo] 496 | else 497 | geomgeog[mezzo][geomgeo[mezzo]/2] 498 | end 499 | 500 | end 501 | 502 | pref = geom.y > 0 ? 32700 : 32600 503 | zone = ((geom.x + 180) / 6 + 1).to_i 504 | zone + pref 505 | end 506 | 507 | # 508 | # Returns the Geometry in its UTM Zone 509 | # 510 | # Return Geometry 511 | def to_utm!(utm=nil) 512 | utm ||= utm_zone 513 | self[postgis_geoms.keys.first] = transform(utm) 514 | end 515 | 516 | def to_utm 517 | dup.to_utm! 518 | end 519 | 520 | # 521 | # Returns Geometry as GeoJSON 522 | # 523 | # http://geojson.org/ 524 | # 525 | def as_geo_json(precision=15, bbox = 0) 526 | postgis_calculate(:AsGeoJSON, self, [precision, bbox]) 527 | end 528 | 529 | # 530 | # ST_PointOnSurface — Returns a POINT guaranteed to lie on the surface. 531 | # 532 | # geometry ST_PointOnSurface(geometry g1);eometry A, geometry B); 533 | # 534 | def point_on_surface 535 | postgis_calculate(:pointonsurface, self) 536 | end 537 | 538 | # 539 | # 540 | # LINESTRING 541 | # 542 | # 543 | # 544 | module LineStringFunctions 545 | 546 | # 547 | # Returns the 2D length of the geometry if it is a linestring, multilinestring, 548 | # ST_Curve, ST_MultiCurve. 0 is returned for areal geometries. For areal geometries 549 | # use 'perimeter'. Measurements are in the units of the spatial reference system 550 | # of the geometry. 551 | # 552 | # Returns Float 553 | # 554 | def length 555 | postgis_calculate(:length, self).to_f 556 | end 557 | 558 | # 559 | # Returns the 3-dimensional or 2-dimensional length of the geometry if it is 560 | # a linestring or multi-linestring. For 2-d lines it will just return the 2-d 561 | # length (same as 'length') 562 | # 563 | # Returns Float 564 | # 565 | def length_3d 566 | postgis_calculate(:length3d, self).to_f 567 | end 568 | 569 | # 570 | # Calculates the length of a geometry on an ellipsoid. This is useful if the 571 | # coordinates of the geometry are in longitude/latitude and a length is 572 | # desired without reprojection. The ellipsoid is a separate database type and 573 | # can be constructed as follows: 574 | # 575 | # SPHEROID[,,] 576 | # 577 | # Example: 578 | # SPHEROID["GRS_1980",6378137,298.257222101] 579 | # 580 | # Defaults to: 581 | # 582 | # SPHEROID["IERS_2003",6378136.6,298.25642] 583 | # 584 | # Returns Float length_spheroid(geometry linestring, spheroid); 585 | # 586 | def length_spheroid(spheroid = EARTH_SPHEROID) 587 | postgis_calculate(:length_spheroid, self, spheroid).to_f 588 | end 589 | 590 | # 591 | # Return the number of points of the geometry. 592 | # PostGis ST_NumPoints does not work as nov/08 593 | # 594 | # Returns Integer ST_NPoints(geometry g1); 595 | # 596 | def num_points 597 | postgis_calculate(:npoints, self).to_i 598 | end 599 | 600 | # 601 | # Returns geometry start point. 602 | # 603 | def start_point 604 | postgis_calculate(:startpoint, self) 605 | end 606 | 607 | # 608 | # Returns geometry end point. 609 | # 610 | def end_point 611 | postgis_calculate(:endpoint, self) 612 | end 613 | 614 | # 615 | # Takes two geometry objects and returns TRUE if their intersection 616 | # "spatially cross", that is, the geometries have some, but not all interior 617 | # points in common. The intersection of the interiors of the geometries must 618 | # not be the empty set and must have a dimensionality less than the the 619 | # maximum dimension of the two input geometries. Additionally, the 620 | # intersection of the two geometries must not equal either of the source 621 | # geometries. Otherwise, it returns FALSE. 622 | # 623 | # 624 | # Returns Boolean ST_Crosses(geometry g1, geometry g2); 625 | # 626 | def crosses? other 627 | postgis_calculate(:crosses, [self, other]) 628 | end 629 | 630 | # 631 | # Warning: PostGIS 1.4+ 632 | # 633 | # Return crossing direction 634 | def line_crossing_direction(other) 635 | postgis_calculate(:lineCrossingDirection, [self, other]) 636 | end 637 | 638 | # 639 | # Returns a float between 0 and 1 representing the location of the closest point 640 | # on LineString to the given Point, as a fraction of total 2d line length. 641 | # 642 | # You can use the returned location to extract a Point (ST_Line_Interpolate_Point) 643 | # or a substring (ST_Line_Substring). 644 | # 645 | # This is useful for approximating numbers of addresses. 646 | # 647 | # Returns float (0 to 1) ST_Line_Locate_Point(geometry a_linestring, geometry a_point); 648 | # 649 | def locate_point point 650 | postgis_calculate(:line_locate_point, [self, point]).to_f 651 | end 652 | 653 | # 654 | # Return a derived geometry collection value with elements that match the 655 | # specified measure. Polygonal elements are not supported. 656 | # 657 | # Semantic is specified by: ISO/IEC CD 13249-3:200x(E) - Text for 658 | # Continuation CD Editing Meeting 659 | # 660 | # Returns geometry ST_Locate_Along_Measure(geometry ageom_with_measure, float a_measure); 661 | # 662 | def locate_along_measure(measure) 663 | postgis_calculate(:locate_along_measure, self, measure) 664 | end 665 | 666 | # 667 | # Return a derived geometry collection value with elements that match the 668 | # specified range of measures inclusively. Polygonal elements are not supported. 669 | # 670 | # Semantic is specified by: ISO/IEC CD 13249-3:200x(E) - Text for Continuation CD Editing Meeting 671 | # 672 | # Returns geometry ST_Locate_Between_Measures(geometry geomA, float measure_start, float measure_end); 673 | # 674 | def locate_between_measures(a, b) 675 | postgis_calculate(:locate_between_measures, self, [a,b]) 676 | end 677 | 678 | # 679 | # Returns a point interpolated along a line. First argument must be a LINESTRING. 680 | # Second argument is a float8 between 0 and 1 representing fraction of total 681 | # linestring length the point has to be located. 682 | # 683 | # See ST_Line_Locate_Point for computing the line location nearest to a Point. 684 | # 685 | # Returns geometry ST_Line_Interpolate_Point(geometry a_linestring, float a_fraction); 686 | # 687 | def interpolate_point(fraction) 688 | postgis_calculate(:line_interpolate_point, self, fraction) 689 | end 690 | 691 | # 692 | # Return a linestring being a substring of the input one starting and ending 693 | # at the given fractions of total 2d length. Second and third arguments are 694 | # float8 values between 0 and 1. This only works with LINESTRINGs. To use 695 | # with contiguous MULTILINESTRINGs use in conjunction with ST_LineMerge. 696 | # 697 | # If 'start' and 'end' have the same value this is equivalent to 'interpolate_point'. 698 | # 699 | # See 'locate_point' for computing the line location nearest to a Point. 700 | # 701 | # Returns geometry ST_Line_Substring(geometry a_linestring, float startfraction, float endfraction); 702 | # 703 | def line_substring(s,e) 704 | postgis_calculate(:line_substring, self, [s, e]) 705 | end 706 | 707 | ### 708 | #Not implemented in postgis yet 709 | # ST_max_distance Returns the largest distance between two line strings. 710 | #def max_distance other 711 | # #float ST_Max_Distance(geometry g1, geometry g2); 712 | # postgis_calculate(:max_distance, [self, other]) 713 | #end 714 | 715 | # 716 | # Returns a (set of) LineString(s) formed by sewing together a MULTILINESTRING. 717 | # 718 | # Only use with MULTILINESTRING/LINESTRINGs. If you feed a polygon or geometry collection into this function, it will return an empty GEOMETRYCOLLECTION 719 | # 720 | # Returns geometry ST_LineMerge(geometry amultilinestring); 721 | # 722 | def line_merge 723 | postgis_calculate(:LineMerge, self, { :stcollect => self}) 724 | end 725 | 726 | end 727 | # 728 | # 729 | # 730 | # 731 | # POINT 732 | # 733 | # 734 | # 735 | # 736 | module PointFunctions 737 | 738 | # 739 | # Returns a float between 0 and 1 representing the location of the closest point 740 | # on LineString to the given Point, as a fraction of total 2d line length. 741 | # 742 | # You can use the returned location to extract a Point (ST_Line_Interpolate_Point) 743 | # or a substring (ST_Line_Substring). 744 | # 745 | # This is useful for approximating numbers of addresses. 746 | # 747 | # Returns float (0 to 1) ST_Line_Locate_Point(geometry a_linestring, geometry a_point); 748 | # 749 | def where_on_line line 750 | postgis_calculate(:line_locate_point, [line, self]).to_f 751 | end 752 | 753 | # 754 | # Linear distance in meters between two lon/lat points. 755 | # Uses a spherical earth and radius of 6370986 meters. 756 | # Faster than 'distance_spheroid', but less accurate. 757 | # 758 | # Only implemented for points. 759 | # 760 | # Returns Float ST_Distance_Sphere(geometry pointlonlatA, geometry pointlonlatB); 761 | # 762 | def distance_sphere_to(other) 763 | postgis_calculate(:distance_sphere, [self, other]).to_f 764 | end 765 | 766 | # 767 | # Calculates the distance on an ellipsoid. This is useful if the 768 | # coordinates of the geometry are in longitude/latitude and a length is 769 | # desired without reprojection. The ellipsoid is a separate database type and 770 | # can be constructed as follows: 771 | # 772 | # This is slower then 'distance_sphere_to', but more precise. 773 | # 774 | # SPHEROID[,,] 775 | # 776 | # Example: 777 | # SPHEROID["GRS_1980",6378137,298.257222101] 778 | # 779 | # Defaults to: 780 | # 781 | # SPHEROID["IERS_2003",6378136.6,298.25642] 782 | # 783 | # Returns ST_Distance_Spheroid(geometry geomA, geometry geomB, spheroid); 784 | # 785 | def distance_spheroid_to(other, spheroid = EARTH_SPHEROID) 786 | postgis_calculate(:distance_spheroid, [self, other], spheroid).to_f 787 | end 788 | 789 | # 790 | # The azimuth of the segment defined by the given Point geometries, 791 | # or NULL if the two points are coincident. Return value is in radians. 792 | # 793 | # The Azimuth is mathematical concept defined as the angle, in this case 794 | # measured in radian, between a reference plane and a point. 795 | # 796 | # Returns Float ST_Azimuth(geometry pointA, geometry pointB); 797 | # 798 | def azimuth other 799 | #TODO: return if not point/point 800 | postgis_calculate(:azimuth, [self, other]).to_f 801 | rescue 802 | ActiveRecord::StatementInvalid 803 | end 804 | 805 | # 806 | # True if the geometry is a point and is inside the circle. 807 | # 808 | # Returns Boolean ST_point_inside_circle(geometry, float, float, float) 809 | # 810 | def inside_circle?(x,y,r) 811 | postgis_calculate(:point_inside_circle, self, [x,y,r]) 812 | end 813 | 814 | end 815 | 816 | # 817 | # 818 | # 819 | # 820 | # Polygon 821 | # 822 | # 823 | # 824 | # 825 | module PolygonFunctions 826 | 827 | # 828 | # The area of the geometry if it is a polygon or multi-polygon. 829 | # Return the area measurement of an ST_Surface or ST_MultiSurface value. 830 | # Area is in the units of the spatial reference system. 831 | # 832 | # Accepts optional parameter, the srid to transform to. 833 | # 834 | # Returns Float ST_Area(geometry g1); 835 | # 836 | def area transform=nil 837 | postgis_calculate(:area, self, { :transform => transform }).to_f 838 | end 839 | 840 | # 841 | # Returns the 2D perimeter of the geometry if it is a ST_Surface, ST_MultiSurface 842 | # (Polygon, Multipolygon). 0 is returned for non-areal geometries. For linestrings 843 | # use 'length'. Measurements are in the units of the spatial reference system of 844 | # the geometry. 845 | # 846 | # Accepts optional parameter, the sridto transform to. 847 | # 848 | # Returns Float ST_Perimeter(geometry g1); 849 | # 850 | def perimeter transform=nil 851 | postgis_calculate(:perimeter, self, { :transform => transform }).to_f 852 | end 853 | 854 | # 855 | # Returns the 3-dimensional perimeter of the geometry, if it is a polygon or multi-polygon. 856 | # If the geometry is 2-dimensional, then the 2-dimensional perimeter is returned. 857 | # 858 | # Returns Float ST_Perimeter3D(geometry geomA); 859 | # 860 | def perimeter3d 861 | postgis_calculate(:perimeter3d, self).to_f 862 | end 863 | 864 | # 865 | # True if the LineString's start and end points are coincident. 866 | # 867 | # This method implements the OpenGIS Simple Features Implementation 868 | # Specification for SQL. 869 | # 870 | # SQL-MM defines the result of ST_IsClosed(NULL) to be 0, while PostGIS returns NULL. 871 | # 872 | # Returns boolean ST_IsClosed(geometry g); 873 | # 874 | def closed? 875 | postgis_calculate(:isclosed, self) 876 | end 877 | alias_method "is_closed?", "closed?" 878 | 879 | # 880 | # True if no point in Geometry B is outside Geometry A 881 | # 882 | # This function call will automatically include a bounding box comparison 883 | # that will make use of any indexes that are available on the geometries. 884 | # To avoid index use, use the function _ST_Covers. 885 | # 886 | # Do not call with a GEOMETRYCOLLECTION as an argument 887 | # Do not use this function with invalid geometries. You will get unexpected results. 888 | # 889 | # Performed by the GEOS module. 890 | # 891 | # Returns Boolean ST_Covers(geometry geomA, geometry geomB); 892 | # 893 | def covers? other 894 | postgis_calculate(:covers, [self, other]) 895 | end 896 | 897 | end 898 | 899 | # 900 | # 901 | # 902 | # 903 | # MultiPolygon 904 | # 905 | # 906 | # 907 | # 908 | module MultiPolygonFunctions 909 | end 910 | 911 | # 912 | # Generic Geometry 913 | # 914 | module GeometryFunctions 915 | end 916 | end 917 | end 918 | # NEW 919 | #ST_OrderingEquals — Returns true if the given geometries represent the same geometry and points are in the same directional order. 920 | #boolean ST_OrderingEquals(g 921 | # ST_PointOnSurface — Returns a POINT guaranteed to lie on the surface. 922 | #geometry ST_PointOnSurface(geometry g1);eometry A, geometry B); 923 | 924 | 925 | #x ST_SnapToGrid(geometry, geometry, sizeX, sizeY, sizeZ, sizeM) 926 | # ST_X , ST_Y, SE_M, SE_Z, SE_IsMeasured has_m? 927 | --------------------------------------------------------------------------------