Class: Georeference

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Housekeeping, Shared::Citable, Shared::IsData, Shared::Taggable
Defined in:
app/models/georeference.rb

Overview

rubocop:disable Metrics/AbcSize, MethodLength, CyclomaticComplexity A georeference is an assertion that some shape, as derived from some method, describes the location of some collecting event.

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 it's Source can be provided. This is not equivalent to a method.

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

Direct Known Subclasses

GeoLocate, GoogleMap, VerbatimData

Defined Under Namespace

Classes: GeoLocate, GoogleMap, VerbatimData

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Methods included from Housekeeping

#has_polymorphic_relationship?

Instance Attribute Details

- (String) api_request

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

Returns:

  • (String)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Integer) collecting_event_id

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

Returns:

  • (Integer)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Integer) error_depth

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)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Integer) error_geographic_item_id

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

Generally, it will represent a polygon.

Returns:

  • (Integer)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Integer) error_radius

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 st_centroid() of the geographic item.

Returns:

  • (Integer)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Integer) geographic_item_id

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

Returns:

  • (Integer)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Object) iframe_response

used to pass the geolocate from Tulane through



70
71
72
# File 'app/models/georeference.rb', line 70

def iframe_response
  @iframe_response
end

- (Boolean) is_median_z

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

Returns:

  • (Boolean)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Boolean) is_public

True if this georeference can be shared, otherwise false.

Returns:

  • (Boolean)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Boolean) is_undefined_z

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

Returns:

  • (Boolean)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Boolean) no_cached

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

Returns:

  • (Boolean)

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



102
103
104
# File 'app/models/georeference.rb', line 102

def no_cached
  @no_cached
end

- (Integer) position

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

Returns:

  • (Integer)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (Integer) project_id

the project ID

Returns:

  • (Integer)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

- (String) type

The type name of the this georeference definition.

Returns:

  • (String)


64
65
66
67
68
69
70
71
72
73
74
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
# File 'app/models/georeference.rb', line 64

class Georeference < ActiveRecord::Base
  include Housekeeping
  include Shared::Taggable
  include Shared::IsData
  include Shared::Citable

  attr_accessor :iframe_response # used to pass the geolocate from Tulane through

  acts_as_list scope: [:collecting_event_id]

  belongs_to :error_geographic_item, class_name: 'GeographicItem', foreign_key: :error_geographic_item_id
  belongs_to :collecting_event, inverse_of: :georeferences
  belongs_to :geographic_item, inverse_of: :georeferences

  has_many :collection_objects, through: :collecting_event

  validates :geographic_item, presence: true
  validates :type, presence: true
  # validates :collecting_event, presence: true
  validates :collecting_event_id, uniqueness: {scope: [:type, :geographic_item_id, :project_id]}
  # validates_uniqueness_of :collecting_event_id, scope: [:type, :geographic_item_id, :project_id]

  # validate :proper_data_is_provided
  validate :add_error_radius
  validate :add_error_depth
  validate :add_obj_inside_err_geo_item
  validate :add_obj_inside_err_radius
  validate :add_err_geo_item_inside_err_radius
  validate :add_error_radius_inside_area
  validate :add_error_geo_item_inside_area
  validate :add_obj_inside_area

  validate :geographic_item_present_if_error_radius_provided

  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

  after_save :set_cached, if: '!self.no_cached'

  # instance methods

  # @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]
  #   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?
    value = GeographicItem.connection.select_all(
      "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").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

  def latitude
    geographic_item.center_coords[1]
  end

  def longitude
    geographic_item.center_coords[0]
  end

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

  # class methods

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

    georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
    georeferences
  end

  # @param [geographic_item.id, double]
  # @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?
    Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
  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: geographic_area)
    partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
    partial_gr
  end

  def self.generate_download(scope)
    CSV.generate do |csv|
      csv << column_names
      scope.order(id: :asc).each do |o|
        csv << o.attributes.values_at(*column_names).collect {|i|
          i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
        }
      end
    end
  end

  # TODO: not yet sure what the params are going to look like. what is below just represents a guess
  # @param [Hash] arguments from _collecting_event_selection form
  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

  protected

  def set_cached
    collecting_event.cache_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
    unless error_radius.blank?
      unless geographic_item.blank?
        unless geographic_item.geo_object.blank?
          val = error_box
          unless val.blank?
            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
      unless error_radius.blank? # 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 geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item
  def check_obj_inside_area
    # case 6
    retval = true
    unless collecting_event.nil?
      unless collecting_event.geographic_area.nil? ||
        !geographic_item.geo_object ||
        collecting_event.geographic_area.default_geographic_item.nil?
        retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
      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
  #     eb = self.error_box
  #     gi = collecting_event.default_area_geographic_item
  #     if eb && gi
  #       retval = gi.contains?(eb)
  #     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 to a geographic area outside 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 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 20,000 kilometers (12,400 miles).
  def add_error_radius
    errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
      error_radius > 20_000_000 # 20,000 km
  end

  def geographic_item_present_if_error_radius_provided
    if !error_radius.blank? &&
      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

  # @param [Double, Double, Double, Double] two latitude/longitude pairs in decimal degrees
  #   find the heading between them.
  # @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
end

Class Method Details

+ (Object) batch_create_from_georeference_matcher(arguments)

TODO: not yet sure what the params are going to look like. what is below just represents a guess

Parameters:

  • arguments (Hash)

    from _collecting_event_selection form



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
# File 'app/models/georeference.rb', line 244

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

+ (Scope) filter(params)

Returns of selected georeferences

Parameters:

  • of (Array)

    parameters in the style of 'params'

Returns:

  • (Scope)

    of selected georeferences



185
186
187
188
189
190
# File 'app/models/georeference.rb', line 185

def self.filter(params)
  collecting_events = CollectingEvent.filter(params)

  georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.pluck(:id))
  georeferences
end

+ (Object) generate_download(scope)



231
232
233
234
235
236
237
238
239
240
# File 'app/models/georeference.rb', line 231

def self.generate_download(scope)
  CSV.generate do |csv|
    csv << column_names
    scope.order(id: :asc).each do |o|
      csv << o.attributes.values_at(*column_names).collect {|i|
        i.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
      }
    end
  end
end

+ (Scope) with_geographic_area(geographic_area)

returns all georeferences which have collecting_events which have geographic_areas which match geographic_areas as a GeographicArea TODO: or, (in the future) a string matching a geographic_area.name

Parameters:

Returns:

  • (Scope)

    Georeferences



225
226
227
228
229
# File 'app/models/georeference.rb', line 225

def self.with_geographic_area(geographic_area)
  partials   = CollectingEvent.where(geographic_area: geographic_area)
  partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
  partial_gr
end

+ (Scope) with_locality(string)

return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which equals String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”)

Parameters:

  • locality (String)

    string

Returns:

  • (Scope)

    Georeferences



216
217
218
# File 'app/models/georeference.rb', line 216

def self.with_locality(string)
  with_locality_as(string, false)
end

+ (Scope) with_locality_as(string, like)

Bool = true if 'contains' Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”) TODO: Arelize

Parameters:

  • String (String, Boolean)

    to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',

Returns:

  • (Scope)

    Georeferences which are attached to a CollectingEvent which has a verbatim_locality which includes, or is equal to 'string' somewhere



279
280
281
282
283
284
# File 'app/models/georeference.rb', line 279

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

+ (Scope) with_locality_like(string)

includes String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”)

Parameters:

  • locality (String)

    string

Returns:

  • (Scope)

    Georeferences all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which



206
207
208
# File 'app/models/georeference.rb', line 206

def self.with_locality_like(string)
  with_locality_as(string, true)
end

+ (scope) within_radius_of_item(geographic_item_id, distance)

Returns georeferences all georeferences within some distance of a geographic_item, by id

Parameters:

  • (geographic_item.id, double)

Returns:

  • (scope)

    georeferences all georeferences within some distance of a geographic_item, by id



195
196
197
198
# File 'app/models/georeference.rb', line 195

def self.within_radius_of_item(geographic_item_id, distance)
  return where(id: -1) if geographic_item_id.nil? || distance.nil?
  Georeference.joins(:geographic_item).where("st_distance(#{GeographicItem::GEOGRAPHY_SQL}, (#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}")
end

Instance Method Details

- (Boolean) add_err_geo_item_inside_err_radius (protected)

Returns true iff error_radius contains error_geographic_item.

Returns:

  • (Boolean)

    true iff error_radius contains error_geographic_item.



430
431
432
433
434
435
436
# File 'app/models/georeference.rb', line 430

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

- (Boolean) add_error_depth (protected)

Returns true iff error_depth is less than 8.8 kilometers (5.5 miles).

Returns:

  • (Boolean)

    true iff error_depth is less than 8.8 kilometers (5.5 miles).



449
450
451
452
# File 'app/models/georeference.rb', line 449

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

- (Boolean) add_error_geo_item_inside_area (protected)

Returns true iff collecting_event area contains georeference error_geographic_item.

Returns:

  • (Boolean)

    true iff collecting_event area contains georeference error_geographic_item.



412
413
414
415
416
417
418
# File 'app/models/georeference.rb', line 412

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

- (Boolean) add_error_radius (protected)

Returns true iff error_radius is less than 20,000 kilometers (12,400 miles).

Returns:

  • (Boolean)

    true iff error_radius is less than 20,000 kilometers (12,400 miles).



455
456
457
458
# File 'app/models/georeference.rb', line 455

def add_error_radius
  errors.add(:error_radius, ' must be less than 20,000 kilometers (12,400 miles).') if error_radius &&
    error_radius > 20_000_000 # 20,000 km
end

- (Boolean) add_error_radius_inside_area (protected)

Returns true iff collecting_event area contains georeference error_radius bounding box.

Returns:

  • (Boolean)

    true iff collecting_event area contains georeference error_radius bounding box.



421
422
423
424
425
426
427
# File 'app/models/georeference.rb', line 421

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

- (Boolean) add_obj_inside_area (protected)

Returns true iff collecting_event contains georeference geographic_item.

Returns:

  • (Boolean)

    true iff collecting_event contains georeference geographic_item.



404
405
406
407
408
409
# File 'app/models/georeference.rb', line 404

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 to a geographic area outside the supplied georeference/geographic item')
  end
end

- (Boolean) add_obj_inside_err_geo_item (protected)

Returns true iff error_geographic_item contains geographic_item.

Returns:

  • (Boolean)

    true iff error_geographic_item contains geographic_item.



444
445
446
# File 'app/models/georeference.rb', line 444

def add_obj_inside_err_geo_item
  errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
end

- (Boolean) add_obj_inside_err_radius (protected)

Returns true iff error_radius contains geographic_item.

Returns:

  • (Boolean)

    true iff error_radius contains geographic_item.



439
440
441
# File 'app/models/georeference.rb', line 439

def add_obj_inside_err_radius
  errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
end

- (Boolean) check_err_geo_item_inside_err_radius (protected)

Returns true if error_geographic_item is completely contained in error_box

Returns:

  • (Boolean)

    true if error_geographic_item is completely contained in error_box



329
330
331
332
333
334
335
336
337
338
339
340
# File 'app/models/georeference.rb', line 329

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

- (Boolean) check_error_geo_item_inside_area (protected)

collecting_event.geographic_area.default_geographic_item

Returns:

  • (Boolean)

    true if error_geographic_item.geo_object is completely contained in



359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'app/models/georeference.rb', line 359

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

- (Boolean) check_error_radius_inside_area (protected)

collecting_event.geographic_area.default_geographic_item

Returns:

  • (Boolean)

    true if error_box is completely contained in



344
345
346
347
348
349
350
351
352
353
354
355
# File 'app/models/georeference.rb', line 344

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
    unless error_radius.blank? # rubocop:disable Style/IfUnlessModifier
      retval = ga_gi.contains?(eb) if ga_gi && eb
    end
  end
  retval
end

- (Boolean) check_obj_inside_area (protected)

Returns true if geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item

Returns:

  • (Boolean)

    true if geographic_item.geo_object is completely contained in collecting_event.geographic_area.default_geographic_item



375
376
377
378
379
380
381
382
383
384
385
386
# File 'app/models/georeference.rb', line 375

def check_obj_inside_area
  # case 6
  retval = true
  unless collecting_event.nil?
    unless collecting_event.geographic_area.nil? ||
      !geographic_item.geo_object ||
      collecting_event.geographic_area.default_geographic_item.nil?
      retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
    end
  end
  retval
end

- (Boolean) check_obj_inside_err_geo_item (protected)

Returns true if geographic_item is completely contained in error_geographic_item

Returns:

  • (Boolean)

    true if geographic_item is completely contained in error_geographic_item



296
297
298
299
300
301
302
303
304
305
306
307
# File 'app/models/georeference.rb', line 296

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

- (Boolean) check_obj_inside_err_radius (protected)

Returns true if geographic_item is completely contained in error_box

Returns:

  • (Boolean)

    true if geographic_item is completely contained in error_box



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/georeference.rb', line 311

def check_obj_inside_err_radius
  # case 2
  retval = true
  # if !error_radius.blank? && geographic_item && geographic_item.geo_object
  unless error_radius.blank?
    unless geographic_item.blank?
      unless geographic_item.geo_object.blank?
        val = error_box
        unless val.blank?
          retval = val.contains?(geographic_item.geo_object)
        end
      end
    end
  end
  retval
end

- (GeographicItem) error_box

TODO: cleanup, subclass, and calculate with SQL?

Returns:

  • (GeographicItem)

    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)



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/models/georeference.rb', line 120

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

- (Rgeo::polygon?) error_radius_buffer_polygon

Returns a polygon representing the buffer

Returns:

  • (Rgeo::polygon, nil)

    a polygon representing the buffer



142
143
144
145
146
147
# File 'app/models/georeference.rb', line 142

def error_radius_buffer_polygon
  return nil if error_radius.nil? || geographic_item.nil?
  value = GeographicItem.connection.select_all(
    "SELECT ST_BUFFER('#{geographic_item.geo_object}', #{error_radius / 111_319.444444444});").first['st_buffer']
  Gis::FACTORY.parse_wkb(value)
end

- (Object) geographic_item_present_if_error_radius_provided (protected)



460
461
462
463
464
465
466
# File 'app/models/georeference.rb', line 460

def geographic_item_present_if_error_radius_provided
  if !error_radius.blank? &&
    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

- (Double) heading(from_lat_, from_lon_, to_lat_, to_lon_) (protected)

Returns Heading is returned as an angle in degrees clockwise from North.

Parameters:

  • two (Double, Double, Double, Double)

    latitude/longitude pairs in decimal degrees find the heading between them.

Returns:

  • (Double)

    Heading is returned as an angle in degrees clockwise from North.



471
472
473
474
475
476
477
478
479
# File 'app/models/georeference.rb', line 471

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

- (Object) latitude



162
163
164
# File 'app/models/georeference.rb', line 162

def latitude
  geographic_item.center_coords[1]
end

- (Object) longitude



166
167
168
# File 'app/models/georeference.rb', line 166

def longitude
  geographic_item.center_coords[0]
end

- (String?) method_name

Returns the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'

Returns:

  • (String, nil)

    the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map'



110
111
112
113
# File 'app/models/georeference.rb', line 110

def method_name
  return nil if type.blank?
  type.demodulize.underscore
end

- (Object) set_cached (protected)



288
289
290
# File 'app/models/georeference.rb', line 288

def set_cached
  collecting_event.cache_geographic_names
end

- (Hash) to_geo_json_feature

Called by Gis::GeoJSON.feature_collection

Returns:

  • (Hash)

    formed as a GeoJSON 'Feature'



151
152
153
154
155
156
157
158
159
160
# File 'app/models/georeference.rb', line 151

def to_geo_json_feature
  to_simple_json_feature.merge(
    'properties' => {
      'georeference' => {
        'id'  => id,
        'tag' => "Georeference ID = #{id}"
      }
    }
  )
end

- (Object) to_simple_json_feature

TODO: parametrize to include gazeteer

i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')


172
173
174
175
176
177
178
179
# File 'app/models/georeference.rb', line 172

def to_simple_json_feature
  geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
  {
    'type'       => 'Feature',
    'geometry'   => geometry,
    'properties' => {}
  }
end