spatial_features 1.3.10 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8a4f197e3ba07d1cc425b2b8b8367603c1434643
4
- data.tar.gz: 3b6ad7bbd92da3ba7d2b781305463807181a8eb5
3
+ metadata.gz: 4825ec1877a47df3ef4c5ccb0d83ee648e093322
4
+ data.tar.gz: a9aa87b0eed16d9e1a12a629ece02ec164365b2c
5
5
  SHA512:
6
- metadata.gz: d03677e9c02f0d812740eab6e283a83a8b64f83a39d179199149a375dc5c6f6920c1ce63754b75c1f1975fbfee592553946585994bc77cc480c62e9067cfad54
7
- data.tar.gz: 5eb03110f86a432f45dea48fad7fdb4fdfd155178fc0e0f255a666b291830f732ba6ba02b6c7296761de13e84140ff38a093d1cd0458a2a92b5b11727b0aeefa
6
+ metadata.gz: c5b3b9952c83e2010e6138be8784b87047c11a8694a7ecf4178617415d253bbf39978ebd985b079e9dacc3bacedcb312e70467314f31bd12af27f18606c9bf0a
7
+ data.tar.gz: 88244ebe9df2c9d123de36aac02f9fe23cbebe946918fea36ac74c51f90bb74b7f044511ff5034aefb29851b158aa6392ffb765b41536ca66c3d1807efe61fb3
@@ -15,70 +15,117 @@ module SpatialFeatures
15
15
  end
16
16
 
17
17
  module ClassMethods
18
- # Add methods to generate cache keys for a record or all records of this class
19
- # NOTE: features are never updated, only deleted and created, therefore we can
20
- # tell if they have changed by finding the maximum id and count instead of needing timestamps
21
- def features_cache_key
22
- max_id, count = Feature.where(:spatial_model_type => self).pluck("MAX(id), COUNT(*)").first
23
- "#{name}/#{max_id}-#{count}"
24
- end
25
-
26
- def intersecting(other, options = {})
27
- within_buffer(other, 0, options)
28
- end
29
-
30
- def within_buffer(other, buffer_in_meters = 0, options = {})
31
- raise "Can't intersect with #{other} because it does not implement has_features" unless other.has_spatial_features?
32
-
33
- if options[:cache] != false # CACHED
34
- return all.extending(UncachedRelation) unless other.spatial_cache_for?(self, buffer_in_meters) # Don't use the cache if it doesn't exist
35
-
36
- scope = cached_spatial_join(other)
37
- .select("#{table_name}.*, spatial_proximities.distance_in_meters, spatial_proximities.intersection_area_in_square_meters")
38
-
39
- scope = scope.where("spatial_proximities.distance_in_meters <= ?", buffer_in_meters) if buffer_in_meters
40
- else # NON-CACHED
41
- scope = joins_features_for(other)
42
- .select("#{table_name}.*")
43
- .group("#{table_name}.#{primary_key}")
44
-
45
- scope = scope.where('ST_DWithin(features_for.geom, features_for_other.geom, ?)', buffer_in_meters) if buffer_in_meters
46
- scope = scope.select("MIN(ST_Distance(features_for.geom, features_for_other.geom)) AS distance_in_meters") if options[:distance]
47
- scope = scope.select("SUM(ST_Area(ST_Intersection(features_for.geom, features_for_other.geom))) AS intersection_area_in_square_meters") if options[:intersection_area]
48
- end
49
-
50
- return scope
51
- end
52
-
53
- def polygons
54
- Feature.polygons.where(:spatial_model_type => self.class)
55
- end
56
-
57
- def lines
58
- Feature.lines.where(:spatial_model_type => self.class)
59
- end
60
-
61
- def points
62
- Feature.points.where(:spatial_model_type => self.class)
63
- end
64
-
65
- def cached_spatial_join(other)
66
- raise "Cannot use cached spatial join for the same class" if other.class.name == self.name
67
-
68
- other_column = other.class.name < self.name ? :model_a : :model_b
69
- self_column = other_column == :model_a ? :model_b : :model_a
70
-
71
- joins("INNER JOIN spatial_proximities ON spatial_proximities.#{self_column}_type = '#{self}' AND spatial_proximities.#{self_column}_id = #{table_name}.id AND spatial_proximities.#{other_column}_type = '#{other.class}' AND spatial_proximities.#{other_column}_id = '#{other.id}'")
72
- end
73
-
74
- def joins_features_for(other, table_alias = 'features_for')
75
- joins_features(table_alias)
76
- .joins(%Q(INNER JOIN features "#{table_alias}_other" ON "#{table_alias}_other".spatial_model_type = '#{other.class.name}' AND "#{table_alias}_other".spatial_model_id = #{other.id}))
77
- end
78
-
79
- def joins_features(table_alias = 'features_for')
80
- joins(%Q(INNER JOIN features "#{table_alias}" ON "#{table_alias}".spatial_model_type = '#{name}' AND "#{table_alias}".spatial_model_id = #{table_name}.id))
81
- end
18
+ # Add methods to generate cache keys for a record or all records of this class
19
+ # NOTE: features are never updated, only deleted and created, therefore we can
20
+ # tell if they have changed by finding the maximum id and count instead of needing timestamps
21
+ def features_cache_key
22
+ max_id, count = features.pluck("MAX(id), COUNT(*)").first
23
+ "#{name}/#{max_id}-#{count}"
24
+ end
25
+
26
+ def intersecting(other, options = {})
27
+ within_buffer(other, 0, options)
28
+ end
29
+
30
+ def within_buffer(other, buffer_in_meters = 0, options = {})
31
+ if options[:cache] != false # CACHED
32
+ return all.extending(UncachedRelation) unless class_for(other).spatial_cache_for?(self, buffer_in_meters) # Don't use the cache if it doesn't exist
33
+
34
+ scope = cached_spatial_join(other)
35
+ .select("#{table_name}.*, spatial_proximities.distance_in_meters, spatial_proximities.intersection_area_in_square_meters")
36
+ .group("#{table_name}.#{primary_key}")
37
+
38
+ scope = scope.where("spatial_proximities.distance_in_meters <= ?", buffer_in_meters) if buffer_in_meters
39
+ else # NON-CACHED
40
+ scope = joins_features_for(other)
41
+ .select("#{table_name}.*")
42
+ .group("#{table_name}.#{primary_key}")
43
+
44
+ scope = scope.where('ST_DWithin(features_for.geom, features_for_other.geom, ?)', buffer_in_meters) if buffer_in_meters
45
+ scope = scope.select("MIN(ST_Distance(features_for.geom, features_for_other.geom)) AS distance_in_meters") if options[:distance]
46
+ scope = scope.select("SUM(ST_Area(ST_Intersection(features_for.geom, features_for_other.geom))) AS intersection_area_in_square_meters") if options[:intersection_area]
47
+ end
48
+
49
+ return scope
50
+ end
51
+
52
+ def covering(other)
53
+ scope = joins_features_for(other).select("#{table_name}.*").group("#{table_name}.#{primary_key}")
54
+ scope = scope.where('ST_Covers(features_for.geom, features_for_other.geom)')
55
+
56
+ return scope
57
+ end
58
+
59
+ def polygons
60
+ features.polygons
61
+ end
62
+
63
+ def lines
64
+ features.lines
65
+ end
66
+
67
+ def points
68
+ features.points
69
+ end
70
+
71
+ def spatial_cache_for?(other, buffer_in_meters)
72
+ if cache = spatial_cache_for(other)
73
+ return cache.intersection_cache_distance.nil? if buffer_in_meters.nil? # cache must be total if no buffer_in_meters
74
+ return false if cache.stale? # cache must be for current features
75
+ return true if cache.intersection_cache_distance.nil? # always good if cache is total
76
+
77
+ return buffer_in_meters <= cache.intersection_cache_distance
78
+ else
79
+ return false
80
+ end
81
+ end
82
+
83
+
84
+ def features
85
+ Feature.where(:spatial_model_type => self, :spatial_model_id => all)
86
+ end
87
+
88
+ def joins_features_for(other, table_alias = 'features_for')
89
+ joins_features(table_alias)
90
+ .joins(%Q(INNER JOIN features "#{table_alias}_other" ON "#{table_alias}_other".spatial_model_type = '#{class_for(other)}' AND "#{table_alias}_other".spatial_model_id IN (#{ids_sql_for(other)})))
91
+ end
92
+
93
+ def joins_features(table_alias = 'features_for')
94
+ joins(%Q(INNER JOIN features "#{table_alias}" ON "#{table_alias}".spatial_model_type = '#{name}' AND "#{table_alias}".spatial_model_id = #{table_name}.id))
95
+ end
96
+
97
+ private
98
+
99
+ def cached_spatial_join(other)
100
+ other_class = class_for(other)
101
+
102
+ raise "Cannot use cached spatial join for the same class" if self == other_class
103
+
104
+ other_column = other_class.name < self.name ? :model_a : :model_b
105
+ self_column = other_column == :model_a ? :model_b : :model_a
106
+
107
+ joins("INNER JOIN spatial_proximities ON spatial_proximities.#{self_column}_type = '#{self}' AND spatial_proximities.#{self_column}_id = #{table_name}.id AND spatial_proximities.#{other_column}_type = '#{other_class}' AND spatial_proximities.#{other_column}_id IN (#{ids_sql_for(other)})")
108
+ end
109
+
110
+ def spatial_cache_for(other)
111
+ SpatialCache.where(:spatial_model_type => self, :intersection_model_type => class_for(other)).first
112
+ end
113
+
114
+ # Returns the class for the given, class, scope, or record
115
+ def class_for(other)
116
+ case other
117
+ when ActiveRecord::Base
118
+ other.class
119
+ when ActiveRecord::Relation
120
+ other.klass
121
+ else
122
+ other
123
+ end
124
+ end
125
+
126
+ def ids_sql_for(other)
127
+ other.is_a?(ActiveRecord::Base) ? other.id : other.unscope(:select).select(:id).to_sql
128
+ end
82
129
  end
83
130
 
84
131
  module InstanceMethods
@@ -88,78 +135,65 @@ module SpatialFeatures
88
135
 
89
136
  def features_cache_key
90
137
  max_id, count = features.pluck("MAX(id), COUNT(*)").first
91
- "#{self.class.name}/#{self.id}-#{max_id}-#{count}"
92
- end
93
-
94
- def polygons?
95
- !features.polygons.empty?
96
- end
97
-
98
- def lines?
99
- !features.lines.empty?
100
- end
101
-
102
- def points?
103
- !features.points.empty?
104
- end
105
-
106
- def features?
107
- features.present?
108
- end
109
-
110
- # Returns true if the model stores a hash of the features so we don't need to process the features if they haven't changed
111
- def has_spatial_features_hash?
112
- respond_to?(:features_hash)
113
- end
114
-
115
- def intersects?(other)
116
- self.class.intersecting(other).exists?(self)
117
- end
118
-
119
- def total_intersection_area_in_square_meters(klass, options = {})
120
- self.class
121
- .select(%Q(ST_Area(ST_Intersection(ST_Union(features_for.geog_lowres::geometry), ST_Union(features_for_other.geog_lowres::geometry))::geography) AS intersection_area_in_square_meters))
122
- .joins(%Q(INNER JOIN features "features_for" ON "features_for".spatial_model_type = '#{self.class}' AND "features_for".spatial_model_id = #{self.class.table_name}.id))
123
- .joins(%Q(INNER JOIN features "features_for_other" ON "features_for_other".spatial_model_type = '#{klass}'))
124
- .where(:id => self.id)
125
- .where('ST_DWithin(features_for.geog_lowres, features_for_other.geog_lowres, 0)')
126
- .group("#{self.class.table_name}.id")
127
- .first
128
- .try(:intersection_area_in_square_meters) || 0
129
- end
130
-
131
- def total_intersection_area_in_hectares(klass)
132
- Formatters::HECTARES.call(total_intersection_area_in_square_meters(klass))
133
- end
134
-
135
- def total_intersection_area_percentage(klass)
136
- return 0.0 unless features_area_in_square_meters > 0
137
-
138
- ((total_intersection_area_in_square_meters(klass) / features_area_in_square_meters) * 100).round(1)
139
- end
140
-
141
- def features_area_in_square_meters
142
- @features_area_in_square_meters ||= features.sum('features.area')
143
- end
144
-
145
- def features_area_in_hectares
146
- Formatters::HECTARES.call(features_area_in_square_meters)
147
- end
148
-
149
- def spatial_cache_for(klass)
150
- spatial_cache.where(:intersection_model_type => klass).first
151
- end
152
-
153
- def spatial_cache_for?(klass, buffer_in_meters)
154
- if cache = spatial_cache_for(klass)
155
- return cache.intersection_cache_distance.nil? if buffer_in_meters.nil? # cache must be total if no buffer_in_meters
156
- return false if cache.stale? # cache must be for current features
157
- return true if cache.intersection_cache_distance.nil? # always good if cache is total
138
+ "#{self.class.name}/#{self.id}-#{max_id}-#{count}"
139
+ end
140
+
141
+ def polygons?
142
+ !features.polygons.empty?
143
+ end
144
+
145
+ def lines?
146
+ !features.lines.empty?
147
+ end
148
+
149
+ def points?
150
+ !features.points.empty?
151
+ end
152
+
153
+ def features?
154
+ features.present?
155
+ end
156
+
157
+ # Returns true if the model stores a hash of the features so we don't need to process the features if they haven't changed
158
+ def has_spatial_features_hash?
159
+ respond_to?(:features_hash)
160
+ end
161
+
162
+ def covers?(other)
163
+ self.class.covering(other).exists?(self)
164
+ end
165
+
166
+ def intersects?(other)
167
+ self.class.intersecting(other).exists?(self)
168
+ end
169
+
170
+ def total_intersection_area_in_square_meters(klass, options = {})
171
+ self.class
172
+ .select(%Q(ST_Area(ST_Intersection(ST_Union(features_for.geog_lowres::geometry), ST_Union(features_for_other.geog_lowres::geometry))::geography) AS intersection_area_in_square_meters))
173
+ .joins_features_for(klass)
174
+ .where(:id => self.id)
175
+ .where('ST_DWithin(features_for.geog_lowres, features_for_other.geog_lowres, 0)')
176
+ .group("#{self.class.table_name}.id")
177
+ .first
178
+ .try(:intersection_area_in_square_meters) || 0
179
+ end
158
180
 
159
- return buffer_in_meters <= cache.intersection_cache_distance
160
- else
161
- return false
162
- end
163
- end
181
+ def total_intersection_area_in_hectares(klass)
182
+ Formatters::HECTARES.call(total_intersection_area_in_square_meters(klass))
183
+ end
184
+
185
+ def total_intersection_area_percentage(klass)
186
+ return 0.0 unless features_area_in_square_meters > 0
187
+
188
+ ((total_intersection_area_in_square_meters(klass) / features_area_in_square_meters) * 100).round(1)
189
+ end
190
+
191
+ def features_area_in_square_meters
192
+ @features_area_in_square_meters ||= features.sum('features.area')
193
+ end
194
+
195
+ def features_area_in_hectares
196
+ Formatters::HECTARES.call(features_area_in_square_meters)
197
+ end
164
198
  end
165
199
  end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "1.3.10"
2
+ VERSION = "1.4.0"
3
3
  end
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: 1.3.10
4
+ version: 1.4.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: 2015-07-29 00:00:00.000000000 Z
12
+ date: 2015-12-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
138
  version: '0'
139
139
  requirements: []
140
140
  rubyforge_project:
141
- rubygems_version: 2.4.7
141
+ rubygems_version: 2.4.6
142
142
  signing_key:
143
143
  specification_version: 4
144
144
  summary: Adds spatial methods to a model.