spatial_features 2.18.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 +33 -6
- data/app/models/abstract_feature.rb +79 -17
- data/app/models/aggregate_feature.rb +5 -2
- data/app/models/feature.rb +5 -11
- 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 +26 -2
- data/lib/spatial_features/has_spatial_features/queued_spatial_processing.rb +3 -3
- data/lib/spatial_features/has_spatial_features.rb +40 -20
- data/lib/spatial_features/importers/base.rb +2 -1
- 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/importers/kml.rb +30 -3
- data/lib/spatial_features/importers/kml_file.rb +7 -3
- data/lib/spatial_features/unzip.rb +4 -0
- data/lib/spatial_features/utils.rb +1 -1
- data/lib/spatial_features/version.rb +1 -1
- data/lib/spatial_features.rb +3 -2
- metadata +19 -19
- data/config/initializers/chroma_serializers.rb +0 -15
- 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,
|
|
@@ -109,8 +108,17 @@ Person.new(:features => [Feature.new(:geog => 'some binary PostGIS Geography str
|
|
|
109
108
|
You can specify multiple import sources for geometry. Each key is a method that returns the data for the Importer, and
|
|
110
109
|
each value is the Importer to use to parse the data. See each Importer for more details.
|
|
111
110
|
```ruby
|
|
111
|
+
def ImageImporter
|
|
112
|
+
def self.call(feature, image_paths)
|
|
113
|
+
image_paths.each do |pathname|
|
|
114
|
+
# ...
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
112
119
|
class Location < ActiveRecord::Base
|
|
113
|
-
has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => '
|
|
120
|
+
has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File', :geojson => 'ESRIGeoJSON' },
|
|
121
|
+
:image_handlers => ['ImageImporter']
|
|
114
122
|
|
|
115
123
|
def remote_kml_url
|
|
116
124
|
"www.test.com/kml/#{id}.kml"
|
|
@@ -154,3 +162,22 @@ Feature.reset_column_information
|
|
|
154
162
|
AbstractFeature.update_all(:type => 'Feature')
|
|
155
163
|
Feature.refresh_aggregates
|
|
156
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
|
|
|
@@ -4,7 +4,11 @@ class AggregateFeature < AbstractFeature
|
|
|
4
4
|
has_many :features, lambda { |aggregate| where(:spatial_model_type => aggregate.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
|
|
5
5
|
|
|
6
6
|
# Aggregate the features for the spatial model into a single feature
|
|
7
|
-
|
|
7
|
+
before_validation :set_geog, :on => :create, :unless => :geog?
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def set_geog
|
|
8
12
|
feature_array_sql = <<~SQL
|
|
9
13
|
ARRAY[
|
|
10
14
|
(#{features.select('ST_UNION(ST_CollectionExtract(geog::geometry, 1))').to_sql}),
|
|
@@ -19,6 +23,5 @@ class AggregateFeature < AbstractFeature
|
|
|
19
23
|
FROM (SELECT unnest(#{feature_array_sql})) AS features
|
|
20
24
|
WHERE NOT ST_IsEmpty(unnest)
|
|
21
25
|
SQL
|
|
22
|
-
self.save!
|
|
23
26
|
end
|
|
24
27
|
end
|
data/app/models/feature.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_dependency SpatialFeatures::Engine.root.join('app/models/abstract_feature')
|
|
2
|
+
|
|
1
3
|
class Feature < AbstractFeature
|
|
2
4
|
class_attribute :automatically_refresh_aggregate
|
|
3
5
|
self.automatically_refresh_aggregate = true
|
|
@@ -13,15 +15,7 @@ class Feature < AbstractFeature
|
|
|
13
15
|
|
|
14
16
|
after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
def self.cache_derivatives(options = {})
|
|
18
|
-
super
|
|
19
|
-
update_all <<-SQL.squish
|
|
20
|
-
kml = ST_AsKML(geog, 6),
|
|
21
|
-
kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
|
|
22
|
-
kml_centroid = ST_AsKML(centroid)
|
|
23
|
-
SQL
|
|
24
|
-
end
|
|
18
|
+
attr_accessor :importable_image_paths # :nodoc:
|
|
25
19
|
|
|
26
20
|
def self.defer_aggregate_refresh(&block)
|
|
27
21
|
start_at = Feature.maximum(:id).to_i + 1
|
|
@@ -52,8 +46,8 @@ class Feature < AbstractFeature
|
|
|
52
46
|
end
|
|
53
47
|
|
|
54
48
|
def refresh_aggregate
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
aggregate_feature&.destroy # Destroy the existing aggregate feature to ensure its cache key changes when it is refreshed
|
|
50
|
+
create_aggregate_feature!
|
|
57
51
|
end
|
|
58
52
|
|
|
59
53
|
def automatically_refresh_aggregate?
|
|
@@ -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
|
|
@@ -8,7 +8,7 @@ module SpatialFeatures
|
|
|
8
8
|
included do
|
|
9
9
|
extend ActiveModel::Callbacks
|
|
10
10
|
define_model_callbacks :update_features
|
|
11
|
-
spatial_features_options.reverse_merge!(:import => {}, spatial_cache: [])
|
|
11
|
+
spatial_features_options.reverse_merge!(:import => {}, :spatial_cache => [], :image_handlers => [])
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
module ClassMethods
|
|
@@ -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)
|
|
@@ -78,13 +85,30 @@ module SpatialFeatures
|
|
|
78
85
|
"SpatialFeatures::Importers::#{importer_name}".constantize
|
|
79
86
|
end
|
|
80
87
|
|
|
88
|
+
def handle_images(feature)
|
|
89
|
+
return if feature.importable_image_paths.nil? || feature.importable_image_paths.empty?
|
|
90
|
+
|
|
91
|
+
Array(spatial_features_options[:image_handlers]).each do |image_handler|
|
|
92
|
+
image_handler_from_name(image_handler).call(feature, feature.importable_image_paths)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def image_handler_from_name(handler_name)
|
|
97
|
+
handler_name.to_s.constantize
|
|
98
|
+
end
|
|
99
|
+
|
|
81
100
|
def import_features(imports, skip_invalid)
|
|
82
101
|
features.delete_all
|
|
83
102
|
valid, invalid = Feature.defer_aggregate_refresh do
|
|
84
103
|
Feature.without_caching_derivatives do
|
|
85
104
|
imports.flat_map(&:features).partition do |feature|
|
|
86
105
|
feature.spatial_model = self
|
|
87
|
-
feature.save
|
|
106
|
+
if feature.save
|
|
107
|
+
handle_images(feature)
|
|
108
|
+
true
|
|
109
|
+
else
|
|
110
|
+
false
|
|
111
|
+
end
|
|
88
112
|
end
|
|
89
113
|
end
|
|
90
114
|
end
|
|
@@ -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
|
|
@@ -77,7 +77,7 @@ module SpatialFeatures
|
|
|
77
77
|
update_cached_status(:success)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def error(job)
|
|
80
|
+
def error(job, exception)
|
|
81
81
|
update_cached_status(:failure)
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -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)
|
|
@@ -46,7 +46,8 @@ module SpatialFeatures
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def build_feature(record)
|
|
49
|
-
|
|
49
|
+
importable_image_paths = record.importable_image_paths if record.respond_to?(:importable_image_paths)
|
|
50
|
+
Feature.new(:name => record.name, :metadata => record.metadata, :feature_type => record.feature_type, :geog => record.geog, :importable_image_paths => importable_image_paths, :make_valid => @make_valid)
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
end
|
|
@@ -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
|
|
@@ -3,6 +3,14 @@ require 'ostruct'
|
|
|
3
3
|
module SpatialFeatures
|
|
4
4
|
module Importers
|
|
5
5
|
class KML < Base
|
|
6
|
+
# <SimpleData name> keys that may contain <img> tags
|
|
7
|
+
IMAGE_METADATA_KEYS = %w[pdfmaps_photos].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(data, base_dir: nil, **args)
|
|
10
|
+
@base_dir = base_dir
|
|
11
|
+
super data, **args
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
private
|
|
7
15
|
|
|
8
16
|
def each_record(&block)
|
|
@@ -18,10 +26,11 @@ module SpatialFeatures
|
|
|
18
26
|
next if blank_feature?(feature)
|
|
19
27
|
|
|
20
28
|
geog = geom_from_kml(feature)
|
|
21
|
-
|
|
22
29
|
next if geog.blank?
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
importable_image_paths = images_from_metadata(metadata)
|
|
32
|
+
|
|
33
|
+
yield OpenStruct.new(:feature_type => sql_type, :geog => geog, :name => name, :metadata => metadata, :importable_image_paths => importable_image_paths)
|
|
25
34
|
end
|
|
26
35
|
end
|
|
27
36
|
end
|
|
@@ -32,17 +41,35 @@ module SpatialFeatures
|
|
|
32
41
|
|
|
33
42
|
def geom_from_kml(kml)
|
|
34
43
|
geom = nil
|
|
44
|
+
conn = nil
|
|
35
45
|
|
|
36
46
|
# 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)
|
|
47
|
+
#
|
|
48
|
+
# We manually checkout a new connection since Rails re-uses DB connections across threads.
|
|
37
49
|
Thread.new do
|
|
38
|
-
|
|
50
|
+
conn = ActiveRecord::Base.connection_pool.checkout
|
|
51
|
+
geom = conn.select_value("SELECT ST_GeomFromKML(#{conn.quote(kml.to_s)})")
|
|
39
52
|
rescue ActiveRecord::StatementInvalid => e # Discard Invalid KML features
|
|
40
53
|
geom = nil
|
|
54
|
+
ensure
|
|
55
|
+
ActiveRecord::Base.connection_pool.checkin(conn) if conn
|
|
41
56
|
end.join
|
|
42
57
|
|
|
43
58
|
return geom
|
|
44
59
|
end
|
|
45
60
|
|
|
61
|
+
def images_from_metadata(metadata)
|
|
62
|
+
IMAGE_METADATA_KEYS.flat_map do |key|
|
|
63
|
+
images = metadata.delete(key)
|
|
64
|
+
next unless images
|
|
65
|
+
|
|
66
|
+
Nokogiri::HTML.fragment(images).css("img").map do |img|
|
|
67
|
+
next unless (src = img["src"])
|
|
68
|
+
@base_dir.join(src.downcase)
|
|
69
|
+
end
|
|
70
|
+
end.compact
|
|
71
|
+
end
|
|
72
|
+
|
|
46
73
|
def extract_metadata(placemark)
|
|
47
74
|
metadata = {}
|
|
48
75
|
metadata.merge! extract_table(placemark)
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
module SpatialFeatures
|
|
2
2
|
module Importers
|
|
3
3
|
class KMLFile < KML
|
|
4
|
-
def initialize(path_or_url,
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
def initialize(path_or_url, **options)
|
|
5
|
+
path = Download.open_each(path_or_url, unzip: [/\.kml$/], downcase: true).first
|
|
6
|
+
super ::File.read(path), base_dir: Pathname.new(path).dirname, **options
|
|
7
7
|
rescue SocketError, Errno::ECONNREFUSED, OpenURI::HTTPError
|
|
8
8
|
url = URI(path_or_url)
|
|
9
9
|
raise ImportError, "KML server is not responding. Ensure server is running and accessible at #{[url.scheme, "//#{url.host}", url.port].select(&:present?).join(':')}."
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
def cache_key
|
|
13
|
+
@cache_key ||= Digest::MD5.hexdigest(@data)
|
|
14
|
+
end
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -2,6 +2,9 @@ require 'fileutils'
|
|
|
2
2
|
|
|
3
3
|
module SpatialFeatures
|
|
4
4
|
module Unzip
|
|
5
|
+
# paths containing '__macosx' or beginning with a '.'
|
|
6
|
+
IGNORED_ENTRY_PATHS = /(\A|\/)(__macosx|\.)/i.freeze
|
|
7
|
+
|
|
5
8
|
def self.paths(file_path, find: nil, **extract_options)
|
|
6
9
|
paths = extract(file_path, **extract_options)
|
|
7
10
|
|
|
@@ -16,6 +19,7 @@ module SpatialFeatures
|
|
|
16
19
|
def self.extract(file_path, output_dir = Dir.mktmpdir, downcase: false)
|
|
17
20
|
[].tap do |paths|
|
|
18
21
|
entries(file_path).each do |entry|
|
|
22
|
+
next if entry.name =~ IGNORED_ENTRY_PATHS
|
|
19
23
|
output_filename = entry.name
|
|
20
24
|
output_filename = output_filename.downcase if downcase
|
|
21
25
|
path = "#{output_dir}/#{output_filename}"
|
|
@@ -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
|
|
@@ -60,47 +60,47 @@ dependencies:
|
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '3.0'
|
|
62
62
|
- !ruby/object:Gem::Dependency
|
|
63
|
-
name:
|
|
63
|
+
name: rgeo-geojson
|
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- - "
|
|
66
|
+
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 1.
|
|
68
|
+
version: 2.1.1
|
|
69
69
|
type: :runtime
|
|
70
70
|
prerelease: false
|
|
71
71
|
version_requirements: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- - "
|
|
73
|
+
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 1.
|
|
75
|
+
version: 2.1.1
|
|
76
76
|
- !ruby/object:Gem::Dependency
|
|
77
|
-
name:
|
|
77
|
+
name: rubyzip
|
|
78
78
|
requirement: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- - "
|
|
80
|
+
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version:
|
|
82
|
+
version: 1.0.0
|
|
83
83
|
type: :runtime
|
|
84
84
|
prerelease: false
|
|
85
85
|
version_requirements: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
|
-
- - "
|
|
87
|
+
- - ">="
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version:
|
|
89
|
+
version: 1.0.0
|
|
90
90
|
- !ruby/object:Gem::Dependency
|
|
91
|
-
name:
|
|
91
|
+
name: nokogiri
|
|
92
92
|
requirement: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version:
|
|
96
|
+
version: '1.6'
|
|
97
97
|
type: :runtime
|
|
98
98
|
prerelease: false
|
|
99
99
|
version_requirements: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version:
|
|
103
|
+
version: '1.6'
|
|
104
104
|
- !ruby/object:Gem::Dependency
|
|
105
105
|
name: pg
|
|
106
106
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -144,7 +144,6 @@ files:
|
|
|
144
144
|
- app/models/feature.rb
|
|
145
145
|
- app/models/spatial_cache.rb
|
|
146
146
|
- app/models/spatial_proximity.rb
|
|
147
|
-
- config/initializers/chroma_serializers.rb
|
|
148
147
|
- config/initializers/mime_types.rb
|
|
149
148
|
- config/initializers/register_oids.rb
|
|
150
149
|
- lib/spatial_features.rb
|
|
@@ -156,10 +155,10 @@ files:
|
|
|
156
155
|
- lib/spatial_features/has_spatial_features/feature_import.rb
|
|
157
156
|
- lib/spatial_features/has_spatial_features/queued_spatial_processing.rb
|
|
158
157
|
- lib/spatial_features/importers/base.rb
|
|
158
|
+
- lib/spatial_features/importers/esri_geo_json.rb
|
|
159
159
|
- lib/spatial_features/importers/file.rb
|
|
160
|
-
- lib/spatial_features/importers/
|
|
160
|
+
- lib/spatial_features/importers/geo_json.rb
|
|
161
161
|
- lib/spatial_features/importers/geomark.rb
|
|
162
|
-
- lib/spatial_features/importers/json_arcgis.rb
|
|
163
162
|
- lib/spatial_features/importers/kml.rb
|
|
164
163
|
- lib/spatial_features/importers/kml_file.rb
|
|
165
164
|
- lib/spatial_features/importers/kml_file_arcgis.rb
|
|
@@ -190,7 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
190
189
|
- !ruby/object:Gem::Version
|
|
191
190
|
version: '0'
|
|
192
191
|
requirements: []
|
|
193
|
-
|
|
192
|
+
rubyforge_project:
|
|
193
|
+
rubygems_version: 2.7.6.3
|
|
194
194
|
signing_key:
|
|
195
195
|
specification_version: 4
|
|
196
196
|
summary: Adds spatial methods to a model.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Chroma
|
|
2
|
-
class Color
|
|
3
|
-
module Serializers
|
|
4
|
-
# Google's Fusion Table colouring expects the alpha value in the last position, not the first
|
|
5
|
-
def to_ft_hex
|
|
6
|
-
[
|
|
7
|
-
to_2char_hex(@rgb.r),
|
|
8
|
-
to_2char_hex(@rgb.g),
|
|
9
|
-
to_2char_hex(@rgb.b),
|
|
10
|
-
to_2char_hex(alpha * 255)
|
|
11
|
-
].join('')
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -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
|