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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 86ef03aa25c8f1b883c2dfc313ca200eb26f22b0
4
- data.tar.gz: 25b5c3887d7b9cf1cb560d7b0baf38cc4fdd6f73
3
+ metadata.gz: 1def32770d636b7b99b5922ad45b94476ee8297b
4
+ data.tar.gz: ee9cde1574fbb7b24011ddc0a92c97ae12843214
5
5
  SHA512:
6
- metadata.gz: c437c6f85a9bf15ecd88637d09a348a92498a9e450a4885731e4daedff1c04fbcd626a9ffd4e6ac1430da612b794fc4b8f5ecff62a7c1039c295bab8f39ddbf7
7
- data.tar.gz: 503b88e79c31b67c0069b5cb1346b26879f782227234e82e4bdcfc0b080ea920418767a333b6a1f5b9d30f43375bf3e5a666e0c2915505d8fbf38097e486839e
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
- ## Migration
6
- ```ruby
7
- execute("
8
- CREATE TABLE features (
9
- id integer NOT NULL,
10
- spatial_model_type character varying(255),
11
- spatial_model_id integer,
12
- name character varying(255),
13
- feature_type character varying(255),
14
- geog geography,
15
- geom geometry(Geometry,3005),
16
- geom_lowres geometry(Geometry,3005),
17
- kml text,
18
- kml_lowres text,
19
- metadata hstore,
20
- area double precision,
21
- north numeric(9,6),
22
- east numeric(9,6),
23
- south numeric(9,6),
24
- west numeric(9,6),
25
- centroid geography,
26
- kml_centroid text
27
- );
28
-
29
- CREATE TABLE spatial_caches (
30
- id integer NOT NULL,
31
- intersection_model_type character varying(255),
32
- spatial_model_type character varying(255),
33
- spatial_model_id integer,
34
- created_at timestamp without time zone,
35
- updated_at timestamp without time zone,
36
- intersection_cache_distance double precision,
37
- features_hash character varying(255)
38
- );
39
-
40
- CREATE TABLE spatial_proximities (
41
- id integer NOT NULL,
42
- model_a_type character varying(255),
43
- model_a_id integer,
44
- model_b_type character varying(255),
45
- model_b_id integer,
46
- distance_in_meters double precision,
47
- intersection_area_in_square_meters double precision
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
 
@@ -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('features.*, ST_IsValidReason(geog::geometry) AS invalid_geometry_message').where.not('ST_IsValid(geog::geometry)')
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 = joins_features_for(other).select("#{table_name}.*")
131
- scope = scope.where('ST_Intersects(features.geom, features_for_other.geom)') if buffer_in_meters == 0 # Optimize the 0 buffer case, ST_DWithin was slower in testing
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, features_for_other.geom)) AS distance_in_meters") if options[:distance]
142
- scope = scope.select("ST_Area(ST_UNION(ST_Intersection(features.geom, features_for_other.geom))) AS intersection_area_in_square_meters") if options[:intersection_area]
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
- def cached_spatial_join(other)
147
- other_class = class_for(other)
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
- raise "Cannot use cached spatial join for the same class" if self == other_class
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
- other_column = other_class.name < self.name ? :model_a : :model_b
152
- self_column = other_column == :model_a ? :model_b : :model_a
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
- 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)})")
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).reverse_merge(:import => {})
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
- "SpatialFeatures::Importers::#{importer_name}".constantize.new(data, :make_valid => make_valid) if data.present?
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
- `gdalsrsinfo "#{file.path}" -o proj4`[/'(.+)'/,1] # Sanitize "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n"
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)
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.1.7"
2
+ VERSION = "2.2.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: 2.1.7
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-11-30 00:00:00.000000000 Z
12
+ date: 2016-12-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails