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<