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 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