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 +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
|