app/models/feature.rb in spatial_features-2.7.8 vs app/models/feature.rb in spatial_features-2.8.0
- old
+ new
@@ -1,170 +1,62 @@
-class Feature < ActiveRecord::Base
- belongs_to :spatial_model, :polymorphic => :true, :autosave => false
+class Feature < AbstractFeature
+ class_attribute :automatically_refresh_aggregate
+ self.automatically_refresh_aggregate = true
- attr_writer :make_valid
+ class_attribute :lowres_precision
+ self.lowres_precision = 5
- FEATURE_TYPES = %w(polygon point line)
+ has_one :aggregate_feature, lambda { |feature| where(:spatial_model_type => feature.spatial_model_type) }, :foreign_key => :spatial_model_id, :primary_key => :spatial_model_id
- before_validation :sanitize_feature_type
- validates_presence_of :geog
- validate :validate_geometry
validates_inclusion_of :feature_type, :in => FEATURE_TYPES
- before_save :sanitize
- after_save :cache_derivatives
- def self.cache_key
- "#{maximum(:id)}-#{count}"
- end
+ after_save :refresh_aggregate, if: :automatically_refresh_aggregate?
- def self.with_metadata(k, v)
- if k.present? && v.present?
- where('metadata->? = ?', k, v)
- else
- all
- end
- end
+ def self.defer_aggregate_refresh(&block)
+ start_at = Feature.maximum(:id).to_i + 1
+ output = without_aggregate_refresh(&block)
- def self.polygons
- where(:feature_type => 'polygon')
- end
+ where(:id => start_at..Float::INFINITY).refresh_aggregates
- def self.lines
- where(:feature_type => 'line')
+ return output
end
- def self.points
- where(:feature_type => 'point')
+ def self.without_aggregate_refresh
+ old = Feature.automatically_refresh_aggregate
+ Feature.automatically_refresh_aggregate = false
+ yield
+ ensure
+ Feature.automatically_refresh_aggregate = old
end
- def self.area_in_square_meters(geom = 'geom_lowres')
- current_scope = all.polygons
- unscoped { connection.select_value(select("ST_Area(ST_Union(#{geom}))").from(current_scope, :features)).to_f }
- end
+ def self.refresh_aggregates
+ # Find one feature from each spatial model and trigger the aggregate feature refresh
+ ids = select('MAX(id)')
+ .where.not(:spatial_model_type => nil, :spatial_model_id => nil)
+ .group('spatial_model_type, spatial_model_id')
- def self.total_intersection_area_in_square_meters(other_features, geom = 'geom_lowres')
- scope = unscope(:select).select("ST_Union(#{geom}) AS geom").polygons
- other_scope = other_features.polygons
-
- query = unscoped.select('ST_Area(ST_Intersection(ST_Union(features.geom), ST_Union(other_features.geom)))')
- .from(scope, "features")
- .joins("INNER JOIN (#{other_scope.to_sql}) AS other_features ON ST_Intersects(features.geom, other_features.geom)")
- return connection.select_value(query).to_f
+ where(:id => ids).find_each(&:refresh_aggregate)
end
- def self.intersecting(other)
- join_other_features(other).where('ST_Intersects(features.geom_lowres, other_features.geom_lowres)').uniq
+ def refresh_aggregate
+ # puts "Refreshing AggregateFeature for #{spatial_model_type} #{spatial_model_id}"
+ build_aggregate_feature unless aggregate_feature&.persisted?
+ aggregate_feature.refresh
end
- def self.invalid(column = 'geog::geometry')
- select("features.*, ST_IsValidReason(#{column}) AS invalid_geometry_message").where.not("ST_IsValid(#{column})")
+ def automatically_refresh_aggregate?
+ # Check if there is a spatial model id because nothing prevents is from creating a Feature without one. Depending on
+ # how you assign a feature to a record, you may end up saving it before assigning it to a record, thereby leaving
+ # this field blank.
+ spatial_model_id? && automatically_refresh_aggregate
end
- def self.valid
- where('ST_IsValid(geog::geometry)')
- end
-
- def envelope(buffer_in_meters = 0)
- envelope_json = JSON.parse(self.class.select("ST_AsGeoJSON(ST_Envelope(ST_Buffer(features.geog, #{buffer_in_meters})::geometry)) AS result").where(:id => id).first.result)
- envelope_json = envelope_json["coordinates"].first
-
- raise "Can't calculate envelope for Feature #{self.id}" if envelope_json.blank?
-
- return envelope_json.values_at(0,2)
- end
-
+ # Features are used for display so we also cache their KML representation
def self.cache_derivatives(options = {})
- options.reverse_merge! :lowres_simplification => 2, :lowres_precision => 5
-
+ super
update_all <<-SQL.squish
- geom = ST_Transform(geog::geometry, #{detect_srid('geom')}),
- north = ST_YMax(geog::geometry),
- east = ST_XMax(geog::geometry),
- south = ST_YMin(geog::geometry),
- west = ST_XMin(geog::geometry),
- area = ST_Area(geog),
- centroid = ST_PointOnSurface(geog::geometry)
- SQL
-
- invalid('geom').update_all <<-SQL.squish
- geom = ST_Buffer(geom, 0)
- SQL
-
- update_all <<-SQL.squish
- geom_lowres = ST_SimplifyPreserveTopology(geom, #{options[:lowres_simplification]})
- SQL
-
- invalid('geom_lowres').update_all <<-SQL.squish
- geom_lowres = ST_Buffer(geom_lowres, 0)
- SQL
-
- update_all <<-SQL.squish
kml = ST_AsKML(geog, 6),
- kml_lowres = ST_AsKML(geom_lowres, #{options[:lowres_precision]}),
+ kml_lowres = ST_AsKML(geom_lowres, #{options.fetch(:lowres_precision, lowres_precision)}),
kml_centroid = ST_AsKML(centroid)
SQL
- end
-
- def feature_bounds
- {n: north, e: east, s: south, w: west}
- end
-
- def cache_derivatives(*args)
- self.class.where(:id => self.id).cache_derivatives(*args)
- end
-
- def kml(options = {})
- geometry = options[:lowres] ? kml_lowres : super()
- geometry = "<MultiGeometry>#{geometry}#{kml_centroid}</MultiGeometry>" if options[:centroid]
- return geometry
- end
-
- def make_valid?
- @make_valid
- end
-
- private
-
- def make_valid
- self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Buffer('#{sanitize}', 0)")
- end
-
- # Use ST_Force2D to discard z-coordinates that cause failures later in the process
- def sanitize
- self.geog = ActiveRecord::Base.connection.select_value("SELECT ST_Force2D('#{geog}')")
- end
-
- SRID_CACHE = {}
- def self.detect_srid(column_name)
- SRID_CACHE[column_name] ||= connection.select_value("SELECT Find_SRID('public', '#{table_name}', '#{column_name}')")
- end
-
- def self.join_other_features(other)
- joins('INNER JOIN features AS other_features ON true').where(:other_features => {:id => other})
- end
-
- def validate_geometry
- return unless geog?
-
- error = geometry_validation_message
- if error && make_valid?
- make_valid
- self.make_valid = false
- validate_geometry
- elsif error
- errors.add :geog, error
- end
- end
-
- def geometry_validation_message
- error = self.class.connection.select_one(self.class.unscoped.invalid.from("(SELECT '#{sanitize_input_for_sql(self.geog)}'::geometry AS geog) #{self.class.table_name}"))
- return error.fetch('invalid_geometry_message') if error
- end
-
- def sanitize_feature_type
- self.feature_type = FEATURE_TYPES.find {|type| self.feature_type.to_s.strip.downcase.include?(type) }
- end
-
- def sanitize_input_for_sql(input)
- self.class.send(:sanitize_sql_for_conditions, input)
end
end