lib/queries/collecting_event/filter.rb
module Queries
module CollectingEvent
class Filter < Query::Filter
# Params exists for all CollectingEvent attributes except these.
# geographic_area_id is excluded because we handle it specially in conjunction with `geographic_area_mode``
# Definition must preceed include.
ATTRIBUTES = (::CollectingEvent.core_attributes - %w{geographic_area_id} + %w(cached_level0_geographic_name cached_level1_geographic_name cached_level2_geographic_name)).map(&:to_sym).freeze
include Queries::Concerns::Attributes
include Queries::Concerns::Citations
include Queries::Concerns::DataAttributes
include Queries::Concerns::DateRanges
include Queries::Concerns::Depictions
include Queries::Concerns::Notes
include Queries::Concerns::Protocols
include Queries::Concerns::Tags
include Queries::Helpers
# This list of params are those that only occur
# in this CollectingEvent filter. For those
# that overlap other filters place in PARAMS.
#
# Used to define a base collecting query for CollectionObject
# filters scope, this is still used in CollectionObject query.
# This is "necessary" so that we can use CollectingEvent
# referencing facets in the CollectionObject filter for
# convienience.
#
BASE_PARAMS = [
*Queries::Concerns::Attributes.params,
*ATTRIBUTES,
:collectors,
:collecting_event_object_id,
:collection_objects,
:collector_id,
:collector_id_or,
:collecting_event_id,
:determiner_name_regex,
:geo_json,
:geographic_area,
:geographic_area_id,
:geographic_area_mode,
:georeferences,
:in_labels,
:md5_verbatim_label,
:radius,
:use_max,
:use_min,
:wkt,
collecting_event_id: [],
collector_id: [],
geographic_area_id: [],
].inject([{}]){|ary, k| k.is_a?(Hash) ? ary.last.merge!(k) : ary.unshift(k); ary}.freeze
PARAMS = [
*BASE_PARAMS,
:otu_id,
:collection_object_id,
collection_object_id: [],
otu_id: [],
].inject([{}]){|ary, k| k.is_a?(Hash) ? ary.last.merge!(k) : ary.unshift(k); ary}.freeze
def self.base_params
a = BASE_PARAMS.dup
b = a.pop.keys
(a + b).uniq
end
# @return [Boolean, nil]
# true - A collector role exists
# false - A collector role exists
# nil - not applied
attr_accessor :collectors
# @param collecting_event_id [ Array, Integer, nil]
# One or more collecting_event_id
attr_accessor :collecting_event_id
# Wildcard wrapped matching any label
attr_accessor :in_labels
# If true then in_labels checks only the MD5
attr_accessor :md5_verbatim_label
# A spatial representation in well known text
attr_accessor :wkt
# Integer in Meters
# !! defaults to 100m
attr_accessor :radius
# @return [Hash, nil]
# in geo_json format (no Feature ...) ?!
attr_accessor :geo_json
# DONE: singularize and handle array or single
# @return [Array]
attr_accessor :otu_id
# DONE: singularize and handle array or single
# @return [Array]
attr_accessor :collector_id
# @return [Boolean]
# @param collector_id_or [String]
# `false`, nil - treat the ids in collector_id as "or"
# 'true' - treat the ids in collector_id as "and" (only CollectingEvent with all and only all of collector_id will match)
attr_accessor :collector_id_or
# @param collection_objects [String, nil]
# legal values are 'true', 'false'
# `true` - match only CollectingEvents with associated CollectionObjects
# `false` - match only CollectingEvents without associated CollectionObjects
# @return collection_objects [Boolean, nil]
#
# whether the CollectingEvent has associated CollectionObjects
attr_accessor :collection_objects
# @return Array
# @param collection_object_id [Array, Integer, String]
# all collecting events matching collection objects
attr_accessor :collection_object_id
# @return [True, False, nil]
# true - georeferences
# false - not georeferenced
# nil - not applied
attr_accessor :georeferences
# @return [True, False, nil]
# true - has geographic_area present
# false - without geographic_area
# nil - not applied
attr_accessor :geographic_area
# See /lib/queries/otu/filter.rb
attr_accessor :geographic_area_id
attr_accessor :geographic_area_mode
# @return [String, nil]
# the maximum number of CollectionObjects linked to CollectingEvent
attr_accessor :use_max
# @return [String, nil]
# the minimum number of CollectionObjects linked to CollectingEvent
attr_accessor :use_min
def initialize(query_params)
super
@collectors = boolean_param(params, :collectors )
@collecting_event_id = params[:collecting_event_id]
@collection_object_id = params[:collection_object_id]
@collection_objects = boolean_param(params, :collection_objects )
@collector_id = params[:collector_id]
@collector_id_or = boolean_param(params, :collector_id_or )
@geo_json = params[:geo_json]
@geographic_area = boolean_param(params, :geographic_area)
@geographic_area_id = params[:geographic_area_id]
@geographic_area_mode = boolean_param(params, :geographic_area_mode)
@georeferences = boolean_param(params, :georeferences)
@in_labels = params[:in_labels]
@md5_verbatim_label = params[:md5_verbatim_label]&.to_s&.downcase == 'true'
@otu_id = params[:otu_id].presence
@radius = params[:radius].presence || 100.0
@use_max = params[:use_max]
@use_min = params[:use_min]
@wkt = params[:wkt]
set_attributes_params(params)
set_citations_params(params)
set_data_attributes_params(params)
set_date_params(params)
set_depiction_params(params)
set_notes_params(params)
set_protocols_params(params)
set_tags_params(params)
end
def collecting_event_id
[@collecting_event_id].flatten.compact
end
def collection_object_id
[@collection_object_id].flatten.compact
end
def geographic_area_id
[@geographic_area_id].flatten.compact
end
def collector_id
[@collector_id].flatten.compact
end
def otu_id
[@otu_id].flatten.compact
end
def use_facet
return nil if (use_min.blank? && use_max.blank?)
min_max = [use_min&.to_i, use_max&.to_i ].compact
q = ::CollectingEvent.joins(:collection_objects)
.select('collecting_events.*, COUNT(collection_objects.collecting_event_id)')
.group('collecting_events.id')
.having("COUNT(collecting_event_id) >= #{min_max[0]}")
# Untested
q = q.having("COUNT(collecting_event_id) <= #{min_max[1]}") if min_max[1]
::CollectingEvent.from('(' + q.to_sql + ') as collecting_events').distinct
end
def geographic_area_facet
return nil if geographic_area.nil?
if geographic_area
::CollectingEvent.where.not(geographic_area_id: nil).distinct
else
::CollectingEvent.where(geographic_area_id: nil).distinct
end
end
def geographic_area_id_facet
return nil if geographic_area_id.empty?
a = nil
case geographic_area_mode
when nil, true # exact and spatial start the same
a = ::GeographicArea.where(id: geographic_area_id)
when false # descendants
a = ::GeographicArea.descendants_of_any(geographic_area_id)
end
b = nil
case geographic_area_mode
when nil, false # exact, descendants
return ::CollectingEvent.where(geographic_area: a)
when true # spatial
i = ::GeographicItem.joins(:geographic_areas).where(geographic_areas: a) # .unscope
wkt_shape = ::GeographicItem.st_union(i).to_a.first['collection'].to_s
return from_wkt(wkt_shape)
end
end
def georeferences_facet
return nil if georeferences.nil?
if georeferences
::CollectingEvent.joins(:georeferences).distinct
else
::CollectingEvent.left_outer_joins(:georeferences)
.where(georeferences: {id: nil})
.distinct
end
end
# @return Scope
def collection_objects_facet
return nil if collection_objects.nil?
subquery = ::CollectionObject.where(::CollectionObject.arel_table[:collecting_event_id].eq(::CollectingEvent.arel_table[:id])).arel.exists
::CollectingEvent.where(collection_objects ? subquery : subquery.not)
end
# TODO: dry with Source, TaxonName, etc.
def collector_id_facet
return nil if collector_id.empty?
o = table
r = ::Role.arel_table
a = o.alias('a_')
b = o.project(a[Arel.star]).from(a)
c = r.alias('r1')
b = b.join(c, Arel::Nodes::OuterJoin)
.on(
a[:id].eq(c[:role_object_id])
.and(c[:role_object_type].eq('CollectingEvent'))
.and(c[:type].eq('Collector'))
)
e = c[:id].not_eq(nil)
f = c[:person_id].in(collector_id)
b = b.where(e.and(f))
b = b.group(a['id'])
b = b.having(a['id'].count.eq(collector_id.length)) unless collector_id_or
b = b.as('col_z_')
::CollectingEvent.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(o['id']))))
end
def wkt_facet
return nil if wkt.blank?
from_wkt(wkt)
end
# TODO: check, this should be simplifiable.
def from_wkt(wkt_shape)
a = RGeo::WKRep::WKTParser.new(Gis::FACTORY, support_wkt12: true)
b = a.parse(wkt_shape)
spatial_query(b.geometry_type.to_s, wkt_shape)
end
# Shape is a Hash in GeoJSON format
def geo_json_facet
return nil if geo_json.nil?
if a = RGeo::GeoJSON.decode(geo_json)
return spatial_query(a.geometry_type.to_s, a.to_s)
else
return nil
end
end
# TODO: Spatial concern?
def spatial_query(geometry_type, wkt)
case geometry_type
when 'Point'
::CollectingEvent
.joins(:geographic_items)
.where(::GeographicItem.within_radius_of_wkt_sql(wkt, radius ))
when 'Polygon', 'MultiPolygon'
::CollectingEvent
.joins(:geographic_items)
.where(::GeographicItem.contained_by_wkt_sql(wkt))
else
nil
end
end
def any_label_facet
return nil if in_labels.blank?
t = "%#{in_labels}%"
table[:verbatim_label].matches(t).or(table[:print_label].matches(t)).or(table[:document_label].matches(t))
end
def verbatim_label_md5_facet
return nil unless md5_verbatim_label && in_labels.present?
md5 = ::Utilities::Strings.generate_md5(in_labels)
table[:md5_of_verbatim_label].eq(md5)
end
def collecting_event_id_facet
return nil if collecting_event_id.empty?
table[:id].in(collecting_event_id)
end
def otu_id_facet
return nil if otu_id.empty?
::CollectingEvent.joins(:otus).where(otus: {id: otu_id}).distinct
end
def matching_collection_object_id
return nil if collection_object_id.empty?
::CollectingEvent.joins(:collection_objects).where(collection_objects: {id: collection_object_id}).distinct
end
def collectors_facet
return nil if collectors.nil?
if collectors
::CollectingEvent.joins(:collectors)
else
::CollectingEvent.where.missing(:collectors)
end
end
def biological_association_query_facet
return nil if biological_association_query.nil?
s = 'WITH query_ba_ces AS (' + biological_association_query.all.to_sql + ') ' +
::CollectingEvent.joins(:collection_objects)
.joins("LEFT JOIN query_ba_ces as query_ba_ces1 on collection_objects.id = query_ba_ces1.biological_association_subject_id AND query_ba_ces1.biological_association_subject_type = 'CollectionObject'")
.joins("LEFT JOIN query_ba_ces as query_ba_ces2 on collection_objects.id = query_ba_ces2.biological_association_object_id AND query_ba_ces2.biological_association_object_type = 'CollectionObject'")
.where('(query_ba_ces1.id) IS NOT NULL OR (query_ba_ces2.id IS NOT NULL)')
.to_sql
::CollectingEvent.from('(' + s + ') as collecting_events').distinct
end
def otu_query_facet
return nil if otu_query.nil?
s = 'WITH query_otu_ces AS (' + otu_query.all.to_sql + ') ' +
::CollectingEvent.joins(:otus)
.joins('JOIN query_otu_ces as query_otu_ces1 on query_otu_ces1.id = otus.id')
.to_sql
::CollectingEvent.from('(' + s + ') as collecting_events').distinct
end
def collection_object_query_facet
return nil if collection_object_query.nil?
s = 'WITH query_co_ce AS (' + collection_object_query.all.to_sql + ') ' +
::CollectingEvent
.joins(:collection_objects)
.joins('JOIN query_co_ce as query_co_ce1 on collection_objects.id = query_co_ce1.id')
.to_sql
::CollectingEvent.from('(' + s + ') as collecting_events').distinct
end
def taxon_name_query_facet
return nil if taxon_name_query.nil?
s = 'WITH query_tn_ce AS (' + taxon_name_query.all.to_sql + ') ' +
::CollectingEvent
.joins(collection_objects: [:otus])
.joins('JOIN query_tn_ce as query_tn_ce1 on otus.taxon_name_id = query_tn_ce1.id')
.to_sql
::CollectingEvent.from('(' + s + ') as collecting_events').distinct
end
def housekeeping_extensions
[
housekeeping_extension_query(target: ::DataAttribute, joins: [:data_attributes]),
housekeeping_extension_query(target: ::Georeference, joins: [:georeferences]),
housekeeping_extension_query(target: ::Note, joins: [:notes]),
housekeeping_extension_query(target: ::Role, joins: [:roles]),
]
end
# @return [Array]
def and_clauses
[
between_date_range_facet,
any_label_facet,
collecting_event_id_facet,
verbatim_label_md5_facet,
]
end
def merge_clauses
[
biological_association_query_facet,
collection_object_query_facet,
otu_query_facet,
taxon_name_query_facet,
collectors_facet,
collection_objects_facet,
collector_id_facet,
geo_json_facet,
geographic_area_facet,
geographic_area_id_facet,
georeferences_facet,
matching_collection_object_id,
otu_id_facet,
use_facet,
wkt_facet,
]
end
end
end
end