dr-postgis_adapter 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +21 -0
- data/Gemfile.lock +53 -0
- data/History.txt +6 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +380 -0
- data/Rakefile +81 -0
- data/lib/postgis_adapter/acts_as_geom.rb +49 -0
- data/lib/postgis_adapter/common_spatial_adapter.rb +184 -0
- data/lib/postgis_adapter/functions/bbox.rb +130 -0
- data/lib/postgis_adapter/functions/class.rb +68 -0
- data/lib/postgis_adapter/functions/common.rb +926 -0
- data/lib/postgis_adapter/functions.rb +176 -0
- data/lib/postgis_adapter/init.rb +458 -0
- data/lib/postgis_adapter/railtie.rb +7 -0
- data/lib/postgis_adapter.rb +16 -0
- data/postgis_adapter.gemspec +20 -0
- data/rails/init.rb +28 -0
- data/spec/db/models_postgis.rb +65 -0
- data/spec/db/schema_postgis.rb +98 -0
- data/spec/postgis_adapter/acts_as_geom_spec.rb +30 -0
- data/spec/postgis_adapter/common_spatial_adapter_spec.rb +254 -0
- data/spec/postgis_adapter/functions/bbox_spec.rb +45 -0
- data/spec/postgis_adapter/functions/class_spec.rb +79 -0
- data/spec/postgis_adapter/functions/common_spec.rb +428 -0
- data/spec/postgis_adapter/functions_spec.rb +60 -0
- data/spec/postgis_adapter_spec.rb +238 -0
- data/spec/spec_helper.rb +45 -0
- metadata +127 -0
@@ -0,0 +1,184 @@
|
|
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
|
@@ -0,0 +1,130 @@
|
|
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
|
@@ -0,0 +1,68 @@
|
|
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(#{default_geometry}, 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
|
+
# where "ST_DWithin(ST_GeomFromText(?, 4326), geometry, ?)", location_as_text, distance
|
54
|
+
where "ST_DWithin(#{default_geometry}, ST_GeomFromEWKT(?), ?)", other.as_hex_ewkb, margin
|
55
|
+
end
|
56
|
+
|
57
|
+
def all_within(other)
|
58
|
+
find(:all, :conditions => "ST_Within(geom, ST_GeomFromEWKT(E'#{other.as_hex_ewkb}'))")
|
59
|
+
end
|
60
|
+
|
61
|
+
def by_boundaries sort='asc'
|
62
|
+
find(:all, :order => "ST_Boundary(geom) #{sort}" )
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|