spatial_features 2.1.7 → 2.2.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 +60 -46
- data/app/models/feature.rb +8 -3
- data/lib/spatial_features/has_spatial_features.rb +32 -37
- data/lib/spatial_features/has_spatial_features/feature_import.rb +7 -2
- data/lib/spatial_features/importers/shapefile.rb +2 -1
- data/lib/spatial_features/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1def32770d636b7b99b5922ad45b94476ee8297b
|
4
|
+
data.tar.gz: ee9cde1574fbb7b24011ddc0a92c97ae12843214
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd2474c0f6c3b0d4d888f9c3e1357072c71db5c5c610d0cbf56a4721879acc421a5ffa934f2fa4c471903c63bade65f5e41321f58c3404114e5b624e3b672e6a
|
7
|
+
data.tar.gz: aeb3d1fdd3bec3caebaf74067c059d8303cefb7d77d51bd08e4bb3078a8ef9628f7ba3b32d7e573217298a974a5e3f23c583d4231813922aeb1ae335986b632b
|
data/README.md
CHANGED
@@ -2,52 +2,66 @@
|
|
2
2
|
|
3
3
|
Adds spatial methods to a model.
|
4
4
|
|
5
|
-
##
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
1. Install libraries
|
8
|
+
- PostGIS
|
9
|
+
- libgeos and gdal (optional libraries required for Shapefile import)
|
10
|
+
|
11
|
+
```bash
|
12
|
+
# Ubuntu Installation instructions. Source: https://github.com/rgeo/rgeo/issues/26#issuecomment-106059741
|
13
|
+
sudo apt-get -y install libgeos-3.4.2 libgeos-dev libproj0 libproj-dev gdal-bin
|
14
|
+
sudo ln -s /usr/lib/libgeos-3.4.2.so /usr/lib/libgeos.so
|
15
|
+
sudo ln -s /usr/lib/libgeos-3.4.2.so /usr/lib/libgeos.so.1
|
16
|
+
```
|
17
|
+
|
18
|
+
2. Create spatial tables
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
execute("
|
22
|
+
CREATE TABLE features (
|
23
|
+
id integer NOT NULL,
|
24
|
+
spatial_model_type character varying(255),
|
25
|
+
spatial_model_id integer,
|
26
|
+
name character varying(255),
|
27
|
+
feature_type character varying(255),
|
28
|
+
geog geography,
|
29
|
+
geom geometry(Geometry,3005),
|
30
|
+
geom_lowres geometry(Geometry,3005),
|
31
|
+
kml text,
|
32
|
+
kml_lowres text,
|
33
|
+
metadata hstore,
|
34
|
+
area double precision,
|
35
|
+
north numeric(9,6),
|
36
|
+
east numeric(9,6),
|
37
|
+
south numeric(9,6),
|
38
|
+
west numeric(9,6),
|
39
|
+
centroid geography,
|
40
|
+
kml_centroid text
|
41
|
+
);
|
42
|
+
|
43
|
+
CREATE TABLE spatial_caches (
|
44
|
+
id integer NOT NULL,
|
45
|
+
intersection_model_type character varying(255),
|
46
|
+
spatial_model_type character varying(255),
|
47
|
+
spatial_model_id integer,
|
48
|
+
created_at timestamp without time zone,
|
49
|
+
updated_at timestamp without time zone,
|
50
|
+
intersection_cache_distance double precision,
|
51
|
+
features_hash character varying(255)
|
52
|
+
);
|
53
|
+
|
54
|
+
CREATE TABLE spatial_proximities (
|
55
|
+
id integer NOT NULL,
|
56
|
+
model_a_type character varying(255),
|
57
|
+
model_a_id integer,
|
58
|
+
model_b_type character varying(255),
|
59
|
+
model_b_id integer,
|
60
|
+
distance_in_meters double precision,
|
61
|
+
intersection_area_in_square_meters double precision
|
62
|
+
);
|
63
|
+
")
|
64
|
+
```
|
51
65
|
|
52
66
|
## Usage
|
53
67
|
|
data/app/models/feature.rb
CHANGED
@@ -48,8 +48,8 @@ class Feature < ActiveRecord::Base
|
|
48
48
|
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').uniq
|
49
49
|
end
|
50
50
|
|
51
|
-
def self.invalid
|
52
|
-
select(
|
51
|
+
def self.invalid(column = 'geog::geometry')
|
52
|
+
select("features.*, ST_IsValidReason(#{column}) AS invalid_geometry_message").where.not("ST_IsValid(#{column})")
|
53
53
|
end
|
54
54
|
|
55
55
|
def self.valid
|
@@ -78,6 +78,10 @@ class Feature < ActiveRecord::Base
|
|
78
78
|
centroid = ST_PointOnSurface(geog::geometry)
|
79
79
|
SQL
|
80
80
|
|
81
|
+
invalid('geom').update_all <<-SQL
|
82
|
+
geom = ST_Buffer(geom, 0)
|
83
|
+
SQL
|
84
|
+
|
81
85
|
update_all <<-SQL.squish
|
82
86
|
geom_lowres = ST_SimplifyPreserveTopology(geom, #{options[:lowres_simplification]})
|
83
87
|
SQL
|
@@ -118,8 +122,9 @@ class Feature < ActiveRecord::Base
|
|
118
122
|
self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Force2D('#{geog}')")
|
119
123
|
end
|
120
124
|
|
125
|
+
SRID_CACHE = {}
|
121
126
|
def self.detect_srid(column_name)
|
122
|
-
connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
|
127
|
+
SRID_CACHE[column_name] ||= connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
|
123
128
|
end
|
124
129
|
|
125
130
|
def self.join_other_features(other)
|
@@ -2,13 +2,13 @@ module SpatialFeatures
|
|
2
2
|
module ActMethod
|
3
3
|
def has_spatial_features(options = {})
|
4
4
|
unless acts_like?(:spatial_features)
|
5
|
+
class_attribute :spatial_features_options
|
6
|
+
self.spatial_features_options = {:make_valid => true}
|
7
|
+
|
5
8
|
extend ClassMethods
|
6
9
|
include InstanceMethods
|
7
10
|
include DelayedFeatureImport
|
8
11
|
|
9
|
-
class_attribute :spatial_features_options
|
10
|
-
self.spatial_features_options = {:make_valid => true}
|
11
|
-
|
12
12
|
has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
|
13
13
|
|
14
14
|
scope :with_features, lambda { joins(:features).uniq }
|
@@ -60,13 +60,6 @@ module SpatialFeatures
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
-
def covering(other)
|
64
|
-
scope = joins_features_for(other).select("#{table_name}.*").group("#{table_name}.#{primary_key}")
|
65
|
-
scope = scope.where('ST_Covers(features.geom, features_for_other.geom)')
|
66
|
-
|
67
|
-
return scope
|
68
|
-
end
|
69
|
-
|
70
63
|
def polygons
|
71
64
|
features.polygons
|
72
65
|
end
|
@@ -87,18 +80,6 @@ module SpatialFeatures
|
|
87
80
|
end
|
88
81
|
end
|
89
82
|
|
90
|
-
# Returns a scope that includes the features for this record as the table_alias and the features for other as #{table_alias}_other
|
91
|
-
# Can be used to perform spatial calculations on the relationship between the two sets of features
|
92
|
-
def joins_features_for(other, table_alias = 'features_for')
|
93
|
-
joins(:features).joins(%Q(INNER JOIN (#{other_features_union(other).to_sql}) AS "#{table_alias}_other" ON true))
|
94
|
-
end
|
95
|
-
|
96
|
-
def other_features_union(other)
|
97
|
-
scope = Feature.select('ST_Union(geom) AS geom').where(:spatial_model_type => class_for(other))
|
98
|
-
scope = scope.where(:spatial_model_id => other) unless class_for(other) == other
|
99
|
-
return scope
|
100
|
-
end
|
101
|
-
|
102
83
|
# 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
|
103
84
|
def has_spatial_features_hash?
|
104
85
|
column_names.include? 'features_hash'
|
@@ -126,10 +107,20 @@ module SpatialFeatures
|
|
126
107
|
return scope
|
127
108
|
end
|
128
109
|
|
110
|
+
def cached_spatial_join(other)
|
111
|
+
other_class = class_for(other)
|
112
|
+
|
113
|
+
raise "Cannot use cached spatial join for the same class" if self == other_class
|
114
|
+
|
115
|
+
other_column = other_class.name < self.name ? :model_a : :model_b
|
116
|
+
self_column = other_column == :model_a ? :model_b : :model_a
|
117
|
+
|
118
|
+
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)})")
|
119
|
+
end
|
120
|
+
|
129
121
|
def uncached_within_buffer_scope(other, buffer_in_meters, options)
|
130
|
-
scope =
|
131
|
-
scope = scope.
|
132
|
-
scope = scope.where('ST_DWithin(features.geom, features_for_other.geom, ?)', buffer_in_meters) if buffer_in_meters.to_f > 0
|
122
|
+
scope = spatial_join(other, buffer_in_meters)
|
123
|
+
scope = scope.select("#{table_name}.*")
|
133
124
|
|
134
125
|
# Ensure records with multiple features don't appear multiple times
|
135
126
|
if options[:distance] || options[:intersection_area]
|
@@ -138,20 +129,28 @@ module SpatialFeatures
|
|
138
129
|
scope = scope.distinct
|
139
130
|
end
|
140
131
|
|
141
|
-
scope = scope.select("MIN(ST_Distance(features.geom,
|
142
|
-
scope = scope.select("ST_Area(ST_UNION(ST_Intersection(features.geom,
|
132
|
+
scope = scope.select("MIN(ST_Distance(features.geom, other_features.geom)) AS distance_in_meters") if options[:distance]
|
133
|
+
scope = scope.select("ST_Area(ST_UNION(ST_Intersection(features.geom, other_features.geom))) AS intersection_area_in_square_meters") if options[:intersection_area]
|
143
134
|
return scope
|
144
135
|
end
|
145
136
|
|
146
|
-
|
147
|
-
|
137
|
+
# Returns a scope that includes the features for this record as the table_alias and the features for other as other_alias
|
138
|
+
# Performs a spatial intersection between the two sets of features, within the buffer distance given in meters
|
139
|
+
def spatial_join(other, buffer = 0, table_alias = 'features', other_alias = 'other_features', geom = 'geom_lowres')
|
140
|
+
scope = features_scope(self).select("#{geom} AS geom").select(:spatial_model_id)
|
148
141
|
|
149
|
-
|
142
|
+
other_scope = features_scope(other)
|
143
|
+
other_scope = other_scope.select("ST_Union(#{geom}) AS geom").select("ST_Buffer(ST_Union(#{geom}), #{buffer.to_i}) AS buffered_geom")
|
150
144
|
|
151
|
-
|
152
|
-
|
145
|
+
return joins(%Q(INNER JOIN (#{scope.to_sql}) AS #{table_alias} ON #{table_alias}.spatial_model_id = #{table_name}.id))
|
146
|
+
.joins(%Q(INNER JOIN (#{other_scope.to_sql}) AS #{other_alias} ON ST_Intersects(#{table_alias}.geom, #{other_alias}.buffered_geom)))
|
147
|
+
end
|
153
148
|
|
154
|
-
|
149
|
+
def features_scope(other)
|
150
|
+
scope = Feature
|
151
|
+
scope = scope.where(:spatial_model_type => class_for(other))
|
152
|
+
scope = scope.where(:spatial_model_id => other) unless class_for(other) == other
|
153
|
+
return scope
|
155
154
|
end
|
156
155
|
|
157
156
|
# Returns the class for the given, class, scope, or record
|
@@ -201,10 +200,6 @@ module SpatialFeatures
|
|
201
200
|
features.present?
|
202
201
|
end
|
203
202
|
|
204
|
-
def covers?(other)
|
205
|
-
self.class.covering(other).exists?(self)
|
206
|
-
end
|
207
|
-
|
208
203
|
def intersects?(other)
|
209
204
|
self.class.intersecting(other).exists?(self)
|
210
205
|
end
|
@@ -7,10 +7,11 @@ module SpatialFeatures
|
|
7
7
|
included do
|
8
8
|
extend ActiveModel::Callbacks
|
9
9
|
define_model_callbacks :update_features
|
10
|
+
spatial_features_options.reverse_merge!(:import => {})
|
10
11
|
end
|
11
12
|
|
12
13
|
def update_features!(skip_invalid: false, options: {})
|
13
|
-
options = options.reverse_merge(spatial_features_options)
|
14
|
+
options = options.reverse_merge(spatial_features_options)
|
14
15
|
|
15
16
|
ActiveRecord::Base.transaction do
|
16
17
|
imports = spatial_feature_imports(options[:import], options[:make_valid])
|
@@ -32,10 +33,14 @@ module SpatialFeatures
|
|
32
33
|
def spatial_feature_imports(import_options, make_valid)
|
33
34
|
import_options.collect do |data_method, importer_name|
|
34
35
|
data = send(data_method)
|
35
|
-
|
36
|
+
spatial_importer_from_name(importer_name).new(data, :make_valid => make_valid) if data.present?
|
36
37
|
end.compact
|
37
38
|
end
|
38
39
|
|
40
|
+
def spatial_importer_from_name(importer_name)
|
41
|
+
"SpatialFeatures::Importers::#{importer_name}".constantize
|
42
|
+
end
|
43
|
+
|
39
44
|
def import_features(imports, skip_invalid)
|
40
45
|
self.features.delete_all
|
41
46
|
valid, invalid = imports.flat_map(&:features).partition do |feature|
|
@@ -21,7 +21,8 @@ module SpatialFeatures
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def proj4_from_file(file)
|
24
|
-
|
24
|
+
# Sanitize "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n"
|
25
|
+
`gdalsrsinfo "#{file.path}" -o proj4`[/'(.+)'/,1].presence || raise('Could not determine shapefile projection. Check that `gdalsrsinfo` is installed.')
|
25
26
|
end
|
26
27
|
|
27
28
|
def data_from_wkt(wkt, proj4)
|
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.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: 2016-
|
12
|
+
date: 2016-12-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|