spatial_features 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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