spatial_features 2.17.1 → 2.19.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/README.md +5 -1
- data/app/models/abstract_feature.rb +11 -8
- data/app/models/aggregate_feature.rb +1 -1
- data/app/models/feature.rb +10 -0
- data/lib/spatial_features/download.rb +16 -6
- data/lib/spatial_features/has_spatial_features/feature_import.rb +2 -2
- data/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb +60 -4
- data/lib/spatial_features/has_spatial_features.rb +6 -2
- data/lib/spatial_features/importers/base.rb +6 -0
- data/lib/spatial_features/importers/file.rb +22 -6
- data/lib/spatial_features/importers/geojson.rb +29 -0
- data/lib/spatial_features/importers/json_arcgis.rb +1 -5
- data/lib/spatial_features/importers/kml.rb +1 -1
- data/lib/spatial_features/importers/shapefile.rb +26 -8
- data/lib/spatial_features/unzip.rb +3 -3
- data/lib/spatial_features/utils.rb +9 -0
- data/lib/spatial_features/validation.rb +36 -42
- data/lib/spatial_features/version.rb +1 -1
- data/lib/spatial_features.rb +1 -0
- metadata +7 -21
- data/config/initializers/chroma_serializers.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0d49ba8050fb39cb929127a633f92bddc901d7851e5e02ac7459db8edbfe790
|
4
|
+
data.tar.gz: 3f0c2d8ed22051efe5e11f0735c5430d8f9a0d11c94550b2719be338531e0f1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6d53a0c2c448b51f9344e39be1e271f8a084ccee0e1d3aeefd0252c3d9dfb2b7afcb8ab0b6822e1485a44aecdf79f62615f59f6f169073c74fc61ce70107d32
|
7
|
+
data.tar.gz: 7dc96d943a8a8695d05ad246c4ffaaa166ff4be40fe10745a671ac4ae89b72ecbae810241fd1dde605d72c57989695d7cbb07a55575799dd8b65eb32d8819d61
|
data/README.md
CHANGED
@@ -110,7 +110,7 @@ You can specify multiple import sources for geometry. Each key is a method that
|
|
110
110
|
each value is the Importer to use to parse the data. See each Importer for more details.
|
111
111
|
```ruby
|
112
112
|
class Location < ActiveRecord::Base
|
113
|
-
has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File' }
|
113
|
+
has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => 'GeoJSON' }
|
114
114
|
|
115
115
|
def remote_kml_url
|
116
116
|
"www.test.com/kml/#{id}.kml"
|
@@ -119,6 +119,10 @@ class Location < ActiveRecord::Base
|
|
119
119
|
def file
|
120
120
|
File.open('local/files/my_kml')
|
121
121
|
end
|
122
|
+
|
123
|
+
def geojson
|
124
|
+
{ "type" => "FeatureCollection", "features" => [] }
|
125
|
+
end
|
122
126
|
end
|
123
127
|
```
|
124
128
|
|
@@ -49,14 +49,13 @@ class AbstractFeature < ActiveRecord::Base
|
|
49
49
|
where(:feature_type => 'point')
|
50
50
|
end
|
51
51
|
|
52
|
-
def self.
|
53
|
-
# where("ST_DWithin(features.geog, ST_SetSRID( ST_Point( -71.104, 42.315), 4326)::geography, :distance)", :lat => lat, :lng => lng, :distance => distance_in_meters)
|
52
|
+
def self.within_distance_of_point(lat, lng, distance_in_meters)
|
54
53
|
where("ST_DWithin(features.geog, ST_Point(:lng, :lat), :distance)", :lat => lat, :lng => lng, :distance => distance_in_meters)
|
55
54
|
end
|
56
55
|
|
57
56
|
def self.area_in_square_meters(geom = 'geom_lowres')
|
58
57
|
current_scope = all.polygons
|
59
|
-
unscoped {
|
58
|
+
unscoped { SpatialFeatures::Utils.select_db_value(select("ST_Area(ST_Union(#{geom}))").from(current_scope, :features)).to_f }
|
60
59
|
end
|
61
60
|
|
62
61
|
def self.total_intersection_area_in_square_meters(other_features, geom = 'geom_lowres')
|
@@ -66,13 +65,17 @@ class AbstractFeature < ActiveRecord::Base
|
|
66
65
|
query = base_class.unscoped.select('ST_Area(ST_Intersection(ST_Union(features.geom), ST_Union(other_features.geom)))')
|
67
66
|
.from(scope, "features")
|
68
67
|
.joins("INNER JOIN (#{other_scope.to_sql}) AS other_features ON ST_Intersects(features.geom, other_features.geom)")
|
69
|
-
return
|
68
|
+
return SpatialFeatures::Utils.select_db_value(query).to_f
|
70
69
|
end
|
71
70
|
|
72
71
|
def self.intersecting(other)
|
73
72
|
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').uniq
|
74
73
|
end
|
75
74
|
|
75
|
+
def self.within_distance(other, distance_in_meters)
|
76
|
+
join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).uniq
|
77
|
+
end
|
78
|
+
|
76
79
|
def self.invalid(column = 'geog::geometry')
|
77
80
|
select("features.*, ST_IsValidReason(#{column}) AS invalid_geometry_message").where.not("ST_IsValid(#{column})")
|
78
81
|
end
|
@@ -147,7 +150,7 @@ class AbstractFeature < ActiveRecord::Base
|
|
147
150
|
)
|
148
151
|
)
|
149
152
|
SQL
|
150
|
-
|
153
|
+
SpatialFeatures::Utils.select_db_value(all.select(sql))
|
151
154
|
end
|
152
155
|
|
153
156
|
def feature_bounds
|
@@ -175,17 +178,17 @@ class AbstractFeature < ActiveRecord::Base
|
|
175
178
|
private
|
176
179
|
|
177
180
|
def make_valid
|
178
|
-
self.geog =
|
181
|
+
self.geog = SpatialFeatures::Utils.select_db_value("SELECT ST_Buffer('#{sanitize}', 0)")
|
179
182
|
end
|
180
183
|
|
181
184
|
# Use ST_Force2D to discard z-coordinates that cause failures later in the process
|
182
185
|
def sanitize
|
183
|
-
self.geog =
|
186
|
+
self.geog = SpatialFeatures::Utils.select_db_value("SELECT ST_Force2D('#{geog}')")
|
184
187
|
end
|
185
188
|
|
186
189
|
SRID_CACHE = {}
|
187
190
|
def self.detect_srid(column_name)
|
188
|
-
SRID_CACHE[column_name] ||=
|
191
|
+
SRID_CACHE[column_name] ||= SpatialFeatures::Utils.select_db_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
|
189
192
|
end
|
190
193
|
|
191
194
|
def self.join_other_features(other)
|
@@ -14,7 +14,7 @@ class AggregateFeature < AbstractFeature
|
|
14
14
|
SQL
|
15
15
|
|
16
16
|
# Remove empty features so ST_COLLECT doesn't choke. This seems to be a difference between PostGIS 2.x and 3.x
|
17
|
-
self.geog =
|
17
|
+
self.geog = SpatialFeatures::Utils.select_db_value <<~SQL
|
18
18
|
SELECT COALESCE(ST_Collect(unnest)::geography, ST_GeogFromText('MULTIPOLYGON EMPTY'))
|
19
19
|
FROM (SELECT unnest(#{feature_array_sql})) AS features
|
20
20
|
WHERE NOT ST_IsEmpty(unnest)
|
data/app/models/feature.rb
CHANGED
@@ -9,6 +9,8 @@ class Feature < AbstractFeature
|
|
9
9
|
|
10
10
|
validates_inclusion_of :feature_type, :in => FEATURE_TYPES
|
11
11
|
|
12
|
+
before_save :truncate_name
|
13
|
+
|
12
14
|
after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
|
13
15
|
|
14
16
|
# Features are used for display so we also cache their KML representation
|
@@ -60,4 +62,12 @@ class Feature < AbstractFeature
|
|
60
62
|
# this field blank.
|
61
63
|
spatial_model_id? && automatically_refresh_aggregate && saved_change_to_geog?
|
62
64
|
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def truncate_name
|
69
|
+
return unless name?
|
70
|
+
col_size = Feature.columns_hash["name"]&.limit || 255
|
71
|
+
self.name = name.to_s.truncate(col_size)
|
72
|
+
end
|
63
73
|
end
|
@@ -4,20 +4,30 @@ module SpatialFeatures
|
|
4
4
|
module Download
|
5
5
|
# file can be a url, path, or file, any of which can return be a zipped archive
|
6
6
|
def self.read(file, unzip: nil, **unzip_options)
|
7
|
-
file =
|
7
|
+
file = Download.open_each(file, unzip: unzip, **unzip_options).first
|
8
8
|
path = ::File.path(file)
|
9
9
|
return ::File.read(path)
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
# file can be a url, path, or file, any of which can return be a zipped archive
|
13
|
+
def self.open(file)
|
13
14
|
file = Kernel.open(file)
|
14
15
|
file = normalize_file(file) if file.is_a?(StringIO)
|
15
|
-
if unzip && Unzip.is_zip?(file)
|
16
|
-
file = find_in_zip(file, find: unzip, **unzip_options)
|
17
|
-
end
|
18
16
|
return file
|
19
17
|
end
|
20
18
|
|
19
|
+
# file can be a url, path, or file, any of which can return be a zipped archive
|
20
|
+
def self.open_each(file, unzip: nil, **unzip_options)
|
21
|
+
file = Download.open(file)
|
22
|
+
files = if unzip && Unzip.is_zip?(file)
|
23
|
+
find_in_zip(file, find: unzip, **unzip_options)
|
24
|
+
else
|
25
|
+
[file]
|
26
|
+
end
|
27
|
+
|
28
|
+
return files.map { |f| File.open(f) }
|
29
|
+
end
|
30
|
+
|
21
31
|
def self.normalize_file(file)
|
22
32
|
Tempfile.new.tap do |temp|
|
23
33
|
temp.binmode
|
@@ -33,7 +43,7 @@ module SpatialFeatures
|
|
33
43
|
end
|
34
44
|
|
35
45
|
def self.find_in_zip(file, find:, **unzip_options)
|
36
|
-
|
46
|
+
Unzip.paths(file, :find => find, **unzip_options)
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
@@ -68,8 +68,8 @@ module SpatialFeatures
|
|
68
68
|
|
69
69
|
def spatial_feature_imports(import_options, make_valid)
|
70
70
|
import_options.flat_map do |data_method, importer_name|
|
71
|
-
Array.wrap(send(data_method)).
|
72
|
-
spatial_importer_from_name(importer_name).
|
71
|
+
Array.wrap(send(data_method)).flat_map do |data|
|
72
|
+
spatial_importer_from_name(importer_name).create_all(data, :make_valid => make_valid) if data.present?
|
73
73
|
end
|
74
74
|
end.compact
|
75
75
|
end
|
@@ -11,7 +11,22 @@ module SpatialFeatures
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def updating_features?
|
14
|
-
|
14
|
+
case spatial_processing_status(:update_features!)
|
15
|
+
when :queued, :processing
|
16
|
+
true
|
17
|
+
else
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def updating_features_failed?
|
23
|
+
spatial_processing_status(:update_features!) == :failure
|
24
|
+
end
|
25
|
+
|
26
|
+
def spatial_processing_status(method_name)
|
27
|
+
if has_attribute?(:spatial_processing_status_cache)
|
28
|
+
spatial_processing_status_cache[method_name.to_s]&.to_sym
|
29
|
+
end
|
15
30
|
end
|
16
31
|
|
17
32
|
def feature_update_error
|
@@ -27,17 +42,58 @@ module SpatialFeatures
|
|
27
42
|
end
|
28
43
|
|
29
44
|
def spatial_processing_jobs(suffix = nil)
|
30
|
-
Delayed::Job.where(
|
45
|
+
Delayed::Job.where(:queue => "#{spatial_processing_queue_name}#{suffix}")
|
31
46
|
end
|
32
47
|
|
33
48
|
private
|
34
49
|
|
35
50
|
def queue_spatial_task(method_name, *args)
|
36
|
-
|
51
|
+
Delayed::Job.enqueue SpatialProcessingJob.new(self, method_name, *args), :queue => spatial_processing_queue_name + method_name
|
37
52
|
end
|
38
53
|
|
39
54
|
def spatial_processing_queue_name
|
40
|
-
"#{
|
55
|
+
"#{model_name}/#{id}/"
|
56
|
+
end
|
57
|
+
|
58
|
+
# CLASSES
|
59
|
+
|
60
|
+
class SpatialProcessingJob
|
61
|
+
def initialize(record, method_name, *args)
|
62
|
+
@record = record
|
63
|
+
@method_name = method_name
|
64
|
+
@args = args
|
65
|
+
end
|
66
|
+
|
67
|
+
def enqueue(job)
|
68
|
+
update_cached_status(:queued)
|
69
|
+
end
|
70
|
+
|
71
|
+
def perform
|
72
|
+
update_cached_status(:processing)
|
73
|
+
@record.send(@method_name, *@args)
|
74
|
+
end
|
75
|
+
|
76
|
+
def success(job)
|
77
|
+
update_cached_status(:success)
|
78
|
+
end
|
79
|
+
|
80
|
+
def error(job)
|
81
|
+
update_cached_status(:failure)
|
82
|
+
end
|
83
|
+
|
84
|
+
def failure(job)
|
85
|
+
update_cached_status(:failure)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def update_cached_status(state)
|
91
|
+
if @record.has_attribute?(:spatial_processing_status_cache)
|
92
|
+
cache = @record.spatial_processing_status_cache || {}
|
93
|
+
cache[@method_name] = state
|
94
|
+
@record.update_column(:spatial_processing_status_cache, cache)
|
95
|
+
end
|
96
|
+
end
|
41
97
|
end
|
42
98
|
end
|
43
99
|
end
|
@@ -203,7 +203,7 @@ module SpatialFeatures
|
|
203
203
|
|
204
204
|
def bounds
|
205
205
|
if association(:aggregate_feature).loaded?
|
206
|
-
aggregate_feature
|
206
|
+
aggregate_feature&.feature_bounds
|
207
207
|
else
|
208
208
|
result = aggregate_features.pluck(:north, :east, :south, :west).first
|
209
209
|
[:north, :east, :south, :west].zip(result.map {|bound| BigDecimal(bound) }).to_h.with_indifferent_access if result
|
@@ -217,7 +217,11 @@ module SpatialFeatures
|
|
217
217
|
end
|
218
218
|
|
219
219
|
def features_area_in_square_meters
|
220
|
-
aggregate_feature
|
220
|
+
if association(:aggregate_feature).loaded?
|
221
|
+
aggregate_feature&.area
|
222
|
+
else
|
223
|
+
aggregate_features.pluck(:area).first
|
224
|
+
end
|
221
225
|
end
|
222
226
|
|
223
227
|
def total_intersection_area_in_square_meters(other)
|
@@ -19,6 +19,12 @@ module SpatialFeatures
|
|
19
19
|
@cache_key ||= Digest::MD5.hexdigest(@data)
|
20
20
|
end
|
21
21
|
|
22
|
+
# factory method that should always be used instead of `new` when creating importers
|
23
|
+
# returns an array of Importer::* objects
|
24
|
+
def self.create_all(data, **options)
|
25
|
+
[new(data, **options)]
|
26
|
+
end
|
27
|
+
|
22
28
|
private
|
23
29
|
|
24
30
|
def build_features
|
@@ -3,18 +3,34 @@ require 'open-uri'
|
|
3
3
|
module SpatialFeatures
|
4
4
|
module Importers
|
5
5
|
class File < SimpleDelegator
|
6
|
-
|
6
|
+
INVALID_ARCHIVE = "Archive did not contain a .kml or .shp file. Supported formats are KMZ, KML, and zipped ArcGIS shapefiles.".freeze
|
7
|
+
|
8
|
+
def self.create_all(data, **options)
|
9
|
+
Download.open_each(data, unzip: [/\.kml$/, /\.shp$/], downcase: true).map do |file|
|
10
|
+
new(data, **options, current_file: file)
|
11
|
+
end
|
12
|
+
rescue Unzip::PathNotFound
|
13
|
+
raise ImportError, INVALID_ARCHIVE
|
14
|
+
end
|
15
|
+
|
16
|
+
# The File importer may be initialized multiple times by `::create_all` if it
|
17
|
+
# receives ZIP data containing multiple KML or SHP files. We use `current_file`
|
18
|
+
# to distinguish which file in the archive is currently being
|
19
|
+
# processed.
|
20
|
+
#
|
21
|
+
# If no `current_file` is passed then we just take the first valid file that we find.
|
22
|
+
def initialize(data, *args, current_file: nil, **options)
|
7
23
|
begin
|
8
|
-
|
24
|
+
current_file ||= Download.open_each(data, unzip: [/\.kml$/, /\.shp$/], downcase: true).first
|
9
25
|
rescue Unzip::PathNotFound
|
10
|
-
raise ImportError,
|
26
|
+
raise ImportError, INVALID_ARCHIVE
|
11
27
|
end
|
12
28
|
|
13
|
-
case ::File.extname(
|
29
|
+
case ::File.extname(current_file.path.downcase)
|
14
30
|
when '.kml'
|
15
|
-
__setobj__(KMLFile.new(
|
31
|
+
__setobj__(KMLFile.new(current_file, *args, **options))
|
16
32
|
when '.shp'
|
17
|
-
__setobj__(Shapefile.new(
|
33
|
+
__setobj__(Shapefile.new(current_file, *args, **options))
|
18
34
|
else
|
19
35
|
raise ImportError, "Could not import file. Supported formats are KMZ, KML, and zipped ArcGIS shapefiles"
|
20
36
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
module SpatialFeatures
|
5
|
+
module Importers
|
6
|
+
class GeoJSON < Base
|
7
|
+
def cache_key
|
8
|
+
@cache_key ||= Digest::MD5.hexdigest(@data.to_json)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def each_record(&block)
|
14
|
+
return unless @data
|
15
|
+
|
16
|
+
@data.fetch('features', []).each do |record|
|
17
|
+
metadata = record['properties'] || {}
|
18
|
+
name = metadata.delete('name')
|
19
|
+
yield OpenStruct.new(
|
20
|
+
:feature_type => record['geometry']['type'],
|
21
|
+
:geog => SpatialFeatures::Utils.geom_from_json(record['geometry']),
|
22
|
+
:name => name,
|
23
|
+
:metadata => metadata
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -15,7 +15,7 @@ module SpatialFeatures
|
|
15
15
|
json['features'].each do |record|
|
16
16
|
yield OpenStruct.new(
|
17
17
|
:feature_type => record['geometry']['type'],
|
18
|
-
:geog => geom_from_json(record['geometry']),
|
18
|
+
:geog => SpatialFeatures::Utils.geom_from_json(record['geometry']),
|
19
19
|
:metadata => record['properties']
|
20
20
|
)
|
21
21
|
end
|
@@ -24,10 +24,6 @@ module SpatialFeatures
|
|
24
24
|
def esri_json_to_geojson(url)
|
25
25
|
JSON.parse(`ogr2ogr -f GeoJSON /dev/stdout "#{url}" OGRGeoJSON`)
|
26
26
|
end
|
27
|
-
|
28
|
-
def geom_from_json(geometry)
|
29
|
-
ActiveRecord::Base.connection.select_value("SELECT ST_GeomFromGeoJSON('#{geometry.to_json}')")
|
30
|
-
end
|
31
27
|
end
|
32
28
|
end
|
33
29
|
end
|
@@ -35,7 +35,7 @@ module SpatialFeatures
|
|
35
35
|
|
36
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
37
|
Thread.new do
|
38
|
-
geom =
|
38
|
+
geom = SpatialFeatures::Utils.select_db_value("SELECT ST_GeomFromKML(#{ActiveRecord::Base.connection.quote(kml.to_s)})")
|
39
39
|
rescue ActiveRecord::StatementInvalid => e # Discard Invalid KML features
|
40
40
|
geom = nil
|
41
41
|
end.join
|
@@ -7,7 +7,7 @@ module SpatialFeatures
|
|
7
7
|
class_attribute :default_proj4_projection
|
8
8
|
|
9
9
|
def initialize(data, *args, proj4: nil, **options)
|
10
|
-
super(data,
|
10
|
+
super(data, **options)
|
11
11
|
@proj4 = proj4
|
12
12
|
end
|
13
13
|
|
@@ -15,8 +15,21 @@ module SpatialFeatures
|
|
15
15
|
@cache_key ||= Digest::MD5.file(archive).to_s
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.create_all(data, **options)
|
19
|
+
Download.open_each(data, unzip: [/\.shp$/], downcase: true).map do |file|
|
20
|
+
new(file, **options)
|
21
|
+
end
|
22
|
+
rescue Unzip::PathNotFound
|
23
|
+
raise ImportError, INVALID_ARCHIVE
|
24
|
+
end
|
25
|
+
|
18
26
|
private
|
19
27
|
|
28
|
+
def build_features
|
29
|
+
validate_shapefile!
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
20
33
|
def each_record(&block)
|
21
34
|
RGeo::Shapefile::Reader.open(file.path) do |records|
|
22
35
|
records.each do |record|
|
@@ -49,17 +62,22 @@ module SpatialFeatures
|
|
49
62
|
SQL
|
50
63
|
end
|
51
64
|
|
52
|
-
|
65
|
+
# the individual SHP file for processing (automatically extracted from a ZIP archive if necessary)
|
53
66
|
def file
|
54
|
-
@file ||=
|
55
|
-
|
56
|
-
|
67
|
+
@file ||= Unzip.is_zip?(archive) ? possible_shp_files.first : archive
|
68
|
+
end
|
69
|
+
|
70
|
+
# a zip archive may contain multiple SHP files
|
71
|
+
def possible_shp_files
|
72
|
+
@possible_shp_files ||= begin
|
73
|
+
Download.open_each(archive, unzip: /\.shp$/, downcase: true)
|
74
|
+
rescue Unzip::PathNotFound
|
75
|
+
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a SHP file"
|
57
76
|
end
|
58
77
|
end
|
59
78
|
|
60
|
-
def
|
61
|
-
|
62
|
-
Validation.validate_shapefile_archive!(Download.entries(archive), default_proj4_projection: default_proj4_projection)
|
79
|
+
def validate_shapefile!
|
80
|
+
Validation.validate_shapefile!(file, default_proj4_projection: default_proj4_projection)
|
63
81
|
end
|
64
82
|
|
65
83
|
def archive
|
@@ -6,11 +6,11 @@ module SpatialFeatures
|
|
6
6
|
paths = extract(file_path, **extract_options)
|
7
7
|
|
8
8
|
if find = Array.wrap(find).presence
|
9
|
-
paths = paths.
|
10
|
-
raise(PathNotFound, "Archive did not contain a file matching #{find}")
|
9
|
+
paths = paths.select {|path| find.any? {|pattern| path.index(pattern) } }
|
10
|
+
raise(PathNotFound, "Archive did not contain a file matching #{find}") if paths.empty?
|
11
11
|
end
|
12
12
|
|
13
|
-
return paths
|
13
|
+
return Array(paths)
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.extract(file_path, output_dir = Dir.mktmpdir, downcase: false)
|
@@ -52,5 +52,14 @@ module SpatialFeatures
|
|
52
52
|
object.unscope(:select).select(:id).to_sql
|
53
53
|
end
|
54
54
|
end
|
55
|
+
|
56
|
+
def select_db_value(query)
|
57
|
+
ActiveRecord::Base.connection.select_value(query)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Convert a hash of GeoJSON data into a PostGIS geometry object
|
61
|
+
def geom_from_json(geometry)
|
62
|
+
select_db_value("SELECT ST_GeomFromGeoJSON('#{geometry.to_json}')")
|
63
|
+
end
|
55
64
|
end
|
56
65
|
end
|
@@ -1,55 +1,49 @@
|
|
1
1
|
module SpatialFeatures
|
2
2
|
module Validation
|
3
|
-
# SHP file must come first
|
4
3
|
REQUIRED_SHAPEFILE_COMPONENT_EXTENSIONS = %w[shp shx dbf prj].freeze
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
5
|
+
class << self
|
6
|
+
# Check if a shapefile includes the required component files, otherwise
|
7
|
+
# raise an exception.
|
8
|
+
#
|
9
|
+
# This validation operates by checking sibling files in the same directory,
|
10
|
+
# similar to how `rgeo-shapefile` validates SHP files.
|
11
|
+
#
|
12
|
+
# @param [File] shp_file A File object
|
13
|
+
# @param [String] default_proj4_projection Optional, if supplied we don't raise an exception when we're missing a .PRJ file
|
14
|
+
def validate_shapefile!(shp_file, default_proj4_projection: nil)
|
15
|
+
basename = File.basename(shp_file.path, '.*')
|
16
|
+
path = shp_file.path.to_s.sub(/\.shp$/i, "")
|
17
|
+
|
18
|
+
required_extensions = REQUIRED_SHAPEFILE_COMPONENT_EXTENSIONS
|
19
|
+
required_extensions -= ['prj'] if default_proj4_projection
|
20
|
+
|
21
|
+
required_extensions.each do |ext|
|
22
|
+
component_path = "#{path}.#{ext}"
|
23
|
+
next if ::File.file?(component_path) && ::File.readable?(component_path)
|
24
|
+
|
25
|
+
case ext
|
26
|
+
when "prj"
|
27
|
+
raise ::SpatialFeatures::Importers::IndeterminateShapefileProjection, "Shapefile archive is missing a projection file: #{File.basename(component_path)}"
|
28
|
+
else
|
29
|
+
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a required file: #{File.basename(component_path)}"
|
30
|
+
end
|
19
31
|
end
|
20
32
|
|
21
|
-
|
33
|
+
true
|
22
34
|
end
|
23
35
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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)}"
|
36
|
+
# Validation helper that takes examines an entire ZIP file
|
37
|
+
#
|
38
|
+
# Useful for validating before persisting records but not used internally
|
39
|
+
def validate_shapefile_archive!(path, default_proj4_projection: nil, allow_generic_zip_files: false)
|
40
|
+
Download.open_each(path, unzip: /\.shp$/, downcase: true).each do |shp_file|
|
41
|
+
validate_shapefile!(shp_file, default_proj4_projection: default_proj4_projection)
|
45
42
|
end
|
43
|
+
rescue Unzip::PathNotFound
|
44
|
+
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a SHP file" \
|
45
|
+
unless allow_generic_zip_files
|
46
46
|
end
|
47
|
-
|
48
|
-
true
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.expected_component_path(basename, ext)
|
52
|
-
"#{basename}.#{ext}"
|
53
47
|
end
|
54
48
|
end
|
55
49
|
end
|
data/lib/spatial_features.rb
CHANGED
@@ -20,6 +20,7 @@ require 'spatial_features/has_spatial_features/feature_import'
|
|
20
20
|
|
21
21
|
require 'spatial_features/importers/base'
|
22
22
|
require 'spatial_features/importers/file'
|
23
|
+
require 'spatial_features/importers/geojson'
|
23
24
|
require 'spatial_features/importers/kml'
|
24
25
|
require 'spatial_features/importers/kml_file'
|
25
26
|
require 'spatial_features/importers/kml_file_arcgis'
|
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.
|
4
|
+
version: 2.19.0
|
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: 2021-
|
12
|
+
date: 2021-11-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -87,20 +87,6 @@ dependencies:
|
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '1.6'
|
90
|
-
- !ruby/object:Gem::Dependency
|
91
|
-
name: chroma
|
92
|
-
requirement: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - "~>"
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 0.1.0
|
97
|
-
type: :runtime
|
98
|
-
prerelease: false
|
99
|
-
version_requirements: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: 0.1.0
|
104
90
|
- !ruby/object:Gem::Dependency
|
105
91
|
name: pg
|
106
92
|
requirement: !ruby/object:Gem::Requirement
|
@@ -144,7 +130,6 @@ files:
|
|
144
130
|
- app/models/feature.rb
|
145
131
|
- app/models/spatial_cache.rb
|
146
132
|
- app/models/spatial_proximity.rb
|
147
|
-
- config/initializers/chroma_serializers.rb
|
148
133
|
- config/initializers/mime_types.rb
|
149
134
|
- config/initializers/register_oids.rb
|
150
135
|
- lib/spatial_features.rb
|
@@ -157,6 +142,7 @@ files:
|
|
157
142
|
- lib/spatial_features/has_spatial_features/queued_spatial_processing.rb
|
158
143
|
- lib/spatial_features/importers/base.rb
|
159
144
|
- lib/spatial_features/importers/file.rb
|
145
|
+
- lib/spatial_features/importers/geojson.rb
|
160
146
|
- lib/spatial_features/importers/geomark.rb
|
161
147
|
- lib/spatial_features/importers/json_arcgis.rb
|
162
148
|
- lib/spatial_features/importers/kml.rb
|
@@ -174,7 +160,7 @@ homepage: https://github.com/culturecode/spatial_features
|
|
174
160
|
licenses:
|
175
161
|
- MIT
|
176
162
|
metadata: {}
|
177
|
-
post_install_message:
|
163
|
+
post_install_message:
|
178
164
|
rdoc_options: []
|
179
165
|
require_paths:
|
180
166
|
- lib
|
@@ -189,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
175
|
- !ruby/object:Gem::Version
|
190
176
|
version: '0'
|
191
177
|
requirements: []
|
192
|
-
rubygems_version: 3.0.
|
193
|
-
signing_key:
|
178
|
+
rubygems_version: 3.0.3.1
|
179
|
+
signing_key:
|
194
180
|
specification_version: 4
|
195
181
|
summary: Adds spatial methods to a model.
|
196
182
|
test_files: []
|
@@ -1,15 +0,0 @@
|
|
1
|
-
module Chroma
|
2
|
-
class Color
|
3
|
-
module Serializers
|
4
|
-
# Google's Fusion Table colouring expects the alpha value in the last position, not the first
|
5
|
-
def to_ft_hex
|
6
|
-
[
|
7
|
-
to_2char_hex(@rgb.r),
|
8
|
-
to_2char_hex(@rgb.g),
|
9
|
-
to_2char_hex(@rgb.b),
|
10
|
-
to_2char_hex(alpha * 255)
|
11
|
-
].join('')
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|