spatial_features 2.20.0 → 3.0.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 +24 -6
- data/app/models/abstract_feature.rb +79 -17
- data/app/models/feature.rb +0 -10
- data/config/initializers/mime_types.rb +3 -0
- data/lib/spatial_features/controller_helpers/spatial_extensions.rb +20 -10
- data/lib/spatial_features/has_spatial_features/feature_import.rb +7 -0
- data/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb +2 -2
- data/lib/spatial_features/has_spatial_features.rb +40 -20
- data/lib/spatial_features/importers/esri_geo_json.rb +27 -0
- data/lib/spatial_features/importers/file.rb +9 -5
- data/lib/spatial_features/importers/{geojson.rb → geo_json.rb} +10 -4
- data/lib/spatial_features/utils.rb +1 -1
- data/lib/spatial_features/version.rb +1 -1
- data/lib/spatial_features.rb +3 -2
- metadata +20 -5
- data/lib/spatial_features/importers/json_arcgis.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d3fe76f0ce82837de428e401ce4f6a20a84b97acb2085374a8bf8fe1daba5c8
|
4
|
+
data.tar.gz: fed058553913c07b27838424dc07b1b63677f28798fae7d5534b55de7c1aa713
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f6aebd3553502127a91f68d5f692bd6311a8c7af0aca22bccc54cec712aa8371696b0f8e19c0c7aaffa9e98c1ee809d478612a5f7c2441452a202bc9f53280a
|
7
|
+
data.tar.gz: fcf4f4eb0fc85f82ff726ec950936b9c45bdd9aaf85eb06067acac71b250185b6640e89f3536e5e5e1759b93efbee66e855460964ba112aa4f26a77cefae0abe
|
data/README.md
CHANGED
@@ -30,10 +30,9 @@ Adds spatial methods to a model.
|
|
30
30
|
name character varying(255),
|
31
31
|
feature_type character varying(255),
|
32
32
|
geog geography,
|
33
|
-
geom geometry(Geometry,
|
34
|
-
geom_lowres geometry(Geometry,
|
35
|
-
|
36
|
-
kml_lowres text,
|
33
|
+
geom geometry(Geometry,4326),
|
34
|
+
geom_lowres geometry(Geometry,4326),
|
35
|
+
tilegeom geometry(Geometry,3857),
|
37
36
|
metadata hstore,
|
38
37
|
area double precision,
|
39
38
|
north numeric(9,6),
|
@@ -41,7 +40,6 @@ Adds spatial methods to a model.
|
|
41
40
|
south numeric(9,6),
|
42
41
|
west numeric(9,6),
|
43
42
|
centroid geography,
|
44
|
-
kml_centroid text
|
45
43
|
);
|
46
44
|
|
47
45
|
CREATE SEQUENCE features_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
|
@@ -53,6 +51,7 @@ Adds spatial methods to a model.
|
|
53
51
|
CREATE INDEX index_features_on_spatial_model_id_and_spatial_model_type ON features USING btree (spatial_model_id, spatial_model_type);
|
54
52
|
CREATE INDEX index_features_on_geom ON features USING gist (geom);
|
55
53
|
CREATE INDEX index_features_on_geom_lowres ON features USING gist (geom_lowres);
|
54
|
+
CREATE INDEX index_features_on_tilegeom ON features USING gist (tilegeom);
|
56
55
|
|
57
56
|
CREATE TABLE spatial_caches (
|
58
57
|
id integer NOT NULL,
|
@@ -118,7 +117,7 @@ def ImageImporter
|
|
118
117
|
end
|
119
118
|
|
120
119
|
class Location < ActiveRecord::Base
|
121
|
-
has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => '
|
120
|
+
has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => 'ESRIGeoJSON' },
|
122
121
|
:image_handlers => ['ImageImporter']
|
123
122
|
|
124
123
|
def remote_kml_url
|
@@ -163,3 +162,22 @@ Feature.reset_column_information
|
|
163
162
|
AbstractFeature.update_all(:type => 'Feature')
|
164
163
|
Feature.refresh_aggregates
|
165
164
|
```
|
165
|
+
|
166
|
+
## Upgrading From 2.8.x to 3.0
|
167
|
+
Cached KML layers are no longer generated as Mapbox Vector Tile is the primary expected output. The columns can be left
|
168
|
+
in place or you can remove the KML cache columns.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
remove_column :features, :kml
|
172
|
+
remove_column :features, :kml_lowres
|
173
|
+
remove_column :features, :kml_centroid
|
174
|
+
```
|
175
|
+
|
176
|
+
A new `tilegeom` column has been added to support MVT tile output, which is now the preferred map output format instead
|
177
|
+
of GeoJSON or KML. It keeps memory usage low and is fast to generate.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
add_column :features, :tilegeom, :geometry
|
181
|
+
add_index :features, :tilegeom, :using => :gist
|
182
|
+
Feature.update_all('tilegeom = ST_Transform(geom, 3857)')
|
183
|
+
```
|
@@ -117,7 +117,8 @@ class AbstractFeature < ActiveRecord::Base
|
|
117
117
|
SQL
|
118
118
|
|
119
119
|
update_all <<-SQL.squish
|
120
|
-
geom_lowres = ST_SimplifyPreserveTopology(geom, #{options.fetch(:lowres_simplification, lowres_simplification)})
|
120
|
+
geom_lowres = ST_SimplifyPreserveTopology(geom, #{options.fetch(:lowres_simplification, lowres_simplification)}),
|
121
|
+
tilegeom = ST_Transform(geom, 3857)
|
121
122
|
SQL
|
122
123
|
|
123
124
|
invalid('geom_lowres').update_all <<-SQL.squish
|
@@ -125,7 +126,44 @@ class AbstractFeature < ActiveRecord::Base
|
|
125
126
|
SQL
|
126
127
|
end
|
127
128
|
|
128
|
-
def self.
|
129
|
+
def self.mvt(*args)
|
130
|
+
select_sql = mvt_sql(*args)
|
131
|
+
|
132
|
+
# Result is a hex string representing the desired binary output so we need to convert it to binary
|
133
|
+
result = SpatialFeatures::Utils.select_db_value(select_sql)
|
134
|
+
result.remove!(/^\\x/)
|
135
|
+
result = result.scan(/../).map(&:hex).pack('c*')
|
136
|
+
|
137
|
+
return result
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.mvt_sql(tile_x, tile_y, zoom, properties: true, centroids: false, metadata: {}, scope: nil)
|
141
|
+
if centroids
|
142
|
+
column = 'ST_Transform(centroid::geometry, 3857)' # MVT works in SRID 3857
|
143
|
+
else
|
144
|
+
column = 'tilegeom'
|
145
|
+
end
|
146
|
+
|
147
|
+
subquery = select(:id)
|
148
|
+
.select("ST_AsMVTGeom(#{column}, ST_TileEnvelope(#{zoom}, #{tile_x}, #{tile_y}), extent => 4096, buffer => 64) AS geom")
|
149
|
+
.where("#{column} && ST_TileEnvelope(#{zoom}, #{tile_x}, #{tile_y}, margin => (64.0 / 4096))")
|
150
|
+
.order(:id)
|
151
|
+
|
152
|
+
# Merge additional scopes in to allow joins and other columns to be included in the feature output
|
153
|
+
subquery = subquery.merge(scope) unless scope.nil?
|
154
|
+
|
155
|
+
# Add metadata
|
156
|
+
metadata.each do |column, value|
|
157
|
+
subquery = subquery.select("#{value} AS #{column}")
|
158
|
+
end
|
159
|
+
|
160
|
+
select_sql = <<~SQL
|
161
|
+
SELECT ST_AsMVT(mvtgeom.*, 'default', 4096, 'geom', 'id') AS mvt
|
162
|
+
FROM (#{subquery.to_sql}) mvtgeom
|
163
|
+
SQL
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.geojson(lowres: false, precision: 6, properties: true, srid: 4326, centroids: false, features_only: false, include_record_identifiers: false) # default srid is 4326 so output is Google Maps compatible
|
129
167
|
if centroids
|
130
168
|
column = 'centroid'
|
131
169
|
elsif lowres
|
@@ -134,27 +172,52 @@ class AbstractFeature < ActiveRecord::Base
|
|
134
172
|
column = 'geog'
|
135
173
|
end
|
136
174
|
|
137
|
-
properties_sql =
|
138
|
-
|
175
|
+
properties_sql = []
|
176
|
+
|
177
|
+
if include_record_identifiers
|
178
|
+
properties_sql << "hstore(ARRAY['feature_name', name::varchar, 'feature_id', id::varchar, 'spatial_model_type', spatial_model_type::varchar, 'spatial_model_id', spatial_model_id::varchar])"
|
179
|
+
end
|
180
|
+
|
181
|
+
if properties
|
182
|
+
properties_sql << "metadata"
|
183
|
+
end
|
184
|
+
|
185
|
+
if properties.is_a?(Hash)
|
186
|
+
properties_sql << <<~SQL
|
187
|
+
hstore(ARRAY[#{properties.flatten.map {|e| "'#{e.to_s}'" }.join(',')}])
|
188
|
+
SQL
|
189
|
+
end
|
190
|
+
|
191
|
+
properties_sql = <<~SQL if properties_sql.present?
|
192
|
+
, 'properties', hstore_to_json(#{properties_sql.join(' || ')})
|
139
193
|
SQL
|
140
194
|
|
141
195
|
sql = <<~SQL
|
196
|
+
json_agg(
|
197
|
+
json_build_object(
|
198
|
+
'type', 'Feature',
|
199
|
+
'geometry', ST_AsGeoJSON(#{column}, #{precision})::json
|
200
|
+
#{properties_sql}
|
201
|
+
)
|
202
|
+
)
|
203
|
+
SQL
|
204
|
+
|
205
|
+
sql = <<~SQL unless features_only
|
142
206
|
json_build_object(
|
143
207
|
'type', 'FeatureCollection',
|
144
|
-
'features',
|
145
|
-
json_build_object(
|
146
|
-
'type', 'Feature',
|
147
|
-
'geometry', ST_AsGeoJSON(#{column}, #{precision})::json
|
148
|
-
#{properties_sql}
|
149
|
-
)
|
150
|
-
)
|
208
|
+
'features', #{sql}
|
151
209
|
)
|
152
210
|
SQL
|
153
211
|
SpatialFeatures::Utils.select_db_value(all.select(sql))
|
154
212
|
end
|
155
213
|
|
156
|
-
def
|
157
|
-
|
214
|
+
def self.bounds
|
215
|
+
values = pluck('MAX(north) AS north, MAX(east) AS east, MIN(south) AS south, MIN(west) AS west').first
|
216
|
+
[:north, :east, :south, :west].zip(values).to_h.with_indifferent_access.transform_values!(&:to_f) if values&.compact.present?
|
217
|
+
end
|
218
|
+
|
219
|
+
def bounds
|
220
|
+
slice(:north, :east, :south, :west).with_indifferent_access.transform_values!(&:to_f)
|
158
221
|
end
|
159
222
|
|
160
223
|
def cache_derivatives(*args)
|
@@ -162,9 +225,8 @@ class AbstractFeature < ActiveRecord::Base
|
|
162
225
|
end
|
163
226
|
|
164
227
|
def kml(options = {})
|
165
|
-
|
166
|
-
|
167
|
-
return geometry
|
228
|
+
column = options[:lowres] ? 'geom_lowres' : 'geog'
|
229
|
+
return SpatialFeatures::Utils.select_db_value(self.class.where(:id => id).select("ST_AsKML(#{column}, 6)"))
|
168
230
|
end
|
169
231
|
|
170
232
|
def geojson(*args)
|
@@ -210,7 +272,7 @@ class AbstractFeature < ActiveRecord::Base
|
|
210
272
|
|
211
273
|
def geometry_validation_message
|
212
274
|
klass = self.class.base_class # Use the base class because we don't want to have to include a type column in our select
|
213
|
-
error = klass.connection.select_one(klass.unscoped.invalid.from("(SELECT '#{sanitize_input_for_sql(self.geog)}'::geometry AS geog) #{klass.table_name}"))
|
275
|
+
error = klass.connection.select_one(klass.unscoped.invalid.from("(SELECT '#{sanitize_input_for_sql(self.geog)}'::geography::geometry AS geog) #{klass.table_name}")) # Ensure we cast to geography because the geog attribute value may not have been coerced to geography yet, so we want it to apply the +-180/90 bounds to any odd geometry that will happen when we save to the database
|
214
276
|
return error.fetch('invalid_geometry_message') if error
|
215
277
|
end
|
216
278
|
|
data/app/models/feature.rb
CHANGED
@@ -17,16 +17,6 @@ class Feature < AbstractFeature
|
|
17
17
|
|
18
18
|
attr_accessor :importable_image_paths # :nodoc:
|
19
19
|
|
20
|
-
# Features are used for display so we also cache their KML representation
|
21
|
-
def self.cache_derivatives(options = {})
|
22
|
-
super
|
23
|
-
update_all <<-SQL.squish
|
24
|
-
kml = ST_AsKML(geog, 6),
|
25
|
-
kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
|
26
|
-
kml_centroid = ST_AsKML(centroid)
|
27
|
-
SQL
|
28
|
-
end
|
29
|
-
|
30
20
|
def self.defer_aggregate_refresh(&block)
|
31
21
|
start_at = Feature.maximum(:id).to_i + 1
|
32
22
|
output = without_aggregate_refresh(&block)
|
@@ -12,11 +12,13 @@ module SpatialExtensions
|
|
12
12
|
@nearby_records = scope_for_search(scope).within_buffer(target, distance, :distance => true, :intersection_area => true).order('distance_in_meters ASC, intersection_area_in_square_meters DESC, id ASC')
|
13
13
|
@target = target
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
if block_given?
|
16
|
+
block.call(@nearby_records)
|
17
|
+
else
|
18
|
+
respond_to do |format|
|
19
|
+
format.html { render :template => 'shared/spatial/feature_proximity', :layout => false }
|
20
|
+
format.kml { render :template => 'shared/spatial/feature_proximity' }
|
21
|
+
end
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
@@ -25,10 +27,12 @@ module SpatialExtensions
|
|
25
27
|
@klass = klass_for_search(scope)
|
26
28
|
@target = target
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
if block_given?
|
31
|
+
block.call(@venn_polygons)
|
32
|
+
else
|
33
|
+
respond_to do |format|
|
34
|
+
format.kml { render :template => 'shared/spatial/feature_venn_polygons' }
|
35
|
+
end
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
@@ -37,6 +41,12 @@ module SpatialExtensions
|
|
37
41
|
end
|
38
42
|
|
39
43
|
def scope_for_search(scope)
|
40
|
-
params.key?(:ids)
|
44
|
+
if params.key?(:ids)
|
45
|
+
ids = params[:ids]
|
46
|
+
ids = ids.split(/\D/) if ids.is_a?(String)
|
47
|
+
scope.where(:id => ids)
|
48
|
+
else
|
49
|
+
scope
|
50
|
+
end
|
41
51
|
end
|
42
52
|
end
|
@@ -42,6 +42,13 @@ module SpatialFeatures
|
|
42
42
|
|
43
43
|
return true
|
44
44
|
end
|
45
|
+
rescue StandardError => e
|
46
|
+
if skip_invalid
|
47
|
+
Rails.logger.warn "Error updating #{self.class} #{self.id}. #{e.message}"
|
48
|
+
return nil
|
49
|
+
else
|
50
|
+
raise ImportError, e.message, e.backtrace
|
51
|
+
end
|
45
52
|
end
|
46
53
|
|
47
54
|
def update_features_cache_key(cache_key)
|
@@ -41,8 +41,8 @@ module SpatialFeatures
|
|
41
41
|
spatial_processing_jobs('update_features!').where.not(failed_at: nil)
|
42
42
|
end
|
43
43
|
|
44
|
-
def spatial_processing_jobs(
|
45
|
-
Delayed::Job.where(
|
44
|
+
def spatial_processing_jobs(method_name = nil)
|
45
|
+
Delayed::Job.where('queue LIKE ?', "#{spatial_processing_queue_name}#{method_name}%")
|
46
46
|
end
|
47
47
|
|
48
48
|
private
|
@@ -15,6 +15,8 @@ module SpatialFeatures
|
|
15
15
|
|
16
16
|
scope :with_features, lambda { joins(:features).uniq }
|
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
|
+
scope :include_bounds, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :north, :east, :south, :west) }
|
19
|
+
scope :include_area, lambda { SQLHelpers.append_select(joins(:aggregate_feature), :area) }
|
18
20
|
|
19
21
|
scope :with_spatial_cache, lambda {|klass| joins(:spatial_caches).where(:spatial_caches => { :intersection_model_type => Utils.class_name_with_ancestors(klass) }).uniq }
|
20
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") }
|
@@ -31,6 +33,16 @@ module SpatialFeatures
|
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
36
|
+
module SQLHelpers
|
37
|
+
# Add select fields without replacing the implicit `table_name.*`
|
38
|
+
def self.append_select(scope, *fields)
|
39
|
+
if(!scope.select_values.any?)
|
40
|
+
fields.unshift(scope.arel_table[Arel.star])
|
41
|
+
end
|
42
|
+
scope.select(*fields)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
34
46
|
module ClassMethods
|
35
47
|
def acts_like_spatial_features?
|
36
48
|
true
|
@@ -44,7 +56,7 @@ module SpatialFeatures
|
|
44
56
|
within_buffer(other, 0, options)
|
45
57
|
end
|
46
58
|
|
47
|
-
def within_buffer(other, buffer_in_meters = 0, options
|
59
|
+
def within_buffer(other, buffer_in_meters = 0, **options)
|
48
60
|
return none if other.is_a?(ActiveRecord::Base) && other.new_record?
|
49
61
|
|
50
62
|
# Cache only works on single records, not scopes.
|
@@ -68,6 +80,10 @@ module SpatialFeatures
|
|
68
80
|
features.points
|
69
81
|
end
|
70
82
|
|
83
|
+
def bounds
|
84
|
+
aggregate_features.bounds
|
85
|
+
end
|
86
|
+
|
71
87
|
def features
|
72
88
|
type = base_class.to_s # Rails stores polymorphic foreign keys as the base class
|
73
89
|
if all == unscoped
|
@@ -97,24 +113,22 @@ module SpatialFeatures
|
|
97
113
|
end
|
98
114
|
|
99
115
|
def area_in_square_meters
|
100
|
-
|
116
|
+
abstract_features.area
|
101
117
|
end
|
102
118
|
|
103
119
|
private
|
104
120
|
|
105
121
|
def cached_within_buffer_scope(other, buffer_in_meters, options)
|
106
122
|
options = options.reverse_merge(:columns => "#{table_name}.*")
|
107
|
-
|
108
|
-
# Don't use the cache if it doesn't exist
|
109
|
-
unless other.class.unscoped { other.spatial_cache_for?(Utils.class_of(self), buffer_in_meters) } # Unscope so if we're checking for same class intersections the scope doesn't affect this lookup
|
110
|
-
return none.extending(UncachedResult)
|
111
|
-
end
|
112
|
-
|
113
123
|
scope = cached_spatial_join(other)
|
114
124
|
scope = scope.select(options[:columns])
|
115
125
|
scope = scope.where("spatial_proximities.distance_in_meters <= ?", buffer_in_meters) if buffer_in_meters
|
116
126
|
scope = scope.select("spatial_proximities.distance_in_meters") if options[:distance]
|
117
127
|
scope = scope.select("spatial_proximities.intersection_area_in_square_meters") if options[:intersection_area]
|
128
|
+
|
129
|
+
# Don't use the cache if it doesn't exist, but make sure we keep the same select columns in case chained scopes reference them, e.g. order by
|
130
|
+
scope = scope.none.extending(UncachedResult) unless other.class.unscoped { other.spatial_cache_for?(Utils.class_of(self), buffer_in_meters) } # Unscope so if we're checking for same class intersections the scope doesn't affect this lookup
|
131
|
+
|
118
132
|
return scope
|
119
133
|
end
|
120
134
|
|
@@ -174,7 +188,7 @@ module SpatialFeatures
|
|
174
188
|
end
|
175
189
|
|
176
190
|
def features_cache_key
|
177
|
-
"#{self.class.name}/#{id}-#{has_spatial_features_hash? ? features_hash : aggregate_feature.cache_key}"
|
191
|
+
"#{self.class.name}/#{id}-#{has_spatial_features_hash? ? features_hash : (aggregate_feature || features).cache_key}"
|
178
192
|
end
|
179
193
|
|
180
194
|
def polygons?
|
@@ -202,12 +216,15 @@ module SpatialFeatures
|
|
202
216
|
end
|
203
217
|
|
204
218
|
def bounds
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
219
|
+
@bounds ||=
|
220
|
+
if has_attribute?(:north) && has_attribute?(:east) && has_attribute?(:south) && has_attribute?(:west)
|
221
|
+
slice(:north, :east, :south, :west).with_indifferent_access.transform_values!(&:to_f)
|
222
|
+
elsif association(:aggregate_feature).loaded?
|
223
|
+
# Aggregate features can be very large and take a while to load. Avoid loading one just to load the bounds.
|
224
|
+
aggregate_feature&.bounds
|
225
|
+
else
|
226
|
+
aggregate_features.bounds
|
227
|
+
end
|
211
228
|
end
|
212
229
|
|
213
230
|
def total_intersection_area_percentage(klass)
|
@@ -217,11 +234,14 @@ module SpatialFeatures
|
|
217
234
|
end
|
218
235
|
|
219
236
|
def features_area_in_square_meters
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
237
|
+
@features_area_in_square_meters ||=
|
238
|
+
if has_attribute?(:area)
|
239
|
+
area
|
240
|
+
elsif association(:aggregate_feature).loaded?
|
241
|
+
aggregate_feature&.area
|
242
|
+
else
|
243
|
+
aggregate_features.pluck(:area).first
|
244
|
+
end
|
225
245
|
end
|
226
246
|
|
227
247
|
def total_intersection_area_in_square_meters(other)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'spatial_features/importers/geo_json'
|
4
|
+
|
5
|
+
module SpatialFeatures
|
6
|
+
module Importers
|
7
|
+
class ESRIGeoJSON < GeoJSON
|
8
|
+
def parsed_geojson
|
9
|
+
@parsed_geojson ||= JSON.parse(geojson)
|
10
|
+
end
|
11
|
+
|
12
|
+
def geojson
|
13
|
+
@geojson ||= esri_json_to_geojson(@data)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def esri_json_to_geojson(url)
|
19
|
+
if URI.parse(url).relative?
|
20
|
+
`ogr2ogr -t_srs EPSG:4326 -f GeoJSON /dev/stdout "#{url}"` # It is a local file path
|
21
|
+
else
|
22
|
+
`ogr2ogr -t_srs EPSG:4326 -f GeoJSON /dev/stdout "#{url}" OGRGeoJSON`
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -3,14 +3,16 @@ require 'open-uri'
|
|
3
3
|
module SpatialFeatures
|
4
4
|
module Importers
|
5
5
|
class File < SimpleDelegator
|
6
|
-
INVALID_ARCHIVE = "Archive did not contain a .kml
|
6
|
+
INVALID_ARCHIVE = "Archive did not contain a .kml, .shp, .json, or .geojson file.".freeze
|
7
|
+
SUPPORTED_FORMATS = "Supported formats are KMZ, KML, zipped ArcGIS shapefiles, ESRI JSON, and GeoJSON.".freeze
|
7
8
|
|
9
|
+
FILE_PATTERNS = [/\.kml$/, /\.shp$/, /\.json$/, /\.geojson$/]
|
8
10
|
def self.create_all(data, **options)
|
9
|
-
Download.open_each(data, unzip:
|
11
|
+
Download.open_each(data, unzip: FILE_PATTERNS, downcase: true).map do |file|
|
10
12
|
new(data, **options, current_file: file)
|
11
13
|
end
|
12
14
|
rescue Unzip::PathNotFound
|
13
|
-
raise ImportError, INVALID_ARCHIVE
|
15
|
+
raise ImportError, INVALID_ARCHIVE + " " + SUPPORTED_FORMATS
|
14
16
|
end
|
15
17
|
|
16
18
|
# The File importer may be initialized multiple times by `::create_all` if it
|
@@ -21,7 +23,7 @@ module SpatialFeatures
|
|
21
23
|
# If no `current_file` is passed then we just take the first valid file that we find.
|
22
24
|
def initialize(data, *args, current_file: nil, **options)
|
23
25
|
begin
|
24
|
-
current_file ||= Download.open_each(data, unzip:
|
26
|
+
current_file ||= Download.open_each(data, unzip: FILE_PATTERNS, downcase: true).first
|
25
27
|
rescue Unzip::PathNotFound
|
26
28
|
raise ImportError, INVALID_ARCHIVE
|
27
29
|
end
|
@@ -31,8 +33,10 @@ module SpatialFeatures
|
|
31
33
|
__setobj__(KMLFile.new(current_file, *args, **options))
|
32
34
|
when '.shp'
|
33
35
|
__setobj__(Shapefile.new(current_file, *args, **options))
|
36
|
+
when '.json', '.geojson'
|
37
|
+
__setobj__(ESRIGeoJSON.new(current_file.path, *args, **options))
|
34
38
|
else
|
35
|
-
raise ImportError, "Could not import file.
|
39
|
+
raise ImportError, "Could not import file. " + SUPPORTED_FORMATS
|
36
40
|
end
|
37
41
|
end
|
38
42
|
end
|
@@ -5,15 +5,13 @@ module SpatialFeatures
|
|
5
5
|
module Importers
|
6
6
|
class GeoJSON < Base
|
7
7
|
def cache_key
|
8
|
-
@cache_key ||= Digest::MD5.hexdigest(
|
8
|
+
@cache_key ||= Digest::MD5.hexdigest(geojson)
|
9
9
|
end
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
13
|
def each_record(&block)
|
14
|
-
|
15
|
-
|
16
|
-
@data.fetch('features', []).each do |record|
|
14
|
+
parsed_geojson.fetch('features', []).each do |record|
|
17
15
|
metadata = record['properties'] || {}
|
18
16
|
name = metadata.delete('name')
|
19
17
|
yield OpenStruct.new(
|
@@ -24,6 +22,14 @@ module SpatialFeatures
|
|
24
22
|
)
|
25
23
|
end
|
26
24
|
end
|
25
|
+
|
26
|
+
def parsed_geojson
|
27
|
+
@parsed_geojson ||= (@data.is_a?(String) ? JSON.parse(@data) : @data) || {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def geojson
|
31
|
+
@geojson ||= @data.is_a?(String) ? @data : @data.to_json
|
32
|
+
end
|
27
33
|
end
|
28
34
|
end
|
29
35
|
end
|
@@ -59,7 +59,7 @@ module SpatialFeatures
|
|
59
59
|
|
60
60
|
# Convert a hash of GeoJSON data into a PostGIS geometry object
|
61
61
|
def geom_from_json(geometry)
|
62
|
-
|
62
|
+
RGeo::GeoJSON.decode(geometry.to_json).as_text
|
63
63
|
end
|
64
64
|
end
|
65
65
|
end
|
data/lib/spatial_features.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Gems
|
2
2
|
require 'delayed_job_active_record'
|
3
3
|
require 'rgeo/shapefile'
|
4
|
+
require 'rgeo/geo_json'
|
4
5
|
require 'nokogiri'
|
5
6
|
require 'zip'
|
6
7
|
|
@@ -20,11 +21,11 @@ require 'spatial_features/has_spatial_features/feature_import'
|
|
20
21
|
|
21
22
|
require 'spatial_features/importers/base'
|
22
23
|
require 'spatial_features/importers/file'
|
23
|
-
require 'spatial_features/importers/
|
24
|
+
require 'spatial_features/importers/geo_json'
|
25
|
+
require 'spatial_features/importers/esri_geo_json'
|
24
26
|
require 'spatial_features/importers/kml'
|
25
27
|
require 'spatial_features/importers/kml_file'
|
26
28
|
require 'spatial_features/importers/kml_file_arcgis'
|
27
|
-
require 'spatial_features/importers/json_arcgis'
|
28
29
|
require 'spatial_features/importers/geomark'
|
29
30
|
require 'spatial_features/importers/shapefile'
|
30
31
|
|
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:
|
4
|
+
version: 3.0.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: 2022-01-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -59,6 +59,20 @@ dependencies:
|
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '3.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rgeo-geojson
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.1.1
|
69
|
+
type: :runtime
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.1.1
|
62
76
|
- !ruby/object:Gem::Dependency
|
63
77
|
name: rubyzip
|
64
78
|
requirement: !ruby/object:Gem::Requirement
|
@@ -141,10 +155,10 @@ files:
|
|
141
155
|
- lib/spatial_features/has_spatial_features/feature_import.rb
|
142
156
|
- lib/spatial_features/has_spatial_features/queued_spatial_processing.rb
|
143
157
|
- lib/spatial_features/importers/base.rb
|
158
|
+
- lib/spatial_features/importers/esri_geo_json.rb
|
144
159
|
- lib/spatial_features/importers/file.rb
|
145
|
-
- lib/spatial_features/importers/
|
160
|
+
- lib/spatial_features/importers/geo_json.rb
|
146
161
|
- lib/spatial_features/importers/geomark.rb
|
147
|
-
- lib/spatial_features/importers/json_arcgis.rb
|
148
162
|
- lib/spatial_features/importers/kml.rb
|
149
163
|
- lib/spatial_features/importers/kml_file.rb
|
150
164
|
- lib/spatial_features/importers/kml_file_arcgis.rb
|
@@ -175,7 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
189
|
- !ruby/object:Gem::Version
|
176
190
|
version: '0'
|
177
191
|
requirements: []
|
178
|
-
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.7.6.3
|
179
194
|
signing_key:
|
180
195
|
specification_version: 4
|
181
196
|
summary: Adds spatial methods to a model.
|
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
require 'digest/md5'
|
3
|
-
|
4
|
-
module SpatialFeatures
|
5
|
-
module Importers
|
6
|
-
class JsonArcGIS < Base
|
7
|
-
def cache_key
|
8
|
-
@cache_key ||= Digest::MD5.hexdigest(features.to_json)
|
9
|
-
end
|
10
|
-
|
11
|
-
private
|
12
|
-
|
13
|
-
def each_record(&block)
|
14
|
-
json = esri_json_to_geojson(@data)
|
15
|
-
json['features'].each do |record|
|
16
|
-
yield OpenStruct.new(
|
17
|
-
:feature_type => record['geometry']['type'],
|
18
|
-
:geog => SpatialFeatures::Utils.geom_from_json(record['geometry']),
|
19
|
-
:metadata => record['properties']
|
20
|
-
)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def esri_json_to_geojson(url)
|
25
|
-
JSON.parse(`ogr2ogr -f GeoJSON /dev/stdout "#{url}" OGRGeoJSON`)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|