spatial_features 2.16.0 → 2.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0bc8fc084675d82dfe1414f599b32d22b3b4435525246580c7cd2af1f5e3afc
4
- data.tar.gz: 78b7e7fa4b2e3fe9f46931c4b5a0bcf14c3f518a9e813e48fcb3985ac1bd171f
3
+ metadata.gz: e0a8fb7f4886f07f02b8b110f7975d91538324d6cf2c5a70b3b247dfb6719c98
4
+ data.tar.gz: 15c681c5de6fccb10a515c8ff1f8a423a7c9fa1c1fded8955c2072dd76f5d4f2
5
5
  SHA512:
6
- metadata.gz: 1fe304ec107f4d9ca8537a4a665c118bb20a1365b2bee3a58d88bd2ccc65758a4906a812a09632ad779e2df60b715c100967e62bfb97e23c012e96a7d031f958
7
- data.tar.gz: ab611750f2a507937f0f3a62b120c619b7e2b3a9fda837657eeb844bd0e02911d3fd5419a779f60ed0d20bdee6b387e4fb2b9b51e814216b1a0a82bce11852b5
6
+ metadata.gz: 0a9224909db6dd85bf20cf9b9561e78cdc12f094423c86b0280f0e2a96e2dad9ae24030c4a4014b448f566e232b7dd37620d2a8890c411af97487a0047261346
7
+ data.tar.gz: 740174a0882bc82edd053e4adf31318c6afc51860a66a3d50068e3c106cc8ac9351061f9d4990c652c5f41704815e2d5b8c8ef3d6b7aaa47525d9663f01acce4
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
 
@@ -19,17 +19,16 @@ class AbstractFeature < ActiveRecord::Base
19
19
  before_save :sanitize, if: :will_save_change_to_geog?
20
20
  after_save :cache_derivatives, :if => [:automatically_cache_derivatives?, :saved_change_to_geog?]
21
21
 
22
- def self.cache_key
23
- result = connection.select_one(all.select('max(id) AS max, count(*) AS count').to_sql)
24
- "#{result['max']}-#{result['count']}"
25
- end
26
-
27
22
  # for Rails >= 5 ActiveRecord collections we override the collection_cache_key
28
23
  # to prevent Rails doing its default query on `updated_at`
29
24
  def self.collection_cache_key(_collection, _timestamp_column)
30
25
  self.cache_key
31
26
  end
32
27
 
28
+ def self.cache_key
29
+ "#{maximum(:id)}-#{count}"
30
+ end
31
+
33
32
  def self.with_metadata(k, v)
34
33
  if k.present? && v.present?
35
34
  where('metadata->? = ?', k, v)
@@ -57,7 +56,7 @@ class AbstractFeature < ActiveRecord::Base
57
56
 
58
57
  def self.area_in_square_meters(geom = 'geom_lowres')
59
58
  current_scope = all.polygons
60
- unscoped { connection.select_value(select("ST_Area(ST_Union(#{geom}))").from(current_scope, :features)).to_f }
59
+ unscoped { SpatialFeatures::Utils.select_db_value(select("ST_Area(ST_Union(#{geom}))").from(current_scope, :features)).to_f }
61
60
  end
62
61
 
63
62
  def self.total_intersection_area_in_square_meters(other_features, geom = 'geom_lowres')
@@ -67,7 +66,7 @@ class AbstractFeature < ActiveRecord::Base
67
66
  query = base_class.unscoped.select('ST_Area(ST_Intersection(ST_Union(features.geom), ST_Union(other_features.geom)))')
68
67
  .from(scope, "features")
69
68
  .joins("INNER JOIN (#{other_scope.to_sql}) AS other_features ON ST_Intersects(features.geom, other_features.geom)")
70
- return connection.select_value(query).to_f
69
+ return SpatialFeatures::Utils.select_db_value(query).to_f
71
70
  end
72
71
 
73
72
  def self.intersecting(other)
@@ -148,11 +147,11 @@ class AbstractFeature < ActiveRecord::Base
148
147
  )
149
148
  )
150
149
  SQL
151
- connection.select_value(all.select(sql))
150
+ SpatialFeatures::Utils.select_db_value(all.select(sql))
152
151
  end
153
152
 
154
153
  def feature_bounds
155
- {n: north, e: east, s: south, w: west}
154
+ slice(:north, :east, :south, :west)
156
155
  end
157
156
 
158
157
  def cache_derivatives(*args)
@@ -176,17 +175,17 @@ class AbstractFeature < ActiveRecord::Base
176
175
  private
177
176
 
178
177
  def make_valid
179
- self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Buffer('#{sanitize}', 0)")
178
+ self.geog = SpatialFeatures::Utils.select_db_value("SELECT ST_Buffer('#{sanitize}', 0)")
180
179
  end
181
180
 
182
181
  # Use ST_Force2D to discard z-coordinates that cause failures later in the process
183
182
  def sanitize
184
- self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Force2D('#{geog}')")
183
+ self.geog = SpatialFeatures::Utils.select_db_value("SELECT ST_Force2D('#{geog}')")
185
184
  end
186
185
 
187
186
  SRID_CACHE = {}
188
187
  def self.detect_srid(column_name)
189
- SRID_CACHE[column_name] ||= connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
188
+ SRID_CACHE[column_name] ||= SpatialFeatures::Utils.select_db_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
190
189
  end
191
190
 
192
191
  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 = ActiveRecord::Base.connection.select_value <<~SQL
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)
@@ -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 = open(file, unzip: unzip, **unzip_options)
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
- def self.open(file, unzip: nil, **unzip_options)
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
- return File.open(Unzip.paths(file, :find => find, **unzip_options))
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)).map do |data|
72
- spatial_importer_from_name(importer_name).new(data, :make_valid => make_valid) if data.present?
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
@@ -201,6 +201,15 @@ module SpatialFeatures
201
201
  self.class.unscoped { self.class.intersecting(other).exists?(id) }
202
202
  end
203
203
 
204
+ def bounds
205
+ if association(:aggregate_feature).loaded?
206
+ aggregate_feature&.feature_bounds
207
+ else
208
+ result = aggregate_features.pluck(:north, :east, :south, :west).first
209
+ [:north, :east, :south, :west].zip(result.map {|bound| BigDecimal(bound) }).to_h.with_indifferent_access if result
210
+ end
211
+ end
212
+
204
213
  def total_intersection_area_percentage(klass)
205
214
  return 0.0 unless features_area_in_square_meters > 0
206
215
 
@@ -208,7 +217,11 @@ module SpatialFeatures
208
217
  end
209
218
 
210
219
  def features_area_in_square_meters
211
- aggregate_feature&.area
220
+ if association(:aggregate_feature).loaded?
221
+ aggregate_feature&.area
222
+ else
223
+ aggregate_features.pluck(:area).first
224
+ end
212
225
  end
213
226
 
214
227
  def total_intersection_area_in_square_meters(other)
@@ -228,6 +241,11 @@ module SpatialFeatures
228
241
  return false
229
242
  end
230
243
  end
244
+
245
+ # Scope to perform SQL-only calculations on a record's aggregate feature. This avoids loading the large data payload if all that is needed is metadata
246
+ def aggregate_features
247
+ self.class.where(id: id).aggregate_features
248
+ end
231
249
  end
232
250
 
233
251
  module FeaturesAssociationExtensions
@@ -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
- def initialize(data, *args)
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
- file = Download.open(data, unzip: [/\.kml$/, /\.shp$/], downcase: true)
24
+ current_file ||= Download.open_each(data, unzip: [/\.kml$/, /\.shp$/], downcase: true).first
9
25
  rescue Unzip::PathNotFound
10
- raise ImportError, "Archive did not contain a .kml or .shp file. Supported formats are KMZ, KML, and zipped ArcGIS shapefiles."
26
+ raise ImportError, INVALID_ARCHIVE
11
27
  end
12
28
 
13
- case ::File.extname(file.path.downcase)
29
+ case ::File.extname(current_file.path.downcase)
14
30
  when '.kml'
15
- __setobj__(KMLFile.new(file, *args))
31
+ __setobj__(KMLFile.new(current_file, *args, **options))
16
32
  when '.shp'
17
- __setobj__(Shapefile.new(file, *args))
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 = ActiveRecord::Base.connection.select_value("SELECT ST_GeomFromKML(#{ActiveRecord::Base.connection.quote(kml.to_s)})")
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, *args, **options)
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 ||= begin
55
- validate_file!
56
- Download.open(archive, unzip: /\.shp$/, downcase: true)
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 validate_file!
61
- return unless Unzip.is_zip?(archive)
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.detect {|path| find.any? {|pattern| path.index(pattern) } }
10
- raise(PathNotFound, "Archive did not contain a file matching #{find}") unless paths.present?
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
- # 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."
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
- obj[ext] = File.basename(f.name, '.*')
33
+ true
22
34
  end
23
35
 
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)}"
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
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.16.0"
2
+ VERSION = "2.17.3"
3
3
  end
@@ -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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spatial_features
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.16.0
4
+ version: 2.17.3
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: 2021-07-17 00:00:00.000000000 Z
12
+ date: 2021-10-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -157,6 +157,7 @@ files:
157
157
  - lib/spatial_features/has_spatial_features/queued_spatial_processing.rb
158
158
  - lib/spatial_features/importers/base.rb
159
159
  - lib/spatial_features/importers/file.rb
160
+ - lib/spatial_features/importers/geojson.rb
160
161
  - lib/spatial_features/importers/geomark.rb
161
162
  - lib/spatial_features/importers/json_arcgis.rb
162
163
  - lib/spatial_features/importers/kml.rb