SpeciesFileGroup/taxonworks

View on GitHub
app/models/field_occurrence/dwc_extensions.rb

Summary

Maintainability
C
1 day
Test Coverage
module FieldOccurrence::DwcExtensions

  extend ActiveSupport::Concern

  # include FieldOccurrence::DwcExtensions::TaxonworksExtensions

  included do

    # A current list of mappable values
    # Even though it is a Hash it maintains key order, which is
    # semi-useful for quick reporting.
    DWC_OCCURRENCE_MAP = {
   #  catalogNumber: :dwc_catalog_number,
   #  otherCatalogNumbers: :dwc_other_catalog_numbers,
   #  individualCount: :dwc_individual_count,
   #  preparations: :dwc_preparations,
   #  lifeStage: :dwc_life_stage,
   #  sex: :dwc_sex,
   #  caste: :dwc_caste,
   #  country: :dwc_country,
   #  stateProvince: :dwc_state_province,
   #  county: :dwc_county,

   #  eventDate: :dwc_event_date,
   #  eventTime: :dwc_event_time,
   #  year: :dwc_year,
   #  month: :dwc_month,
   #  day: :dwc_day,
   #  startDayOfYear: :dwc_start_day_of_year,
   #  endDayOfYear: :dwc_end_day_of_year,

   #  fieldNumber: :dwc_field_number,
   #  maximumElevationInMeters: :dwc_maximum_elevation_in_meters,
   #  minimumElevationInMeters: :dwc_minimum_elevation_in_meters,
   #  samplingProtocol: :dwc_sampling_protocol,
   #  habitat: :dwc_verbatim_habitat,
   #  verbatimElevation: :dwc_verbatim_elevation,
   #  verbatimEventDate: :dwc_verbatim_event_date,
   #  verbatimLocality: :dwc_verbatim_locality,
   #  waterBody: :dwc_water_body,
   #  minimumDepthInMeters: :dwc_minimum_depth_in_meters,
   #  maximumDepthInMeters: :dwc_maximum_depth_in_meters,
   #  verbatimDepth: :dwc_verbatim_depth,
   #  identifiedBy: :dwc_identified_by,
   #  identifiedByID: :dwc_identified_by_id,
   #  dateIdentified: :dwc_date_identified,
   #  nomenclaturalCode: :dwc_nomenclatural_code,
   #  kingdom: :dwc_kingdom,

   #  phylum: :dwc_phylum,
   #  dwcClass: :dwc_class,
   #  order: :dwc_order,
   #  higherClassification: :dwc_higher_classification,

   #  superfamily: :dwc_superfamily,
   #  family: :dwc_family,
   #  subfamily: :dwc_subfamily,
   #  tribe: :dwc_tribe,
   #  subtribe: :dwc_subtribe,
   #  genus: :dwc_genus,
   #  specificEpithet: :dwc_specific_epithet,
   #  infraspecificEpithet: :dwc_infraspecific_epithet,
   #  scientificName: :dwc_scientific_name,
   #  scientificNameAuthorship: :dwc_taxon_name_authorship,
   #  taxonRank: :dwc_taxon_rank,
   #  previousIdentifications: :dwc_previous_identifications,

   #  typeStatus: :dwc_type_status,

   #  institutionCode: :dwc_institution_code,
   #  institutionID: :dwc_institution_id,

   #  recordedBy: :dwc_recorded_by,
   #  recordedByID: :dwc_recorded_by_id,

   #  # Georeference "Interface'
   #  verbatimCoordinates: :dwc_verbatim_coordinates,
   #  verbatimLatitude: :dwc_verbatim_latitude,
   #  verbatimLongitude: :dwc_verbatim_longitude,
   #  decimalLatitude: :dwc_decimal_latitude,
   #  decimalLongitude: :dwc_decimal_longitude,
   #  footprintWKT: :dwc_footprint_wkt,

   #  coordinateUncertaintyInMeters: :dwc_coordinate_uncertainty_in_meters,
   #  geodeticDatum: :dwc_geodetic_datum,
   #  georeferenceProtocol: :dwc_georeference_protocol,
   #  georeferenceRemarks: :dwc_georeference_remarks,
   #  georeferenceSources: :dwc_georeference_sources,
   #  georeferencedBy: :dwc_georeferenced_by,
   #  georeferencedDate: :dwc_georeferenced_date,
   #  verbatimSRS: :dwc_verbatim_srs,

   #  occurrenceStatus: :dwc_occurrence_status,

   #  # TODO: move to a proper extension(?)
   #  associatedMedia: :dwc_associated_media,

   #  # TODO: move to a proper extension(?)
   #  associatedTaxa: :dwc_associated_taxa,

   #  occurrenceRemarks: :dwc_occurrence_remarks,

   #  identificationRemarks: :dwc_identification_remarks,

   #  eventRemarks: :dwc_event_remarks,

   #  verbatimLabel: :dwc_verbatim_label,

   #  # -- Core taxon? --
   #  # nomenclaturalCode
   #  # scientificName
   #  # taxonmicStatus NOT DONE
   #  # scientificNameAuthorship
   #  # scientificNameID  NOT DONE
   #  # taxonRank
   #  # namePublishedIn NOT DONE
    }.freeze

    # verbatim label data

    attr_accessor :georeference_attributes

    # @return [Hash]
    # getter returning georeference related attributes
    def georeference_attributes(force = false)
      if force
        @georeference_attributes = set_georeference_attributes
      else
        @georeference_attributes ||= set_georeference_attributes
      end
    end
  end

  # @return [Hash]
  #
  def set_georeference_attributes
    case collecting_event&.dwc_georeference_source
    when :georeference
      collecting_event.preferred_georeference.dwc_georeference_attributes
    when :verbatim
      h = collecting_event.dwc_georeference_attributes

      if a = collecting_event&.attribute_updater(:verbatim_latitude)
        h[:georeferencedBy] = User.find(a).name
      end

      # verbatim_longitude could technically be different, but...
      h[:georeferencedDate] = collecting_event&.attribute_updated(:verbatim_latitude)

      h

    when :geographic_area
      h = collecting_event.geographic_area.dwc_georeference_attributes
      if a = collecting_event&.attribute_updater(:geographic_area_id)
        h[:georeferencedBy] = User.find(a).name
      end

      h[:georeferencedDate] = collecting_event&.attribute_updated(:geographic_area_id)

      h
    else
      {}
    end
  end

  def is_fossil?
    biocuration_classes.where(uri: DWC_FOSSIL_URI).any?
  end

  # use buffered if any
  # if not check CE verbatim_label
  def dwc_verbatim_label
    b = [buffered_collecting_event, buffered_determinations, buffered_other_labels].compact
    return  b.join("\n\n") if b.present?
    collecting_event&.verbatim_label.presence
  end

  def dwc_occurrence_status
    'present'
  end

  # https://dwc.tdwg.org/list/#dwc_georeferenceRemarks
  def dwc_occurrence_remarks
    notes.collect{|n| n.text}&.join(FieldOccurrence::DWC_DELIMITER)
  end

  # https://dwc.tdwg.org/list/#dwc_identificationRemarks
  def dwc_identification_remarks
    current_taxon_determination&.notes&.collect { |n| n.text }&.join(FieldOccurrence::DWC_DELIMITER)
  end

  def dwc_event_remarks
    collecting_event&.notes&.collect {|n| n.text}&.join(FieldOccurrence::DWC_DELIMITER)
  end

  # https://dwc.tdwg.org/terms/#dwc:associatedMedia
  def dwc_associated_media
    images.collect{|i| api_image_link(i) }.join(FieldOccurrence::DWC_DELIMITER).presence
  end

  # https://dwc.tdwg.org/terms/#dwc:associatedtaxa
  def dwc_associated_taxa
    dwc_internal_attribute_for(:collection_object, :associatedTaxa)
  end

  # TODO: likeley a helper
  def api_image_link(image)
    s = ENV['SERVER_NAME']
    if s.nil?
      s ||= 'http://127.0.0.1:3000'
    else
      s = 'https://' + s
    end

    s = s + '/api/v1/images/' + image.image_file_fingerprint # An experiment, use md5 as a proxy for id (also unique id)
  end

  def dwc_georeference_sources
    georeference_attributes[:georeferenceSources]
  end

  def dwc_georeference_remarks
    georeference_attributes[:georeferenceRemarks]
  end

  def dwc_footprint_wkt
    georeference_attributes[:footprintWKT]
  end

  def dwc_georeferenced_by
    georeference_attributes[:georeferencedBy]
  end

  def dwc_georeferenced_date
    georeference_attributes[:georeferencedDate]
  end

  def dwc_geodetic_datum
    georeference_attributes[:geodeticDatum]
  end

  def dwc_verbatim_srs
    georeference_attributes[:dwcVerbatimSrs]
  end

  # georeferenceDate
  # technically could look at papertrail to see when geographic_area_id appeared
  def dwc_georeferenced_date
    collecting_event&.attribute_updated(:geographic_area_id)
  end

  # TODO: extend to Georeferences when we understand how to describe spatial uncertainty
  def dwc_coordinate_uncertainty_in_meters
    if georeference_attributes[:coordinateUncertaintyInMeters]
      georeference_attributes[:coordinateUncertaintyInMeters]
    else
      collecting_event&.verbatim_geolocation_uncertainty
    end
  end

  def dwc_verbatim_latitude
    collecting_event&.verbatim_latitude
  end

  def dwc_verbatim_longitude
    collecting_event&.verbatim_longitude
  end

  def dwc_other_catalog_numbers
    i = identifiers.where.not('type ilike ?', 'Identifier::Global::Uuid%').order(:position).to_a
    i.shift
    i.map(&:cached).join(FieldOccurrence::DWC_DELIMITER).presence
  end

  def dwc_previous_identifications
    a = taxon_determinations.order(:position).to_a
    a.shift
    a.collect{|d| ApplicationController.helpers.label_for_taxon_determination(d)}.join(FieldOccurrence::DWC_DELIMITER).presence
  end

  def dwc_internal_attribute_for(target = :collection_object, dwc_term_name)
    return nil if dwc_term_name.nil?

    case target
    when  :collecting_event
      return nil unless collecting_event
      collecting_event.internal_attributes.includes(:predicate)
        .where(
          controlled_vocabulary_terms: {uri: ::DWC_ATTRIBUTE_URIS[dwc_term_name.to_sym] })
        .pluck(:value)&.join(', ').presence
    when :collection_object
      internal_attributes.includes(:predicate)
        .where(
          controlled_vocabulary_terms: {uri: ::DWC_ATTRIBUTE_URIS[dwc_term_name.to_sym] })
        .pluck(:value)&.join(', ').presence
    else
      nil
    end
  end

  def dwc_water_body
    dwc_internal_attribute_for(:collecting_event, :waterBody)
  end

  def dwc_minimum_depth_in_meters
    dwc_internal_attribute_for(:collecting_event, :minimumDepthInMeters)
  end

  def dwc_maximum_depth_in_meters
    dwc_internal_attribute_for(:collecting_event, :maximumDepthInMeters)
  end

  def dwc_verbatim_depth
    dwc_internal_attribute_for(:collecting_event, :verbatimDepth)
  end

  # TODO: consider CVT attributes with Predicates linked to URIs
  def dwc_life_stage
    biocuration_classes.tagged_with_uri(::DWC_ATTRIBUTE_URIS[:lifeStage])
      .pluck(:name)&.join(', ').presence # `.presence` is a Rails extension
  end

  # TODO: consider CVT attributes with Predicates linked to URIs
  def dwc_sex
    biocuration_classes.tagged_with_uri(::DWC_ATTRIBUTE_URIS[:sex])
      .pluck(:name)&.join(', ').presence
  end

  def dwc_caste
    biocuration_classes.tagged_with_uri(::DWC_ATTRIBUTE_URIS[:caste])
       .pluck(:name)&.join(', ').presence
  end

  def dwc_verbatim_coordinates
    return nil unless collecting_event
    [collecting_event.verbatim_latitude, collecting_event.verbatim_longitude].compact.join(' ').presence
  end

  def dwc_verbatim_elevation
    collecting_event&.verbatim_elevation
  end

  def dwc_maximum_elevation_in_meters
    collecting_event&.maximum_elevation
  end

  def dwc_minimum_elevation_in_meters
    collecting_event&.minimum_elevation
  end

  # TODO: Reconcile with Protocol (capital P) assignments
  def dwc_sampling_protocol
    collecting_event&.verbatim_method
  end

  def dwc_field_number
    return nil unless collecting_event
    collecting_event.identifiers.where(type: 'Identifier::Local::TripCode').first&.cached
  end

  def dwc_verbatim_habitat
    collecting_event&.verbatim_habitat
  end

  def dwc_infraspecific_epithet
    %w{variety form subspecies}.each do |n| # add more as observed
      return taxonomy[n].last if taxonomy[n]
    end
    nil
  end

  def dwc_taxon_rank
    current_taxon_name&.rank
  end

  # holotype of Ctenomys sociabilis. Pearson O. P., and M. I. Christie. 1985. Historia Natural, 5(37):388, holotype of Pinus abies | holotype of Picea abies
  def dwc_type_status
    type_materials.all.collect{|t|
      ApplicationController.helpers.label_for_type_material(t)
    }.join(FieldOccurrence::DWC_DELIMITER).presence
  end

  # ISO 8601:2004(E).
  def dwc_date_identified
    current_taxon_determination&.date.presence
  end

  def dwc_higher_classification
    v = taxonomy.values.collect{|a| a.kind_of?(Array) ? a.second : a}
    v.shift
    v.pop
    v.compact
    v.join(FieldOccurrence::DWC_DELIMITER)
  end

  def dwc_kingdom
    taxonomy['kingdom']
  end

  def dwc_phylum
    taxonomy['phylum']
  end

  def dwc_class
    taxonomy['class']
  end

  def dwc_order
    taxonomy['order']
  end

  # http://rs.tdwg.org/dwc/terms/superfamily
  def dwc_superfamily
    taxonomy['superfamily']
  end

  # http://rs.tdwg.org/dwc/terms/family
  def dwc_family
    taxonomy['family']
  end

  # http://rs.tdwg.org/dwc/terms/subfamily
  def dwc_subfamily
    taxonomy['subfamily']
  end

  # http://rs.tdwg.org/dwc/terms/tribe
  def dwc_tribe
    taxonomy['tribe']
  end

  # http://rs.tdwg.org/dwc/terms/subtribe
  def dwc_subtribe
    taxonomy['subtribe']
  end

  # http://rs.tdwg.org/dwc/terms/genus
  def dwc_genus
    taxonomy['genus'] && taxonomy['genus'].compact.join(' ').presence
  end

  # http://rs.tdwg.org/dwc/terms/species
  def dwc_specific_epithet
    taxonomy['species'] && taxonomy['species'].compact.join(' ').presence
  end

  def dwc_scientific_name
    current_taxon_name.try(:cached_name_and_author_year)
  end

  def dwc_taxon_name_authorship
    current_taxon_name.try(:cached_author_year)
  end

  # Definition: A list (concatenated and separated) of names of people, groups, or organizations responsible for recording the original Occurrence. The primary collector or observer, especially one who applies a personal identifier (recordNumber), should be listed first.
  #
  # This was interpreted as collectors (in the field in this context), not those who recorded other aspects of the data.
  def dwc_recorded_by
    v = nil
    if collecting_event
      v = collecting_event.collectors
        .order('roles.position')
        .map(&:name)
        .join(FieldOccurrence::DWC_DELIMITER)
        .presence
      v = collecting_event.verbatim_collectors.presence if v.blank?
    end
    v
  end

  # See dwc_recorded_by
  # TODO: Expand to any GlobalIdentifier
  def dwc_recorded_by_id
    if collecting_event
      collecting_event.collectors
        .order('roles.position')
        .map(&:orcid)
        .compact
        .join(FieldOccurrence::DWC_DELIMITER)
        .presence
    end
  end

  def dwc_identified_by
    # TaxonWorks allows for groups of determiners to collaborate on a single determination if they collectively came to a conclusion.
    current_taxon_determination&.determiners&.map(&:name)&.join(FieldOccurrence::DWC_DELIMITER).presence
  end

  def dwc_identified_by_id
    # TaxonWorks allows for groups of determiners to collaborate on a single determination if they collectively came to a conclusion.
    current_taxon_determination&.determiners&.map(&:orcid)&.join(FieldOccurrence::DWC_DELIMITER).presence
  end

  def dwc_institution_code
    repository.try(:acronym)
  end

  def dwc_collection_code
    catalog_number_namespace&.verbatim_short_name || catalog_number_namespace&.short_name
  end

  def dwc_catalog_number
    catalog_number_cached # via delegation
  end

  # TODO: handle ranged lots
  def dwc_individual_count
    total
  end

  def dwc_country
    v = try(:collecting_event).try(:geographic_names)
    v[:country] if v
  end

  def dwc_state_province
    v = try(:collecting_event).try(:geographic_names)
    v[:state] if v
  end

  def dwc_county
    v = try(:collecting_event).try(:geographic_names)
    v[:county] if v
  end

  def dwc_locality
    collecting_event.try(:verbatim_locality)
  end

  def dwc_decimal_latitude
    georeference_attributes[:decimalLatitude]
  end

  def dwc_decimal_longitude
    georeference_attributes[:decimalLongitude]
  end

  def dwc_verbatim_locality
    collecting_event.try(:verbatim_locality)
  end

  def dwc_nomenclatural_code
    current_otu.try(:taxon_name).try(:nomenclatural_code)
  end

  def dwc_event_time
    return unless collecting_event

    %w{start_time end_time}
      .map { |t| %w{hour minute second}
      .map { |p| collecting_event["#{t}_#{p}"] }
      .map { |p| '%02d' % p if p } # At least two digits
      }
        .map { |t| t.compact.join(':') }
        .reject(&:blank?)
        .join('/').presence
  end

  def dwc_verbatim_event_date
    collecting_event&.verbatim_date
  end

  def dwc_event_date
    return unless collecting_event

    %w{start_date end_date}
      .map { |d| %w{year month day}
      .map { |p| collecting_event["#{d}_#{p}"] }
      .map { |p| '%02d' % p if p } # At least two digits
      }
        .map { |d| d.compact.join('-') }
        .reject(&:blank?)
        .join('/').presence
  end

  def dwc_year
    return unless collecting_event
    collecting_event.start_date_year.presence
  end

  def dwc_month
    return unless collecting_event
    collecting_event.start_date_month.presence
  end

  def dwc_day
    return unless collecting_event
    collecting_event.start_date_day.presence
  end

  def dwc_start_day_of_year
    return unless collecting_event
    collecting_event.start_day_of_year.presence
  end

  def dwc_end_day_of_year
    return unless collecting_event
    collecting_event.end_day_of_year.presence
  end

  def dwc_preparations
    preparation_type_name
  end

  # we assert custody, NOT ownership
  def dwc_institution_code
    repository_acronym
  end

  # we assert custody, NOT ownership
  def dwc_institution_id
    repository_url
  end

  def dwc_georeference_protocol
    georeference_attributes[:georeferenceProtocol]
  end

end