activerecord-spatialite-adapter 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # SpatiaLite adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ # :stopdoc:
38
+
39
+ module Arel
40
+ module Visitors
41
+
42
+ class SpatiaLite < SQLite
43
+
44
+ FUNC_MAP = {
45
+ 'st_wkttosql' => 'GeomFromText',
46
+ }
47
+
48
+ include ::RGeo::ActiveRecord::SpatialToSql
49
+
50
+ def st_func(standard_name_)
51
+ FUNC_MAP[standard_name_.downcase] || standard_name_
52
+ end
53
+
54
+ end
55
+
56
+ VISITORS['spatialite'] = ::Arel::Visitors::SpatiaLite
57
+
58
+ end
59
+ end
60
+
61
+ # :startdoc:
@@ -97,4 +97,5 @@ end
97
97
  ::RGeo::ActiveRecord::TaskHacker.modify('db:test:purge', 'test', 'spatialite') do |config_|
98
98
  dbfile_ = config_["database"] || config_["dbfile"]
99
99
  ::File.delete(dbfile_) if ::File.exist?(dbfile_)
100
+ create_database(config_)
100
101
  end
@@ -0,0 +1,234 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # SpatiaLite adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ # :stopdoc:
38
+
39
+ module ActiveRecord
40
+
41
+ module ConnectionAdapters
42
+
43
+ module SpatiaLiteAdapter
44
+
45
+
46
+ class MainAdapter < SQLite3Adapter
47
+
48
+
49
+ @@native_database_types = nil
50
+
51
+
52
+ def adapter_name
53
+ SpatiaLiteAdapter::ADAPTER_NAME
54
+ end
55
+
56
+
57
+ def spatial_column_constructor(name_)
58
+ ::RGeo::ActiveRecord::DEFAULT_SPATIAL_COLUMN_CONSTRUCTORS[name_]
59
+ end
60
+
61
+
62
+ def native_database_types
63
+ @@native_database_types ||= super.merge(:spatial => {:name => 'geometry'})
64
+ end
65
+
66
+
67
+ def spatialite_version
68
+ @spatialite_version ||= SQLiteAdapter::Version.new(select_value('SELECT spatialite_version()'))
69
+ end
70
+
71
+
72
+ def srs_database_columns
73
+ {:name_column => 'ref_sys_name', :proj4text_column => 'proj4text', :auth_name_column => 'auth_name', :auth_srid_column => 'auth_srid'}
74
+ end
75
+
76
+
77
+ def quote(value_, column_=nil)
78
+ if ::RGeo::Feature::Geometry.check_type(value_)
79
+ "GeomFromWKB(X'#{::RGeo::WKRep::WKBGenerator.new(:hex_format => true).generate(value_)}', #{value_.srid})"
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+
86
+ def columns(table_name_, name_=nil) #:nodoc:
87
+ spatial_info_ = spatial_column_info(table_name_)
88
+ table_structure(table_name_).map do |field_|
89
+ col_ = SpatialColumn.new(field_['name'], field_['dflt_value'], field_['type'], field_['notnull'].to_i == 0)
90
+ info_ = spatial_info_[field_['name']]
91
+ if info_
92
+ col_.set_srid(info_[:srid])
93
+ end
94
+ col_
95
+ end
96
+ end
97
+
98
+
99
+ def indexes(table_name_, name_=nil)
100
+ results_ = super.map do |index_|
101
+ ::RGeo::ActiveRecord::SpatialIndexDefinition.new(index_.table, index_.name, index_.unique, index_.columns, index_.lengths)
102
+ end
103
+ table_name_ = table_name_.to_s
104
+ names_ = select_values("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'idx_#{quote_string(table_name_)}_%' AND rootpage=0") || []
105
+ results_ + names_.map do |n_|
106
+ col_name_ = n_.sub("idx_#{table_name_}_", '')
107
+ ::RGeo::ActiveRecord::SpatialIndexDefinition.new(table_name_, n_, false, [col_name_], [], true)
108
+ end
109
+ end
110
+
111
+
112
+ def create_table(table_name_, options_={})
113
+ table_name_ = table_name_.to_s
114
+ table_definition_ = SpatialTableDefinition.new(self)
115
+ table_definition_.primary_key(options_[:primary_key] || ::ActiveRecord::Base.get_primary_key(table_name_.singularize)) unless options_[:id] == false
116
+ yield table_definition_ if block_given?
117
+ if options_[:force] && table_exists?(table_name_)
118
+ drop_table(table_name_, options_)
119
+ end
120
+
121
+ create_sql_ = "CREATE#{' TEMPORARY' if options_[:temporary]} TABLE "
122
+ create_sql_ << "#{quote_table_name(table_name_)} ("
123
+ create_sql_ << table_definition_.to_sql
124
+ create_sql_ << ") #{options_[:options]}"
125
+ execute create_sql_
126
+
127
+ table_definition_.spatial_columns.each do |col_|
128
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_)}', '#{quote_string(col_.name.to_s)}', #{col_.srid}, '#{quote_string(col_.spatial_type.gsub('_','').upcase)}', 'XY', #{col_.null ? 0 : 1})")
129
+ end
130
+ end
131
+
132
+
133
+ def drop_table(table_name_, options_={})
134
+ indexes(table_name_).each do |index_|
135
+ remove_index(table_name_, :spatial => true, :column => index_.columns[0]) if index_.spatial
136
+ end
137
+ execute("DELETE from geometry_columns where f_table_name='#{quote_string(table_name_.to_s)}'")
138
+ super
139
+ end
140
+
141
+
142
+ def add_column(table_name_, column_name_, type_, options_={})
143
+ if (info_ = spatial_column_constructor(type_.to_sym))
144
+ limit_ = options_[:limit]
145
+ options_.merge!(limit_) if limit_.is_a?(::Hash)
146
+ type_ = (options_[:type] || info_[:type] || type_).to_s.gsub('_', '').upcase
147
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_.to_s)}', '#{quote_string(column_name_.to_s)}', #{options_[:srid].to_i}, '#{quote_string(type_.to_s)}', 'XY', #{options_[:null] == false ? 0 : 1})")
148
+ else
149
+ super
150
+ end
151
+ end
152
+
153
+
154
+ def add_index(table_name_, column_name_, options_={})
155
+ if options_[:spatial]
156
+ column_name_ = column_name_.first if column_name_.kind_of?(::Array) && column_name_.size == 1
157
+ table_name_ = table_name_.to_s
158
+ column_name_ = column_name_.to_s
159
+ spatial_info_ = spatial_column_info(table_name_)
160
+ unless spatial_info_[column_name_]
161
+ raise ::ArgumentError, "Can't create spatial index because column '#{column_name_}' in table '#{table_name_}' is not a geometry column"
162
+ end
163
+ result_ = select_value("SELECT CreateSpatialIndex('#{quote_string(table_name_)}', '#{quote_string(column_name_)}')").to_i
164
+ if result_ == 0
165
+ raise ::ArgumentError, "Spatial index already exists on table '#{table_name_}', column '#{column_name_}'"
166
+ end
167
+ result_
168
+ else
169
+ super
170
+ end
171
+ end
172
+
173
+
174
+ def remove_index(table_name_, options_={})
175
+ if options_[:spatial]
176
+ table_name_ = table_name_.to_s
177
+ column_ = options_[:column]
178
+ if column_
179
+ column_ = column_[0] if column_.kind_of?(::Array)
180
+ column_ = column_.to_s
181
+ else
182
+ index_name_ = options_[:name]
183
+ unless index_name_
184
+ raise ::ArgumentError, "You need to specify a column or index name to remove a spatial index."
185
+ end
186
+ if index_name_ =~ /^idx_#{table_name_}_(\w+)$/
187
+ column_ = $1
188
+ else
189
+ raise ::ArgumentError, "Unknown spatial index name: #{index_name_.inspect}."
190
+ end
191
+ end
192
+ spatial_info_ = spatial_column_info(table_name_)
193
+ unless spatial_info_[column_]
194
+ raise ::ArgumentError, "Can't remove spatial index because column '#{column_}' in table '#{table_name_}' is not a geometry column"
195
+ end
196
+ index_name_ = "idx_#{table_name_}_#{column_}"
197
+ has_index_ = select_value("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='#{quote_string(index_name_)}'").to_i > 0
198
+ unless has_index_
199
+ raise ::ArgumentError, "Spatial index not present on table '#{table_name_}', column '#{column_}'"
200
+ end
201
+ execute("SELECT DisableSpatialIndex('#{quote_string(table_name_)}', '#{quote_string(column_)}')")
202
+ execute("DROP TABLE #{quote_table_name(index_name_)}")
203
+ else
204
+ super
205
+ end
206
+ end
207
+
208
+
209
+ def spatial_column_info(table_name_)
210
+ info_ = execute("SELECT * FROM geometry_columns WHERE f_table_name='#{quote_string(table_name_.to_s)}'")
211
+ result_ = {}
212
+ info_.each do |row_|
213
+ result_[row_['f_geometry_column']] = {
214
+ :name => row_['f_geometry_column'],
215
+ :type => row_['type'],
216
+ :dimension => row_['coord_dimension'],
217
+ :srid => row_['srid'],
218
+ :has_index => row_['spatial_index_enabled'],
219
+ }
220
+ end
221
+ result_
222
+ end
223
+
224
+
225
+ end
226
+
227
+
228
+ end
229
+
230
+ end
231
+
232
+ end
233
+
234
+ # :startdoc:
@@ -0,0 +1,163 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # SpatiaLite adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ module ActiveRecord
38
+
39
+ module ConnectionAdapters
40
+
41
+ module SpatiaLiteAdapter
42
+
43
+
44
+ # A utility class that parses the native (internal) SpatiaLite
45
+ # format. This is used to read and return an attribute value as an
46
+ # RGeo object.
47
+
48
+ class NativeFormatParser
49
+
50
+
51
+ # Create a parser that generates features using the given factory.
52
+
53
+ def initialize(factory_)
54
+ @factory = factory_
55
+ end
56
+
57
+
58
+ # Parse the given binary data and return an object.
59
+ # Raises ::RGeo::Error::ParseError on failure.
60
+
61
+ def parse(data_)
62
+ @little_endian = data_[1,1] == "\x01"
63
+ srid_ = data_[2,4].unpack(@little_endian ? 'V' : 'N').first
64
+ begin
65
+ _start_scanner(data_)
66
+ obj_ = _parse_object(false)
67
+ _get_byte(0xfe)
68
+ ensure
69
+ _clean_scanner
70
+ end
71
+ obj_
72
+ end
73
+
74
+
75
+ def _parse_object(contained_) # :nodoc:
76
+ _get_byte(contained_ ? 0x69 : 0x7c)
77
+ type_code_ = _get_integer
78
+ case type_code_
79
+ when 1
80
+ coords_ = _get_doubles(2)
81
+ @factory.point(*coords_)
82
+ when 2
83
+ _parse_line_string
84
+ when 3
85
+ interior_rings_ = (1.._get_integer).map{ _parse_line_string }
86
+ exterior_ring_ = interior_rings_.shift || @factory.linear_ring([])
87
+ @factory.polygon(exterior_ring_, interior_rings_)
88
+ when 4
89
+ @factory.multi_point((1.._get_integer).map{ _parse_object(1) })
90
+ when 5
91
+ @factory.multi_line_string((1.._get_integer).map{ _parse_object(2) })
92
+ when 6
93
+ @factory.multi_polygon((1.._get_integer).map{ _parse_object(3) })
94
+ when 7
95
+ @factory.collection((1.._get_integer).map{ _parse_object(true) })
96
+ else
97
+ raise ::RGeo::Error::ParseError, "Unknown type value: #{type_code_}."
98
+ end
99
+ end
100
+
101
+
102
+ def _parse_line_string # :nodoc:
103
+ count_ = _get_integer
104
+ coords_ = _get_doubles(2 * count_)
105
+ @factory.line_string((0...count_).map{ |i_| @factory.point(*coords_[2*i_,2]) })
106
+ end
107
+
108
+
109
+ def _start_scanner(data_) # :nodoc:
110
+ @_data = data_
111
+ @_len = data_.length
112
+ @_pos = 38
113
+ end
114
+
115
+
116
+ def _clean_scanner # :nodoc:
117
+ @_data = nil
118
+ end
119
+
120
+
121
+ def _get_byte(expect_=nil) # :nodoc:
122
+ if @_pos + 1 > @_len
123
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill 1 byte"
124
+ end
125
+ str_ = @_data[@_pos, 1]
126
+ @_pos += 1
127
+ val_ = str_.unpack("C").first
128
+ if expect_ && expect_ != val_
129
+ raise ::RGeo::Error::ParseError, "Expected byte 0x#{expect_.to_s(16)} but got 0x#{val_.to_s(16)}"
130
+ end
131
+ val_
132
+ end
133
+
134
+
135
+ def _get_integer # :nodoc:
136
+ if @_pos + 4 > @_len
137
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill 1 integer"
138
+ end
139
+ str_ = @_data[@_pos, 4]
140
+ @_pos += 4
141
+ str_.unpack("#{@little_endian ? 'V' : 'N'}").first
142
+ end
143
+
144
+
145
+ def _get_doubles(count_) # :nodoc:
146
+ len_ = 8 * count_
147
+ if @_pos + len_ > @_len
148
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill #{count_} doubles"
149
+ end
150
+ str_ = @_data[@_pos, len_]
151
+ @_pos += len_
152
+ str_.unpack("#{@little_endian ? 'E' : 'G'}*")
153
+ end
154
+
155
+
156
+ end
157
+
158
+
159
+ end
160
+
161
+ end
162
+
163
+ end