spatial_features 2.12.2 → 2.15.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/app/models/abstract_feature.rb +21 -8
- data/app/models/feature.rb +13 -12
- data/lib/spatial_features.rb +1 -9
- data/lib/spatial_features/controller_helpers/spatial_extensions.rb +0 -2
- data/lib/spatial_features/download.rb +7 -1
- data/lib/spatial_features/has_spatial_features.rb +15 -5
- data/lib/spatial_features/has_spatial_features/feature_import.rb +15 -5
- data/lib/spatial_features/importers/kml.rb +16 -3
- data/lib/spatial_features/importers/shapefile.rb +16 -2
- data/lib/spatial_features/validation.rb +55 -0
- data/lib/spatial_features/version.rb +1 -1
- metadata +13 -44
- data/lib/spatial_features/has_fusion_table_features.rb +0 -95
- data/lib/spatial_features/has_fusion_table_features/api.rb +0 -111
- data/lib/spatial_features/has_fusion_table_features/configuration.rb +0 -15
- data/lib/spatial_features/has_fusion_table_features/service.rb +0 -104
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79fb559903943c11893b5d338f5c8030971f23051cf3611a326d0e013720f5e5
|
4
|
+
data.tar.gz: 1311241026ec5dc76a90e04f8fcc9e93c92965875f0967e1b27a0e1f64694ab3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8080e32e08c262fb3c44c6a228b0c599a1030ec52f9daea7862a06a98aa72b46dad77fb73127d5d40e6e999549ea44ebde0b77e826b1776e92eefe6b4a14898c
|
7
|
+
data.tar.gz: 3d591754fdc5a1ddb92146d805074aa5dc4bc65f5261eb0c169546f886b5dee784de7f24f56829a359641ae778960ffd54923c6b2535261d2e1194e0c9db0859
|
@@ -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')}),
|
@@ -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 = {})
|
@@ -149,10 +166,6 @@ class AbstractFeature < ActiveRecord::Base
|
|
149
166
|
@make_valid
|
150
167
|
end
|
151
168
|
|
152
|
-
def wkt
|
153
|
-
ActiveRecord::Base.connection.select_value("SELECT ST_ASEWKT('#{geom}')")
|
154
|
-
end
|
155
|
-
|
156
169
|
private
|
157
170
|
|
158
171
|
def make_valid
|
data/app/models/feature.rb
CHANGED
@@ -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 =
|
34
|
-
.where.not(:
|
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
|
data/lib/spatial_features.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
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
|
-
|
96
|
+
owner_class_has_loaded_column?('features_area')
|
97
97
|
end
|
98
98
|
|
99
99
|
def area_in_square_meters
|
100
|
-
features.
|
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
|
@@ -184,7 +190,11 @@ module SpatialFeatures
|
|
184
190
|
end
|
185
191
|
|
186
192
|
def features?
|
187
|
-
features.
|
193
|
+
if features.loaded?
|
194
|
+
features.present?
|
195
|
+
else
|
196
|
+
features.exists?
|
197
|
+
end
|
188
198
|
end
|
189
199
|
|
190
200
|
def intersects?(other)
|
@@ -198,7 +208,7 @@ module SpatialFeatures
|
|
198
208
|
end
|
199
209
|
|
200
210
|
def features_area_in_square_meters
|
201
|
-
|
211
|
+
aggregate_feature&.area
|
202
212
|
end
|
203
213
|
|
204
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
|
-
|
82
|
+
features.delete_all
|
83
83
|
valid, invalid = Feature.defer_aggregate_refresh do
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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 ||=
|
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
|
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.15.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:
|
12
|
+
date: 2021-07-06 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: '
|
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: '
|
33
|
+
version: '7.0'
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
35
|
name: delayed_job_active_record
|
36
36
|
requirement: !ruby/object:Gem::Requirement
|
@@ -51,28 +51,28 @@ dependencies:
|
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '3.0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
57
|
version_requirements: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '3.0'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
63
|
name: rubyzip
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
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:
|
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: '
|
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: '
|
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
|
@@ -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,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
|