activerecord-postgis-adapter 2.2.2 → 3.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/active_record/connection_adapters/postgis_adapter.rb +2 -1
- data/lib/active_record/connection_adapters/postgis_adapter/arel_tosql.rb +9 -5
- data/lib/active_record/connection_adapters/postgis_adapter/create_connection.rb +7 -1
- data/lib/active_record/connection_adapters/postgis_adapter/main_adapter.rb +44 -185
- data/lib/active_record/connection_adapters/postgis_adapter/oid/spatial.rb +119 -0
- data/lib/active_record/connection_adapters/postgis_adapter/schema_statements.rb +110 -0
- data/lib/active_record/connection_adapters/postgis_adapter/spatial_column.rb +32 -122
- data/lib/active_record/connection_adapters/postgis_adapter/spatial_column_info.rb +2 -2
- data/lib/active_record/connection_adapters/postgis_adapter/spatial_table_definition.rb +115 -65
- data/lib/active_record/connection_adapters/postgis_adapter/version.rb +1 -1
- data/test/basic_test.rb +129 -159
- data/test/ddl_test.rb +240 -256
- data/test/nested_class_test.rb +7 -18
- data/test/spatial_queries_test.rb +67 -85
- data/test/tasks_test.rb +116 -117
- data/test/test_helper.rb +20 -0
- data/test/type_test.rb +26 -0
- metadata +33 -22
- data/lib/active_record/connection_adapters/postgis_adapter/common_adapter_methods.rb +0 -53
@@ -0,0 +1,110 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module PostGISAdapter
|
4
|
+
module SchemaStatements
|
5
|
+
# override
|
6
|
+
# pass table_name to #new_column
|
7
|
+
def columns(table_name)
|
8
|
+
# Limit, precision, and scale are all handled by the superclass.
|
9
|
+
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
|
10
|
+
oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
|
11
|
+
default_value = extract_value_from_default(oid, default)
|
12
|
+
default_function = extract_default_function(default_value, default)
|
13
|
+
new_column(table_name, column_name, default_value, oid, type, notnull == 'f', default_function)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# override
|
18
|
+
def new_column(table_name, column_name, default, cast_type, sql_type = nil, null = true, default_function = nil)
|
19
|
+
# JDBC gets true/false in Rails 4, where other platforms get 't'/'f' strings.
|
20
|
+
if null.is_a?(String)
|
21
|
+
null = (null == 't')
|
22
|
+
end
|
23
|
+
|
24
|
+
column_info = spatial_column_info(table_name).get(column_name, sql_type)
|
25
|
+
|
26
|
+
SpatialColumn.new(@rgeo_factory_settings,
|
27
|
+
table_name,
|
28
|
+
column_name,
|
29
|
+
default,
|
30
|
+
cast_type,
|
31
|
+
sql_type,
|
32
|
+
null,
|
33
|
+
column_info)
|
34
|
+
end
|
35
|
+
|
36
|
+
# override
|
37
|
+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L533
|
38
|
+
#
|
39
|
+
# returns Postgresql sql type string
|
40
|
+
# examples:
|
41
|
+
# "geometry(Point,4326)"
|
42
|
+
# "geography(Point,4326)"
|
43
|
+
#
|
44
|
+
# note: type alone is not enough to detect the sql type,
|
45
|
+
# so `limit` is used to pass the additional information. :(
|
46
|
+
#
|
47
|
+
# type_to_sql(:geography, "Point,4326")
|
48
|
+
# => "geography(Point,4326)"
|
49
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
50
|
+
case type
|
51
|
+
when :geometry, :geography
|
52
|
+
"#{ type.to_s }(#{ limit })"
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# override
|
59
|
+
def native_database_types
|
60
|
+
# Add spatial types
|
61
|
+
super.merge(
|
62
|
+
geography: "geography",
|
63
|
+
geometry: "geometry",
|
64
|
+
geometry_collection: "geometry_collection",
|
65
|
+
line_string: "line_string",
|
66
|
+
multi_line_string: "multi_line_string",
|
67
|
+
multi_point: "multi_point",
|
68
|
+
multi_polygon: "multi_polygon",
|
69
|
+
spatial: "geometry",
|
70
|
+
st_point: "st_point",
|
71
|
+
st_polygon: "st_polygon",
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
# override
|
76
|
+
def create_table_definition(name, temporary, options, as = nil)
|
77
|
+
PostGISAdapter::TableDefinition.new(native_database_types, name, temporary, options, as, self)
|
78
|
+
end
|
79
|
+
|
80
|
+
# memoize hash of column infos for tables
|
81
|
+
def spatial_column_info(table_name)
|
82
|
+
@spatial_column_info ||= {}
|
83
|
+
@spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize_type_map(map)
|
87
|
+
super
|
88
|
+
|
89
|
+
%w(
|
90
|
+
geography
|
91
|
+
geometry
|
92
|
+
geometry_collection
|
93
|
+
line_string
|
94
|
+
multi_line_string
|
95
|
+
multi_point
|
96
|
+
multi_polygon
|
97
|
+
st_point
|
98
|
+
st_polygon
|
99
|
+
)
|
100
|
+
.each do |geo_type|
|
101
|
+
map.register_type(geo_type) do |oid, _, sql_type|
|
102
|
+
OID::Spatial.new(oid, sql_type)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -3,164 +3,74 @@ module ActiveRecord # :nodoc:
|
|
3
3
|
module PostGISAdapter # :nodoc:
|
4
4
|
class SpatialColumn < ConnectionAdapters::PostgreSQLColumn # :nodoc:
|
5
5
|
|
6
|
-
|
6
|
+
# sql_type examples:
|
7
|
+
# "Geometry(Point,4326)"
|
8
|
+
# "Geography(Point,4326)"
|
9
|
+
# cast_type example classes:
|
10
|
+
# OID::Spatial
|
11
|
+
# OID::Integer
|
12
|
+
def initialize(factory_settings, table_name, name, default, cast_type, sql_type = nil, null = true, opts = nil)
|
7
13
|
@factory_settings = factory_settings
|
8
14
|
@table_name = table_name
|
9
|
-
@geographic = !!(sql_type =~ /geography/i)
|
15
|
+
@geographic = !!(sql_type =~ /geography\(/i)
|
10
16
|
if opts
|
11
17
|
# This case comes from an entry in the geometry_columns table
|
12
|
-
|
18
|
+
set_geometric_type_from_name(opts[:type])
|
13
19
|
@srid = opts[:srid].to_i
|
14
20
|
@has_z = !!opts[:has_z]
|
15
21
|
@has_m = !!opts[:has_m]
|
16
22
|
elsif @geographic
|
17
23
|
# Geographic type information is embedded in the SQL type
|
18
|
-
@geometric_type = ::RGeo::Feature::Geometry
|
19
24
|
@srid = 4326
|
20
25
|
@has_z = @has_m = false
|
21
|
-
|
22
|
-
params = $1.split(',')
|
23
|
-
if params.size >= 2
|
24
|
-
if params.first =~ /([a-z]+[^zm])(z?)(m?)/i
|
25
|
-
@has_z = $2.length > 0
|
26
|
-
@has_m = $3.length > 0
|
27
|
-
@geometric_type = ::RGeo::ActiveRecord.geometric_type_from_name($1)
|
28
|
-
end
|
29
|
-
if params.last =~ /(\d+)/
|
30
|
-
@srid = $1.to_i
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
26
|
+
build_from_sql_type(sql_type)
|
34
27
|
elsif sql_type =~ /geography|geometry|point|linestring|polygon/i
|
35
|
-
#
|
36
|
-
@geometric_type =
|
37
|
-
|
38
|
-
else
|
39
|
-
# Non-spatial column
|
40
|
-
@geometric_type = @has_z = @has_m = @srid = nil
|
28
|
+
# A geometry column with no geometry_columns entry.
|
29
|
+
# @geometric_type = geo_type_from_sql_type(sql_type)
|
30
|
+
build_from_sql_type(sql_type)
|
41
31
|
end
|
42
|
-
super(name, default,
|
32
|
+
super(name, default, cast_type, sql_type, null)
|
43
33
|
if spatial?
|
44
34
|
if @srid
|
45
|
-
@limit = { srid: @srid, type:
|
35
|
+
@limit = { srid: @srid, type: geometric_type.type_name.underscore }
|
46
36
|
@limit[:has_z] = true if @has_z
|
47
37
|
@limit[:has_m] = true if @has_m
|
48
38
|
@limit[:geographic] = true if @geographic
|
49
|
-
else
|
50
|
-
@limit = { no_constraints: true }
|
51
39
|
end
|
52
40
|
end
|
53
41
|
end
|
54
42
|
|
55
|
-
attr_reader :geographic
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
43
|
+
attr_reader :geographic,
|
44
|
+
:geometric_type,
|
45
|
+
:has_m,
|
46
|
+
:has_z,
|
47
|
+
:limit, # override
|
48
|
+
:srid
|
60
49
|
|
61
|
-
|
62
|
-
|
63
|
-
|
50
|
+
alias :geographic? :geographic
|
51
|
+
alias :has_z? :has_z
|
52
|
+
alias :has_m? :has_m
|
64
53
|
|
65
54
|
def spatial?
|
66
|
-
|
55
|
+
cast_type.respond_to?(:spatial?) && cast_type.spatial?
|
67
56
|
end
|
68
57
|
|
58
|
+
# TODO: delete - unused?
|
69
59
|
def has_spatial_constraints?
|
70
|
-
|
71
|
-
end
|
72
|
-
|
73
|
-
def klass
|
74
|
-
spatial? ? ::RGeo::Feature::Geometry : super
|
75
|
-
end
|
76
|
-
|
77
|
-
def type_cast(value)
|
78
|
-
if spatial?
|
79
|
-
SpatialColumn.convert_to_geometry(value, @factory_settings, @table_name, name,
|
80
|
-
@geographic, @srid, @has_z, @has_m)
|
81
|
-
else
|
82
|
-
super
|
83
|
-
end
|
60
|
+
!!@srid
|
84
61
|
end
|
85
62
|
|
86
63
|
private
|
87
64
|
|
88
|
-
def
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
def self.convert_to_geometry(input, factory_settings, table_name, column, geographic, srid, has_z, has_m)
|
93
|
-
if srid
|
94
|
-
constraints = {
|
95
|
-
geographic: geographic,
|
96
|
-
has_z_coordinate: has_z,
|
97
|
-
has_m_coordinate: has_m,
|
98
|
-
srid: srid
|
99
|
-
}
|
100
|
-
else
|
101
|
-
constraints = nil
|
102
|
-
end
|
103
|
-
if ::RGeo::Feature::Geometry === input
|
104
|
-
factory = factory_settings.get_column_factory(table_name, column, constraints)
|
105
|
-
::RGeo::Feature.cast(input, factory) rescue nil
|
106
|
-
elsif input.respond_to?(:to_str)
|
107
|
-
input = input.to_str
|
108
|
-
if input.length == 0
|
109
|
-
nil
|
110
|
-
else
|
111
|
-
factory = factory_settings.get_column_factory(table_name, column, constraints)
|
112
|
-
marker = input[0,1]
|
113
|
-
if marker == "\x00" || marker == "\x01" || input[0,4] =~ /[0-9a-fA-F]{4}/
|
114
|
-
::RGeo::WKRep::WKBParser.new(factory, support_ewkb: true).parse(input) rescue nil
|
115
|
-
else
|
116
|
-
::RGeo::WKRep::WKTParser.new(factory, support_ewkt: true).parse(input) rescue nil
|
117
|
-
end
|
118
|
-
end
|
119
|
-
else
|
120
|
-
nil
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
end
|
125
|
-
|
126
|
-
# Register spatial types with the postgres OID mechanism
|
127
|
-
# so we can recognize custom columns coming from the database.
|
128
|
-
class SpatialOID < PostgreSQLAdapter::OID::Type # :nodoc:
|
129
|
-
|
130
|
-
def initialize(factory_generator)
|
131
|
-
@factory_generator = factory_generator
|
65
|
+
def set_geometric_type_from_name(name)
|
66
|
+
@geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
|
132
67
|
end
|
133
68
|
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
end
|
138
|
-
|
139
|
-
end
|
140
|
-
|
141
|
-
PostgreSQLAdapter::OID.register_type('geometry', SpatialOID.new(nil))
|
142
|
-
PostgreSQLAdapter::OID.register_type('geography', SpatialOID.new(::RGeo::Geographic.method(:spherical_factory)))
|
143
|
-
|
144
|
-
# This is a hack to ActiveRecord::ModelSchema. We have to "decorate" the decorate_columns
|
145
|
-
# method to apply class-specific customizations to spatial type casting.
|
146
|
-
module DecorateColumnsModification # :nodoc:
|
147
|
-
|
148
|
-
def decorate_columns(columns_hash)
|
149
|
-
columns_hash = super(columns_hash)
|
150
|
-
return unless columns_hash
|
151
|
-
canonical_columns_ = self.columns_hash
|
152
|
-
columns_hash.each do |name, col|
|
153
|
-
if col.is_a?(SpatialOID) && (canonical = canonical_columns_[name]) && canonical.spatial?
|
154
|
-
columns_hash[name] = canonical
|
155
|
-
end
|
156
|
-
end
|
157
|
-
columns_hash
|
69
|
+
def build_from_sql_type(sql_type)
|
70
|
+
geo_type, @srid, @has_z, @has_m = OID::Spatial.parse_sql_type(sql_type)
|
71
|
+
set_geometric_type_from_name(geo_type)
|
158
72
|
end
|
159
|
-
|
160
73
|
end
|
161
|
-
|
162
|
-
::ActiveRecord::Base.extend(DecorateColumnsModification)
|
163
|
-
|
164
74
|
end
|
165
75
|
end
|
166
76
|
end
|
@@ -30,9 +30,9 @@ module ActiveRecord # :nodoc:
|
|
30
30
|
result
|
31
31
|
end
|
32
32
|
|
33
|
-
#
|
33
|
+
# do not query the database for non-spatial columns/tables
|
34
34
|
def get(column_name, type)
|
35
|
-
return
|
35
|
+
return unless MainAdapter.spatial_column_options(type.to_sym)
|
36
36
|
@spatial_column_info ||= all
|
37
37
|
@spatial_column_info[column_name]
|
38
38
|
end
|
@@ -1,120 +1,170 @@
|
|
1
1
|
module ActiveRecord # :nodoc:
|
2
2
|
module ConnectionAdapters # :nodoc:
|
3
3
|
module PostGISAdapter # :nodoc:
|
4
|
-
class TableDefinition <
|
4
|
+
class TableDefinition < PostgreSQL::TableDefinition # :nodoc:
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
super(types, name, temporary, options, as)
|
11
|
-
end
|
12
|
-
else
|
13
|
-
def initialize(types, name, temporary, options, base)
|
14
|
-
@base = base
|
15
|
-
@spatial_columns_hash = {}
|
16
|
-
super(types, name, temporary, options)
|
17
|
-
end
|
6
|
+
def initialize(types, name, temporary, options, as, adapter)
|
7
|
+
@adapter = adapter
|
8
|
+
@spatial_columns_hash = {}
|
9
|
+
super(types, name, temporary, options, as)
|
18
10
|
end
|
19
11
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
30
|
-
if type == :spatial
|
12
|
+
# super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L320
|
13
|
+
def new_column_definition(name, type, options)
|
14
|
+
if (info = MainAdapter.spatial_column_options(type.to_sym))
|
15
|
+
geo_type = ColumnDefinition.geo_type(options[:type] || type || info[:type])
|
16
|
+
base_type = info[:type] || (options[:geographic] ? :geography : :geometry)
|
17
|
+
|
18
|
+
# puts name.dup << " - " << type.to_s << " - " << options.to_s << " :: " << geo_type.to_s << " - " << base_type.to_s
|
19
|
+
|
31
20
|
if (limit = options.delete(:limit))
|
32
21
|
options.merge!(limit) if limit.is_a?(::Hash)
|
33
22
|
end
|
34
23
|
if options[:geographic]
|
35
|
-
|
36
|
-
spatial_type = (options[:type] || 'geometry').to_s.upcase.gsub('_', '')
|
37
|
-
spatial_type << 'Z' if options[:has_z]
|
38
|
-
spatial_type << 'M' if options[:has_m]
|
39
|
-
options[:limit] = "#{spatial_type},#{options[:srid] || 4326}"
|
40
|
-
end
|
41
|
-
name = name.to_s
|
42
|
-
if primary_key_column_name == name
|
43
|
-
raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
|
24
|
+
options[:limit] = ColumnDefinition.options_to_limit(geo_type, options)
|
44
25
|
end
|
45
|
-
column =
|
46
|
-
column.
|
47
|
-
column.
|
48
|
-
column.
|
49
|
-
column.
|
50
|
-
column.
|
26
|
+
column = super(name, base_type, options)
|
27
|
+
column.spatial_type = geo_type
|
28
|
+
column.geographic = options[:geographic]
|
29
|
+
column.srid = options[:srid]
|
30
|
+
column.has_z = options[:has_z]
|
31
|
+
column.has_m = options[:has_m]
|
51
32
|
(column.geographic? ? @columns_hash : @spatial_columns_hash)[name] = column
|
52
33
|
else
|
53
|
-
super(name, type, options)
|
34
|
+
column = super(name, type, options)
|
54
35
|
end
|
55
|
-
|
36
|
+
|
37
|
+
column
|
56
38
|
end
|
57
39
|
|
40
|
+
def non_geographic_spatial_columns
|
41
|
+
@spatial_columns_hash.values
|
42
|
+
end
|
43
|
+
|
44
|
+
def spatial(name, options = {})
|
45
|
+
raise "You must set a type. For example: 't.spatial type: :st_point'" unless options[:type]
|
46
|
+
column(name, options[:type], options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def geography(name, options = {})
|
50
|
+
column(name, :geography, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def geometry(name, options = {})
|
54
|
+
column(name, :geometry, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def geometry_collection(name, options = {})
|
58
|
+
column(name, :geometry_collection, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def line_string(name, options = {})
|
62
|
+
column(name, :line_string, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def multi_line_string(name, options = {})
|
66
|
+
column(name, :multi_line_string, options)
|
67
|
+
end
|
68
|
+
|
69
|
+
def multi_point(name, options = {})
|
70
|
+
column(name, :multi_point, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def multi_polygon(name, options = {})
|
74
|
+
column(name, :multi_polygon, options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def st_point(name, options = {})
|
78
|
+
column(name, :st_point, options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def st_polygon(name, options = {})
|
82
|
+
column(name, :st_polygon, options)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
58
87
|
def create_column_definition(name, type)
|
59
|
-
if type
|
88
|
+
if MainAdapter.spatial_column_options(type.to_sym)
|
60
89
|
PostGISAdapter::ColumnDefinition.new(name, type)
|
61
90
|
else
|
62
91
|
super
|
63
92
|
end
|
64
93
|
end
|
94
|
+
end
|
65
95
|
|
66
|
-
|
67
|
-
|
96
|
+
class ColumnDefinition < PostgreSQL::ColumnDefinition
|
97
|
+
# needs to accept the spatial type? or figure out from limit ?
|
98
|
+
|
99
|
+
def self.options_to_limit(type, options = {})
|
100
|
+
spatial_type = geo_type(type)
|
101
|
+
spatial_type << "Z" if options[:has_z]
|
102
|
+
spatial_type << "M" if options[:has_m]
|
103
|
+
spatial_type << ",#{ options[:srid] || 4326 }"
|
104
|
+
spatial_type
|
68
105
|
end
|
69
106
|
|
70
|
-
|
107
|
+
# limit is how column options are passed to #type_to_sql
|
108
|
+
# returns: "Point,4326"
|
109
|
+
def limit
|
110
|
+
"".tap do |value|
|
111
|
+
value << self.class.geo_type(spatial_type)
|
112
|
+
value << "Z" if has_z?
|
113
|
+
value << "M" if has_m?
|
114
|
+
value << ",#{ srid }"
|
115
|
+
end
|
116
|
+
end
|
71
117
|
|
72
|
-
|
118
|
+
def self.geo_type(type = "GEOMETRY")
|
119
|
+
g_type = type.to_s.gsub("_", "").upcase
|
120
|
+
return "POINT" if g_type == "STPOINT"
|
121
|
+
return "POLYGON" if g_type == "STPOLYGON"
|
122
|
+
g_type
|
123
|
+
end
|
73
124
|
|
74
125
|
def spatial_type
|
75
126
|
@spatial_type
|
76
127
|
end
|
77
128
|
|
129
|
+
def spatial_type=(value)
|
130
|
+
@spatial_type = value.to_s
|
131
|
+
end
|
132
|
+
|
78
133
|
def geographic?
|
79
134
|
@geographic
|
80
135
|
end
|
81
136
|
|
137
|
+
def geographic=(value)
|
138
|
+
@geographic = !!value
|
139
|
+
end
|
140
|
+
|
82
141
|
def srid
|
83
142
|
if @srid
|
84
143
|
@srid.to_i
|
85
144
|
else
|
86
|
-
geographic? ? 4326 : PostGISAdapter::DEFAULT_SRID
|
145
|
+
geographic? ? 4326 : PostGISAdapter::MainAdapter::DEFAULT_SRID
|
87
146
|
end
|
88
147
|
end
|
89
148
|
|
90
|
-
def
|
91
|
-
@
|
92
|
-
end
|
93
|
-
|
94
|
-
def has_m?
|
95
|
-
@has_m
|
96
|
-
end
|
97
|
-
|
98
|
-
def set_geographic(value)
|
99
|
-
@geographic = !!value
|
149
|
+
def srid=(value)
|
150
|
+
@srid = value
|
100
151
|
end
|
101
152
|
|
102
|
-
def
|
103
|
-
@
|
153
|
+
def has_z?
|
154
|
+
@has_z
|
104
155
|
end
|
105
156
|
|
106
|
-
def
|
107
|
-
@
|
157
|
+
def has_z=(value)
|
158
|
+
@has_z = !!value
|
108
159
|
end
|
109
160
|
|
110
|
-
def
|
111
|
-
@
|
161
|
+
def has_m?
|
162
|
+
@has_m
|
112
163
|
end
|
113
164
|
|
114
|
-
def
|
165
|
+
def has_m=(value)
|
115
166
|
@has_m = !!value
|
116
167
|
end
|
117
|
-
|
118
168
|
end
|
119
169
|
|
120
170
|
end
|