spatial_features 2.7.8 → 2.8.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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/app/models/abstract_feature.rb +167 -0
- data/app/models/aggregate_feature.rb +17 -0
- data/app/models/feature.rb +36 -144
- data/lib/spatial_features/has_spatial_features/feature_import.rb +5 -3
- data/lib/spatial_features/has_spatial_features.rb +4 -19
- data/lib/spatial_features/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3acf24e6695a55827725efd2ca6290b96a4ba54d230e8aa176a7bbd356fe6fe
|
4
|
+
data.tar.gz: 48e9527307f5daabd632acf5e76f9d544bcfc3beaeadacb349c37184986321fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 668a6afb0582a4470d1cab7b2e582392ae8fe392514cc0ac363488458052dd6f90dff13c5004aa5423b25f843bc7b79eec0e1e3e056802d80963b40bd4c6f7e9
|
7
|
+
data.tar.gz: 7c17ddc09d78bd90ef1d75a5f0cb67ee0ebb47fe286c6a272f3812f5b733e2e29dfce265e2ea8b91512d6dabcb1d2ccad089acf5b3e15c0a7368c41eda2a11e3
|
data/README.md
CHANGED
@@ -0,0 +1,167 @@
|
|
1
|
+
class AbstractFeature < ActiveRecord::Base
|
2
|
+
self.table_name = 'features'
|
3
|
+
|
4
|
+
class_attribute :lowres_simplification
|
5
|
+
self.lowres_simplification = 2 # Threshold in meters
|
6
|
+
|
7
|
+
belongs_to :spatial_model, :polymorphic => :true, :autosave => false
|
8
|
+
|
9
|
+
attr_writer :make_valid
|
10
|
+
|
11
|
+
FEATURE_TYPES = %w(polygon point line)
|
12
|
+
|
13
|
+
before_validation :sanitize_feature_type
|
14
|
+
validates_presence_of :geog
|
15
|
+
validate :validate_geometry
|
16
|
+
before_save :sanitize
|
17
|
+
after_save :cache_derivatives
|
18
|
+
|
19
|
+
def self.cache_key
|
20
|
+
"#{maximum(:id)}-#{count}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.with_metadata(k, v)
|
24
|
+
if k.present? && v.present?
|
25
|
+
where('metadata->? = ?', k, v)
|
26
|
+
else
|
27
|
+
all
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.polygons
|
32
|
+
where(:feature_type => 'polygon')
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.lines
|
36
|
+
where(:feature_type => 'line')
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.points
|
40
|
+
where(:feature_type => 'point')
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.area_in_square_meters(geom = 'geom_lowres')
|
44
|
+
current_scope = all.polygons
|
45
|
+
unscoped { connection.select_value(select("ST_Area(ST_Union(#{geom}))").from(current_scope, :features)).to_f }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.total_intersection_area_in_square_meters(other_features, geom = 'geom_lowres')
|
49
|
+
scope = unscope(:select).select("ST_Union(#{geom}) AS geom").polygons
|
50
|
+
other_scope = other_features.polygons
|
51
|
+
|
52
|
+
query = base_class.unscoped.select('ST_Area(ST_Intersection(ST_Union(features.geom), ST_Union(other_features.geom)))')
|
53
|
+
.from(scope, "features")
|
54
|
+
.joins("INNER JOIN (#{other_scope.to_sql}) AS other_features ON ST_Intersects(features.geom, other_features.geom)")
|
55
|
+
return connection.select_value(query).to_f
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.intersecting(other)
|
59
|
+
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').uniq
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.invalid(column = 'geog::geometry')
|
63
|
+
select("features.*, ST_IsValidReason(#{column}) AS invalid_geometry_message").where.not("ST_IsValid(#{column})")
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.valid
|
67
|
+
where('ST_IsValid(geog::geometry)')
|
68
|
+
end
|
69
|
+
|
70
|
+
def envelope(buffer_in_meters = 0)
|
71
|
+
envelope_json = JSON.parse(self.class.select("ST_AsGeoJSON(ST_Envelope(ST_Buffer(features.geog, #{buffer_in_meters})::geometry)) AS result").where(:id => id).first.result)
|
72
|
+
envelope_json = envelope_json["coordinates"].first
|
73
|
+
|
74
|
+
raise "Can't calculate envelope for Feature #{self.id}" if envelope_json.blank?
|
75
|
+
|
76
|
+
return envelope_json.values_at(0,2)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.cache_derivatives(options = {})
|
80
|
+
update_all <<-SQL.squish
|
81
|
+
geom = ST_Transform(geog::geometry, #{detect_srid('geom')}),
|
82
|
+
north = ST_YMax(geog::geometry),
|
83
|
+
east = ST_XMax(geog::geometry),
|
84
|
+
south = ST_YMin(geog::geometry),
|
85
|
+
west = ST_XMin(geog::geometry),
|
86
|
+
area = ST_Area(geog),
|
87
|
+
centroid = ST_PointOnSurface(geog::geometry)
|
88
|
+
SQL
|
89
|
+
|
90
|
+
invalid('geom').update_all <<-SQL.squish
|
91
|
+
geom = ST_Buffer(geom, 0)
|
92
|
+
SQL
|
93
|
+
|
94
|
+
update_all <<-SQL.squish
|
95
|
+
geom_lowres = ST_SimplifyPreserveTopology(geom, #{options.fetch(:lowres_simplification, lowres_simplification)})
|
96
|
+
SQL
|
97
|
+
|
98
|
+
invalid('geom_lowres').update_all <<-SQL.squish
|
99
|
+
geom_lowres = ST_Buffer(geom_lowres, 0)
|
100
|
+
SQL
|
101
|
+
end
|
102
|
+
|
103
|
+
def feature_bounds
|
104
|
+
{n: north, e: east, s: south, w: west}
|
105
|
+
end
|
106
|
+
|
107
|
+
def cache_derivatives(*args)
|
108
|
+
self.class.where(:id => self.id).cache_derivatives(*args)
|
109
|
+
end
|
110
|
+
|
111
|
+
def kml(options = {})
|
112
|
+
geometry = options[:lowres] ? kml_lowres : super()
|
113
|
+
geometry = "<MultiGeometry>#{geometry}#{kml_centroid}</MultiGeometry>" if options[:centroid]
|
114
|
+
return geometry
|
115
|
+
end
|
116
|
+
|
117
|
+
def make_valid?
|
118
|
+
@make_valid
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def make_valid
|
124
|
+
self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Buffer('#{sanitize}', 0)")
|
125
|
+
end
|
126
|
+
|
127
|
+
# Use ST_Force2D to discard z-coordinates that cause failures later in the process
|
128
|
+
def sanitize
|
129
|
+
self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Force2D('#{geog}')")
|
130
|
+
end
|
131
|
+
|
132
|
+
SRID_CACHE = {}
|
133
|
+
def self.detect_srid(column_name)
|
134
|
+
SRID_CACHE[column_name] ||= connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.join_other_features(other)
|
138
|
+
joins('INNER JOIN features AS other_features ON true').where(:other_features => {:id => other})
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_geometry
|
142
|
+
return unless geog?
|
143
|
+
|
144
|
+
error = geometry_validation_message
|
145
|
+
if error && make_valid?
|
146
|
+
make_valid
|
147
|
+
self.make_valid = false
|
148
|
+
validate_geometry
|
149
|
+
elsif error
|
150
|
+
errors.add :geog, error
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def geometry_validation_message
|
155
|
+
klass = self.class.base_class # Use the base class because we don't want to have to include a type column in our select
|
156
|
+
error = klass.connection.select_one(klass.unscoped.invalid.from("(SELECT '#{sanitize_input_for_sql(self.geog)}'::geometry AS geog) #{klass.table_name}"))
|
157
|
+
return error.fetch('invalid_geometry_message') if error
|
158
|
+
end
|
159
|
+
|
160
|
+
def sanitize_feature_type
|
161
|
+
self.feature_type = FEATURE_TYPES.find {|type| self.feature_type.to_s.strip.downcase.include?(type) }
|
162
|
+
end
|
163
|
+
|
164
|
+
def sanitize_input_for_sql(input)
|
165
|
+
self.class.send(:sanitize_sql_for_conditions, input)
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_dependency SpatialFeatures::Engine.root.join('app/models/abstract_feature')
|
2
|
+
|
3
|
+
class AggregateFeature < AbstractFeature
|
4
|
+
has_many :features, lambda { |aggregate| where(:spatial_model_type => aggregate.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
|
5
|
+
|
6
|
+
# Aggregate the features for the spatial model into a single feature
|
7
|
+
def refresh
|
8
|
+
self.geog = ActiveRecord::Base.connection.select_value <<~SQL
|
9
|
+
SELECT ST_Collect(ARRAY[
|
10
|
+
(#{features.select('ST_UNION(ST_CollectionExtract(geog::geometry, 1))').to_sql}),
|
11
|
+
(#{features.select('ST_UNION(ST_CollectionExtract(geog::geometry, 2))').to_sql}),
|
12
|
+
(#{features.select('ST_UNION(ST_CollectionExtract(geog::geometry, 3))').to_sql})
|
13
|
+
])::geography
|
14
|
+
SQL
|
15
|
+
self.save!
|
16
|
+
end
|
17
|
+
end
|
data/app/models/feature.rb
CHANGED
@@ -1,170 +1,62 @@
|
|
1
|
-
class Feature <
|
2
|
-
|
1
|
+
class Feature < AbstractFeature
|
2
|
+
class_attribute :automatically_refresh_aggregate
|
3
|
+
self.automatically_refresh_aggregate = true
|
3
4
|
|
4
|
-
|
5
|
+
class_attribute :lowres_precision
|
6
|
+
self.lowres_precision = 5
|
5
7
|
|
6
|
-
|
8
|
+
has_one :aggregate_feature, lambda { |feature| where(:spatial_model_type => feature.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
|
7
9
|
|
8
|
-
before_validation :sanitize_feature_type
|
9
|
-
validates_presence_of :geog
|
10
|
-
validate :validate_geometry
|
11
10
|
validates_inclusion_of :feature_type, :in => FEATURE_TYPES
|
12
|
-
before_save :sanitize
|
13
|
-
after_save :cache_derivatives
|
14
11
|
|
15
|
-
|
16
|
-
"#{maximum(:id)}-#{count}"
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.with_metadata(k, v)
|
20
|
-
if k.present? && v.present?
|
21
|
-
where('metadata->? = ?', k, v)
|
22
|
-
else
|
23
|
-
all
|
24
|
-
end
|
25
|
-
end
|
12
|
+
after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
|
26
13
|
|
27
|
-
def self.
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def self.lines
|
32
|
-
where(:feature_type => 'line')
|
33
|
-
end
|
14
|
+
def self.defer_aggregate_refresh(&block)
|
15
|
+
start_at = Feature.maximum(:id).to_i + 1
|
16
|
+
output = without_aggregate_refresh(&block)
|
34
17
|
|
35
|
-
|
36
|
-
where(:feature_type => 'point')
|
37
|
-
end
|
18
|
+
where(:id => start_at..Float::INFINITY).refresh_aggregates
|
38
19
|
|
39
|
-
|
40
|
-
current_scope = all.polygons
|
41
|
-
unscoped { connection.select_value(select("ST_Area(ST_Union(#{geom}))").from(current_scope, :features)).to_f }
|
20
|
+
return output
|
42
21
|
end
|
43
22
|
|
44
|
-
def self.
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
.joins("INNER JOIN (#{other_scope.to_sql}) AS other_features ON ST_Intersects(features.geom, other_features.geom)")
|
51
|
-
return connection.select_value(query).to_f
|
23
|
+
def self.without_aggregate_refresh
|
24
|
+
old = Feature.automatically_refresh_aggregate
|
25
|
+
Feature.automatically_refresh_aggregate = false
|
26
|
+
yield
|
27
|
+
ensure
|
28
|
+
Feature.automatically_refresh_aggregate = old
|
52
29
|
end
|
53
30
|
|
54
|
-
def self.
|
55
|
-
|
56
|
-
|
31
|
+
def self.refresh_aggregates
|
32
|
+
# Find one feature from each spatial model and trigger the aggregate feature refresh
|
33
|
+
ids = select('MAX(id)')
|
34
|
+
.where.not(:spatial_model_type => nil, :spatial_model_id => nil)
|
35
|
+
.group('spatial_model_type, spatial_model_id')
|
57
36
|
|
58
|
-
|
59
|
-
select("features.*, ST_IsValidReason(#{column}) AS invalid_geometry_message").where.not("ST_IsValid(#{column})")
|
37
|
+
where(:id => ids).find_each(&:refresh_aggregate)
|
60
38
|
end
|
61
39
|
|
62
|
-
def
|
63
|
-
|
40
|
+
def refresh_aggregate
|
41
|
+
# puts "Refreshing AggregateFeature for #{spatial_model_type} #{spatial_model_id}"
|
42
|
+
build_aggregate_feature unless aggregate_feature&.persisted?
|
43
|
+
aggregate_feature.refresh
|
64
44
|
end
|
65
45
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
return envelope_json.values_at(0,2)
|
46
|
+
def automatically_refresh_aggregate?
|
47
|
+
# Check if there is a spatial model id because nothing prevents is from creating a Feature without one. Depending on
|
48
|
+
# how you assign a feature to a record, you may end up saving it before assigning it to a record, thereby leaving
|
49
|
+
# this field blank.
|
50
|
+
spatial_model_id? && automatically_refresh_aggregate
|
73
51
|
end
|
74
52
|
|
53
|
+
# Features are used for display so we also cache their KML representation
|
75
54
|
def self.cache_derivatives(options = {})
|
76
|
-
|
77
|
-
|
78
|
-
update_all <<-SQL.squish
|
79
|
-
geom = ST_Transform(geog::geometry, #{detect_srid('geom')}),
|
80
|
-
north = ST_YMax(geog::geometry),
|
81
|
-
east = ST_XMax(geog::geometry),
|
82
|
-
south = ST_YMin(geog::geometry),
|
83
|
-
west = ST_XMin(geog::geometry),
|
84
|
-
area = ST_Area(geog),
|
85
|
-
centroid = ST_PointOnSurface(geog::geometry)
|
86
|
-
SQL
|
87
|
-
|
88
|
-
invalid('geom').update_all <<-SQL.squish
|
89
|
-
geom = ST_Buffer(geom, 0)
|
90
|
-
SQL
|
91
|
-
|
92
|
-
update_all <<-SQL.squish
|
93
|
-
geom_lowres = ST_SimplifyPreserveTopology(geom, #{options[:lowres_simplification]})
|
94
|
-
SQL
|
95
|
-
|
96
|
-
invalid('geom_lowres').update_all <<-SQL.squish
|
97
|
-
geom_lowres = ST_Buffer(geom_lowres, 0)
|
98
|
-
SQL
|
99
|
-
|
55
|
+
super
|
100
56
|
update_all <<-SQL.squish
|
101
57
|
kml = ST_AsKML(geog, 6),
|
102
|
-
kml_lowres = ST_AsKML(geom_lowres, #{options
|
58
|
+
kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
|
103
59
|
kml_centroid = ST_AsKML(centroid)
|
104
60
|
SQL
|
105
61
|
end
|
106
|
-
|
107
|
-
def feature_bounds
|
108
|
-
{n: north, e: east, s: south, w: west}
|
109
|
-
end
|
110
|
-
|
111
|
-
def cache_derivatives(*args)
|
112
|
-
self.class.where(:id => self.id).cache_derivatives(*args)
|
113
|
-
end
|
114
|
-
|
115
|
-
def kml(options = {})
|
116
|
-
geometry = options[:lowres] ? kml_lowres : super()
|
117
|
-
geometry = "<MultiGeometry>#{geometry}#{kml_centroid}</MultiGeometry>" if options[:centroid]
|
118
|
-
return geometry
|
119
|
-
end
|
120
|
-
|
121
|
-
def make_valid?
|
122
|
-
@make_valid
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def make_valid
|
128
|
-
self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Buffer('#{sanitize}', 0)")
|
129
|
-
end
|
130
|
-
|
131
|
-
# Use ST_Force2D to discard z-coordinates that cause failures later in the process
|
132
|
-
def sanitize
|
133
|
-
self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Force2D('#{geog}')")
|
134
|
-
end
|
135
|
-
|
136
|
-
SRID_CACHE = {}
|
137
|
-
def self.detect_srid(column_name)
|
138
|
-
SRID_CACHE[column_name] ||= connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
|
139
|
-
end
|
140
|
-
|
141
|
-
def self.join_other_features(other)
|
142
|
-
joins('INNER JOIN features AS other_features ON true').where(:other_features => {:id => other})
|
143
|
-
end
|
144
|
-
|
145
|
-
def validate_geometry
|
146
|
-
return unless geog?
|
147
|
-
|
148
|
-
error = geometry_validation_message
|
149
|
-
if error && make_valid?
|
150
|
-
make_valid
|
151
|
-
self.make_valid = false
|
152
|
-
validate_geometry
|
153
|
-
elsif error
|
154
|
-
errors.add :geog, error
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def geometry_validation_message
|
159
|
-
error = self.class.connection.select_one(self.class.unscoped.invalid.from("(SELECT '#{sanitize_input_for_sql(self.geog)}'::geometry AS geog) #{self.class.table_name}"))
|
160
|
-
return error.fetch('invalid_geometry_message') if error
|
161
|
-
end
|
162
|
-
|
163
|
-
def sanitize_feature_type
|
164
|
-
self.feature_type = FEATURE_TYPES.find {|type| self.feature_type.to_s.strip.downcase.include?(type) }
|
165
|
-
end
|
166
|
-
|
167
|
-
def sanitize_input_for_sql(input)
|
168
|
-
self.class.send(:sanitize_sql_for_conditions, input)
|
169
|
-
end
|
170
62
|
end
|
@@ -79,9 +79,11 @@ module SpatialFeatures
|
|
79
79
|
|
80
80
|
def import_features(imports, skip_invalid)
|
81
81
|
self.features.delete_all
|
82
|
-
valid, invalid =
|
83
|
-
|
84
|
-
|
82
|
+
valid, invalid = Feature.defer_aggregate_refresh do
|
83
|
+
imports.flat_map(&:features).partition do |feature|
|
84
|
+
feature.spatial_model = self
|
85
|
+
feature.save
|
86
|
+
end
|
85
87
|
end
|
86
88
|
|
87
89
|
errors = imports.flat_map(&:errors)
|
@@ -11,6 +11,7 @@ module SpatialFeatures
|
|
11
11
|
include FeatureImport
|
12
12
|
|
13
13
|
has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
|
14
|
+
has_one :aggregate_feature, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete
|
14
15
|
|
15
16
|
scope :with_features, lambda { joins(:features).uniq }
|
16
17
|
scope :without_features, lambda { joins("LEFT OUTER JOIN features ON features.spatial_model_type = '#{Utils.base_class(name)}' AND features.spatial_model_id = #{table_name}.id").where("features.id IS NULL") }
|
@@ -128,24 +129,8 @@ module SpatialFeatures
|
|
128
129
|
scope = spatial_join(other, buffer_in_meters)
|
129
130
|
scope = scope.select(options[:columns])
|
130
131
|
|
131
|
-
|
132
|
-
|
133
|
-
scope = scope.group("#{table_name}.#{primary_key}") # Aggregate functions require grouping
|
134
|
-
else
|
135
|
-
scope = scope.distinct
|
136
|
-
end
|
137
|
-
|
138
|
-
scope = scope.select("MIN(ST_Distance(features.geom, other_features.geom)) AS distance_in_meters") if options[:distance]
|
139
|
-
scope = scope.select <<~SQL if options[:intersection_area]
|
140
|
-
ST_Area(
|
141
|
-
ST_Intersection(
|
142
|
-
/* Extract only polygons to calculations since we're calculating area */
|
143
|
-
/* Union to aggregate features from all records in the query before intersection to flatten geometry and avoid possible self intersection */
|
144
|
-
ST_Union(ST_CollectionExtract(features.geom, 3)),
|
145
|
-
ST_Union(ST_CollectionExtract(other_features.geom, 3))
|
146
|
-
)
|
147
|
-
) AS intersection_area_in_square_meters
|
148
|
-
SQL
|
132
|
+
scope = scope.select("ST_Distance(features.geom, other_features.geom) AS distance_in_meters") if options[:distance]
|
133
|
+
scope = scope.select("ST_Area(ST_Intersection(features.geom, other_features.geom)) AS intersection_area_in_square_meters") if options[:intersection_area]
|
149
134
|
|
150
135
|
return scope
|
151
136
|
end
|
@@ -165,7 +150,7 @@ module SpatialFeatures
|
|
165
150
|
end
|
166
151
|
|
167
152
|
def features_scope(other)
|
168
|
-
scope =
|
153
|
+
scope = AggregateFeature
|
169
154
|
scope = scope.where(:spatial_model_type => Utils.base_class_of(other).to_s)
|
170
155
|
scope = scope.where(:spatial_model_id => other) unless Utils.class_of(other) == other
|
171
156
|
return scope
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spatial_features
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Wallace
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-
|
12
|
+
date: 2019-10-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -167,6 +167,8 @@ files:
|
|
167
167
|
- MIT-LICENSE
|
168
168
|
- README.md
|
169
169
|
- Rakefile
|
170
|
+
- app/models/abstract_feature.rb
|
171
|
+
- app/models/aggregate_feature.rb
|
170
172
|
- app/models/feature.rb
|
171
173
|
- app/models/spatial_cache.rb
|
172
174
|
- app/models/spatial_proximity.rb
|