Class: Georeference

Overview

A Georeference is an assertion that some shape, as derived from some method, describes the location of some CollectingEvent.

A georeference contains three components:

1) A reference to a CollectingEvent (who, where, when, how)
2) A reference to a GeographicItem (a shape)
3) A method by which the shape was associated with the collecting event (via `type` subclassing).

If a georeference was published its Source can be provided. This is not equivalent to providing a method for deriving the georeference.

Contains information about a location on the face of the Earth, consisting of:

st_centroid() of the geographic item.

Direct Known Subclasses

Exif, GPX, GeoLocate, GoogleMap, Leaflet, Point, VerbatimData, Wkt

Defined Under Namespace

Classes: Exif, GPX, GeoLocate, GoogleMap, Leaflet, Point, VerbatimData, Wkt

Constant Summary

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shared::IsData

#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_destroyable?, #is_editable?, #is_in_use?, #is_in_users_projects?, #metamorphosize, #similar

Methods included from Shared::Confidences

#reject_confidences

Methods included from Shared::DataAttributes

#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes

Methods included from Shared::Citations

#cited?, #mark_citations_for_destruction, #nomenclature_date, #origin_citation_source_id, #reject_citations, #requires_citation?, #sources_by_topic_id

Methods included from Shared::ProtocolRelationships

#protocolled?, #reject_protocols

Methods included from Shared::Tags

#reject_tags, #tag_with, #tagged?, #tagged_with?

Methods included from Shared::Notes

#concatenated_notes_string, #reject_notes

Methods included from SoftValidation

#clear_soft_validations, #fix_for, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations, #soft_validators

Methods included from Housekeeping

#has_polymorphic_relationship?

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#api_requestString

The text of the GeoLocation request (::GeoLocate), or the verbatim data (VerbatimData).

Returns:

  • (String)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#collecting_event_idInteger

The id of a CollectingEvent which represents the event of this georeference definition.

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#day_georeferencedInteger?

Returns:

  • (Integer, nil)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#error_depthInteger

The distance in meters of the radius of the area of vertical uncertainty of the accuracy of the location of this georeference definition.

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#error_geographic_item_idInteger

The id of a GeographicItem which represents the (error) representation of this georeference definition.

Generally, it will represent a polygon.

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#error_radiusInteger

the radius of the area of horizontal uncertainty of the accuracy of the location of this georeference definition. Measured in meters. Corresponding error areas are draw from the

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#geographic_item_idInteger

The id of a GeographicItem which represents the (non-error) representation of this georeference definition. Generally, it will represent a point.

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#iframe_responseObject

used to handle the geolocate from Tulane response



90
91
92
# File 'app/models/georeference.rb', line 90

def iframe_response
  @iframe_response
end

#is_median_zBoolean

True if this georeference represents an average vertical distance, otherwise false.

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#is_publicBoolean

True if this georeference can be shared, otherwise false.

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#is_undefined_zBoolean

True if this georeference cannot be located vertically, otherwise false.

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#month_georeferencedInteger?

Returns:

  • (Integer, nil)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#no_cachedBoolean

Returns When true, cascading cached values (e.g. in CollectingEvent) are not built.

Returns:

  • (Boolean)

    When true, cascading cached values (e.g. in CollectingEvent) are not built



130
131
132
# File 'app/models/georeference.rb', line 130

def no_cached
  @no_cached
end

#positionInteger

An arbitrary ordering mechanism, the first georeference is routinely defaulted to in the application.

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#project_idInteger

the project ID

Returns:

  • (Integer)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#typeString

The type name of the this georeference definition.

Returns:

  • (String)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

#year_georeferencedInteger?

Returns 4 digit year the georeference was first created/captured.

Returns:

  • (Integer, nil)

    4 digit year the georeference was first created/captured



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'app/models/georeference.rb', line 75

class Georeference < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::ProtocolRelationships
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Confidences # qualitative, not spatial
  include Shared::Maps
  include Shared::DwcOccurrenceHooks
  include Shared::IsData

  include Shared::Maps

  attr_accessor :iframe_response # used to handle the geolocate from Tulane response

  acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top

  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences
  has_many :otus, through: :collection_objects, source: 'otus'

  has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object
  has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences

  validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year }
  validates :month_georeferenced, date_month: true
  validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced},
    unless: -> { year_georeferenced.nil? || month_georeferenced.nil? }

  validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] }
  validates :geographic_item, presence: true
  validates :type, presence: true # TODO: technically not needed

  validate :add_err_geo_item_inside_err_radius
  validate :add_error_depth
  validate :add_error_geo_item_intersects_area
  validate :add_error_radius
  validate :add_error_radius_inside_area
  validate :add_obj_inside_area
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :geographic_item_present_if_error_radius_provided

  # validate :add_error_geo_item_inside_area

  accepts_nested_attributes_for :geographic_item, :error_geographic_item

  # @return [Boolean]
  #  When true, cascading cached values (e.g. in CollectingEvent) are not built
  attr_accessor :no_cached

  before_validation :round_error_radius
  
  after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) }

  after_destroy :set_cached_collecting_event

  def self.point_type
    joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
  end

  # @param [Array] of parameters in the style of 'params'
  # @return [Scope] of selected georeferences
  def self.filter_by(params)
    collecting_events = CollectingEvent.filter_by(params)

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
    georeferences
  end

  # @param [Integer] geographic_item_id
  # @param [Integer] distance
  # @return [Scope] georeferences
  #   all georeferences within some distance of a geographic_item, by id
  def self.within_radius_of_item(geographic_item_id, distance)
    return where(id: -1) if geographic_item_id.nil? || distance.nil?
    # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
    # => "name='foo''bar' and group_id=4"
    q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
      "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
    # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
    #                                                    GeographicItem::GEOGRAPHY_SQL,
    #                                                    GeographicItem.select_geography_sql(geographic_item_id),
    #                                                    distance])
    Georeference.joins(:geographic_item).where(q1)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  #   all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # includes String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality_like(string)
    with_locality_as(string, true)
  end

  # @param [String] locality string
  # @return [Scope] Georeferences
  # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  # equals String somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  def self.with_locality(string)
    with_locality_as(string, false)
  end

  # @param [GeographicArea]
  # @return [Scope] Georeferences
  # returns all georeferences which have collecting_events which have geographic_areas which match
  # geographic_areas as a GeographicArea
  # TODO: or, (in the future) a string matching a geographic_area.name
  def self.with_geographic_area(geographic_area)
    partials = CollectingEvent.where(geographic_area:)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [ActionController::Parameters] arguments from _collecting_event_selection form
  # @return [Array] of georeferences
  def self.batch_create_from_georeference_matcher(arguments)
    gr = Georeference.find(arguments[:georeference_id].to_param)

    result = []

    collecting_event_list = arguments[:checked_ids]

    unless collecting_event_list.nil?
      collecting_event_list.each do |event_id|
        new_gr = Georeference.new(
          collecting_event_id: event_id.to_i,
          geographic_item_id: gr.geographic_item_id,
          error_radius: gr.error_radius,
          error_depth: gr.error_depth,
          error_geographic_item_id: gr.error_geographic_item_id,
          type: gr.type,
          is_public: gr.is_public,
          api_request: gr.api_request,
          is_undefined_z: gr.is_undefined_z,
          is_median_z: gr.is_median_z)
        if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
          new_gr.save!
          result.push new_gr
        end
      end
    end
    result
  end

  # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
  # Bool = true if 'contains'
  # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
  #   includes, or is equal to 'string' somewhere
  # Joins collecting_event.rb and matches %String% against verbatim_locality
  # .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
  # TODO: Arelize
  def self.with_locality_as(string, like)
    likeness = like ? '%' : ''
    query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

    Georeference.where(collecting_event: CollectingEvent.where(query))
  end

  def dwc_occurrences
    DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
      .where(g: {id:})
      .distinct
  end

  # @return [Hash]
  #   The interface to DwcOccurrence writiing for Georeference based values.
  #   See subclasses for super extensions.
  def dwc_georeference_attributes(h = {})
    georeferenced_by = if georeference_authors.any?
                         georeference_authors.collect{|a| a.cached}.join('|')
                       else
                         creator.name
                       end
    h.merge!(
      footprintWKT: geographic_item.to_wkt,
      georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
      georeferencedBy: georeferenced_by,
      georeferencedDate: created_at,
      georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
    )

    if geographic_item.type == 'GeographicItem::Point'
      b = geographic_item.to_a
      h[:decimalLongitude] = b.first
      h[:decimalLatitude] = b.second
      h[:coordinateUncertaintyInMeters] = error_radius
    end

    h
  end

  # @return [String, nil]
  #   the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'
  def method_name
    return nil if type.blank?
    type.demodulize.underscore
  end

  # @return [GeographicItem, nil]
  #   a square which represents either the bounding box of the
  #   circle represented by the error_radius, or the bounding box of the error_geographic_item
  #   !! We assume the radius calculation is always larger (TODO: do we?  discuss with Jim)
  # TODO: cleanup, subclass, and calculate with SQL?
  def error_box
    retval = nil

    if error_radius.nil?
      retval = error_geographic_item.dup unless error_geographic_item.nil?
    else
      unless geographic_item.nil?
        if geographic_item.geo_object_type
          case geographic_item.geo_object_type
          when :point
            retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
          when :polygon, :multi_polygon
            retval = geographic_item.geo_object
          end
        end
      end
    end
    retval
  end

  # @return [Rgeo::polygon, nil]
  #   a polygon representing the buffer
  def error_radius_buffer_polygon
    return nil if error_radius.nil? || geographic_item.nil?
    sql_str = ActivRecord::Base.send(
      :sanitize_sql_array,
      ['SELECT ST_Buffer(?, ?)',
       geographic_item.geo_object.to_s,
       (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
    value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
    Gis::FACTORY.parse_wkb(value)
  end

  # Called by Gis::GeoJSON.feature_collection
  # @return [Hash] formed as a GeoJSON 'Feature'
  def to_geo_json_feature
    to_simple_json_feature.merge(
      'properties' => {
        'georeference' => {
          'id' => id,
          'tag' => "Georeference ID = #{id}"
        }
      }
    )
  end

  # @return [Float]
  def latitude
    geographic_item.center_coords[0]
  end

  # @return [Float]
  def longitude
    geographic_item.center_coords[1]
  end

  # TODO: parametrize to include gazeteer
  #   i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
  # @return [JSON Feature]
  def to_simple_json_feature
    geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
    {
      'type' => 'Feature',
      'geometry' => geometry,
      'properties' => {}
    }
  end

  # Calculate the radius from error shapes
  # !! Used to retroactively rebuild the radius from the polygon shape
  def radius_from_error_shape
    error_geographic_item&.radius
  end

  protected

  def set_cached_collecting_event
    collecting_event.send(:set_cached)
  end

  def set_cached
    collecting_event.send(:set_cached_geographic_names)
  end

  # validation methods

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_geographic_item
  def check_obj_inside_err_geo_item
    # case 1
    retval = true
    unless geographic_item.nil? || !geographic_item.geo_object
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  # def check_obj_inside_err_radius
  #   # case 2
  #   retval = true
  #   if !error_radius.blank? && geographic_item && geographic_item.geo_object
  #     retval = error_box.contains?(geographic_item.geo_object)
  #   end
  #   retval
  # end

  # @return [Boolean]
  #   true if geographic_item is completely contained in error_box
  def check_obj_inside_err_radius
    # case 2
    retval = true
    # if !error_radius.blank? && geographic_item && geographic_item.geo_object
    if error_radius.present?
      if geographic_item.present?
        if geographic_item.geo_object.present?
          val = error_box
          if val.present?
            retval = val.contains?(geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item is completely contained in error_box
  def check_err_geo_item_inside_err_radius
    # case 3
    retval = true
    unless error_radius.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          retval = error_box.contains?(error_geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_box is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_radius_inside_area
    # case 4
    retval = true
    if collecting_event
      ga_gi = collecting_event.geographic_area_default_geographic_item
      eb = error_box
      if error_radius.present? # rubocop:disable Style/IfUnlessModifier
        retval = ga_gi.contains?(eb) if ga_gi && eb
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object is completely contained in
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_inside_area
    # case 5
    retval = true
    unless collecting_event.nil?
      unless error_geographic_item.nil?
        if error_geographic_item.geo_object # is NOT false
          unless collecting_event.geographic_area.nil?
            retval = collecting_event.geographic_area.default_geographic_item
              .contains?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps)
  # collecting_event.geographic_area.default_geographic_item
  def check_error_geo_item_intersects_area
    # case 5.5
    retval = true
    if collecting_event.present?
      if error_geographic_item.present?
        if error_geographic_item.geo_object.present?
          if collecting_event.geographic_area.present?
            # !! TODO: check fir nil case
            retval = collecting_event.geographic_area.default_geographic_item
              .intersects?(error_geographic_item.geo_object)
          end
        end
      end
    end
    retval
  end

  # @return [Boolean]
  #    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
  # .default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    if collecting_event.present?
      if geographic_item.present? && collecting_event.geographic_area.present?
        if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
          retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
        end
      end
    end
    retval
  end

  # @return [Boolean] true iff collecting_event contains georeference geographic_item.
  def add_obj_inside_area
    unless check_obj_inside_area
      errors.add(
        :geographic_item,
        'for georeference is not contained in the geographic area bound to the collecting event')
      errors.add(
        :collecting_event,
        'is assigned a geographic area which does not contain the supplied georeference/geographic item')
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
  def add_error_geo_item_inside_area
    unless check_error_geo_item_inside_area
      problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
  def add_error_geo_item_intersects_area
    unless check_error_geo_item_intersects_area
      problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
      errors.add(:error_geographic_item, problem)
      errors.add(:collecting_event, problem)
    end
  end

  # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
  def add_error_radius_inside_area
    unless check_error_radius_inside_area
      problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
      errors.add(:error_radius, problem)
      errors.add(:collecting_event, problem) # probably don't need error here
    end
  end

  # @return [Boolean] true iff error_radius contains error_geographic_item.
  def add_err_geo_item_inside_err_radius
    unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
      problem = 'error_radius must contain error_geographic_item.'
      errors.add(:error_radius, problem)
      errors.add(:error_geographic_item, problem)
    end
  end

  # @return [Boolean] true iff error_radius contains geographic_item.
  def add_obj_inside_err_radius
    errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
  end

  # @return [Boolean] true iff error_geographic_item contains geographic_item.
  def add_obj_inside_err_geo_item
    errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
  end

  # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
  def add_error_depth
    errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
      error_depth > 8_800 # 8,800 meters
  end

  # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
  def add_error_radius
    if error_radius.present? && error_radius > 10_000 # 10 km
      errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
    end
  end

  def geographic_item_present_if_error_radius_provided
    if error_radius.present? &&
        geographic_item_id.blank? && # provide existing
        geographic_item.blank? # provide new
      errors.add(:error_radius, 'can only be provided when geographic item is provided')
    end
  end

  # TODO: Should be in lib/utilities/geo.rb.
  # @param [Double] from_lat_
  # @param [Double] from_lon_
  # @param [Double] to_lat_
  # @param [Double] to_lon_
  # @return [Double] Heading is returned as an angle in degrees clockwise from North.
  def heading(from_lat_, from_lon_, to_lat_, to_lon_)
    from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
    to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
    delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
    y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
    x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
      ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
    DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
  end

  private

  def round_error_radius
    if error_radius.present?
      write_attribute(:error_radius, error_radius.round)
    end
  end

end

Class Method Details

.batch_create_from_georeference_matcher(arguments) ⇒ Array

TODO: not yet sure what the params are going to look like. what is below just represents a guess

Parameters:

  • arguments (ActionController::Parameters)

    from _collecting_event_selection form

Returns:

  • (Array)

    of georeferences



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'app/models/georeference.rb', line 202

def self.batch_create_from_georeference_matcher(arguments)
  gr = Georeference.find(arguments[:georeference_id].to_param)

  result = []

  collecting_event_list = arguments[:checked_ids]

  unless collecting_event_list.nil?
    collecting_event_list.each do |event_id|
      new_gr = Georeference.new(
        collecting_event_id: event_id.to_i,
        geographic_item_id: gr.geographic_item_id,
        error_radius: gr.error_radius,
        error_depth: gr.error_depth,
        error_geographic_item_id: gr.error_geographic_item_id,
        type: gr.type,
        is_public: gr.is_public,
        api_request: gr.api_request,
        is_undefined_z: gr.is_undefined_z,
        is_median_z: gr.is_median_z)
      if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
        new_gr.save!
        result.push new_gr
      end
    end
  end
  result
end

.filter_by(params) ⇒ Scope

Returns of selected georeferences.

Parameters:

  • of (Array)

    parameters in the style of ‘params’

Returns:

  • (Scope)

    of selected georeferences



144
145
146
147
148
149
# File 'app/models/georeference.rb', line 144

def self.filter_by(params)
  collecting_events = CollectingEvent.filter_by(params)

  georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
  georeferences
end

.point_typeObject



138
139
140
# File 'app/models/georeference.rb', line 138

def self.point_type
  joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'})
end

.with_geographic_area(geographic_area) ⇒ Scope

returns all georeferences which have collecting_events which have geographic_areas which match geographic_areas as a GeographicArea TODO: or, (in the future) a string matching a geographic_area.name

Parameters:

Returns:

  • (Scope)

    Georeferences



193
194
195
196
197
# File 'app/models/georeference.rb', line 193

def self.with_geographic_area(geographic_area)
  partials = CollectingEvent.where(geographic_area:)
  partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
  partial_gr
end

.with_locality(string) ⇒ Scope

return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which equals String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”)

Parameters:

  • locality (String)

    string

Returns:

  • (Scope)

    Georeferences



184
185
186
# File 'app/models/georeference.rb', line 184

def self.with_locality(string)
  with_locality_as(string, false)
end

.with_locality_as(string, like) ⇒ Scope

Bool = true if ‘contains’ Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”) TODO: Arelize

Parameters:

  • String (String, Boolean)

    to find in collecting_event.verbatim_locality, Bool = false for ‘Starts with’,

Returns:

  • (Scope)

    Georeferences which are attached to a CollectingEvent which has a verbatim_locality which includes, or is equal to ‘string’ somewhere



238
239
240
241
242
243
# File 'app/models/georeference.rb', line 238

def self.with_locality_as(string, like)
  likeness = like ? '%' : ''
  query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"

  Georeference.where(collecting_event: CollectingEvent.where(query))
end

.with_locality_like(string) ⇒ Scope

includes String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”)

Parameters:

  • locality (String)

    string

Returns:

  • (Scope)

    Georeferences all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which



174
175
176
# File 'app/models/georeference.rb', line 174

def self.with_locality_like(string)
  with_locality_as(string, true)
end

.within_radius_of_item(geographic_item_id, distance) ⇒ Scope

Returns georeferences all georeferences within some distance of a geographic_item, by id.

Parameters:

  • geographic_item_id (Integer)
  • distance (Integer)

Returns:

  • (Scope)

    georeferences all georeferences within some distance of a geographic_item, by id



155
156
157
158
159
160
161
162
163
164
165
166
# File 'app/models/georeference.rb', line 155

def self.within_radius_of_item(geographic_item_id, distance)
  return where(id: -1) if geographic_item_id.nil? || distance.nil?
  # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
  # => "name='foo''bar' and group_id=4"
  q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
    "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
  # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
  #                                                    GeographicItem::GEOGRAPHY_SQL,
  #                                                    GeographicItem.select_geography_sql(geographic_item_id),
  #                                                    distance])
  Georeference.joins(:geographic_item).where(q1)
end

Instance Method Details

#add_err_geo_item_inside_err_radiusBoolean (protected)

Returns true iff error_radius contains error_geographic_item.

Returns:

  • (Boolean)

    true iff error_radius contains error_geographic_item.



545
546
547
548
549
550
551
# File 'app/models/georeference.rb', line 545

def add_err_geo_item_inside_err_radius
  unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
    problem = 'error_radius must contain error_geographic_item.'
    errors.add(:error_radius, problem)
    errors.add(:error_geographic_item, problem)
  end
end

#add_error_depthBoolean (protected)

Returns true iff error_depth is less than 8.8 kilometers (5.5 miles).

Returns:

  • (Boolean)

    true iff error_depth is less than 8.8 kilometers (5.5 miles).



564
565
566
567
# File 'app/models/georeference.rb', line 564

def add_error_depth
  errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
    error_depth > 8_800 # 8,800 meters
end

#add_error_geo_item_inside_areaBoolean (protected)

Returns true iff collecting_event area contains georeference error_geographic_item.

Returns:

  • (Boolean)

    true iff collecting_event area contains georeference error_geographic_item.



518
519
520
521
522
523
524
# File 'app/models/georeference.rb', line 518

def add_error_geo_item_inside_area
  unless check_error_geo_item_inside_area
    problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
    errors.add(:error_geographic_item, problem)
    errors.add(:collecting_event, problem)
  end
end

#add_error_geo_item_intersects_areaBoolean (protected)

Returns true iff collecting_event area intersects georeference error_geographic_item.

Returns:

  • (Boolean)

    true iff collecting_event area intersects georeference error_geographic_item.



527
528
529
530
531
532
533
# File 'app/models/georeference.rb', line 527

def add_error_geo_item_intersects_area
  unless check_error_geo_item_intersects_area
    problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
    errors.add(:error_geographic_item, problem)
    errors.add(:collecting_event, problem)
  end
end

#add_error_radiusBoolean (protected)

Returns true iff error_radius is less than 10 kilometers (6.6 miles).

Returns:

  • (Boolean)

    true iff error_radius is less than 10 kilometers (6.6 miles).



570
571
572
573
574
# File 'app/models/georeference.rb', line 570

def add_error_radius
  if error_radius.present? && error_radius > 10_000 # 10 km
    errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).')
  end
end

#add_error_radius_inside_areaBoolean (protected)

Returns true iff collecting_event area contains georeference error_radius bounding box.

Returns:

  • (Boolean)

    true iff collecting_event area contains georeference error_radius bounding box.



536
537
538
539
540
541
542
# File 'app/models/georeference.rb', line 536

def add_error_radius_inside_area
  unless check_error_radius_inside_area
    problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
    errors.add(:error_radius, problem)
    errors.add(:collecting_event, problem) # probably don't need error here
  end
end

#add_obj_inside_areaBoolean (protected)

Returns true iff collecting_event contains georeference geographic_item.

Returns:

  • (Boolean)

    true iff collecting_event contains georeference geographic_item.



506
507
508
509
510
511
512
513
514
515
# File 'app/models/georeference.rb', line 506

def add_obj_inside_area
  unless check_obj_inside_area
    errors.add(
      :geographic_item,
      'for georeference is not contained in the geographic area bound to the collecting event')
    errors.add(
      :collecting_event,
      'is assigned a geographic area which does not contain the supplied georeference/geographic item')
  end
end

#add_obj_inside_err_geo_itemBoolean (protected)

Returns true iff error_geographic_item contains geographic_item.

Returns:

  • (Boolean)

    true iff error_geographic_item contains geographic_item.



559
560
561
# File 'app/models/georeference.rb', line 559

def add_obj_inside_err_geo_item
  errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
end

#add_obj_inside_err_radiusBoolean (protected)

Returns true iff error_radius contains geographic_item.

Returns:

  • (Boolean)

    true iff error_radius contains geographic_item.



554
555
556
# File 'app/models/georeference.rb', line 554

def add_obj_inside_err_radius
  errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
end

#check_err_geo_item_inside_err_radiusBoolean (protected)

Returns true if error_geographic_item is completely contained in error_box.

Returns:

  • (Boolean)

    true if error_geographic_item is completely contained in error_box



424
425
426
427
428
429
430
431
432
433
434
435
# File 'app/models/georeference.rb', line 424

def check_err_geo_item_inside_err_radius
  # case 3
  retval = true
  unless error_radius.nil?
    unless error_geographic_item.nil?
      if error_geographic_item.geo_object # is NOT false
        retval = error_box.contains?(error_geographic_item.geo_object)
      end
    end
  end
  retval
end

#check_error_geo_item_inside_areaBoolean (protected)

collecting_event.geographic_area.default_geographic_item

Returns:

  • (Boolean)

    true if error_geographic_item.geo_object is completely contained in



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'app/models/georeference.rb', line 454

def check_error_geo_item_inside_area
  # case 5
  retval = true
  unless collecting_event.nil?
    unless error_geographic_item.nil?
      if error_geographic_item.geo_object # is NOT false
        unless collecting_event.geographic_area.nil?
          retval = collecting_event.geographic_area.default_geographic_item
            .contains?(error_geographic_item.geo_object)
        end
      end
    end
  end
  retval
end

#check_error_geo_item_intersects_areaBoolean (protected)

collecting_event.geographic_area.default_geographic_item

Returns:

  • (Boolean)

    true if error_geographic_item.geo_object intersects (overlaps)



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'app/models/georeference.rb', line 472

def check_error_geo_item_intersects_area
  # case 5.5
  retval = true
  if collecting_event.present?
    if error_geographic_item.present?
      if error_geographic_item.geo_object.present?
        if collecting_event.geographic_area.present?
          # !! TODO: check fir nil case
          retval = collecting_event.geographic_area.default_geographic_item
            .intersects?(error_geographic_item.geo_object)
        end
      end
    end
  end
  retval
end

#check_error_radius_inside_areaBoolean (protected)

collecting_event.geographic_area.default_geographic_item

Returns:

  • (Boolean)

    true if error_box is completely contained in



439
440
441
442
443
444
445
446
447
448
449
450
# File 'app/models/georeference.rb', line 439

def check_error_radius_inside_area
  # case 4
  retval = true
  if collecting_event
    ga_gi = collecting_event.geographic_area_default_geographic_item
    eb = error_box
    if error_radius.present? # rubocop:disable Style/IfUnlessModifier
      retval = ga_gi.contains?(eb) if ga_gi && eb
    end
  end
  retval
end

#check_obj_inside_areaBoolean (protected)

.default_geographic_item

Returns:

  • (Boolean)

    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area



492
493
494
495
496
497
498
499
500
501
502
503
# File 'app/models/georeference.rb', line 492

def check_obj_inside_area
  # case 6
  retval = true
  if collecting_event.present?
    if geographic_item.present? && collecting_event.geographic_area.present?
      if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      end
    end
  end
  retval
end

#check_obj_inside_err_geo_itemBoolean (protected)

Returns true if geographic_item is completely contained in error_geographic_item.

Returns:

  • (Boolean)

    true if geographic_item is completely contained in error_geographic_item



380
381
382
383
384
385
386
387
388
389
390
391
# File 'app/models/georeference.rb', line 380

def check_obj_inside_err_geo_item
  # case 1
  retval = true
  unless geographic_item.nil? || !geographic_item.geo_object
    unless error_geographic_item.nil?
      if error_geographic_item.geo_object # is NOT false
        retval = error_geographic_item.contains?(geographic_item.geo_object)
      end
    end
  end
  retval
end

#check_obj_inside_err_radiusBoolean (protected)

Returns true if geographic_item is completely contained in error_box.

Returns:

  • (Boolean)

    true if geographic_item is completely contained in error_box



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'app/models/georeference.rb', line 406

def check_obj_inside_err_radius
  # case 2
  retval = true
  # if !error_radius.blank? && geographic_item && geographic_item.geo_object
  if error_radius.present?
    if geographic_item.present?
      if geographic_item.geo_object.present?
        val = error_box
        if val.present?
          retval = val.contains?(geographic_item.geo_object)
        end
      end
    end
  end
  retval
end

#dwc_georeference_attributes(h = {}) ⇒ Hash

Returns The interface to DwcOccurrence writiing for Georeference based values. See subclasses for super extensions.

Returns:

  • (Hash)

    The interface to DwcOccurrence writiing for Georeference based values. See subclasses for super extensions.



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'app/models/georeference.rb', line 256

def dwc_georeference_attributes(h = {})
  georeferenced_by = if georeference_authors.any?
                       georeference_authors.collect{|a| a.cached}.join('|')
                     else
                       creator.name
                     end
  h.merge!(
    footprintWKT: geographic_item.to_wkt,
    georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence,
    georeferencedBy: georeferenced_by,
    georeferencedDate: created_at,
    georeferenceProtocol: protocols.collect{|p| p.name}.join('|')
  )

  if geographic_item.type == 'GeographicItem::Point'
    b = geographic_item.to_a
    h[:decimalLongitude] = b.first
    h[:decimalLatitude] = b.second
    h[:coordinateUncertaintyInMeters] = error_radius
  end

  h
end

#dwc_occurrencesObject



245
246
247
248
249
250
251
# File 'app/models/georeference.rb', line 245

def dwc_occurrences
  DwcOccurrence
    .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
    .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id')
    .where(g: {id:})
    .distinct
end

#error_boxGeographicItem?

TODO: cleanup, subclass, and calculate with SQL?

Returns:

  • (GeographicItem, nil)

    a square which represents either the bounding box of the circle represented by the error_radius, or the bounding box of the error_geographic_item !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim)



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'app/models/georeference.rb', line 292

def error_box
  retval = nil

  if error_radius.nil?
    retval = error_geographic_item.dup unless error_geographic_item.nil?
  else
    unless geographic_item.nil?
      if geographic_item.geo_object_type
        case geographic_item.geo_object_type
        when :point
          retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
        when :polygon, :multi_polygon
          retval = geographic_item.geo_object
        end
      end
    end
  end
  retval
end

#error_radius_buffer_polygonRgeo::polygon?

Returns a polygon representing the buffer.

Returns:

  • (Rgeo::polygon, nil)

    a polygon representing the buffer



314
315
316
317
318
319
320
321
322
323
# File 'app/models/georeference.rb', line 314

def error_radius_buffer_polygon
  return nil if error_radius.nil? || geographic_item.nil?
  sql_str = ActivRecord::Base.send(
    :sanitize_sql_array,
    ['SELECT ST_Buffer(?, ?)',
     geographic_item.geo_object.to_s,
     (error_radius / Utilities::Geo::ONE_WEST_MEAN)])
  value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
  Gis::FACTORY.parse_wkb(value)
end

#geographic_item_present_if_error_radius_providedObject (protected)



576
577
578
579
580
581
582
# File 'app/models/georeference.rb', line 576

def geographic_item_present_if_error_radius_provided
  if error_radius.present? &&
      geographic_item_id.blank? && # provide existing
      geographic_item.blank? # provide new
    errors.add(:error_radius, 'can only be provided when geographic item is provided')
  end
end

#heading(from_lat_, from_lon_, to_lat_, to_lon_) ⇒ Double (protected)

TODO: Should be in lib/utilities/geo.rb.

Parameters:

  • from_lat_ (Double)
  • from_lon_ (Double)
  • to_lat_ (Double)
  • to_lon_ (Double)

Returns:

  • (Double)

    Heading is returned as an angle in degrees clockwise from North.



590
591
592
593
594
595
596
597
598
# File 'app/models/georeference.rb', line 590

def heading(from_lat_, from_lon_, to_lat_, to_lon_)
  from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
  to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
  delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
  y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
  x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
    ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
  DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
end

#latitudeFloat

Returns:

  • (Float)


339
340
341
# File 'app/models/georeference.rb', line 339

def latitude
  geographic_item.center_coords[0]
end

#longitudeFloat

Returns:

  • (Float)


344
345
346
# File 'app/models/georeference.rb', line 344

def longitude
  geographic_item.center_coords[1]
end

#method_nameString?

Returns the underscored version of the type, e.g. Georeference::GoogleMap => ‘google_map’.

Returns:

  • (String, nil)

    the underscored version of the type, e.g. Georeference::GoogleMap => ‘google_map’



282
283
284
285
# File 'app/models/georeference.rb', line 282

def method_name
  return nil if type.blank?
  type.demodulize.underscore
end

#radius_from_error_shapeObject

Calculate the radius from error shapes !! Used to retroactively rebuild the radius from the polygon shape



362
363
364
# File 'app/models/georeference.rb', line 362

def radius_from_error_shape
  error_geographic_item&.radius
end

#round_error_radiusObject (private)



602
603
604
605
606
# File 'app/models/georeference.rb', line 602

def round_error_radius
  if error_radius.present?
    write_attribute(:error_radius, error_radius.round)
  end
end

#set_cachedObject (protected)



372
373
374
# File 'app/models/georeference.rb', line 372

def set_cached
  collecting_event.send(:set_cached_geographic_names)
end

#set_cached_collecting_eventObject (protected)



368
369
370
# File 'app/models/georeference.rb', line 368

def set_cached_collecting_event
  collecting_event.send(:set_cached)
end

#to_geo_json_featureHash

Called by Gis::GeoJSON.feature_collection

Returns:

  • (Hash)

    formed as a GeoJSON ‘Feature’



327
328
329
330
331
332
333
334
335
336
# File 'app/models/georeference.rb', line 327

def to_geo_json_feature
  to_simple_json_feature.merge(
    'properties' => {
      'georeference' => {
        'id' => id,
        'tag' => "Georeference ID = #{id}"
      }
    }
  )
end

#to_simple_json_featureJSON Feature

TODO: parametrize to include gazeteer

i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')

Returns:

  • (JSON Feature)


351
352
353
354
355
356
357
358
# File 'app/models/georeference.rb', line 351

def to_simple_json_feature
  geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
  {
    'type' => 'Feature',
    'geometry' => geometry,
    'properties' => {}
  }
end