spatial_features 3.1.0 → 3.3.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 +8 -2
- data/app/models/abstract_feature.rb +13 -7
- data/app/models/spatial_proximity.rb +28 -4
- data/config/initializers/mime_types.rb +4 -0
- data/lib/spatial_features/caching.rb +6 -4
- data/lib/spatial_features/download.rb +3 -3
- data/lib/spatial_features/has_spatial_features/feature_import.rb +13 -7
- data/lib/spatial_features/has_spatial_features.rb +14 -17
- data/lib/spatial_features/importers/base.rb +1 -0
- data/lib/spatial_features/importers/file.rb +24 -9
- data/lib/spatial_features/importers/geomark.rb +2 -2
- data/lib/spatial_features/importers/kml.rb +18 -2
- data/lib/spatial_features/importers/kml_file_arcgis.rb +1 -1
- data/lib/spatial_features/importers/shapefile.rb +2 -2
- data/lib/spatial_features/venn_polygons.rb +1 -1
- data/lib/spatial_features/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ba1ab9dfa18160f48292f26a63f75031187e44cc04aff73dba792832d482063
|
4
|
+
data.tar.gz: b25d8a6f4bd0412c3d2d9dd34a57c620a0593979304ddb9061c7f3b3e2d7f834
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1be4b279b3341582262bdb8d802c0786b1eb3607cef070f2a03813b2bc987d21fb0aa92323d7b49dbec01aa8b2e128f0b9982ce356dc4836875ccd5ad89f7092
|
7
|
+
data.tar.gz: 8e70dfe6f6bad6545c29fc5dbf1406b308b9cb44ff48ebfc849469912af6cf30640c829c2baf363a6790e9207f78d3c039276c002a50ce6319ccf0d9450f9df7
|
data/README.md
CHANGED
@@ -31,7 +31,6 @@ 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),
|
35
34
|
tilegeom geometry(Geometry,3857),
|
36
35
|
metadata hstore,
|
37
36
|
area double precision,
|
@@ -50,7 +49,6 @@ Adds spatial methods to a model.
|
|
50
49
|
CREATE INDEX index_features_on_feature_type ON features USING btree (feature_type);
|
51
50
|
CREATE INDEX index_features_on_spatial_model_id_and_spatial_model_type ON features USING btree (spatial_model_id, spatial_model_type);
|
52
51
|
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);
|
54
52
|
CREATE INDEX index_features_on_tilegeom ON features USING gist (tilegeom);
|
55
53
|
|
56
54
|
CREATE TABLE spatial_caches (
|
@@ -182,6 +180,14 @@ add_index :features, :tilegeom, :using => :gist
|
|
182
180
|
Feature.update_all('tilegeom = ST_Transform(geom, 3857)')
|
183
181
|
```
|
184
182
|
|
183
|
+
## Upgrading From 3.0/3.1 to 3.2
|
184
|
+
SpatialProximity now expects the `model_a` and `model_b` records to be decided based on the name of the record type so
|
185
|
+
queries can be optimized. Migrate existing SpatialProximity rows to this new scheme by running the code below.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
SpatialProximity.normalize
|
189
|
+
```
|
190
|
+
|
185
191
|
## Testing
|
186
192
|
|
187
193
|
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')
|
@@ -69,11 +75,11 @@ class AbstractFeature < ActiveRecord::Base
|
|
69
75
|
end
|
70
76
|
|
71
77
|
def self.intersecting(other)
|
72
|
-
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').
|
78
|
+
join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').distinct
|
73
79
|
end
|
74
80
|
|
75
81
|
def self.within_distance(other, distance_in_meters)
|
76
|
-
join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).
|
82
|
+
join_other_features(other).where('ST_DWithin(features.geom_lowres, other_features.geom_lowres, ?)', distance_in_meters).distinct
|
77
83
|
end
|
78
84
|
|
79
85
|
def self.invalid(column = 'geog::geometry')
|
@@ -126,8 +132,8 @@ class AbstractFeature < ActiveRecord::Base
|
|
126
132
|
SQL
|
127
133
|
end
|
128
134
|
|
129
|
-
def self.mvt(*args)
|
130
|
-
select_sql = mvt_sql(*args)
|
135
|
+
def self.mvt(*args, **kwargs)
|
136
|
+
select_sql = mvt_sql(*args, **kwargs)
|
131
137
|
|
132
138
|
# Result is a hex string representing the desired binary output so we need to convert it to binary
|
133
139
|
result = SpatialFeatures::Utils.select_db_value(select_sql)
|
@@ -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
|
@@ -2,10 +2,34 @@ class SpatialProximity < ActiveRecord::Base
|
|
2
2
|
belongs_to :model_a, :polymorphic => true
|
3
3
|
belongs_to :model_b, :polymorphic => true
|
4
4
|
|
5
|
-
def self.between(
|
6
|
-
where
|
7
|
-
(#{SpatialFeatures::Utils.polymorphic_condition(
|
8
|
-
(#{SpatialFeatures::Utils.polymorphic_condition(scope_2, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope_1, 'model_b')})
|
5
|
+
def self.between(scope1, scope2)
|
6
|
+
where condition_sql(scope1, scope2, <<~SQL.squish)
|
7
|
+
(#{SpatialFeatures::Utils.polymorphic_condition(scope1, 'model_a')} AND #{SpatialFeatures::Utils.polymorphic_condition(scope2, 'model_b')})
|
9
8
|
SQL
|
10
9
|
end
|
10
|
+
|
11
|
+
def self.condition_sql(scope1, scope2, template, pattern_a = 'model_a', pattern_b = 'model_b')
|
12
|
+
scope1_type = SpatialFeatures::Utils.base_class_of(scope1).to_s
|
13
|
+
scope2_type = SpatialFeatures::Utils.base_class_of(scope2).to_s
|
14
|
+
|
15
|
+
if scope1_type < scope2_type
|
16
|
+
template
|
17
|
+
elsif scope1_type > scope2_type
|
18
|
+
template.gsub(pattern_a, 'model_c').gsub(pattern_b, pattern_a).gsub('model_c', pattern_b)
|
19
|
+
else
|
20
|
+
<<~SQL.squish
|
21
|
+
(#{template}) OR (#{template.gsub(pattern_a, 'model_c').gsub(pattern_b, pattern_a).gsub('model_c', pattern_b)})
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ensure the 'earliest' model is always model a
|
27
|
+
def self.normalize
|
28
|
+
unnormalized
|
29
|
+
.update_all('model_a_type = model_b_type, model_b_type = model_a_type, model_a_id = model_b_id, model_b_id = model_a_id')
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.unnormalized
|
33
|
+
where('model_a_type > model_b_type OR (model_a_type = model_b_type AND model_a_id > model_b_id)')
|
34
|
+
end
|
11
35
|
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
|
@@ -84,11 +84,13 @@ module SpatialFeatures
|
|
84
84
|
results.each do |id, distance, area|
|
85
85
|
klass_record.id = id
|
86
86
|
SpatialProximity.create! do |proximity|
|
87
|
+
# Always make the spatial model earliest type and id be model a so we can optimize queries
|
88
|
+
data = [[Utils.base_class(record).to_s, record.id], [Utils.base_class(klass_record).to_s, klass_record.id]]
|
89
|
+
data.sort!
|
90
|
+
|
87
91
|
# Set id and type instead of model to avoid autosaving the klass_record
|
88
|
-
proximity.model_a_id =
|
89
|
-
proximity.
|
90
|
-
proximity.model_b_id = klass_record.id
|
91
|
-
proximity.model_b_type = Utils.base_class(klass_record)
|
92
|
+
proximity.model_a_type, proximity.model_a_id = data.first
|
93
|
+
proximity.model_b_type, proximity.model_b_id = data.second
|
92
94
|
proximity.distance_in_meters = distance
|
93
95
|
proximity.intersection_area_in_square_meters = area
|
94
96
|
end
|
@@ -4,14 +4,14 @@ module SpatialFeatures
|
|
4
4
|
module Download
|
5
5
|
# file can be a url, path, or file, any of which can return be a zipped archive
|
6
6
|
def self.open(file)
|
7
|
-
file =
|
7
|
+
file = URI.open(file)
|
8
8
|
file = normalize_file(file) if file.is_a?(StringIO)
|
9
9
|
return file
|
10
10
|
end
|
11
11
|
|
12
12
|
# file can be a url, path, or file, any of which can return be a zipped archive
|
13
|
-
def self.open_each(
|
14
|
-
file = Download.open(
|
13
|
+
def self.open_each(path_or_url, unzip: nil, **unzip_options)
|
14
|
+
file = Download.open(path_or_url)
|
15
15
|
files = if unzip && Unzip.is_zip?(file)
|
16
16
|
find_in_zip(file, find: unzip, **unzip_options)
|
17
17
|
else
|
@@ -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
|
-
end
|
44
43
|
|
45
|
-
|
44
|
+
if imports.present? && features.compact_blank.empty? && !allow_blank
|
45
|
+
raise EmptyImportError, "No spatial features were found when updating"
|
46
|
+
end
|
47
|
+
end
|
46
48
|
end
|
49
|
+
|
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)
|
@@ -13,14 +13,14 @@ module SpatialFeatures
|
|
13
13
|
has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
|
14
14
|
has_one :aggregate_feature, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete
|
15
15
|
|
16
|
-
scope :with_features, lambda { joins(:features).
|
16
|
+
scope :with_features, lambda { joins(:features).distinct }
|
17
17
|
scope :without_features, lambda { joins("LEFT OUTER JOIN features ON features.spatial_model_type = '#{Utils.base_class(name)}' AND features.spatial_model_id = #{table_name}.id").where("features.id IS NULL") }
|
18
18
|
scope :include_bounds, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :north, :east, :south, :west) }
|
19
19
|
scope :include_area, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :area) }
|
20
20
|
|
21
|
-
scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).
|
21
|
+
scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).distinct }
|
22
22
|
scope :without_spatial_cache, lambda {|klass| joins("LEFT OUTER JOIN #{SpatialCache.table_name} ON #{SpatialCache.table_name}.spatial_model_id = #{table_name}.id AND #{SpatialCache.table_name}.spatial_model_type = '#{Utils.base_class(name)}' and intersection_model_type IN ('#{Utils.class_name_with_ancestors(klass).join("','") }')").where("#{SpatialCache.table_name}.spatial_model_id IS NULL") }
|
23
|
-
scope :with_stale_spatial_cache, lambda { joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").
|
23
|
+
scope :with_stale_spatial_cache, lambda { has_spatial_features_hash? ? joins(:spatial_caches).where("#{table_name}.features_hash != spatial_caches.features_hash").distinct : none }
|
24
24
|
|
25
25
|
has_many :spatial_caches, :as => :spatial_model, :dependent => :delete_all, :class_name => 'SpatialCache'
|
26
26
|
has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
|
@@ -136,10 +136,8 @@ module SpatialFeatures
|
|
136
136
|
other_class = Utils.base_class_of(other)
|
137
137
|
self_class = Utils.base_class_of(self)
|
138
138
|
|
139
|
-
joins <<~SQL
|
140
|
-
|
141
|
-
ON (spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
|
142
|
-
OR (spatial_proximities.model_b_type = '#{self_class}' AND spatial_proximities.model_b_id = #{table_name}.id AND spatial_proximities.model_a_type = '#{other_class}' AND spatial_proximities.model_a_id IN (#{Utils.id_sql(other)}))
|
139
|
+
joins "INNER JOIN spatial_proximities ON " + SpatialProximity.condition_sql(self, other, <<~SQL.squish)
|
140
|
+
(spatial_proximities.model_a_type = '#{self_class}' AND spatial_proximities.model_a_id = #{table_name}.id AND spatial_proximities.model_b_type = '#{other_class}' AND spatial_proximities.model_b_id IN (#{Utils.id_sql(other)}))
|
143
141
|
SQL
|
144
142
|
end
|
145
143
|
|
@@ -228,20 +226,19 @@ module SpatialFeatures
|
|
228
226
|
end
|
229
227
|
|
230
228
|
def total_intersection_area_percentage(klass)
|
231
|
-
|
229
|
+
total_area = features_area_in_square_meters
|
232
230
|
|
233
|
-
|
231
|
+
return if total_area.nil?
|
232
|
+
return 0.0 if total_area.zero?
|
233
|
+
|
234
|
+
((total_intersection_area_in_square_meters(klass) / total_area) * 100).round(1)
|
234
235
|
end
|
235
236
|
|
236
237
|
def features_area_in_square_meters
|
237
|
-
@features_area_in_square_meters ||=
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
aggregate_feature&.area
|
242
|
-
else
|
243
|
-
aggregate_features.pluck(:area).first
|
244
|
-
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
|
245
242
|
end
|
246
243
|
|
247
244
|
def total_intersection_area_in_square_meters(other)
|
@@ -3,16 +3,23 @@ require 'open-uri'
|
|
3
3
|
module SpatialFeatures
|
4
4
|
module Importers
|
5
5
|
class File < SimpleDelegator
|
6
|
-
|
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
|
-
|
22
|
+
invalid_archive!(data)
|
16
23
|
end
|
17
24
|
|
18
25
|
# The File importer may be initialized multiple times by `::create_all` if it
|
@@ -21,24 +28,32 @@ module SpatialFeatures
|
|
21
28
|
# processed.
|
22
29
|
#
|
23
30
|
# If no `current_file` is passed then we just take the first valid file that we find.
|
24
|
-
def initialize(data,
|
31
|
+
def initialize(data, current_file: nil, **options)
|
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
|
-
|
35
|
+
invalid_archive!(data)
|
29
36
|
end
|
30
37
|
|
31
|
-
|
38
|
+
filename = current_file.path.downcase
|
39
|
+
|
40
|
+
case ::File.extname(filename)
|
32
41
|
when '.kml'
|
33
|
-
__setobj__(KMLFile.new(current_file,
|
42
|
+
__setobj__(KMLFile.new(current_file, **options))
|
34
43
|
when '.shp'
|
35
|
-
__setobj__(Shapefile.new(current_file,
|
44
|
+
__setobj__(Shapefile.new(current_file, **options))
|
36
45
|
when '.json', '.geojson'
|
37
|
-
__setobj__(ESRIGeoJSON.new(current_file.path,
|
46
|
+
__setobj__(ESRIGeoJSON.new(current_file.path, **options))
|
38
47
|
else
|
39
|
-
|
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,9 +6,15 @@ module SpatialFeatures
|
|
6
6
|
# <SimpleData name> keys that may contain <img> tags
|
7
7
|
IMAGE_METADATA_KEYS = %w[pdfmaps_photos].freeze
|
8
8
|
|
9
|
-
|
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
|
+
|
15
|
+
def initialize(data, base_dir: nil, **options)
|
10
16
|
@base_dir = base_dir
|
11
|
-
super data, **
|
17
|
+
super data, **options
|
12
18
|
end
|
13
19
|
|
14
20
|
private
|
@@ -39,6 +45,7 @@ module SpatialFeatures
|
|
39
45
|
@kml_document ||= begin
|
40
46
|
doc = Nokogiri::XML(@data)
|
41
47
|
raise ImportError, "Invalid KML document (root node was '#{doc.root&.name}')" unless doc.root&.name.to_s.casecmp?('kml')
|
48
|
+
raise ImportError, "NetworkLink elements are not supported" unless doc.search('NetworkLink').empty?
|
42
49
|
doc
|
43
50
|
end
|
44
51
|
end
|
@@ -51,6 +58,8 @@ module SpatialFeatures
|
|
51
58
|
geom = nil
|
52
59
|
conn = nil
|
53
60
|
|
61
|
+
strip_altitude(kml)
|
62
|
+
|
54
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)
|
55
64
|
#
|
56
65
|
# We manually checkout a new connection since Rails re-uses DB connections across threads.
|
@@ -105,6 +114,13 @@ module SpatialFeatures
|
|
105
114
|
end
|
106
115
|
return metadata
|
107
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
|
108
124
|
end
|
109
125
|
end
|
110
126
|
end
|
@@ -6,7 +6,7 @@ module SpatialFeatures
|
|
6
6
|
class Shapefile < Base
|
7
7
|
class_attribute :default_proj4_projection
|
8
8
|
|
9
|
-
def initialize(data,
|
9
|
+
def initialize(data, proj4: nil, **options)
|
10
10
|
super(data, **options)
|
11
11
|
@proj4 = proj4
|
12
12
|
end
|
@@ -20,7 +20,7 @@ module SpatialFeatures
|
|
20
20
|
new(file, **options)
|
21
21
|
end
|
22
22
|
rescue Unzip::PathNotFound
|
23
|
-
|
23
|
+
invalid_archive!(data)
|
24
24
|
end
|
25
25
|
|
26
26
|
private
|
@@ -56,7 +56,7 @@ module SpatialFeatures
|
|
56
56
|
# Instantiate objects to hold the kml and records for each venn polygon
|
57
57
|
polygons.group_by{|row| row['kml']}.collect do |kml, rows|
|
58
58
|
# Uniq on row id in case a single record had self intersecting multi geometry, which would cause it to appear duplicated on a single venn polygon
|
59
|
-
records = rows.uniq{|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
|
59
|
+
records = rows.uniq {|row| row.values_at('id', 'type') }.collect{|row| eager_load_hash.fetch(row['type']).detect{|record| record.id == row['id'].to_i } }
|
60
60
|
OpenStruct.new(:kml => kml, :records => records)
|
61
61
|
end
|
62
62
|
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.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:
|
12
|
+
date: 2023-10-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -203,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
203
|
- !ruby/object:Gem::Version
|
204
204
|
version: '0'
|
205
205
|
requirements: []
|
206
|
-
rubygems_version: 3.
|
206
|
+
rubygems_version: 3.3.23
|
207
207
|
signing_key:
|
208
208
|
specification_version: 4
|
209
209
|
summary: Adds spatial methods to a model.
|