spatial_features 2.13.0 → 2.15.1

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
  SHA256:
3
- metadata.gz: 88c320989e3fc2fa794540eceda94ca135c83992bdb8d95beae96cb23547253b
4
- data.tar.gz: '08acd270a048acc72c637876d36f7d4d39b4270f47f02f8cd51d80a834348c07'
3
+ metadata.gz: 32a3f5efdd999a1b4e229e6f9e270ef62711345939d94b1cb2c86e1740f5ec26
4
+ data.tar.gz: 90cf9b26c0d1816e1a65b7fc499827fdbb83d26f83f9a80ce13d48818832170c
5
5
  SHA512:
6
- metadata.gz: 4597a17999f3503d80ae00d40072c9335a9a0141fc277f952328a868b3d339cdcc236929b2bdc914f42eafe1229cde317202eb7064cab956d467257747d44152
7
- data.tar.gz: e859fc1b6fb70a1f2ad453d5e8017799f2d7b6c9212fd04f8abaf3c4c7958e7383ebd71b23fd51accb9f5ed168955afcfa49ae20b5d0486b44f0580a907a2586
6
+ metadata.gz: 00ba9b1b12ca3858f5f9ff44c5a4cbb5b1275248df062b2a608f24cf83ff0495ac811074bfa1b4e88d3fdacf0b83c09462c72c003f78a25ab30d212f7fef5ec4
7
+ data.tar.gz: 26c67a7e211df58ff05e20cb3b33dbf2c98dc938e0263a000ab2b744a4c71581bef751fd4871d44bc3068f575a301b487074469cbdae32c457bb122341c21358
@@ -1,6 +1,9 @@
1
1
  class AbstractFeature < ActiveRecord::Base
2
2
  self.table_name = 'features'
3
3
 
4
+ class_attribute :automatically_cache_derivatives
5
+ self.automatically_cache_derivatives = true
6
+
4
7
  class_attribute :lowres_simplification
5
8
  self.lowres_simplification = 2 # Threshold in meters
6
9
 
@@ -12,15 +15,21 @@ class AbstractFeature < ActiveRecord::Base
12
15
 
13
16
  before_validation :sanitize_feature_type
14
17
  validates_presence_of :geog
15
- validate :validate_geometry
16
- before_save :sanitize
17
- after_save :cache_derivatives, :if => :saved_change_to_geog?
18
+ validate :validate_geometry, if: :will_save_change_to_geog?
19
+ before_save :sanitize, if: :will_save_change_to_geog?
20
+ after_save :cache_derivatives, :if => [:automatically_cache_derivatives?, :saved_change_to_geog?]
18
21
 
19
22
  def self.cache_key
20
23
  result = connection.select_one(all.select('max(id) AS max, count(*) AS count').to_sql)
21
24
  "#{result['max']}-#{result['count']}"
22
25
  end
23
26
 
27
+ # for Rails >= 5 ActiveRecord collections we override the collection_cache_key
28
+ # to prevent Rails doing its default query on `updated_at`
29
+ def self.collection_cache_key(_collection, _timestamp_column)
30
+ self.cache_key
31
+ end
32
+
24
33
  def self.with_metadata(k, v)
25
34
  if k.present? && v.present?
26
35
  where('metadata->? = ?', k, v)
@@ -82,6 +91,14 @@ class AbstractFeature < ActiveRecord::Base
82
91
  return envelope_json.values_at(0,2)
83
92
  end
84
93
 
94
+ def self.without_caching_derivatives(&block)
95
+ old = automatically_cache_derivatives
96
+ self.automatically_cache_derivatives = false
97
+ block.call
98
+ ensure
99
+ self.automatically_cache_derivatives = old
100
+ end
101
+
85
102
  def self.cache_derivatives(options = {})
86
103
  update_all <<-SQL.squish
87
104
  geom = ST_Transform(geog::geometry, #{detect_srid('geom')}),
@@ -106,10 +123,10 @@ class AbstractFeature < ActiveRecord::Base
106
123
  SQL
107
124
  end
108
125
 
109
- def self.geojson(lowres: false, precision: 6, properties: {}, srid: 4326) # default srid is 4326 so output is Google Maps compatible
126
+ def self.geojson(lowres: false, precision: 6, properties: true, srid: 4326) # default srid is 4326 so output is Google Maps compatible
110
127
  column = lowres ? "ST_Transform(geom_lowres, #{srid})" : 'geog'
111
- properties_sql = <<~SQL if properties.present?
112
- , 'properties', json_build_object(#{properties.map {|k,v| "'#{k}',#{v}" }.join(',') })
128
+ properties_sql = <<~SQL if properties
129
+ , 'properties', hstore_to_json(metadata)
113
130
  SQL
114
131
 
115
132
  sql = <<~SQL
@@ -132,7 +149,7 @@ class AbstractFeature < ActiveRecord::Base
132
149
  end
133
150
 
134
151
  def cache_derivatives(*args)
135
- self.class.where(:id => self.id).cache_derivatives(*args)
152
+ self.class.default_scoped.where(:id => self.id).cache_derivatives(*args)
136
153
  end
137
154
 
138
155
  def kml(options = {})
@@ -11,6 +11,16 @@ class Feature < AbstractFeature
11
11
 
12
12
  after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
13
13
 
14
+ # Features are used for display so we also cache their KML representation
15
+ def self.cache_derivatives(options = {})
16
+ super
17
+ update_all <<-SQL.squish
18
+ kml = ST_AsKML(geog, 6),
19
+ kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
20
+ kml_centroid = ST_AsKML(centroid)
21
+ SQL
22
+ end
23
+
14
24
  def self.defer_aggregate_refresh(&block)
15
25
  start_at = Feature.maximum(:id).to_i + 1
16
26
  output = without_aggregate_refresh(&block)
@@ -30,9 +40,10 @@ class Feature < AbstractFeature
30
40
 
31
41
  def self.refresh_aggregates
32
42
  # Find one feature from each spatial model and trigger the aggregate feature refresh
33
- ids = select('MAX(id)')
34
- .where.not(:spatial_model_type => nil, :spatial_model_id => nil)
43
+ ids = where.not(:spatial_model_type => nil)
44
+ .where.not(:spatial_model_id => nil)
35
45
  .group('spatial_model_type, spatial_model_id')
46
+ .pluck('MAX(id)')
36
47
 
37
48
  # Unscope so that newly built AggregateFeatures get their type column set correctly
38
49
  AbstractFeature.unscoped { where(:id => ids).find_each(&:refresh_aggregate) }
@@ -49,14 +60,4 @@ class Feature < AbstractFeature
49
60
  # this field blank.
50
61
  spatial_model_id? && automatically_refresh_aggregate && saved_change_to_geog?
51
62
  end
52
-
53
- # Features are used for display so we also cache their KML representation
54
- def self.cache_derivatives(options = {})
55
- super
56
- update_all <<-SQL.squish
57
- kml = ST_AsKML(geog, 6),
58
- kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
59
- kml_centroid = ST_AsKML(centroid)
60
- SQL
61
- end
62
63
  end
@@ -3,9 +3,6 @@ require 'delayed_job_active_record'
3
3
  require 'rgeo/shapefile'
4
4
  require 'nokogiri'
5
5
  require 'zip'
6
- require 'googleauth'
7
- require 'google/apis/fusiontables_v2'
8
- require 'google/apis/drive_v3'
9
6
 
10
7
  # LIB
11
8
  require 'spatial_features/caching'
@@ -15,16 +12,12 @@ require 'spatial_features/controller_helpers/spatial_extensions'
15
12
  require 'spatial_features/download'
16
13
  require 'spatial_features/unzip'
17
14
  require 'spatial_features/utils'
15
+ require 'spatial_features/validation'
18
16
 
19
17
  require 'spatial_features/has_spatial_features'
20
18
  require 'spatial_features/has_spatial_features/queued_spatial_processing'
21
19
  require 'spatial_features/has_spatial_features/feature_import'
22
20
 
23
- require 'spatial_features/has_fusion_table_features'
24
- require 'spatial_features/has_fusion_table_features/api'
25
- require 'spatial_features/has_fusion_table_features/configuration'
26
- require 'spatial_features/has_fusion_table_features/service'
27
-
28
21
  require 'spatial_features/importers/base'
29
22
  require 'spatial_features/importers/file'
30
23
  require 'spatial_features/importers/kml'
@@ -38,7 +31,6 @@ require 'spatial_features/engine'
38
31
 
39
32
  # Load the act method
40
33
  ActiveRecord::Base.send :extend, SpatialFeatures::ActMethod
41
- ActiveRecord::Base.send :extend, SpatialFeatures::FusionTables::ActMethod
42
34
 
43
35
  # Suppress date warnings when unzipping KMZ saved by Google Earth, see https://github.com/rubyzip/rubyzip/issues/112
44
36
  Zip.warn_invalid_date = false
@@ -6,8 +6,6 @@ module SpatialExtensions
6
6
  model.failed_feature_update_jobs.destroy_all
7
7
  model.delay_update_features!
8
8
  end
9
-
10
- redirect_to :back
11
9
  end
12
10
 
13
11
  def abstract_proximity_action(scope, target, distance, &block)
@@ -12,7 +12,7 @@ module SpatialFeatures
12
12
  def self.open(file, unzip: nil, **unzip_options)
13
13
  file = Kernel.open(file)
14
14
  file = normalize_file(file) if file.is_a?(StringIO)
15
- if Unzip.is_zip?(file)
15
+ if unzip && Unzip.is_zip?(file)
16
16
  file = find_in_zip(file, find: unzip, **unzip_options)
17
17
  end
18
18
  return file
@@ -26,6 +26,12 @@ module SpatialFeatures
26
26
  end
27
27
  end
28
28
 
29
+ def self.entries(file)
30
+ file = Kernel.open(file)
31
+ file = normalize_file(file) if file.is_a?(StringIO)
32
+ Unzip.entries(file)
33
+ end
34
+
29
35
  def self.find_in_zip(file, find:, **unzip_options)
30
36
  return File.open(Unzip.paths(file, :find => find, **unzip_options))
31
37
  end
@@ -88,16 +88,16 @@ module SpatialFeatures
88
88
 
89
89
  # 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
90
90
  def has_spatial_features_hash?
91
- column_names.include? 'features_hash'
91
+ owner_class_has_loaded_column?('features_hash')
92
92
  end
93
93
 
94
94
  # Returns true if the model stores a cache of the features area
95
95
  def has_features_area?
96
- column_names.include? 'features_area'
96
+ owner_class_has_loaded_column?('features_area')
97
97
  end
98
98
 
99
99
  def area_in_square_meters
100
- features.area_in_square_meters
100
+ features.area
101
101
  end
102
102
 
103
103
  private
@@ -160,6 +160,12 @@ module SpatialFeatures
160
160
  scope = scope.where(:spatial_model_id => other) unless Utils.class_of(other) == other
161
161
  return scope
162
162
  end
163
+
164
+ def owner_class_has_loaded_column?(column_name)
165
+ return false unless connected?
166
+ return false unless table_exists?
167
+ column_names.include? column_name
168
+ end
163
169
  end
164
170
 
165
171
  module InstanceMethods
@@ -202,7 +208,7 @@ module SpatialFeatures
202
208
  end
203
209
 
204
210
  def features_area_in_square_meters
205
- @features_area_in_square_meters ||= features.area
211
+ aggregate_feature&.area
206
212
  end
207
213
 
208
214
  def total_intersection_area_in_square_meters(other)
@@ -79,14 +79,24 @@ module SpatialFeatures
79
79
  end
80
80
 
81
81
  def import_features(imports, skip_invalid)
82
- self.features.delete_all
82
+ features.delete_all
83
83
  valid, invalid = Feature.defer_aggregate_refresh do
84
- imports.flat_map(&:features).partition do |feature|
85
- feature.spatial_model = self
86
- feature.save
84
+ Feature.without_caching_derivatives do
85
+ imports.flat_map(&:features).partition do |feature|
86
+ feature.spatial_model = self
87
+ feature.save
88
+ end
87
89
  end
88
90
  end
89
91
 
92
+ if persisted?
93
+ features.reset # Reset the association cache because we've updated the features
94
+ features.cache_derivatives
95
+ else
96
+ self.features = valid # Assign the features so when we save this record we update the foreign key on the features
97
+ Feature.where(id: features).cache_derivatives
98
+ end
99
+
90
100
  errors = imports.flat_map(&:errors)
91
101
  invalid.each do |feature|
92
102
  errors << "Feature #{feature.name}: #{feature.errors.full_messages.to_sentence}"
@@ -98,7 +108,7 @@ module SpatialFeatures
98
108
  raise ImportError, "Error updating #{self.class} #{self.id}. #{errors.to_sentence}"
99
109
  end
100
110
 
101
- self.features = valid
111
+ return features
102
112
  end
103
113
 
104
114
  def features_cache_key_matches?(cache_key)
@@ -14,10 +14,14 @@ module SpatialFeatures
14
14
  else
15
15
  metadata = {}
16
16
  end
17
-
17
+
18
18
  next if blank_feature?(feature)
19
19
 
20
- yield OpenStruct.new(:feature_type => sql_type, :geog => geom_from_kml(feature), :name => name, :metadata => metadata)
20
+ geog = geom_from_kml(feature)
21
+
22
+ next if geog.blank?
23
+
24
+ yield OpenStruct.new(:feature_type => sql_type, :geog => geog, :name => name, :metadata => metadata)
21
25
  end
22
26
  end
23
27
  end
@@ -27,7 +31,16 @@ module SpatialFeatures
27
31
  end
28
32
 
29
33
  def geom_from_kml(kml)
30
- ActiveRecord::Base.connection.select_value("SELECT ST_GeomFromKML(#{ActiveRecord::Base.connection.quote(kml.to_s)})")
34
+ geom = nil
35
+
36
+ # Do query in a new thread so we use a new connection (if the query fails it will poison the transaction of the current connection)
37
+ Thread.new do
38
+ geom = ActiveRecord::Base.connection.select_value("SELECT ST_GeomFromKML(#{ActiveRecord::Base.connection.quote(kml.to_s)})")
39
+ rescue ActiveRecord::StatementInvalid => e # Discard Invalid KML features
40
+ geom = nil
41
+ end.join
42
+
43
+ return geom
31
44
  end
32
45
 
33
46
  def extract_metadata(placemark)
@@ -12,7 +12,7 @@ module SpatialFeatures
12
12
  end
13
13
 
14
14
  def cache_key
15
- @cache_key ||= Digest::MD5.hexdigest(features.to_json)
15
+ @cache_key ||= Digest::MD5.file(archive).to_s
16
16
  end
17
17
 
18
18
  private
@@ -49,13 +49,27 @@ module SpatialFeatures
49
49
  SQL
50
50
  end
51
51
 
52
+
52
53
  def file
53
- @file ||= Download.open(@data, unzip: /\.shp$/, downcase: true)
54
+ @file ||= begin
55
+ validate_file!
56
+ Download.open(archive, unzip: /\.shp$/, downcase: true)
57
+ end
58
+ end
59
+
60
+ def validate_file!
61
+ return unless Unzip.is_zip?(archive)
62
+ Validation.validate_shapefile_archive!(Download.entries(archive), default_proj4_projection: default_proj4_projection)
63
+ end
64
+
65
+ def archive
66
+ @archive ||= Download.open(@data)
54
67
  end
55
68
  end
56
69
 
57
70
  # ERRORS
58
71
  class IndeterminateShapefileProjection < SpatialFeatures::ImportError; end
59
72
  class IncompleteShapefileArchive < SpatialFeatures::ImportError; end
73
+ class InvalidShapefileArchive < SpatialFeatures::ImportError; end
60
74
  end
61
75
  end
@@ -0,0 +1,55 @@
1
+ module SpatialFeatures
2
+ module Validation
3
+ # SHP file must come first
4
+ REQUIRED_SHAPEFILE_COMPONENT_EXTENSIONS = %w[shp shx dbf prj].freeze
5
+
6
+ # Check if a shapefile archive includes the required component files, otherwise
7
+ # raise an exception.
8
+ #
9
+ # @param [Zip::File] zip_file A Zip::File object
10
+ # @param [String] default_proj4_projection Optional, if supplied we don't raise an exception when we're missing a .PRJ file
11
+ # @param [Boolean] allow_generic_zip_files When true, we skip validation entirely if the archive does not contain a .SHP file
12
+ def self.validate_shapefile_archive!(zip_file, default_proj4_projection: nil, allow_generic_zip_files: false)
13
+ zip_file_entries = zip_file.entries.each_with_object({}) do |f, obj|
14
+ ext = File.extname(f.name).downcase[1..-1]
15
+ next unless ext
16
+
17
+ if ext.casecmp?("shp") && obj.key?(ext)
18
+ raise ::SpatialFeatures::Importers::InvalidShapefileArchive, "Zip files that contain multiple Shapefiles are not supported. Please separate each Shapefile into its own zip file."
19
+ end
20
+
21
+ obj[ext] = File.basename(f.name, '.*')
22
+ end
23
+
24
+ shapefile_basename = zip_file_entries["shp"]
25
+ unless shapefile_basename
26
+ # not a shapefile archive but we don't care
27
+ return if allow_generic_zip_files
28
+
29
+ raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a SHP file"
30
+ end
31
+
32
+ REQUIRED_SHAPEFILE_COMPONENT_EXTENSIONS[1..-1].each do |ext|
33
+ ext_basename = zip_file_entries[ext]
34
+ next if ext_basename&.casecmp?(shapefile_basename)
35
+
36
+ case ext
37
+ when "prj"
38
+ # special case for missing projection files to allow using default_proj4_projection
39
+ next if default_proj4_projection
40
+
41
+ raise ::SpatialFeatures::Importers::IndeterminateShapefileProjection, "Shapefile archive is missing a projection file: #{expected_component_path(shapefile_basename, ext)}"
42
+ else
43
+ # for all un-handled cases of missing files raise the more generic error
44
+ raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a required file: #{expected_component_path(shapefile_basename, ext)}"
45
+ end
46
+ end
47
+
48
+ true
49
+ end
50
+
51
+ def self.expected_component_path(basename, ext)
52
+ "#{basename}.#{ext}"
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.13.0"
2
+ VERSION = "2.15.1"
3
3
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spatial_features
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.0
4
+ version: 2.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Wallace
8
8
  - Nicholas Jakobsen
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-09-08 00:00:00.000000000 Z
12
+ date: 2021-07-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -20,7 +20,7 @@ dependencies:
20
20
  version: '4.2'
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
- version: '6.0'
23
+ version: '7.0'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
@@ -30,7 +30,7 @@ dependencies:
30
30
  version: '4.2'
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '6.0'
33
+ version: '7.0'
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: delayed_job_active_record
36
36
  requirement: !ruby/object:Gem::Requirement
@@ -63,16 +63,16 @@ dependencies:
63
63
  name: rubyzip
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '1.1'
68
+ version: 1.0.0
69
69
  type: :runtime
70
70
  prerelease: false
71
71
  version_requirements: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '1.1'
75
+ version: 1.0.0
76
76
  - !ruby/object:Gem::Dependency
77
77
  name: nokogiri
78
78
  requirement: !ruby/object:Gem::Requirement
@@ -87,34 +87,6 @@ dependencies:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
89
  version: '1.6'
90
- - !ruby/object:Gem::Dependency
91
- name: googleauth
92
- requirement: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 0.5.1
97
- type: :runtime
98
- prerelease: false
99
- version_requirements: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.5.1
104
- - !ruby/object:Gem::Dependency
105
- name: google-api-client
106
- requirement: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '0.9'
111
- type: :runtime
112
- prerelease: false
113
- version_requirements: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '0.9'
118
90
  - !ruby/object:Gem::Dependency
119
91
  name: chroma
120
92
  requirement: !ruby/object:Gem::Requirement
@@ -135,14 +107,14 @@ dependencies:
135
107
  requirements:
136
108
  - - "~>"
137
109
  - !ruby/object:Gem::Version
138
- version: '0'
110
+ version: '1'
139
111
  type: :development
140
112
  prerelease: false
141
113
  version_requirements: !ruby/object:Gem::Requirement
142
114
  requirements:
143
115
  - - "~>"
144
116
  - !ruby/object:Gem::Version
145
- version: '0'
117
+ version: '1'
146
118
  - !ruby/object:Gem::Dependency
147
119
  name: rspec
148
120
  requirement: !ruby/object:Gem::Requirement
@@ -180,10 +152,6 @@ files:
180
152
  - lib/spatial_features/controller_helpers/spatial_extensions.rb
181
153
  - lib/spatial_features/download.rb
182
154
  - lib/spatial_features/engine.rb
183
- - lib/spatial_features/has_fusion_table_features.rb
184
- - lib/spatial_features/has_fusion_table_features/api.rb
185
- - lib/spatial_features/has_fusion_table_features/configuration.rb
186
- - lib/spatial_features/has_fusion_table_features/service.rb
187
155
  - lib/spatial_features/has_spatial_features.rb
188
156
  - lib/spatial_features/has_spatial_features/feature_import.rb
189
157
  - lib/spatial_features/has_spatial_features/queued_spatial_processing.rb
@@ -198,6 +166,7 @@ files:
198
166
  - lib/spatial_features/uncached_result.rb
199
167
  - lib/spatial_features/unzip.rb
200
168
  - lib/spatial_features/utils.rb
169
+ - lib/spatial_features/validation.rb
201
170
  - lib/spatial_features/venn_polygons.rb
202
171
  - lib/spatial_features/version.rb
203
172
  - lib/tasks/spatial_features_tasks.rake
@@ -205,7 +174,7 @@ homepage: https://github.com/culturecode/spatial_features
205
174
  licenses:
206
175
  - MIT
207
176
  metadata: {}
208
- post_install_message:
177
+ post_install_message:
209
178
  rdoc_options: []
210
179
  require_paths:
211
180
  - lib
@@ -220,8 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
189
  - !ruby/object:Gem::Version
221
190
  version: '0'
222
191
  requirements: []
223
- rubygems_version: 3.0.3
224
- signing_key:
192
+ rubygems_version: 3.0.8
193
+ signing_key:
225
194
  specification_version: 4
226
195
  summary: Adds spatial methods to a model.
227
196
  test_files: []
@@ -1,95 +0,0 @@
1
- module SpatialFeatures
2
- module FusionTables
3
- module ActMethod
4
- def has_fusion_table_features(options = {})
5
- class_attribute :fusion_table_features_options
6
- self.fusion_table_features_options = options
7
-
8
- after_update_features :expire_fusion_table
9
-
10
- include InstanceMethods
11
- extend ClassMethods
12
- end
13
- end
14
-
15
- module ClassMethods
16
- def to_fusion_condition
17
- sanitize_sql(["spatial_model_id IN (?)", pluck(:id)])
18
- end
19
-
20
- def update_fusion_tables(group_options = {})
21
- fusion_table_groups(group_options) do |fusion_table_id, records, group_features|
22
- API.set_features(fusion_table_id, group_features, :colour => fusion_table_features_options[:colour])
23
- end
24
- end
25
-
26
- def delete_fusion_tables(group_options = {})
27
- fusion_table_groups(group_options) do |fusion_table_id, records, group_features|
28
- API.delete_table(fusion_table_id)
29
- end
30
- @fusion_table_id_cache = nil
31
- end
32
-
33
- def acts_like_fusion_table_features?
34
- true
35
- end
36
-
37
- def fusion_table_id_cache
38
- @fusion_table_id_cache ||= Hash.new {|hash, table_name| hash[table_name] = API.find_or_create_table(table_name) }
39
- .merge(API.tables.collect {|table| [table.name, table.table_id] }.to_h) # Warm the cache
40
- end
41
-
42
- private
43
-
44
- def fusion_table_groups(only: [], except: [])
45
- groups = all.group_by(&:fusion_table_id)
46
- groups.select! {|fusion_table_id, _| Array.wrap(only).include?(fusion_table_id) } if only.present?
47
- groups.select! {|fusion_table_id, _| !Array.wrap(except).include?(fusion_table_id) } if except.present?
48
- groups.each do |fusion_table_id, records|
49
- yield fusion_table_id, records, features.where(:spatial_model_id => records)
50
- end
51
- end
52
- end
53
-
54
- module InstanceMethods
55
- def acts_like_fusion_table_features?
56
- true
57
- end
58
-
59
- def stale_fusion_table?
60
- @stale_fusion_table
61
- end
62
-
63
- def expire_fusion_table
64
- @stale_fusion_table = true
65
- end
66
-
67
- def update_fusion_table
68
- self.class.update_fusion_tables(only: fusion_table_id)
69
- end
70
-
71
- def delete_fusion_table
72
- self.class.delete_fusion_tables(only: fusion_table_id)
73
- end
74
-
75
- def fusion_table_id
76
- self.class.fusion_table_id_cache[fusion_table_name]
77
- end
78
-
79
- def fusion_table_name
80
- case fusion_table_features_options[:table_name]
81
- when Symbol
82
- send(fusion_table_features_options[:table_name])
83
- when String
84
- fusion_table_features_options[:table_name]
85
- else
86
- self.class.table_name
87
- end
88
- end
89
-
90
- def to_fusion_condition
91
- self.class.where(:id => self).to_fusion_condition
92
- end
93
- end
94
- end
95
- end
@@ -1,111 +0,0 @@
1
- module SpatialFeatures
2
- module FusionTables
3
- module API
4
- extend self
5
-
6
- FEATURE_COLUMNS = {
7
- :name => 'STRING',
8
- :spatial_model_type => 'STRING',
9
- :spatial_model_id => 'NUMBER',
10
- :kml_lowres => 'LOCATION',
11
- :colour => 'STRING',
12
- :metadata => 'STRING'
13
- }
14
- TABLE_STYLE = {
15
- :polygon_options => { :fill_color_styler => { :kind => 'fusiontables#fromColumn', :column_name => 'colour' },
16
- :stroke_color_styler => { :kind => 'fusiontables#fromColumn', :column_name => 'colour' },
17
- :stroke_weight => 1
18
- },
19
- :polyline_options => { :stroke_color_styler => { :kind => 'fusiontables#fromColumn', :column_name => 'colour'} }
20
- }
21
-
22
- TABLE_TEMPLATE = {
23
- :body => "<h3>{name}</h3>{metadata}"
24
- }
25
-
26
- def find_or_create_table(name)
27
- find_table(name) || create_table(name)
28
- end
29
-
30
- def create_table(name)
31
- table_id = service.create_table(name, FEATURE_COLUMNS.collect {|name, type| {:name => name, :type => type} })
32
- service.share_table(table_id)
33
- service.insert_style(table_id, TABLE_STYLE)
34
- service.insert_template(table_id, TABLE_TEMPLATE)
35
- return table_id
36
- end
37
-
38
- def find_table(name)
39
- service.tables.find {|table| table.name == name }.try(:table_id)
40
- end
41
-
42
- def delete_table(table_id)
43
- service.delete_table(table_id)
44
- end
45
-
46
- def tables
47
- service.tables
48
- end
49
-
50
- def set_features(table_id, features, colour: nil)
51
- service.replace_rows(table_id, features_to_csv(features, colour))
52
- end
53
-
54
- def set_style(table_id, style)
55
- service.style_ids(table_id).each do |style_id|
56
- service.delete_style(table_id, style_id)
57
- end
58
- service.insert_style(table_id, style)
59
- end
60
-
61
- def service
62
- @service ||= Service.new(Configuration.service_account_credentials)
63
- end
64
-
65
- private
66
-
67
- def features_to_csv(features, colour)
68
- ActiveRecord::Associations::Preloader.new.preload(features, :spatial_model) if colour.is_a?(Symbol)
69
-
70
- csv = CSV.generate do |csv|
71
- features.each do |feature|
72
- csv << FEATURE_COLUMNS.keys.collect do |attribute|
73
- case attribute
74
- when :colour
75
- render_feature_colour(feature, colour)
76
- when :metadata
77
- render_feature_metadata(feature)
78
- else
79
- feature.send(attribute)
80
- end
81
- end
82
- end
83
- end
84
-
85
- file = Tempfile.new('features')
86
- file.write(csv)
87
- return file
88
- end
89
-
90
- def render_feature_metadata(feature)
91
- feature.metadata.collect do |name, val|
92
- "<b>#{name}:</b> #{val}"
93
- end.join('<br/>')
94
- end
95
-
96
- def render_feature_colour(feature, colour)
97
- case colour
98
- when Symbol
99
- feature.spatial_model.send(colour)
100
- when Proc
101
- colour.call(feature)
102
- else
103
- colour
104
- end.paint.to_ft_hex
105
-
106
- rescue Chroma::Errors::UnrecognizedColor
107
- nil
108
- end
109
- end
110
- end
111
- end
@@ -1,15 +0,0 @@
1
- module SpatialFeatures
2
- module FusionTables
3
- def self.config
4
- if block_given?
5
- yield Configuration
6
- else
7
- Configuration
8
- end
9
- end
10
-
11
- module Configuration
12
- mattr_accessor :service_account_credentials
13
- end
14
- end
15
- end
@@ -1,104 +0,0 @@
1
- module SpatialFeatures
2
- module FusionTables
3
- class Service
4
- APPLICATION_NAME = 'Fusion Tables + Spatial Features'
5
- GOOGLE_AUTH_SCOPES = %w(https://www.googleapis.com/auth/fusiontables https://www.googleapis.com/auth/drive)
6
-
7
- def initialize(service_account_credentials_path)
8
- @authorization = get_authorization(service_account_credentials_path, GOOGLE_AUTH_SCOPES)
9
- end
10
-
11
- def table_ids
12
- tables.collect(&:table_id)
13
- end
14
-
15
- def tables
16
- fusion_tables_service.list_tables(max_results: 10000).items || []
17
- end
18
-
19
- def create_table(name, columns = [], table_options = {})
20
- table_object = {:name => name, :columns => columns, :is_exportable => true}.merge(table_options)
21
- fusion_tables_service.insert_table(table_object, :fields => 'table_id').table_id
22
- end
23
-
24
- def delete_table(table_id)
25
- fusion_tables_service.delete_table(table_id)
26
- end
27
-
28
- def style_ids(table_id)
29
- styles(table_id).collect(&:style_id)
30
- end
31
-
32
- def styles(table_id)
33
- fusion_tables_service.list_styles(table_id).items
34
- end
35
-
36
- def delete_style(table_id, style_id)
37
- fusion_tables_service.delete_style(table_id, style_id, :fields => nil)
38
- end
39
-
40
- def insert_style(table_id, style)
41
- style.reverse_merge! 'name' => 'default_table_style', 'isDefaultForTable' => true
42
- fusion_tables_service.insert_style(table_id, style, :fields => 'styleId')
43
- end
44
-
45
- def delete_template(table_id, template_id)
46
- fusion_tables_service.delete_template(table_id, template_id, :fields => nil)
47
- end
48
-
49
- def insert_template(table_id, template)
50
- template.reverse_merge! 'name' => 'default_table_template'
51
- fusion_tables_service.insert_template(table_id, template, :fields => 'templateId')
52
- end
53
-
54
- def delete_row(table_id, row_id)
55
- fusion_tables_service.sql_query("DELETE FROM #{table_id} WHERE ROWID = #{row_id}")
56
- end
57
-
58
- def row_ids(table_id, conditions = {})
59
- clause = conditions.collect {|column, value| ActiveRecord::Base.send(:sanitize_sql_array, ["? IN (?)", column, value]) }.join(' AND ')
60
- where = "WHERE #{clause}" if clause.present?
61
- return fusion_tables_service.sql_query_get("SELECT rowid FROM #{table_id} #{where}}").rows.flatten
62
- end
63
-
64
- # Process mutliple commands in a single HTTP request
65
- def bulk(&block)
66
- fusion_tables_service.batch do
67
- block.call(self)
68
- end
69
- end
70
-
71
- def replace_rows(table_id, csv)
72
- fusion_tables_service.replace_table_rows(table_id, :upload_source => csv, :options => {:open_timeout_sec => 1.hour})
73
- end
74
-
75
- def upload_rows(table_id, csv)
76
- fusion_tables_service.import_rows(table_id, :upload_source => csv, :options => {:open_timeout_sec => 1.hour})
77
- end
78
-
79
- def share_table(table_id)
80
- permission = {:type => 'anyone', :role => 'reader', :withLink => true}
81
- drive_service.create_permission(table_id, permission, :fields => 'id')
82
- end
83
-
84
- def fusion_tables_service
85
- @fusion_tables_service ||= Google::Apis::FusiontablesV2::FusiontablesService.new.tap do |service|
86
- service.client_options.application_name = APPLICATION_NAME
87
- service.authorization = @authorization
88
- end
89
- end
90
-
91
- def drive_service
92
- @drive_service ||= Google::Apis::DriveV3::DriveService.new.tap do |drive|
93
- drive.client_options.application_name = APPLICATION_NAME
94
- drive.authorization = @authorization
95
- end
96
- end
97
-
98
- def get_authorization(service_account_credentials_path, scopes)
99
- ENV['GOOGLE_APPLICATION_CREDENTIALS'] = service_account_credentials_path
100
- return Google::Auth.get_application_default(scopes)
101
- end
102
- end
103
- end
104
- end