achirkunov-spatial_adapter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,193 @@
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 app, you can add a gem dependency in environment.rb:
32
+
33
+ config.gem "spatial_adapter"
34
+
35
+ === As a Rails Plugin
36
+
37
+ In your Rails project, run the following:
38
+
39
+ script/plugin install git://github.com/fragility/spatial_adapter.git
40
+
41
+ You need to have Git installed first.
42
+
43
+ == Configuration
44
+
45
+ Choose the database type for which you would like to use spatial_adapter, and
46
+ load each with
47
+
48
+ require 'spatial_adapter/[database]'
49
+
50
+ where [database] should be replaced with one of the following:
51
+
52
+ - postgresql
53
+ - mysql
54
+
55
+ For example to use the PostgreSQL spatial adapter:
56
+
57
+ require 'spatial_adapter/postgresql'
58
+
59
+ In a Rails app, spatial_adapter will automatically load the adapter for the database
60
+ specified in your database.yml configuration.
61
+
62
+ == Operations
63
+
64
+ Geometric columns in your ActiveRecord models now appear just like any other
65
+ column of other basic data types. They can also be dumped in ruby schema mode
66
+ and loaded in migrations the same way as columns of basic types.
67
+
68
+ === Migrations
69
+
70
+ Here is an example of code for the creation of a table with a geometric column
71
+ in PostGIS, along with the addition of a spatial index on the column:
72
+
73
+ ActiveRecord::Schema.define do
74
+ create_table :table_points, :force => true do |t|
75
+ t.string :data
76
+ t.point :geom, :null => false, :srid => 123, :with_z => true
77
+ end
78
+
79
+ add_index :table_points, :geom, :spatial => true
80
+ end
81
+
82
+ Here is a related statement valid for MySql version <= 5.0.16:
83
+
84
+ ActiveRecord::Schema.define do
85
+ create_table "table_points", ;options=>"ENGINE=MyISAM", :force => true do |t|
86
+ t.string :data
87
+ t.point :geom, :null => false
88
+ end
89
+
90
+ add_index :table_points, :geom, :spatial => true
91
+ end
92
+
93
+ === Differences Between Databases
94
+
95
+ - On all versions of MySQL, the :srid, :with_z, and :with_m options are ignored, since
96
+ they are not supported.
97
+
98
+ - On MySQL versions <= 5.0.16, you have to add <tt>:options =>
99
+ "ENGINE=MyISAM"</tt> to the create_table statement, since only MyISAM tables
100
+ can have spatial columns. In addition, only MyISAM tables may have spatial
101
+ indexes.
102
+
103
+ === Models
104
+
105
+ Create your ActiveRecord models normally. Spatial Adapter will automatically
106
+ handle spatial columns, converting them to the appropriate GeoRuby type.
107
+
108
+ class TablePoint < ActiveRecord::Base
109
+ end
110
+
111
+ === Access
112
+
113
+ Here is an example of row creation and access, using the model and the table
114
+ defined above:
115
+
116
+ pt = TablePoint.new(
117
+ :data => "Hello!",
118
+ :geom => Point.from_x_y_z(-1.6, 2.8, -3.4, 123))
119
+ pt.save
120
+ pt = TablePoint.find_first
121
+ puts pt.geom.x #access the geom column like any other
122
+
123
+ === Fixtures
124
+
125
+ If you use fixtures for your unit tests, at some point, you will want to input
126
+ a geometry. You could transform your geometries to a form suitable for YAML
127
+ yourself every time but Spatial Adapter provides a method to do it for you:
128
+ +to_fixture_format+. You would use it like this, if the geometric column is a
129
+ point:
130
+
131
+ fixture:
132
+ id: 1
133
+ data: HELLO
134
+ geom: <%= Point.from_x_y(123.5,321.9).to_fixture_format %>
135
+
136
+ === Finder Enhancements
137
+
138
+ Enhancements to find_by_* and friends has been removed from this version of
139
+ Spatial Adapter until a cleaner implementation can be made. (The previous
140
+ implementation made adapter-specific modifications to ActiveRecord::Base,
141
+ which prevented multiple adapters from being loaded at once.)
142
+
143
+ === Geometric data types
144
+
145
+ Ruby geometric datatypes are currently made available only through the GeoRuby
146
+ library (http://georuby.rubyforge.org/): This is where the
147
+ <tt>Point.from_x_y</tt> in the example above comes from.
148
+
149
+ == Warning
150
+
151
+ - Since ActiveRecord seems to keep only the string values directly returned
152
+ from the database, it translates from these to the correct types everytime
153
+ an attribute is read, which is probably ok for simple types, but might be
154
+ less than efficient for geometries, since the EWKB string has to be parsed
155
+ everytime. Also it means you cannot modify the geometry object returned from
156
+ an attribute directly:
157
+
158
+ place = Place.find_first
159
+ place.the_geom.y=123456.7 # this doesn't work
160
+
161
+ Since the translation to a geometry is performed every time the_geom is read,
162
+ the change to y will not be saved! You would have to do something like this:
163
+
164
+ place = Place.find_first
165
+ the_geom = place.the_geom
166
+ the_geom.y=123456.7
167
+ place.the_geom = the_geom
168
+
169
+ == License
170
+
171
+ The Spatial Adapter for ActiveRecord is released under the MIT license.
172
+
173
+ == Latest Changes
174
+
175
+ Spatial Adapter has been refactored and is now available as a Ruby gem. The
176
+ dependency on Rails has been removed. Unfortunately, the current version is
177
+ without some of the previous functionality, until a cleaner implementation is
178
+ made.
179
+
180
+ The previous release is available on the "legacy" branch.
181
+
182
+ === Removed Features in 0.2.0
183
+
184
+ - Compatibility with ActiveRecord/Rails older than version 2.2.2
185
+ - enhancements to find_by_* for spatial columns
186
+ - to_fixture_format extension to the GeoRuby types
187
+
188
+ These will hopefully be added back in the near future.
189
+
190
+ == Support
191
+
192
+ Any questions, enhancement proposals, bug notifications or corrections can be
193
+ made via the project page at http://github.com/fragility/spatial_adapter
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,37 @@
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
+ end
32
+
33
+ require 'spatial_adapter/common/raw_geom_info'
34
+ require 'spatial_adapter/common/spatial_column'
35
+ require 'spatial_adapter/common/schema_definitions'
36
+ require 'spatial_adapter/common/schema_dumper'
37
+ require 'spatial_adapter/common/table_definition'
@@ -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,75 @@
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 geographic?
14
+ false
15
+ end
16
+
17
+ # Redefines type_cast to add support for geometries
18
+ # alias_method :type_cast_without_spatial, :type_cast
19
+ def type_cast(value)
20
+ return nil if value.nil?
21
+ if @geometry_type.nil?
22
+ super
23
+ else
24
+ self.class.string_to_geometry(value)
25
+ end
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
+ if @geometry_type.nil?
34
+ super
35
+ else
36
+ "#{self.class.name}.string_to_geometry(#{var_name})"
37
+ end
38
+ end
39
+
40
+
41
+ #Redefines klass to add support for geometries
42
+ # alias_method :klass_without_spatial, :klass
43
+ def klass
44
+ if @geometry_type.nil?
45
+ super
46
+ else
47
+ GeoRuby::SimpleFeatures::Geometry
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ #Redefines the simplified_type method to spatial columns
54
+ def simplified_type(field_type)
55
+ case field_type
56
+ when /geography|geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i then :string
57
+ else super
58
+ end
59
+ end
60
+
61
+ # less simlpified geometric type to be use in migrations
62
+ def geometry_simplified_type(sql_type)
63
+ case sql_type
64
+ when /^point$/i then :point
65
+ when /^linestring$/i then :line_string
66
+ when /^polygon$/i then :polygon
67
+ when /^geometry$/i then :geometry
68
+ when /multipoint/i then :multi_point
69
+ when /multilinestring/i then :multi_line_string
70
+ when /multipolygon/i then :multi_polygon
71
+ when /geometrycollection/i then :geometry_collection
72
+ end
73
+ end
74
+ end
75
+ end