spatial_features 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -2
- data/app/models/abstract_feature.rb +4 -4
- data/app/models/spatial_proximity.rb +28 -4
- data/lib/spatial_features/caching.rb +6 -4
- data/lib/spatial_features/download.rb +3 -3
- data/lib/spatial_features/has_spatial_features/feature_import.rb +2 -2
- data/lib/spatial_features/has_spatial_features.rb +10 -9
- data/lib/spatial_features/importers/file.rb +4 -4
- data/lib/spatial_features/importers/geomark.rb +2 -2
- data/lib/spatial_features/importers/kml.rb +3 -2
- data/lib/spatial_features/importers/kml_file_arcgis.rb +1 -1
- data/lib/spatial_features/importers/shapefile.rb +1 -1
- data/lib/spatial_features/venn_polygons.rb +1 -1
- data/lib/spatial_features/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fabdd3f0f73eb21327f8e3deb1b7dda5969d97bdb7c053c60199a63fe5cd04b1
|
4
|
+
data.tar.gz: 777c5dc188ba968ab4cba25e58fa2f361c9d3814781ecfb9e751cf0fc1b40b0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3f28e99e814f1fcc2ba49edf8a59ff957b9f92c9e0d990f93409c61f00d4c496e7ac5f60eb3616660df720c615033f62dbe02d707c1584cc4cee7d9cb88067c
|
7
|
+
data.tar.gz: 18e776f2d1bc792e7a0313d5742345e93adca24f060472bb10a36611eb3e62cf7fe8fcf2d7d520db16cd9b1755e85b4e40e75cabcb4110e25c397e42087fd9e0
|
data/README.md
CHANGED
@@ -31,7 +31,6 @@ Adds spatial methods to a model.
|
|
31
31
|
feature_type character varying(255),
|
32
32
|
geog geography,
|
33
33
|
geom geometry(Geometry,4326),
|
34
|
-
geom_lowres geometry(Geometry,4326),
|
35
34
|
tilegeom geometry(Geometry,3857),
|
36
35
|
metadata hstore,
|
37
36
|
area double precision,
|
@@ -50,7 +49,6 @@ Adds spatial methods to a model.
|
|
50
49
|
CREATE INDEX index_features_on_feature_type ON features USING btree (feature_type);
|
51
50
|
CREATE INDEX index_features_on_spatial_model_id_and_spatial_model_type ON features USING btree (spatial_model_id, spatial_model_type);
|
52
51
|
CREATE INDEX index_features_on_geom ON features USING gist (geom);
|
53
|
-
CREATE INDEX index_features_on_geom_lowres ON features USING gist (geom_lowres);
|
54
52
|
CREATE INDEX index_features_on_tilegeom ON features USING gist (tilegeom);
|
55
53
|
|
56
54
|
CREATE TABLE spatial_caches (
|
@@ -182,6 +180,14 @@ add_index :features, :tilegeom, :using => :gist
|
|
182
180
|
Feature.update_all('tilegeom = ST_Transform(geom, 3857)')
|
183
181
|
```
|
184
182
|
|
183
|
+
## Upgrading From 3.0/3.1 to 3.2
|
184
|
+
SpatialProximity now expects the `model_a` and `model_b` records to be decided based on the name of the record type so
|
185
|
+
queries can be optimized. Migrate existing SpatialProximity rows to this new scheme by running the code below.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
SpatialProximity.normalize
|
189
|
+
```
|
190
|
+
|
185
191
|
## Testing
|
186
192
|
|
187
193
|
Create a postgres database:
|
@@ -69,11 +69,11 @@ class AbstractFeature < ActiveRecord::Base
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def self.intersecting(other)
|
72
|
-
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').
|
72
|
+
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').distinct
|
73
73
|
end
|
74
74
|
|
75
75
|
def self.within_distance(other, distance_in_meters)
|
76
|
-
join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).
|
76
|
+
join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).distinct
|
77
77
|
end
|
78
78
|
|
79
79
|
def self.invalid(column = 'geog::geometry')
|
@@ -126,8 +126,8 @@ class AbstractFeature < ActiveRecord::Base
|
|
126
126
|
SQL
|
127
127
|
end
|
128
128
|
|
129
|
-
def self.mvt(*args)
|
130
|
-
select_sql = mvt_sql(*args)
|
129
|
+
def self.mvt(*args, **kwargs)
|
130
|
+
select_sql = mvt_sql(*args, **kwargs)
|
131
131
|
|
132
132
|
# Result is a hex string representing the desired binary output so we need to convert it to binary
|
133
133
|
result = SpatialFeatures::Utils.select_db_value(select_sql)
|
@@ -2,10 +2,34 @@ class SpatialProximity < ActiveRecord::Base
|
|
2
2
|
belongs_to :model_a, :polymorphic => true
|
3
3
|
belongs_to :model_b, :polymorphic => true
|
4
4
|
|
5
|
-
def self.between(
|
6
|
-
where
|
7
|
-
(#{SpatialFeatures::Utils.polymorphic_condition(
|
8
|
-
(#{SpatialFeatures::Utils.polymorphic_condition(scope_2, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope_1, 'model_b')})
|
5
|
+
def self.between(scope1, scope2)
|
6
|
+
where condition_sql(scope1, scope2, <<~SQL.squish)
|
7
|
+
(#{SpatialFeatures::Utils.polymorphic_condition(scope1, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope2, 'model_b')})
|
9
8
|
SQL
|
10
9
|
end
|
10
|
+
|
11
|
+
def self.condition_sql(scope1, scope2, template, pattern_a = 'model_a', pattern_b = 'model_b')
|
12
|
+
scope1_type = SpatialFeatures::Utils.base_class_of(scope1).to_s
|
13
|
+
scope2_type = SpatialFeatures::Utils.base_class_of(scope2).to_s
|
14
|
+
|
15
|
+
if scope1_type < scope2_type
|
16
|
+
template
|
17
|
+
elsif scope1_type > scope2_type
|
18
|
+
template.gsub(pattern_a, 'model_c').gsub(pattern_b, pattern_a).gsub('model_c', pattern_b)
|
19
|
+
else
|
20
|
+
<<~SQL.squish
|
21
|
+
(#{template}) OR (#{template.gsub(pattern_a, 'model_c').gsub(pattern_b, pattern_a).gsub('model_c', pattern_b)})
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ensure the 'earliest' model is always model a
|
27
|
+
def self.normalize
|
28
|
+
unnormalized
|
29
|
+
.update_all('model_a_type = model_b_type, model_b_type = model_a_type, model_a_id = model_b_id, model_b_id = model_a_id')
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.unnormalized
|
33
|
+
where('model_a_type > model_b_type OR (model_a_type = model_b_type AND model_a_id > model_b_id)')
|
34
|
+
end
|
11
35
|
end
|
@@ -84,11 +84,13 @@ module SpatialFeatures
|
|
84
84
|
results.each do |id, distance, area|
|
85
85
|
klass_record.id = id
|
86
86
|
SpatialProximity.create! do |proximity|
|
87
|
+
# Always make the spatial model earliest type and id be model a so we can optimize queries
|
88
|
+
data = [[Utils.base_class(record).to_s, record.id], [Utils.base_class(klass_record).to_s, klass_record.id]]
|
89
|
+
data.sort!
|
90
|
+
|
87
91
|
# Set id and type instead of model to avoid autosaving the klass_record
|
88
|
-
proximity.model_a_id =
|
89
|
-
proximity.
|
90
|
-
proximity.model_b_id = klass_record.id
|
91
|
-
proximity.model_b_type = Utils.base_class(klass_record)
|
92
|
+
proximity.model_a_type, proximity.model_a_id = data.first
|
93
|
+
proximity.model_b_type, proximity.model_b_id = data.second
|
92
94
|
proximity.distance_in_meters = distance
|
93
95
|
proximity.intersection_area_in_square_meters = area
|
94
96
|
end
|
@@ -4,14 +4,14 @@ module SpatialFeatures
|
|
4
4
|
module Download
|
5
5
|
# file can be a url, path, or file, any of which can return be a zipped archive
|
6
6
|
def self.open(file)
|
7
|
-
file =
|
7
|
+
file = URI.open(file)
|
8
8
|
file = normalize_file(file) if file.is_a?(StringIO)
|
9
9
|
return file
|
10
10
|
end
|
11
11
|
|
12
12
|
# file can be a url, path, or file, any of which can return be a zipped archive
|
13
|
-
def self.open_each(
|
14
|
-
file = Download.open(
|
13
|
+
def self.open_each(path_or_url, unzip: nil, **unzip_options)
|
14
|
+
file = Download.open(path_or_url)
|
15
15
|
files = if unzip && Unzip.is_zip?(file)
|
16
16
|
find_in_zip(file, find: unzip, **unzip_options)
|
17
17
|
else
|
@@ -41,9 +41,9 @@ module SpatialFeatures
|
|
41
41
|
update_spatial_cache(options.slice(:spatial_cache))
|
42
42
|
end
|
43
43
|
end
|
44
|
-
|
45
|
-
return true
|
46
44
|
end
|
45
|
+
|
46
|
+
return true
|
47
47
|
rescue StandardError => e
|
48
48
|
if skip_invalid
|
49
49
|
Rails.logger.warn "Error updating #{self.class} #{self.id}. #{e.message}"
|
@@ -13,14 +13,14 @@ module SpatialFeatures
|
|
13
13
|
has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
|
14
14
|
has_one :aggregate_feature, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete
|
15
15
|
|
16
|
-
scope :with_features, lambda { joins(:features).
|
16
|
+
scope :with_features, lambda { joins(:features).distinct }
|
17
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") }
|
18
18
|
scope :include_bounds, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :north, :east, :south, :west) }
|
19
19
|
scope :include_area, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :area) }
|
20
20
|
|
21
|
-
scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).
|
21
|
+
scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).distinct }
|
22
22
|
scope :without_spatial_cache, lambda {|klass| joins("LEFT OUTER JOIN #{SpatialCache.table_name} ON #{SpatialCache.table_name}.spatial_model_id = #{table_name}.id AND #{SpatialCache.table_name}.spatial_model_type = '#{Utils.base_class(name)}' and intersection_model_type IN ('#{Utils.class_name_with_ancestors(klass).join("','") }')").where("#{SpatialCache.table_name}.spatial_model_id IS NULL") }
|
23
|
-
scope :with_stale_spatial_cache, lambda { joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").
|
23
|
+
scope :with_stale_spatial_cache, lambda { has_spatial_features_hash? ? joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").distinct : none }
|
24
24
|
|
25
25
|
has_many :spatial_caches, :as => :spatial_model, :dependent => :delete_all, :class_name => 'SpatialCache'
|
26
26
|
has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
|
@@ -136,10 +136,8 @@ module SpatialFeatures
|
|
136
136
|
other_class = Utils.base_class_of(other)
|
137
137
|
self_class = Utils.base_class_of(self)
|
138
138
|
|
139
|
-
joins <<~SQL
|
140
|
-
|
141
|
-
ON (spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
|
142
|
-
OR (spatial_proximities.model_b_type = '#{self_class}' AND spatial_proximities.model_b_id = #{table_name}.id AND spatial_proximities.model_a_type = '#{other_class}' AND spatial_proximities.model_a_id IN (#{Utils.id_sql(other)}))
|
139
|
+
joins "INNER JOIN spatial_proximities ON " + SpatialProximity.condition_sql(self, other, <<~SQL.squish)
|
140
|
+
(spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
|
143
141
|
SQL
|
144
142
|
end
|
145
143
|
|
@@ -228,9 +226,12 @@ module SpatialFeatures
|
|
228
226
|
end
|
229
227
|
|
230
228
|
def total_intersection_area_percentage(klass)
|
231
|
-
|
229
|
+
total_area = features_area_in_square_meters
|
232
230
|
|
233
|
-
|
231
|
+
return if total_area.nil?
|
232
|
+
return 0.0 if total_area.zero?
|
233
|
+
|
234
|
+
((total_intersection_area_in_square_meters(klass) / total_area) * 100).round(1)
|
234
235
|
end
|
235
236
|
|
236
237
|
def features_area_in_square_meters
|
@@ -21,7 +21,7 @@ module SpatialFeatures
|
|
21
21
|
# processed.
|
22
22
|
#
|
23
23
|
# If no `current_file` is passed then we just take the first valid file that we find.
|
24
|
-
def initialize(data,
|
24
|
+
def initialize(data, current_file: nil, **options)
|
25
25
|
begin
|
26
26
|
current_file ||= Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).first
|
27
27
|
rescue Unzip::PathNotFound
|
@@ -30,11 +30,11 @@ module SpatialFeatures
|
|
30
30
|
|
31
31
|
case ::File.extname(current_file.path.downcase)
|
32
32
|
when '.kml'
|
33
|
-
__setobj__(KMLFile.new(current_file,
|
33
|
+
__setobj__(KMLFile.new(current_file, **options))
|
34
34
|
when '.shp'
|
35
|
-
__setobj__(Shapefile.new(current_file,
|
35
|
+
__setobj__(Shapefile.new(current_file, **options))
|
36
36
|
when '.json', '.geojson'
|
37
|
-
__setobj__(ESRIGeoJSON.new(current_file.path,
|
37
|
+
__setobj__(ESRIGeoJSON.new(current_file.path, **options))
|
38
38
|
else
|
39
39
|
raise ImportError, "Could not import file. " + SUPPORTED_FORMATS
|
40
40
|
end
|
@@ -6,9 +6,9 @@ module SpatialFeatures
|
|
6
6
|
# <SimpleData name> keys that may contain <img> tags
|
7
7
|
IMAGE_METADATA_KEYS = %w[pdfmaps_photos].freeze
|
8
8
|
|
9
|
-
def initialize(data, base_dir: nil, **
|
9
|
+
def initialize(data, base_dir: nil, **options)
|
10
10
|
@base_dir = base_dir
|
11
|
-
super data, **
|
11
|
+
super data, **options
|
12
12
|
end
|
13
13
|
|
14
14
|
private
|
@@ -39,6 +39,7 @@ module SpatialFeatures
|
|
39
39
|
@kml_document ||= begin
|
40
40
|
doc = Nokogiri::XML(@data)
|
41
41
|
raise ImportError, "Invalid KML document (root node was '#{doc.root&.name}')" unless doc.root&.name.to_s.casecmp?('kml')
|
42
|
+
raise ImportError, "NetworkLink elements are not supported" unless doc.search('NetworkLink').empty?
|
42
43
|
doc
|
43
44
|
end
|
44
45
|
end
|
@@ -56,7 +56,7 @@ module SpatialFeatures
|
|
56
56
|
# Instantiate objects to hold the kml and records for each venn polygon
|
57
57
|
polygons.group_by{|row| row['kml']}.collect do |kml, rows|
|
58
58
|
# Uniq on row id in case a single record had self intersecting multi geometry, which would cause it to appear duplicated on a single venn polygon
|
59
|
-
records = rows.uniq{|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
|
59
|
+
records = rows.uniq {|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
|
60
60
|
OpenStruct.new(:kml => kml, :records => records)
|
61
61
|
end
|
62
62
|
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: 3.
|
4
|
+
version: 3.2.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:
|
12
|
+
date: 2023-08-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -203,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
203
|
- !ruby/object:Gem::Version
|
204
204
|
version: '0'
|
205
205
|
requirements: []
|
206
|
-
rubygems_version: 3.
|
206
|
+
rubygems_version: 3.3.23
|
207
207
|
signing_key:
|
208
208
|
specification_version: 4
|
209
209
|
summary: Adds spatial methods to a model.
|