spatial_features 3.2.0 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fabdd3f0f73eb21327f8e3deb1b7dda5969d97bdb7c053c60199a63fe5cd04b1
4
- data.tar.gz: 777c5dc188ba968ab4cba25e58fa2f361c9d3814781ecfb9e751cf0fc1b40b0e
3
+ metadata.gz: 7ba1ab9dfa18160f48292f26a63f75031187e44cc04aff73dba792832d482063
4
+ data.tar.gz: b25d8a6f4bd0412c3d2d9dd34a57c620a0593979304ddb9061c7f3b3e2d7f834
5
5
  SHA512:
6
- metadata.gz: e3f28e99e814f1fcc2ba49edf8a59ff957b9f92c9e0d990f93409c61f00d4c496e7ac5f60eb3616660df720c615033f62dbe02d707c1584cc4cee7d9cb88067c
7
- data.tar.gz: 18e776f2d1bc792e7a0313d5742345e93adca24f060472bb10a36611eb3e62cf7fe8fcf2d7d520db16cd9b1755e85b4e40e75cabcb4110e25c397e42087fd9e0
6
+ metadata.gz: 1be4b279b3341582262bdb8d802c0786b1eb3607cef070f2a03813b2bc987d21fb0aa92323d7b49dbec01aa8b2e128f0b9982ce356dc4836875ccd5ad89f7092
7
+ data.tar.gz: 8e70dfe6f6bad6545c29fc5dbf1406b308b9cb44ff48ebfc849469912af6cf30640c829c2baf363a6790e9207f78d3c039276c002a50ce6319ccf0d9450f9df7
@@ -49,8 +49,14 @@ class AbstractFeature < ActiveRecord::Base
49
49
  where(:feature_type => 'point')
50
50
  end
51
51
 
52
- def self.within_distance_of_point(lat, lng, distance_in_meters)
53
- where("ST_DWithin(features.geog, ST_Point(:lng, :lat), :distance)", :lat => lat, :lng => lng, :distance => distance_in_meters)
52
+ def self.within_distance_of_point(lat, lng, distance_in_meters, geom = 'geom_lowres')
53
+ point_sql =
54
+ case geom.to_s
55
+ when 'geog' then 'ST_Point(:lng, :lat)'
56
+ else "ST_Transform(ST_SetSRID(ST_Point(:lng, :lat), 4326), #{detect_srid(geom)})"
57
+ end
58
+
59
+ where("ST_DWithin(features.#{geom}, #{point_sql}, :distance)", :lat => lat, :lng => lng, :distance => distance_in_meters)
54
60
  end
55
61
 
56
62
  def self.area_in_square_meters(geom = 'geom_lowres')
@@ -248,7 +254,7 @@ class AbstractFeature < ActiveRecord::Base
248
254
  self.geog = SpatialFeatures::Utils.select_db_value("SELECT ST_Force2D('#{geog}')")
249
255
  end
250
256
 
251
- SRID_CACHE = {}
257
+ SRID_CACHE = HashWithIndifferentAccess.new
252
258
  def self.detect_srid(column_name)
253
259
  SRID_CACHE[column_name] ||= SpatialFeatures::Utils.select_db_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
254
260
  end
@@ -3,3 +3,7 @@ Mime::Type.register "application/vnd.bounds+json", :bounds
3
3
  Mime::Type.register "application/vnd.mapbox-vector-tile", :mvt
4
4
  Mime::Type.register "application/vnd.google-earth.kml+xml", :kml
5
5
  Mime::Type.register "application/vnd.google-earth.kmz", :kmz
6
+ Mime::Type.register "application/zip", :zip
7
+ Mime::Type.register "application/zip", :piz
8
+ Mime::Type.register "application/zip", :zap
9
+ Mime::Type.register "application/zip", :shpz
@@ -13,14 +13,14 @@ module SpatialFeatures
13
13
  end
14
14
 
15
15
  module ClassMethods
16
- def update_features!(skip_invalid: false, **options)
16
+ def update_features!(skip_invalid: false, allow_blank: false, **options)
17
17
  find_each do |record|
18
- record.update_features!(skip_invalid: skip_invalid, **options)
18
+ record.update_features!(skip_invalid: skip_invalid, allow_blank: allow_blank, **options)
19
19
  end
20
20
  end
21
21
  end
22
22
 
23
- def update_features!(skip_invalid: false, **options)
23
+ def update_features!(skip_invalid: false, allow_blank: false, **options)
24
24
  options = options.reverse_merge(spatial_features_options)
25
25
  tmpdir = options.fetch(:tmpdir) { Dir.mktmpdir("ruby_spatial_features") }
26
26
 
@@ -31,7 +31,7 @@ module SpatialFeatures
31
31
  return if features_cache_key_matches?(cache_key)
32
32
 
33
33
  run_callbacks :update_features do
34
- import_features(imports, skip_invalid)
34
+ features = import_features(imports, skip_invalid)
35
35
  update_features_cache_key(cache_key)
36
36
  update_features_area
37
37
 
@@ -40,11 +40,17 @@ module SpatialFeatures
40
40
  else
41
41
  update_spatial_cache(options.slice(:spatial_cache))
42
42
  end
43
+
44
+ if imports.present? && features.compact_blank.empty? && !allow_blank
45
+ raise EmptyImportError, "No spatial features were found when updating"
46
+ end
43
47
  end
44
48
  end
45
49
 
46
50
  return true
47
51
  rescue StandardError => e
52
+ raise e if e.is_a?(EmptyImportError)
53
+
48
54
  if skip_invalid
49
55
  Rails.logger.warn "Error updating #{self.class} #{self.id}. #{e.message}"
50
56
  return nil
@@ -137,7 +143,7 @@ module SpatialFeatures
137
143
  raise ImportError, "Error updating #{self.class} #{self.id}. #{errors.to_sentence}"
138
144
  end
139
145
 
140
- return features
146
+ valid
141
147
  end
142
148
 
143
149
  def features_cache_key_matches?(cache_key)
@@ -235,14 +235,10 @@ module SpatialFeatures
235
235
  end
236
236
 
237
237
  def features_area_in_square_meters
238
- @features_area_in_square_meters ||=
239
- if has_attribute?(:area)
240
- area
241
- elsif association(:aggregate_feature).loaded?
242
- aggregate_feature&.area
243
- else
244
- aggregate_features.pluck(:area).first
245
- end
238
+ @features_area_in_square_meters ||= area if has_attribute?(:area) # Calculated area column
239
+ @features_area_in_square_meters ||= features_area if has_attribute?(:features_area) # Cached area column
240
+ @features_area_in_square_meters ||= aggregate_feature&.area if association(:aggregate_feature).loaded?
241
+ @features_area_in_square_meters ||= aggregate_features.pluck(:area).first
246
242
  end
247
243
 
248
244
  def total_intersection_area_in_square_meters(other)
@@ -56,4 +56,5 @@ module SpatialFeatures
56
56
  # EXCEPTIONS
57
57
 
58
58
  class ImportError < StandardError; end
59
+ class EmptyImportError < ImportError; end
59
60
  end
@@ -3,16 +3,23 @@ require 'open-uri'
3
3
  module SpatialFeatures
4
4
  module Importers
5
5
  class File < SimpleDelegator
6
- INVALID_ARCHIVE = "Archive did not contain a .kml, .shp, .json, or .geojson file.".freeze
6
+ SUPPORTED_SPATIAL_FORMATS = %w(application/zip application/x-shp+zip application/vnd.google-earth.kml+xml application/vnd.google-earth.kmz application/vnd.geo+json).freeze
7
+ SUPPORTED_SPATIAL_FILE_EXTENSIONS = %w(.zip .zap .piz .shpz .kml .kmz .json .geojson).freeze
7
8
  SUPPORTED_FORMATS = "Supported formats are KMZ, KML, zipped ArcGIS shapefiles, ESRI JSON, and GeoJSON.".freeze
8
9
 
10
+ def self.invalid_archive!(filename)
11
+ filename = ::File.basename(filename.to_s)
12
+ raise ImportError, "#{filename} did not contain a spatial file. #{SUPPORTED_FORMATS}"
13
+ end
14
+ delegate :invalid_archive!, :to => :class
15
+
9
16
  FILE_PATTERNS = [/\.kml$/, /\.shp$/, /\.json$/, /\.geojson$/]
10
17
  def self.create_all(data, **options)
11
18
  Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).map do |file|
12
19
  new(data, **options, current_file: file)
13
20
  end
14
21
  rescue Unzip::PathNotFound
15
- raise ImportError, INVALID_ARCHIVE + " " + SUPPORTED_FORMATS
22
+ invalid_archive!(data)
16
23
  end
17
24
 
18
25
  # The File importer may be initialized multiple times by `::create_all` if it
@@ -25,10 +32,12 @@ module SpatialFeatures
25
32
  begin
26
33
  current_file ||= Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).first
27
34
  rescue Unzip::PathNotFound
28
- raise ImportError, INVALID_ARCHIVE
35
+ invalid_archive!(data)
29
36
  end
30
37
 
31
- case ::File.extname(current_file.path.downcase)
38
+ filename = current_file.path.downcase
39
+
40
+ case ::File.extname(filename)
32
41
  when '.kml'
33
42
  __setobj__(KMLFile.new(current_file, **options))
34
43
  when '.shp'
@@ -36,9 +45,15 @@ module SpatialFeatures
36
45
  when '.json', '.geojson'
37
46
  __setobj__(ESRIGeoJSON.new(current_file.path, **options))
38
47
  else
39
- raise ImportError, "Could not import file. " + SUPPORTED_FORMATS
48
+ import_error!(filename)
40
49
  end
41
50
  end
51
+
52
+ private
53
+
54
+ def import_error!(filename)
55
+ raise ImportError, "Could not import #{filename}. " + SUPPORTED_FORMATS
56
+ end
42
57
  end
43
58
  end
44
59
  end
@@ -6,6 +6,12 @@ module SpatialFeatures
6
6
  # <SimpleData name> keys that may contain <img> tags
7
7
  IMAGE_METADATA_KEYS = %w[pdfmaps_photos].freeze
8
8
 
9
+ # matches a coordinate pair with an optional altitude, including invalid altitudes like NaN
10
+ # -118.1,50.9,NaN
11
+ # -118.1,50.9,0
12
+ # -118.1,50.9
13
+ COORDINATES_WITH_ALTITUDE = /((-?\d+\.\d+,-?\d+\.\d+)(,-?[a-zA-Z\d\.]+))/.freeze
14
+
9
15
  def initialize(data, base_dir: nil, **options)
10
16
  @base_dir = base_dir
11
17
  super data, **options
@@ -52,6 +58,8 @@ module SpatialFeatures
52
58
  geom = nil
53
59
  conn = nil
54
60
 
61
+ strip_altitude(kml)
62
+
55
63
  # 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)
56
64
  #
57
65
  # We manually checkout a new connection since Rails re-uses DB connections across threads.
@@ -106,6 +114,13 @@ module SpatialFeatures
106
114
  end
107
115
  return metadata
108
116
  end
117
+
118
+ def strip_altitude(kml)
119
+ kml.css('coordinates').each do |coordinates|
120
+ next unless COORDINATES_WITH_ALTITUDE.match?(coordinates.content)
121
+ coordinates.content = coordinates.content.gsub(COORDINATES_WITH_ALTITUDE, '\2')
122
+ end
123
+ end
109
124
  end
110
125
  end
111
126
  end
@@ -20,7 +20,7 @@ module SpatialFeatures
20
20
  new(file, **options)
21
21
  end
22
22
  rescue Unzip::PathNotFound
23
- raise ImportError, INVALID_ARCHIVE
23
+ invalid_archive!(data)
24
24
  end
25
25
 
26
26
  private
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "3.2.0"
2
+ VERSION = "3.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spatial_features
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.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: 2023-08-12 00:00:00.000000000 Z
12
+ date: 2023-10-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails