activerecord-spatialite-adapter 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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