rs_spatial_adapter 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ include SpatialAdapter
2
+
3
+ ActiveRecord::ConnectionAdapters::TableDefinition.class_eval do
4
+ SpatialAdapter.geometry_data_types.keys.each do |column_type|
5
+ class_eval <<-EOV
6
+ def #{column_type}(*args)
7
+ options = args.extract_options!
8
+ column_names = args
9
+
10
+ column_names.each { |name| column(name, '#{column_type}', options) }
11
+ end
12
+ EOV
13
+ end
14
+ end
@@ -0,0 +1,98 @@
1
+ require 'spatial_adapter'
2
+ require 'active_record/connection_adapters/mysql_adapter'
3
+
4
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
5
+ include SpatialAdapter
6
+
7
+ def supports_geographic?
8
+ false
9
+ end
10
+
11
+ alias :original_native_database_types :native_database_types
12
+ def native_database_types
13
+ original_native_database_types.merge!(geometry_data_types)
14
+ end
15
+
16
+ alias :original_quote :quote
17
+ #Redefines the quote method to add behaviour for when a Geometry is encountered ; used when binding variables in find_by methods
18
+ def quote(value, column = nil)
19
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
20
+ "GeomFromWKB(0x#{value.as_hex_wkb},#{value.srid})"
21
+ else
22
+ original_quote(value,column)
23
+ end
24
+ end
25
+
26
+ #Redefinition of columns to add the information that a column is geometric
27
+ def columns(table_name, name = nil)#:nodoc:
28
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
29
+ columns = []
30
+ result = execute(sql, name)
31
+ result.each do |field|
32
+ klass = field[1] =~ /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i ? ActiveRecord::ConnectionAdapters::SpatialMysqlColumn : ActiveRecord::ConnectionAdapters::MysqlColumn
33
+ columns << klass.new(field[0], field[4], field[1], field[2] == "YES")
34
+ end
35
+ result.free
36
+ columns
37
+ end
38
+
39
+
40
+ #operations relative to migrations
41
+
42
+ #Redefines add_index to support the case where the index is spatial
43
+ #If the :spatial key in the options table is true, then the sql string for a spatial index is created
44
+ def add_index(table_name,column_name,options = {})
45
+ index_name = options[:name] || index_name(table_name,:column => Array(column_name))
46
+
47
+ if options[:spatial]
48
+ execute "CREATE SPATIAL INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ #Check the nature of the index : If it is SPATIAL, it is indicated in the IndexDefinition object (redefined to add the spatial flag in spatial_adapter_common.rb)
55
+ def indexes(table_name, name = nil)#:nodoc:
56
+ indexes = []
57
+ current_index = nil
58
+ execute("SHOW KEYS FROM #{table_name}", name).each do |row|
59
+ if current_index != row[2]
60
+ next if row[2] == "PRIMARY" # skip the primary key
61
+ current_index = row[2]
62
+ indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.new(row[0], row[2], row[1] == "0", [], row[10] == "SPATIAL")
63
+ end
64
+ indexes.last.columns << row[4]
65
+ end
66
+ indexes
67
+ end
68
+
69
+ #Get the table creation options : Only the engine for now. The text encoding could also be parsed and returned here.
70
+ def options_for(table)
71
+ result = execute("show table status like '#{table}'")
72
+ engine = result.fetch_row[1]
73
+ if engine !~ /inno/i #inno is default so do nothing for it in order not to clutter the migration
74
+ "ENGINE=#{engine}"
75
+ else
76
+ nil
77
+ end
78
+ end
79
+ end
80
+
81
+
82
+ module ActiveRecord
83
+ module ConnectionAdapters
84
+ class SpatialMysqlColumn < MysqlColumn
85
+ include SpatialAdapter::SpatialColumn
86
+
87
+ #MySql-specific geometry string parsing. By default, MySql returns geometries in strict wkb format with "0" characters in the first 4 positions.
88
+ def self.string_to_geometry(string)
89
+ return string unless string.is_a?(String)
90
+ begin
91
+ GeoRuby::SimpleFeatures::Geometry.from_ewkb(string[4..-1])
92
+ rescue Exception => exception
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,97 @@
1
+ require 'spatial_adapter'
2
+ require 'active_record/connection_adapters/mysql2_adapter'
3
+
4
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
5
+ include SpatialAdapter
6
+
7
+ def supports_geographic?
8
+ false
9
+ end
10
+
11
+ alias :original_native_database_types :native_database_types
12
+ def native_database_types
13
+ original_native_database_types.merge!(geometry_data_types)
14
+ end
15
+
16
+ alias :original_quote :quote
17
+ #Redefines the quote method to add behaviour for when a Geometry is encountered ; used when binding variables in find_by methods
18
+ def quote(value, column = nil)
19
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
20
+ "GeomFromWKB(0x#{value.as_hex_wkb},#{value.srid})"
21
+ else
22
+ original_quote(value,column)
23
+ end
24
+ end
25
+
26
+ #Redefinition of columns to add the information that a column is geometric
27
+ def columns(table_name, name = nil)#:nodoc:
28
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
29
+ columns = []
30
+ result = execute(sql, name)
31
+ result.each do |field|
32
+ klass = field[1] =~ /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i ? ActiveRecord::ConnectionAdapters::SpatialMysql2Column : ActiveRecord::ConnectionAdapters::Mysql2Column
33
+ columns << klass.new(field[0], field[4], field[1], field[2] == "YES")
34
+ end
35
+ columns
36
+ end
37
+
38
+
39
+ #operations relative to migrations
40
+
41
+ #Redefines add_index to support the case where the index is spatial
42
+ #If the :spatial key in the options table is true, then the sql string for a spatial index is created
43
+ def add_index(table_name,column_name,options = {})
44
+ index_name = options[:name] || index_name(table_name,:column => Array(column_name))
45
+
46
+ if options[:spatial]
47
+ execute "CREATE SPATIAL INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ #Check the nature of the index : If it is SPATIAL, it is indicated in the IndexDefinition object (redefined to add the spatial flag in spatial_adapter_common.rb)
54
+ def indexes(table_name, name = nil)#:nodoc:
55
+ indexes = []
56
+ current_index = nil
57
+ (execute("SHOW KEYS FROM #{table_name}", name) || []).each do |row|
58
+ if current_index != row[2]
59
+ next if row[2] == "PRIMARY" # skip the primary key
60
+ current_index = row[2]
61
+ indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.new(row[0], row[2], row[1] == "0", [], row[10] == "SPATIAL")
62
+ end
63
+ indexes.last.columns << row[4]
64
+ end
65
+ indexes
66
+ end
67
+
68
+ #Get the table creation options : Only the engine for now. The text encoding could also be parsed and returned here.
69
+ def options_for(table)
70
+ result = execute("show table status like '#{table}'")
71
+ engine = result.first[1]
72
+ if engine !~ /inno/i #inno is default so do nothing for it in order not to clutter the migration
73
+ "ENGINE=#{engine}"
74
+ else
75
+ nil
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ module ActiveRecord
82
+ module ConnectionAdapters
83
+ class SpatialMysql2Column < Mysql2Column
84
+ include SpatialAdapter::SpatialColumn
85
+
86
+ #MySql-specific geometry string parsing. By default, MySql returns geometries in strict wkb format with "0" characters in the first 4 positions.
87
+ def self.string_to_geometry(string)
88
+ return string unless string.is_a?(String)
89
+ begin
90
+ GeoRuby::SimpleFeatures::Geometry.from_ewkb(string[4..-1])
91
+ rescue Exception => exception
92
+ nil
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,388 @@
1
+ require 'spatial_adapter'
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
4
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
5
+ include SpatialAdapter
6
+
7
+ def postgis_version
8
+ begin
9
+ select_value("SELECT postgis_full_version()").scan(/POSTGIS="([\d\.]*)"/)[0][0]
10
+ rescue ActiveRecord::StatementInvalid
11
+ nil
12
+ end
13
+ end
14
+
15
+ def postgis_major_version
16
+ version = postgis_version
17
+ version ? version.scan(/^(\d)\.\d\.\d$/)[0][0].to_i : nil
18
+ end
19
+
20
+ def postgis_minor_version
21
+ version = postgis_version
22
+ version ? version.scan(/^\d\.(\d)\.\d$/)[0][0].to_i : nil
23
+ end
24
+
25
+ def spatial?
26
+ !postgis_version.nil?
27
+ end
28
+
29
+ def supports_geographic?
30
+ postgis_major_version > 1 || (postgis_major_version == 1 && postgis_minor_version >= 5)
31
+ end
32
+
33
+ alias :original_native_database_types :native_database_types
34
+ def native_database_types
35
+ original_native_database_types.merge!(geometry_data_types)
36
+ end
37
+
38
+ alias :original_quote :quote
39
+ #Redefines the quote method to add behaviour for when a Geometry is encountered
40
+ def quote(value, column = nil)
41
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
42
+ "'#{value.as_hex_ewkb}'"
43
+ else
44
+ original_quote(value,column)
45
+ end
46
+ end
47
+
48
+ def columns(table_name, name = nil) #:nodoc:
49
+ raw_geom_infos = column_spatial_info(table_name)
50
+
51
+ column_definitions(table_name).collect do |name, type, default, notnull|
52
+ case type
53
+ when /geography/i
54
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_from_geography(name, default, type, notnull == 'f')
55
+ when /geometry/i
56
+ raw_geom_info = raw_geom_infos[name]
57
+ if raw_geom_info.nil?
58
+ # This column isn't in the geometry_columns table, so we don't know anything else about it
59
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.create_simplified(name, default, notnull == "f")
60
+ else
61
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.new(name, default, raw_geom_info.type, notnull == "f", raw_geom_info.srid, raw_geom_info.with_z, raw_geom_info.with_m)
62
+ end
63
+ else
64
+ ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(name, default, type, notnull == "f")
65
+ end
66
+ end
67
+ end
68
+
69
+ def create_table(table_name, options = {})
70
+ # Using the subclassed table definition
71
+ table_definition = ActiveRecord::ConnectionAdapters::PostgreSQLTableDefinition.new(self)
72
+ table_definition.primary_key(options[:primary_key] || ActiveRecord::Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
73
+
74
+ yield table_definition if block_given?
75
+
76
+ if options[:force] && table_exists?(table_name)
77
+ drop_table(table_name, options)
78
+ end
79
+
80
+ create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
81
+ create_sql << "#{quote_table_name(table_name)} ("
82
+ create_sql << table_definition.to_sql
83
+ create_sql << ") #{options[:options]}"
84
+
85
+ # This is the additional portion for PostGIS
86
+ unless table_definition.geom_columns.nil?
87
+ table_definition.geom_columns.each do |geom_column|
88
+ geom_column.table_name = table_name
89
+ create_sql << "; " + geom_column.to_sql
90
+ end
91
+ end
92
+
93
+ execute create_sql
94
+ end
95
+
96
+ alias :original_remove_column :remove_column
97
+ def remove_column(table_name, *column_names)
98
+ column_names = column_names.flatten
99
+ columns(table_name).each do |col|
100
+ if column_names.include?(col.name.to_sym)
101
+ # Geometry columns have to be removed using DropGeometryColumn
102
+ if col.is_a?(SpatialColumn) && col.spatial? && !col.geographic?
103
+ execute "SELECT DropGeometryColumn('#{table_name}','#{col.name}')"
104
+ else
105
+ original_remove_column(table_name, col.name)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ alias :original_add_column :add_column
112
+ def add_column(table_name, column_name, type, options = {})
113
+ unless geometry_data_types[type].nil?
114
+ geom_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumnDefinition.new(self, column_name, type, nil, nil, options[:null], options[:srid] || -1 , options[:with_z] || false , options[:with_m] || false, options[:geographic] || false)
115
+ if geom_column.geographic
116
+ default = options[:default]
117
+ notnull = options[:null] == false
118
+
119
+ execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{geom_column.to_sql}")
120
+
121
+ change_column_default(table_name, column_name, default) if options_include_default?(options)
122
+ change_column_null(table_name, column_name, false, default) if notnull
123
+ else
124
+ geom_column.table_name = table_name
125
+ execute geom_column.to_sql
126
+ end
127
+ else
128
+ original_add_column(table_name, column_name, type, options)
129
+ end
130
+ end
131
+
132
+ # Adds an index to a column.
133
+ def add_index(table_name, column_name, options = {})
134
+ column_names = Array(column_name)
135
+ index_name = index_name(table_name, :column => column_names)
136
+
137
+ if Hash === options # legacy support, since this param was a string
138
+ index_type = options[:unique] ? "UNIQUE" : ""
139
+ index_name = options[:name] || index_name
140
+ index_method = options[:spatial] ? 'USING GIST' : ""
141
+ else
142
+ index_type = options
143
+ end
144
+ quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
145
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_method} (#{quoted_column_names})"
146
+ end
147
+
148
+ # Returns the list of all indexes for a table.
149
+ #
150
+ # This is a full replacement for the ActiveRecord method and as a result
151
+ # has a higher probability of breaking in future releases.
152
+ def indexes(table_name, name = nil)
153
+ schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
154
+
155
+ # Changed from upstread: link to pg_am to grab the index type (e.g. "gist")
156
+ result = query(<<-SQL, name)
157
+ SELECT distinct i.relname, d.indisunique, d.indkey, t.oid, am.amname
158
+ FROM pg_class t, pg_class i, pg_index d, pg_attribute a, pg_am am
159
+ WHERE i.relkind = 'i'
160
+ AND d.indexrelid = i.oid
161
+ AND d.indisprimary = 'f'
162
+ AND t.oid = d.indrelid
163
+ AND t.relname = '#{table_name}'
164
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
165
+ AND i.relam = am.oid
166
+ AND a.attrelid = t.oid
167
+ ORDER BY i.relname
168
+ SQL
169
+
170
+
171
+ indexes = []
172
+
173
+ indexes = result.map do |row|
174
+ index_name = row[0]
175
+ unique = row[1] == 't'
176
+ indkey = row[2].split(" ")
177
+ oid = row[3]
178
+ indtype = row[4]
179
+
180
+ # Changed from upstream: need to get the column types to test for spatial indexes
181
+ columns = query(<<-SQL, "Columns for index #{row[0]} on #{table_name}").inject({}) {|attlist, r| attlist[r[1]] = [r[0], r[2]]; attlist}
182
+ SELECT a.attname, a.attnum, t.typname
183
+ FROM pg_attribute a, pg_type t
184
+ WHERE a.attrelid = #{oid}
185
+ AND a.attnum IN (#{indkey.join(",")})
186
+ AND a.atttypid = t.oid
187
+ SQL
188
+
189
+ # Only GiST indexes on spatial columns denote a spatial index
190
+ spatial = indtype == 'gist' && columns.size == 1 && (columns.values.first[1] == 'geometry' || columns.values.first[1] == 'geography')
191
+
192
+ column_names = indkey.map {|attnum| columns[attnum] ? columns[attnum][0] : nil }
193
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, spatial)
194
+ end
195
+
196
+ indexes
197
+ end
198
+
199
+ def disable_referential_integrity(&block) #:nodoc:
200
+ if supports_disable_referential_integrity?() then
201
+ execute(tables_without_postgis.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
202
+ end
203
+ yield
204
+ ensure
205
+ if supports_disable_referential_integrity?() then
206
+ execute(tables_without_postgis.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
207
+ end
208
+ end
209
+
210
+ private
211
+
212
+ def tables_without_postgis
213
+ tables - %w{ geometry_columns spatial_ref_sys }
214
+ end
215
+
216
+ def column_spatial_info(table_name)
217
+ constr = query("SELECT * FROM geometry_columns WHERE f_table_name = '#{table_name}'")
218
+
219
+ raw_geom_infos = {}
220
+ constr.each do |constr_def_a|
221
+ raw_geom_infos[constr_def_a[3]] ||= SpatialAdapter::RawGeomInfo.new
222
+ raw_geom_infos[constr_def_a[3]].type = constr_def_a[6]
223
+ raw_geom_infos[constr_def_a[3]].dimension = constr_def_a[4].to_i
224
+ raw_geom_infos[constr_def_a[3]].srid = constr_def_a[5].to_i
225
+
226
+ if raw_geom_infos[constr_def_a[3]].type[-1] == ?M
227
+ raw_geom_infos[constr_def_a[3]].with_m = true
228
+ raw_geom_infos[constr_def_a[3]].type.chop!
229
+ else
230
+ raw_geom_infos[constr_def_a[3]].with_m = false
231
+ end
232
+ end
233
+
234
+ raw_geom_infos.each_value do |raw_geom_info|
235
+ #check the presence of z and m
236
+ raw_geom_info.convert!
237
+ end
238
+
239
+ raw_geom_infos
240
+
241
+ end
242
+ end
243
+
244
+ module ActiveRecord
245
+ module ConnectionAdapters
246
+ class PostgreSQLTableDefinition < TableDefinition
247
+ attr_reader :geom_columns
248
+
249
+ def column(name, type, options = {})
250
+ unless (@base.geometry_data_types[type.to_sym].nil? or
251
+ (options[:create_using_addgeometrycolumn] == false))
252
+
253
+ column = self[name] || PostgreSQLColumnDefinition.new(@base, name, type)
254
+ column.null = options[:null]
255
+ column.srid = options[:srid] || -1
256
+ column.with_z = options[:with_z] || false
257
+ column.with_m = options[:with_m] || false
258
+ column.geographic = options[:geographic] || false
259
+
260
+ if column.geographic
261
+ @columns << column unless @columns.include? column
262
+ else
263
+ # Hold this column for later
264
+ @geom_columns ||= []
265
+ @geom_columns << column
266
+ end
267
+ self
268
+ else
269
+ super(name, type, options)
270
+ end
271
+ end
272
+ end
273
+
274
+ class PostgreSQLColumnDefinition < ColumnDefinition
275
+ attr_accessor :table_name
276
+ attr_accessor :srid, :with_z, :with_m, :geographic
277
+ attr_reader :spatial
278
+
279
+ def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil, null=nil, srid=-1, with_z=false, with_m=false, geographic=false)
280
+ super(base, name, type, limit, default, null)
281
+ @table_name = nil
282
+ @spatial = true
283
+ @srid = srid
284
+ @with_z = with_z
285
+ @with_m = with_m
286
+ @geographic = geographic
287
+ end
288
+
289
+ def sql_type
290
+ if geographic
291
+ type_sql = base.geometry_data_types[type.to_sym][:name]
292
+ type_sql += "Z" if with_z
293
+ type_sql += "M" if with_m
294
+ # SRID is not yet supported (defaults to 4326)
295
+ #type_sql += ", #{srid}" if (srid && srid != -1)
296
+ type_sql = "geography(#{type_sql})"
297
+ type_sql
298
+ else
299
+ super
300
+ end
301
+ end
302
+
303
+ def to_sql
304
+ if spatial && !geographic
305
+ type_sql = base.geometry_data_types[type.to_sym][:name]
306
+ type_sql += "M" if with_m and !with_z
307
+ if with_m and with_z
308
+ dimension = 4
309
+ elsif with_m or with_z
310
+ dimension = 3
311
+ else
312
+ dimension = 2
313
+ end
314
+
315
+ column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})"
316
+ column_sql += ";ALTER TABLE #{table_name} ALTER #{name} SET NOT NULL" if null == false
317
+ column_sql
318
+ else
319
+ super
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ module ActiveRecord
327
+ module ConnectionAdapters
328
+ class SpatialPostgreSQLColumn < PostgreSQLColumn
329
+ include SpatialAdapter::SpatialColumn
330
+
331
+ def initialize(name, default, sql_type = nil, null = true, srid=-1, with_z=false, with_m=false, geographic = false)
332
+ super(name, default, sql_type, null, srid, with_z, with_m)
333
+ @geographic = geographic
334
+ end
335
+
336
+ def geographic?
337
+ @geographic
338
+ end
339
+
340
+ #Transforms a string to a geometry. PostGIS returns a HewEWKB string.
341
+ def self.string_to_geometry(string)
342
+ return string unless string.is_a?(String)
343
+ GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(string) rescue nil
344
+ end
345
+
346
+ def self.create_simplified(name, default, null = true)
347
+ new(name, default, "geometry", null)
348
+ end
349
+
350
+ def self.create_from_geography(name, default, sql_type, null = true)
351
+ params = extract_geography_params(sql_type)
352
+ new(name, default, sql_type, null, params[:srid], params[:with_z], params[:with_m], true)
353
+ end
354
+
355
+ private
356
+
357
+ # Add detection of PostGIS-specific geography columns
358
+ def geometry_simplified_type(sql_type)
359
+ case sql_type
360
+ when /geography\(point/i then :point
361
+ when /geography\(linestring/i then :line_string
362
+ when /geography\(polygon/i then :polygon
363
+ when /geography\(multipoint/i then :multi_point
364
+ when /geography\(multilinestring/i then :multi_line_string
365
+ when /geography\(multipolygon/i then :multi_polygon
366
+ when /geography\(geometrycollection/i then :geometry_collection
367
+ when /geography/i then :geometry
368
+ else
369
+ super
370
+ end
371
+ end
372
+
373
+ def self.extract_geography_params(sql_type)
374
+ params = {
375
+ :srid => 0,
376
+ :with_z => false,
377
+ :with_m => false
378
+ }
379
+ if sql_type =~ /geography(?:\((?:\w+?)(Z)?(M)?(?:,(\d+))?\))?/i
380
+ params[:with_z] = $1 == 'Z'
381
+ params[:with_m] = $2 == 'M'
382
+ params[:srid] = $3.to_i
383
+ end
384
+ params
385
+ end
386
+ end
387
+ end
388
+ end