spatial_features 3.1.0 → 3.3.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 +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.
|