spatial_features 2.1.7 → 2.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 +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
|