spatial_features 1.7.1 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 670d368c7bf08ca4ff1280a2d61760ad5a391a03
4
- data.tar.gz: 707999939635257129e3f8e354c2c73e6ed9dc8a
3
+ metadata.gz: ee690ccc750978af8db4b018a4564dcad10085cc
4
+ data.tar.gz: cb9e15cdee984d7c7398f68ec95af49a973a0571
5
5
  SHA512:
6
- metadata.gz: 748760b027e02b3832319a26056ac2b2898ff2eb7db19de2819f1e04f67c95fbf8a8ad240051e4dc515ecfc503f6289cbaf5a90ebb615e7fda98630675180f15
7
- data.tar.gz: fccaf84adbfc1bccd73acd83bb73552c2f4261beee9adbb508b24aaf9667ffdaa0d2f2ae0a543da4dab06c96f237562f32e01a05a1458ad6d153315d7a61dd99
6
+ metadata.gz: d1816cccfc59bb3d8ad6dfd19467a734e0889fd658224d19099e0c7266dd30b16ecc6ff5c305c557649fe4278c3c0dcab9be4ee8f10d5aa1db37103c60a119d1
7
+ data.tar.gz: 97f3e7e7eff1d3feac000560651944bd391d5533055a3131b29ad39901ee9442d23d70854f2b10c796006129cd38f8e336e2a5f73d53cffbcb642d3ac43ad5ca
data/README.md CHANGED
@@ -48,3 +48,33 @@ execute("
48
48
  );
49
49
  ")
50
50
  ```
51
+
52
+ ## Usage
53
+
54
+ In your model
55
+
56
+ ```ruby
57
+ class Location < ActiveRecord::Base
58
+ has_spatial_features
59
+ end
60
+
61
+ Person.new(:features => [Feature.new(:geog => 'some binary PostGIS Geography string')])
62
+ ```
63
+
64
+ ### Import
65
+
66
+ You can specify multiple import sources for geometry. Each key is a method that returns the data for the Importer, and
67
+ each value is the Importer to use to parse the data. See each Importer for more details.
68
+ ```ruby
69
+ class Location < ActiveRecord::Base
70
+ has_spatial_features :import => { :remote_kml_url => 'KMLFile', :file => 'File' }
71
+
72
+ def remote_kml_url
73
+ "www.test.com/kml/#{id}.kml"
74
+ end
75
+
76
+ def file
77
+ File.open('local/files/my_kml')
78
+ end
79
+ end
80
+ ```
@@ -1,10 +1,16 @@
1
1
  class Feature < ActiveRecord::Base
2
2
  belongs_to :spatial_model, :polymorphic => :true
3
3
 
4
+ attr_writer :make_valid
5
+
6
+ FEATURE_TYPES = %w(polygon point line)
7
+
4
8
  before_validation :sanitize_feature_type
5
9
  validates_presence_of :geog
6
10
  validate :geometry_is_valid
7
- validates_inclusion_of :feature_type, :in => ['polygon', 'point', 'line']
11
+ validates_inclusion_of :feature_type, :in => FEATURE_TYPES
12
+ before_save :sanitize
13
+ before_save :make_valid, if: :make_valid?
8
14
  after_save :cache_derivatives
9
15
 
10
16
  def self.with_metadata(k, v)
@@ -98,8 +104,21 @@ class Feature < ActiveRecord::Base
98
104
  return geometry
99
105
  end
100
106
 
107
+ def make_valid?
108
+ @make_valid
109
+ end
110
+
101
111
  private
102
112
 
113
+ def make_valid
114
+ self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_CollectionExtract(ST_MakeValid('#{sanitize}'),3)")
115
+ end
116
+
117
+ # Use ST_Force_2D to discard z-coordinates that cause failures later in the process
118
+ def sanitize
119
+ self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Force2D('#{geog}')")
120
+ end
121
+
103
122
  def self.detect_srid(column_name)
104
123
  connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
105
124
  end
@@ -116,7 +135,7 @@ class Feature < ActiveRecord::Base
116
135
  end
117
136
 
118
137
  def sanitize_feature_type
119
- self.feature_type = self.feature_type.to_s.strip.downcase
138
+ self.feature_type = FEATURE_TYPES.find {|type| self.feature_type.to_s.strip.downcase.include?(type) }
120
139
  end
121
140
 
122
141
  def sanitize_input_for_sql(input)
@@ -0,0 +1,32 @@
1
+ require 'open-uri'
2
+
3
+ module SpatialFeatures
4
+ module Download
5
+ # file can be a url, path, or file, any of which can return be a zipped archive
6
+ def self.read(file, unzip: nil)
7
+ file = open(file, unzip: unzip)
8
+ path = ::File.path(file)
9
+ return ::File.read(path)
10
+ end
11
+
12
+ def self.open(file, unzip: nil)
13
+ file = Kernel.open(file)
14
+ file = normalize_file(file) if file.is_a?(StringIO)
15
+ file = find_in_zip(file, unzip) if Unzip.is_zip?(file)
16
+ return file
17
+ end
18
+
19
+ def self.normalize_file(file)
20
+ Tempfile.new.tap do |temp|
21
+ temp.binmode
22
+ temp.write(file.read)
23
+ temp.rewind
24
+ end
25
+ end
26
+
27
+ def self.find_in_zip(file, unzip)
28
+ raise "Must specify an :unzip option if opening a zip file. e.g. open(file, :find => '.shp')" unless unzip
29
+ return File.open(Unzip.paths(file, :find => unzip))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ module SpatialFeatures
2
+ module DelayedFeatureImport
3
+ include FeatureImport
4
+
5
+ def queue_feature_update!(options = {})
6
+ job = UpdateFeaturesJob.new(options.merge :spatial_model_type => self.class, :spatial_model_id => self.id)
7
+ Delayed::Job.enqueue(job, :queue => delayed_jobs_queue_name)
8
+ end
9
+
10
+ def updating_features?
11
+ running_feature_update_jobs.exists?
12
+ end
13
+
14
+ def feature_update_error
15
+ (failed_feature_update_jobs.first.try(:last_error) || '').split("\n").first
16
+ end
17
+
18
+ def running_feature_update_jobs
19
+ feature_update_jobs.where(failed_at: nil)
20
+ end
21
+
22
+ def failed_feature_update_jobs
23
+ feature_update_jobs.where.not(failed_at: nil)
24
+ end
25
+
26
+ def feature_update_jobs
27
+ Delayed::Job.where(queue: delayed_jobs_queue_name)
28
+ end
29
+
30
+ private
31
+
32
+ def delayed_jobs_queue_name
33
+ "#{self.class}/#{self.id}/update_features"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,64 @@
1
+ require 'digest/md5'
2
+
3
+ module SpatialFeatures
4
+ module FeatureImport
5
+ def update_features!(skip_invalid: false, options: {})
6
+ options = options.reverse_merge(spatial_features_options).reverse_merge(:import => {})
7
+
8
+ ActiveRecord::Base.transaction do
9
+ imports = spatial_feature_imports(options[:import], options[:make_valid])
10
+ cache_key = Digest::MD5.hexdigest(imports.collect(&:cache_key).join)
11
+
12
+ return if features_cache_key_matches?(cache_key)
13
+
14
+ import_features(imports)
15
+ validate_features!(imports, skip_invalid)
16
+ set_features_cache_key(cache_key)
17
+
18
+ return true
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def spatial_feature_imports(import_options, make_valid)
25
+ import_options.collect do |data_method, importer_name|
26
+ data = send(data_method)
27
+ "SpatialFeatures::Importers::#{importer_name}".constantize.new(data, :make_valid => make_valid) if data.present?
28
+ end.compact
29
+ end
30
+
31
+ def import_features(imports)
32
+ self.features.destroy_all
33
+ self.features = imports.flat_map(&:features)
34
+ end
35
+
36
+ def validate_features!(imports, skip_invalid = false)
37
+ invalid = features.select {|feature| feature.errors.present? }
38
+ features.destroy(invalid)
39
+
40
+ return if skip_invalid
41
+
42
+ errors = imports.flat_map(&:errors)
43
+ invalid.each do |feature|
44
+ errors << "Feature #{feature.name}: #{feature.errors.full_messages.to_sentence}"
45
+ end
46
+
47
+ if errors.present?
48
+ raise ImportError, "Error updating #{self.class} #{self.id}. #{errors.to_sentence}"
49
+ end
50
+ end
51
+
52
+ def features_cache_key_matches?(cache_key)
53
+ has_spatial_features_hash? && cache_key == features_hash
54
+ end
55
+
56
+ def set_features_cache_key(cache_key)
57
+ return unless has_spatial_features_hash?
58
+ self.features_hash = cache_key
59
+ update_column(:features_hash, cache_key) unless new_record?
60
+ end
61
+ end
62
+
63
+ class ImportError < StandardError; end
64
+ end
@@ -1,29 +1,41 @@
1
1
  module SpatialFeatures
2
2
  module ActMethod
3
3
  def has_spatial_features(options = {})
4
- extend ClassMethods
5
- include InstanceMethods
4
+ unless acts_like?(:spatial_features)
5
+ extend ClassMethods
6
+ include InstanceMethods
7
+ include DelayedFeatureImport
6
8
 
7
- has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
9
+ class_attribute :spatial_features_options
10
+ self.spatial_features_options = {}
8
11
 
9
- scope :with_features, lambda { joins(:features).uniq }
10
- scope :without_features, lambda { joins("LEFT OUTER JOIN features ON features.spatial_model_type = '#{name}' AND features.spatial_model_id = #{table_name}.id").where("features.id IS NULL") }
12
+ has_many :features, lambda { extending FeaturesAssociationExtensions }, :as => :spatial_model, :dependent => :delete_all
11
13
 
12
- scope :with_spatial_cache, lambda {|klass| joins(:spatial_cache).where(:spatial_caches => { :intersection_model_type => klass }).uniq }
13
- scope :without_spatial_cache, lambda {|klass| joins("LEFT OUTER JOIN #{SpatialCache.table_name} ON spatial_model_id = #{table_name}.id AND spatial_model_type = '#{name}' and intersection_model_type = '#{klass}'").where('spatial_model_id IS NULL') }
14
- scope :with_stale_spatial_cache, lambda { joins(:spatial_cache).where("#{table_name}.features_hash != spatial_caches.features_hash").uniq } if has_spatial_features_hash?
14
+ scope :with_features, lambda { joins(:features).uniq }
15
+ scope :without_features, lambda { joins("LEFT OUTER JOIN features ON features.spatial_model_type = '#{name}' AND features.spatial_model_id = #{table_name}.id").where("features.id IS NULL") }
15
16
 
16
- has_many :spatial_cache, :as => :spatial_model, :dependent => :delete_all
17
- has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
18
- has_many :model_b_spatial_proximities, :as => :model_b, :class_name => 'SpatialProximity', :dependent => :delete_all
17
+ scope :with_spatial_cache, lambda {|klass| joins(:spatial_cache).where(:spatial_caches => { :intersection_model_type => klass }).uniq }
18
+ scope :without_spatial_cache, lambda {|klass| joins("LEFT OUTER JOIN #{SpatialCache.table_name} ON spatial_model_id = #{table_name}.id AND spatial_model_type = '#{name}' and intersection_model_type = '#{klass}'").where('spatial_model_id IS NULL') }
19
+ scope :with_stale_spatial_cache, lambda { joins(:spatial_cache).where("#{table_name}.features_hash != spatial_caches.features_hash").uniq } if has_spatial_features_hash?
19
20
 
20
- after_save :update_features_area, :if => :features_hash_changed? if has_features_area? && has_spatial_features_hash?
21
+ has_many :spatial_cache, :as => :spatial_model, :dependent => :delete_all
22
+ has_many :model_a_spatial_proximities, :as => :model_a, :class_name => 'SpatialProximity', :dependent => :delete_all
23
+ has_many :model_b_spatial_proximities, :as => :model_b, :class_name => 'SpatialProximity', :dependent => :delete_all
21
24
 
22
- delegate :has_spatial_features_hash?, :has_features_area?, :to => self
25
+ after_save :update_features_area, :if => :features_hash_changed? if has_features_area? && has_spatial_features_hash?
26
+
27
+ delegate :has_spatial_features_hash?, :has_features_area?, :to => self
28
+ end
29
+
30
+ self.spatial_features_options = self.spatial_features_options.merge(options)
23
31
  end
24
32
  end
25
33
 
26
34
  module ClassMethods
35
+ def acts_like_spatial_features?
36
+ true
37
+ end
38
+
27
39
  # Add methods to generate cache keys for a record or all records of this class
28
40
  # NOTE: features are never updated, only deleted and created, therefore we can
29
41
  # tell if they have changed by finding the maximum id and count instead of needing timestamps
@@ -164,7 +176,7 @@ module SpatialFeatures
164
176
  end
165
177
 
166
178
  module InstanceMethods
167
- def has_spatial_features?
179
+ def acts_like_spatial_features?
168
180
  true
169
181
  end
170
182
 
@@ -0,0 +1,51 @@
1
+ require 'digest/md5'
2
+
3
+ module SpatialFeatures
4
+ module Importers
5
+ class Base
6
+ attr_reader :errors
7
+
8
+ def initialize(data, make_valid: false)
9
+ @make_valid = make_valid
10
+ @data = data
11
+ @errors = []
12
+ end
13
+
14
+ def features
15
+ @features ||= build_features
16
+ end
17
+
18
+ def cache_key
19
+ @cache_key ||= Digest::MD5.hexdigest(@data)
20
+ end
21
+
22
+ private
23
+
24
+ def build_features
25
+ new_features = []
26
+
27
+ each_record do |record|
28
+ begin
29
+ new_features << build_feature(record)
30
+ rescue => e
31
+ @errors << e.message
32
+ end
33
+ end
34
+
35
+ return new_features
36
+ end
37
+
38
+ def each_record(&block)
39
+ raise NotImplementedError, 'Subclasses should implement this method and yield objects that can be passed to #build_feature'
40
+ end
41
+
42
+ def build_feature(record)
43
+ Feature.new(:name => record.name, :metadata => record.metadata, :feature_type => record.feature_type, :geog => record.geog, :make_valid => @make_valid)
44
+ end
45
+ end
46
+ end
47
+
48
+ # EXCEPTIONS
49
+
50
+ class ImportError < StandardError; end
51
+ end
@@ -0,0 +1,21 @@
1
+ require 'open-uri'
2
+
3
+ module SpatialFeatures
4
+ module Importers
5
+ class File < SimpleDelegator
6
+ def initialize(data, *args)
7
+ file = Download.open(data, unzip: %w(.kml .shp))
8
+
9
+ if file.path.end_with? '.kml'
10
+ __setobj__(KMLFile.new(file, *args))
11
+
12
+ elsif file.path.end_with? '.shp'
13
+ __setobj__(Shapefile.new(file, *args))
14
+
15
+ else
16
+ raise ImportError, "Could not import file. Supported formats are KMZ, KML, and zipped ArcGIS shapefiles"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module SpatialFeatures
2
+ module Importers
3
+ class Geomark < KMLFile
4
+ def initialize(geomark, *args)
5
+ super geomark_url(geomark), *args
6
+ end
7
+
8
+ private
9
+
10
+ def geomark_url(geomark)
11
+ "http://apps.gov.bc.ca/pub/geomark/geomarks/#{geomark}/parts.kml?srid=4326"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ require 'ostruct'
2
+
3
+ module SpatialFeatures
4
+ module Importers
5
+ class KML < Base
6
+ private
7
+
8
+ def each_record(&block)
9
+ Nokogiri::XML(@data).css('Placemark').each do |placemark|
10
+ name = placemark.css('name').text
11
+ metadata = {:description => placemark.css('description').text}
12
+
13
+ {'Polygon' => 'POLYGON', 'LineString' => 'LINE', 'Point' => 'POINT'}.each do |kml_type, sql_type|
14
+ placemark.css(kml_type).each do |placemark|
15
+ yield OpenStruct.new(:feature_type => sql_type, :geog => geom_from_kml(placemark), :name => name, :metadata => metadata)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def geom_from_kml(kml)
22
+ ActiveRecord::Base.connection.select_value("SELECT ST_GeomFromKML('#{kml}')")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ module SpatialFeatures
2
+ module Importers
3
+ class KMLFile < KML
4
+ def initialize(path_or_url, *args)
5
+ super Download.read(path_or_url, unzip: '.kml'), *args
6
+
7
+ rescue SocketError, Errno::ECONNREFUSED, OpenURI::HTTPError
8
+ url = URI(path_or_url)
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
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require 'ostruct'
2
+
3
+ module SpatialFeatures
4
+ module Importers
5
+ class KMLFileArcGIS < KMLFile
6
+ def initialize(data, *args)
7
+ super
8
+
9
+ rescue SocketError, Errno::ECONNREFUSED
10
+ url = URI(data)
11
+ raise ImportError, "ArcGIS Server is not responding. Ensure ArcGIS Server is running and accessible at #{[url.scheme, "//#{url.host}", url.port].select(&:present?).join(':')}."
12
+ rescue OpenURI::HTTPError
13
+ raise ImportError, "ArcGIS Map Service not found. Ensure ArcGIS Server is running and accessible at #{path_or_url}."
14
+ end
15
+
16
+ private
17
+
18
+ # ArcGIS includes metadata as an html table in the description
19
+ def each_record(&block)
20
+ super do |record|
21
+ record.metadata = Hash[Nokogiri::XML(record.metadata[:description]).css('td').collect(&:text).each_slice(2).to_a]
22
+ yield record
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ require 'ostruct'
2
+ require 'digest/md5'
3
+
4
+ module SpatialFeatures
5
+ module Importers
6
+ class Shapefile < 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
+ file = Download.open(@data, unzip: '.shp')
15
+ proj4 = proj4_from_file(file)
16
+ RGeo::Shapefile::Reader.open(file.path) do |records|
17
+ records.each do |record|
18
+ yield OpenStruct.new data_from_wkt(record.geometry.as_text, proj4).merge(:metadata => record.attributes)
19
+ end
20
+ end
21
+ end
22
+
23
+ def proj4_from_file(file)
24
+ `gdalsrsinfo "#{file.path}" -o proj4`[/'(.+)'/,1] # Sanitize "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n"
25
+ end
26
+
27
+ def data_from_wkt(wkt, proj4)
28
+ ActiveRecord::Base.connection.select_one <<-SQL
29
+ SELECT ST_Transform(ST_GeomFromText('#{wkt}'), '#{proj4}', 4326) AS geog, GeometryType(ST_GeomFromText('#{wkt}')) AS feature_type
30
+ SQL
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ module SpatialFeatures
2
+ module Unzip
3
+ def self.paths(file_path, find: nil)
4
+ dir = Dir.mktmpdir
5
+ paths = []
6
+
7
+ entries(file_path).each do |entry|
8
+ path = "#{dir}/#{entry.name}"
9
+ entry.extract(path)
10
+ paths << path
11
+ end
12
+
13
+ if find = Array.wrap(find).presence
14
+ paths = paths.detect {|path| find.any? {|pattern| path.include?(pattern) } }
15
+ raise(ImportError, "No file matched #{find}") unless paths.present?
16
+ end
17
+
18
+ return paths
19
+ end
20
+
21
+ def self.names(file_path)
22
+ entries(file_path).collect(&:name)
23
+ end
24
+
25
+ def self.entries(file_path)
26
+ Zip::File.open(File.path(file_path))
27
+ end
28
+
29
+ def self.is_zip?(file)
30
+ zip = file.readline.start_with?('PK')
31
+ file.rewind
32
+ return zip
33
+ rescue EOFError
34
+ return false
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module SpatialFeatures
2
- VERSION = "1.7.1"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -1,4 +1,4 @@
1
- class ArcGISUpdateFeaturesJob < Struct.new(:options)
1
+ class UpdateFeaturesJob < Struct.new(:options)
2
2
  def perform
3
3
  model = options[:spatial_model_type].find(options[:spatial_model_id])
4
4
 
@@ -22,7 +22,7 @@ class ArcGISUpdateFeaturesJob < Struct.new(:options)
22
22
  normalized_messages = []
23
23
 
24
24
  if message =~ /invalid KML representation/
25
- normalized_messages += invalid_kml_reason(message).presence || ["invalid KML being generated by ArcGIS."]
25
+ normalized_messages += invalid_kml_reason(message).presence || ["KML importer received invalid geometry."]
26
26
  end
27
27
 
28
28
  if message =~ /Self-intersection/
@@ -1,13 +1,28 @@
1
+ # Gems
2
+ require 'rgeo/shapefile'
3
+ require 'nokogiri'
4
+ require 'zip'
5
+
1
6
  # LIB
2
7
  require 'spatial_features/caching'
3
8
  require 'spatial_features/venn_polygons'
4
- require 'spatial_features/has_spatial_features'
9
+ require 'spatial_features/controller_helpers/spatial_extensions'
10
+ require 'spatial_features/download'
11
+ require 'spatial_features/unzip'
5
12
 
6
- require 'spatial_features/import/arcgis_kmz_features'
13
+ require 'spatial_features/has_spatial_features'
14
+ require 'spatial_features/has_spatial_features/feature_import'
15
+ require 'spatial_features/has_spatial_features/delayed_feature_import'
7
16
 
8
- require 'spatial_features/controller_helpers/spatial_extensions'
17
+ require 'spatial_features/importers/base'
18
+ require 'spatial_features/importers/file'
19
+ require 'spatial_features/importers/kml'
20
+ require 'spatial_features/importers/kml_file'
21
+ require 'spatial_features/importers/kml_file_arcgis'
22
+ require 'spatial_features/importers/geomark'
23
+ require 'spatial_features/importers/shapefile'
9
24
 
10
- require 'spatial_features/workers/arcgis_update_features_job'
25
+ require 'spatial_features/workers/update_features_job'
11
26
 
12
27
  require 'spatial_features/engine'
13
28
 
@@ -19,3 +34,6 @@ end
19
34
 
20
35
  # Load the act method
21
36
  ActiveRecord::Base.send :extend, SpatialFeatures::ActMethod
37
+
38
+ # Suppress date warnings when unzipping KMZ saved by Google Earth, see https://github.com/rubyzip/rubyzip/issues/112
39
+ Zip.warn_invalid_date = false
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: 1.7.1
4
+ version: 2.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: 2016-09-12 00:00:00.000000000 Z
12
+ date: 2016-11-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -18,9 +18,6 @@ dependencies:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
20
  version: '4.2'
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: 4.2.0
24
21
  type: :runtime
25
22
  prerelease: false
26
23
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,23 +25,62 @@ dependencies:
28
25
  - - "~>"
29
26
  - !ruby/object:Gem::Version
30
27
  version: '4.2'
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 4.2.0
34
28
  - !ruby/object:Gem::Dependency
35
29
  name: delayed_job_active_record
36
30
  requirement: !ruby/object:Gem::Requirement
37
31
  requirements:
38
32
  - - "~>"
39
33
  - !ruby/object:Gem::Version
40
- version: 4.0.3
34
+ version: '4.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '4.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rgeo-shapefile
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.4'
41
49
  type: :runtime
42
50
  prerelease: false
43
51
  version_requirements: !ruby/object:Gem::Requirement
44
52
  requirements:
45
53
  - - "~>"
46
54
  - !ruby/object:Gem::Version
47
- version: 4.0.3
55
+ version: '0.4'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rubyzip
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.1'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: nokogiri
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.6'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.6'
48
84
  - !ruby/object:Gem::Dependency
49
85
  name: pg
50
86
  requirement: !ruby/object:Gem::Requirement
@@ -63,16 +99,16 @@ dependencies:
63
99
  name: rspec
64
100
  requirement: !ruby/object:Gem::Requirement
65
101
  requirements:
66
- - - ">="
102
+ - - "~>"
67
103
  - !ruby/object:Gem::Version
68
- version: '0'
104
+ version: '3.5'
69
105
  type: :development
70
106
  prerelease: false
71
107
  version_requirements: !ruby/object:Gem::Requirement
72
108
  requirements:
73
- - - ">="
109
+ - - "~>"
74
110
  - !ruby/object:Gem::Version
75
- version: '0'
111
+ version: '3.5'
76
112
  description: Adds spatial methods to a model.
77
113
  email:
78
114
  - contact@culturecode.ca
@@ -90,12 +126,22 @@ files:
90
126
  - lib/spatial_features.rb
91
127
  - lib/spatial_features/caching.rb
92
128
  - lib/spatial_features/controller_helpers/spatial_extensions.rb
129
+ - lib/spatial_features/download.rb
93
130
  - lib/spatial_features/engine.rb
94
131
  - lib/spatial_features/has_spatial_features.rb
95
- - lib/spatial_features/import/arcgis_kmz_features.rb
132
+ - lib/spatial_features/has_spatial_features/delayed_feature_import.rb
133
+ - lib/spatial_features/has_spatial_features/feature_import.rb
134
+ - lib/spatial_features/importers/base.rb
135
+ - lib/spatial_features/importers/file.rb
136
+ - lib/spatial_features/importers/geomark.rb
137
+ - lib/spatial_features/importers/kml.rb
138
+ - lib/spatial_features/importers/kml_file.rb
139
+ - lib/spatial_features/importers/kml_file_arcgis.rb
140
+ - lib/spatial_features/importers/shapefile.rb
141
+ - lib/spatial_features/unzip.rb
96
142
  - lib/spatial_features/venn_polygons.rb
97
143
  - lib/spatial_features/version.rb
98
- - lib/spatial_features/workers/arcgis_update_features_job.rb
144
+ - lib/spatial_features/workers/update_features_job.rb
99
145
  - lib/tasks/spatial_features_tasks.rake
100
146
  homepage: https://github.com/culturecode/spatial_features
101
147
  licenses:
@@ -117,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
163
  version: '0'
118
164
  requirements: []
119
165
  rubyforge_project:
120
- rubygems_version: 2.4.5.1
166
+ rubygems_version: 2.5.2
121
167
  signing_key:
122
168
  specification_version: 4
123
169
  summary: Adds spatial methods to a model.
@@ -1,144 +0,0 @@
1
- module ArcGISKmzFeatures
2
- require 'open-uri'
3
- require 'digest/md5'
4
-
5
- def update_features!(options = {})
6
- @skip_invalid = options[:skip_invalid]
7
- @make_valid = options[:make_valid]
8
- @feature_error_messages = []
9
- kml_array = []
10
- cache_kml = ''
11
-
12
- Array(arcgis_kmz_url).each do |url|
13
- kml_array << open_kmz_url(url)
14
- cache_kml << kml_array.last.to_s
15
- end
16
-
17
- if has_spatial_features_hash?
18
- new_features_hash = Digest::MD5.hexdigest(cache_kml) if cache_kml.present?
19
-
20
- if new_features_hash != self.features_hash
21
- replace_features(kml_array)
22
- update_attributes(:features_hash => new_features_hash)
23
- else
24
- return false
25
- end
26
- else
27
- replace_features(kml_array)
28
- end
29
-
30
- return true
31
- end
32
-
33
- def queue_feature_update!(options = {})
34
- Delayed::Job.enqueue ArcGISUpdateFeaturesJob.new(options.merge :spatial_model_type => self.class, :spatial_model_id => self.id), :queue => delayed_jobs_queue_name
35
- end
36
-
37
- def updating_features?
38
- running_feature_update_jobs.exists?
39
- end
40
-
41
- def feature_update_error
42
- (failed_feature_update_jobs.first.try(:last_error) || '').split("\n").first
43
- end
44
-
45
- def running_feature_update_jobs
46
- feature_update_jobs.where(failed_at: nil)
47
- end
48
-
49
- def failed_feature_update_jobs
50
- feature_update_jobs.where.not(failed_at: nil)
51
- end
52
-
53
- def feature_update_jobs
54
- Delayed::Job.where(queue: delayed_jobs_queue_name)
55
- end
56
-
57
- private
58
-
59
- def delayed_jobs_queue_name
60
- "#{self.class}/#{self.id}/update_features"
61
- end
62
-
63
- def replace_features(kml_array)
64
- new_features = []
65
- kml_array.each {|kml| new_features.concat build_features(kml) }
66
-
67
- ActiveRecord::Base.transaction do
68
- self.features.destroy_all
69
- new_features.each(&:save)
70
- self.clear_association_cache # clear_association_cache so after_feature_update knows about the new features
71
-
72
- @feature_error_messages.concat new_features.collect {|feature| "Feature #{feature.name}: #{feature.errors.full_messages.to_sentence}" if feature.errors.present? }.compact.flatten
73
- if @feature_error_messages.present? && !@skip_invalid
74
- raise UpdateError, "Error updating #{self.class} #{self.id}. #{@feature_error_messages.to_sentence}"
75
- end
76
- end
77
- end
78
-
79
- def build_features(kml)
80
- new_type_features = []
81
-
82
- extract_kml_features(kml) do |feature_type, feature, name, metadata|
83
- begin
84
- new_type_features << build_feature(feature_type, name, metadata, build_geom(feature))
85
- rescue => e
86
- @feature_error_messages << e.message
87
- end
88
- end
89
-
90
- return new_type_features
91
- end
92
-
93
- # Use ST_Force_2D to discard z-coordinates that cause failures later in the process
94
- def build_geom(feature)
95
- if make_valid?
96
- geom = ActiveRecord::Base.connection.select_value("SELECT ST_CollectionExtract(ST_MakeValid(ST_Force_2D(ST_GeomFromKML('#{feature}'))),3)")
97
- else
98
- geom = ActiveRecord::Base.connection.select_value("SELECT ST_Force_2D(ST_GeomFromKML('#{feature}'))")
99
- end
100
- end
101
-
102
- def extract_kml_features(kml, &block)
103
- Nokogiri::XML(kml).css('Placemark').each do |placemark|
104
- name = placemark.css('name').text
105
- metadata = Hash[Nokogiri::XML(placemark.css('description').text).css('td').collect(&:text).each_slice(2).to_a]
106
-
107
- {'Polygon' => 'POLYGON', 'LineString' => 'LINE', 'Point' => 'POINT'}.each do |kml_type, sql_type|
108
- placemark.css(kml_type).each do |feature|
109
- yield sql_type, feature, name, metadata
110
- end
111
- end
112
- end
113
- end
114
-
115
- def build_feature(feature_type, name, metadata, geom)
116
- Feature.new(:spatial_model => self, :name => name, :metadata => metadata, :feature_type => feature_type, :geog => geom)
117
- end
118
-
119
- def open_kmz_url(url)
120
- url = URI(url)
121
-
122
- Zip::InputStream.open(open(url)) do |io|
123
- while (entry = io.get_next_entry)
124
- return io.read if entry.name.downcase == 'doc.kml'
125
- end
126
- end
127
-
128
- return nil
129
-
130
- rescue SocketError, Errno::ECONNREFUSED
131
- raise UpdateError, "ArcGIS Server is not responding. Ensure ArcGIS Server is running and accessible at #{[url.scheme, "//#{url.host}", url.port].select(&:present?).join(':')}."
132
- rescue OpenURI::HTTPError
133
- raise UpdateError, "ArcGIS Map Service not found. Ensure ArcGIS Server is running and accessible at #{url}."
134
- rescue => e
135
- raise UpdateError, e.message
136
- end
137
-
138
- # Can be overridden to use PostGIS to force geometry to be valid
139
- def make_valid?
140
- !!@make_valid
141
- end
142
-
143
- class UpdateError < StandardError; end
144
- end