spatial_features 3.2.0 → 3.4.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 +12 -0
- data/app/models/abstract_feature.rb +9 -3
- data/lib/spatial_features/has_spatial_features/feature_import.rb +12 -6
- data/lib/spatial_features/has_spatial_features.rb +4 -8
- data/lib/spatial_features/importers/base.rb +13 -2
- data/lib/spatial_features/importers/file.rb +23 -6
- data/lib/spatial_features/importers/kml.rb +15 -0
- data/lib/spatial_features/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60a558353e17d6facde9b9dccccb43022f3c46af98401bf6ad4ceccee6a62b65
|
|
4
|
+
data.tar.gz: f2bfcad2b407e9b9329d79650fa3098260524b083b7031caeaa323012fb2fa60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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!(
|
|
16
|
+
def update_features!(**options)
|
|
17
17
|
find_each do |record|
|
|
18
|
-
record.update_features!(
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
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.
|
|
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-
|
|
12
|
+
date: 2023-10-24 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rails
|