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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 639dee46fedd2cf507642b41119ff99e93506b54d891ab42d21a1f24fcf5a679
4
- data.tar.gz: 956175227c6a59acc867810a51796303218eac70c9bc49fe0d0f8222ea8558e6
3
+ metadata.gz: 6d3fe76f0ce82837de428e401ce4f6a20a84b97acb2085374a8bf8fe1daba5c8
4
+ data.tar.gz: fed058553913c07b27838424dc07b1b63677f28798fae7d5534b55de7c1aa713
5
5
  SHA512:
6
- metadata.gz: 6a1ac7a0dca15400e0e03f0f1c2192e22a4c1e818edde2a832f925e6ee803a9284c5236684228e25dbeabf2d147aacef4f4c05170216b0857941c87ed9b9a395
7
- data.tar.gz: da6fc8be7449202dfe46de18f0829e49664626ba17b90b6e3c1eae7a059f38a10d8310046e4f2bf44fb80bf78f2cdeea26624f429fc0d7b5873775b332086712
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,3005),
34
- geom_lowres geometry(Geometry,3005),
35
- kml text,
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 => '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.geojson(lowres: false, precision: 6, properties: true, srid: 4326, centroids: false) # default srid is 4326 so output is Google Maps compatible
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 = <<~SQL if properties
138
- , 'properties', hstore_to_json(metadata)
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', json_agg(
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 feature_bounds
157
- slice(:north, :east, :south, :west)
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
- geometry = options[:lowres] ? kml_lowres : super()
166
- geometry = "<MultiGeometry>#{geometry}#{kml_centroid}</MultiGeometry>" if options[:centroid]
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
 
@@ -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)
@@ -1 +1,4 @@
1
1
  Mime::Type.register "application/vnd.geo+json", :geojson
2
+ Mime::Type.register "application/vnd.mapbox-vector-tile", :mvt
3
+ Mime::Type.register "application/vnd.google-earth.kml+xml", :kml
4
+ Mime::Type.register "application/vnd.google-earth.kmz", :kmz
@@ -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
- yield if block_given?
16
-
17
- respond_to do |format|
18
- format.html { render :template => 'shared/spatial/feature_proximity', :layout => false }
19
- format.kml { render :template => 'shared/spatial/feature_proximity' }
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
- yield if block_given?
29
-
30
- respond_to do |format|
31
- format.kml { render :template => 'shared/spatial/feature_venn_polygons' }
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) ? scope.where(:id => params[:ids]) : scope
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(suffix = nil)
45
- Delayed::Job.where(:queue => "#{spatial_processing_queue_name}#{suffix}")
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
- features.area
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
- if association(:aggregate_feature).loaded?
206
- aggregate_feature&.feature_bounds
207
- else
208
- result = aggregate_features.pluck(:north, :east, :south, :west).first
209
- [:north, :east, :south, :west].zip(result.map {|bound| BigDecimal(bound) }).to_h.with_indifferent_access if result
210
- end
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
- if association(:aggregate_feature).loaded?
221
- aggregate_feature&.area
222
- else
223
- aggregate_features.pluck(:area).first
224
- end
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 or .shp file. Supported formats are KMZ, KML, and zipped ArcGIS shapefiles.".freeze
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: [/\.kml$/, /\.shp$/], downcase: true).map do |file|
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: [/\.kml$/, /\.shp$/], downcase: true).first
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. Supported formats are KMZ, KML, and zipped ArcGIS shapefiles"
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(@data.to_json)
8
+ @cache_key ||= Digest::MD5.hexdigest(geojson)
9
9
  end
10
10
 
11
11
  private
12
12
 
13
13
  def each_record(&block)
14
- return unless @data
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
- select_db_value("SELECT ST_GeomFromGeoJSON('#{geometry.to_json}')")
62
+ RGeo::GeoJSON.decode(geometry.to_json).as_text
63
63
  end
64
64
  end
65
65
  end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "2.20.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -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/geojson'
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: 2.20.0
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: 2021-12-27 00:00:00.000000000 Z
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/geojson.rb
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
- rubygems_version: 3.0.3
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