/
georeference.rb
544 lines (487 loc) · 19.9 KB
/
georeference.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# 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 its Source can be provided. This is not equivalent to providing a method for deriving the georeference.
#
# Contains information about a location on the face of the Earth, consisting of:
#
# @!attribute geographic_item_id
# @return [Integer]
# The id of a GeographicItem which represents the (non-error) representation of this georeference definition.
# Generally, it will represent a point.
#
# @!attribute collecting_event_id
# @return [Integer]
# The id of a CollectingEvent which represents the event of this georeference definition.
#
# @!attribute error_radius
# @return [Integer]
# 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.
#
# @!attribute error_depth
# @return [Integer]
# The distance in meters of the radius of the area of vertical uncertainty of the accuracy of the location of
# this georeference definition.
#
# @!attribute error_geographic_item_id
# @return [Integer]
# The id of a GeographicItem which represents the (error) representation of this georeference definition.
# Generally, it will represent a polygon.
#
# @!attribute type
# @return [String]
# The type name of the this georeference definition.
#
# @!attribute position
# @return [Integer]
# An arbitrary ordering mechanism, the first georeference is routinely defaulted to in the application.
#
# @!attribute is_public
# @return [Boolean]
# True if this georeference can be shared, otherwise false.
#
# @!attribute api_request
# @return [String]
# The text of the GeoLocation request (::GeoLocate), or the verbatim data (VerbatimData).
#
# @!attribute project_id
# @return [Integer]
# the project ID
#
# @!attribute is_undefined_z
# @return [Boolean]
# True if this georeference cannot be located vertically, otherwise false.
#
# @!attribute is_median_z
# @return [Boolean]
# True if this georeference represents an average vertical distance, otherwise false.
#
class Georeference < ApplicationRecord
include Housekeeping
include Shared::Notes
include Shared::Tags
include Shared::IsData
include Shared::Citations
include Shared::HasRoles
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
has_many :georeferencer_roles, -> { order('roles.position ASC') },
class_name: 'Georeferencer',
as: :role_object, validate: true
has_many :georeferencers, -> { order('roles.position ASC') },
through: :georeferencer_roles,
source: :person, validate: true
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_intersects_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, unless: -> { 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, nil]
# a square which represents either the bounding box of the
# circle represented by the error_radius, or the bounding box of the error_geographic_item
# !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim)
# TODO: cleanup, subclass, and calculate with SQL?
def error_box
retval = nil
if error_radius.nil?
retval = error_geographic_item.dup unless error_geographic_item.nil?
else
unless geographic_item.nil?
if geographic_item.geo_object_type
case geographic_item.geo_object_type
when :point
retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius)
when :polygon, :multi_polygon
retval = geographic_item.geo_object
end
end
end
end
retval
end
# @return [Rgeo::polygon, nil]
# a polygon representing the buffer
def error_radius_buffer_polygon
return nil if error_radius.nil? || geographic_item.nil?
sql_str = ActivRecord::Base.send(:sanitize_sql_array, ['SELECT ST_Buffer(?, ?)',
geographic_item.geo_object.to_s,
(error_radius / 111_319.444444444)])
value = GeographicItem.connection.select_all(sql_str).first['st_buffer']
Gis::FACTORY.parse_wkb(value)
end
# rubocop:disable Style/StringHashKeys
# Called by Gis::GeoJSON.feature_collection
# @return [Hash] formed as a GeoJSON 'Feature'
def to_geo_json_feature
to_simple_json_feature.merge(
'properties' => {
'georeference' => {
'id' => id,
'tag' => "Georeference ID = #{id}"
}
}
)
end
# @return [Float]
def latitude
geographic_item.center_coords[0]
end
# @return [Float]
def longitude
geographic_item.center_coords[1]
end
# TODO: parametrize to include gazeteer
# i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
# @return [JSON Feature]
def to_simple_json_feature
geometry = RGeo::GeoJSON.encode(geographic_item.geo_object)
{
'type' => 'Feature',
'geometry' => geometry,
'properties' => {}
}
end
# rubocop:enable Style/StringHashKeys
# class methods
# @param [Array] of parameters in the style of 'params'
# @return [Scope] of selected georeferences
def self.filter_by(params)
collecting_events = CollectingEvent.filter_by(params)
georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids)
georeferences
end
# @param [Integer] geographic_item_id
# @param [Integer] distance
# @return [Scope] georeferences
# all georeferences within some distance of a geographic_item, by id
def self.within_radius_of_item(geographic_item_id, distance)
return where(id: -1) if geographic_item_id.nil? || distance.nil?
# sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
# => "name='foo''bar' and group_id=4"
q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \
"(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}"
# q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?',
# GeographicItem::GEOGRAPHY_SQL,
# GeographicItem.select_geography_sql(geographic_item_id),
# distance])
Georeference.joins(:geographic_item).where(q1)
end
# @param [String] locality string
# @return [Scope] Georeferences
# all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
# includes String somewhere
# Joins collecting_event.rb and matches %String% against verbatim_locality
# .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
def self.with_locality_like(string)
with_locality_as(string, true)
end
# @param [String] locality string
# @return [Scope] Georeferences
# return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
# equals String somewhere
# Joins collecting_event.rb and matches %String% against verbatim_locality
# .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
def self.with_locality(string)
with_locality_as(string, false)
end
# @param [GeographicArea]
# @return [Scope] Georeferences
# returns all georeferences which have collecting_events which have geographic_areas which match
# geographic_areas as a GeographicArea
# TODO: or, (in the future) a string matching a geographic_area.name
def self.with_geographic_area(geographic_area)
partials = CollectingEvent.where(geographic_area: geographic_area)
partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id))
partial_gr
end
# TODO: not yet sure what the params are going to look like. what is below just represents a guess
# @param [ActionController::Parameters] arguments from _collecting_event_selection form
# @return [Array] of georeferences
def self.batch_create_from_georeference_matcher(arguments)
gr = Georeference.find(arguments[:georeference_id].to_param)
result = []
collecting_event_list = arguments[:checked_ids]
unless collecting_event_list.nil?
collecting_event_list.each do |event_id|
new_gr = Georeference.new(collecting_event_id: event_id.to_i,
geographic_item_id: gr.geographic_item_id,
error_radius: gr.error_radius,
error_depth: gr.error_depth,
error_geographic_item_id: gr.error_geographic_item_id,
type: gr.type,
is_public: gr.is_public,
api_request: gr.api_request,
is_undefined_z: gr.is_undefined_z,
is_median_z: gr.is_median_z)
if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event.
new_gr.save!
result.push new_gr
end
end
end
result
end
# @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with',
# Bool = true if 'contains'
# @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which
# includes, or is equal to 'string' somewhere
# Joins collecting_event.rb and matches %String% against verbatim_locality
# .where(id in CollectingEvent.where{verbatim_locality like "%var%"})
# TODO: Arelize
def self.with_locality_as(string, like)
likeness = like ? '%' : ''
query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'"
Georeference.where(collecting_event: CollectingEvent.where(query))
end
protected
# @return [Hash] of names of geographic areas
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
# retval = error_box.contains?(geographic_item.geo_object)
# end
# retval
# end
# @return [Boolean]
# true if geographic_item is completely contained in error_box
def check_obj_inside_err_radius
# case 2
retval = true
# if !error_radius.blank? && geographic_item && geographic_item.geo_object
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 error_geographic_item.geo_object intersects (overlaps)
# collecting_event.geographic_area.default_geographic_item
def check_error_geo_item_intersects_area
# case 5.5
retval = true
if collecting_event.present?
if error_geographic_item.present?
if error_geographic_item.geo_object.present?
if collecting_event.geographic_area.present?
retval = collecting_event.geographic_area.default_geographic_item
.intersects?(error_geographic_item.geo_object)
end
end
end
end
retval
end
# @return [Boolean]
# true if geographic_item.geo_object is completely contained in collecting_event.geographic_area
# .default_geographic_item
def check_obj_inside_area
# case 6
retval = true
if collecting_event.present?
if geographic_item.present? && collecting_event.geographic_area.present?
if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present?
retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object)
end
end
end
retval
end
# # @return [Boolean] true 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 a geographic area which does not contain the supplied georeference/geographic item')
end
end
# @return [Boolean] true iff collecting_event area contains georeference error_geographic_item.
def add_error_geo_item_inside_area
unless check_error_geo_item_inside_area
problem = 'collecting_event geographic area must contain georeference error_geographic_item.'
errors.add(:error_geographic_item, problem)
errors.add(:collecting_event, problem)
end
end
# @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item.
def add_error_geo_item_intersects_area
unless check_error_geo_item_intersects_area
problem = 'collecting_event geographic area must intersect georeference error_geographic_item.'
errors.add(:error_geographic_item, problem)
errors.add(:collecting_event, problem)
end
end
# @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box.
def add_error_radius_inside_area
unless check_error_radius_inside_area
problem = 'collecting_event geographic area must contain georeference error_radius bounding box.'
errors.add(:error_radius, problem)
errors.add(:collecting_event, problem) # probably don't need error here
end
end
# @return [Boolean] true iff error_radius contains error_geographic_item.
def add_err_geo_item_inside_err_radius
unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause
problem = 'error_radius must contain error_geographic_item.'
errors.add(:error_radius, problem)
errors.add(:error_geographic_item, problem)
end
end
# @return [Boolean] true iff error_radius contains geographic_item.
def add_obj_inside_err_radius
errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius
end
# @return [Boolean] true iff error_geographic_item contains geographic_item.
def add_obj_inside_err_geo_item
errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item
end
# @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles).
def add_error_depth
errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth &&
error_depth > 8_800 # 8,800 meters
end
# @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles).
def add_error_radius
errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') if error_radius.present? &&
error_radius > 10_000 # 10 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] from_lat_
# @param [Double] from_lon_
# @param [Double] to_lat_
# @param [Double] to_lon_
# @return [Double] Heading is returned as an angle in degrees clockwise from North.
def heading(from_lat_, from_lon_, to_lat_, to_lon_)
from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_
to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_
delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_)
y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_)
x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) -
::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_)
DEGREES_PER_RADIAN * ::Math.atan2(y_, x_)
end
end