spatial_features 3.2.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/app/models/abstract_feature.rb +9 -3
- data/config/initializers/mime_types.rb +4 -0
- data/lib/spatial_features/has_spatial_features/feature_import.rb +11 -5
- data/lib/spatial_features/has_spatial_features.rb +4 -8
- data/lib/spatial_features/importers/base.rb +1 -0
- data/lib/spatial_features/importers/file.rb +20 -5
- data/lib/spatial_features/importers/kml.rb +15 -0
- data/lib/spatial_features/importers/shapefile.rb +1 -1
- 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: 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
|
@@ -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
|
@@ -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
|
@@ -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
|
+
|
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)
|
@@ -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
|
@@ -25,10 +32,12 @@ module SpatialFeatures
|
|
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
42
|
__setobj__(KMLFile.new(current_file, **options))
|
34
43
|
when '.shp'
|
@@ -36,9 +45,15 @@ module SpatialFeatures
|
|
36
45
|
when '.json', '.geojson'
|
37
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,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.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: 2023-
|
12
|
+
date: 2023-10-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|