spatial_features 2.20.0 → 3.0.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 +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
|