spatial_features 3.2.0 → 3.4.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: 60a558353e17d6facde9b9dccccb43022f3c46af98401bf6ad4ceccee6a62b65
4
+ data.tar.gz: f2bfcad2b407e9b9329d79650fa3098260524b083b7031caeaa323012fb2fa60
5
5
  SHA512:
6
- metadata.gz: e3f28e99e814f1fcc2ba49edf8a59ff957b9f92c9e0d990f93409c61f00d4c496e7ac5f60eb3616660df720c615033f62dbe02d707c1584cc4cee7d9cb88067c
7
- data.tar.gz: 18e776f2d1bc792e7a0313d5742345e93adca24f060472bb10a36611eb3e62cf7fe8fcf2d7d520db16cd9b1755e85b4e40e75cabcb4110e25c397e42087fd9e0
6
+ metadata.gz: 065fa148ffcdd4e221f9aa117117302aa51d805e3b5d0b984d76bae81cd163681a428dec37adf9c51e993065ac93c88c75487545ac353b309ff321b3934a3b62
7
+ data.tar.gz: 0ffb13eae72eacdc0a96ec06da6e1e4ee1ffd304f36f80f72fdadebb54269ed8e208b66279c3325217729b36d14727fef03246a989ab5172625fdd3f5694e7fb
data/README.md CHANGED
@@ -31,6 +31,7 @@ Adds spatial methods to a model.
31
31
  feature_type character varying(255),
32
32
  geog geography,
33
33
  geom geometry(Geometry,4326),
34
+ geom_lowres geometry(Geometry,4326),
34
35
  tilegeom geometry(Geometry,3857),
35
36
  metadata hstore,
36
37
  area double precision,
@@ -49,6 +50,7 @@ Adds spatial methods to a model.
49
50
  CREATE INDEX index_features_on_feature_type ON features USING btree (feature_type);
50
51
  CREATE INDEX index_features_on_spatial_model_id_and_spatial_model_type ON features USING btree (spatial_model_id, spatial_model_type);
51
52
  CREATE INDEX index_features_on_geom ON features USING gist (geom);
53
+ CREATE INDEX index_features_on_geom_lowres ON features USING gist (geom_lowres);
52
54
  CREATE INDEX index_features_on_tilegeom ON features USING gist (tilegeom);
53
55
 
54
56
  CREATE TABLE spatial_caches (
@@ -188,6 +190,16 @@ queries can be optimized. Migrate existing SpatialProximity rows to this new sch
188
190
  SpatialProximity.normalize
189
191
  ```
190
192
 
193
+ ## Upgrading From 3.2/3.3 to 3.4
194
+ Features now record the source they were imported from in the new `source_identifier` column. This column is indexed and
195
+ can be used filter features by source.
196
+
197
+ ```ruby
198
+ add_column :features, :source_identifier, :string
199
+ add_index :features, :source_identifier
200
+ MyModel.update_features!(:force => true) # Force an `update_features!` will populate the source_identifier column.
201
+ ```
202
+
191
203
  ## Testing
192
204
 
193
205
  Create a postgres database:
@@ -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
@@ -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!(**options)
17
17
  find_each do |record|
18
- record.update_features!(skip_invalid: skip_invalid, **options)
18
+ record.update_features!(**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, force: false, **options)
24
24
  options = options.reverse_merge(spatial_features_options)
25
25
  tmpdir = options.fetch(:tmpdir) { Dir.mktmpdir("ruby_spatial_features") }
26
26
 
@@ -28,10 +28,10 @@ module SpatialFeatures
28
28
  imports = spatial_feature_imports(options[:import], options[:make_valid], options[:tmpdir])
29
29
  cache_key = Digest::MD5.hexdigest(imports.collect(&:cache_key).join)
30
30
 
31
- return if features_cache_key_matches?(cache_key)
31
+ return if !force && 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)
@@ -4,12 +4,14 @@ module SpatialFeatures
4
4
  module Importers
5
5
  class Base
6
6
  attr_reader :errors
7
+ attr_accessor :source_identifier # An identifier for the source of the features. Used to differentiate groups of features on the spatial model.
7
8
 
8
- def initialize(data, make_valid: false, tmpdir: nil)
9
+ def initialize(data, make_valid: false, tmpdir: nil, source_identifier: nil)
9
10
  @make_valid = make_valid
10
11
  @data = data
11
12
  @errors = []
12
13
  @tmpdir = tmpdir
14
+ @source_identifier = source_identifier
13
15
  end
14
16
 
15
17
  def features
@@ -48,7 +50,15 @@ module SpatialFeatures
48
50
 
49
51
  def build_feature(record)
50
52
  importable_image_paths = record.importable_image_paths if record.respond_to?(:importable_image_paths)
51
- Feature.new(:name => record.name, :metadata => record.metadata, :feature_type => record.feature_type, :geog => record.geog, :importable_image_paths => importable_image_paths, :make_valid => @make_valid)
53
+ Feature.new do |feature|
54
+ feature.name = record.name
55
+ feature.metadata = record.metadata
56
+ feature.feature_type = record.feature_type
57
+ feature.geog = record.geog
58
+ feature.importable_image_paths = importable_image_paths
59
+ feature.make_valid = @make_valid
60
+ feature.source_identifier = source_identifier
61
+ end
52
62
  end
53
63
  end
54
64
  end
@@ -56,4 +66,5 @@ module SpatialFeatures
56
66
  # EXCEPTIONS
57
67
 
58
68
  class ImportError < StandardError; end
69
+ class EmptyImportError < ImportError; end
59
70
  end
@@ -23,22 +23,39 @@ module SpatialFeatures
23
23
  # If no `current_file` is passed then we just take the first valid file that we find.
24
24
  def initialize(data, current_file: nil, **options)
25
25
  begin
26
- current_file ||= Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).first
26
+ @current_file = current_file || Download.open_each(data, unzip: FILE_PATTERNS, downcase: true, tmpdir: options[:tmpdir]).first
27
27
  rescue Unzip::PathNotFound
28
28
  raise ImportError, INVALID_ARCHIVE
29
29
  end
30
30
 
31
- case ::File.extname(current_file.path.downcase)
31
+ case ::File.extname(data).downcase
32
+ when '.kmz' # KMZ always has a single kml in it, so no need to show mention it
33
+ options[:source_identifier] = ::File.basename(data)
34
+ else
35
+ options[:source_identifier] = [::File.basename(data), ::File.basename(@current_file.path)].uniq.join('/')
36
+ end
37
+
38
+ case ::File.extname(filename)
32
39
  when '.kml'
33
- __setobj__(KMLFile.new(current_file, **options))
40
+ __setobj__(KMLFile.new(@current_file, **options))
34
41
  when '.shp'
35
- __setobj__(Shapefile.new(current_file, **options))
42
+ __setobj__(Shapefile.new(@current_file, **options))
36
43
  when '.json', '.geojson'
37
- __setobj__(ESRIGeoJSON.new(current_file.path, **options))
44
+ __setobj__(ESRIGeoJSON.new(@current_file.path, **options))
38
45
  else
39
- raise ImportError, "Could not import file. " + SUPPORTED_FORMATS
46
+ import_error
40
47
  end
41
48
  end
49
+
50
+ private
51
+
52
+ def import_error!
53
+ raise ImportError, "Could not import #{filename}. " + SUPPORTED_FORMATS
54
+ end
55
+
56
+ def filename
57
+ @filename ||= @current_file.path.downcase
58
+ end
42
59
  end
43
60
  end
44
61
  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
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "3.2.0"
2
+ VERSION = "3.4.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.4.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-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails