activerecord-postgis-adapter 2.2.2 → 3.0.0.beta1
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.
- 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
|