rs_spatial_adapter 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006 Guilhem Vellut <guilhem.vellut+georuby@gmail.com>
2
+ Copyright (c) 2010 Pete Deffendol <pete@fragility.us>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,197 @@
1
+ = Spatial Adapter for ActiveRecord
2
+
3
+ This is the Spatial Adapter for ActiveRecord. It enhances ActiveRecord to
4
+ handle spatial datatypes in the following databases:
5
+
6
+ - PostgreSQL (using PostGIS)
7
+ - MySQL (using Spatial Extensions)
8
+
9
+ == Dependencies
10
+
11
+ The following gems are required:
12
+
13
+ - GeoRuby
14
+ - ActiveRecord (version 2.2.2 and up)
15
+
16
+ For PostgreSQL:
17
+
18
+ - PostGIS version 1.4.0 or higher should be installed in your database
19
+
20
+ == Installation
21
+
22
+ Choose ONE of the following installation methods. You shouldn't have to do both.
23
+
24
+ === From RubyGems
25
+
26
+ This is the preferred method of installation, and will pull in the required
27
+ dependencies as well.
28
+
29
+ gem install spatial_adapter
30
+
31
+ In a Rails 2.x app, you can add a gem dependency in environment.rb:
32
+
33
+ config.gem 'spatial_adapter'
34
+
35
+ In a Rails 3 app, add a gem dependency to Gemfile:
36
+
37
+ gem 'spatial_adapter'
38
+
39
+ === As a Rails Plugin
40
+
41
+ In your Rails project, run the following:
42
+
43
+ script/plugin install git://github.com/fragility/spatial_adapter.git
44
+
45
+ You need to have Git installed first.
46
+
47
+ == Configuration
48
+
49
+ Choose the database type for which you would like to use spatial_adapter, and
50
+ load each with
51
+
52
+ require 'spatial_adapter/[database]'
53
+
54
+ where [database] should be replaced with one of the following:
55
+
56
+ - postgresql
57
+ - mysql
58
+
59
+ For example to use the PostgreSQL spatial adapter:
60
+
61
+ require 'spatial_adapter/postgresql'
62
+
63
+ In a Rails app, spatial_adapter will automatically load the adapter for the database
64
+ specified in your database.yml configuration.
65
+
66
+ == Operations
67
+
68
+ Geometric columns in your ActiveRecord models now appear just like any other
69
+ column of other basic data types. They can also be dumped in ruby schema mode
70
+ and loaded in migrations the same way as columns of basic types.
71
+
72
+ === Migrations
73
+
74
+ Here is an example of code for the creation of a table with a geometric column
75
+ in PostGIS, along with the addition of a spatial index on the column:
76
+
77
+ ActiveRecord::Schema.define do
78
+ create_table :table_points, :force => true do |t|
79
+ t.string :data
80
+ t.point :geom, :null => false, :srid => 123, :with_z => true
81
+ end
82
+
83
+ add_index :table_points, :geom, :spatial => true
84
+ end
85
+
86
+ Here is a related statement valid for MySql version <= 5.0.16:
87
+
88
+ ActiveRecord::Schema.define do
89
+ create_table "table_points", ;options=>"ENGINE=MyISAM", :force => true do |t|
90
+ t.string :data
91
+ t.point :geom, :null => false
92
+ end
93
+
94
+ add_index :table_points, :geom, :spatial => true
95
+ end
96
+
97
+ === Differences Between Databases
98
+
99
+ - On all versions of MySQL, the :srid, :with_z, and :with_m options are ignored, since
100
+ they are not supported.
101
+
102
+ - On MySQL versions <= 5.0.16, you have to add <tt>:options =>
103
+ "ENGINE=MyISAM"</tt> to the create_table statement, since only MyISAM tables
104
+ can have spatial columns. In addition, only MyISAM tables may have spatial
105
+ indexes.
106
+
107
+ === Models
108
+
109
+ Create your ActiveRecord models normally. Spatial Adapter will automatically
110
+ handle spatial columns, converting them to the appropriate GeoRuby type.
111
+
112
+ class TablePoint < ActiveRecord::Base
113
+ end
114
+
115
+ === Access
116
+
117
+ Here is an example of row creation and access, using the model and the table
118
+ defined above:
119
+
120
+ pt = TablePoint.new(
121
+ :data => "Hello!",
122
+ :geom => Point.from_x_y_z(-1.6, 2.8, -3.4, 123))
123
+ pt.save
124
+ pt = TablePoint.find_first
125
+ puts pt.geom.x #access the geom column like any other
126
+
127
+ === Fixtures
128
+
129
+ If you use fixtures for your unit tests, at some point, you will want to input
130
+ a geometry. You could transform your geometries to a form suitable for YAML
131
+ yourself every time but Spatial Adapter provides a method to do it for you:
132
+ +to_fixture_format+. You would use it like this, if the geometric column is a
133
+ point:
134
+
135
+ fixture:
136
+ id: 1
137
+ data: HELLO
138
+ geom: <%= Point.from_x_y(123.5,321.9).to_fixture_format %>
139
+
140
+ === Finder Enhancements
141
+
142
+ Enhancements to find_by_* and friends has been removed from this version of
143
+ Spatial Adapter until a cleaner implementation can be made. (The previous
144
+ implementation made adapter-specific modifications to ActiveRecord::Base,
145
+ which prevented multiple adapters from being loaded at once.)
146
+
147
+ === Geometric data types
148
+
149
+ Ruby geometric datatypes are currently made available only through the GeoRuby
150
+ library (http://georuby.rubyforge.org/): This is where the
151
+ <tt>Point.from_x_y</tt> in the example above comes from.
152
+
153
+ == Warning
154
+
155
+ - Since ActiveRecord seems to keep only the string values directly returned
156
+ from the database, it translates from these to the correct types everytime
157
+ an attribute is read, which is probably ok for simple types, but might be
158
+ less than efficient for geometries, since the EWKB string has to be parsed
159
+ everytime. Also it means you cannot modify the geometry object returned from
160
+ an attribute directly:
161
+
162
+ place = Place.find_first
163
+ place.the_geom.y=123456.7 # this doesn't work
164
+
165
+ Since the translation to a geometry is performed every time the_geom is read,
166
+ the change to y will not be saved! You would have to do something like this:
167
+
168
+ place = Place.find_first
169
+ the_geom = place.the_geom
170
+ the_geom.y=123456.7
171
+ place.the_geom = the_geom
172
+
173
+ == License
174
+
175
+ The Spatial Adapter for ActiveRecord is released under the MIT license.
176
+
177
+ == Latest Changes
178
+
179
+ Spatial Adapter has been refactored and is now available as a Ruby gem. The
180
+ dependency on Rails has been removed. Unfortunately, the current version is
181
+ without some of the previous functionality, until a cleaner implementation is
182
+ made.
183
+
184
+ The previous release is available on the "legacy" branch.
185
+
186
+ === Removed Features in 0.2.0
187
+
188
+ - Compatibility with ActiveRecord/Rails older than version 2.2.2
189
+ - enhancements to find_by_* for spatial columns
190
+ - to_fixture_format extension to the GeoRuby types
191
+
192
+ These will hopefully be added back in the near future.
193
+
194
+ == Support
195
+
196
+ Any questions, enhancement proposals, bug notifications or corrections can be
197
+ made via the project page at http://github.com/fragility/spatial_adapter
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.2.0
@@ -0,0 +1,41 @@
1
+ # This file should typically not be directly require'd into your project. You
2
+ # should require the database-specific adapter you desire, e.g.
3
+ #
4
+ # require 'spatial_adapter/postgresql'
5
+ #
6
+ # Why is this file here?
7
+ #
8
+ # Mostly to keep Rails happy when using config.gem to specify dependencies.
9
+ # The Rails init code (rails/init.rb) will then load the adapter that matches
10
+ # your database.yml configuration.
11
+
12
+ require 'geo_ruby'
13
+ require 'active_record'
14
+
15
+ include GeoRuby::SimpleFeatures
16
+
17
+ module SpatialAdapter
18
+ # Translation of geometric data types
19
+ def geometry_data_types
20
+ {
21
+ :point => { :name => "POINT" },
22
+ :line_string => { :name => "LINESTRING" },
23
+ :polygon => { :name => "POLYGON" },
24
+ :geometry_collection => { :name => "GEOMETRYCOLLECTION" },
25
+ :multi_point => { :name => "MULTIPOINT" },
26
+ :multi_line_string => { :name => "MULTILINESTRING" },
27
+ :multi_polygon => { :name => "MULTIPOLYGON" },
28
+ :geometry => { :name => "GEOMETRY"}
29
+ }
30
+ end
31
+
32
+ class NotCompatibleError < ::StandardError
33
+ end
34
+ end
35
+
36
+ require 'spatial_adapter/common/raw_geom_info'
37
+ require 'spatial_adapter/common/spatial_column'
38
+ require 'spatial_adapter/common/schema_definitions'
39
+ require 'spatial_adapter/common/schema_dumper'
40
+ require 'spatial_adapter/common/table_definition'
41
+ require 'spatial_adapter/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,23 @@
1
+ module SpatialAdapter
2
+ class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc:
3
+ def convert!
4
+ self.type = "geometry" if self.type.nil? #if geometry the geometrytype constraint is not present : need to set the type here then
5
+
6
+ if dimension == 4
7
+ self.with_m = true
8
+ self.with_z = true
9
+ elsif dimension == 3
10
+ if with_m
11
+ self.with_z = false
12
+ self.with_m = true
13
+ else
14
+ self.with_z = true
15
+ self.with_m = false
16
+ end
17
+ else
18
+ self.with_z = false
19
+ self.with_m = false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+
3
+ ActiveRecord::ConnectionAdapters::IndexDefinition.class_eval do
4
+ attr_accessor :spatial
5
+
6
+ alias_method :initialize_without_spatial, :initialize
7
+ def initialize(table, name, unique, columns, spatial = false)
8
+ initialize_without_spatial(table, name, unique, columns)
9
+ @spatial = spatial
10
+ end
11
+ end
@@ -0,0 +1,136 @@
1
+ ActiveRecord::SchemaDumper.ignore_tables << "spatial_ref_sys" << "geometry_columns"
2
+
3
+ ActiveRecord::SchemaDumper.class_eval do
4
+ # These are the valid options for a column specification (spatial options added)
5
+ VALID_COLUMN_SPEC_KEYS = [:name, :limit, :precision, :scale, :default, :null, :srid, :with_z, :with_m, :geographic]
6
+
7
+ def table(table, stream)
8
+ columns = @connection.columns(table)
9
+ begin
10
+ tbl = StringIO.new
11
+
12
+ # first dump primary key column
13
+ if @connection.respond_to?(:pk_and_sequence_for)
14
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
15
+ elsif @connection.respond_to?(:primary_key)
16
+ pk = @connection.primary_key(table)
17
+ end
18
+
19
+ tbl.print " create_table #{table.inspect}"
20
+ if columns.detect { |c| c.name == pk }
21
+ if pk != 'id'
22
+ tbl.print %Q(, :primary_key => "#{pk}")
23
+ end
24
+ else
25
+ tbl.print ", :id => false"
26
+ end
27
+
28
+ # Added by Spatial Adapter to ensure correct MySQL table engine
29
+ if @connection.respond_to?(:options_for)
30
+ res = @connection.options_for(table)
31
+ tbl.print ", :options=>'#{res}'" if res
32
+ end
33
+
34
+ tbl.print ", :force => true"
35
+ tbl.puts " do |t|"
36
+
37
+ # then dump all non-primary key columns
38
+ column_specs = columns.map do |column|
39
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
40
+ next if column.name == pk
41
+ spec = column_spec(column)
42
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
43
+ spec
44
+ end.compact
45
+
46
+ # find all migration keys used in this table
47
+ keys = VALID_COLUMN_SPEC_KEYS & column_specs.map(&:keys).flatten
48
+
49
+ # figure out the lengths for each column based on above keys
50
+ lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
51
+
52
+ # the string we're going to sprintf our values against, with standardized column widths
53
+ format_string = lengths.map{ |len| "%-#{len}s" }
54
+
55
+ # find the max length for the 'type' column, which is special
56
+ type_length = column_specs.map{ |column| column[:type].length }.max
57
+
58
+ # add column type definition to our format string
59
+ format_string.unshift " t.%-#{type_length}s "
60
+
61
+ format_string *= ''
62
+
63
+ column_specs.each do |colspec|
64
+ values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
65
+ values.unshift colspec[:type]
66
+ tbl.print((format_string % values).gsub(/,\s*$/, ''))
67
+ tbl.puts
68
+ end
69
+
70
+ tbl.puts " end"
71
+ tbl.puts
72
+
73
+ indexes(table, tbl)
74
+
75
+ tbl.rewind
76
+ stream.print tbl.read
77
+ rescue => e
78
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
79
+ stream.puts "# #{e.message}"
80
+ stream.puts
81
+ end
82
+
83
+ stream
84
+ end
85
+
86
+
87
+ def indexes(table, stream)
88
+ if (indexes = @connection.indexes(table)).any?
89
+ add_index_statements = indexes.map do |index|
90
+ statment_parts = [ ('add_index ' + index.table.inspect) ]
91
+ statment_parts << index.columns.inspect
92
+ statment_parts << (':name => ' + index.name.inspect)
93
+ statment_parts << ':unique => true' if index.unique
94
+ # Add spatial option (this is the only change from the original method)
95
+ statment_parts << ':spatial => true' if index.spatial
96
+
97
+ ' ' + statment_parts.join(', ')
98
+ end
99
+
100
+ stream.puts add_index_statements.sort.join("\n")
101
+ stream.puts
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ # Build specification for a table column
108
+ def column_spec(column)
109
+ spec = {}
110
+ spec[:name] = column.name.inspect
111
+
112
+ # AR has an optimisation which handles zero-scale decimals as integers. This
113
+ # code ensures that the dumper still dumps the column as a decimal.
114
+ spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) }
115
+ 'decimal'
116
+ else
117
+ column.type.to_s
118
+ end
119
+ spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal'
120
+ spec[:precision] = column.precision.inspect if !column.precision.nil?
121
+ spec[:scale] = column.scale.inspect if !column.scale.nil?
122
+ spec[:null] = 'false' if !column.null
123
+ spec[:default] = default_string(column.default) if column.has_default?
124
+
125
+ # Additions for spatial columns
126
+ if column.is_a?(SpatialColumn)
127
+ # Override with specific geometry type
128
+ spec[:type] = column.geometry_type.to_s
129
+ spec[:srid] = column.srid.inspect if column.srid != -1
130
+ spec[:with_z] = 'true' if column.with_z
131
+ spec[:with_m] = 'true' if column.with_m
132
+ spec[:geographic] = 'true' if column.geographic?
133
+ end
134
+ spec
135
+ end
136
+ end
@@ -0,0 +1,70 @@
1
+ module SpatialAdapter
2
+ module SpatialColumn
3
+ attr_reader :geometry_type, :srid, :with_z, :with_m
4
+
5
+ def initialize(name, default, sql_type = nil, null = true, srid=-1, with_z=false, with_m=false)
6
+ super(name, default, sql_type, null)
7
+ @geometry_type = geometry_simplified_type(@sql_type)
8
+ @srid = srid
9
+ @with_z = with_z
10
+ @with_m = with_m
11
+ end
12
+
13
+ def spatial?
14
+ !@geometry_type.nil?
15
+ end
16
+
17
+ def geographic?
18
+ false
19
+ end
20
+
21
+ # Redefines type_cast to add support for geometries
22
+ # alias_method :type_cast_without_spatial, :type_cast
23
+ def type_cast(value)
24
+ return nil if value.nil?
25
+ spatial? ? self.class.string_to_geometry(value) : super
26
+ end
27
+
28
+ #Redefines type_cast_code to add support for geometries.
29
+ #
30
+ #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.
31
+ # alias_method :type_cast_code_without_spatial, :type_cast_code
32
+ def type_cast_code(var_name)
33
+ spatial? ? "#{self.class.name}.string_to_geometry(#{var_name})" : super
34
+ end
35
+
36
+
37
+ #Redefines klass to add support for geometries
38
+ # alias_method :klass_without_spatial, :klass
39
+ def klass
40
+ spatial? ? GeoRuby::SimpleFeatures::Geometry : super
41
+ end
42
+
43
+ private
44
+
45
+ # Maps additional data types to base Rails/Arel types
46
+ #
47
+ # For Rails 3, only the types defined by Arel can be used. We'll
48
+ # use :string since the database returns the columns as hex strings.
49
+ def simplified_type(field_type)
50
+ case field_type
51
+ when /geography|geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i then :string
52
+ else super
53
+ end
54
+ end
55
+
56
+ # less simlpified geometric type to be use in migrations
57
+ def geometry_simplified_type(sql_type)
58
+ case sql_type
59
+ when /^point$/i then :point
60
+ when /^linestring$/i then :line_string
61
+ when /^polygon$/i then :polygon
62
+ when /^geometry$/i then :geometry
63
+ when /multipoint/i then :multi_point
64
+ when /multilinestring/i then :multi_line_string
65
+ when /multipolygon/i then :multi_polygon
66
+ when /geometrycollection/i then :geometry_collection
67
+ end
68
+ end
69
+ end
70
+ end